repixelizer/docs/tile-graph-algorithm-map.md

427 lines
11 KiB
Markdown
Raw Normal View History

2026-04-23 02:25:50 +01:00
# Tile-Graph Algorithm Map
## Why this map exists
2026-04-23 02:25:50 +01:00
The tile-graph path is trying to do one stubbornly honest thing:
2026-04-23 02:25:50 +01:00
Take a fake-pixel mural, split it into real opaque paint islands, slice those islands by the chosen output lattice, learn which slices continue into neighboring cells, and then reassemble the image using only those real slices.
2026-04-23 02:25:50 +01:00
When the output looks wrong, this map is here to answer:
2026-04-23 02:25:50 +01:00
Where did the machine start lying?
2026-04-23 02:25:50 +01:00
## One-sentence machine
2026-04-23 03:11:56 +01:00
The current tile-graph machine is:
2026-04-23 03:11:56 +01:00
`source image -> edge scout -> connected components -> component-cell overlap reduction -> per-cell candidate buckets -> learned same-component adjacency -> local discrete assignment`
2026-04-23 03:11:56 +01:00
Or, in pictures:
2026-04-23 02:25:50 +01:00
- the source image is the mural
- connected components are paint islands
- the chosen lattice is a sheet of graph paper laid over the mural
- the overlap reducer is a cookie cutter that stamps each island against each graph-paper square it actually touches
- the solver is the mason choosing which stamped shard goes in each final slot
2026-04-23 02:25:50 +01:00
## What was cut away
2026-04-23 02:25:50 +01:00
These ideas are gone:
2026-04-23 03:11:56 +01:00
- subsampling the mural before extraction with `source_region_stride`
- per-component seed walking and window stepping
- stroke-specific PCA slicing
- ranking candidates by resemblance to a lattice portrait (`sharp_rgba`, `edge_rgba`)
- injecting `sharp_pixel` / `edge_pixel` when extraction failed
- sampled one-cell-away RGBA deltas pretending to be a graph
- a dedicated tile-graph source-reference object
2026-04-23 02:25:50 +01:00
That matters because all of those were ways for the machine to stop listening to the actual source-owned tiles.
2026-04-23 03:11:56 +01:00
## Stage 0: The pipeline picks a ruler
2026-04-23 03:11:56 +01:00
Function:
2026-04-23 02:25:50 +01:00
- `run_pipeline(...)` in `src/repixelizer/pipeline.py`
2026-04-23 02:25:50 +01:00
Inputs:
2026-04-23 02:25:50 +01:00
- `source_rgba`
- chosen `InferenceResult`
- `target_width`
- `target_height`
- `phase_x`
- `phase_y`
2026-04-23 03:11:56 +01:00
Meaning:
2026-04-23 02:25:50 +01:00
Before anything else, the system chooses the graph paper.
2026-04-23 02:25:50 +01:00
If the graph paper is wrong, every later step is cutting the mural against the wrong ruler.
2026-04-23 03:11:56 +01:00
Once the lattice is pinned, the tile-graph path now runs directly on that lattice. It no longer burns time on phase-rerank probes or hybrid sidecars.
2026-04-23 03:11:56 +01:00
## Stage 1: Edge scout
2026-04-23 02:25:50 +01:00
Function:
2026-04-23 04:18:24 +01:00
- `analyze_tile_graph_source(...)` in `src/repixelizer/analysis.py`
2026-04-23 04:18:24 +01:00
Output:
2026-04-23 02:25:50 +01:00
- `TileGraphSourceAnalysis.edge_map`
2026-04-23 03:11:56 +01:00
Meaning:
This is a scout walking over the mural with a lantern and chalk, marking where the paint changes sharply.
2026-04-23 03:11:56 +01:00
It is not deciding colors. It is only measuring local cliff faces.
2026-04-23 02:25:50 +01:00
Important variable:
2026-04-23 02:25:50 +01:00
- `edge_map[y, x]`
2026-04-23 02:25:50 +01:00
Why it exists:
2026-04-23 02:25:50 +01:00
- connected components need a detail hint
- candidate budgeting still uses edge-heavy cells as "hard mode"
2026-04-23 02:25:50 +01:00
## Stage 2: Every source pixel is assigned to an output cell
2026-04-23 02:25:50 +01:00
Function:
2026-04-23 02:25:50 +01:00
- `build_tile_graph_model(...)`
2026-04-23 02:25:50 +01:00
Key variables:
2026-04-23 03:11:56 +01:00
- `cell_w`
- `cell_h`
- `projected_flat_index`
- `cell_mean_rgba_flat`
- `cell_alpha_mean_flat`
- `cell_alpha_support_flat`
- `cell_edge_strength_flat`
2026-04-23 03:11:56 +01:00
Meaning:
2026-04-23 02:25:50 +01:00
Imagine dropping transparent graph paper over the mural and writing the output cell index on every source pixel.
2026-04-23 02:25:50 +01:00
That gives us direct per-cell source summaries:
2026-04-23 02:25:50 +01:00
- average color already under the slot
- average alpha already under the slot
- strongest alpha under the slot
- strongest edge under the slot
2026-04-23 02:25:50 +01:00
These are not a separate portrait of what the slot "should" be. They are literally summaries of the pixels already living under that slot.
2026-04-23 02:25:50 +01:00
## Stage 3: Connected components
2026-04-23 03:11:56 +01:00
Functions:
2026-04-23 03:11:56 +01:00
- `_segment_atomic_source_regions(...)`
- `_segment_atomic_source_regions_cpu(...)`
2026-04-23 02:25:50 +01:00
Output:
2026-04-23 02:25:50 +01:00
- `AtomicRegionLabeling`
- `pixel_linear`
- `component_ids`
- `component_sizes`
- `component_count`
2026-04-23 02:25:50 +01:00
Meaning:
2026-04-23 02:25:50 +01:00
Now the mural is split into paint islands.
2026-04-23 02:25:50 +01:00
Pixels join the same island only if:
2026-04-23 02:25:50 +01:00
- they are opaque enough
- their premultiplied colors are similar enough
- their alpha is similar enough
2026-04-23 02:25:50 +01:00
This is literal connected-component labeling over the full-resolution source.
2026-04-23 02:25:50 +01:00
Important consequence:
- every opaque source pixel belongs to exactly one component
2026-04-23 02:25:50 +01:00
## Stage 4: Reduce component-cell overlaps
2026-04-23 02:25:50 +01:00
Function:
2026-04-23 02:25:50 +01:00
- `_extract_source_region_tiles(...)`
2026-04-23 03:11:56 +01:00
This is the new heart of the machine.
2026-04-23 02:25:50 +01:00
Instead of walking each component with seed queues and cell-sized windows, the reducer does something much simpler:
2026-04-23 02:25:50 +01:00
1. take every opaque labeled source pixel
2. project it onto the chosen output lattice
3. form a compound key: `(component_id, output_cell)`
4. sort by that key
5. reduce each run into one candidate shard
2026-04-23 02:25:50 +01:00
This is the same basic mental pattern as `reduce_by_key` / `segment_reduce`: sort once, then aggregate consecutive runs.
2026-04-23 02:25:50 +01:00
What each reduced shard stores:
2026-04-23 02:25:50 +01:00
- `rep_linear`
- `rep_rgba`
- `area_ratio`
- `coverage`
- `edge_peak`
- `source_center_x`
- `source_center_y`
- `coord_x`, `coord_y`, `flat_index`
- `component_id`
2026-04-23 02:25:50 +01:00
Natural-language picture:
2026-04-23 03:11:56 +01:00
Instead of hand-carving the island with a pocketknife, we stamp every island against the graph paper and keep one shard per `(island, cell)` overlap.
2026-04-23 03:11:56 +01:00
## Stage 5: Candidate buckets
2026-04-23 02:25:50 +01:00
Still inside:
2026-04-23 02:25:50 +01:00
- `_extract_source_region_tiles(...)`
- `_select_source_region_candidates(...)`
Meaning:
2026-04-23 02:25:50 +01:00
Each output cell now gets a bucket of legal shards.
2026-04-23 02:25:50 +01:00
The reducer first creates every real `(component, cell)` overlap shard.
2026-04-23 02:25:50 +01:00
Then bucket pruning keeps the best local candidates using only tile-owned evidence:
2026-04-23 02:25:50 +01:00
- area fit
- coverage
- edge peak
2026-04-23 02:25:50 +01:00
Important rule:
2026-04-23 02:25:50 +01:00
- if a cell has opaque source support, it must have at least one real extracted shard
- if pruning would empty a supported cell, the best overlap shard for that cell is kept anyway
- background or near-background cells can also get a transparent candidate
2026-04-23 02:25:50 +01:00
What this prevents:
2026-04-23 02:25:50 +01:00
- silent empty foreground buckets
- fake sharp/edge fallback pixels
2026-04-23 02:25:50 +01:00
## Stage 6: Learn adjacency from the reduced shards
2026-04-23 02:25:50 +01:00
Still inside:
2026-04-23 02:25:50 +01:00
- `_extract_source_region_tiles(...)`
2026-04-23 02:25:50 +01:00
Meaning:
2026-04-23 02:25:50 +01:00
Each shard now asks a simpler question than before:
2026-04-23 02:25:50 +01:00
"Does my same component continue into the cell to the right, down, left, or up?"
2026-04-23 03:11:56 +01:00
If a same-component overlap shard exists in the neighboring cell, that shard's representative color becomes the expected neighbor color for that direction.
2026-04-23 03:11:56 +01:00
Stored per shard:
2026-04-23 02:25:50 +01:00
- `neighbor_rgba[4, 4]`
- `neighbor_mask[4]`
2026-04-23 02:25:50 +01:00
This is an intentionally narrower graph than the earlier heuristic:
2026-04-23 02:25:50 +01:00
- it only learns continuity that the same connected paint island actually exhibits
- it does not try to smooth across boundaries between different components
2026-04-23 02:25:50 +01:00
That is a carefully chosen bias:
2026-04-23 02:25:50 +01:00
- continuity within a real island is trustworthy
- continuity across a contour is where fake smoothing usually starts
2026-04-23 02:25:50 +01:00
## Stage 7: Solver model
2026-04-23 02:25:50 +01:00
Dataclass:
2026-04-23 02:25:50 +01:00
- `TileGraphModel`
2026-04-23 02:25:50 +01:00
Current essential fields:
2026-04-23 02:25:50 +01:00
- `candidate_rgba`
- `candidate_area_ratio`
- `candidate_coverage`
- `candidate_edge_peak`
- `candidate_neighbor_rgba`
- `candidate_neighbor_mask`
- `cell_candidate_offsets`
- `cell_candidate_indices`
- `cell_mean_rgba`
- `cell_alpha_mean`
- `cell_edge_strength`
2026-04-23 02:25:50 +01:00
Meaning:
2026-04-23 03:11:56 +01:00
This is the tray of legal shards plus the local evidence under each output slot.
2026-04-23 03:11:56 +01:00
## Stage 8: Unary cost
2026-04-23 03:11:56 +01:00
Function:
2026-04-23 03:11:56 +01:00
2026-04-23 02:25:50 +01:00
- `_tile_graph_unary_cost_torch(...)`
Terms:
2026-04-23 02:25:50 +01:00
- `color_error`
- candidate versus actual mean color already under the slot
- `area_error`
- how close the shard is to one full cell
- `alpha_error`
- candidate alpha versus actual mean alpha under the slot
- `coverage_error`
- how incomplete the overlap is
- `edge_error`
- candidate edge peak versus strongest edge under the slot
2026-04-23 02:25:50 +01:00
Meaning:
2026-04-23 03:11:56 +01:00
The unary cost is the bouncer at each slot asking:
2026-04-23 03:11:56 +01:00
"Does this shard honestly fit the patch of mural under this square?"
2026-04-23 02:25:50 +01:00
Important difference from the old machine:
2026-04-23 02:25:50 +01:00
- this is local source support
- not a pre-baked portrait from another reference object
2026-04-23 02:25:50 +01:00
## Stage 9: Pairwise cost
2026-04-23 02:25:50 +01:00
Functions:
2026-04-23 02:25:50 +01:00
- `_pair_penalty_selected_torch(...)`
- `_pair_penalty_option_right_torch(...)`
- `_pair_penalty_option_left_torch(...)`
- `_pair_penalty_option_down_torch(...)`
- `_pair_penalty_option_up_torch(...)`
2026-04-23 02:25:50 +01:00
Meaning:
2026-04-23 02:25:50 +01:00
Now the mason cares about seams.
2026-04-23 02:25:50 +01:00
If a shard says "my component really continues to the right as color X," then placing a very different right neighbor costs something.
2026-04-23 02:25:50 +01:00
This is narrower than the old fake graph, but more honest:
2026-04-23 03:11:56 +01:00
- it rewards real within-component continuation
- it stops encouraging smoothing across unrelated components
2026-04-23 03:11:56 +01:00
## Stage 10: Parity refinement
2026-04-23 02:25:50 +01:00
Function:
2026-04-23 02:25:50 +01:00
- `optimize_tile_graph(...)`
2026-04-23 02:25:50 +01:00
What it does:
2026-04-23 02:25:50 +01:00
1. build candidate buckets
2. compute unary cost
3. choose an initial argmin candidate per cell
4. alternate parity updates across the grid
5. if refinement worsens source-fidelity, keep the initial assignment
2026-04-23 02:25:50 +01:00
Meaning:
2026-04-23 02:25:50 +01:00
The mason lays an initial mosaic, then alternates black and white squares of the chessboard trying to improve local seams without moving everything at once.
2026-04-23 02:25:50 +01:00
Important truth:
2026-04-23 02:25:50 +01:00
- this is still a local discrete optimizer
- it is not globally optimal
- if the buckets are wrong, refinement will not save the image
2026-04-23 02:25:50 +01:00
## Data dictionary
2026-04-23 02:25:50 +01:00
### `AtomicRegionLabeling.pixel_linear`
2026-04-23 02:25:50 +01:00
- flattened source-pixel indices for every opaque labeled pixel
2026-04-23 02:25:50 +01:00
### `AtomicRegionLabeling.component_ids`
2026-04-23 02:25:50 +01:00
- component id for each `pixel_linear`
2026-04-23 02:25:50 +01:00
### `projected_flat_index`
2026-04-23 02:25:50 +01:00
- output-cell index for each source pixel under the chosen lattice
### `compound_key`
- `(component_id * output_area) + output_cell`
This is the reducer's key. Every identical key means "these pixels belong to the same component-cell overlap shard."
2026-04-23 02:25:50 +01:00
### `region_buckets[flat_index]`
2026-04-23 02:25:50 +01:00
- all legal source-owned shards currently available for one output cell
2026-04-23 02:25:50 +01:00
### `candidate_neighbor_rgba`
2026-04-23 02:25:50 +01:00
- expected neighboring shard colors learned from same-component continuation
2026-04-23 02:25:50 +01:00
## What this machine is optimizing for
2026-04-23 02:25:50 +01:00
In plain language:
2026-04-23 02:25:50 +01:00
- choose only real source-owned shards
- prefer shards that genuinely fit the local patch of mural
- preserve continuation within real paint islands
- refuse to invent a foreground shard when extraction did not produce one
2026-04-23 02:25:50 +01:00
## What can still go wrong
2026-04-23 02:25:50 +01:00
The likely remaining lies are now:
2026-04-23 02:25:50 +01:00
- the lattice is wrong
- component labeling merged or split the wrong source regions
- the direct overlap shard for a difficult contour cell is too coarse
- candidate truncation threw away the only good shard in a crowded cell
- same-component adjacency is too narrow to express a more complex local motif
- the parity solver settles into a mediocre local arrangement
2026-04-23 02:25:50 +01:00
## Current reality check
2026-04-23 03:11:56 +01:00
Pinned cleaned badge run after the reduce-by-key extraction rewrite:
2026-04-23 03:11:56 +01:00
- target: `126x126`
- phase: `(0.0, -0.2)`
- mode: `tile-graph`
- phase rerank: off
2026-04-23 03:11:56 +01:00
Measured result:
2026-04-23 03:11:56 +01:00
- initial source-fidelity: `0.2036`
- final source-fidelity: `0.2036`
- the solver kept the initial assignment
2026-04-23 03:11:56 +01:00
That is a small quality regression from the previous `0.1814` cut, but it is still far better than the earlier fixed-lattice collapse around `0.5055`.
2026-04-23 02:25:50 +01:00
## Big-picture judgment
2026-04-23 02:25:50 +01:00
Does the current machine make more sense than the old one?
2026-04-23 02:25:50 +01:00
Yes.
2026-04-23 02:25:50 +01:00
The dataflow is now much cleaner:
2026-04-23 02:25:50 +01:00
1. observe the mural
2. label the paint islands
3. stamp those islands against the chosen lattice
4. keep the resulting overlap shards
5. learn only the continuation those shards actually exhibit
6. assemble the output from those shards
2026-04-23 02:25:50 +01:00
That is the right shape.
2026-04-23 02:25:50 +01:00
The remaining problem is no longer "why is the machine inventing stories?"
It is:
"Are these overlap shards expressive enough for the finest contours, and can we compute the labels fast enough?"