@@ -484,13 +484,21 @@ commands_consolidated primitives_consolidated tour_consolidated
484484output; ` commands_fitted ` is the pre-beautification snapshot kept for
485485diagnostics.
486486
487- ### 6.7 Per-command raw-segment labels (` labels.py ` )
488-
489- When the vectorizer is built with ` labeled_segments=segment.labeled_segments ` ,
490- it produces ` labeled_commands_consolidated ` — one ` LabeledCommand ` per
491- emitted command. Drawing commands (pen-down line / arc / circle) carry
492- a list of ` CommandSpan ` runs naming the raw segments their primitive
493- came from; spins and pen-up transit commands carry an empty span list.
487+ ### 6.7 Per-command raw-segment labels (low geometry, ` labels.py ` )
488+
489+ The shared label types ` CommandSpan ` and ` LabeledCommand ` live in
490+ ` vectorize/labels_common.py ` ; both vectorizers emit them, so low- and
491+ high-geometry labels are the same type and reference the same
492+ raw-segment ids (directly comparable). Low geometry produces them
493+ * structurally* (this section); high geometry produces them
494+ * geometrically* (§6.8).
495+
496+ When the low-geometry vectorizer is built with
497+ ` labeled_segments=segment.labeled_segments ` , it produces
498+ ` labeled_commands_consolidated ` — one ` LabeledCommand ` per emitted
499+ command. Drawing commands (pen-down line / arc / circle) carry a list
500+ of ` CommandSpan ` runs naming the raw segments their primitive came
501+ from; spins and pen-up transit commands carry an empty span list.
494502
495503Each ` CommandSpan ` has:
496504
@@ -526,10 +534,41 @@ exactly one `CommandSpan` covering `raw[0:len]` and ratio
526534the same code path when a primitive consumed only part of a raw
527535segment (a corner split mid-stroke).
528536
529- Important scope note: labels are produced for ` commands_consolidated `
530- only. ` OptimizeRoute ` (stage 5) reorders commands and re-emits transit
531- spins/lines, so its ` commands ` output is ** not** labeled — the labels
532- are tied to the consolidated tour the labeler walked.
537+ Important scope note: the * structural* low-geometry labeler is tied to
538+ the consolidated tour it walked, so it only labels
539+ ` commands_consolidated ` . ` OptimizeRoute ` (stage 5) reorders commands
540+ and re-emits transit spins/lines, so that stream is ** not** structurally
541+ labeled — but the geometric labeler in §6.8 can label any stream,
542+ including the optimized one, since it reads only the emitted geometry.
543+
544+ ### 6.8 Per-command raw-segment labels (high geometry / geometric, ` labels_common.py ` )
545+
546+ The high-geometry baseline is frozen and tracks no point-range
547+ provenance through its segment/classify/order logic, so there is no
548+ structural command → primitive → polyline chain to walk. Its labels
549+ are recovered ** geometrically** by ` label_commands_geometric ` , which
550+ needs only the emitted command stream plus the raw segments:
551+
552+ 1 . Replay the command stream to recover each pen-down command's drawn
553+ path (a line's two endpoints, an arc's center / radius / sweep).
554+ 2 . Sample that path at ~ 1 point per pixel.
555+ 3 . Match each sample to the nearest raw-segment pixel via a KDTree —
556+ the same KDTree construction the segment stage uses (§4.1).
557+ 4 . Compress consecutive same-id samples into ` CommandSpan ` runs, taking
558+ the raw-index range from the matched pixels and the ratio range
559+ from the sample parameters.
560+
561+ ` HighGeometryVectorize ` takes an optional ` raw_segments ` argument
562+ (the pipeline passes ` segment.segments ` ); when supplied it populates
563+ ` labeled_commands ` parallel to ` commands ` . For the geometric labeler,
564+ ` LabeledCommand.primitive_id ` is the drawing command's ordinal in draw
565+ order (a stable handle, not an index into any primitive list) and
566+ ` final_segment_index ` is ` None ` (the baseline doesn't carry the
567+ final-segment abstraction).
568+
569+ Because it reads only geometry, the same function labels the
570+ route-optimized stream too — call it with ` optimized.commands ` and the
571+ same ` segment.segments ` when you need labels on stage 5's output.
533572
534573---
535574
@@ -638,7 +677,7 @@ become?" without re-deriving it from geometry. The chain is:
638677
639678```
640679binary pixel → skeleton pixel → raw segment id → final segment span → drawing command
641- (§3.3) (§4.1) (§4.1) (§6.7)
680+ (§3.3) (§4.1) (§4.1) (§6.7 / §6.8 )
642681```
643682
644683Each hop is implemented independently and exposes its lookup
@@ -649,7 +688,15 @@ granularity they need:
649688| -----| -------| ----------| -------|
650689| binary pixel → skeleton pixel | Skeletonize | ` labeling: (H, W) int32 ` (-1 = outside binary) | ` Skeletonize.labeling ` |
651690| skeleton/final pixel → raw segment id (+ index, + raw range) | Segment | ` labeled_segments: List[LabeledSegment] ` with per-pixel ` raw_ids ` /` raw_indices ` and run-compressed ` RawSegmentSpan ` lists | ` Segment.labeled_segments ` |
652- | drawing command → raw segment id (+ index, + ratio) | Vectorize | ` labeled_commands_consolidated: List[LabeledCommand] ` with per-command ` CommandSpan ` lists | ` LowGeometryVectorize.labeled_commands_consolidated ` |
691+ | drawing command → raw segment id (+ index, + ratio), low geometry | Vectorize | ` labeled_commands_consolidated: List[LabeledCommand] ` (structural) | ` LowGeometryVectorize.labeled_commands_consolidated ` |
692+ | drawing command → raw segment id (+ index, + ratio), high geometry | Vectorize | ` labeled_commands: List[LabeledCommand] ` (geometric) | ` HighGeometryVectorize.labeled_commands ` |
693+
694+ Both vectorizers emit the same ` CommandSpan ` / ` LabeledCommand ` types
695+ (defined in ` vectorize/labels_common.py ` ) against the same raw-segment
696+ ids, so a low- and a high-geometry command are directly comparable.
697+ They differ only in * how* the link is found: low geometry tracks
698+ provenance structurally (§6.7); high geometry, which is frozen and
699+ keeps no provenance, recovers it geometrically (§6.8).
653700
654701Walking forward: a binary pixel ` (y, x) ` lands on
655702` Skeletonize.labeling[y, x] ` , a row-major flat index into the True
@@ -685,10 +732,14 @@ Scope limits worth knowing:
685732 the cascade — fusion bridges hug ink between the joined endpoints,
686733 and repair's LS-solved junction point sits within the original
687734 cluster.
688- - Command labels are produced for ` commands_consolidated ` only;
689- ` OptimizeRoute ` reorders commands and re-emits transit
690- spins/lines, so its ` commands ` output is not labeled. Re-running
691- the labeler against the optimized tour would close that gap.
735+ - The low-geometry command mapping is * structural* and is produced
736+ for ` commands_consolidated ` only; ` OptimizeRoute ` reorders commands
737+ and re-emits transit spins/lines, so that stream isn't structurally
738+ labeled. The high-geometry command mapping is * geometric*
739+ (sample-and-match against the raw segments) and so works on any
740+ command stream — including the route-optimized one. Use
741+ ` label_commands_geometric(optimized.commands, segment.segments, …) `
742+ when you need labels on stage 5's output.
692743
693744---
694745
@@ -757,6 +808,7 @@ release/
757808 labels.py final-polyline pixel → raw segment id (KDTree)
758809 graph/ stage 3 — StrokeGraph (endpoints→vertices)
759810 vectorize/
811+ labels_common.py shared CommandSpan/LabeledCommand + geometric labeler
760812 low_geometry/ stage 4, the real path
761813 fitting.py polyline → primitive chain (corners/inflections/MDL)
762814 primitives.py Line / Arc / Circle
@@ -765,8 +817,9 @@ release/
765817 beautify.py detect near-relations; merge arcs into circles
766818 manifest.py constraint bundle types
767819 routing.py Eulerian / Chinese-Postman ordering
768- labels.py drawing command → raw segment spans (+ ratios )
820+ labels.py drawing command → raw segment spans (structural )
769821 high_geometry/ stage 4, the naive comparison baseline
822+ (command labels recovered geometrically)
770823
771824test.py runs every example end to end, prints metrics
772825test.sh unit tests, then the example run
@@ -802,7 +855,12 @@ print(opt_low.estimated_time_after) # estimated draw time (s)
802855# Cross-stage labels (see §9):
803856print (skeleton.labeling.shape) # (H, W) int32 per-pixel
804857print (segment.labeled_segments[0 ].spans) # raw-segment runs
805- print (low.labeled_commands_consolidated[0 ].spans) # raw-segment per command
858+ print (low.labeled_commands_consolidated[0 ].spans) # low-geom command labels
859+ print (high.labeled_commands[0 ].spans) # high-geom command labels
860+
861+ # Geometric labeler works on any command stream, incl. the optimized one:
862+ from release.vectorize.labels_common import label_commands_geometric
863+ opt_labels = label_commands_geometric(opt_low.commands, segment.segments)
806864```
807865
808866A single example processes in ~ 10–15 s; the full suite takes several
@@ -819,13 +877,17 @@ For each example the harness writes a set of debug PNGs into
819877| ` <name>.segments.labeling.png ` | 2 | every final-polyline span coloured by its raw-segment id (§4.1) |
820878| ` <name>.graph.png ` | 3 | stroke graph with vertices and edges |
821879| ` <name>.vectorized.svg ` | 4 | high / low / optimized 3-panel comparison |
822- | ` <name>.commands.labeling.png ` | 4 | every drawing command coloured by its raw-segment-span ids (§6.7) |
880+ | ` <name>.commands.labeling.png ` | 4 | every low-geometry drawing command coloured by its raw-segment-span ids (§6.7) |
881+ | ` <name>.commands.labeling.high.png ` | 4 | same for the high-geometry baseline, recovered geometrically (§6.8) |
823882| ` <name>.heatmap.png ` , ` <name>.overlay.png ` , ` <name>.overlay.clean.png ` | 5–6 | firmware-time heatmap and source-image overlays |
824883
825- Adjacent labels in the three ` *.labeling.png ` renders always land at
884+ Adjacent labels in the four ` *.labeling* .png ` renders always land at
826885well-separated hues (golden-ratio palette + stride renumbering), so
827886an over-merge shows as a single colour where two would be expected
828- and an over-split shows as a hue jump inside one continuous run.
887+ and an over-split shows as a hue jump inside one continuous run. The
888+ two ` commands.labeling ` renders share the raw-segment colour basis,
889+ so you can compare how the low and high vectorizers carve the same
890+ raw strokes into commands.
829891
830892---
831893
@@ -844,7 +906,10 @@ and an over-split shows as a hue jump inside one continuous run.
844906 every caller — a stale 5-tuple unpack is exactly the bug that used
845907 to crash the test harness.
846908- ** High geometry is frozen.** It is a baseline, not a place to add
847- features. Improvements go in low geometry.
909+ features. Improvements go in low geometry. (The raw-segment command
910+ labels it now carries are the exception that proves the rule: they're
911+ recovered * geometrically* from its emitted commands without touching
912+ the frozen segment/classify/order logic — see §6.8.)
848913- ** Two command snapshots.** ` commands_consolidated ` is the real
849914 output; ` commands_fitted ` is a diagnostic snapshot. Don't confuse
850915 them. Neither is route-optimized — that's ` OptimizeRoute ` .
@@ -867,11 +932,13 @@ and an over-split shows as a hue jump inside one continuous run.
867932 "all but last" slice sentinel. To slice the raw segment regardless
868933 of direction, use
869934 ` raw_segments[id][min(raw_start, raw_end) : max(raw_start, raw_end) + 1] ` .
870- - ** ` OptimizeRoute ` invalidates command labels.** Stage 5 reorders
871- primitives and re-emits transit commands, so its ` commands ` output
872- is unlabeled. Use ` low.labeled_commands_consolidated ` (against
873- ` low.commands_consolidated ` ) if you need labels; re-label the
874- optimized tour if you need labels on the post-optimization output.
935+ - ** ` OptimizeRoute ` invalidates the structural command labels.**
936+ Stage 5 reorders primitives and re-emits transit commands, so the
937+ low-geometry structural labels (` low.labeled_commands_consolidated ` ,
938+ tied to ` low.commands_consolidated ` ) don't carry over to its output.
939+ The geometric labeler does carry over: call
940+ ` label_commands_geometric(opt_low.commands, segment.segments) ` for
941+ labels on the post-optimization stream (§6.8).
875942
876943---
877944
0 commit comments