Single SourceStudios Engage SSS
v0.1.0 · editable install only · 1 commit, 2026-08-01

The video-to-CRT-rotoscope renderer that never assigns a colour.

pixelize is a Python CLI, tkinter GUI, and packaged arm64 macOS app that converts a video file or URL into a monochrome-CRT rotoscope in the style of Wahyu Ichwandardi's "This is America on Macintosh SE." It quantises every frame to 1-bit line art, then reconstructs colour by looking up a measured phosphor transfer function, so hair renders warm brown and thin strokes render cool navy without any code path ever deciding what colour a person is.

Editable install only, no PyPI publish
1 commit, hash dd6f4c8
0 traditional unit tests
RMSE 4.32 vs 6.0 threshold
arm64 Pixelize.app, ad-hoc signed
4.32RMSE vs 6.0 pass threshold, CRT fidelity
0.335→0.997thin-structure recall, chair/guitar fixtures
2,910current-path production Python LOC
5QA fixture clips, 2 with hand-verified ground truth
What it is

One command in, a rotoscoped video out. No styling flags.

The pipeline demuxes the source with ffmpeg, runs salient-foreground segmentation (rembg, isnet-general-use) with temporal hysteresis, quantises the subject to 1-bit line art on a roughly 160px-tall grid using per-frame Otsu thresholds, positions face-landmark strokes from YuNet and a mediapipe mesh, then reconstructs colour by looking up a measured phosphor transfer function keyed on signed distance to the ink boundary. Colour is never chosen; it is read off a lookup table built from measurement.

The CLI surface is one console entry point (pixelize) and one GUI entry point (pixelize-gui), each taking two positional arguments (input, output) and two optional flags (--limit, --work-dir). input accepts a local file path or a YouTube URL resolved through yt-dlp. There is no third path: no preset library, no colour picker, no style parameter exposed to the end user.

warm brown, hair cool navy, thin strokes both are LUT lookups, not decisions
Hard numbers

Every figure below traces to a command or a file path.

