Spaces:
Running
Running
| """Static, self-contained HTML export β the shareable artifact. | |
| Two pages, both built from the same `tuning.render` panels the Gradio app uses: | |
| - `audit_html(result, recipe, ...)` step-by-step audit of ONE run (like the old | |
| scripts/audit_pipeline.py, but for any recipe), optionally with the gold IoU. | |
| - `leaderboard_html(entries)` a recipe-vs-recipe metrics table to pick a winner. | |
| All images are inlined as data-URI JPEGs, so a single .html file is the deliverable. | |
| """ | |
| from __future__ import annotations | |
| from lawn_estimator.segmentation import ( | |
| CASCADE_PRIMARY_MODEL_ID, | |
| LAWN_MODEL_ID, | |
| _predict_classes, | |
| ) | |
| from tuning import render as R | |
| _STYLE = """ | |
| :root{--bg:#0f130d;--card:#181d14;--ink:#e6ebe0;--soft:#9fb090;--line:#2c3626;--acc:#5db85d} | |
| *{box-sizing:border-box} | |
| body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.55 "Avenir Next",system-ui,sans-serif} | |
| main{max-width:960px;margin:0 auto;padding:24px 16px 64px} | |
| h1{font-size:clamp(22px,4vw,30px);margin:.2em 0} | |
| .lead{color:var(--soft);margin:0 0 8px} | |
| .final{background:var(--card);border:1px solid var(--line);border-left:4px solid var(--acc); | |
| border-radius:10px;padding:12px 16px;margin:14px 0 6px;font-size:18px} | |
| .final b{color:var(--acc);font-size:22px} | |
| .step{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px;margin:18px 0} | |
| .hd{display:flex;align-items:center;gap:10px} | |
| .hd .n{background:var(--acc);color:#06210a;font-weight:700;width:28px;height:28px;border-radius:50%; | |
| display:flex;align-items:center;justify-content:center;flex:none} | |
| .hd h2{font-size:17px;margin:0;flex:1}.hd .sq{color:var(--acc);font-weight:600;font-variant-numeric:tabular-nums} | |
| .dsc{color:var(--soft);font-size:14px;margin:8px 2px 10px} | |
| .step img{width:100%;border-radius:8px;display:block} | |
| .legend{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px} | |
| .chip{display:flex;align-items:center;gap:6px;background:#0f130d;border:1px solid var(--line); | |
| border-radius:7px;padding:4px 9px;font-size:12.5px} | |
| .chip .sw{width:13px;height:13px;border-radius:3px;border:1px solid #0006} | |
| .tblwrap{overflow-x:auto;margin:18px 0} | |
| table{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums} | |
| th,td{border:1px solid var(--line);padding:9px 11px;text-align:right} | |
| thead th{background:#20281a;color:var(--soft)}th.l,td.l{text-align:left} | |
| tr.best td{background:#1c2a17} | |
| """ | |
| def _legend_html(legend) -> str: | |
| if not legend: | |
| return "" | |
| chips = "".join( | |
| f'<span class="chip"><span class="sw" style="background:{e["color"]}"></span>' | |
| f'{e["name"]} <b>{e["pct"]}%</b></span>' for e in legend) | |
| return f'<div class="legend">{chips}</div>' | |
| def _step(n, title, desc, sqft, img, legend="") -> str: | |
| return (f'<section class="step"><div class="hd"><span class="n">{n}</span>' | |
| f'<h2>{title}</h2><span class="sq">{sqft}</span></div>' | |
| f'<p class="dsc">{desc}</p><img src="{img}" alt="{title}">{legend}</section>') | |
| def _segmentation_steps(cap, recipe, start_n) -> list[str]: | |
| """Per-class panels appropriate to the recipe's segmenter (or its fusion base).""" | |
| img = cap["image"] | |
| steps, n = [], start_n | |
| # For fusion, show the LEAF-ON base segmenter's read (that's the lawn source). | |
| eff = recipe.fusion_base_segmenter if recipe.segmenter == "leafon_leafoff_fusion" else recipe.segmenter | |
| if eff == "eomt-cascade": | |
| panel, leg = R.class_panel(img, _predict_classes(CASCADE_PRIMARY_MODEL_ID, img), R.ADE_NAMES) | |
| steps.append(_step(n, "Segmentation β EoMT-DINOv3 (cascade primary)", | |
| "Primary read, real ADE20K class names.", "", R.b64(panel), _legend_html(leg))) | |
| n += 1 | |
| panel, leg = R.class_panel(img, _predict_classes(LAWN_MODEL_ID, img), R.M2F_NAMES) | |
| steps.append(_step(n, "Segmentation β mask2former (arbiter)", | |
| "Arbitrates the cascade's sidewalk/earth classes.", "", R.b64(panel), _legend_html(leg))) | |
| n += 1 | |
| elif eff in ("mask2former", "eomt-solo", "trained"): | |
| from tuning.harness import _segmenter_model_id | |
| model_id = _segmenter_model_id(recipe) if eff == recipe.segmenter else ( | |
| "tue-mps/eomt-dinov3-ade-semantic-large-512" if eff == "eomt-solo" else LAWN_MODEL_ID) | |
| names = R.ADE_NAMES if "eomt" in model_id or "ade" in model_id else R.M2F_NAMES | |
| panel, leg = R.class_panel(img, _predict_classes(model_id, img), names) | |
| steps.append(_step(n, f"Segmentation β {model_id}", | |
| "Per-class prediction for the chosen model.", "", R.b64(panel), _legend_html(leg))) | |
| n += 1 | |
| # color / sam3 have no per-class map to show here. | |
| return steps | |
| def audit_html(result, recipe, gold_mask=None, gold_meta=None) -> str: | |
| """Step-by-step audit page for one recipe run.""" | |
| cap = result.capture | |
| img = cap["image"] | |
| steps = [] | |
| steps.append(_step(1, "Analysis imagery", | |
| f"{recipe.imagery} tile, {img.size[0]}Γ{img.size[1]} px β every measurement runs on this.", | |
| "", R.b64(img))) | |
| steps.append(_step(2, "Legal parcel", | |
| f"County parcel boundary (yellow). {cap['parcel_area_sqft']:,.0f} sqft legal lot.", | |
| "", R.b64(R.draw_outlines(img, cap["legal_outlines"], (255, 220, 0))))) | |
| ext = "; ".join(f"{e.get('street') or '?'} +{e['area_sqft']:,.0f}" for e in cap["extensions"]) | |
| geo_img = R.draw_outlines(R.draw_outlines(img, cap["legal_outlines"], (255, 220, 0)), | |
| cap["estimation_outlines"], (0, 220, 220)) | |
| steps.append(_step(3, "Estimation geometry (to the curb)", | |
| f"Parcel extended to street-facing curbs (cyan). {cap['estimation_area_sqft']:,.0f} sqft. " | |
| f"Extensions: {ext or 'none'}.", "", R.b64(geo_img))) | |
| steps.append(_step(4, "Color-threshold vegetation (baseline)", | |
| "Pure HSV/green rule β the sanity baseline, not the estimate.", | |
| f"{cap['rgb_veg_sqft']:,.0f} sqft", | |
| R.b64(R.overlay(img, cap["veg_in_parcel"], (60, 220, 60))))) | |
| n = 5 | |
| seg_steps = _segmentation_steps(cap, recipe, n) | |
| steps.extend(seg_steps) | |
| n += len(seg_steps) | |
| if cap.get("leaf_off_image") is not None: # leaf-on/leaf-off fusion | |
| loff = cap["leaf_off_image"] | |
| steps.append(_step(n, "Leaf-off (DOGIS) β same frame", | |
| "The pixel-registered leaf-off ortho: bare branches expose the ground under canopy.", | |
| "", R.b64(loff))) | |
| n += 1 | |
| steps.append(_step(n, f"Carved hardscape ({recipe.fusion_hardscape_detector})", | |
| "Hardscape found on the leaf-off tile under the canopy (red) β SUBTRACTED from lawn. " | |
| "This is the hidden driveway/patio the canopy flag only warns about.", | |
| "", R.b64(R.overlay(loff, cap["fusion_carve"], (235, 60, 60))))) | |
| n += 1 | |
| steps.append(_step(n, "Lawn-area mask (candidate lawn)", | |
| "Segmenter decision (+ any reclaim) β the pixels eligible as lawn.", | |
| "", R.b64(R.overlay(img, cap["lawn_area_in_parcel"], (60, 220, 60))))) | |
| n += 1 | |
| if cap.get("not_lawn_mask") is not None: | |
| steps.append(_step(n, f"Restrict β {recipe.restrict} not-lawn mask", | |
| "The roof/driveway/sidewalk (red) this restrict tool removes from the " | |
| "LiDAR count. Compare SAM1 vs SAM3 here β same lot, different recipe.", | |
| "", R.b64(R.overlay(img, cap["not_lawn_mask"], (235, 60, 60))))) | |
| n += 1 | |
| if cap["est"].viz.get("ground_px") is not None: | |
| steps.append(_step(n, "LiDAR ground points β classified", | |
| "Ground returns colored: green = counted as lawn, red = removed (hardscape/roof, " | |
| "incl. restrict). sqft = green / all-ground Γ ground-sampled area.", | |
| f"{result.lawn_sqft:,.0f} sqft", R.b64(R.lidar_panel(img, cap["est"].viz)))) | |
| n += 1 | |
| if gold_mask is not None: | |
| scored = "" | |
| from tuning.metrics import mask_scores | |
| sc = mask_scores(cap["lawn_area_in_parcel"], gold_mask) | |
| true_sqft = (gold_meta or {}).get("mask_sqft") | |
| if true_sqft: | |
| err = (result.lawn_sqft - true_sqft) / true_sqft * 100 | |
| scored = f" Β· true {true_sqft:,.0f} sqft, error {err:+.1f}%" | |
| steps.append(_step(n, "vs. gold (hand-drawn mowable area)", | |
| f"Green = agree, red = recipe over-counts, blue = recipe misses. " | |
| f"IoU {sc['iou']}, precision {sc['precision']}, recall {sc['recall']}{scored}.", | |
| "", R.b64(R.iou_overlay(img, cap["lawn_area_in_parcel"], gold_mask)))) | |
| final = (f'<div class="final">Measured lawn: <b>{result.lawn_sqft:,.0f} sqft</b> Β· ' | |
| f'recipe: {recipe.name} Β· method: {result.method} ({result.confidence})</div>') | |
| body = f'<h1>Pipeline audit</h1><p class="lead">{result.address}</p>{final}' + "".join(steps) | |
| return f'<title>Pipeline audit β {result.address}</title><style>{_STYLE}</style><main>{body}</main>' | |
| def leaderboard_html(entries: list[dict]) -> str: | |
| """`entries` = [{name, summary(dict from metrics.aggregate)}], best MAE highlighted.""" | |
| ranked = sorted([e for e in entries if e["summary"].get("n")], | |
| key=lambda e: e["summary"]["mae"]) | |
| cols = [("mae", "MAE sqft"), ("mape_pct", "MAPE %"), ("median_ape_pct", "median APE %"), | |
| ("bias_pct", "bias %"), ("rmse", "RMSE"), ("r2", "RΒ²"), ("mean_iou", "mean IoU"), ("n", "lots")] | |
| head = "".join(f"<th>{lbl}</th>" for _, lbl in cols) | |
| rows = "" | |
| for i, e in enumerate(ranked): | |
| s = e["summary"] | |
| cells = "".join(f"<td>{'' if s.get(k) is None else s.get(k)}</td>" for k, _ in cols) | |
| rows += f'<tr class="{"best" if i == 0 else ""}"><td class="l">{e["name"]}</td>{cells}</tr>' | |
| body = (f'<h1>Recipe leaderboard</h1>' | |
| f'<p class="lead">Ranked by MAE against the gold set β lowest error at top.</p>' | |
| f'<div class="tblwrap"><table><thead><tr><th class="l">recipe</th>{head}</tr></thead>' | |
| f'<tbody>{rows}</tbody></table></div>') | |
| return f'<title>Recipe leaderboard</title><style>{_STYLE}</style><main>{body}</main>' | |