Report blank YOLO label QA tiles
This commit is contained in:
@@ -68,6 +68,18 @@ def test_all_in_one_dockerfile_copies_operator_scripts_for_runtime_use() -> None
|
|||||||
assert "COPY scripts/train_operator_yolo_detector.sh /app/scripts/train_operator_yolo_detector.sh" in dockerfile
|
assert "COPY scripts/train_operator_yolo_detector.sh /app/scripts/train_operator_yolo_detector.sh" in dockerfile
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_in_one_dockerfile_copies_operator_scripts_after_dependency_install() -> None:
|
||||||
|
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
dependency_install_index = dockerfile.index('RUN /usr/bin/python3.11 -m venv /opt/geointel/venv \\')
|
||||||
|
operator_copy_index = dockerfile.index(
|
||||||
|
"COPY scripts/render_operator_yolo_label_qa_contact_sheets.py "
|
||||||
|
"/app/scripts/render_operator_yolo_label_qa_contact_sheets.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert operator_copy_index > dependency_install_index
|
||||||
|
|
||||||
|
|
||||||
def test_compose_does_not_require_missing_root_env_file() -> None:
|
def test_compose_does_not_require_missing_root_env_file() -> None:
|
||||||
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,20 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def write_patterned_image(path: Path, color: tuple[int, int, int]) -> None:
|
||||||
|
image = Image.new("RGB", (64, 64), color=color)
|
||||||
|
draw = ImageDraw.Draw(image)
|
||||||
|
draw.rectangle((8, 8, 38, 30), fill=(220, 220, 210))
|
||||||
|
draw.line((0, 48, 64, 24), fill=(30, 40, 50), width=4)
|
||||||
|
image.save(path)
|
||||||
|
|
||||||
|
|
||||||
def test_operator_yolo_label_qa_contact_sheets_render_visual_artifacts(tmp_path: Path) -> None:
|
def test_operator_yolo_label_qa_contact_sheets_render_visual_artifacts(tmp_path: Path) -> None:
|
||||||
script_path = ROOT / "scripts" / "render_operator_yolo_label_qa_contact_sheets.py"
|
script_path = ROOT / "scripts" / "render_operator_yolo_label_qa_contact_sheets.py"
|
||||||
assert script_path.exists()
|
assert script_path.exists()
|
||||||
@@ -29,9 +37,9 @@ def test_operator_yolo_label_qa_contact_sheets_render_visual_artifacts(tmp_path:
|
|||||||
(image_train / "dense_000.png", (120, 130, 140)),
|
(image_train / "dense_000.png", (120, 130, 140)),
|
||||||
(image_train / "invalid_000.png", (80, 100, 120)),
|
(image_train / "invalid_000.png", (80, 100, 120)),
|
||||||
(image_val / "missing_000.png", (90, 120, 90)),
|
(image_val / "missing_000.png", (90, 120, 90)),
|
||||||
(image_val / "negative_000.png", (50, 50, 55)),
|
|
||||||
):
|
):
|
||||||
Image.new("RGB", (64, 64), color=color).save(path)
|
write_patterned_image(path, color)
|
||||||
|
Image.new("RGB", (64, 64), color=(255, 255, 255)).save(image_val / "negative_000.png")
|
||||||
|
|
||||||
(labels_train / "dense_000.txt").write_text(
|
(labels_train / "dense_000.txt").write_text(
|
||||||
"0 0.500000 0.500000 0.500000 0.500000\n"
|
"0 0.500000 0.500000 0.500000 0.500000\n"
|
||||||
@@ -141,12 +149,16 @@ def test_operator_yolo_label_qa_contact_sheets_render_visual_artifacts(tmp_path:
|
|||||||
assert report["rendered_tile_count"] == 4
|
assert report["rendered_tile_count"] == 4
|
||||||
assert report["missing_label_file_count"] == 1
|
assert report["missing_label_file_count"] == 1
|
||||||
assert report["invalid_label_count"] == 1
|
assert report["invalid_label_count"] == 1
|
||||||
|
assert report["low_visual_variance_tile_count"] == 1
|
||||||
assert [tile["sample_slug"] for tile in report["selected_tiles"]] == [
|
assert [tile["sample_slug"] for tile in report["selected_tiles"]] == [
|
||||||
"dense",
|
"dense",
|
||||||
"invalid",
|
"invalid",
|
||||||
"missing",
|
"missing",
|
||||||
"negative",
|
"negative",
|
||||||
]
|
]
|
||||||
|
tile_by_slug = {tile["sample_slug"]: tile for tile in report["selected_tiles"]}
|
||||||
|
assert tile_by_slug["negative"]["low_visual_variance"] is True
|
||||||
|
assert tile_by_slug["dense"]["low_visual_variance"] is False
|
||||||
|
|
||||||
sheet_path = output_dir / report["contact_sheets"][0]["path"]
|
sheet_path = output_dir / report["contact_sheets"][0]["path"]
|
||||||
assert sheet_path.exists()
|
assert sheet_path.exists()
|
||||||
@@ -159,4 +171,5 @@ def test_operator_yolo_label_qa_contact_sheets_render_visual_artifacts(tmp_path:
|
|||||||
assert "Operator YOLO Label QA Contact Sheets" in markdown
|
assert "Operator YOLO Label QA Contact Sheets" in markdown
|
||||||
assert "missing label files: 1" in markdown
|
assert "missing label files: 1" in markdown
|
||||||
assert "invalid label rows: 1" in markdown
|
assert "invalid label rows: 1" in markdown
|
||||||
|
assert "low-variance rendered tiles: 1" in markdown
|
||||||
assert "contact_sheet_001.png" in markdown
|
assert "contact_sheet_001.png" in markdown
|
||||||
|
|||||||
@@ -46,6 +46,17 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY backend/ /app/
|
COPY backend/ /app/
|
||||||
COPY fixtures/ /app/fixtures/
|
COPY fixtures/ /app/fixtures/
|
||||||
|
|
||||||
|
RUN /usr/bin/python3.11 -m venv /opt/geointel/venv \
|
||||||
|
&& pip install --no-cache-dir --upgrade pip setuptools \
|
||||||
|
&& extras=".[gis]" \
|
||||||
|
&& if [ "$GEOINTEL_INSTALL_AI" = "true" ]; then extras=".[gis,ai]"; fi \
|
||||||
|
&& pip install --no-cache-dir "$extras" \
|
||||||
|
&& python scripts/gis_import_smoke.py \
|
||||||
|
&& python scripts/yolo_preflight.py --json >/tmp/geointel-yolo-preflight.json \
|
||||||
|
&& rm -f /etc/nginx/sites-enabled/default \
|
||||||
|
&& mkdir -p /app/storage /run/nginx /var/log/nginx
|
||||||
|
|
||||||
COPY scripts/prepare_operator_real_data_samples.py /app/scripts/prepare_operator_real_data_samples.py
|
COPY scripts/prepare_operator_real_data_samples.py /app/scripts/prepare_operator_real_data_samples.py
|
||||||
COPY scripts/export_operator_yolo_tile_dataset.py /app/scripts/export_operator_yolo_tile_dataset.py
|
COPY scripts/export_operator_yolo_tile_dataset.py /app/scripts/export_operator_yolo_tile_dataset.py
|
||||||
COPY scripts/audit_operator_yolo_dataset_quality.py /app/scripts/audit_operator_yolo_dataset_quality.py
|
COPY scripts/audit_operator_yolo_dataset_quality.py /app/scripts/audit_operator_yolo_dataset_quality.py
|
||||||
@@ -55,17 +66,8 @@ COPY deploy/unraid/nginx-all-in-one.conf /etc/nginx/conf.d/default.conf
|
|||||||
COPY deploy/unraid/all-in-one-start.sh /usr/local/bin/geointel-all-in-one-start
|
COPY deploy/unraid/all-in-one-start.sh /usr/local/bin/geointel-all-in-one-start
|
||||||
COPY --from=frontend-build /frontend/dist/ /usr/share/nginx/html/
|
COPY --from=frontend-build /frontend/dist/ /usr/share/nginx/html/
|
||||||
|
|
||||||
RUN /usr/bin/python3.11 -m venv /opt/geointel/venv \
|
RUN chmod +x /usr/local/bin/geointel-all-in-one-start \
|
||||||
&& pip install --no-cache-dir --upgrade pip setuptools \
|
&& chmod +x /app/scripts/train_operator_yolo_detector.sh
|
||||||
&& extras=".[gis]" \
|
|
||||||
&& if [ "$GEOINTEL_INSTALL_AI" = "true" ]; then extras=".[gis,ai]"; fi \
|
|
||||||
&& pip install --no-cache-dir "$extras" \
|
|
||||||
&& python scripts/gis_import_smoke.py \
|
|
||||||
&& python scripts/yolo_preflight.py --json >/tmp/geointel-yolo-preflight.json \
|
|
||||||
&& chmod +x /usr/local/bin/geointel-all-in-one-start \
|
|
||||||
&& chmod +x /app/scripts/train_operator_yolo_detector.sh \
|
|
||||||
&& rm -f /etc/nginx/sites-enabled/default \
|
|
||||||
&& mkdir -p /app/storage /run/nginx /var/log/nginx
|
|
||||||
|
|
||||||
VOLUME ["/var/lib/postgresql/data", "/app/storage"]
|
VOLUME ["/var/lib/postgresql/data", "/app/storage"]
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -436,8 +436,8 @@ The renderer writes `operator_yolo_label_qa_summary.json`,
|
|||||||
`operator_yolo_label_qa_contact_sheet.md` and `contact_sheet_001.png`. It draws
|
`operator_yolo_label_qa_contact_sheet.md` and `contact_sheet_001.png`. It draws
|
||||||
existing YOLO labels on existing tile images only; it does not run inference,
|
existing YOLO labels on existing tile images only; it does not run inference,
|
||||||
train a model, fetch providers or create fake detections. Missing image files,
|
train a model, fetch providers or create fake detections. Missing image files,
|
||||||
missing label files and invalid YOLO rows are reported in the JSON/Markdown
|
missing label files, invalid YOLO rows and low-variance/blank-looking rendered
|
||||||
artifacts.
|
tiles are reported in the JSON/Markdown artifacts.
|
||||||
|
|
||||||
Current Tower audit status:
|
Current Tower audit status:
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ def parse_args() -> argparse.Namespace:
|
|||||||
parser.add_argument("--max-tiles", type=int, default=24, help="Maximum selected tiles to render")
|
parser.add_argument("--max-tiles", type=int, default=24, help="Maximum selected tiles to render")
|
||||||
parser.add_argument("--columns", type=int, default=4, help="Contact-sheet columns")
|
parser.add_argument("--columns", type=int, default=4, help="Contact-sheet columns")
|
||||||
parser.add_argument("--thumb-size", type=int, default=256, help="Rendered tile thumbnail size in pixels")
|
parser.add_argument("--thumb-size", type=int, default=256, help="Rendered tile thumbnail size in pixels")
|
||||||
|
parser.add_argument(
|
||||||
|
"--blank-range-threshold",
|
||||||
|
type=int,
|
||||||
|
default=3,
|
||||||
|
help="Mark rendered images with grayscale max-min range at or below this value as low variance.",
|
||||||
|
)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
@@ -146,6 +152,7 @@ def draw_tile_card(
|
|||||||
thumb_size: int,
|
thumb_size: int,
|
||||||
invalid_label_count: int,
|
invalid_label_count: int,
|
||||||
missing_label_file: bool,
|
missing_label_file: bool,
|
||||||
|
low_visual_variance: bool,
|
||||||
) -> Image.Image:
|
) -> Image.Image:
|
||||||
header_height = 44
|
header_height = 44
|
||||||
card = Image.new("RGB", (thumb_size, thumb_size + header_height), color=(245, 247, 250))
|
card = Image.new("RGB", (thumb_size, thumb_size + header_height), color=(245, 247, 250))
|
||||||
@@ -163,6 +170,8 @@ def draw_tile_card(
|
|||||||
subtitle_parts.append("missing-label-file")
|
subtitle_parts.append("missing-label-file")
|
||||||
if invalid_label_count:
|
if invalid_label_count:
|
||||||
subtitle_parts.append(f"invalid:{invalid_label_count}")
|
subtitle_parts.append(f"invalid:{invalid_label_count}")
|
||||||
|
if low_visual_variance:
|
||||||
|
subtitle_parts.append("low-variance")
|
||||||
draw.text((6, 6), title[:44], fill=(255, 255, 255), font=font)
|
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)
|
draw.text((6, 24), " | ".join(subtitle_parts)[:52], fill=(191, 219, 254), font=font)
|
||||||
|
|
||||||
@@ -180,6 +189,12 @@ def draw_tile_card(
|
|||||||
return card
|
return card
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
return (max_value - min_value) <= blank_range_threshold
|
||||||
|
|
||||||
|
|
||||||
def build_contact_sheet(cards: list[Image.Image], columns: int, output_path: Path) -> None:
|
def build_contact_sheet(cards: list[Image.Image], columns: int, output_path: Path) -> None:
|
||||||
if not cards:
|
if not cards:
|
||||||
return
|
return
|
||||||
@@ -216,6 +231,7 @@ def build_report(summary: dict[str, Any], summary_path: Path, args: argparse.Nam
|
|||||||
missing_label_file_count = 0
|
missing_label_file_count = 0
|
||||||
invalid_label_count = 0
|
invalid_label_count = 0
|
||||||
valid_label_count = 0
|
valid_label_count = 0
|
||||||
|
low_visual_variance_tile_count = 0
|
||||||
|
|
||||||
for tile in selected_tiles:
|
for tile in selected_tiles:
|
||||||
image_path = resolve_path(tile.get("image_path"), summary_path)
|
image_path = resolve_path(tile.get("image_path"), summary_path)
|
||||||
@@ -227,9 +243,13 @@ def build_report(summary: dict[str, Any], summary_path: Path, args: argparse.Nam
|
|||||||
missing_label_file_count += 1
|
missing_label_file_count += 1
|
||||||
|
|
||||||
rendered = False
|
rendered = False
|
||||||
|
low_visual_variance = False
|
||||||
if image_path is None or not image_path.exists():
|
if image_path is None or not image_path.exists():
|
||||||
missing_image_count += 1
|
missing_image_count += 1
|
||||||
else:
|
else:
|
||||||
|
low_visual_variance = image_has_low_visual_variance(image_path, args.blank_range_threshold)
|
||||||
|
if low_visual_variance:
|
||||||
|
low_visual_variance_tile_count += 1
|
||||||
rendered_cards.append(
|
rendered_cards.append(
|
||||||
draw_tile_card(
|
draw_tile_card(
|
||||||
image_path=image_path,
|
image_path=image_path,
|
||||||
@@ -238,6 +258,7 @@ def build_report(summary: dict[str, Any], summary_path: Path, args: argparse.Nam
|
|||||||
thumb_size=args.thumb_size,
|
thumb_size=args.thumb_size,
|
||||||
invalid_label_count=tile_invalid_count,
|
invalid_label_count=tile_invalid_count,
|
||||||
missing_label_file=missing_label_file,
|
missing_label_file=missing_label_file,
|
||||||
|
low_visual_variance=low_visual_variance,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
rendered = True
|
rendered = True
|
||||||
@@ -255,6 +276,7 @@ def build_report(summary: dict[str, Any], summary_path: Path, args: argparse.Nam
|
|||||||
"valid_label_count": len(boxes),
|
"valid_label_count": len(boxes),
|
||||||
"invalid_label_count": tile_invalid_count,
|
"invalid_label_count": tile_invalid_count,
|
||||||
"missing_label_file": missing_label_file,
|
"missing_label_file": missing_label_file,
|
||||||
|
"low_visual_variance": low_visual_variance,
|
||||||
"rendered": rendered,
|
"rendered": rendered,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -285,6 +307,8 @@ def build_report(summary: dict[str, Any], summary_path: Path, args: argparse.Nam
|
|||||||
"missing_label_file_count": missing_label_file_count,
|
"missing_label_file_count": missing_label_file_count,
|
||||||
"invalid_label_count": invalid_label_count,
|
"invalid_label_count": invalid_label_count,
|
||||||
"valid_label_count": valid_label_count,
|
"valid_label_count": valid_label_count,
|
||||||
|
"low_visual_variance_tile_count": low_visual_variance_tile_count,
|
||||||
|
"blank_range_threshold": args.blank_range_threshold,
|
||||||
"contact_sheets": contact_sheets,
|
"contact_sheets": contact_sheets,
|
||||||
"selected_tiles": selected_report_tiles,
|
"selected_tiles": selected_report_tiles,
|
||||||
},
|
},
|
||||||
@@ -302,6 +326,7 @@ def write_markdown(report: dict[str, Any], output_dir: Path) -> None:
|
|||||||
f"- missing images: {report['missing_image_count']}",
|
f"- missing images: {report['missing_image_count']}",
|
||||||
f"- missing label files: {report['missing_label_file_count']}",
|
f"- missing label files: {report['missing_label_file_count']}",
|
||||||
f"- invalid label rows: {report['invalid_label_count']}",
|
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"- valid labels rendered: {report['valid_label_count']}",
|
||||||
"",
|
"",
|
||||||
"## Contact Sheets",
|
"## Contact Sheets",
|
||||||
@@ -323,6 +348,8 @@ def write_markdown(report: dict[str, Any], output_dir: Path) -> None:
|
|||||||
flags.append("missing-label-file")
|
flags.append("missing-label-file")
|
||||||
if tile["invalid_label_count"]:
|
if tile["invalid_label_count"]:
|
||||||
flags.append(f"invalid:{tile['invalid_label_count']}")
|
flags.append(f"invalid:{tile['invalid_label_count']}")
|
||||||
|
if tile["low_visual_variance"]:
|
||||||
|
flags.append("low-variance")
|
||||||
flag_text = ", ".join(flags) if flags else "ok"
|
flag_text = ", ".join(flags) if flags else "ok"
|
||||||
lines.append(
|
lines.append(
|
||||||
"- "
|
"- "
|
||||||
|
|||||||
Reference in New Issue
Block a user