diff --git a/.gitignore b/.gitignore index 1c0094a..7bfc780 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,8 @@ analysis_out/op/* # Which silent misses another model witnessed (#46). Small, and it carries the # unwitnessed list that is the gallery's actual work queue. !analysis_out/silent_witness.json +!analysis_out/farfield_forensics.json +!analysis_out/silent_activation.json # Generated tagging pages live next to their (committed) galleries; regenerate with # scripts/analysis/make_tagger.py rather than tracking a build artifact. diff --git a/analysis_out/farfield_forensics.json b/analysis_out/farfield_forensics.json new file mode 100644 index 0000000..c9cea5c --- /dev/null +++ b/analysis_out/farfield_forensics.json @@ -0,0 +1,186 @@ +{ + "threshold": 0.3, + "boundary_m": 18.0, + "judgeable_source_px": 30.0, + "match_tolerance": 0.2, + "counts": { + "far_gt": 700, + "far_hits": 453, + "far_silent": 83, + "unwitnessed_far": 46, + "rated": 37, + "below_floor": 9 + }, + "verdicts": { + "visible": 34, + "context-only": 2, + "unclear": 1 + }, + "per_split": { + "richmond": { + "far_silent": 13, + "unwitnessed": 5, + "rated": 4, + "stored_width_min": 4096, + "stored_width_max": 12288 + }, + "bend": { + "far_silent": 14, + "unwitnessed": 9, + "rated": 9, + "stored_width_min": 13312, + "stored_width_max": 16384 + }, + "clovis": { + "far_silent": 9, + "unwitnessed": 4, + "rated": 3, + "stored_width_min": 5760, + "stored_width_max": 5760 + }, + "morgantown": { + "far_silent": 6, + "unwitnessed": 2, + "rated": 1, + "stored_width_min": 4096, + "stored_width_max": 4096 + }, + "annapolis": { + "far_silent": 14, + "unwitnessed": 8, + "rated": 2, + "stored_width_min": 8000, + "stored_width_max": 8000 + }, + "paterson": { + "far_silent": 7, + "unwitnessed": 4, + "rated": 4, + "stored_width_min": 16384, + "stored_width_max": 16384 + }, + "gainesville": { + "far_silent": 20, + "unwitnessed": 14, + "rated": 14, + "stored_width_min": 16384, + "stored_width_max": 16384 + } + }, + "populations": { + "rated (reached the deck)": { + "n": 37, + "dist_q1_med_q3": [ + 23.593050428658366, + 27.600046715707496, + 39.39802964423609 + ], + "px_q1_med_q3": [ + 19.855774091985104, + 28.34337145668962, + 33.157152723882376 + ] + }, + "below the floor (excluded)": { + "n": 9, + "dist_q1_med_q3": [ + 55.415367641067064, + 116.69577442978571, + 150.0 + ], + "px_q1_med_q3": [ + 5.215189175235227, + 6.703570717172544, + 14.116632435829143 + ] + }, + "witnessed (never queued)": { + "n": 37, + "dist_q1_med_q3": [ + 23.35001330569262, + 29.546807172079987, + 36.30290837799241 + ], + "px_q1_med_q3": [ + 21.548642002455033, + 26.47590217546387, + 33.50226683145265 + ] + }, + "ALL far-field silent misses": { + "n": 83, + "dist_q1_med_q3": [ + 23.641568560526633, + 31.24504274559346, + 42.60726425142641 + ], + "px_q1_med_q3": [ + 18.360211340231608, + 25.03687969495929, + 33.08910634599019 + ] + }, + "far-field hits (for contrast)": { + "n": 453, + "dist_q1_med_q3": [ + 20.269464509192268, + 21.346794177516905, + 33.89167310588107 + ], + "px_q1_med_q3": [ + 23.081727887595456, + 36.646175991578325, + 38.59393403957555 + ] + } + }, + "auc_rated_vs_unrated_px": 0.600470035252644, + "rated_median_px_percentile": 0.6204819277108434, + "auc_hit_vs_silent_px": 0.7181706960291497, + "bands": { + "18-25 m": { + "n_gt": 395, + "recall": 0.7772151898734178, + "silent": 25, + "rated": 14, + "visible": 12 + }, + "25-40 m": { + "n_gt": 226, + "recall": 0.5486725663716814, + "silent": 32, + "rated": 14, + "visible": 13 + }, + "40-150 m": { + "n_gt": 72, + "recall": 0.2916666666666667, + "silent": 22, + "rated": 9, + "visible": 9 + }, + "clamp>=150": { + "n_gt": 5, + "recall": 0.2, + "silent": 3, + "rated": 0, + "visible": 0 + } + }, + "matched_size_detection_rate_q1_med_q3": [ + 0.3076923076923077, + 0.5700934579439252, + 0.7402912621359223 + ], + "above_horizon": { + "rated (reached the deck)": 0, + "below the floor (excluded)": 3, + "witnessed (never queued)": 0, + "ALL far-field silent misses": 3, + "far-field hits (for contrast)": 1 + }, + "rated_by_tier": { + "mapillary": 10, + "gsv": 27 + } +} \ No newline at end of file diff --git a/analysis_out/silent_activation.json b/analysis_out/silent_activation.json new file mode 100644 index 0000000..ffdb45c --- /dev/null +++ b/analysis_out/silent_activation.json @@ -0,0 +1,2571 @@ +{ + "threshold": 0.3, + "null_trials": 200, + "null_seed": 20260731, + "n": 128, + "skipped_no_imagery": 0, + "model": "projectsidewalk/rampnet-model", + "tta": false, + "results": [ + { + "city": "annapolis", + "pano": "1040516234061456", + "x": 0.06834458144797188, + "y": 0.5705305826510851, + "field": "near", + "dist_m": 11.1, + "px": 70.5, + "group": "witnessed", + "verdict": null, + "act": 0.43735, + "null_pct": 0.75, + "null_med": 0.09615, + "null_p95": 0.66166, + "above_own_null_p95": false, + "argmax_off_px": 22.0, + "act_at_site": 0.16377, + "nearest_peak_px": 30.0, + "nearest_peak_score": 0.518 + }, + { + "city": "annapolis", + "pano": "1040516234061456", + "x": 0.07834260319860353, + "y": 0.5472727272727272, + "field": "near", + "dist_m": 16.7, + "px": 46.8, + "group": "witnessed", + "verdict": null, + "act": 0.51325, + "null_pct": 0.805, + "null_med": 0.07194, + "null_p95": 0.62719, + "above_own_null_p95": false, + "argmax_off_px": 21.7, + "act_at_site": 0.18835, + "nearest_peak_px": 22.5, + "nearest_peak_score": 0.518 + }, + { + "city": "annapolis", + "pano": "1074557820562424", + "x": 0.3886869118430398, + "y": 0.6736363821318655, + "field": "near", + "dist_m": 4.1, + "px": 189.9, + "group": "witnessed", + "verdict": null, + "act": 0.02373, + "null_pct": 1.0, + "null_med": 0.00237, + "null_p95": 0.02284, + "above_own_null_p95": true, + "argmax_off_px": 22.4, + "act_at_site": 0.01009, + "nearest_peak_px": 41.6, + "nearest_peak_score": 0.097 + }, + { + "city": "annapolis", + "pano": "1074557820562424", + "x": 0.6681082605432581, + "y": 0.5894862321196448, + "field": "near", + "dist_m": 8.7, + "px": 90.4, + "group": "witnessed", + "verdict": null, + "act": 0.03074, + "null_pct": 0.81, + "null_med": 0.01836, + "null_p95": 0.07319, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.00146, + "nearest_peak_px": 37.6, + "nearest_peak_score": 0.065 + }, + { + "city": "annapolis", + "pano": "1074557820562424", + "x": 0.6844444876006155, + "y": 0.566969696969697, + "field": "near", + "dist_m": 11.7, + "px": 66.8, + "group": "witnessed", + "verdict": null, + "act": 0.00593, + "null_pct": 0.39, + "null_med": 0.01071, + "null_p95": 0.07319, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.00262, + "nearest_peak_px": 50.3, + "nearest_peak_score": 0.065 + }, + { + "city": "annapolis", + "pano": "1074557820562424", + "x": 0.7112355588852785, + "y": 0.5416619902731012, + "field": "far", + "dist_m": 19.0, + "px": 41.2, + "group": "witnessed", + "verdict": null, + "act": 0.01281, + "null_pct": 0.505, + "null_med": 0.01246, + "null_p95": 0.06504, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.00209, + "nearest_peak_px": 77.6, + "nearest_peak_score": 0.065 + }, + { + "city": "annapolis", + "pano": "1265191387234427", + "x": 0.45847228033833237, + "y": 0.5232714292347035, + "field": "far", + "dist_m": 34.1, + "px": 22.9, + "group": "witnessed", + "verdict": null, + "act": 0.11185, + "null_pct": 0.875, + "null_med": 0.00197, + "null_p95": 0.72167, + "above_own_null_p95": false, + "argmax_off_px": 22.3, + "act_at_site": 0.04899, + "nearest_peak_px": 31.5, + "nearest_peak_score": 0.166 + }, + { + "city": "annapolis", + "pano": "263456786691932", + "x": 0.5268878662384862, + "y": 0.49995505498717574, + "field": "far", + "dist_m": 150.0, + "px": 5.2, + "group": "below_floor", + "verdict": null, + "act": 0.13246, + "null_pct": 0.9, + "null_med": 0.00153, + "null_p95": 0.49731, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.00845, + "nearest_peak_px": 42.2, + "nearest_peak_score": 0.826 + }, + { + "city": "annapolis", + "pano": "263456786691932", + "x": 0.4183953537314284, + "y": 0.4974932147254313, + "field": "far", + "dist_m": 150.0, + "px": 5.2, + "group": "below_floor", + "verdict": null, + "act": 0.38089, + "null_pct": 0.97, + "null_med": 0.00131, + "null_p95": 0.19137, + "above_own_null_p95": true, + "argmax_off_px": 22.4, + "act_at_site": 0.02107, + "nearest_peak_px": 33.2, + "nearest_peak_score": 0.719 + }, + { + "city": "annapolis", + "pano": "297886629678677", + "x": 0.49330807772549706, + "y": 0.5068181818181818, + "field": "far", + "dist_m": 116.7, + "px": 6.7, + "group": "below_floor", + "verdict": null, + "act": 0.04198, + "null_pct": 0.74, + "null_med": 0.00693, + "null_p95": 0.61309, + "above_own_null_p95": false, + "argmax_off_px": 22.2, + "act_at_site": 0.01469, + "nearest_peak_px": 61.8, + "nearest_peak_score": 0.575 + }, + { + "city": "annapolis", + "pano": "3946684388792708", + "x": 0.47874228458533047, + "y": 0.5073934572348755, + "field": "far", + "dist_m": 107.6, + "px": 7.3, + "group": "witnessed", + "verdict": null, + "act": 0.48864, + "null_pct": 0.935, + "null_med": 0.00194, + "null_p95": 0.59463, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.13056, + "nearest_peak_px": 31.1, + "nearest_peak_score": 0.55 + }, + { + "city": "annapolis", + "pano": "846876249550191", + "x": 0.6507399277472887, + "y": 0.5029791914869305, + "field": "far", + "dist_m": 150.0, + "px": 5.2, + "group": "below_floor", + "verdict": null, + "act": 0.00131, + "null_pct": 0.495, + "null_med": 0.00159, + "null_p95": 0.72933, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.0, + "nearest_peak_px": 118.7, + "nearest_peak_score": 0.086 + }, + { + "city": "annapolis", + "pano": "865533918497811", + "x": 0.5506606758523545, + "y": 0.509002398377811, + "field": "far", + "dist_m": 88.4, + "px": 8.9, + "group": "witnessed", + "verdict": null, + "act": 0.0567, + "null_pct": 0.865, + "null_med": 0.00068, + "null_p95": 0.10336, + "above_own_null_p95": false, + "argmax_off_px": 22.1, + "act_at_site": 0.02945, + "nearest_peak_px": 32.0, + "nearest_peak_score": 0.073 + }, + { + "city": "annapolis", + "pano": "869773754548856", + "x": 0.057741298825384924, + "y": 0.5468686837861031, + "field": "near", + "dist_m": 16.9, + "px": 46.4, + "group": "witnessed", + "verdict": null, + "act": 0.50114, + "null_pct": 0.985, + "null_med": 0.00292, + "null_p95": 0.06203, + "above_own_null_p95": true, + "argmax_off_px": 22.5, + "act_at_site": 0.11586, + "nearest_peak_px": 31.4, + "nearest_peak_score": 0.64 + }, + { + "city": "annapolis", + "pano": "877430540067191", + "x": 0.4602280449077488, + "y": 0.5382795461355264, + "field": "far", + "dist_m": 20.7, + "px": 37.8, + "group": "witnessed", + "verdict": null, + "act": 0.61482, + "null_pct": 0.93, + "null_med": 0.00059, + "null_p95": 0.68466, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.19303, + "nearest_peak_px": 25.1, + "nearest_peak_score": 0.64 + }, + { + "city": "annapolis", + "pano": "877430540067191", + "x": 0.543034271131033, + "y": 0.5143504507599699, + "field": "far", + "dist_m": 55.4, + "px": 14.1, + "group": "below_floor", + "verdict": null, + "act": 0.35867, + "null_pct": 0.91, + "null_med": 0.00093, + "null_p95": 0.64023, + "above_own_null_p95": false, + "argmax_off_px": 22.1, + "act_at_site": 0.13544, + "nearest_peak_px": 33.4, + "nearest_peak_score": 0.685 + }, + { + "city": "annapolis", + "pano": "900958664931957", + "x": 0.45630241240838754, + "y": 0.5082816065558751, + "field": "far", + "dist_m": 96.1, + "px": 8.1, + "group": "below_floor", + "verdict": null, + "act": 0.19369, + "null_pct": 0.765, + "null_med": 0.00145, + "null_p95": 0.64171, + "above_own_null_p95": false, + "argmax_off_px": 22.0, + "act_at_site": 0.03081, + "nearest_peak_px": 40.1, + "nearest_peak_score": 0.907 + }, + { + "city": "annapolis", + "pano": "928118321383655", + "x": 0.9267534093454298, + "y": 0.5339254534343074, + "field": "far", + "dist_m": 23.4, + "px": 33.5, + "group": "rated", + "verdict": "visible", + "act": 0.07621, + "null_pct": 0.815, + "null_med": 0.0, + "null_p95": 0.38994, + "above_own_null_p95": false, + "argmax_off_px": 22.2, + "act_at_site": 0.01318, + "nearest_peak_px": 42.1, + "nearest_peak_score": 0.636 + }, + { + "city": "annapolis", + "pano": "967427853833872", + "x": 0.784888461045941, + "y": 0.5372327375413968, + "field": "far", + "dist_m": 21.3, + "px": 36.8, + "group": "rated", + "verdict": "visible", + "act": 0.10143, + "null_pct": 0.69, + "null_med": 0.00459, + "null_p95": 0.69251, + "above_own_null_p95": false, + "argmax_off_px": 21.9, + "act_at_site": 0.00894, + "nearest_peak_px": 32.9, + "nearest_peak_score": 0.184 + }, + { + "city": "annapolis", + "pano": "967427853833872", + "x": 0.8266666851621686, + "y": 0.5766666666666667, + "field": "near", + "dist_m": 10.2, + "px": 76.9, + "group": "witnessed", + "verdict": null, + "act": 0.59154, + "null_pct": 0.865, + "null_med": 0.01575, + "null_p95": 0.68965, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.07891, + "nearest_peak_px": 25.0, + "nearest_peak_score": 0.65 + }, + { + "city": "annapolis", + "pano": "997534874804455", + "x": 0.573055605680311, + "y": 0.5427722231683144, + "field": "far", + "dist_m": 18.5, + "px": 42.3, + "group": "witnessed", + "verdict": null, + "act": 0.7841, + "null_pct": 1.0, + "null_med": 0.00065, + "null_p95": 0.01352, + "above_own_null_p95": true, + "argmax_off_px": 22.1, + "act_at_site": 0.24718, + "nearest_peak_px": 24.9, + "nearest_peak_score": 0.816 + }, + { + "city": "bend", + "pano": "0SgaioiGd5Yi_5wc6m-nJg", + "x": 0.5369325345142446, + "y": 0.5218091707287849, + "field": "far", + "dist_m": 36.4, + "px": 21.5, + "group": "rated", + "verdict": "visible", + "act": 0.65561, + "null_pct": 1.0, + "null_med": 0.00068, + "null_p95": 0.4966, + "above_own_null_p95": true, + "argmax_off_px": 22.3, + "act_at_site": 0.06793, + "nearest_peak_px": 23.5, + "nearest_peak_score": 0.673 + }, + { + "city": "bend", + "pano": "62TiKmlmxt_xn_68w1lgAw", + "x": 0.6216161554509944, + "y": 0.56, + "field": "near", + "dist_m": 13.1, + "px": 59.7, + "group": "rated", + "verdict": "visible", + "act": 0.19676, + "null_pct": 0.905, + "null_med": 0.00383, + "null_p95": 0.67697, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.0, + "nearest_peak_px": 38.5, + "nearest_peak_score": 0.943 + }, + { + "city": "bend", + "pano": "8KtYwvo9b34f3zm-afipNg", + "x": 0.6496969696969697, + "y": 0.5402020171194366, + "field": "far", + "dist_m": 19.7, + "px": 39.7, + "group": "rated", + "verdict": "visible", + "act": 0.01822, + "null_pct": 0.89, + "null_med": 0.00177, + "null_p95": 0.58302, + "above_own_null_p95": false, + "argmax_off_px": 17.7, + "act_at_site": 0.011, + "nearest_peak_px": 114.0, + "nearest_peak_score": 0.583 + }, + { + "city": "bend", + "pano": "A5AVKFhm7unr9Rs55aQ1Tw", + "x": 0.5327272727272727, + "y": 0.5305050474224668, + "field": "far", + "dist_m": 26.0, + "px": 30.1, + "group": "rated", + "verdict": "visible", + "act": 0.03169, + "null_pct": 0.94, + "null_med": 0.00041, + "null_p95": 0.52899, + "above_own_null_p95": false, + "argmax_off_px": 10.2, + "act_at_site": 0.02331, + "nearest_peak_px": 85.6, + "nearest_peak_score": 0.654 + }, + { + "city": "bend", + "pano": "E15SCY6RTiDuNUu_trZk4Q", + "x": 0.43373736757220643, + "y": 0.5777777746951941, + "field": "near", + "dist_m": 10.0, + "px": 78.0, + "group": "witnessed", + "verdict": null, + "act": 0.77546, + "null_pct": 0.665, + "null_med": 0.58039, + "null_p95": 0.98708, + "above_own_null_p95": false, + "argmax_off_px": 22.2, + "act_at_site": 0.07158, + "nearest_peak_px": 24.2, + "nearest_peak_score": 0.812 + }, + { + "city": "bend", + "pano": "I8Y9zDjNKL1k5HUv6BSyFA", + "x": 0.4805241360875154, + "y": 0.5284267909756982, + "field": "far", + "dist_m": 27.9, + "px": 28.0, + "group": "witnessed", + "verdict": null, + "act": 0.04119, + "null_pct": 0.6, + "null_med": 0.02167, + "null_p95": 0.94633, + "above_own_null_p95": false, + "argmax_off_px": 2.6, + "act_at_site": 0.03581, + "nearest_peak_px": 76.1, + "nearest_peak_score": 0.171 + }, + { + "city": "bend", + "pano": "I8Y9zDjNKL1k5HUv6BSyFA", + "x": 0.06549370841548144, + "y": 0.5253159080351347, + "field": "far", + "dist_m": 31.4, + "px": 24.9, + "group": "witnessed", + "verdict": null, + "act": 0.05574, + "null_pct": 0.65, + "null_med": 0.02482, + "null_p95": 0.94633, + "above_own_null_p95": false, + "argmax_off_px": 22.0, + "act_at_site": 0.01167, + "nearest_peak_px": 31.9, + "nearest_peak_score": 0.065 + }, + { + "city": "bend", + "pano": "NS7lT-itv0JiN6UHVvBxsg", + "x": 0.023434337269176136, + "y": 0.5490909090909091, + "field": "near", + "dist_m": 16.1, + "px": 48.6, + "group": "witnessed", + "verdict": null, + "act": 0.29271, + "null_pct": 0.745, + "null_med": 0.00555, + "null_p95": 0.89445, + "above_own_null_p95": false, + "argmax_off_px": 22.3, + "act_at_site": 0.05392, + "nearest_peak_px": 29.1, + "nearest_peak_score": 0.349 + }, + { + "city": "bend", + "pano": "OedGesfk606bGhg4OQESiQ", + "x": 0.5339577798669883, + "y": 0.5185880969654076, + "field": "far", + "dist_m": 42.8, + "px": 18.3, + "group": "rated", + "verdict": "visible", + "act": 0.54807, + "null_pct": 0.94, + "null_med": 2e-05, + "null_p95": 0.62738, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.03405, + "nearest_peak_px": 23.8, + "nearest_peak_score": 0.101 + }, + { + "city": "bend", + "pano": "PeJlsIXjn8VkEoryRRBBMg", + "x": 0.5296969881924716, + "y": 0.5233333518288352, + "field": "far", + "dist_m": 34.0, + "px": 23.0, + "group": "witnessed", + "verdict": null, + "act": 0.04689, + "null_pct": 0.745, + "null_med": 0.00099, + "null_p95": 0.78178, + "above_own_null_p95": false, + "argmax_off_px": 2.6, + "act_at_site": 0.0447, + "nearest_peak_px": 91.4, + "nearest_peak_score": 0.099 + }, + { + "city": "bend", + "pano": "Tn5W3kvbdH7_Fpk29a_hrQ", + "x": 0.5673504791405647, + "y": 0.538278379142594, + "field": "far", + "dist_m": 20.7, + "px": 37.8, + "group": "witnessed", + "verdict": null, + "act": 0.8038, + "null_pct": 1.0, + "null_med": 0.0, + "null_p95": 0.58952, + "above_own_null_p95": true, + "argmax_off_px": 22.2, + "act_at_site": 0.04501, + "nearest_peak_px": 26.3, + "nearest_peak_score": 0.885 + }, + { + "city": "bend", + "pano": "VIMFndX_oqm7vJJD8gDyEA", + "x": 0.2727272727272727, + "y": 0.6214141383315578, + "field": "near", + "dist_m": 6.2, + "px": 125.5, + "group": "witnessed", + "verdict": null, + "act": 0.3702, + "null_pct": 1.0, + "null_med": 0.00201, + "null_p95": 0.06855, + "above_own_null_p95": true, + "argmax_off_px": 22.4, + "act_at_site": 0.01045, + "nearest_peak_px": 34.4, + "nearest_peak_score": 0.619 + }, + { + "city": "bend", + "pano": "WGdZz7jOkmmmyQYpbw3F8w", + "x": 0.9244565295638657, + "y": 0.5513961301799177, + "field": "near", + "dist_m": 15.3, + "px": 51.0, + "group": "witnessed", + "verdict": null, + "act": 0.05529, + "null_pct": 0.83, + "null_med": 0.00862, + "null_p95": 0.26395, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.00916, + "nearest_peak_px": 40.4, + "nearest_peak_score": 0.189 + }, + { + "city": "bend", + "pano": "X769L3ombUWjLtZDlE2fqg", + "x": 0.41818181818181815, + "y": 0.5462626231800426, + "field": "near", + "dist_m": 17.1, + "px": 45.8, + "group": "witnessed", + "verdict": null, + "act": 0.50922, + "null_pct": 0.97, + "null_med": 0.00084, + "null_p95": 0.48416, + "above_own_null_p95": true, + "argmax_off_px": 22.4, + "act_at_site": 0.02012, + "nearest_peak_px": 33.2, + "nearest_peak_score": 0.978 + }, + { + "city": "bend", + "pano": "bVadNrPq_zXgbJjhhDSwDw", + "x": 0.007272727272727273, + "y": 0.525656562573982, + "field": "far", + "dist_m": 30.9, + "px": 25.3, + "group": "rated", + "verdict": "visible", + "act": 0.79537, + "null_pct": 0.955, + "null_med": 0.00161, + "null_p95": 0.74986, + "above_own_null_p95": true, + "argmax_off_px": 7.5, + "act_at_site": 0.66475, + "nearest_peak_px": 101.8, + "nearest_peak_score": 0.951 + }, + { + "city": "bend", + "pano": "h_1bUHhTLKugR5XhzA1-nA", + "x": 0.03515151515151515, + "y": 0.5208080777254972, + "field": "far", + "dist_m": 38.2, + "px": 20.5, + "group": "rated", + "verdict": "visible", + "act": 0.02126, + "null_pct": 0.9, + "null_med": 0.00034, + "null_p95": 0.93052, + "above_own_null_p95": false, + "argmax_off_px": 7.1, + "act_at_site": 0.01241, + "nearest_peak_px": 66.3, + "nearest_peak_score": 0.931 + }, + { + "city": "bend", + "pano": "mdQY8aMSdYrnHfHNXqj6GQ", + "x": 0.47636365485913823, + "y": 0.5322222160570549, + "field": "far", + "dist_m": 24.6, + "px": 31.8, + "group": "rated", + "verdict": "visible", + "act": 0.01836, + "null_pct": 0.935, + "null_med": 0.00081, + "null_p95": 0.02377, + "above_own_null_p95": false, + "argmax_off_px": 5.5, + "act_at_site": 0.01557, + "nearest_peak_px": null, + "nearest_peak_score": null + }, + { + "city": "bend", + "pano": "qNYRurrxEhqZvO8GrwE9nA", + "x": 0.05818181818181818, + "y": 0.5353535322709517, + "field": "far", + "dist_m": 22.4, + "px": 34.9, + "group": "rated", + "verdict": "visible", + "act": 0.82096, + "null_pct": 0.995, + "null_med": 0.00065, + "null_p95": 0.5015, + "above_own_null_p95": true, + "argmax_off_px": 22.3, + "act_at_site": 0.10248, + "nearest_peak_px": 26.0, + "nearest_peak_score": 1.009 + }, + { + "city": "bend", + "pano": "rQtHh0Wai-D1FvbvqBlWdw", + "x": 0.012121212121212121, + "y": 0.5620201989376183, + "field": "near", + "dist_m": 12.7, + "px": 61.8, + "group": "witnessed", + "verdict": null, + "act": 0.84289, + "null_pct": 1.0, + "null_med": 0.00344, + "null_p95": 0.48298, + "above_own_null_p95": true, + "argmax_off_px": 12.8, + "act_at_site": 0.65267, + "nearest_peak_px": 127.7, + "nearest_peak_score": 0.395 + }, + { + "city": "bend", + "pano": "sUMfdZmScVLBGMjvZ0MjCA", + "x": 0.05757575757575758, + "y": 0.5268686837861032, + "field": "far", + "dist_m": 29.5, + "px": 26.5, + "group": "witnessed", + "verdict": null, + "act": 0.57787, + "null_pct": 0.93, + "null_med": 0.00035, + "null_p95": 0.63499, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.02106, + "nearest_peak_px": 24.8, + "nearest_peak_score": 0.626 + }, + { + "city": "bend", + "pano": "xKoseQiPNzFwaWfw9Rl6hg", + "x": 0.9183838815400095, + "y": 0.5435353966915246, + "field": "far", + "dist_m": 18.2, + "px": 43.1, + "group": "rated", + "verdict": "visible", + "act": 0.5538, + "null_pct": 0.99, + "null_med": 0.00212, + "null_p95": 0.37167, + "above_own_null_p95": true, + "argmax_off_px": 22.3, + "act_at_site": 0.00749, + "nearest_peak_px": 31.1, + "nearest_peak_score": 0.718 + }, + { + "city": "clovis", + "pano": "1197447770701938", + "x": 0.18847373570874265, + "y": 0.5232198555538492, + "field": "far", + "dist_m": 34.2, + "px": 22.9, + "group": "rated", + "verdict": "visible", + "act": 0.01759, + "null_pct": 0.825, + "null_med": 0.00329, + "null_p95": 0.47612, + "above_own_null_p95": false, + "argmax_off_px": 13.5, + "act_at_site": 0.01128, + "nearest_peak_px": 119.5, + "nearest_peak_score": 0.865 + }, + { + "city": "clovis", + "pano": "170392751596494", + "x": 0.5034343372691762, + "y": 0.6679798103101325, + "field": "near", + "dist_m": 4.3, + "px": 182.4, + "group": "rated", + "verdict": "definition", + "act": 0.00903, + "null_pct": 0.52, + "null_med": 0.00703, + "null_p95": 0.22006, + "above_own_null_p95": false, + "argmax_off_px": 16.6, + "act_at_site": 0.0, + "nearest_peak_px": 98.4, + "nearest_peak_score": 0.598 + }, + { + "city": "clovis", + "pano": "170392751596494", + "x": 0.1819896265401673, + "y": 0.5323912532511264, + "field": "far", + "dist_m": 24.5, + "px": 32.0, + "group": "witnessed", + "verdict": null, + "act": 0.04457, + "null_pct": 0.855, + "null_med": 0.00366, + "null_p95": 0.45153, + "above_own_null_p95": false, + "argmax_off_px": 2.5, + "act_at_site": 0.04081, + "nearest_peak_px": 170.1, + "nearest_peak_score": 0.821 + }, + { + "city": "clovis", + "pano": "2493443314123605", + "x": 0.26485329169322935, + "y": 0.52684806088005, + "field": "far", + "dist_m": 29.6, + "px": 26.5, + "group": "rated", + "verdict": "context-only", + "act": 0.20188, + "null_pct": 0.965, + "null_med": 0.00078, + "null_p95": 0.01867, + "above_own_null_p95": true, + "argmax_off_px": 22.4, + "act_at_site": 0.00061, + "nearest_peak_px": 36.4, + "nearest_peak_score": 0.704 + }, + { + "city": "clovis", + "pano": "2652019851757090", + "x": 0.7288364699667899, + "y": 0.5275368230970484, + "field": "far", + "dist_m": 28.8, + "px": 27.1, + "group": "witnessed", + "verdict": null, + "act": 0.51119, + "null_pct": 0.995, + "null_med": 0.00158, + "null_p95": 0.23289, + "above_own_null_p95": true, + "argmax_off_px": 22.4, + "act_at_site": 0.06502, + "nearest_peak_px": 27.2, + "nearest_peak_score": 0.571 + }, + { + "city": "clovis", + "pano": "308441850838672", + "x": 0.2837870519216822, + "y": 0.5241603733501503, + "field": "far", + "dist_m": 32.9, + "px": 23.8, + "group": "witnessed", + "verdict": null, + "act": 0.18835, + "null_pct": 0.905, + "null_med": 0.00312, + "null_p95": 0.49247, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.08307, + "nearest_peak_px": 36.9, + "nearest_peak_score": 0.412 + }, + { + "city": "clovis", + "pano": "490105898988611", + "x": 0.7212718231654784, + "y": 0.5165137807332276, + "field": "far", + "dist_m": 48.1, + "px": 16.2, + "group": "below_floor", + "verdict": null, + "act": 0.03129, + "null_pct": 0.69, + "null_med": 0.01351, + "null_p95": 0.124, + "above_own_null_p95": false, + "argmax_off_px": 8.8, + "act_at_site": 0.02599, + "nearest_peak_px": 119.4, + "nearest_peak_score": 0.332 + }, + { + "city": "clovis", + "pano": "506669223705998", + "x": 0.4197979736328125, + "y": 0.7164646587949811, + "field": "near", + "dist_m": 3.1, + "px": 253.1, + "group": "rated", + "verdict": "visible", + "act": 0.00587, + "null_pct": 0.865, + "null_med": 0.00139, + "null_p95": 0.00991, + "above_own_null_p95": false, + "argmax_off_px": 7.3, + "act_at_site": 0.00234, + "nearest_peak_px": 67.9, + "nearest_peak_score": 0.067 + }, + { + "city": "clovis", + "pano": "517195446066836", + "x": 0.7853786777937632, + "y": 0.5312450987720753, + "field": "far", + "dist_m": 25.4, + "px": 30.8, + "group": "witnessed", + "verdict": null, + "act": 0.04203, + "null_pct": 0.785, + "null_med": 0.00449, + "null_p95": 0.84255, + "above_own_null_p95": false, + "argmax_off_px": 7.4, + "act_at_site": 0.02923, + "nearest_peak_px": 98.5, + "nearest_peak_score": 0.388 + }, + { + "city": "clovis", + "pano": "744451279581577", + "x": 0.8218503168644709, + "y": 0.5419524444382605, + "field": "far", + "dist_m": 18.9, + "px": 41.5, + "group": "rated", + "verdict": "context-only", + "act": 0.02171, + "null_pct": 0.78, + "null_med": 0.003, + "null_p95": 0.39018, + "above_own_null_p95": false, + "argmax_off_px": 2.1, + "act_at_site": 0.02074, + "nearest_peak_px": 82.7, + "nearest_peak_score": 0.201 + }, + { + "city": "clovis", + "pano": "995842280952828", + "x": 0.8439816441860816, + "y": 0.5412045611449773, + "field": "far", + "dist_m": 19.2, + "px": 40.7, + "group": "witnessed", + "verdict": null, + "act": 0.16884, + "null_pct": 0.835, + "null_med": 0.01262, + "null_p95": 0.70261, + "above_own_null_p95": false, + "argmax_off_px": 21.8, + "act_at_site": 0.04023, + "nearest_peak_px": 27.8, + "nearest_peak_score": 0.193 + }, + { + "city": "gainesville", + "pano": "82y6sIvPm-_shG58klusgw", + "x": 0.9838377058871877, + "y": 0.529215300788914, + "field": "far", + "dist_m": 27.2, + "px": 28.8, + "group": "witnessed", + "verdict": null, + "act": 0.7172, + "null_pct": 1.0, + "null_med": 0.02369, + "null_p95": 0.08254, + "above_own_null_p95": true, + "argmax_off_px": 22.0, + "act_at_site": 0.14758, + "nearest_peak_px": 23.4, + "nearest_peak_score": 0.761 + }, + { + "city": "gainesville", + "pano": "BtYzecFoq3-H5qaGzBh4iw", + "x": 0.5141463694807856, + "y": 0.5136182314475036, + "field": "far", + "dist_m": 58.4, + "px": 13.4, + "group": "witnessed", + "verdict": null, + "act": 0.66627, + "null_pct": 1.0, + "null_med": 0.00142, + "null_p95": 0.00317, + "above_own_null_p95": true, + "argmax_off_px": 22.4, + "act_at_site": 0.06909, + "nearest_peak_px": 23.8, + "nearest_peak_score": 0.688 + }, + { + "city": "gainesville", + "pano": "DXKg24FIKMrGUX6ilHn5Ew", + "x": 0.04402622544390468, + "y": 0.5304555636627928, + "field": "far", + "dist_m": 26.0, + "px": 30.0, + "group": "rated", + "verdict": "visible", + "act": 0.02989, + "null_pct": 0.85, + "null_med": 0.0, + "null_p95": 0.34754, + "above_own_null_p95": false, + "argmax_off_px": 6.8, + "act_at_site": 0.02565, + "nearest_peak_px": 91.1, + "nearest_peak_score": 0.28 + }, + { + "city": "gainesville", + "pano": "GRTC3zOCFSc8QMIrAtwtzw", + "x": 0.5538633036587365, + "y": 0.5287539011201986, + "field": "far", + "dist_m": 27.6, + "px": 28.3, + "group": "rated", + "verdict": "visible", + "act": 0.71013, + "null_pct": 0.92, + "null_med": 0.00504, + "null_p95": 0.71175, + "above_own_null_p95": false, + "argmax_off_px": 22.1, + "act_at_site": 0.11245, + "nearest_peak_px": 24.9, + "nearest_peak_score": 0.814 + }, + { + "city": "gainesville", + "pano": "HoOapW5g53qbEFjLA7yv3Q", + "x": 0.5438323641216953, + "y": 0.5186555832260843, + "field": "far", + "dist_m": 42.6, + "px": 18.4, + "group": "rated", + "verdict": "visible", + "act": 0.01008, + "null_pct": 0.425, + "null_med": 0.01636, + "null_p95": 0.77996, + "above_own_null_p95": false, + "argmax_off_px": 22.2, + "act_at_site": 0.00919, + "nearest_peak_px": 98.4, + "nearest_peak_score": 0.94 + }, + { + "city": "gainesville", + "pano": "HoOapW5g53qbEFjLA7yv3Q", + "x": 0.49645185038970213, + "y": 0.5153353168595021, + "field": "far", + "dist_m": 51.9, + "px": 15.1, + "group": "rated", + "verdict": "visible", + "act": 0.02859, + "null_pct": 0.695, + "null_med": 0.01124, + "null_p95": 0.69386, + "above_own_null_p95": false, + "argmax_off_px": 22.3, + "act_at_site": 4e-05, + "nearest_peak_px": 52.0, + "nearest_peak_score": 0.94 + }, + { + "city": "gainesville", + "pano": "KEwb17BzIjKVur60MpItuA", + "x": 0.4149449639405539, + "y": 0.5201712934675728, + "field": "far", + "dist_m": 39.4, + "px": 19.9, + "group": "rated", + "verdict": "visible", + "act": 0.00993, + "null_pct": 0.455, + "null_med": 0.01613, + "null_p95": 0.97489, + "above_own_null_p95": false, + "argmax_off_px": 22.0, + "act_at_site": 0.00286, + "nearest_peak_px": 77.5, + "nearest_peak_score": 0.975 + }, + { + "city": "gainesville", + "pano": "KEwb17BzIjKVur60MpItuA", + "x": 0.5282188547813629, + "y": 0.5106295616442303, + "field": "far", + "dist_m": 74.8, + "px": 10.5, + "group": "rated", + "verdict": "visible", + "act": 0.34073, + "null_pct": 0.885, + "null_med": 0.00752, + "null_p95": 0.65191, + "above_own_null_p95": false, + "argmax_off_px": 22.3, + "act_at_site": 0.02027, + "nearest_peak_px": 37.6, + "nearest_peak_score": 0.881 + }, + { + "city": "gainesville", + "pano": "KEwb17BzIjKVur60MpItuA", + "x": 0.06060247094985196, + "y": 0.5317774160151664, + "field": "far", + "dist_m": 25.0, + "px": 31.3, + "group": "rated", + "verdict": "visible", + "act": 0.00068, + "null_pct": 0.12, + "null_med": 0.01494, + "null_p95": 0.88099, + "above_own_null_p95": false, + "argmax_off_px": 14.4, + "act_at_site": 0.0, + "nearest_peak_px": 93.7, + "nearest_peak_score": 0.387 + }, + { + "city": "gainesville", + "pano": "KbZFzQai9wfXijoQDscWFw", + "x": 0.5275994119413667, + "y": 0.5188418251833842, + "field": "far", + "dist_m": 42.2, + "px": 18.5, + "group": "rated", + "verdict": "visible", + "act": 0.40851, + "null_pct": 0.895, + "null_med": 0.00695, + "null_p95": 0.61287, + "above_own_null_p95": false, + "argmax_off_px": 21.9, + "act_at_site": 0.04844, + "nearest_peak_px": 30.8, + "nearest_peak_score": 0.467 + }, + { + "city": "gainesville", + "pano": "Mm5gyUYqmJmjeGopC1IgKg", + "x": 0.51082045775724, + "y": 0.5301554461174719, + "field": "far", + "dist_m": 26.3, + "px": 29.7, + "group": "rated", + "verdict": "visible", + "act": 0.16738, + "null_pct": 0.87, + "null_med": 0.0004, + "null_p95": 0.92031, + "above_own_null_p95": false, + "argmax_off_px": 22.2, + "act_at_site": 0.01692, + "nearest_peak_px": 27.9, + "nearest_peak_score": 0.235 + }, + { + "city": "gainesville", + "pano": "PSS2iz32Sj0nE2ILJ6i8rA", + "x": 0.3940450819064303, + "y": 0.5218858587521047, + "field": "far", + "dist_m": 36.3, + "px": 21.5, + "group": "witnessed", + "verdict": null, + "act": 0.2538, + "null_pct": 0.785, + "null_med": 0.00652, + "null_p95": 0.95693, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.10707, + "nearest_peak_px": 23.5, + "nearest_peak_score": 0.26 + }, + { + "city": "gainesville", + "pano": "XuX3LCIaNmEI9FSBenjToA", + "x": 0.3716333464134779, + "y": 0.5417326217955608, + "field": "far", + "dist_m": 19.0, + "px": 41.3, + "group": "rated", + "verdict": "visible", + "act": 0.03549, + "null_pct": 0.685, + "null_med": 0.00299, + "null_p95": 0.39622, + "above_own_null_p95": false, + "argmax_off_px": 8.9, + "act_at_site": 0.0314, + "nearest_peak_px": 170.1, + "nearest_peak_score": 0.465 + }, + { + "city": "gainesville", + "pano": "_2GSllZzac7Zfd02VoVgCA", + "x": 0.07699117603437579, + "y": 0.5336038037736293, + "field": "far", + "dist_m": 23.6, + "px": 33.2, + "group": "rated", + "verdict": "visible", + "act": 0.04277, + "null_pct": 0.875, + "null_med": 0.00096, + "null_p95": 0.5174, + "above_own_null_p95": false, + "argmax_off_px": 13.2, + "act_at_site": 0.0384, + "nearest_peak_px": 43.9, + "nearest_peak_score": 0.118 + }, + { + "city": "gainesville", + "pano": "amZHKZbJ7pOJ9kvr2VlK5A", + "x": 0.8177778209339489, + "y": 0.7714141660748106, + "field": "near", + "dist_m": 2.2, + "px": 358.1, + "group": "witnessed", + "verdict": null, + "act": 0.04526, + "null_pct": 0.92, + "null_med": 0.00107, + "null_p95": 0.18638, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.00097, + "nearest_peak_px": 43.2, + "nearest_peak_score": 0.232 + }, + { + "city": "gainesville", + "pano": "dVrNCxUTFAfeyVNMXQ1LTw", + "x": 0.9172647622999139, + "y": 0.5348749208434239, + "field": "far", + "dist_m": 22.7, + "px": 34.4, + "group": "rated", + "verdict": "unclear", + "act": 0.03819, + "null_pct": 0.555, + "null_med": 0.0266, + "null_p95": 0.48771, + "above_own_null_p95": false, + "argmax_off_px": 8.8, + "act_at_site": 0.02392, + "nearest_peak_px": 168.3, + "nearest_peak_score": 0.446 + }, + { + "city": "gainesville", + "pano": "ipoVm9spHhQGCMROwSdmmA", + "x": 0.8437018555235514, + "y": 0.5851702349969968, + "field": "near", + "dist_m": 9.1, + "px": 85.8, + "group": "rated", + "verdict": "unclear", + "act": 0.00637, + "null_pct": 0.445, + "null_med": 0.00782, + "null_p95": 0.33869, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.0, + "nearest_peak_px": 62.0, + "nearest_peak_score": 0.874 + }, + { + "city": "gainesville", + "pano": "jL3wxV95A0lO2xKUQukpIg", + "x": 0.42924051720987244, + "y": 0.5374236192304312, + "field": "far", + "dist_m": 21.2, + "px": 37.0, + "group": "witnessed", + "verdict": null, + "act": 0.2477, + "null_pct": 0.915, + "null_med": 0.00081, + "null_p95": 0.69921, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.05639, + "nearest_peak_px": 27.5, + "nearest_peak_score": 0.254 + }, + { + "city": "gainesville", + "pano": "kozsgHrge4ofusqK2D_Hlw", + "x": 0.5380980504469067, + "y": 0.5128683014061106, + "field": "far", + "dist_m": 61.8, + "px": 12.7, + "group": "rated", + "verdict": "visible", + "act": 0.0995, + "null_pct": 0.91, + "null_med": 0.00249, + "null_p95": 0.21676, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.0059, + "nearest_peak_px": 45.7, + "nearest_peak_score": 0.737 + }, + { + "city": "gainesville", + "pano": "ld_LKRyg3hte-YCeFY3avQ", + "x": 0.9792845532492789, + "y": 0.5339509336319833, + "field": "far", + "dist_m": 23.4, + "px": 33.5, + "group": "witnessed", + "verdict": null, + "act": 0.0649, + "null_pct": 0.935, + "null_med": 0.0014, + "null_p95": 0.49211, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.03981, + "nearest_peak_px": 48.7, + "nearest_peak_score": 0.754 + }, + { + "city": "gainesville", + "pano": "q2yX7pwQF7_3SFGxEqlDHQ", + "x": 0.5433173942511713, + "y": 0.5106520927609703, + "field": "far", + "dist_m": 74.7, + "px": 10.5, + "group": "witnessed", + "verdict": null, + "act": 0.01476, + "null_pct": 0.685, + "null_med": 0.0029, + "null_p95": 0.7242, + "above_own_null_p95": false, + "argmax_off_px": 16.5, + "act_at_site": 0.00856, + "nearest_peak_px": 201.8, + "nearest_peak_score": 0.812 + }, + { + "city": "gainesville", + "pano": "qAP6awcZVDK19stGxM31jQ", + "x": 0.37373736757220644, + "y": 0.7051515151515152, + "field": "near", + "dist_m": 3.3, + "px": 235.2, + "group": "witnessed", + "verdict": null, + "act": 0.03248, + "null_pct": 1.0, + "null_med": 0.0, + "null_p95": 0.00858, + "above_own_null_p95": true, + "argmax_off_px": 17.7, + "act_at_site": 0.0129, + "nearest_peak_px": 203.8, + "nearest_peak_score": 0.148 + }, + { + "city": "gainesville", + "pano": "vbW2W3-AE2tr1NJYHCuHgw", + "x": 0.7385858524206913, + "y": 0.6914141845703125, + "field": "near", + "dist_m": 3.6, + "px": 214.7, + "group": "witnessed", + "verdict": null, + "act": 0.00489, + "null_pct": 0.87, + "null_med": 0.0, + "null_p95": 0.0349, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.0, + "nearest_peak_px": 59.0, + "nearest_peak_score": 0.092 + }, + { + "city": "gainesville", + "pano": "wwbkgmoKv4MS5sMy3PGhqA", + "x": 0.5989353478897853, + "y": 0.5208345420613185, + "field": "far", + "dist_m": 38.1, + "px": 20.5, + "group": "rated", + "verdict": "visible", + "act": 0.01115, + "null_pct": 0.79, + "null_med": 0.00551, + "null_p95": 0.01803, + "above_own_null_p95": false, + "argmax_off_px": 1.4, + "act_at_site": 0.01075, + "nearest_peak_px": 62.3, + "nearest_peak_score": 0.616 + }, + { + "city": "morgantown", + "pano": "1045092623940908", + "x": 0.6476767615116004, + "y": 0.637272764263731, + "field": "near", + "dist_m": 5.4, + "px": 144.0, + "group": "witnessed", + "verdict": null, + "act": 0.02084, + "null_pct": 0.66, + "null_med": 0.00886, + "null_p95": 0.13928, + "above_own_null_p95": false, + "argmax_off_px": 15.6, + "act_at_site": 0.01231, + "nearest_peak_px": 101.4, + "nearest_peak_score": 0.062 + }, + { + "city": "morgantown", + "pano": "1061541095152683", + "x": 0.10989899606415719, + "y": 0.5685858524206913, + "field": "near", + "dist_m": 11.4, + "px": 68.5, + "group": "witnessed", + "verdict": null, + "act": 0.08295, + "null_pct": 0.745, + "null_med": 0.01619, + "null_p95": 0.70041, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.02479, + "nearest_peak_px": 32.3, + "nearest_peak_score": 0.148 + }, + { + "city": "morgantown", + "pano": "1061541095152683", + "x": 0.019393939393939394, + "y": 0.5378788063742898, + "field": "far", + "dist_m": 20.9, + "px": 37.4, + "group": "witnessed", + "verdict": null, + "act": 0.59163, + "null_pct": 1.0, + "null_med": 0.0151, + "null_p95": 0.42915, + "above_own_null_p95": true, + "argmax_off_px": 21.9, + "act_at_site": 0.31872, + "nearest_peak_px": 31.9, + "nearest_peak_score": 0.61 + }, + { + "city": "morgantown", + "pano": "1159131791921689", + "x": 0.2137373721960819, + "y": 0.6720201896898674, + "field": "near", + "dist_m": 4.2, + "px": 187.7, + "group": "rated", + "verdict": "unclear", + "act": 0.09927, + "null_pct": 0.99, + "null_med": 0.00426, + "null_p95": 0.07942, + "above_own_null_p95": true, + "argmax_off_px": 22.5, + "act_at_site": 0.00911, + "nearest_peak_px": 33.9, + "nearest_peak_score": 0.117 + }, + { + "city": "morgantown", + "pano": "1314106923314560", + "x": 0.21137609272522312, + "y": 0.5872850842920392, + "field": "near", + "dist_m": 8.9, + "px": 88.0, + "group": "rated", + "verdict": "visible", + "act": 0.00541, + "null_pct": 0.465, + "null_med": 0.0068, + "null_p95": 0.94761, + "above_own_null_p95": false, + "argmax_off_px": 9.1, + "act_at_site": 0.00268, + "nearest_peak_px": 107.9, + "nearest_peak_score": 0.177 + }, + { + "city": "morgantown", + "pano": "1413692656166618", + "x": 0.6690909322102865, + "y": 0.6211110987807765, + "field": "near", + "dist_m": 6.3, + "px": 125.2, + "group": "witnessed", + "verdict": null, + "act": 0.02416, + "null_pct": 0.525, + "null_med": 0.0177, + "null_p95": 0.89464, + "above_own_null_p95": false, + "argmax_off_px": 6.6, + "act_at_site": 0.01795, + "nearest_peak_px": 238.9, + "nearest_peak_score": 0.87 + }, + { + "city": "morgantown", + "pano": "1475722139747056", + "x": 0.017508421618529037, + "y": 0.5198316847675978, + "field": "far", + "dist_m": 40.1, + "px": 19.5, + "group": "witnessed", + "verdict": null, + "act": 0.07888, + "null_pct": 0.885, + "null_med": 0.00121, + "null_p95": 0.37287, + "above_own_null_p95": false, + "argmax_off_px": 18.0, + "act_at_site": 0.022, + "nearest_peak_px": 46.8, + "nearest_peak_score": 0.871 + }, + { + "city": "morgantown", + "pano": "1560632374758055", + "x": 0.014141420306581439, + "y": 0.6687879157788825, + "field": "near", + "dist_m": 4.3, + "px": 183.5, + "group": "rated", + "verdict": "visible", + "act": 0.75602, + "null_pct": 1.0, + "null_med": 0.00117, + "null_p95": 0.0436, + "above_own_null_p95": true, + "argmax_off_px": 18.6, + "act_at_site": 0.19581, + "nearest_peak_px": 163.1, + "nearest_peak_score": 0.938 + }, + { + "city": "morgantown", + "pano": "272487405737151", + "x": 0.652121230616714, + "y": 0.7067676890980114, + "field": "near", + "dist_m": 3.3, + "px": 237.7, + "group": "witnessed", + "verdict": null, + "act": 0.02667, + "null_pct": 0.91, + "null_med": 0.00186, + "null_p95": 0.03559, + "above_own_null_p95": false, + "argmax_off_px": 19.0, + "act_at_site": 0.01583, + "nearest_peak_px": 149.6, + "nearest_peak_score": 0.674 + }, + { + "city": "morgantown", + "pano": "307295108505220", + "x": 0.33939395788944127, + "y": 0.6679798103101325, + "field": "near", + "dist_m": 4.3, + "px": 182.4, + "group": "rated", + "verdict": "visible", + "act": 0.13628, + "null_pct": 0.99, + "null_med": 0.00831, + "null_p95": 0.10338, + "above_own_null_p95": true, + "argmax_off_px": 22.1, + "act_at_site": 0.06337, + "nearest_peak_px": 32.1, + "nearest_peak_score": 0.16 + }, + { + "city": "morgantown", + "pano": "307295108505220", + "x": 0.7777778209339489, + "y": 0.623535341205019, + "field": "near", + "dist_m": 6.1, + "px": 127.9, + "group": "witnessed", + "verdict": null, + "act": 0.04, + "null_pct": 0.59, + "null_med": 0.02187, + "null_p95": 0.29731, + "above_own_null_p95": false, + "argmax_off_px": 17.1, + "act_at_site": 0.02551, + "nearest_peak_px": 143.3, + "nearest_peak_score": 0.107 + }, + { + "city": "morgantown", + "pano": "356422300574178", + "x": 0.1517460980720612, + "y": 0.5434221681507451, + "field": "far", + "dist_m": 18.2, + "px": 43.0, + "group": "witnessed", + "verdict": null, + "act": 0.02629, + "null_pct": 0.42, + "null_med": 0.11222, + "null_p95": 0.88157, + "above_own_null_p95": false, + "argmax_off_px": 10.2, + "act_at_site": 0.02426, + "nearest_peak_px": 79.5, + "nearest_peak_score": 0.338 + }, + { + "city": "morgantown", + "pano": "356422300574178", + "x": 0.09898990515506628, + "y": 0.5483838815400095, + "field": "near", + "dist_m": 16.3, + "px": 47.9, + "group": "rated", + "verdict": "definition", + "act": 0.307, + "null_pct": 0.815, + "null_med": 0.0385, + "null_p95": 0.81614, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.01305, + "nearest_peak_px": 25.5, + "nearest_peak_score": 0.338 + }, + { + "city": "morgantown", + "pano": "390232193697883", + "x": 0.4667830239590223, + "y": 0.5324315816214311, + "field": "far", + "dist_m": 24.5, + "px": 32.0, + "group": "rated", + "verdict": "visible", + "act": 0.0094, + "null_pct": 0.65, + "null_med": 0.00158, + "null_p95": 0.58822, + "above_own_null_p95": false, + "argmax_off_px": 10.3, + "act_at_site": 0.00366, + "nearest_peak_px": 53.2, + "nearest_peak_score": 0.195 + }, + { + "city": "morgantown", + "pano": "413429987710546", + "x": 0.9138790294879624, + "y": 0.528191989328837, + "field": "far", + "dist_m": 28.2, + "px": 27.8, + "group": "witnessed", + "verdict": null, + "act": 0.01013, + "null_pct": 0.31, + "null_med": 0.02036, + "null_p95": 0.28096, + "above_own_null_p95": false, + "argmax_off_px": 13.0, + "act_at_site": 0.00425, + "nearest_peak_px": 109.3, + "nearest_peak_score": 0.854 + }, + { + "city": "morgantown", + "pano": "559825463037840", + "x": 0.469440809267355, + "y": 0.5187997158948636, + "field": "far", + "dist_m": 42.3, + "px": 18.5, + "group": "below_floor", + "verdict": null, + "act": 0.59538, + "null_pct": 1.0, + "null_med": 0.00145, + "null_p95": 0.34272, + "above_own_null_p95": true, + "argmax_off_px": 21.8, + "act_at_site": 0.0929, + "nearest_peak_px": 23.6, + "nearest_peak_score": 0.619 + }, + { + "city": "morgantown", + "pano": "716002773841007", + "x": 0.7119106513357678, + "y": 0.5478255162205575, + "field": "near", + "dist_m": 16.5, + "px": 47.4, + "group": "rated", + "verdict": "unclear", + "act": 0.04758, + "null_pct": 0.735, + "null_med": 0.00188, + "null_p95": 0.65085, + "above_own_null_p95": false, + "argmax_off_px": 5.4, + "act_at_site": 0.04533, + "nearest_peak_px": 65.0, + "nearest_peak_score": 0.202 + }, + { + "city": "morgantown", + "pano": "730248625779882", + "x": 0.12202020818536932, + "y": 0.5839394309303977, + "field": "near", + "dist_m": 9.3, + "px": 84.5, + "group": "witnessed", + "verdict": null, + "act": 0.04757, + "null_pct": 0.845, + "null_med": 0.00222, + "null_p95": 0.86664, + "above_own_null_p95": false, + "argmax_off_px": 0.9, + "act_at_site": 0.04639, + "nearest_peak_px": 147.7, + "nearest_peak_score": 0.961 + }, + { + "city": "morgantown", + "pano": "7393628424017132", + "x": 0.6549494887843277, + "y": 0.6720202451763732, + "field": "near", + "dist_m": 4.2, + "px": 187.7, + "group": "witnessed", + "verdict": null, + "act": 0.21051, + "null_pct": 1.0, + "null_med": 0.00071, + "null_p95": 0.0364, + "above_own_null_p95": true, + "argmax_off_px": 22.5, + "act_at_site": 0.01909, + "nearest_peak_px": 37.2, + "nearest_peak_score": 0.794 + }, + { + "city": "morgantown", + "pano": "775966364379699", + "x": 0.7951515336470171, + "y": 0.6922222345525568, + "field": "near", + "dist_m": 3.6, + "px": 215.9, + "group": "witnessed", + "verdict": null, + "act": 0.17681, + "null_pct": 1.0, + "null_med": 0.00294, + "null_p95": 0.16715, + "above_own_null_p95": true, + "argmax_off_px": 22.5, + "act_at_site": 0.06538, + "nearest_peak_px": 27.6, + "nearest_peak_score": 0.221 + }, + { + "city": "paterson", + "pano": "0Drku25sOlOlWGiVf7uetw", + "x": 0.4169921875, + "y": 0.568359375, + "field": "near", + "dist_m": 11.5, + "px": 68.3, + "group": "witnessed", + "verdict": null, + "act": 0.85164, + "null_pct": 0.795, + "null_med": 0.01799, + "null_p95": 0.89765, + "above_own_null_p95": false, + "argmax_off_px": 22.1, + "act_at_site": 0.64725, + "nearest_peak_px": 24.0, + "nearest_peak_score": 0.855 + }, + { + "city": "paterson", + "pano": "3BTAarqIRrxbpFHUFJhtSQ", + "x": 0.4417136387718572, + "y": 0.5472501048800457, + "field": "near", + "dist_m": 16.7, + "px": 46.8, + "group": "witnessed", + "verdict": null, + "act": 0.79798, + "null_pct": 0.935, + "null_med": 0.00479, + "null_p95": 0.87761, + "above_own_null_p95": false, + "argmax_off_px": 21.9, + "act_at_site": 0.51615, + "nearest_peak_px": 22.9, + "nearest_peak_score": 0.799 + }, + { + "city": "paterson", + "pano": "UOMaPWpm2CRHbfqMgY_hbQ", + "x": 0.9690277847985136, + "y": 0.5280885169135339, + "field": "far", + "dist_m": 28.3, + "px": 27.7, + "group": "witnessed", + "verdict": null, + "act": 0.02564, + "null_pct": 0.835, + "null_med": 0.00099, + "null_p95": 0.87846, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.00382, + "nearest_peak_px": 54.0, + "nearest_peak_score": 0.833 + }, + { + "city": "paterson", + "pano": "WNiUHjkafGBEnMc0nc4wkQ", + "x": 0.04029821423614809, + "y": 0.5180162161163532, + "field": "far", + "dist_m": 44.1, + "px": 17.7, + "group": "rated", + "verdict": "visible", + "act": 0.21378, + "null_pct": 0.87, + "null_med": 0.00076, + "null_p95": 0.92191, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.0505, + "nearest_peak_px": 36.4, + "nearest_peak_score": 0.965 + }, + { + "city": "paterson", + "pano": "Xs3lDDc1o50wJmW9eGx_WA", + "x": 0.2323375734540243, + "y": 0.5969844818988245, + "field": "near", + "dist_m": 7.9, + "px": 98.4, + "group": "rated", + "verdict": "context-only", + "act": 0.77821, + "null_pct": 0.915, + "null_med": 0.00485, + "null_p95": 0.8825, + "above_own_null_p95": false, + "argmax_off_px": 22.3, + "act_at_site": 0.02851, + "nearest_peak_px": 30.6, + "nearest_peak_score": 0.904 + }, + { + "city": "paterson", + "pano": "Z929dNP7sDp8cU6sjIeKAw", + "x": 0.3301010039358428, + "y": 0.583939393939394, + "field": "near", + "dist_m": 9.3, + "px": 84.5, + "group": "witnessed", + "verdict": null, + "act": 0.81958, + "null_pct": 0.96, + "null_med": 0.00293, + "null_p95": 0.79713, + "above_own_null_p95": true, + "argmax_off_px": 22.4, + "act_at_site": 0.4664, + "nearest_peak_px": 27.2, + "nearest_peak_score": 0.957 + }, + { + "city": "paterson", + "pano": "Z929dNP7sDp8cU6sjIeKAw", + "x": 0.6488888827237216, + "y": 0.5887879157788826, + "field": "near", + "dist_m": 8.7, + "px": 89.6, + "group": "witnessed", + "verdict": null, + "act": 0.81925, + "null_pct": 0.91, + "null_med": 0.00293, + "null_p95": 0.9567, + "above_own_null_p95": false, + "argmax_off_px": 22.2, + "act_at_site": 0.38388, + "nearest_peak_px": 25.3, + "nearest_peak_score": 0.927 + }, + { + "city": "paterson", + "pano": "_FfdhRGlkbTABymQR3t1_A", + "x": 0.534812514379721, + "y": 0.5261813014280383, + "field": "far", + "dist_m": 30.3, + "px": 25.8, + "group": "rated", + "verdict": "visible", + "act": 0.44835, + "null_pct": 0.79, + "null_med": 0.00213, + "null_p95": 0.87199, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.1239, + "nearest_peak_px": 23.4, + "nearest_peak_score": 0.459 + }, + { + "city": "paterson", + "pano": "cuFkPrv4UsqzeJCdibl1FQ", + "x": 0.4699032749323206, + "y": 0.5301038638689053, + "field": "far", + "dist_m": 26.4, + "px": 29.7, + "group": "rated", + "verdict": "visible", + "act": 0.15272, + "null_pct": 0.915, + "null_med": 0.00236, + "null_p95": 0.21229, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.09947, + "nearest_peak_px": 37.4, + "nearest_peak_score": 0.212 + }, + { + "city": "paterson", + "pano": "gZ3Acgi38PrzOVBkW-Il4Q", + "x": 0.9907032660546297, + "y": 0.5340394347088994, + "field": "far", + "dist_m": 23.3, + "px": 33.6, + "group": "witnessed", + "verdict": null, + "act": 0.76016, + "null_pct": 1.0, + "null_med": 0.0, + "null_p95": 0.09112, + "above_own_null_p95": true, + "argmax_off_px": 5.7, + "act_at_site": 0.71407, + "nearest_peak_px": 104.0, + "nearest_peak_score": 0.902 + }, + { + "city": "paterson", + "pano": "hV6LwTvkCzyX_vQF3ZG5qQ", + "x": 0.21933867134549537, + "y": 0.5496919988638095, + "field": "near", + "dist_m": 15.9, + "px": 49.2, + "group": "rated", + "verdict": "visible", + "act": 0.04318, + "null_pct": 0.735, + "null_med": 0.01158, + "null_p95": 0.8787, + "above_own_null_p95": false, + "argmax_off_px": 4.9, + "act_at_site": 0.03762, + "nearest_peak_px": 51.5, + "nearest_peak_score": 0.879 + }, + { + "city": "paterson", + "pano": "hV6LwTvkCzyX_vQF3ZG5qQ", + "x": 0.495205520930847, + "y": 0.519235968880955, + "field": "far", + "dist_m": 41.3, + "px": 18.9, + "group": "rated", + "verdict": "visible", + "act": 0.03976, + "null_pct": 0.83, + "null_med": 0.00875, + "null_p95": 0.57709, + "above_own_null_p95": false, + "argmax_off_px": 21.9, + "act_at_site": 0.02798, + "nearest_peak_px": 31.8, + "nearest_peak_score": 0.054 + }, + { + "city": "paterson", + "pano": "ijlwBkjXDfD2wCO_jSjCGA", + "x": 0.05030611525018135, + "y": 0.5254146881445788, + "field": "far", + "dist_m": 31.2, + "px": 25.0, + "group": "witnessed", + "verdict": null, + "act": 0.79011, + "null_pct": 0.95, + "null_med": 7e-05, + "null_p95": 0.82639, + "above_own_null_p95": false, + "argmax_off_px": 22.3, + "act_at_site": 0.23676, + "nearest_peak_px": 25.5, + "nearest_peak_score": 0.906 + }, + { + "city": "paterson", + "pano": "o5_tCsE_7cR4lTvyrqy8mg", + "x": 0.43838388154000946, + "y": 0.5693939393939393, + "field": "near", + "dist_m": 11.3, + "px": 69.3, + "group": "witnessed", + "verdict": null, + "act": 0.83803, + "null_pct": 0.97, + "null_med": 0.00579, + "null_p95": 0.78774, + "above_own_null_p95": true, + "argmax_off_px": 21.9, + "act_at_site": 0.31219, + "nearest_peak_px": 28.9, + "nearest_peak_score": 0.86 + }, + { + "city": "richmond", + "pano": "1023400645666420", + "x": 0.2294949433297822, + "y": 0.5919191857540246, + "field": "near", + "dist_m": 8.4, + "px": 93.0, + "group": "witnessed", + "verdict": null, + "act": 0.23686, + "null_pct": 0.915, + "null_med": 0.01908, + "null_p95": 0.35074, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.01796, + "nearest_peak_px": 24.3, + "nearest_peak_score": 0.242 + }, + { + "city": "richmond", + "pano": "1130501775077894", + "x": 0.7943434281782671, + "y": 0.6032323293974905, + "field": "near", + "dist_m": 7.4, + "px": 105.2, + "group": "witnessed", + "verdict": null, + "act": 0.13406, + "null_pct": 0.44, + "null_med": 0.18214, + "null_p95": 0.96163, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.01453, + "nearest_peak_px": 35.0, + "nearest_peak_score": 0.363 + }, + { + "city": "richmond", + "pano": "1263975528308076", + "x": 0.9712878880356297, + "y": 0.5335353504527698, + "field": "far", + "dist_m": 23.6, + "px": 33.1, + "group": "rated", + "verdict": "visible", + "act": 0.52351, + "null_pct": 0.72, + "null_med": 0.01795, + "null_p95": 0.95893, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.07301, + "nearest_peak_px": 33.1, + "nearest_peak_score": 0.894 + }, + { + "city": "richmond", + "pano": "1288163659019705", + "x": 0.2791919130267519, + "y": 0.6537373675722065, + "field": "near", + "dist_m": 4.8, + "px": 164.1, + "group": "rated", + "verdict": "visible", + "act": 0.12626, + "null_pct": 0.86, + "null_med": 0.00255, + "null_p95": 0.36649, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.01046, + "nearest_peak_px": 42.9, + "nearest_peak_score": 0.512 + }, + { + "city": "richmond", + "pano": "1338631107629110", + "x": 0.5325252463600852, + "y": 0.4973737312085701, + "field": "far", + "dist_m": 150.0, + "px": 5.2, + "group": "below_floor", + "verdict": null, + "act": 0.7454, + "null_pct": 0.99, + "null_med": 0.00262, + "null_p95": 0.40661, + "above_own_null_p95": true, + "argmax_off_px": 22.4, + "act_at_site": 0.2072, + "nearest_peak_px": 29.0, + "nearest_peak_score": 0.875 + }, + { + "city": "richmond", + "pano": "1759178911495771", + "x": 0.5197303339935238, + "y": 0.5219953845879944, + "field": "far", + "dist_m": 36.1, + "px": 21.7, + "group": "rated", + "verdict": "visible", + "act": 0.18581, + "null_pct": 0.855, + "null_med": 0.00299, + "null_p95": 0.45947, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.03628, + "nearest_peak_px": 39.5, + "nearest_peak_score": 0.693 + }, + { + "city": "richmond", + "pano": "372149922554566", + "x": 0.03474748091264204, + "y": 0.5273737312085701, + "field": "far", + "dist_m": 29.0, + "px": 27.0, + "group": "witnessed", + "verdict": null, + "act": 0.60787, + "null_pct": 0.935, + "null_med": 0.00026, + "null_p95": 0.66182, + "above_own_null_p95": false, + "argmax_off_px": 22.2, + "act_at_site": 0.08643, + "nearest_peak_px": 26.8, + "nearest_peak_score": 0.648 + }, + { + "city": "richmond", + "pano": "386363531219281", + "x": 0.4905050936612216, + "y": 0.5144444876006156, + "field": "far", + "dist_m": 55.1, + "px": 14.2, + "group": "rated", + "verdict": "visible", + "act": 0.05344, + "null_pct": 0.765, + "null_med": 0.00616, + "null_p95": 0.71793, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.04411, + "nearest_peak_px": 51.3, + "nearest_peak_score": 0.847 + }, + { + "city": "richmond", + "pano": "3956582084664486", + "x": 0.46505054820667613, + "y": 0.5192929169625947, + "field": "far", + "dist_m": 41.2, + "px": 19.0, + "group": "witnessed", + "verdict": null, + "act": 0.82894, + "null_pct": 0.985, + "null_med": 0.00038, + "null_p95": 0.42644, + "above_own_null_p95": true, + "argmax_off_px": 21.7, + "act_at_site": 0.33381, + "nearest_peak_px": 26.2, + "nearest_peak_score": 0.891 + }, + { + "city": "richmond", + "pano": "423989957171190", + "x": 0.42667711943494924, + "y": 0.5250655456146334, + "field": "far", + "dist_m": 31.7, + "px": 24.7, + "group": "witnessed", + "verdict": null, + "act": 0.03764, + "null_pct": 0.855, + "null_med": 0.00513, + "null_p95": 0.85755, + "above_own_null_p95": false, + "argmax_off_px": 2.1, + "act_at_site": 0.03594, + "nearest_peak_px": 57.4, + "nearest_peak_score": 0.858 + }, + { + "city": "richmond", + "pano": "423989957171190", + "x": 0.5133691152905883, + "y": 0.5247863607532633, + "field": "far", + "dist_m": 32.0, + "px": 24.4, + "group": "witnessed", + "verdict": null, + "act": 0.73132, + "null_pct": 0.9, + "null_med": 0.00532, + "null_p95": 0.85773, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.11268, + "nearest_peak_px": 30.9, + "nearest_peak_score": 0.861 + }, + { + "city": "richmond", + "pano": "423989957171190", + "x": 0.5291509811918936, + "y": 0.5252428801194406, + "field": "far", + "dist_m": 31.5, + "px": 24.9, + "group": "witnessed", + "verdict": null, + "act": 0.86011, + "null_pct": 1.0, + "null_med": 0.00649, + "null_p95": 0.85773, + "above_own_null_p95": true, + "argmax_off_px": 21.7, + "act_at_site": 0.29615, + "nearest_peak_px": 22.7, + "nearest_peak_score": 0.861 + }, + { + "city": "richmond", + "pano": "593421160518191", + "x": 0.7702458544241675, + "y": 0.5173858634506956, + "field": "far", + "dist_m": 45.7, + "px": 17.1, + "group": "witnessed", + "verdict": null, + "act": 0.01255, + "null_pct": 0.115, + "null_med": 0.0757, + "null_p95": 0.90758, + "above_own_null_p95": false, + "argmax_off_px": 10.2, + "act_at_site": 0.00951, + "nearest_peak_px": 70.4, + "nearest_peak_score": 0.339 + }, + { + "city": "richmond", + "pano": "639645829096010", + "x": 0.23492423780036692, + "y": 0.5502974155481681, + "field": "near", + "dist_m": 15.7, + "px": 49.9, + "group": "witnessed", + "verdict": null, + "act": 0.02843, + "null_pct": 0.475, + "null_med": 0.05058, + "null_p95": 0.84704, + "above_own_null_p95": false, + "argmax_off_px": 10.7, + "act_at_site": 0.02378, + "nearest_peak_px": 42.5, + "nearest_peak_score": 0.143 + }, + { + "city": "richmond", + "pano": "662385816800591", + "x": 0.44464645848129736, + "y": 0.5070707009055397, + "field": "far", + "dist_m": 112.5, + "px": 7.0, + "group": "witnessed", + "verdict": null, + "act": 0.14685, + "null_pct": 0.88, + "null_med": 0.00949, + "null_p95": 0.60457, + "above_own_null_p95": false, + "argmax_off_px": 22.3, + "act_at_site": 0.00867, + "nearest_peak_px": 44.1, + "nearest_peak_score": 0.835 + }, + { + "city": "richmond", + "pano": "662385816800591", + "x": 0.5236363821318656, + "y": 0.5209091094045928, + "field": "far", + "dist_m": 38.0, + "px": 20.6, + "group": "witnessed", + "verdict": null, + "act": 0.42569, + "null_pct": 0.91, + "null_med": 0.01023, + "null_p95": 0.82634, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.21966, + "nearest_peak_px": 36.0, + "nearest_peak_score": 0.605 + }, + { + "city": "richmond", + "pano": "723487737079243", + "x": 0.006868693033854166, + "y": 0.5435353966915246, + "field": "far", + "dist_m": 18.2, + "px": 43.1, + "group": "rated", + "verdict": "visible", + "act": 0.94587, + "null_pct": 0.965, + "null_med": 0.01808, + "null_p95": 0.945, + "above_own_null_p95": true, + "argmax_off_px": 7.4, + "act_at_site": 0.77932, + "nearest_peak_px": 117.4, + "nearest_peak_score": 0.945 + } + ] +} \ No newline at end of file diff --git a/docs/curb_ramp_data_sourcing.md b/docs/curb_ramp_data_sourcing.md index 0289546..0bf3b76 100644 --- a/docs/curb_ramp_data_sourcing.md +++ b/docs/curb_ramp_data_sourcing.md @@ -98,6 +98,11 @@ benchmark keeps surfacing (Paterson's paired tactile surfaces, Gainesville's dia ramps) plausibly are fixable that way. Those are two different populations with two different programmes attached, and nobody had sized them. +> **Since qualified by §0c.** The split below stands as a measurement, but its "fixable by" +> column's hard binary does not: the model detects other far-field ramps of the *same apparent +> size* as its silent misses at a median 57% rate, so far-field failure is graded sensitivity, +> not a floor. Read the far/near boundary as a difficulty gradient, not a reachability partition. + Script: `scripts/analysis/miss_decomposition.py` (15 tests). Reads the committed low-floor caches, so no GPU, no network, no imagery. Threshold 0.30 (the #79 recommendation); boundary 18 m, the last distance at which the model still has adequate signal. @@ -287,8 +292,9 @@ what the gallery is for. - **`silent` is still an upper bound**, now with a floor under it. It means the cached detections witness nothing there. Occlusion, deep shadow, debris and GT disagreement all still live inside - the unwitnessed remainder, and separating them needs the imagery — that is #46's gallery half, - **the crops are built but the reviewer pass is not done**. + the unwitnessed remainder, and separating them needs the imagery — that is #46's gallery half. + **The reviewer pass is now done** (one rater, no second — `docs/replication.md` §"What the first + pass produced"), and its far-field verdicts raised their own question: **§0c**. - **The witness test is one-directional.** A witnessed ramp is confirmed recognizable; an unwitnessed one is not confirmed *un*recognizable, since every challenger is weaker than RampNet on this task and may simply have missed it too. @@ -320,7 +326,10 @@ sourcing-addressable: | `unclear` | cannot tell from this imagery | excluded from every rate | **The `visible` rate over those 50, applied to the 59, is what converts the bracket into a point -estimate.** Until it is run, quote the bracket. +estimate.** It has been run (2026-07-31, one rater): near-field `visible` 7 of 13, which puts the +sourcing-addressable population at **~0.013 recall points** (~19 chance-corrected witnessed + 7 +visible, against 2,060 pooled GT — `docs/replication.md`). Single-rater caveat applies, and the +**far-field** verdicts from the same pass raised the question §0c takes up. - **Some of `merged` may be double-marked GT.** 24 of 124 pairs sit below 8 px (~25 cm at 10 m), which is not a physical spacing for two ramps; on the verdict splits that is plausibly one ramp marked twice. If so they are *spurious GT* and leave the population entirely rather than changing @@ -330,6 +339,166 @@ estimate.** Until it is run, quote the bracket. - **`sub_threshold` is not free recall.** Those ramps are recoverable by lowering the threshold, which #54/#55 already evaluated and priced in precision; 0.30 was chosen knowing it. +## 0c. The far-field `visible` anomaly: the pixel floor does not survive its own hits + +The reviewer pass produced a result §0a's framing did not predict. Of the **37 far-field** +silent-miss crops: **34 `visible`, 2 `context-only`, 1 `unclear`** — a 94% visible rate over +rateable crops, with **zero** `occluded` and **zero** `lighting` verdicts. Three facts sharpen it: + +- the rubric licenses `visible` only on the **model-resolution panel** (`benchmark/RUBRICS.md`), + so this is not the reviewer spending the 4× stored pixels the model never received; +- every rated crop is **unwitnessed** — none of the 8 challenger models put anything in radius + either; +- the deepest crops (40–150 m, down to **10.5 model px**) were rated visible **9 of 9**. + +At face value: ramps resolvable at the model's own pixel budget, invisible to all eight models — +against the reading that far-field misses are pixel-starved and unreachable by any training-side +fix. The four-hypothesis study design is on #46 (2026-07-31); this section is **Phase 0**: check +the *sample* (the rated 37 passed two selection filters) and check the framing against the model's +own far-field behaviour, before the verdicts are allowed to mean anything. + +Script: `scripts/analysis/farfield_forensics.py` (21 tests); result JSON +`analysis_out/farfield_forensics.json`. Committed inputs only — the low-floor caches, the witness +list, the gallery manifest and verdicts, and the imagery manifests' `width` fields. No GPU, no +network, no imagery. + +### The sample: survivorship is real, mild, and now quantified + +The 83 far-field silent misses reduce to 37 rated through two filters — **witnessed** (37, +already explained by another model's detection) and the **30-source-pixel judgeability floor** +(9). The floor is not one floor: stored panoramas run 4096–16384 px wide while `geom()` sizes +ramps at the model's 4096-px input, so 30 source px is a different model-pixel cut per split: + +| split | tier | stored px | floor (model px) | far-silent | unwitnessed | rated | +| :--- | :--- | ---: | ---: | ---: | ---: | ---: | +| richmond | mapillary | 4096–12288 | 10.0–30.0 | 13 | 5 | 4 | +| bend | gsv | 13312–16384 | 7.5–9.2 | 14 | 9 | 9 | +| clovis | mapillary | 5760 | 21.3 | 9 | 4 | 3 | +| morgantown | mapillary | 4096 | 30.0 | 6 | 2 | 1 | +| annapolis | mapillary | 8000 | 15.4 | 14 | 8 | 2 | +| paterson | gsv | 16384 | 7.5 | 7 | 4 | 4 | +| gainesville | gsv | 16384 | 7.5 | 20 | 14 | 14 | + +Which split a miss happened in decides whether a reviewer ever saw it — the 16384-px GSV splits +admit far misses down to 7.5 model px while morgantown stops at 30, and the deck comes out +**27 GSV / 10 Mapillary**. + +| population | n | dist q1/med/q3 (m) | px q1/med/q3 | +| :--- | ---: | :---: | :---: | +| rated (reached the deck) | 37 | 23.6 / 27.6 / 39.4 | 19.9 / 28.3 / 33.2 | +| below the floor (excluded) | 9 | 55.4 / 116.7 / 150.0 | 5.2 / 6.7 / 14.1 | +| witnessed (never queued) | 37 | 23.4 / 29.5 / 36.3 | 21.5 / 26.5 / 33.5 | +| **all far-field silent misses** | **83** | 23.6 / 31.2 / 42.6 | 18.4 / 25.0 / 33.1 | +| far-field hits, for contrast | 453 | 20.3 / 21.3 / 33.9 | 23.1 / 36.6 / 38.6 | + +- **AUC(rated px vs unrated far-silent px) = 0.600**; the rated median sits at the **62nd + percentile** of the far-silent size distribution. A bias toward bigger/closer exists and is mild. +- The 9 excluded items are the **extreme tail** — median 117 m, three of them above-horizon clamps + (i.e. not distances at all). So the 94% generalizes to the far-silent *core* (~18–50 m); it says + nothing about the deep tail, which is exactly where pixel starvation is most plausible. +- Mis-binning guards: **zero** above-horizon clamps among the rated 37, and the deck is majority + GSV — the tier where flat-ground distance is trustworthy (Spearman 0.95 vs 0.81). + +### The framing: the model's own hits refute a hard pixel floor + +The decisive check needs no reviewer at all. If far-field silence were pixel-starvation, the model +should not be detecting *other* ramps at the same apparent size. It is: + +| band | GT | recall | silent misses | rated | rated `visible` | +| :--- | ---: | ---: | ---: | ---: | ---: | +| 18–25 m | 395 | 0.777 | 25 | 14 | 12 | +| 25–40 m | 226 | 0.549 | 32 | 14 | 13 | +| 40–150 m | 72 | 0.292 | 22 | 9 | 9 | +| clamp ≥ 150 m | 5 | 0.200 | 3 | 0 | 0 | + +- **Matched-size detection rate**: for each `visible` miss, the model's recall over all far-field + GT within ±20% of that miss's apparent size is **median 0.57** (q1 0.31, q3 0.74). A hard pixel + floor would put these near zero. +- **AUC(far-hit px vs far-silent px) = 0.718** — size matters, but it is far from deciding. +- Recall declines **0.777 → 0.549 → 0.292** across the bands. Even at 40–150 m the model finds + roughly 3 in 10 (and the pooled 25–40 m rate agrees with E1's gold-set 0.49 at the same range). + +**Far-field failure is graded sensitivity, not a cliff.** A silent far-field miss is not a ramp +below a physical detection floor — it is the unlucky tail of a process that succeeds on most +same-sized ramps. That is consistent with `docs/detection_recall_analysis.md`'s sensitivity +finding and with the human verdicts, and inconsistent with reading "more examples do not add +pixels" as a claim about *reachability*. (As a claim about pixels it remains true; the error was +inferring unreachability from it.) + +### Phase 1: attenuated or absent? Almost never absent + +`silent` is a statement about **peaks** — no `peak_local_max` peak ≥ 0.05 within the match radius. +Phase 1 makes the statement about the **heatmap**: `scripts/analysis/silent_activation.py` +(14 tests) loads the published checkpoint (`projectsidewalk/rampnet-model` — the weights every +committed cache came from), runs one pass per panorama holding a silent miss (single-pass fp32, +matching `op_cache`), and reads the max heatmap value inside the match radius. The scaled matcher +space *is* the 512×1024 heatmap grid, so the window is exactly the matcher's. Result JSON: +`analysis_out/silent_activation.json`; run on the local RTX 3070, all 128 pooled silent misses. + +| population | n | act q1 / med / q3 | act ≥ 0.01 | +| :--- | ---: | :---: | ---: | +| near / rated | 13 | 0.009 / 0.099 / 0.197 | 9 | +| near / witnessed | 32 | 0.033 / 0.211 / 0.592 | 30 | +| far / rated | 37 | 0.022 / 0.076 / 0.409 | 34 | +| far / below-floor | 9 | 0.042 / 0.194 / 0.381 | 8 | +| far / witnessed | 37 | 0.045 / 0.188 / 0.615 | 37 | +| **all silent misses** | **128** | 0.032 / 0.136 / 0.548 | **118** | + +What that in-window mass *is* (classes are act ranges; the offset and nearest-peak columns +confirm the intended reading rather than define it): + +| class | definition | n | near / far | rated `visible` | argmax offset med | nearest floor peak med | +| :--- | :--- | ---: | :---: | ---: | ---: | ---: | +| **absent** | act < 0.01 | **10** | 6 / 4 | 5 | 22.0 px | 77.5 px (3.4 R) | +| **faint local** | 0.01 ≤ act < 0.05 | 39 | 12 / 27 | 13 | **10.2 px** | 85.6 px (3.8 R) | +| **tail** | act ≥ 0.05 | 79 | 27 / 52 | 23 | 22.3 px | **31.1 px (1.4 R)** | + +- **Only 10 of 128 silent misses (8%) have a genuinely flat heatmap.** "Silent = the model saw + nothing" is wrong for 92% of the bucket; `silent` was peak bookkeeping, not absence of response. +- **62% are a neighbouring mode's tail.** The argmax sits in the window's outer quarter in 75 of + 79, and the nearest cached floor peak is ~1.4 R away with **median score 0.685** — a *confident* + adjacent detection (70/79 within 2 R). That mode is a neighbour ramp's TP, an FP, or plausibly + this very ramp localized just outside the radius — the `localization` bucket only inspects + *kept* (≥ 0.30) annulus peaks, so a floor-level one leaves a miss "silent". Whichever it is, + this is the σ/representation family again (`merged`'s mechanism), not vocabulary. +- **30% are a faint local response at the site itself** (mass on-site in 30 of 39, nothing else + within ~3.8 R) — the `sub_threshold` continuum extending below the floor. Attenuation, not + blindness. +- For the far-field rated-`visible` population — the anomaly itself — the split is **3 absent / + 12 faint-local / 19 tail**: the model is responding at or next to ~91% of the far ramps a human + called resolvable. Consistent with Phase 0's graded-sensitivity reading; squarely against a + vocabulary hole. +- The strict per-pano null (azimuth-randomized at the site's elevation, self-excluding within 2 R) + passes 31/128 at its p95 — a deliberately hard bar, since the p95 is set by the pano's strongest + modes; the decomposition above is the sharper lens. + +### What changes, what does not, and what is still open + +- **§0a's measured split stands** (247 far / 180 near at 18 m). What falls is the hard binary in + its "fixable by" column: the far field is *harder*, not *unreachable*. +- **The sourcing bracket (§0b) excluded all 83 far-field silent misses from the addressable + population because of that binary.** That exclusion is no longer safe — but Phase 1 cuts the + other way too: of the 45 *near-field* silent misses the 0.013 estimate rests on, only **6 are + heatmap-absent**; the rest are faint-local (12) or an adjacent confident mode (27), i.e. the + calibration and σ families §0b already prices separately. The 0.013 point estimate is + deliberately **not revised** in either direction until Phase 2 (the scale counterfactual, whose + primary target is now the 10 absent sites plus whether scale lifts faint-local over the floor) + and Phase 3 (the decoy control on the verdicts) run. Quote 0.013 with this section attached. +- **Multi-view's remedy logic is untouched** — a ramp invisible at 30 m is at 8 m two panoramas + later whatever the failure mechanism — but §0a's "MV ceiling" column shares the binary + assumption and will move with the same phases. +- **The human-side caveat is live.** One rater; and the 9-of-9 `visible` rate in the deepest band + (down to 10.5 model px) is where pointed-verification bias would show most strongly. Phase 3's + decoy deck should therefore be **stratified by distance band**, oversampling 40–150 m. + +**The takeaway.** "Are far ramps harder?" — yes, threefold (recall 0.777 → 0.292 across the +bands), but Phases 0–1 show distance acting as a **stressor on failure families this taxonomy +already prices, not as a new category of failure**: 62% the σ/representation family, 30% the +`sub_threshold` continuum, 8% genuine absence. The implied lever is therefore decoder- and +representation-side — target σ, peak spacing, threshold calibration, and Phase 2's scale question +for the residual — **not far-field training vocabulary**; and multi-view remains the one remedy +that sidesteps all three mechanisms at once, by re-presenting the same ramp near-field. + ## 1. The current training corpus is mostly one city Stage 1 is built from three cities' open-government inventories (`docs/data_provenance.md` §1). diff --git a/scripts/analysis/farfield_forensics.py b/scripts/analysis/farfield_forensics.py new file mode 100644 index 0000000..7b843da --- /dev/null +++ b/scripts/analysis/farfield_forensics.py @@ -0,0 +1,382 @@ +"""Phase 0 of the far-field `visible` anomaly study: is the rated sample representative? (#46) + +The reviewer pass over the silent misses (``benchmark/miss_taxonomy_46/silent__jonf.json``) +rated **34 of 36** rateable far-field crops ``visible`` — the ramp's own pixels present +and carrying its appearance *in the model-resolution panel*. At face value that +contradicts the pixel-starvation framing E1 attached to the far field +(``docs/curb_ramp_data_sourcing.md`` §0a: "a 1.2 m ramp at 30 m is ~25 px — more +examples do not add pixels"), which is the assumption the 18 m far/near split — and +through it the sourcing bracket and the multi-view sizing — stands on. + +Before that contradiction is allowed to mean anything, the sample has to be checked +(#46, hypothesis H1): the 37 rated far-field crops are not a random draw from the 83 +far-field silent misses. They passed two filters — **unwitnessed** (no other model +detected anything there either) and the **30-source-pixel judgeability floor** — and +the floor bites at a different apparent size on every split, because the stored +panoramas range from 4096 to 16384 px wide while ``geom()`` sizes ramps at the model's +4096-px input. On a 16384-px split the floor admits ramps down to **7.5 model px**; on +morgantown it stops at **30**. If the rated crops are the biggest and closest of the +far field, a 94% visible rate there says little about the population. + +This measures that, from committed data alone: + +* **the two filters' effect on the sample**, split by split — which populations could + even reach the deck, and where the rated 37 sit in the far-silent size distribution; +* **whether apparent size discriminates far-field hits from far-field silent misses** + at all — if the model detects other ramps of the *same* apparent size at a healthy + rate, a hard pixel floor cannot be what makes these particular ramps silent; +* **the mis-binning guards**: above-horizon clamps (``geom()`` sends y <= 0.5 straight + to 150 m) and the GSV/Mapillary tier split, since the flat-ground distance estimate + is weaker on Mapillary rigs (Spearman 0.81 vs 0.95, §0a). + +Everything is read from committed files (``analysis_out/op_cache``, +``analysis_out/silent_witness.json``, ``benchmark/miss_taxonomy_46/``, +``benchmark//imagery_manifest.json``): no GPU, no network, no imagery. + + python scripts/analysis/farfield_forensics.py + python scripts/analysis/farfield_forensics.py --json-out analysis_out/farfield_forensics.json +""" +import argparse +import json +import os +import sys + +REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +OUT = os.environ.get("RAMPNET_ANALYSIS_OUT", os.path.join(REPO, "analysis_out")) +sys.path.insert(0, REPO) +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import miss_taxonomy as mt # noqa: E402 +from miss_decomposition import ( # noqa: E402 + DEFAULT_THRESHOLD, FAR_BOUNDARY_M, TIER, US_SPLITS) +from miss_gallery import ( # noqa: E402 + JUDGEABLE_SOURCE_PX, MODEL_WIDTH, load_queue, source_px, tag_key) + +# Distance bands inside the far field, in metres. The first two match E1's gold-set +# bins (recall 0.90 at 18-25, 0.49 at 25-40), so the pooled-benchmark rates here are +# directly comparable to the numbers the pixel-starvation framing was built on. +# ``geom()`` clamps above-horizon points to 150 m, so the clamp is its own band — +# those distances are not measurements, they are the estimator giving up. +FAR_BANDS = ((18.0, 25.0), (25.0, 40.0), (40.0, 150.0)) + +# Tolerance for "same apparent size" when asking how often the model detects OTHER +# far-field ramps of a rated miss's size. +/-20% of the target px is ~ +/-20% of +# distance (px is 1/d), comfortably inside one of E1's bins. +MATCH_TOL = 0.20 + + +# --------------------------------------------------------------------------- # +# Pure core (no I/O) — unit-tested in tests/test_farfield_forensics.py +# --------------------------------------------------------------------------- # +def auc(a, b): + """P(a random draw from ``a`` exceeds one from ``b``), ties counting half. + + The Mann-Whitney statistic scaled to [0, 1]: 0.5 means the two samples are + indistinguishable on this variable, 1.0 means every ``a`` exceeds every ``b``. + O(n*m); the populations here are dozens-to-hundreds, so clarity wins. + """ + if not a or not b: + return float("nan") + wins = ties = 0 + for x in a: + for y in b: + if x > y: + wins += 1 + elif x == y: + ties += 1 + return (wins + 0.5 * ties) / (len(a) * len(b)) + + +def quartiles(values): + """``(q1, median, q3)`` by simple index — enough for reporting, no interpolation.""" + if not values: + return (float("nan"),) * 3 + v = sorted(values) + n = len(v) + return v[n // 4], v[n // 2], v[(3 * n) // 4] + + +def effective_floor_model_px(source_width, floor=JUDGEABLE_SOURCE_PX, + model_width=MODEL_WIDTH): + """The judgeability floor translated into MODEL pixels for one stored width. + + ``source_px = model_px * source_width / model_width``, so a floor fixed in + source pixels admits smaller model-pixel ramps the wider the stored pano is. + This asymmetry is what shapes the rated deck's composition across splits. + """ + return floor * model_width / source_width + + +def band_of(row, bands=FAR_BANDS): + """The far band a row falls in, ``'clamp'`` for above-horizon rows, else None. + + The clamp test is on ``y``, mirroring ``miss_decomposition.above_horizon`` — + a ground ramp cannot sit at or above the horizon, so its 150 m is an artifact + of an unleveled rig or a hill, not a distance. + """ + if row["y"] <= 0.5: + return "clamp" + for lo, hi in bands: + if lo <= row["dist"] < hi: + return (lo, hi) + return None + + +def matched_rate(rows, px, tol=MATCH_TOL): + """``(hits, n)`` among ``rows`` whose apparent size is within ``tol`` of ``px``. + + The counterfactual the floor question needs: of every far-field GT ramp the + benchmark scores at (about) this apparent size, how many did the model find? + """ + sel = [r for r in rows if abs(r["px"] - px) <= tol * px] + return sum(1 for r in sel if r["hit"]), len(sel) + + +def percentile_rank(population, x): + """Fraction of ``population`` strictly below ``x``, ties counting half.""" + if not population: + return float("nan") + below = sum(1 for v in population if v < x) + ties = sum(1 for v in population if v == x) + return (below + 0.5 * ties) / len(population) + + +def row_key(row): + """The identity a row shares with the witness list and the gallery manifest.""" + return (row["city"], row["pano"], round(float(row["x"]), 6), + round(float(row["y"]), 6)) + + +# --------------------------------------------------------------------------- # +# I/O +# --------------------------------------------------------------------------- # +def stored_widths(city): + """``{pano_id: width}`` from the committed imagery manifest, or ``{}``. + + The manifest was committed to pin the imagery by content hash (#94); its + ``width`` field is what lets this script translate the source-pixel floor + into model pixels without touching a single image. + """ + path = os.path.join(REPO, "benchmark", city, "imagery_manifest.json") + if not os.path.exists(path): + return {} + with open(path, encoding="utf-8") as fh: + payload = json.load(fh) + return {pid: rec["width"] for pid, rec in payload.get("panos", {}).items() + if rec.get("width")} + + +def load_rated(gallery_dir, field="far"): + """The reviewer's rated items: manifest entry + verdict, keyed like the tags. + + ``field`` restricts to one distance population; ``None`` returns all 50 + (Phase 1's activation forensics wants the near-field verdicts too). + """ + with open(os.path.join(gallery_dir, "silent_gallery", "manifest.json"), + encoding="utf-8") as fh: + manifest = json.load(fh) + with open(os.path.join(gallery_dir, "silent__jonf.json"), encoding="utf-8") as fh: + verdicts = json.load(fh)["verdicts"] + out = {} + for key, item in manifest["items"].items(): + if field is not None and item.get("field") != field: + continue + v = verdicts.get(key) + out[key] = {**item, "verdict": v if isinstance(v, str) + else (v or {}).get("verdict")} + return out + + +def main(argv=None): + p = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + p.add_argument("--threshold", type=float, default=DEFAULT_THRESHOLD) + p.add_argument("--witness", default=os.path.join(OUT, "silent_witness.json")) + p.add_argument("--gallery", default=os.path.join(REPO, "benchmark", + "miss_taxonomy_46")) + p.add_argument("--json-out", default=None) + args = p.parse_args(argv) + + # Populations, all at the deployed threshold. -------------------------------- + pooled = [] + for city in US_SPLITS: + loaded = mt.load_rows(city, args.threshold, rng=None) + if loaded is not None: + pooled.extend(loaded[0]) + far = [r for r in pooled if r["field"] == "far"] + far_hits = [r for r in far if r["hit"]] + far_silent = [r for r in far if not r["hit"] and r["bucket"] == "silent"] + + queue = load_queue(args.witness) + unw_far = [r for r in far_silent if row_key(r) in queue] + + rated = load_rated(args.gallery) + rated_keys = {(v["city"], v["pano"], round(float(v["x"]), 6), + round(float(v["y"]), 6)) for v in rated.values()} + rated_rows = [r for r in unw_far if row_key(r) in rated_keys] + excluded_rows = [r for r in unw_far if row_key(r) not in rated_keys] + + widths = {city: stored_widths(city) for city in US_SPLITS} + visible = [v for v in rated.values() if v["verdict"] == "visible"] + + print(f"=== Far-field 'visible' anomaly, Phase 0: sample forensics " + f"(threshold {args.threshold}, boundary {FAR_BOUNDARY_M:.0f} m, #46) ===\n") + print(f"far-field GT {len(far)}, hits {len(far_hits)} " + f"(recall {len(far_hits)/len(far):.3f}), silent misses {len(far_silent)}; " + f"unwitnessed {len(unw_far)}, rated {len(rated_rows)}, " + f"below the floor {len(excluded_rows)}") + tally = {} + for v in rated.values(): + tally[v["verdict"]] = tally.get(v["verdict"], 0) + 1 + print(f"reviewer verdicts over the rated set: " + + ", ".join(f"{k} {n}" for k, n in sorted(tally.items(), + key=lambda kv: -kv[1]))) + + # 1. The floor, split by split. ---------------------------------------------- + print(f"\n{'-'*78}\n1. THE FLOOR IS NOT ONE FLOOR — {JUDGEABLE_SOURCE_PX:.0f} " + f"source px in model pixels, per split\n{'-'*78}") + print(f"{'split':>12} {'tier':>10} {'stored px':>12} {'floor(model px)':>16} " + f"{'far-silent':>11} {'unwitn.':>8} {'rated':>6}") + per_split = {} + for city in US_SPLITS: + fs = [r for r in far_silent if r["city"] == city] + if not fs: + continue + ws = sorted({widths[city].get(r["pano"]) for r in fs + if widths[city].get(r["pano"])}) + w_lo, w_hi = (ws[0], ws[-1]) if ws else (None, None) + floor_lo = effective_floor_model_px(w_hi) if w_hi else float("nan") + floor_hi = effective_floor_model_px(w_lo) if w_lo else float("nan") + floor_s = (f"{floor_lo:.1f}" if w_lo == w_hi else + f"{floor_lo:.1f}-{floor_hi:.1f}") + n_unw = sum(1 for r in unw_far if r["city"] == city) + n_rated = sum(1 for r in rated_rows if r["city"] == city) + stored_s = (f"{w_lo}" if w_lo == w_hi else f"{w_lo}-{w_hi}") if ws else "?" + print(f"{city:>12} {TIER.get(city, '-'):>10} {stored_s:>12} {floor_s:>16} " + f"{len(fs):>11} {n_unw:>8} {n_rated:>6}") + per_split[city] = {"far_silent": len(fs), "unwitnessed": n_unw, + "rated": n_rated, "stored_width_min": w_lo, + "stored_width_max": w_hi} + print("\n The deck's composition follows the floor: the 16384-px splits admit") + print(" far misses down to 7.5 model px, morgantown stops at 30. Which split a") + print(" miss happened in decides whether a reviewer ever saw it.") + + # 2. Where the rated set sits in the far-silent population. ------------------- + print(f"\n{'-'*78}\n2. SURVIVORSHIP — where the rated {len(rated_rows)} sit " + f"among all {len(far_silent)} far-field silent misses\n{'-'*78}") + print(f"{'population':>34} {'n':>4} {'dist q1/med/q3 (m)':>20} " + f"{'px q1/med/q3':>15}") + pops = { + "rated (reached the deck)": rated_rows, + "below the floor (excluded)": excluded_rows, + "witnessed (never queued)": [r for r in far_silent + if row_key(r) not in queue], + "ALL far-field silent misses": far_silent, + "far-field hits (for contrast)": far_hits, + } + stats = {} + for name, rows in pops.items(): + dq = quartiles([r["dist"] for r in rows]) + pq = quartiles([r["px"] for r in rows]) + stats[name] = {"n": len(rows), "dist_q": dq, "px_q": pq} + print(f"{name:>34} {len(rows):>4} " + f"{dq[0]:>6.1f}/{dq[1]:>5.1f}/{dq[2]:>5.1f} " + f"{pq[0]:>5.1f}/{pq[1]:>4.1f}/{pq[2]:>4.1f}") + auc_rated = auc([r["px"] for r in rated_rows], + [r["px"] for r in far_silent + if row_key(r) not in rated_keys]) + med_rank = percentile_rank([r["px"] for r in far_silent], + quartiles([r["px"] for r in rated_rows])[1]) + print(f"\n AUC(rated px vs unrated far-silent px) = {auc_rated:.3f} " + f"(0.5 = no size bias)") + print(f" the rated median px sits at the {med_rank:.0%} percentile of the " + f"far-silent population") + + # 3. Does apparent size even separate hit from silent out here? --------------- + print(f"\n{'-'*78}\n3. DISCRIMINATION — is a far-field silent miss the size " + f"the model cannot see?\n{'-'*78}") + auc_hit = auc([r["px"] for r in far_hits], [r["px"] for r in far_silent]) + print(f" AUC(far-hit px vs far-silent px) = {auc_hit:.3f} " + f"(1.0 would mean size alone decides)\n") + print(f"{'band':>12} {'GT':>6} {'recall':>8} {'silent':>8} {'rated':>6} " + f"{'visible':>8}") + band_rows = {} + key_of = {tag_key(v["pano"], v["x"], v["y"]): v for v in rated.values()} + for band in list(FAR_BANDS) + ["clamp"]: + sel = [r for r in far if band_of(r) == band] + if not sel: + continue + n_hit = sum(1 for r in sel if r["hit"]) + n_sil = sum(1 for r in sel if not r["hit"] and r["bucket"] == "silent") + n_rated = sum(1 for r in rated_rows if band_of(r) == band) + n_vis = sum(1 for r in rated_rows if band_of(r) == band + and key_of.get(tag_key(r["pano"], r["x"], r["y"]), + {}).get("verdict") == "visible") + label = "clamp>=150" if band == "clamp" else f"{band[0]:.0f}-{band[1]:.0f} m" + print(f"{label:>12} {len(sel):>6} {n_hit/len(sel):>8.3f} {n_sil:>8} " + f"{n_rated:>6} {n_vis:>8}") + band_rows[label] = {"n_gt": len(sel), "recall": n_hit / len(sel), + "silent": n_sil, "rated": n_rated, "visible": n_vis} + + rates = [] + for v in visible: + h, n = matched_rate(far, v["model_px"]) + if n: + rates.append(h / n) + rq = quartiles(rates) + print(f"\n MATCHED-SIZE DETECTION RATE: for each of the {len(visible)} " + f"'visible' misses, the model's") + print(f" recall over ALL far-field GT within ±{MATCH_TOL:.0%} of that miss's " + f"apparent size:") + print(f" q1/median/q3 = {rq[0]:.2f} / {rq[1]:.2f} / {rq[2]:.2f}") + print(f" A hard pixel floor would put these near zero. The model routinely") + print(f" detects other ramps of the same apparent size; silence at that size is") + print(f" therefore not size-fated, and 'pixel-starved' is not a sufficient") + print(f" explanation for these misses.") + + # 4. Mis-binning guards. ------------------------------------------------------ + print(f"\n{'-'*78}\n4. GUARDS — how much of this could the distance estimator " + f"be inventing?\n{'-'*78}") + n_clamp = {name: sum(1 for r in rows if r["y"] <= 0.5) + for name, rows in pops.items()} + print(f" above-horizon clamps: rated {n_clamp['rated (reached the deck)']}, " + f"excluded {n_clamp['below the floor (excluded)']}, " + f"all far-silent {n_clamp['ALL far-field silent misses']}") + tiers = {} + for r in rated_rows: + tiers[TIER.get(r["city"], "-")] = tiers.get(TIER.get(r["city"], "-"), 0) + 1 + print(f" rated set by tier: " + + ", ".join(f"{k} {n}" for k, n in sorted(tiers.items())) + + " (flat-ground distance is Spearman 0.95 on gsv, 0.81 on mapillary)") + + if args.json_out: + os.makedirs(os.path.dirname(os.path.abspath(args.json_out)), exist_ok=True) + payload = { + "threshold": args.threshold, "boundary_m": FAR_BOUNDARY_M, + "judgeable_source_px": JUDGEABLE_SOURCE_PX, + "match_tolerance": MATCH_TOL, + "counts": {"far_gt": len(far), "far_hits": len(far_hits), + "far_silent": len(far_silent), + "unwitnessed_far": len(unw_far), + "rated": len(rated_rows), + "below_floor": len(excluded_rows)}, + "verdicts": tally, + "per_split": per_split, + "populations": {name: {"n": s["n"], "dist_q1_med_q3": s["dist_q"], + "px_q1_med_q3": s["px_q"]} + for name, s in stats.items()}, + "auc_rated_vs_unrated_px": auc_rated, + "rated_median_px_percentile": med_rank, + "auc_hit_vs_silent_px": auc_hit, + "bands": band_rows, + "matched_size_detection_rate_q1_med_q3": rq, + "above_horizon": n_clamp, + "rated_by_tier": tiers, + } + with open(args.json_out, "w", encoding="utf-8") as fh: + json.dump(payload, fh, indent=2) + print(f"\nWrote {args.json_out}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/analysis/silent_activation.py b/scripts/analysis/silent_activation.py new file mode 100644 index 0000000..be873ff --- /dev/null +++ b/scripts/analysis/silent_activation.py @@ -0,0 +1,342 @@ +"""Phase 1 of the far-field `visible` anomaly study: attenuated, or absent? (#46) + +The ``silent`` bucket is defined by peak extraction: no ``peak_local_max`` peak at or +above the 0.05 score floor within the match radius. That is a statement about *peaks*, +not about the heatmap — 0.04 and 0.0001 are both "silent", and they are different +failures. If the model produces a real-but-faint localized response at these ramps, +the silent bucket is the tail of the same confidence continuum ``sub_threshold`` +lives on (a calibration/threshold/training story, gainesville's mechanism). If the +heatmap is flat at chance level, the model has no representation of the ramp at all — +a genuine vocabulary or scale gap, which is what Phase 2's scale counterfactual then +separates. + +For every pooled silent miss (near and far — the near-field verdicts feed the 0.013 +sourcing estimate just as directly), this loads the published model, runs one forward +pass per panorama, and reads: + +* ``act`` — the max heatmap value within the match radius of the missed ramp (the + radius and grid are exactly the matcher's: the scaled space *is* the 512x1024 + heatmap). Note ``act`` can exceed the 0.05 floor without contradicting ``silent`` + — a shoulder of a neighbouring peak is not a local maximum; those cases are + counted separately rather than silently pooled. +* a per-miss **null**: the same radius-max at ``NULL_TRIALS`` random azimuths in the + same panorama at the same elevation — the same null shape every #46 analysis uses, + because both ramps and heatmap mass crowd the horizon band. ``act`` is reported as + a percentile of its own panorama's null, so "there is signal here" is a claim + against that pano's actual noise floor, not against zero. + +Model: the published ``projectsidewalk/rampnet-model`` weights — the same checkpoint +every committed cache came from (``operating_point_curve.py`` extract). Single-pass, +no TTA, fp32, matching ``analysis_out/op_cache``. Needs the panorama imagery +(``benchmark//panos``, git-ignored — ``--panos-root`` from a worktree) and a +GPU-ish machine; the RTX 3070 does ~10 s/pano. + + python scripts/analysis/silent_activation.py --panos-root D:/Git/RampNet + python scripts/analysis/silent_activation.py --json-out analysis_out/silent_activation.json +""" +import argparse +import json +import os +import random +import sys + +REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +OUT = os.environ.get("RAMPNET_ANALYSIS_OUT", os.path.join(REPO, "analysis_out")) +sys.path.insert(0, REPO) +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import miss_taxonomy as mt # noqa: E402 +from miss_decomposition import DEFAULT_THRESHOLD, US_SPLITS # noqa: E402 +from farfield_forensics import load_rated, quartiles, row_key # noqa: E402 +from rampnet.detection_eval import ( # noqa: E402 + PANO_SCALE_X, PANO_SCALE_Y, radius_sq_for) + +NULL_TRIALS = 200 +NULL_SEED = 20260731 + +# The heatmap grid IS the matcher's scaled space (PANO_SCALE_X x PANO_SCALE_Y = +# 1024 x 512), asserted at runtime so a future resolution change cannot silently +# desynchronize the two. +HEAT_W, HEAT_H = int(PANO_SCALE_X), int(PANO_SCALE_Y) + + +# --------------------------------------------------------------------------- # +# Pure core (no torch, no I/O) — unit-tested in tests/test_silent_activation.py +# --------------------------------------------------------------------------- # +def radius_max(heat, x, y, radius_sq=None): + """Max heatmap value within the match radius of normalized point ``(x, y)``. + + Columns wrap at the 360-degree seam (a radius crossing x=0 continues at x=1); + rows clamp — there is nothing above the top of a panorama. Values are clipped + to [0, 1] exactly as ``peaks_to_dets`` clips before peak extraction, so an + ``act`` here and a peak score there are on the same scale. + """ + return site_profile(heat, x, y, radius_sq)[0] + + +def site_profile(heat, x, y, radius_sq=None): + """``(act, off_px, center)`` for the window around normalized ``(x, y)``. + + ``off_px`` is how far from the site the in-window maximum sits, and ``center`` + is the value at the site itself — together they separate a response *at* the + ramp from a neighbouring mode's tail reaching *into* the window, which ``act`` + alone cannot do (and 62% of silent misses turn out to need the distinction). + """ + if radius_sq is None: + radius_sq = radius_sq_for() + H, W = len(heat), len(heat[0]) + r = radius_sq ** 0.5 + cx, cy = x * W, y * H + best, off = 0.0, 0.0 + for row in range(max(0, int(cy - r)), min(H, int(cy + r) + 2)): + dy2 = (row - cy) ** 2 + if dy2 >= radius_sq: + continue + span = (radius_sq - dy2) ** 0.5 + for col in range(int(cx - span), int(cx + span) + 2): + dx = col - cx + if dx * dx + dy2 >= radius_sq: + continue + v = min(float(heat[row][col % W]), 1.0) + if v > best: + best, off = v, (dx * dx + dy2) ** 0.5 + center = min(float(heat[min(H - 1, max(0, round(cy)))] + [round(cx) % W]), 1.0) + return best, off, max(center, 0.0) + + +def nearest_peak(preds, x, y): + """``(dist_px, score)`` of the closest cached floor peak, in matcher units. + + ``preds`` are the panorama's cached ``(x, y, score)`` floor peaks (>= 0.05). + For a silent miss any such peak is by definition OUTSIDE the match radius, so + this measures how far away the nearest thing the model actually said is — + the difference between "a neighbouring mode's tail reaches the site" (~1-2 + radii) and "the nearest response is nowhere near" (many radii). + """ + if not preds: + return float("inf"), None + best, score = float("inf"), None + for p in preds: + dx = abs(p[0] - x) * PANO_SCALE_X + dx = min(dx, PANO_SCALE_X - dx) + d = (dx * dx + ((p[1] - y) * PANO_SCALE_Y) ** 2) ** 0.5 + if d < best: + best, score = d, p[2] + return best, score + + +def null_percentile(heat, x, y, rng, trials=NULL_TRIALS, radius_sq=None): + """``(act, percentile, null_med, null_p95)`` of the site's radius-max vs its pano. + + The null keeps the ramp's elevation and randomizes azimuth — the shape every + other #46 null uses. Draws whose window would overlap the site's own window + (wrapped column distance under 2R) are rejected and redrawn: the question is + whether the site's response exceeds what the *rest* of the elevation band + produces, and a draw that reads the site's own bump back would contaminate + exactly the sparse-heatmap case this analysis exists to detect. The percentile + counts ties as half, so a flat heatmap reads 0.5, not 1.0. + """ + if radius_sq is None: + radius_sq = radius_sq_for() + act = radius_max(heat, x, y, radius_sq) + W = len(heat[0]) + exclude = 2.0 * (radius_sq ** 0.5) / W # normalized column distance + draws = [] + while len(draws) < trials: + nx = rng.random() + dx = abs(nx - x) + if min(dx, 1.0 - dx) < exclude: + continue + draws.append(radius_max(heat, nx, y, radius_sq)) + draws.sort() + below = sum(1 for d in draws if d < act) + ties = sum(1 for d in draws if d == act) + pct = (below + 0.5 * ties) / trials + return act, pct, draws[trials // 2], draws[int(trials * 0.95)] + + +def group_of(row, queue_keys, rated_by_rowkey): + """Which selection stratum a silent miss belongs to (Phase 0's partition).""" + key = row_key(row) + if key not in queue_keys: + return "witnessed" + if key in rated_by_rowkey: + return "rated" + return "below_floor" + + +# --------------------------------------------------------------------------- # +# I/O +# --------------------------------------------------------------------------- # +def main(argv=None): + p = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + p.add_argument("--threshold", type=float, default=DEFAULT_THRESHOLD) + p.add_argument("--witness", default=os.path.join(OUT, "silent_witness.json")) + p.add_argument("--gallery", default=os.path.join(REPO, "benchmark", + "miss_taxonomy_46")) + p.add_argument("--panos-root", default=REPO, + help="Checkout holding benchmark//panos (git-ignored, so " + "in a worktree it lives in the main checkout instead).") + p.add_argument("--cities", default=",".join(US_SPLITS)) + p.add_argument("--limit", type=int, default=None, + help="Stop after this many panos (smoke test).") + p.add_argument("--json-out", default=None) + args = p.parse_args(argv) + + import torch + import threshold_sweep as ts + assert (HEAT_W, HEAT_H) == (1024, 512), "heatmap grid != matcher space" + + from miss_gallery import load_queue, pano_path + queue_keys = load_queue(args.witness) + rated = load_rated(args.gallery, field=None) + rated_by_rowkey = {(v["city"], v["pano"], round(float(v["x"]), 6), + round(float(v["y"]), 6)): v for v in rated.values()} + + from operating_point_curve import CACHE_DIR, read_cache + cities = [c.strip() for c in args.cities.split(",") if c.strip()] + by_pano, preds_by = {}, {} + for city in cities: + loaded = mt.load_rows(city, args.threshold, rng=None) + if loaded is None: + continue + for r in loaded[0]: + if not r["hit"] and r["bucket"] == "silent": + by_pano.setdefault((city, r["pano"]), []).append(r) + panos, _ = read_cache(os.path.join(CACHE_DIR, f"{city}.json")) + for pd in panos: + preds_by[(city, pd["pano"])] = pd["preds"] + n_miss = sum(len(v) for v in by_pano.values()) + print(f"=== Silent-miss activation forensics (threshold {args.threshold}, " + f"{n_miss} misses in {len(by_pano)} panos, #46 Phase 1) ===", flush=True) + + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + model = ts.load_model().to(device) + print(f"device={device} model=projectsidewalk/rampnet-model " + f"(single-pass fp32, as op_cache)", flush=True) + + rng = random.Random(NULL_SEED) + radius_sq = radius_sq_for() + results, skipped = [], 0 + for i, ((city, pano), misses) in enumerate(sorted(by_pano.items()), 1): + path = pano_path(city, pano, args.panos_root) + if not os.path.exists(path): + skipped += len(misses) + continue + heat = ts.heatmap_for(model, device, path, use_fp16=False) + for r in misses: + act, pct, null_med, null_p95 = null_percentile( + heat, r["x"], r["y"], rng, radius_sq=radius_sq) + _, off_px, center = site_profile(heat, r["x"], r["y"], radius_sq) + npk_px, npk_score = nearest_peak(preds_by.get((city, pano), []), + r["x"], r["y"]) + key = row_key(r) + v = rated_by_rowkey.get(key) + results.append({ + "city": city, "pano": pano, "x": r["x"], "y": r["y"], + "field": r["field"], "dist_m": round(r["dist"], 1), + "px": round(r["px"], 1), + "group": group_of(r, queue_keys, rated_by_rowkey), + "verdict": v["verdict"] if v else None, + "act": round(act, 5), "null_pct": round(pct, 3), + "null_med": round(null_med, 5), "null_p95": round(null_p95, 5), + "above_own_null_p95": act > null_p95, + "argmax_off_px": round(off_px, 1), + "act_at_site": round(center, 5), + "nearest_peak_px": (round(npk_px, 1) + if npk_px != float("inf") else None), + "nearest_peak_score": (round(npk_score, 3) + if npk_score is not None else None), + }) + del heat + if i % 10 == 0: + print(f" {i}/{len(by_pano)} panos", flush=True) + if args.limit and i >= args.limit: + print(f" --limit {args.limit} reached", flush=True) + break + if skipped: + print(f" [!] {skipped} misses skipped — panorama not on disk under " + f"{args.panos_root}", flush=True) + + # ----------------------------------------------------------------------- # + print(f"\n{'-'*78}\nACTIVATION AT THE MISSED RAMP, by field and stratum\n{'-'*78}") + print(f"{'population':>34} {'n':>4} {'act q1/med/q3':>18} " + f"{'>p95 of own null':>17} {'act>=0.01':>10}") + groups = {} + for field in ("near", "far"): + for grp in ("rated", "below_floor", "witnessed"): + sel = [r for r in results if r["field"] == field and r["group"] == grp] + if not sel: + continue + name = f"{field} / {grp}" + groups[name] = sel + for name, sel in list(groups.items()) + [("ALL silent misses", results)]: + q = quartiles([r["act"] for r in sel]) + n_sig = sum(1 for r in sel if r["above_own_null_p95"]) + n_01 = sum(1 for r in sel if r["act"] >= 0.01) + print(f"{name:>34} {len(sel):>4} " + f"{q[0]:>6.4f}/{q[1]:>6.4f}/{q[2]:>6.4f} " + f"{n_sig:>7}/{len(sel):<7} {n_01:>10}") + + vis = [r for r in results if r["verdict"] == "visible"] + if vis: + q = quartiles([r["act"] for r in vis]) + n_sig = sum(1 for r in vis if r["above_own_null_p95"]) + print(f"\n rated `visible` only (n={len(vis)}): act q1/med/q3 " + f"{q[0]:.4f}/{q[1]:.4f}/{q[2]:.4f}; {n_sig}/{len(vis)} above their " + f"own pano's null p95") + + # What the in-window mass actually IS. A silent miss has no floor peak in + # radius by definition, so act >= 0.05 can only be an outside mode's tail; + # the argmax offset and the nearest cached peak make that checkable rather + # than asserted. + print(f"\n{'-'*78}\nDECOMPOSITION — what the in-window response is\n{'-'*78}") + r_px = radius_sq ** 0.5 + cls = {"absent": [], "faint_local": [], "tail": []} + for r in results: + if r["act"] < 0.01: + cls["absent"].append(r) + elif r["act"] >= 0.05: + cls["tail"].append(r) + else: + cls["faint_local"].append(r) + for name, sel in cls.items(): + if not sel: + continue + med_off = quartiles([r["argmax_off_px"] for r in sel])[1] + npks = [r["nearest_peak_px"] for r in sel if r["nearest_peak_px"]] + med_npk = quartiles(npks)[1] if npks else float("nan") + n_vis = sum(1 for r in sel if r["verdict"] == "visible") + print(f" {name:>12}: {len(sel):>3} (rated visible {n_vis:>2}) " + f"argmax off med {med_off:>4.1f} px nearest floor peak med " + f"{med_npk:>5.1f} px ({med_npk/r_px:.1f}R)") + tail_near_edge = sum(1 for r in cls['tail'] + if r['argmax_off_px'] > 0.75 * r_px) + print(f" tail cases with argmax in the window's outer quarter: " + f"{tail_near_edge}/{len(cls['tail'])} — the mass is entering from " + f"outside, not centred on the ramp") + + print(f"\n Reading: 'absent' = the heatmap is genuinely flat at the site.") + print(f" 'faint_local' = a real sub-floor response at the site itself.") + print(f" 'tail' = a neighbouring supra-floor mode's slope reaches the window —") + print(f" the site contributed no mode of its own, but the model responded to") + print(f" something adjacent (cf. the merged bucket's sigma story). The three") + print(f" continue differently: absent -> Phase 2's scale counterfactual;") + print(f" faint_local -> threshold/calibration (the sub_threshold continuum);") + print(f" tail -> representation (sigma), not vocabulary.") + + if args.json_out: + os.makedirs(os.path.dirname(os.path.abspath(args.json_out)), exist_ok=True) + with open(args.json_out, "w", encoding="utf-8") as fh: + json.dump({"threshold": args.threshold, "null_trials": NULL_TRIALS, + "null_seed": NULL_SEED, "n": len(results), + "skipped_no_imagery": skipped, + "model": "projectsidewalk/rampnet-model", + "tta": False, "results": results}, fh, indent=2) + print(f"\nWrote {args.json_out}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_farfield_forensics.py b/tests/test_farfield_forensics.py new file mode 100644 index 0000000..1d6b100 --- /dev/null +++ b/tests/test_farfield_forensics.py @@ -0,0 +1,197 @@ +"""Tests for the far-field anomaly's Phase 0 sample forensics (#46). + +Two layers, matching the script: + +* pure-function tests — the AUC, the per-split floor translation, the band + classifier (including the above-horizon clamp), and the matched-size rate, + since each one carries a headline claim; +* integration tests against the **committed** inputs (`analysis_out/op_cache`, + `analysis_out/silent_witness.json`, `benchmark/miss_taxonomy_46/`, + `benchmark//imagery_manifest.json`) pinning the population arithmetic the + write-up quotes: 83 far-field silent misses = 37 rated + 9 below the floor + + 37 witnessed. If a cache or manifest changes, these numbers must be re-derived, + not assumed — that is exactly the failure this file exists to catch. + +CPU-only, no network, no imagery, no GPU. +""" +import json +import os +import sys + +import pytest + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, REPO) +sys.path.insert(0, os.path.join(REPO, "scripts", "analysis")) + +import farfield_forensics as ff # noqa: E402 + + +# --------------------------------------------------------------------------- # +# auc — the survivorship and discrimination statistic +# --------------------------------------------------------------------------- # +def test_auc_is_half_for_identical_samples(): + assert ff.auc([1, 2, 3], [1, 2, 3]) == pytest.approx(0.5) + + +def test_auc_is_one_when_a_dominates(): + assert ff.auc([10, 11], [1, 2]) == 1.0 + + +def test_auc_is_zero_when_b_dominates(): + assert ff.auc([1, 2], [10, 11]) == 0.0 + + +def test_auc_counts_ties_half(): + # a = [1], b = [1]: one comparison, tied. + assert ff.auc([1], [1]) == pytest.approx(0.5) + + +def test_auc_empty_is_nan(): + assert ff.auc([], [1]) != ff.auc([], [1]) # NaN + + +# --------------------------------------------------------------------------- # +# effective_floor_model_px — the per-split floor translation +# --------------------------------------------------------------------------- # +def test_floor_on_a_16384_split_is_seven_and_a_half_model_px(): + assert ff.effective_floor_model_px(16384) == pytest.approx(7.5) + + +def test_floor_at_parity_is_the_floor_itself(): + assert ff.effective_floor_model_px(4096) == pytest.approx(30.0) + + +def test_floor_scales_inversely_with_stored_width(): + assert ff.effective_floor_model_px(8000) == pytest.approx(30.0 * 4096 / 8000) + + +# --------------------------------------------------------------------------- # +# band_of — the far bands and the clamp +# --------------------------------------------------------------------------- # +def _row(dist, y=0.7): + return {"dist": dist, "y": y} + + +def test_band_edges_are_half_open(): + assert ff.band_of(_row(18.0)) == (18.0, 25.0) + assert ff.band_of(_row(25.0)) == (25.0, 40.0) + assert ff.band_of(_row(40.0)) == (40.0, 150.0) + + +def test_above_horizon_is_the_clamp_band_regardless_of_distance(): + # geom() sends y <= 0.5 to 150 m; the y is the tell, not the distance. + assert ff.band_of(_row(150.0, y=0.5)) == "clamp" + assert ff.band_of(_row(150.0, y=0.4)) == "clamp" + + +def test_near_field_rows_fall_in_no_band(): + assert ff.band_of(_row(10.0)) is None + + +# --------------------------------------------------------------------------- # +# matched_rate — "how often does the model find OTHER ramps this size?" +# --------------------------------------------------------------------------- # +def _sized(px, hit): + return {"px": px, "hit": hit} + + +def test_matched_rate_counts_only_rows_within_tolerance(): + rows = [_sized(20, True), _sized(24, False), _sized(50, True)] + hits, n = ff.matched_rate(rows, 20.0, tol=0.20) + assert (hits, n) == (1, 2) # the 50-px row is out of band + + +def test_matched_rate_tolerance_is_symmetric_and_inclusive(): + rows = [_sized(16.0, True), _sized(24.0, False)] + hits, n = ff.matched_rate(rows, 20.0, tol=0.20) + assert (hits, n) == (1, 2) + + +def test_matched_rate_with_no_neighbours_is_zero_of_zero(): + assert ff.matched_rate([_sized(100, True)], 20.0) == (0, 0) + + +# --------------------------------------------------------------------------- # +# percentile_rank / quartiles +# --------------------------------------------------------------------------- # +def test_percentile_rank_midpoint(): + assert ff.percentile_rank([1, 2, 3, 4], 2.5) == pytest.approx(0.5) + + +def test_percentile_rank_ties_count_half(): + assert ff.percentile_rank([1, 2, 2, 3], 2) == pytest.approx(0.5) + + +def test_quartiles_are_ordered(): + q1, med, q3 = ff.quartiles(list(range(100))) + assert q1 < med < q3 + + +# --------------------------------------------------------------------------- # +# Integration — the committed populations the write-up quotes +# --------------------------------------------------------------------------- # +WITNESS = os.path.join(REPO, "analysis_out", "silent_witness.json") +GALLERY = os.path.join(REPO, "benchmark", "miss_taxonomy_46") + +needs_committed = pytest.mark.skipif( + not (os.path.exists(WITNESS) + and os.path.exists(os.path.join(GALLERY, "silent__jonf.json"))), + reason="committed witness/gallery files not present") + + +@pytest.fixture(scope="module") +def far_populations(): + import miss_taxonomy as mt + from miss_decomposition import DEFAULT_THRESHOLD, US_SPLITS + pooled = [] + for city in US_SPLITS: + loaded = mt.load_rows(city, DEFAULT_THRESHOLD, rng=None) + if loaded is not None: + pooled.extend(loaded[0]) + far_silent = [r for r in pooled if r["field"] == "far" and not r["hit"] + and r["bucket"] == "silent"] + from miss_gallery import load_queue + queue = load_queue(WITNESS) + unw = [r for r in far_silent if ff.row_key(r) in queue] + rated = ff.load_rated(GALLERY) + rated_keys = {(v["city"], v["pano"], round(float(v["x"]), 6), + round(float(v["y"]), 6)) for v in rated.values()} + return far_silent, unw, rated, rated_keys + + +@needs_committed +def test_the_population_arithmetic_the_writeup_quotes(far_populations): + far_silent, unw, rated, rated_keys = far_populations + assert len(far_silent) == 83 + assert len(unw) == 46 + assert len(rated) == 37 + below = [r for r in unw if ff.row_key(r) not in rated_keys] + assert len(below) == 9 + + +@needs_committed +def test_every_rated_item_is_an_unwitnessed_far_silent_miss(far_populations): + far_silent, unw, rated, rated_keys = far_populations + unw_keys = {ff.row_key(r) for r in unw} + assert rated_keys <= unw_keys + + +@needs_committed +def test_the_far_verdict_tally_matches_the_committed_pass(far_populations): + _, _, rated, _ = far_populations + tally = {} + for v in rated.values(): + tally[v["verdict"]] = tally.get(v["verdict"], 0) + 1 + assert tally == {"visible": 34, "context-only": 2, "unclear": 1} + + +@needs_committed +def test_imagery_manifests_cover_every_rated_pano(): + # The floor table translates the source-px floor through the committed widths; + # a rated pano missing from its manifest would silently drop from that table. + rated = ff.load_rated(GALLERY) + for v in rated.values(): + widths = ff.stored_widths(v["city"]) + assert v["pano"] in widths, (v["city"], v["pano"]) + assert widths[v["pano"]] == v["source_width"] diff --git a/tests/test_silent_activation.py b/tests/test_silent_activation.py new file mode 100644 index 0000000..3de2bb6 --- /dev/null +++ b/tests/test_silent_activation.py @@ -0,0 +1,147 @@ +"""Unit tests for the silent-miss activation forensics (#46, Phase 1). + +Pure core only — no torch, no imagery, no GPU. The heavy path (model inference) +is exercised by running the script itself; what these protect is the geometry: +``radius_max`` must read the heatmap through exactly the matcher's coordinate +convention, or the activation numbers describe the wrong locations. +""" +import os +import random +import sys + +import pytest + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, REPO) +sys.path.insert(0, os.path.join(REPO, "scripts", "analysis")) + +import silent_activation as sa # noqa: E402 +from rampnet.detection_eval import radius_sq_for # noqa: E402 + +RSQ = radius_sq_for() +R = RSQ ** 0.5 # 22.5 heatmap px + + +def _heat(value=0.0): + return [[value] * 1024 for _ in range(512)] + + +# --------------------------------------------------------------------------- # +# radius_max — the matcher's window, applied to the heatmap +# --------------------------------------------------------------------------- # +def test_reads_a_peak_at_the_site(): + h = _heat() + h[256][512] = 0.7 + assert sa.radius_max(h, 512 / 1024, 256 / 512) == pytest.approx(0.7) + + +def test_ignores_a_peak_outside_the_radius(): + h = _heat() + h[256][512 + int(R) + 2] = 0.9 + assert sa.radius_max(h, 512 / 1024, 256 / 512) == 0.0 + + +def test_sees_a_peak_just_inside_the_radius(): + h = _heat() + h[256][512 + int(R) - 1] = 0.9 + assert sa.radius_max(h, 512 / 1024, 256 / 512) == pytest.approx(0.9) + + +def test_columns_wrap_at_the_seam(): + # A site at x~0 must see a peak stored at the right edge of the heatmap. + h = _heat() + h[256][1023] = 0.8 + assert sa.radius_max(h, 2 / 1024, 256 / 512) == pytest.approx(0.8) + + +def test_rows_clamp_at_the_top(): + # A site near the top row must not crash reaching above the panorama. + h = _heat() + h[0][512] = 0.6 + assert sa.radius_max(h, 512 / 1024, 0.0) == pytest.approx(0.6) + + +def test_values_clip_to_one_like_peak_extraction(): + h = _heat() + h[256][512] = 1.7 + assert sa.radius_max(h, 512 / 1024, 256 / 512) == 1.0 + + +# --------------------------------------------------------------------------- # +# null_percentile — signal against the pano's own noise floor +# --------------------------------------------------------------------------- # +def test_flat_heatmap_reads_as_chance(): + act, pct, med, p95 = sa.null_percentile(_heat(0.003), 0.5, 0.5, + random.Random(0), trials=50) + assert act == pytest.approx(0.003) + assert pct == pytest.approx(0.5) # every draw ties the site + assert med == p95 == pytest.approx(0.003) + + +def test_a_lone_bump_at_the_site_beats_its_null(): + h = _heat() + h[256][512] = 0.04 + act, pct, _, p95 = sa.null_percentile(h, 512 / 1024, 256 / 512, + random.Random(0), trials=100) + assert act == pytest.approx(0.04) + assert pct > 0.9 + assert act > p95 + + +def test_a_site_no_better_than_the_horizon_band_fails_the_test(): + # Strong response everywhere along the site's row: the site is nothing special. + h = _heat() + for c in range(0, 1024, 8): + h[256][c] = 0.5 + act, pct, _, p95 = sa.null_percentile(h, 512 / 1024, 256 / 512, + random.Random(0), trials=100) + assert act == pytest.approx(0.5) + assert not act > p95 + + +# --------------------------------------------------------------------------- # +# site_profile / nearest_peak — separating a site response from a neighbour's tail +# --------------------------------------------------------------------------- # +def test_site_profile_centred_bump_has_zero_offset(): + h = _heat() + h[256][512] = 0.4 + act, off, center = sa.site_profile(h, 512 / 1024, 256 / 512) + assert act == pytest.approx(0.4) + assert off == pytest.approx(0.0) + assert center == pytest.approx(0.4) + + +def test_site_profile_offset_bump_reports_its_distance(): + h = _heat() + h[256][512 + 15] = 0.4 + act, off, center = sa.site_profile(h, 512 / 1024, 256 / 512) + assert act == pytest.approx(0.4) + assert off == pytest.approx(15.0) + assert center == 0.0 # nothing at the ramp itself + + +def test_nearest_peak_measures_in_matcher_units_and_wraps(): + # A peak across the seam: x=0.999 vs site x=0.001 is ~2 px away, not ~1022. + d, score = sa.nearest_peak([(0.999, 0.5, 0.7)], 0.001, 0.5) + assert d == pytest.approx(0.002 * 1024, abs=0.01) + assert score == 0.7 + + +def test_nearest_peak_with_no_peaks_is_infinite(): + d, score = sa.nearest_peak([], 0.5, 0.5) + assert d == float("inf") and score is None + + +# --------------------------------------------------------------------------- # +# group_of — Phase 0's partition, reused +# --------------------------------------------------------------------------- # +def _row(city="bend", pano="p1", x=0.25, y=0.6): + return {"city": city, "pano": pano, "x": x, "y": y} + + +def test_group_partition(): + r = _row() + key = sa.row_key(r) + assert sa.group_of(r, set(), {}) == "witnessed" + assert sa.group_of(r, {key}, {}) == "below_floor" + assert sa.group_of(r, {key}, {key: {}}) == "rated"