File size: 4,507 Bytes
d70361b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Quantify the false-positive / hallucination pattern in a set of detected
regions, WITHOUT re-running the model. Reads a saved region list and reports
counts by type, a confidence-bucket histogram, per-type confidence, and a
single "hallucination fraction" metric (low-confidence unclassified regions
as a share of all regions).

Purpose (DDA Grid_54 vs H43X2E1 reports, Jul 2026): give hard numbers behind
"it's still hallucinating" so the fix can be measured, not guessed. Pairs with
scripts/filter_regions.py — analyze first to pick thresholds, then filter.

Input (pick one):
    --in regions.json     JSON list of region dicts (or {"regions": [...]})
    --run-id N            read regions from the app DB (data/satellite_app.db)

Options:
    --halluc-types "Unclassified Ground Change,Other"   types treated as
                        "junk-prone" for the hallucination metric (substring match)
    --halluc-conf 0.55  confidence below which a junk-prone region counts as a
                        likely hallucination

Example:
    python scripts/analyze_regions.py --in regions.json
    python scripts/analyze_regions.py --run-id 37 --halluc-conf 0.5
"""
import argparse
import json
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))

_BUCKETS = [(0.0, 0.25), (0.25, 0.40), (0.40, 0.50), (0.50, 0.75), (0.75, 1.01)]


def _load(args) -> tuple:
    if args.run_id is not None:
        from app.database import SessionLocal
        from app.models import DetectionRun
        db = SessionLocal()
        run = db.query(DetectionRun).filter(DetectionRun.id == args.run_id).first()
        db.close()
        if run is None:
            raise SystemExit(f"No DetectionRun with id={args.run_id}")
        return json.loads(run.regions_json or "[]"), f"DB run #{args.run_id}"
    data = json.loads(Path(args.in_path).read_text(encoding="utf-8"))
    if isinstance(data, dict):
        data = data.get("regions", [])
    return data, args.in_path


def _bucket_label(lo, hi):
    return f"{int(lo*100):>3d}-{int(min(hi,1.0)*100):>3d}%"


def main():
    parser = argparse.ArgumentParser(
        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    src = parser.add_mutually_exclusive_group(required=True)
    src.add_argument("--in", dest="in_path", default="")
    src.add_argument("--run-id", type=int, default=None)
    parser.add_argument("--halluc-types", default="Unclassified Ground Change,Other")
    parser.add_argument("--halluc-conf", type=float, default=0.55)
    args = parser.parse_args()

    regions, source = _load(args)
    n = len(regions)
    if n == 0:
        raise SystemExit(f"No regions in {source}")

    halluc_subs = [s.strip().lower() for s in args.halluc_types.split(",") if s.strip()]

    print(f"Source: {source}")
    print(f"Total regions: {n}\n")

    # By type
    by_type = {}
    for r in regions:
        t = r.get("objectType", "unknown")
        by_type.setdefault(t, []).append(float(r.get("confidence", 0.0)))
    print("By type (count | mean confidence):")
    for t, confs in sorted(by_type.items(), key=lambda kv: -len(kv[1])):
        print(f"  {t:32s} {len(confs):4d} | {sum(confs)/len(confs):.2f}")

    # Confidence histogram
    print("\nConfidence distribution:")
    for lo, hi in _BUCKETS:
        c = sum(1 for r in regions if lo <= float(r.get("confidence", 0.0)) < hi)
        bar = "#" * c
        print(f"  {_bucket_label(lo, hi)}  {c:4d}  {bar}")

    # Area stats
    areas = sorted(float(r.get("area", 0)) for r in regions)
    if areas:
        med = areas[len(areas) // 2]
        print(f"\nArea (px): min={areas[0]:.0f}  median={med:.0f}  max={areas[-1]:.0f}")

    # Hallucination metric
    halluc = [
        r for r in regions
        if any(s in str(r.get("objectType", "")).lower() for s in halluc_subs)
        and float(r.get("confidence", 0.0)) < args.halluc_conf
    ]
    frac = len(halluc) / n
    print(f"\nHallucination metric:")
    print(f"  junk-prone types: {args.halluc_types}")
    print(f"  low-confidence threshold: <{args.halluc_conf}")
    print(f"  likely hallucinations: {len(halluc)}/{n} = {frac*100:.1f}% of all regions")
    print(f"  -> filtering these leaves {n - len(halluc)} confident region(s).")
    if frac > 0.3:
        print("  ** Over 30% of regions are low-confidence junk — strong FP signal;"
              " apply scripts/filter_regions.py before reporting.")


if __name__ == "__main__":
    main()