diff --git a/artifacts/evidence/accuracy/model-training/20260809-reviewedexp6-ai-assisted-review-ledger.json b/artifacts/evidence/accuracy/model-training/20260809-reviewedexp6-ai-assisted-review-ledger.json index dec490f8..27358008 100644 --- a/artifacts/evidence/accuracy/model-training/20260809-reviewedexp6-ai-assisted-review-ledger.json +++ b/artifacts/evidence/accuracy/model-training/20260809-reviewedexp6-ai-assisted-review-ledger.json @@ -1,7 +1,7 @@ { "schema_version": 1, "ledger_id": "reviewedexp6-ai-assisted-review-r2-full-corpus", - "reviewed_at": "2026-08-09T15:00:00+02:00", + "reviewed_at": "2026-08-09T19:10:57+02:00", "reviewer": { "id": "openai-codex", "type": "ai_assistant", @@ -78,12 +78,23 @@ "audit_sha256": "2a219f650376e77a927c1ca37f801ac9fe8c2b8406cab6cd149be07e0557af73", "focused_contact_sheet_path": "/app/storage/operator-data/model-review/reviewedexp6-corpus/nested-pairs-review-r1/contact_sheet_001.png", "focused_contact_sheet_sha256": "7c27fc66bb1978296efc53d3e9f78ae6aa27816d5893c77f44d8803e57a40ee5", + "relationship_highlight_summary_path": "/app/storage/operator-data/model-review/reviewedexp6-corpus/nested-pairs-review-r2/operator_yolo_label_qa_summary.json", + "relationship_highlight_summary_sha256": "e4e6edc5c7223334c4feca9e242ac6d3c345b633a476aa9356b44133dcffe619", + "relationship_highlight_contact_sheet_path": "/app/storage/operator-data/model-review/reviewedexp6-corpus/nested-pairs-review-r2/contact_sheet_001.png", + "relationship_highlight_contact_sheet_sha256": "2a6bdd542b8ac49bcd5ac4d9c84999982461463ddd3c2fe0dc7332ceeb7a5e60", "exact_duplicate_pairs": 0, "near_duplicate_pairs_iou_at_least_0_90": 0, "possible_nested_pairs_containment_at_least_0_98": 43, "flagged_tile_count": 36, + "highlighted_unique_label_rows": 86, + "highlight_legend": { + "exact_duplicate": "cyan", + "near_duplicate": "red", + "possible_nested": "magenta", + "unflagged": "yellow" + }, "share_of_rendered_labels": 0.000543, - "ai_visual_followup": "no systematic duplicate-label pattern observed; relationships mostly correspond to adjacent or complex building components", + "ai_visual_followup": "all 86 implicated label rows were index-highlighted; no cyan exact duplicates or red near duplicates exist, while the 43 magenta nesting pairs are dispersed across dense reference AOIs and predominantly show larger GRB envelopes around smaller building components rather than one systematic duplicate-label pattern", "action": "retain for experimental analysis, require human adjudication before release, and perform no automatic rewrite or exclusion" } } diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index a91fe008..d70b6aa3 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -12565,3 +12565,33 @@ Open: on a dedicated 384 px contact sheet. Visual follow-up found complex/adjacent building components rather than a systematic duplicate-label pattern. - Targeted auditor and renderer tests passed: 9 tests. + +## 2026-08-09 - Index-bound relationship highlighting + +### Improved + +- Connected the relationship-audit manifest to the visual label renderer so + exact duplicates, near duplicates and possible nesting receive distinct + colors and every implicated YOLO row is labelled with its source index. +- Generated a new immutable 36-tile focused sheet on Tower. It renders 17,253 + valid labels, highlights all 86 rows participating in the 43 nesting pairs + and contains zero invalid label rows. +- AI-assisted inspection confirmed that the focused corpus contains only + magenta nesting candidates; no exact or high-IoU duplicate category is + present. The cases remain dispersed dense-GRB review candidates rather than + a safe deterministic rewrite set. +- Retained every source file and label unchanged. Excluding entire flagged + tiles would remove 17,167 unflagged labels and is therefore not justified. + +### Verified + +- Targeted relationship-auditor, renderer and Sprint 167 regression suite: + 10 tests passed. +- Scoped Ruff check, Ruff format check, Python compilation and Git whitespace + check passed. + +### Remaining limitation + +- This is explicit AI-assisted triage, not human adjudication. It improves the + evidence and review precision but does not unlock governed production + training or checkpoint promotion. diff --git a/docs/TODO.md b/docs/TODO.md index 33368dd4..58ccaf0f 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1157,6 +1157,8 @@ This file now starts with the current implementation status. Older preparation/b legitimately. - [x] Persist a deterministic label-relationship audit and a focused 36-tile contact sheet; AI follow-up found no systematic duplicate-label pattern. +- [x] Bind the relationship audit back to exact label-row indices and render + all 86 implicated rows with type-specific highlighting for focused review. - [ ] Convert the AI-assisted ledger into no stronger claim than experimental triage; a real human must independently review and sign the frozen artifacts before the governed training wrapper may unlock. diff --git a/docs/reviews/2026-08-09-ai-assisted-checkpoint-review.md b/docs/reviews/2026-08-09-ai-assisted-checkpoint-review.md index 704d3e23..47c76fd4 100644 --- a/docs/reviews/2026-08-09-ai-assisted-checkpoint-review.md +++ b/docs/reviews/2026-08-09-ai-assisted-checkpoint-review.md @@ -117,3 +117,15 @@ AI-assisted inspection found no systematic duplicate-label pattern: the relationships predominantly represent adjacent or complex building components in dense GRB contexts. All tiles remain available for experimental analysis, while the exact 43 pairs stay visible for human release adjudication. + +A second focused render now binds the relationship manifest back to the exact +YOLO label-row indices. It highlights all 86 implicated rows: cyan for exact +duplicates, red for near duplicates and magenta for possible nesting. The sheet +contains only magenta relationship marks, confirming visually and +machine-readably that there are no exact or high-IoU duplicates in this set. +The nesting cases are geographically dispersed and commonly combine a larger +GRB object envelope with a smaller component. They are not deterministic +rewrite candidates. Excluding the 36 complete tiles would also discard 17,167 +unflagged labels, so no automatic tile exclusion or label mutation was made. +The checksum-bound highlighted sheet and summary are recorded in the AI review +ledger. diff --git a/scripts/README.md b/scripts/README.md index 128ef85f..05462342 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -9,7 +9,10 @@ the script neither reads protected test data nor promotes a model. `render_operator_yolo_label_qa_contact_sheets.py` paginates complete visual reviews with `--tiles-per-sheet` (default `64`). This keeps large corpora inspectable while preserving deterministic tile selection, ordering, label -accounting and stable `contact_sheet_001.png` naming for the first page. +accounting and stable `contact_sheet_001.png` naming for the first page. When +the input is a relationship-audit manifest, exact duplicates are highlighted +cyan, near duplicates red and possible nesting magenta; every highlighted box +also shows its zero-based label-row index. Unflagged labels remain yellow. `audit_yolo_label_relationships.py` performs a read-only, same-class audit of exact duplicate, high-IoU and possible-containment pairs inside YOLO label diff --git a/scripts/render_operator_yolo_label_qa_contact_sheets.py b/scripts/render_operator_yolo_label_qa_contact_sheets.py index d62eee70..048ad6ff 100644 --- a/scripts/render_operator_yolo_label_qa_contact_sheets.py +++ b/scripts/render_operator_yolo_label_qa_contact_sheets.py @@ -250,6 +250,7 @@ def draw_tile_card( invalid_label_count: int, missing_label_file: bool, low_visual_variance: bool, + relationship_highlights: dict[int, str], ) -> Image.Image: header_height = 44 card = Image.new( @@ -277,10 +278,17 @@ def draw_tile_card( subtitle_parts.append(f"invalid:{invalid_label_count}") if low_visual_variance: subtitle_parts.append("low-variance") + if relationship_highlights: + subtitle_parts.append(f"highlighted:{len(relationship_highlights)}") draw.text((6, 6), title[:44], fill=(255, 255, 255), font=font) draw.text((6, 24), " | ".join(subtitle_parts)[:52], fill=(191, 219, 254), font=font) - for box in boxes: + highlight_styles = { + "exact_duplicate": ((0, 220, 255), 5), + "near_duplicate": ((255, 55, 55), 5), + "possible_nested": ((255, 0, 220), 5), + } + for box_index, box in enumerate(boxes): x_center = box["center_x"] * thumb_size y_center = box["center_y"] * thumb_size + header_height width = box["width"] * thumb_size @@ -291,11 +299,65 @@ def draw_tile_card( bottom = min(thumb_size + header_height - 1, y_center + height / 2) if right < left or bottom < top: continue - draw.rectangle((left, top, right, bottom), outline=(255, 214, 10), width=3) + relationship = relationship_highlights.get(box_index) + color, stroke_width = highlight_styles.get(relationship, ((255, 214, 10), 3)) + draw.rectangle((left, top, right, bottom), outline=color, width=stroke_width) + if relationship: + draw.rectangle( + (left, top, min(right, left + 30), min(bottom, top + 12)), + fill=(20, 31, 44), + ) + draw.text( + (left + 2, top + 1), + f"#{box_index}", + fill=color, + font=font, + ) return card +def build_relationship_highlights( + summary: dict[str, Any], +) -> dict[str, dict[int, str]]: + """Map audited label rows to their highest-priority visual warning.""" + priorities = { + "possible_nested": 1, + "near_duplicate": 2, + "exact_duplicate": 3, + } + highlights: dict[str, dict[int, str]] = {} + flagged_tiles = summary.get("flagged_tiles") or [] + if not isinstance(flagged_tiles, list): + return highlights + + for tile in flagged_tiles: + if not isinstance(tile, dict) or not tile.get("label_path"): + continue + tile_highlights = highlights.setdefault(str(tile["label_path"]), {}) + relationships = tile.get("relationships") or [] + if not isinstance(relationships, list): + continue + for relationship in relationships: + if not isinstance(relationship, dict): + continue + relationship_type = str(relationship.get("relationship") or "") + if relationship_type not in priorities: + continue + for field in ("first_index", "second_index"): + index = relationship.get(field) + if not isinstance(index, int) or index < 0: + continue + existing = tile_highlights.get(index) + if ( + existing is None + or priorities[relationship_type] > priorities[existing] + ): + tile_highlights[index] = relationship_type + + return highlights + + def image_has_low_visual_variance(image_path: Path, blank_range_threshold: int) -> bool: image = Image.open(image_path).convert("L") min_value, max_value = image.getextrema() @@ -344,6 +406,8 @@ def build_report( invalid_label_count = 0 valid_label_count = 0 low_visual_variance_tile_count = 0 + relationship_highlight_count = 0 + relationship_highlights = build_relationship_highlights(summary) for tile in selected_tiles: image_path = resolve_path(tile.get("image_path"), summary_path) @@ -351,6 +415,8 @@ def build_report( boxes, tile_invalid_count, missing_label_file = parse_yolo_label_file( label_path ) + tile_relationship_highlights = relationship_highlights.get(str(label_path), {}) + relationship_highlight_count += len(tile_relationship_highlights) invalid_label_count += tile_invalid_count valid_label_count += len(boxes) if missing_label_file: @@ -375,6 +441,7 @@ def build_report( invalid_label_count=tile_invalid_count, missing_label_file=missing_label_file, low_visual_variance=low_visual_variance, + relationship_highlights=tile_relationship_highlights, ) ) rendered = True @@ -393,6 +460,7 @@ def build_report( "invalid_label_count": tile_invalid_count, "missing_label_file": missing_label_file, "low_visual_variance": low_visual_variance, + "relationship_highlight_count": len(tile_relationship_highlights), "rendered": rendered, } ) @@ -435,6 +503,7 @@ def build_report( "invalid_label_count": invalid_label_count, "valid_label_count": valid_label_count, "low_visual_variance_tile_count": low_visual_variance_tile_count, + "relationship_highlight_count": relationship_highlight_count, "blank_range_threshold": args.blank_range_threshold, "contact_sheets": contact_sheets, "selected_tiles": selected_report_tiles, @@ -456,6 +525,7 @@ def write_markdown(report: dict[str, Any], output_dir: Path) -> None: f"- invalid label rows: {report['invalid_label_count']}", f"- low-variance rendered tiles: {report['low_visual_variance_tile_count']}", f"- valid labels rendered: {report['valid_label_count']}", + f"- relationship-highlighted labels: {report['relationship_highlight_count']}", "", "## Contact Sheets", "", diff --git a/tests/test_render_operator_yolo_label_qa_contact_sheets.py b/tests/test_render_operator_yolo_label_qa_contact_sheets.py index 5cbacde0..42157c20 100644 --- a/tests/test_render_operator_yolo_label_qa_contact_sheets.py +++ b/tests/test_render_operator_yolo_label_qa_contact_sheets.py @@ -2,6 +2,7 @@ import pytest from scripts.render_operator_yolo_label_qa_contact_sheets import ( CONTACT_SHEET_NAME_TEMPLATE, + build_relationship_highlights, filter_tiles_by_samples, ) @@ -34,3 +35,40 @@ def test_filter_tiles_by_samples_without_filter_preserves_tiles() -> None: def test_contact_sheet_name_template_is_stable_and_one_indexed() -> None: assert CONTACT_SHEET_NAME_TEMPLATE.format(index=1) == "contact_sheet_001.png" assert CONTACT_SHEET_NAME_TEMPLATE.format(index=12) == "contact_sheet_012.png" + + +def test_relationship_highlights_bind_indices_and_preserve_highest_priority() -> None: + summary = { + "flagged_tiles": [ + { + "label_path": "/data/tile.txt", + "relationships": [ + { + "relationship": "possible_nested", + "first_index": 2, + "second_index": 4, + }, + { + "relationship": "exact_duplicate", + "first_index": 2, + "second_index": 5, + }, + { + "relationship": "near_duplicate", + "first_index": 6, + "second_index": 7, + }, + ], + } + ] + } + + assert build_relationship_highlights(summary) == { + "/data/tile.txt": { + 2: "exact_duplicate", + 4: "possible_nested", + 5: "exact_duplicate", + 6: "near_duplicate", + 7: "near_duplicate", + } + }