Spaces:
Running
Running
File size: 7,764 Bytes
f6f7b53 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 | """Scoring one check-in, end to end.
fetch row -> download both photos -> gates -> scores -> write back
Idempotent: re-running on the same check-in recomputes from the stored photos
and overwrites the result. That matters more than it sounds, because it means
every threshold change can be replayed over the entire pilot dataset without
going back into the field. The photos are the raw evidence; the verdict is
derived and disposable.
"""
from __future__ import annotations
import logging
import random
from . import store
from .embed import cosine, embed
from .scoring import (
Assessment,
assess,
PROXIMITY_REVIEW_M,
check_plant_plausibility,
check_proximity,
gate_duplicate,
gate_gps,
gate_travel,
score_growth,
score_liveness,
score_location,
score_scene,
)
from .signals import (
canopy_fraction,
excess_green,
haversine_m,
liveness_score,
orb_inliers,
phash,
plant_reference,
trunk_width_mm,
)
log = logging.getLogger(__name__)
# Fraction of auto-approved check-ins pulled for human spot-checking.
# Without this we would have no way to estimate the false-ACCEPT rate, which is
# the number that actually matters — a wrongly rejected genuine visit is
# annoying, a wrongly approved fraud is fatal, and nothing else in the system
# measures the second one.
AUDIT_RATE = 0.10
def score_checkin(checkin_id: str) -> Assessment:
row = store.get_checkin(checkin_id)
if row is None:
raise LookupError(f"No check-in {checkin_id}")
tree = store.get_tree(row["tree_id"])
if tree is None:
raise LookupError(f"Check-in {checkin_id} references a missing tree")
wide = store.download_image(row["wide_photo"])
close = store.download_image(row["close_photo"])
# --- fingerprints ------------------------------------------------------
wide_ph = phash(wide)
wide_emb = embed(wide)
close_emb = embed(close)
captured_at = store.parse_ts(row["captured_at"])
prev_visits = store.previous_checkins(row["tree_id"], row["captured_at"])
# --- gates -------------------------------------------------------------
gates = {}
gates["duplicate_image"] = gate_duplicate(
wide_ph, store.other_phashes(checkin_id)
)
gates["gps_radius"] = gate_gps(
tree["lat"],
tree["lng"],
row["lat"],
row["lng"],
row.get("gps_accuracy_m"),
)
prev_any = store.previous_by_submitter(
row["submitted_by"], row["captured_at"], checkin_id
)
gates["travel_speed"] = gate_travel(
row["lat"],
row["lng"],
captured_at,
(
prev_any["id"],
prev_any["lat"],
prev_any["lng"],
store.parse_ts(prev_any["captured_at"]),
)
if prev_any
else None,
)
# --- scores ------------------------------------------------------------
scores: dict[str, dict] = {}
distance = haversine_m(tree["lat"], tree["lng"], row["lat"], row["lng"])
scores["location"] = score_location(distance)
green = excess_green(wide)
scores["liveness"] = score_liveness(green, liveness_score(green))
# Scene match needs something to match AGAINST. On the very first visit
# there is nothing, so the signal is absent rather than zero — assess()
# renormalises and lowers confidence via the coverage term instead of
# treating a registration as suspicious.
if prev_visits:
best_wide = best_close = None
for p in prev_visits:
pw = store.parse_embedding(p.get("wide_embedding"))
pc = store.parse_embedding(p.get("close_embedding"))
if pw is not None:
c = cosine(wide_emb, pw)
best_wide = c if best_wide is None else max(best_wide, c)
if pc is not None:
c = cosine(close_emb, pc)
best_close = c if best_close is None else max(best_close, c)
inliers = None
try:
prev_wide = store.download_image(prev_visits[0]["wide_photo"])
inliers = orb_inliers(wide, prev_wide)
except Exception: # noqa: BLE001 - corroboration only, never fatal
log.warning("ORB comparison failed for %s", checkin_id, exc_info=True)
if best_wide is not None or best_close is not None:
scores["scene_match"] = score_scene(best_wide, best_close, inliers)
canopy_now = canopy_fraction(wide)
canopy_prev = trunk_prev = None
if prev_visits:
prev_growth = ((prev_visits[0].get("signals") or {}).get("scores") or {}).get(
"growth", {}
)
canopy_prev = prev_growth.get("canopy_frac")
trunk_prev = prev_growth.get("trunk_mm")
# Millimetres are derived HERE, from the stored photo and the stored taps —
# never from anything the client calculated. See signals.trunk_width_mm.
measurement = trunk_width_mm(close, row.get("trunk_taps"))
trunk_now = measurement.get("trunk_mm") if measurement and measurement.get("available") else None
scores["growth"] = score_growth(canopy_now, canopy_prev, trunk_now, trunk_prev)
if measurement:
scores["growth"]["measurement"] = measurement
# --- review flags ------------------------------------------------------
# Not gates: these must not auto-approve, but they are not accusations. See
# scoring.check_proximity for why proximity cannot be a gate.
review_flags = {}
# Runs on EVERY visit, not just the first. Registering a chair is the
# obvious case, but swapping one in at visit three is the case a
# first-visit-only check would wave through.
review_flags["plant_plausibility"] = check_plant_plausibility(
wide_emb, close_emb, plant_reference()
)
# Only on the tree's FIRST check-in, i.e. its registration. On every later
# visit the tree already exists and standing near a neighbour is not news —
# re-flagging it every round would bury the reviewer in the same alert.
if not prev_visits:
review_flags["tree_proximity"] = check_proximity(
tree["lat"],
tree["lng"],
store.trees_near_registered_before(
tree["id"],
tree["lat"],
tree["lng"],
tree["created_at"],
PROXIMITY_REVIEW_M,
),
)
# --- combine and persist ----------------------------------------------
result = assess(gates, scores, review_flags=review_flags)
# Carry provenance forward. write_result replaces `signals` WHOLESALE, so a
# self-test tag written at insert time is destroyed by the very act of
# scoring. That is not cosmetic: it made six injected attack rows
# indistinguishable from real pilot data, and the only thing that still
# identified them was the storage path. Provenance is a fact about where a
# row came from, not part of the score, and must survive being scored.
provenance = (row.get("signals") or {}).get("self_test")
if provenance:
result.signals["self_test"] = provenance
audit = result.verdict == "verified" and random.random() < AUDIT_RATE
store.write_result(
checkin_id,
phash=wide_ph,
wide_embedding=wide_emb,
close_embedding=close_emb,
confidence=result.confidence,
verdict=result.verdict,
signals=result.signals,
audit_sample=audit,
)
store.update_tree_status(row["tree_id"], result.confidence, result.verdict)
log.info(
"scored %s -> %s (%d) gates=%s",
checkin_id,
result.verdict,
result.confidence,
{k: v.passed for k, v in gates.items()},
)
return result
|