pip install -r requirements.txt
All processing is driven by a single script:
python process_frames.py [OPTIONS] [FILE ...]
| Invocation | Behaviour |
|---|---|
python process_frames.py |
Process only frames whose raw source has changed (mtime comparison) |
python process_frames.py --all |
Force-reprocess every frame regardless of modification time |
python process_frames.py f1.png f2.png ... |
Process explicit files (used by GitHub Actions) |
CHANGED_FILES="f1\nf2" python process_frames.py |
Explicit files via environment variable (GitHub Actions) |
process_frames.py ← CLI entry point
frame_processor/
__init__.py ← public API exports
pipeline.py ← file discovery & orchestration
processor.py ← core image analysis (DeviceFrameProcessor)
color_extractor.py ← dominant frame body color → hex
indexer.py ← generates device-frames-output/index.json
readme_updater.py ← updates README device list section
models.py ← ScreenBounds, FrameTemplate dataclasses
common.py ← shared logger & constants
1. Determine file list
├── Explicit files (CLI args or CHANGED_FILES env)
├── Changed only (default) — mtime check via pipeline.py
└── All frames (--all flag)
2. process_frame_list() ← pipeline.py
└── DeviceFrameProcessor.process() ← processor.py (per frame)
3. generate_index_file() ← indexer.py
└── Writes device-frames-output/index.json
4. update_readme() ← readme.py
└── Rewrites "List of Devices and Variations" section in README.md
For each raw PNG at device-frames-raw/<Type>/<Model>/<Variant>.png, three files
are written to device-frames-output/<Type>/<Model>/<Variant>/:
| File | Contents |
|---|---|
frame.png |
Original PNG copied as-is (RGBA) |
mask.png |
Greyscale mask — white = screen area, black = frame |
template.json |
Screen bounding box, frame dimensions, and hexColor of the frame body |
The algorithm is defined in frame_processor/processor.py.
- Load PNG and convert to RGBA
- Extract and normalise alpha channel to 0–255 range
- Transparent (α ≤ 10): Screen interior
- Solid (α ≥ 245): Device frame
- Anti-aliased / edge: Everything in between
- Connected-component labeling on the transparency mask
- Reject regions touching image borders (background, not screen)
- Reject tiny regions (holes, speaker grills — < 5 000 pixels)
Chooses the region with:
- Largest area
- Aspect ratio within 1.3–2.5 (covers phones and tablets)
- Fully enclosed by opaque pixels
- Calculate minX, minY, maxX, maxY of the selected region
- Generate bounding box (stored in
template.jsonasscreen) - Extract precise screen contour using edge detection
- Create blank image (same size as frame)
- Fill detected contour with white (255)
- Fill background with black (0)
- Feather inward by ~1 px to avoid edge bleed
Checks that:
- Mask coverage is 50–90% of total frame area
- Mask does not touch any image edge
- Bounding box fully encloses the mask region
Computes a representative #RRGGBB color for the device body and stores it in
template.json. The algorithm lives in
frame_processor/color_extractor.py.
A device frame's cross-section, from outside to inside, looks like:
[transparent] [chamfer] [highlight] [body] [inner bezel] [screen]
The "body" segment is the device's actual color; the inner bezel is near-black on nearly every phone, so a naive "most common opaque pixel" lands on bezel rather than the body. To find the body:
- For each row, take the first opaque pixel and walk inward up to 25 pixels. Same for the last opaque pixel walking inward, and analogously per column from the top and bottom edges.
- While walking, skip leading dark pixels (some phones have a pure-black outer shadow chamfer, e.g. iPhone XS).
- Stop on a sustained dark streak (2 consecutive near-black pixels) — that's the inner bezel — and drop the last 3 collected samples (the brightness ramp from body into bezel) so they don't dominate the histogram.
- Quantize collected RGB samples into 16-step buckets and pick the most
populated bucket. Average the pixels in that bucket for sub-bucket
precision and emit the result as
#RRGGBB.
| Constant | Value | Purpose |
|---|---|---|
ALPHA_CLEAR |
10 | Pixel is considered transparent below this alpha |
MIN_SCREEN_RATIO |
1.3 | Minimum screen aspect ratio |
MAX_SCREEN_RATIO |
2.5 | Maximum screen aspect ratio |
MIN_MASK_COVERAGE |
0.5 | Minimum mask area / frame area |
MAX_MASK_COVERAGE |
0.9 | Maximum mask area / frame area |
MIN_REGION_AREA |
5 000 | Minimum transparent region size in pixels |