Spaces:
Running
Accuracy Experiments
The estimation is accurate enough to ship (and now drives pricing), but the partner's
visual review surfaced a recurring over-count that inflates prices, plus a
seasonal-imagery weakness. This brief scopes three experiments to close that gap. Read
CLAUDE.md (architecture) and ROADMAP.md first.
Why this matters
Our lawn estimate (lawn_sqft) is the pricing basis (Phase 6, live). The partner
reviewed production (Google-imagery) measurements against his own job sheet and confirmed
they're solid overall but over-count in three recurring ways, each of which pushes the
price up:
- Sidewalk in the right-of-way counted as lawn.
- Backyard concrete (patios/pads) counted as lawn.
- Roof edges bleeding into the lawn mask.
He also flagged a season issue: current Google imagery is sometimes leaf-off (bare trees) with dormant, dry grass, which the green-keyed color baseline and the segmentation model read poorly.
Reconciliation context (see the lawn-estimator-pricing memory): our measured lawn runs a
median ~1.27Γ his recorded sqft, so pricing off it is ~+22% vs his historical charges. He
accepts that (he under-measured), but tightening the over-count directly improves price
accuracy β so accuracy is the current highest-value work.
Hard constraints β read before touching anything
HISTORICAL NOTE (2026-07): the "SAM is LOCAL ONLY" constraint below was written while SAM was an experiment. It is superseded β Experiment 2 shipped: SAM (
facebook/sam-vit-base) is now a lazy-loaded prod dependency, pre-baked into the Docker image, behind theSAM_RESTRICTflag (ON in prod). It is imported byrestrict.pyand reached fromstrategies.py. Prod segmentation still runs onmfaytin/mask2former-satellite; SAM is a not-lawn restriction, not the segmenter. The rest of this brief is preserved as-written for provenance.
- Do not perturb production or the byte-identical Douglas gate. The estimation path is
guarded by
data/regression/douglas_qa.csv: runpython -m lawn_estimator.cli --csv data/regression/douglas_qa.csv --imagery naipand diff the numeric columns againstdata/regression/phase35_douglas.csv. A change that intentionally shifts numbers (Experiment 1 will) is a deliberate re-baseline β gate it behind a config flag and review, don't silently move prod output. - SAM work is LOCAL and EXPERIMENTAL only. Do NOT import SAM into
pipeline.py/segmentation.pyor the prod dependency set / Docker image. Usenotebooks/or a scratch script and a separate optional dependency. Prod stays onmfaytin/mask2former-satellite. - Branch/PR flow: work on a feature branch β PR to
dev(auto-deploys the staging Space) β PRdevβmain(auto-deploys prod). CI (ruff + pytest) must be green. Never push to thespaceremote by hand. Details in thelawn-estimator-cicdmemory. - Env: always
"c:/Users/sergi/miniconda3/Scripts/conda.exe" run -p ./conda-env --no-capture-output python β¦(invokingconda-env/python.exedirectly crashes numpy's LAPACK with no traceback).conda runcannot executepython -cwith newlines β write a temp.pyfile. - NAIP β prod. The QA baseline uses NAIP; production uses Google (
imagery=auto+ billed key), which reads ~10β15% lower. Evaluate accuracy on Google imagery, never the NAIP QA CSV.
Evaluation β the owner judges, visually
The partner's square footage is a loose reference, NOT ground truth. He under-measures
(reconciliation showed our lawn ~1.27Γ his recorded sqft), so do not tune to match
data/lawn_care_schedule.csv β treat it only as a rough sanity check.
The real check is visual, and the OWNER makes the call. Generate the 4-panel visualization for each test address (panel 4 = the measured lawn, cyan outline) and surface the images to the owner so he can verify accuracy himself β the same "View" experience as the live app.
- Produce the PNGs by running the pipeline:
python -m lawn_estimator.cli --csv <addresses> --imagery google(or--address "β¦") β PNGs land indata/outputs/google/; panel 4 is the measured lawn. - Show them where the owner can actually see them. He is often away / on mobile, so the
editor side panel isn't enough β publish a shareable page that embeds the PNGs (data-URI
<img>s, openable on a phone) or point him to the staging/live app to run the addresses and hit View. For a geometry change (Experiment 1) show before/after of the same properties so the effect is obvious. data/regression/phase35_douglas.csvis the NAIP byte-identical gate (regression only, not an accuracy reference).
Experiment 1 β Carve the sidewalk out of the right-of-way
Idea: public sidewalks sit at a fairly standard position/width inside the ROW (commonly
a ~4β5 ft walk set back a foot or two from the property line). The ROW extension currently
adds the full ROW_BUFFER_FT (12 ft) strip as candidate lawn on street-facing edges; a
standard sidewalk band within that strip is hardscape, not turf.
Where: geometry.py β parallel_offset_zone builds the outward quads on street-facing
edges (within STREET_MAX_DIST_M); build_estimation_geometry assembles the estimation
geometry + the per-frontage extensions. The sidewalk band is a parallel sub-strip to
subtract from the ROW quad (or to exclude from the lawn mask within the ROW zone).
Notes / risks: offsets/widths vary; a fixed assumption is an approximation β validate against the viz. The RGB mask already excludes some pavement, so measure the incremental effect and don't double-subtract. This touches estimation geometry, so it WILL move Douglas QA numbers β treat as a deliberate, reviewed re-baseline, ideally behind a config flag (off by default) until proven. Highest-value, most tractable, pure geometry β start here.
Experiment 2 β SAM vs mask2former
Idea: Segment Anything (SAM/SAM2) is class-agnostic and may give tighter turf/hardscape
edges than mfaytin/mask2former-satellite. The roadmap frames it as boundary refinement
(prompt SAM with our existing lawn mask), not a wholesale replacement.
Where (reference only β do NOT modify for prod): segmentation.py
(run_lawn_area_model, _run_segmentation, MODEL_LAWN_CLASS_IDS).
How: local notebook/script; install SAM in a separate/optional env (likely needs GPU). Run SAM on the same Google tiles for the partner addresses; compare resulting lawn area/edges to mask2former, against the viz and his sqft. Decide whether SAM as a refinement pass earns its weight. If it graduates, it returns as a proposal β never a silent import into prod.
Findings (2026-07 β tested locally on 10 Douglas lots through the real LiDAR+RGB math):
- SAM runs on CPU with no new dependency (
transformersshipsSamModel). Naive point-prompt refinement expands the mask (wrong direction). "Segment-and-classify" (keep SAM pieces overlapping the mask2former lawn) drops real turf β 82% of what pure-SAM removes is grass across the sample. - The only safe design is the hybrid: mask2former for coverage, SAM subtracts only its confirmed hardscape pieces. Effect is small (~β2%) because the LiDAR path already ignores roof bleed (roofs return no ground points) and the sidewalk carve (Exp 1) handles the ROW.
- A SAM-first per-segment classifier (transparent feature vote per piece) agrees with mask2former within Β±6% on confident pieces; the only disagreement is shadowed/ambiguous strips, which no signal we have resolves β LiDAR intensity is a coin flip (AUC 0.51).
- Net: no free accuracy win β the billed number is LiDAR-dominated and every segmentation
swap agrees or worsens. See the
lawn-estimator-accuracy-exp2memory + the review pages.
The promising direction β SAM as a hardscape restriction mask (not a lawn finder): SAM segments house and concrete beautifully (crisp boundaries) even without a single "building" class, and both LiDAR and mask2former sometimes eat into the roof. Use SAM's precise house/driveway/patio segments to restrict the whole pipeline β exclude any LiDAR ground point (and mask pixel) that falls inside them β to stop that roof/edge bleed. This is the near-term lever, and it also makes SAM the natural auto-labeler for Experiment 4.
Experiment 3 β Season / imagery vintage
Idea: current Google imagery can be leaf-off with dormant/dry grass. The color baseline
run_vegetation_color_threshold (segmentation.py) keys on green (hue + excess-green), so
dry turf fails it, and the model may weaken on dormant grass.
Where: imagery adapters sources/imagery.py (NAIP, county orthos) + sources/google.py;
_fetch_analysis_image in pipeline.py selects the source; the model + color baseline in
segmentation.py. Roadmap "Later" already flags "Winter/leaf-off imagery."
How (measurement-only unless a change proves out): compare sources/seasons for the same
addresses β Google (current, maybe dormant) vs NAIP (leaf-on ~2022) β and measure how
lawn_sqft and the masks shift. Quantify what dry-grass season costs and whether a source
preference or threshold/model tweak helps.
Finding (2026-07): premise NOT reproducible β CLOSED. Current Google analysis imagery is
leaf-ON; the dormant-looking tiles were the county-ortho DISPLAY layer (never analyzed). An
initial "signal" was an artifact of that conflation plus a hardscape-confounded greenness proxy.
NAIP is out (resolution too low). No code. Revisit only if Google actually serves leaf-off
imagery. See lawn-estimator-accuracy-exp3.
Experiment 4 β Train our own segmentation model (LATER β the real ceiling-raise)
Why: every off-the-shelf swap (mask2former variants, SAM, per-segment) either agrees with the current number or worsens it β the billed number is LiDAR-dominated and the disputed area is small
- genuinely ambiguous (shadowed strips, flat hardscape). The only way to move the ceiling is a model trained on our own metro imagery with our classes (house / driveway / sidewalk / deck-patio / lawn / canopy). This is the roadmap's "Fine-tuned segmentation" item, promoted to a first-class experiment.
The bottleneck is labeling, not architecture β so this is about AUTO-LABELING / data-engine techniques, not hand-labeling:
- SAM as the auto-annotator (Meta's Segment Anything data engine β how SA-1B was built: model-assisted masks, human-in-the-loop on the hard cases, progressing toward fully automatic). We've shown SAM segments house/concrete crisply β it proposes object masks we auto-classify (mask2former vote + color + LiDAR ground/roof) into training labels.
- Karpathy-style "data engine" flywheel (Tesla Autopilot): model proposes labels β focus human review on the uncertain / failure cases (our shadowed strips) β retrain β repeat. Active learning, not bulk labeling.
- Free supervision we already have: LiDAR classification (building=6 vs ground=2) auto-labels roof vs at-grade; county building footprints (Microsoft/Google Open Buildings β a roadmap item) auto-label the house; parcel β footprint β SAM-hardscape β lawn candidate. Combine these weak/auto labels to bootstrap a training set at near-zero labeling cost.
Model + validation: fine-tune mask2former or train a DeepLabV3+/UNet on the auto-labeled metro
set; validate against LiDAR + owner review before it ever replaces mfaytin/mask2former-satellite.
Constraints: training needs a GPU (this box is CPU-only) β cloud GPU, out of band. Strictly experimental; prod stays on the current model until a validated replacement earns it. Method references to evaluate: Karpathy's auto-labeling / data-engine ("auto-research") and Meta's AutoData / SAM data-engine work.
Experiment 5 β Green reclaim (turf the model mislabels as water/grass)
Trigger (2026-07-16, owner report): 8571 Young St, Omaha 68122 β a mostly-lawn new-construction lot measured 1,215 sqft (16.4% of the estimation area) while the color baseline alone saw 4,399 sqft (59.5%). Obvious under-count, the opposite of the over-counts above.
Root cause (debug_hardscape.py + pinned-revision check β identical either way): the
model splits large uniform flat turf into classes we don't count. On the Young St tile,
class 6 (published "water") covered 44% of the tile β the entire contiguous backyard
lawn β and 98.7% of those pixels pass the green color threshold. The empirical [1, 4]
lawn mapping was derived on established, tree-heavy lots where class 1 dominates; on
treeless uniform turf (new construction, big open backyards) the model reads the texture
as water/grass, the mask excludes it, the LiDAR ground points there are dropped, and the
estimate comes out severalΓ low.
Tile survey (23 addresses: 5 QA + 18 schedule, Google imagery):
- Class 6 is absent (0%) on 20/23 tiles. Where present it is either turf (Young St 44%, 657 J E George Blvd 3.3% @ 98.8% green, 17531 Madison St 2.4% @ 97% green) or actual water that fails the green gate (708 Kountze Memorial Dr, Bellevue lakefront: class 6 only 3.6% green β correctly not reclaimed).
- Class 2 (published "grass") is a mixed street-edge band on every tile (7β32%) but only 0β8% green β blanket-adding class 2 would wreck good tiles; the green gate keeps its reclaim to +0.2β1.8% of a tile.
Fix (SHIPPED to branch feat/green-reclaim, flag GREEN_RECLAIM, off by default =
byte-identical): reclaim_green_turf in segmentation.py β pixels the model assigned
to GREEN_RECLAIM_CLASS_IDS ([2, 6]) that also pass run_vegetation_color_threshold
are added back to the lawn mask before it gates the LiDAR points. Reuses the veg mask the
pipeline already computes (no extra pass). Zero-network unit tests in
tests/test_segmentation_reclaim.py.
Limitation: the green gate can't recover dormant/dry mislabeled turf (brown grass fails the color threshold β Young St's unestablished front strips stay uncounted). That remaining gap is Experiment 4 territory.
Status / order (2026-07)
- Experiment 1 (sidewalk) β RETIRED (FAILED, owner verdict 2026-07). Built behind
SIDEWALK_CARVEonfeat/sidewalk-carve(β4%, byte-identical off) but the fixed-geometry ROW sub-band carve did not hold up and will NOT ship. Superseded by the SAM/labeler sidewalk-road line-projection direction and the leaf-on/leaf-off fusion carve (empirical, not a fixed band). - Experiment 3 (season) β CLOSED: premise not reproducible (analysis imagery is leaf-on).
- Experiment 2 (SAM) β EXPLORED: no free accuracy win (LiDAR-dominated). Safe use is the hybrid / hardscape-restriction mask (~β2%); real value is interpretability + auto-labeling for #4.
- Experiment 4 (train our own model) β IN PROGRESS: Phase 1 bake-off + the
shipped cascade (
LAWN_CASCADE) live indocs/model-upgrade-plan.md(the plan of record for Phases 2β3, incl. per-step licensing); research facts indocs/phase2-findings.md. Exp 5 (green reclaim, below) shipped as v0.2.