Compiled from a single squashed commit plus the uncommitted work sitting on top of it. Where two numbers exist for the same claim (the README's pre-fix RMSE versus the current one), both are shown.

MetricValue
Commits1
Commit date / hash2026-08-01 17:20:35+02:00, dd6f4c8
Uncommitted changes on top of HEAD9 modified files, 10 untracked paths
Tracked files70
Production Python, pixelize/ package1,785 lines across 12 files
Production Python, scripts/ tooling1,125 lines across 9 files
Total current-path Python LOC2,910
Legacy Python (V17-era, retained as history)122 lines, 2 files
Legacy shell scripts (V16/V17 line)21 files, 2,597 lines
Tracked Markdown docs18 files, 3,448 lines
Package identitypixelize 0.1.0, editable-install only, no PyPI publish, no git tags
CLI surface1 console entry point (pixelize), 1 GUI entry point (pixelize-gui); 2 positional args plus --limit, --work-dir
Utility scripts9: bench_matte.py, render_fixture.py, validate_crt.py, make_contact_sheet.py, build_phosphor_lut.py, build_fixture_gt.py, validate_matte.py, build_app.py, validate_style.py
Traditional unit tests0 (no test_*.py files)
Quality-gate fixtures5 clips (chair, choir, closeup, duo, guitar), each with a manifest; 2 of 5 (chair, guitar) carry hand-verified ground truth
Declared runtime dependencies4: numpy, scipy, pillow, rembg[cpu]
Imported but undeclared dependenciesopencv-contrib-python, mediapipe, yt-dlp, onnxruntime
CRT-fidelity validation, currentRMSE 4.32 against a 6.0 threshold
CRT-fidelity validation, README pre-fixRMSE 4.43 against a 6.0 threshold, 3 held-out frames
Matte thin-structure recall improvement0.335 → 0.997 (chair-leg / guitar-neck fixtures)
Face feature-drawing rate improvement0.5–0.92 → 1.0 across all 5 fixtures
Render costabout 1.1s per art frame; about 1 minute per 10s of 1080p footage
Architecture

Twelve modules, one frame loop, zero colour decisions.

cli.py and gui.py are thin front ends; both call into pipeline.py, which owns the frame loop and orchestrates everything else in sequence.

cli.py
Argparse wrapper: 2 positional args, 2 optional flags.
76 lines
gui.py
Tkinter desktop app with live preview.
477 lines
pipeline.py
Owns the frame loop; orchestrates fetch, demux, matte, line art, CRT recolour, and persistence in order.
152 lines
fetch.py
Resolves a URL via yt-dlp to a local file when the input is not already one.
runtime dep gap
video.py
Demuxes with ffmpeg; frame count is computed as duration × fps, not the container's nb_frames, which was measured wrong by 27 frames on the reference clip.
progress: real
matte.py
Runs rembg (isnet-general-use) per frame with a temporal-hysteresis pass over connected components and motion-gated carry-forward for detector dropouts.
recall 0.335→0.997
face.py
YuNet detection with mediapipe mesh refinement; emits scale-dependent feature strokes and a skin-guard mask.
rate 0.5–0.92→1.0
lineart.py
Renders the 1-bit art on a ~160px grid via two per-frame Otsu splits: contour, solid-fill, interior detail.
no user flags
crt.py + phosphor.py
Turns 1-bit art into colour via a measured, non-interpolated lookup table keyed on signed distance to the ink boundary.
the LUT
style_params.py
Isolates every QA-tunable threshold from the measured, immutable constants in constants.py.
12 modules total

Persistence is applied per written output frame, not per art frame, so trails fade under a held frame instead of reading as a crisp double image. That fix, and the solid-fill separation floor below, both shipped 2026-08-01.

Engineering decisions

Eight calls, each with a measurement behind it.

Decision 1

1-bit-then-CRT, not direct colour rendering

Reference hue tracks stroke thickness (R−B runs from −22 at 1px to +29 at 6px), which is the signature of a monochrome CRT's phosphor response, not an artistic colour choice. Every earlier version, V1 through V17E, tried to assign hair and skin colour directly and failed to generalise.

Decision 2

A measured LUT replaced a two-colour lerp

The original design assumed bloom could be a lerp between a lit and unlit colour. Reference boundary pixels are dark but strongly blue, which no two-colour interpolation reproduces, so colour is looked up by signed distance instead.

Decision 3

Automatic mid-tone dithering, dropped

Applying MacPaint-style dither to the mid-luminance cluster buried any backlit subject's torso in pattern. Removed in favour of a clean body, with shading left as an unsolved, unattempted accent.

Decision 4

Solid-fill gated on measured class separation

A luminance-crushed close-up, 90% of the subject interior below 0.125 luminance, let the two-stage Otsu split "find" a darkest class that was shadowed skin, not hair. MIN_FILL_SEPARATION = 0.10 sits in the gap between that failure (0.047 separation) and every legitimate fill (0.145 to 0.366). Added 2026-08-01.

Decision 5

Persistence moved from per-art-frame to per-written-frame

Blending the previous frame once per art frame held a ghost unchanged across both frames of a 2x hold, reading as a crisp double image instead of a fade. Confirmed by rendering the same timestamp with and without the blend. Added 2026-08-01.

Decision 6

BiRefNet abandoned for matte segmentation

A killed CoreML compile of the swin-based model wedges the Apple Neural Engine service machine-wide until reboot; CPU-only fallback measured 10.5s per frame, 513 minutes for a full render. isnet-general-use plus temporal hysteresis was used instead.

Decision 7

Face landmarks are structural only

face.py was added despite an earlier blanket "no face parsing" rule, on the argument that landmark positions, not colour assignment, are needed to keep skin paper-white and place deliberate feature strokes. The rule against colour assignment still stands.

Gap, not a decision

Dependency declaration is incomplete

pyproject.toml declares only numpy, scipy, pillow, and rembg[cpu], but the code imports, and the local virtualenv has installed, opencv-contrib-python, mediapipe, and yt-dlp. None of those three are transitive dependencies of the declared four. A fresh pip install -e . per the README's own instructions would not install what face.py and fetch.py require.

Timeline

One squashed commit; the real history lives in the changelog.

Git carries a single commit (dd6f4c8, 2026-08-01), so phase history here comes from CHANGELOG.md and on-disk file dates, not commit history.

Nov 2025

The abandoned V1 to V17E line

Geometric zone heuristics and semantic hair/beard segmentation, tracked across five separate status and bugfix documents plus 21 shell scripts. A 2025-11-17 cleanup pass removed 2.4GB of superseded frame dumps.

2026-07-30

V16/V17 retired; pixelize built from scratch

"The semantic hair-fill problem they solved does not exist in the target aesthetic." Demux, matte, 1-bit line art, and measured CRT phosphor simulation built the same day; the tkinter GUI shipped alongside it.

2026-07-31

Packaged as an app; URL input added

GUI button rendering fixed (tk.Button to ttk.Button); packaged as Pixelize.app, thinned to arm64 and ad-hoc signed. YouTube URL input via yt-dlp added to both CLI and GUI.

2026-08-01

Real progress counters; the git repository begins here

Progress reporting switched to real ffmpeg and yt-dlp counters; nb_frames dropped as the frame-count source. The initial commit was made. A full-video QA pass against 60 reference frames found and fixed three defects, closed out the same day.

2026-08-02

Fidelity upgrade, not yet committed

Matte temporal hysteresis, motion-gated carry-forward, face.py's structural landmarks and skin guard, style_params.py split out, fixture ground truth added, and a QA skill introduced. QA run 1 converged in one iteration across all 5 fixtures with zero parameter changes needed. This work sits on disk, uncommitted.

Boundary

What this is not.

Honesty over polish. The gaps below are as real as the numbers above.

Not identity detection

face.py is structural only

This is not a face-recognition, identity, or skin-tone classification tool. face.py performs landmark detection only and is explicitly forbidden from making colour decisions.

Known failure mode

Matte starvation on underexposed footage

A severely underexposed, crushed-dark shot fails the QA rubric by eye. This failure mode is known and currently unresolved.

Not distributable

Pixelize.app is checkout-specific

The packaged app is not relocatable and points at this specific checkout's virtualenv. The package has never been published to PyPI, and no git tag or release exists.

No unit-test suite

Correctness is argued, not covered

There are zero traditional unit tests. Correctness rests entirely on three numeric validator scripts run against five hand-picked fixture clips, which is targeted evidence, not general-purpose test coverage.

Evidence: compiled from the pixel-yt repository on 2026-08-03. Every number on this page traces to a file path or command output in the source tree. Last updated: 2026-08-03.

Single Source

Every number on the dossier and whitepaper pages traces to a file path or command output in the source tree.

LinkedIn Facebook (c) 2026 Single Source Studios (Pty) Ltd