| species | family | fish | confidence (per fish) |
|---|---|---|---|
| Yellowfin hind Cephalopholis hemistiktos | Serranidae | 11 fish1 w12_fish1 w22_fish1 w32_fish1 w32_fish5 w40_fish1 w40_fish3 w40_fish7 w58_fish1 w58_fish2 w58_fish5 | 68% · medium 86% · high 83% · high 62% · medium 95% · high 85% · high 74% · medium 73% · medium 70% · medium 61% · medium 59% · medium |
| Chocolate-dip chromis Chromis dimidiata | Pomacentridae | 8 fish3 w02_fish7 w12_fish5 w22_fish3 w32_fish4 w40_fish4 w58_fish7 w68_fish1 | 99% · high 97% · high 98% · high 92% · high 91% · high 99% · high 99% · high 100% · high |
| Checkerboard wrasse Halichoeres hortulanus | Labridae | 6 w02_fish1 w02_fish2 w12_fish2 w58_fish9 w58_fish11 w68_fish3 | 100% · high 99% · high 100% · high 100% · high 99% · high 98% · high |
| Orange-lined triggerfish Balistapus undulatus | Balistidae | 5 fish2 w02_fish4 w22_fish2 w40_fish2 w58_fish6 | 100% · high 100% · high 98% · high 100% · high 98% · high |
| Cheeklined wrasse Oxycheilinus digramma | Labridae | 5 w02_fish8 w02_fish9 w02_fish10 w12_fish9 w58_fish10 | 67% · medium 36% · low 41% · low 36% · low 46% · low |
| Eritrean butterflyfish Chaetodon paucifasciatus | Chaetodontidae | 3 w12_fish3 w22_fish4 w40_fish6 | 100% · high 100% · high 93% · high |
| Blacktip grouper Epinephelus fasciatus | Serranidae | 2 w32_fish3 w40_fish5 | 60% · low 62% · low |
| Redmouth grouper Aethaloperca rogaa | Serranidae | 2 w58_fish8 w68_fish5 | 83% · high 83% · high |
| Bluestreak cleaner wrasse Labroides dimidiatus | Labridae | 1 w02_fish6 | 100% · high |
| Coral hind Cephalopholis miniata | Serranidae | 1 w58_fish3 | 93% · high |
Confidence: BioCLIP probability + agreement across crops; "low" should be read as "unknown, closest match". The same resident individual is probably counted in several windows (no cross-window re-identification).
Why do the models look blurred, and what fixes it? Nine changes (more iterations, quality-based frame selection, colour / water / motion-blur image formation, lossless crops) were tested one at a time on six fish against a fixed held-out set, then reviewed. Only more iterations (12000 instead of 4000) passed and was rolled out to all fish as v2; the previous models stay available as v1. The gains are small: every render remains much blurrier than the photos (the remaining blur points at pose alignment).
Rollout (v2 = 12000 iterations, everything else as v1): 20 of 44 fish have a v2 model with videos; 20 are evaluated on their fixed held-out frames. v2 is sharper on 16 (grain-robust sharpness ratio; beyond training noise on 10; geometric mean x1.27) and has lower LPIPS on 19 (mean -0.0146). Verdict per fish (LPIPS beyond noise): better 14, same within noise 6, worse 0. Each fish page has a v1 vs v2 table and side-by-side turntables.
| fish | species | LPIPS v1 → v2 | dLPIPS | PSNR mask v1 → v2 | sharpness x | verdict |
|---|---|---|---|---|---|---|
| w12_fish2 | Checkerboard wrasse | 0.2214 → 0.1660 | -0.0555 | 18.59 → 18.58 | x2.62 | better |
| w58_fish3 | Coral hind | 0.1853 → 0.1350 | -0.0503 | 21.28 → 21.54 | x1.73 | better |
| w22_fish4 | Eritrean butterflyfish | 0.2057 → 0.1594 | -0.0463 | 16.93 → 17.80 | x1.56 | better |
| w02_fish1 | Checkerboard wrasse | 0.1882 → 0.1589 | -0.0293 | 19.26 → 19.44 | x1.46 | better |
| w40_fish6 | Eritrean butterflyfish | 0.2109 → 0.1961 | -0.0147 | 17.50 → 17.67 | x0.93 | better |
| w40_fish2 | Orange-lined triggerfish | 0.1626 → 0.1479 | -0.0147 | 22.71 → 23.09 | x1.70 | better |
| w58_fish6 | Orange-lined triggerfish | 0.1393 → 0.1250 | -0.0143 | 26.08 → 26.55 | x1.00 | better |
| w58_fish9 | Checkerboard wrasse | 0.1373 → 0.1251 | -0.0122 | 25.56 → 25.65 | x1.81 | better |
| w12_fish3 | Eritrean butterflyfish | 0.2143 → 0.2035 | -0.0108 | 17.15 → 17.36 | x0.98 | better |
| w22_fish1 | Yellowfin hind | 0.1942 → 0.1860 | -0.0082 | 21.52 → 21.36 | x1.43 | better |
| w68_fish5 | Redmouth grouper | 0.1287 → 0.1207 | -0.0081 | 23.52 → 23.82 | x1.52 | better |
| w68_fish3 | Checkerboard wrasse | 0.0923 → 0.0850 | -0.0073 | 27.18 → 27.58 | x1.21 | better |
| w40_fish1 | Yellowfin hind | 0.1806 → 0.1758 | -0.0047 | 18.79 → 18.89 | x1.25 | better |
| w58_fish7 | Chocolate-dip chromis | 0.1314 → 0.1272 | -0.0041 | 21.08 → 21.65 | x1.03 | better |
| w58_fish8 | Redmouth grouper | 0.1163 → 0.1132 | -0.0031 | 25.66 → 25.87 | x1.04 | same (within noise) |
| w02_fish4 | Orange-lined triggerfish | 0.1294 → 0.1266 | -0.0028 | 22.86 → 23.16 | x0.99 | same (within noise) |
| w02_fish6 | Bluestreak cleaner wrasse | 0.1039 → 0.1014 | -0.0026 | 17.93 → 18.37 | x1.05 | same (within noise) |
| fish2 | Orange-lined triggerfish | 0.1694 → 0.1672 | -0.0022 | 21.31 → 21.32 | x1.08 | same (within noise) |
| fish3 | Chocolate-dip chromis | 0.1571 → 0.1554 | -0.0018 | 21.81 → 21.88 | x1.03 | same (within noise) |
| w12_fish1 | Yellowfin hind | 0.1547 → 0.1558 | +0.0010 | 22.35 → 22.07 | x1.06 | same (within noise) |
Started 2026-11-11. Goal: find out why the final gs_final models (4D GS, fins preset, 30 fps, 4000 iters; see docs/method_choice.md, docs/RESULTS.md) look blurred, and fix it.
cap_segments, capped at 24 s) regardless of sharpness / size / pose quality, although most fish have many more clean 10 fps frames over other visits (e.g. w40_fish1: 3259 kept frames). Within-fish sharpness (Laplacian variance in the eroded mask, native-resolution) varies 3-8x (p90/p10): fish2 7.7x, w58_fish8 3.4x, w40_fish7 4.6x, w12_fish3 4.2x.fish2 (triggerfish, good), w12_fish3 (butterflyfish, good), w02_fish6 (cleaner wrasse, small), w58_fish8 (redmouth grouper), w32_fish1 (hind, fair, belly artifact), w40_fish7 (hind, poor).
gs_v2 (gs_final is kept), regenerate videos and the website.Decision rule: adopt a change if LPIPS and sharpness ratio improve on >= 4/6 fish with PSNR not dropping > 0.3 dB, and it looks better. Risks: coverage holes when too picky (hence the coverage constraint); water model trading off with per-frame colour gain (regularise); motion-blur model costs 3-5x render time. Not adopted (probe at most): per-frame deblur / super-resolution networks — hallucinated detail is inconsistent across views.
Results go to docs/quality_experiment.md.
Plan: quality_experiment_plan.md. This file collects the results phase by phase. Fish set (6): fish2, w12_fish3, w02_fish6, w58_fish8, w32_fish1, w40_fish7.
| piece | file | output | ||
|---|---|---|---|---|
| per-frame quality features + score | scripts/12_frame_quality.py --fish <name> ... | <dir>/frame_quality.json for the 10 fps base dir and the _hf dir, work/quality_exp/<name>/frame_quality.png | ||
| fixed held-out set + exclusion rule | scripts/13_heldout.py --fish <name> ...; `quality_lib.training_exclusion(name, 'base'\ | 'hf')` | work/quality_exp/<name>/heldout.json, heldout_sheet.jpg | |
| evaluation of any model | scripts/tests/quality_eval.py --model <dir> --fish <name> [--tag X] | work/quality_exp/<name>/eval/<tag>/metrics.json, sheet.jpg, views.png, comp/ | ||
| comparison sheet real \ | A \ | B ... | scripts/tests/quality_eval.py --fish <name> --compare <evaldir>,<evaldir> --labels B0,S2 | compare_sheet.jpg |
| Phase A diagnosis | scripts/tests/quality_diag.py --fish <name> ... | work/quality_exp/diag/<name>.json, <name>_diag.png, diag_summary.json | ||
| shared helpers | scripts/quality_lib.py | paths across windows, native sharpness, ellipsoid fit, exclusion, model rendering |
None of these needs FISH_WINDOW: the window follows from the fish name.
All image measures at native camera resolution: the crop square is cut from the full video frame (frames/f_*.jpg, hf_frames/<name>/h_*.jpg; integer offset, no resampling) and the crop's mask is resized onto it. Checked: the patch registers exactly with the crops (MAD 0 at zero offset, rising at +-0.5 px), and the 30 fps decode is pixel-identical to the 10 fps decode at k = 0.
sharp_lap: trimmed Laplacian energy (mean squared 3x3 Laplacian, clipped at the 98th percentile of |Laplacian|) inside SAM mask AND soft alpha > 0.95, eroded by 20 % of the fish thickness. Why not the plain Laplacian variance in the eroded SAM mask: the 10 fps SAM masks leak 1-2 px beyond the fish edge in places, and with a 4 %-of-length erosion the fish/background edge dominated the measure. On identical video frames the 10 fps and the 30 fps crops (their own SAM masks) gave 2-4x different values (w58_fish8 frame 4619: 310 vs 73); median |log ratio| over ~60 coincident frames 0.45 (w32_fish1) / 0.21 (w58_fish8) for the plain variance, 0.09 / 0.02 with soft-alpha intersection + trimming. Residual disagreements come from frames where the two SAM masks include different objects.sharp_rank = within-fish percentile of sharp_lap (10 + 30 fps pooled), because texture differs by species.speed_px_s (centroid speed, least squares over +-0.1 s), dir_ratio (gradient energy along / across the motion), length_px, fit_iou (projected pose ellipsoid vs mask), pose flags, view direction (view_az, view_el, view_side), dist (pose depth in half-lengths), blue_frac, frame-filter fields (keep, weight, occlusion, reasons), touches, bg_ok, and the gs_final repair / weight / error of the frame where present.score = gate (0.40 sharp_rank + 0.20 (1 - rank speed) + 0.15 rank length + 0.25 q_pose), gate = keep pose_valid ff_weight (1 - occlusion) (0.5 if touches) (0.8 if slow), q_pose = clip((fit_iou - 0.4) / 0.4, 0, 1), halved for shape / depth outliers.Candidates: 10 fps base frames (they have plates and soft alpha) that are kept, pose-valid, not slow, not outliers, bg_ok, not touching the border, unoccluded (no occlusion / overlap reason), ellipsoid fit >= the fish median and in the sharper half (thresholds relaxed in steps when too few pass). Greedy choice by score + view novelty + visit novelty - exclusion cost, >= 0.5 s apart.
Exclusion rule: every training frame (10 or 30 fps) within +-0.3 s of a held-out frame is excluded in every arm; heldout.json lists the ids (exclude.base, exclude.hf; 30 fps ids matched by time, id = 3*(i10-1)+1+k, t = start + (id-1)/30). Use quality_lib.training_exclusion(name, 'hf') in a trainer.
Deviation from the plan (40-60 frames): one held-out frame removes 7 frames at 10 fps and 19 at 30 fps. 40-60 held-out frames would remove most of the training data of the smaller fish (w02_fish6 has 135 usable 10 fps frames). The selection therefore stops (above 12 frames) before more than 35 % of the usable 10 fps frames or 30 % of the gs_final 30 fps training frames would be excluded, and penalises frames whose exclusion window is full of usable frames.
| fish | held-out | candidates | visits (held-out / usable) | flank +z / -z / head / tail | usable 10 fps excluded | gs_final 30 fps train excluded | held-out near gs_final training | stop |
|---|---|---|---|---|---|---|---|---|
| fish2 | 16 | 63 | 5 / 7 | 6 / 4 / 1 / 5 | 31 % | 28 % | 16 / 16 | exclusion budget |
| w12_fish3 | 24 | 64 | 12 / 15 | 4 / 16 / 2 / 2 | 32 % | 28 % | 9 / 24 | no candidate >= 0.5 s apart |
| w02_fish6 | 12 | 61 | 6 / 7 | 3 / 7 / 1 / 1 | 41 % (floor of 12) | 36 % | 10 / 12 | exclusion budget |
| w58_fish8 | 23 | 120 | 6 / 6 | 13 / 10 / 0 / 0 | 34 % | 13 % | 5 / 23 | exclusion budget |
| w32_fish1 | 40 | 159 | 13 / 33 | 18 / 15 / 2 / 5 | 12 % | 3 % | 1 / 40 | target reached |
| w40_fish7 | 13 | 41 | 3 / 6 | 2 / 3 / 3 / 5 | 29 % | 28 % | 10 / 13 | exclusion budget |
128 held-out frames in total (plan: 40-60 per fish; see above). The arms must retrain with the exclusion, so they lose 3-36 % of the 30 fps training frames of gs_final. The fair baseline for the arms is therefore not B0 but "B0x" = the gs_final recipe retrained with the exclusion; B0 is kept as an optimistic reference.
Same for every model, gaussians frozen, evaluated at the base crop resolution (256 px): initial pose = raw 05 pose (independent of what the model saw), discrete hypothesis search (current / twin / flip / twin+flip / upright twins / +-25 deg yaw, each translation-aligned and refined 20 steps at half resolution, lowest loss wins), then 200 Adam steps on pose, colour gain/bias and (deforming models) bending + caudal angle, coarse-to-fine. Loss = the 06_train_gs.py --bg_composite loss with fixed weights (composite over the plate, 0.8 L1 + 0.2 D-SSIM, soft-alpha / inside / far alpha priors, L2 on the deltas). Metrics: PSNR full (observed crop) and on the SAM mask, LPIPS-alex on the fish box (224 px) and on the full crop, sharpness ratio (trimmed gradient energy render / real, same mask and clip level), Laplacian ratio, IoU; six canonical views (gradient energy of the unbent model). contamination = held-out frames within 0.3 s of a training frame of the evaluated model.
Date: 2026-11-11. Inputs: 12_frame_quality.py on the 6 fish (10 fps + 30 fps), 13_heldout.py, tests/quality_eval.py on gs_final (B0), tests/quality_diag.py (re-render of gs_final on its own training frames with the refined poses / bending / caudal / colour of training, composited over the plates at the training resolution). Raw outputs: work/quality_exp/diag/<fish>.json, <fish>_diag.png, diag_summary.json, work/quality_exp/<fish>/eval/B0/.
| fish | n | PSNR full | PSNR mask | LPIPS fish | LPIPS full | sharpness ratio (median) | IoU | flank-view gradient energy | raw pose switched by TTO | contaminated |
|---|---|---|---|---|---|---|---|---|---|---|
| fish2 | 16 | 24.14 | 21.31 | 0.165 | 0.144 | 0.119 | 0.897 | 12.51 | 2 | 16/16 |
| w12_fish3 | 24 | 21.85 | 17.15 | 0.205 | 0.189 | 0.398 | 0.885 | 59.32 | 13 | 9/24 |
| w02_fish6 | 12 | 24.27 | 17.93 | 0.103 | 0.093 | 0.595 | 0.872 | 280.02 | 4 | 10/12 |
| w58_fish8 | 23 | 25.79 | 25.66 | 0.116 | 0.105 | 0.172 | 0.919 | 0.13 | 12 | 5/23 |
| w32_fish1 | 40 | 22.22 | 18.68 | 0.156 | 0.138 | 0.130 | 0.902 | 6.70 | 27 | 1/40 |
| w40_fish7 | 13 | 20.63 | 17.85 | 0.196 | 0.174 | 0.033 | 0.870 | 0.63 | 4 | 10/13 |
fish2, older 10 fps gs_robust_4d (generality check) | 16 | 23.68 | 20.21 | 0.174 | 0.151 | 0.084 | 0.904 | 9.39 | 2 | 16/16 |
metrics_run1.json vs metrics.json). Runtime 5-11 s per held-out frame with two evaluations sharing the GPU (2-7 min per fish). --reuse_tto recomputes the metrics from the stored fit in seconds.fish2/gs_robust_4d (no fins, no plate training) scores below gs_final on every metric (LPIPS 0.174 vs 0.165, PSNR mask 20.21 vs 21.31).Per-frame residuals of gs_final (median over the training frames):
| fish | train frames | PSNR mask | LPIPS fish | sharpness ratio | render / real sharpness: vs median frame, vs p90 frame | renders sharper than the p10 real frame | equivalent blur sigma (native px) |
|---|---|---|---|---|---|---|---|
| fish2 | 793 | 23.38 | 0.131 | 0.20 | 0.22, 0.04 | 38 % | 1.6 |
| w12_fish3 | 417 | 18.55 | 0.197 | 0.53 | 0.43, 0.18 | 61 % | 0.7 |
| w02_fish6 | 309 | 17.10 | 0.089 | 0.78 | 0.72, 0.33 | 78 % | 0.9 |
| w58_fish8 | 571 | 27.20 | 0.089 | 0.20 | 0.20, 0.10 | 2 % | 2.8 |
| w32_fish1 | 647 | 21.91 | 0.114 | 0.13 | 0.12, 0.07 | 2 % | 2.1 |
| w40_fish7 | 537 | 20.19 | 0.201 | 0.07 | 0.06, 0.02 | 4 % | 2.0 |
(Sharpness = trimmed gradient energy inside the fish, same mask and clip level for render and crop. Equivalent blur = Gaussian sigma for which the blurred real crop has the render's gradient energy, converted to native pixels.)
Spearman rho of the per-frame residual with the frame's features (training frames; PSNR mask: + = feature goes with a better fit; LPIPS: - = better):
| feature | PSNR mask: fish2 / w12 / w02 / w58 / w32 / w40 | LPIPS fish: same order | consistent? |
|---|---|---|---|
| ellipsoid fit IoU (pose / silhouette agreement) | +0.39 / +0.48 / +0.45 / +0.46 / +0.52 / +0.29 | -0.27 / -0.54 / -0.45 / -0.40 / -0.45 / -0.06 | yes, 6/6 and 5/6 |
side-on view (view_side) | +0.14 / +0.44 / +0.41 / +0.62 / +0.51 / +0.29 | -0.61 / -0.58 / -0.67 / -0.62 / -0.77 / -0.63 | yes, 6/6 and 6/6 |
| native length | -0.19 / +0.16 / +0.30 / +0.34 / +0.68 / +0.25 | -0.39 / -0.39 / -0.52 / -0.57 / -0.88 / -0.54 | mostly, 5/6 and 6/6 |
| input sharpness rank | -0.47 / -0.17 / -0.11 / +0.52 / +0.18 / +0.03 | +0.39 / -0.15 / -0.02 / -0.21 / -0.18 / +0.23 | no (mixed signs) |
| image speed | -0.24 / -0.04 / +0.53 / +0.36 / -0.25 / +0.02 | 0.00 / -0.09 / -0.66 / -0.53 / +0.36 / +0.04 | no |
| distance (pose depth in half-lengths) | +0.19 / +0.14 / -0.09 / +0.32 / -0.31 / +0.23 | +0.02 / 0.00 / +0.10 / -0.18 / +0.18 / -0.24 | no |
| blue fraction of the fish | +0.69 / +0.24 / +0.10 / +0.55 / +0.36 / -0.05 | -0.28 / -0.03 / +0.21 / -0.42 / -0.41 / -0.12 | weak (bluer = lower contrast, easier PSNR) |
| per-frame colour-gain deviation | +0.21 / +0.14 / -0.29 / +0.14 / -0.25 / +0.09 | -0.03 / +0.01 / +0.23 / +0.02 / +0.33 / +0.34 | no |
| quality score (12) | -0.41 / +0.06 / +0.01 / +0.58 / +0.41 / +0.08 | +0.16 / -0.35 / -0.10 / -0.22 / -0.38 / +0.13 | no (sharpness term pulls the wrong way) |
Sharpness ratio (render / real) vs the input's sharpness rank: -0.58 / -0.26 / -0.59 / -0.79 / -0.63 / -0.78: the sharper the photo, the larger the gap, because the render's sharpness is nearly the same in every frame (scatter in <fish>_diag.png, panel 2). By decile of the input sharpness, fish2's LPIPS rises from 0.115 (deciles 4-6) to 0.20 (deciles 9-10), PSNR mask falls from 24.4 to 18.7 dB. By decile of the ellipsoid fit, LPIPS falls from the worst to the best decile in every fish (w12_fish3 0.265 -> 0.161, w02_fish6 0.183 -> 0.066, fish2 0.222 -> 0.145, w32_fish1 0.131 -> 0.114 with a peak of 0.194 in decile 3; w40_fish7 nearly flat 0.201 -> 0.191).
Measurement checks:
The 30 fps decode is pixel-identical to the 10 fps decode, so any sharpness difference between the 30 fps training set and the other 10 fps frames is a selection effect of cap_segments (longest visits, 24 s cap). Native sharpness (trimmed Laplacian energy, usable frames):
| fish | usable 10 fps frames | inside the 30 fps segments | median sharpness inside / outside | 10 fps frames outside that are sharper than the median 30 fps training frame |
|---|---|---|---|---|
| fish2 | 310 | 304 | 0.24x (only 6 outside, not meaningful) | 6 |
| w12_fish3 | 394 | 159 | 1.55x | 52 |
| w02_fish6 | 135 | 121 | 2.05x | 0 |
| w58_fish8 | 388 | 209 | 0.22x | 174 |
| w32_fish1 | 2156 | 240 | 0.81x | 1209 |
| w40_fish7 | 259 | 201 | 0.74x | 31 |
Within-fish sharpness varies 6-35x (p90/p10, robust measure; usable 10 fps frames: fish2 18x, w12_fish3 6.4x, w02_fish6 35x, w58_fish8 11x, w32_fish1 14x, w40_fish7 13x). The sub-frame phase k of the 30 fps grid makes no difference (paired k1/k0, k2/k0 sharpness ratios 0.98-1.05).
The per-frame colour gains of gs_final are small (p10-p90 within 0.94-1.05 per channel for w12_fish3, within 0.97-1.03 for the other five), and the distance correlations above are weak and of mixed sign. The colour cast lives in the canonical colours instead (the models are cyan; e.g. w12_fish3's yellow body is pale green in views.png), i.e. it is a colour-fidelity problem for the turntables (arm C1), not a cause of blur. Red gain vs distance is clearly negative only for w12_fish3 (rho -0.62).
Steering for phase B:
fit_iou, view_side and length_px do. Suggested arm score: drop or down-weight frames with low ellipsoid fit and keep the view coverage constraint; sharpness as a secondary criterion. S2 has the most room on w58_fish8, w32_fish1, w40_fish7 (and w12_fish3).Date: 2026-11-11. The data side of the arms S1, S2, S3, L1, C1 for the 6 fish. Every arm is a NEW folder next to <name>_hf (<root>/fish/<name>_<arm>, root = work/ or work/w<MM>/); _hf, gs_final, the base dirs and their frame_quality.json are only read. Train an arm with the usual command and --fish <name>_<arm> (FISH_WINDOW=<MM> for window fish), e.g. 06_train_gs.py --fish w32_fish1_s2 --fins --deform --iters 4000 --hyp_max_frames 300 --out <tag>.
Held-out exclusion is already applied in every arm folder: the frames within +-0.3 s of a held-out frame (by time, arm_lib.excluded_by_time, same rule as quality_lib.training_exclusion) are not in meta.json / poses.json / frame_filter.json at all, so no trainer flag is needed (checked by 14_arm_data.py --summary: heldout_frames_present = 0 for every arm, see work/quality_exp/datasets.json). Note for trainers: quality_lib.training_exclusion() takes the BASE name (w32_fish1), not the arm folder name; on an arm folder the exclusion is a no-op.
| piece | file | |
|---|---|---|
| arm score, view bins, coverage selection, exclusion by time, dataset subsetting, C1 gains | scripts/arm_lib.py (new) | |
S1, S3, C1 builders, --summary | scripts/14_arm_data.py --fish ... --arms s1,c1,s3 (new) | |
| S2 (`--select quality --budget N\ | 0), L1 (--like _hf --lossless), --out-suffix, --exclude-heldout, --pose-source like_hf` | scripts/04d_hifps.py (defaults unchanged: without the new flags it plans and writes <name>_hf exactly as before; the new flags refuse to run with the default suffix _hf) |
| frame quality of an arm data set, ranks vs the existing base + _hf pool | scripts/12_frame_quality.py --fish ... --dataset _s2 (writes only <name>_s2/frame_quality.json) |
Arm score (arm_lib.q_arm, used by S1, S2, S3): q_arm = gate (0.35 q_pose + 0.35 q_sharp + 0.15 q_size + 0.15 q_motion) with gate and q_ of 12_frame_quality.py. Phase A asked for selection by pose quality as much as by sharpness, so pose fit gets the same weight as sharpness (12's score: 0.40 sharpness / 0.25 pose). A first version with 0.40 pose / 0.25 sharpness chose the same S2 windows for w12_fish3, so the choice is not sensitive there. The view enters as a constraint, not as a score term: view bins = 8 azimuth bins of 45 deg (centred on head-on) x 3 elevation bands (< -30, -30..30, > 30 deg) of the camera direction in the fish frame (view_az, view_el of 12, from the poses).
frame_select.json (format agreed with the trainer side): <dataset dir>/frame_select.json = {"note": ..., "frames": {"<id>": weight}}, weight in [0, 1], 0 = drop, a frame missing from the file is dropped when the file is used. Present in _s1 (weight 1 for every kept frame; the folder holds only those frames, so it is redundant there) and _s3 (the soft weights). Not present in _s2, _l1, _c1.
<name>_s1 (14_arm_data.py --arms s1): the _hf training frames (valid pose, keep, weight > 0) minus the exclusion; keep 50 % by q_arm; every view bin keeps at least max(1, ceil(25 %)) of its frames (its best ones), so no bin that had frames becomes empty. Only the kept frames are in the folder (copied crops / plates / soft alpha, filtered jsons).<name>_s2 (04d_hifps.py --fish <name> --out-suffix _s2 --select quality --exclude-heldout --pose-source like_hf, FISH_WINDOW as usual): anchors are 10 fps base frames over ALL visits with q_arm > 0 (frames near a held-out frame count 0); window = anchor +- 5 frames at 10 fps (+-0.5 s, same pose segment, frames not yet taken), value = sum of q_arm in the window / 11. Phase 1: one best window per view bin that holds >= max(3, 2 % of the usable frames) and is not yet covered; phase 2: best windows until the PREDICTED number of training frames (30 fps ids whose interpolated pose is valid, keep of both bracketing 10 fps frames, not excluded; same rules as the poses stage) reaches 1.03 x the budget. Budget = training frames of <name>_hf after the exclusion (B0x has the same number). The windows are merged into runs, decoded, segmented (SAM 2.1, QC, re-prompting), cropped at the _hf crop resolution, poses interpolated from the same pose source as _hf (like_hf: fish2 gs_robust refined poses, the others raw 05 poses), frame filter mapped, then trimmed to exactly the budget (frames removed from the ends of the lowest-value runs, coverage runs last) before plates + soft alpha (04c via the 30 fps adapter) and diag. Decoded frames in hf_frames/<name>_s2/ (the _hf frames / masks / SAM cache are not touched). Selection details in <name>_s2/hifps_report.json ('select', 'trim').<name>_s3 (12_frame_quality.py --fish <name> --dataset _s2, then 14_arm_data.py --arms s3): a copy of _s2 (same frames: no hard drops beyond S2) with soft weights w = 0.25 + 0.75 x percentile rank of q_arm among the S2 training frames (mean 0.63) in frame_select.json. frame_filter_s3.json = frame_filter.json with its weights multiplied by w, for the existing trainer (--filter_file frame_filter_s3.json, its per-frame loss weight). Use one of the two, not both, or the weights are applied twice.<name>_l1 (04d_hifps.py --fish <name> --out-suffix _l1 --lossless --like _hf --exclude-heldout): the same frames, crop squares and SAM masks as _hf (minus the exclusion); the RGB is re-cut from PNG frames decoded straight from the video (hf_frames/<name>_l1/, 2.5 GB per fish). Poses, frame filter, plates, soft alpha and bg_alpha.json are copied from _hf (the plates are medians of the 10 fps JPEG frames; their JPEG noise is averaged out). Colour finding: the stream is tagged BT.709 (tv range), but the JPEG path of the whole pipeline ends up with BT.601 colours (ffmpeg converts yuv420p -> yuvj420p by range only, the JPEG decoder applies BT.601). A default rgb24 decode would use BT.709 and differ by -10 (G) / -7 (R) levels from every other image, so --lossless forces in_color_matrix=bt601:in_range=tv. Checks (fish box, 16 frames per fish, all 6 fish): the same id matches best in 16/16 frames, MAD 2.0-2.2 levels vs the JPEG frame (= the JPEG loss), mean difference B +0.8..+0.9, G -0.5, R +0.6..+0.7 levels; crop alpha bit-identical to _hf (max difference 0); RGB MAD inside the mask 1.3-1.6 levels.<name>_c1 (14_arm_data.py --arms c1): _hf frames minus the exclusion, ONE diagonal colour transform per fish applied to the RGB of crops/ AND crops_bg/ (alpha and plate confidence unchanged), recorded in <name>_c1/colour_transform.json ({"type": "diagonal", "gains_rgb": [r, g, b], "space": "8-bit sRGB values as stored", ...}, out = clip(round(in x gain))). Estimate (arm_lib.plate_wb_gains): up to 300 plates, pixels with plate confidence >= 128 and soft alpha <= 13 (not the fish), luminance between its 60th and 99th percentile (sand / pale rock); m = their mean colour; gains = (1 / m)^0.8, normalised to keep the luminance of m; then all gains scaled down if the 99.5th percentile of the corrected brightest channel of fish pixels would exceed 250 (clip guard). Why strength 0.8: the full grey-world correction (exponent 1, also tried with the median and the mode of the chromaticity and with the mean over all plate pixels) turned the sand pink in all 4 fish checked, i.e. it over-corrects the red channel, which is also the noisiest. Previews (crop | C1 crop | plate | C1 plate): work/quality_exp/<name>/c1_preview.jpg. Evaluation note: the held-out crops of the base dirs are not corrected; the per-channel colour gain of the test-time fit can absorb a diagonal transform, or apply gains_rgb to the held-out crops (for colour-fidelity comparisons of the canonical views the correction is the point of the arm).Training frames = valid pose, keep, weight > 0, after the exclusion (what the trainer trains on, before its own every-10th validation split).
| fish | _hf (B0x) | S1 | S2 | S3 (weighted) | L1 / C1 frames (all, incl. invalid poses) | crop res |
|---|---|---|---|---|---|---|
| fish2 | 631 | 316 | 629 (2 short: all usable footage used) | 629 | 859 | 512 |
| w12_fish3 | 329 | 164 | 329 | 329 | 580 | 256 |
| w02_fish6 | 218 | 109 | 218 | 218 | 347 | 512 |
| w58_fish8 | 552 | 276 | 552 | 552 | 622 | 256 |
| w32_fish1 | 700 | 350 | 700 | 700 | 702 | 256 |
| w40_fish7 | 429 | 214 | 429 | 429 | 541 | 256 |
S1 vs all _hf training frames (medians; sharpness = native trimmed Laplacian energy, only comparable within a fish):
| fish | kept / of | forced by coverage | bins | q_arm | sharpness | ellipsoid fit | native length px |
|---|---|---|---|---|---|---|---|
| fish2 | 316 / 631 | 164 | 13 | 0.73 vs 0.63 | 3035 vs 1752 (1.73x) | 0.825 vs 0.832 | 260 vs 255 |
| w12_fish3 | 164 / 329 | 87 | 10 | 0.78 vs 0.63 | 34139 vs 24796 (1.38x) | 0.839 vs 0.825 | 140 vs 125 |
| w02_fish6 | 109 / 218 | 58 | 7 | 0.67 vs 0.56 | 3878 vs 1712 (2.27x) | 0.790 vs 0.759 | 336 vs 333 |
| w58_fish8 | 276 / 552 | 142 | 7 | 0.70 vs 0.62 | 65 vs 42 (1.56x) | 0.847 vs 0.840 | 166 vs 160 |
| w32_fish1 | 350 / 700 | 176 | 6 | 0.57 vs 0.53 | 642 vs 486 (1.32x) | 0.843 vs 0.828 | 153 vs 148 |
| w40_fish7 | 214 / 429 | 110 | 9 | 0.53 vs 0.46 | 1424 vs 1106 (1.29x) | 0.796 vs 0.774 | 111 vs 107 |
S2 vs _hf (medians over the usable frames of each 30 fps data set, frame_quality.json; for _hf this includes its frames near held-out frames; the 10 fps columns count the usable 10 fps frames each data set draws on):
| fish | windows (coverage) -> runs | usable 10 fps: S2 / _hf / all | visits S2 / _hf | view bins S2 / _hf / all | sharpness S2 vs _hf (30 fps) | fit | length px | speed px/s |
|---|---|---|---|---|---|---|---|---|
| fish2 | 31 (7) -> 15 | 214 / 208 / 214 | 7 / 6 | 12 / 11 / 12 | 1755 vs 2052 (0.86x) | 0.831 vs 0.839 | 254 vs 259 | 163 vs 171 |
| w12_fish3 | 12 (6) -> 12 | 122 / 112 / 266 | 7 / 3 | 13 / 10 / 15 | 16173 vs 20578 (0.79x) | 0.828 vs 0.832 | 138 vs 123 | 102 vs 113 |
| w02_fish6 | 14 (3) -> 16 | 78 / 75 / 80 | 7 / 6 | 9 / 7 / 9 | 1819 vs 4061 (0.45x) | 0.761 vs 0.772 | 326 vs 289 | 654 vs 577 |
| w58_fish8 | 21 (6) -> 18 | 197 / 182 / 255 | 6 / 2 | 7 / 7 / 7 | 57 vs 42 (1.36x) | 0.842 vs 0.843 | 161 vs 158 | 75 vs 55 |
| w32_fish1 | 24 (6) -> 24 | 263 / 233 / 1904 | 12 / 1 | 8 / 6 / 8 | 837 vs 485 (1.72x) | 0.815 vs 0.829 | 138 vs 147 | 9.7 vs 12.6 |
| w40_fish7 | 18 (5) -> 17 | 153 / 140 / 184 | 5 / 3 | 9 / 9 / 9 | 1372 vs 1291 (1.06x) | 0.769 vs 0.777 | 111 vs 108 | 22 vs 25 |
Reading: S2 has real room only where the fish has many usable frames outside the production segments: w32_fish1 (263 of 1904 usable 10 fps frames, 12 visits instead of 1, 1.72x sharper), w58_fish8 (6 visits instead of 2, 1.36x sharper) and to a small degree w40_fish7 (1.06x). For fish2 the _hf segments already cover all usable frames, so S2 is the same footage cut into windows (0.86x sharpness at 30 fps; 629 instead of 631 training frames: 2 short because every usable 10 fps frame is already used). w02_fish6 uses 78 of its 80 usable 10 fps frames either way and ends up blurrier at 30 fps (0.45x: different in-between frames around the same anchors), w12_fish3 trades sharpness (0.79x) for more visits (7 vs 3), view bins (13 vs 10) and size: its production segments are its sharpest part (Phase A, A3). S2's runs are short (1-2 s windows, 12-24 runs), so temporal smoothness and the pose-hypothesis chains see shorter chains than in _hf.
C1 gains (R, G, B) and L1:
| fish | C1 gains RGB | full grey-world gains (not used) | clip guard scale | fish px newly clipped | L1 frames | L1 MAD vs JPEG (levels) |
|---|---|---|---|---|---|---|
| fish2 | 1.43, 0.90, 0.90 | 1.55, 0.87, 0.87 | 1.00 | 0.00 % | 859 | 2.09 |
| w12_fish3 | 1.60, 0.72, 0.76 | 2.19, 0.80, 0.86 | 0.85 | 1.56 % | 580 | 2.19 |
| w02_fish6 | 1.46, 0.89, 0.89 | 1.60, 0.86, 0.86 | 1.00 | 0.21 % | 347 | 2.03 |
| w58_fish8 | 1.93, 0.85, 0.90 | 2.24, 0.80, 0.86 | 1.00 | 0.00 % | 622 | 2.05 |
| w32_fish1 | 1.54, 0.88, 0.90 | 1.71, 0.85, 0.87 | 1.00 | 0.04 % | 702 | 2.10 |
| w40_fish7 | 1.53, 0.88, 0.92 | 1.69, 0.84, 0.89 | 1.00 | 0.04 % | 541 | 2.16 |
All 36 runs passed (6 fish x S1, S2, S3, S3 with --filter_file frame_filter_s3.json, L1, C1): 06_train_gs.py --fish <name>_<arm> --fins --deform --iters 50 --out _smoke (FISH_WINDOW for window fish) with the working-tree trainer of 2026-11-11 ~12:00-14:00 (which the trainer agent was editing at the time). Deviation: all runs except the first two (w58_fish8 S1, S2) added --tto_steps 10, because with the GPU shared by other jobs the default 150-step final evaluation made a 50-iteration run take 5-10 min; it only shortens the final evaluation. Every run loaded the expected training frames (train + val = the "training frames" above; the trainer's valid-frame count additionally includes keep = false frames), all plates and soft alphas, and wrote a model; the _smoke / _smoke_ff output folders were deleted afterwards. frame_filter_s3.json was picked up (mean weight 0.59-0.63 vs 0.91-1.0 with frame_filter.json). Logs: work/quality_exp/logs/smoke_A.log, smoke_B.log (+ .json).
Plates usable for the composite loss (bg_ok of 04c, frames with a valid pose):
| fish | _hf-based (S1 / L1 / C1) | S2 / S3 |
|---|---|---|
| fish2 | 503 / 631 | 498 / 629 |
| w12_fish3 | 110 / 329 | 188 / 329 |
| w02_fish6 | 185 / 218 | 186 / 219 |
| w58_fish8 | 436 / 555 | 437 / 555 |
| w32_fish1 | 652 / 700 | 381 / 702 |
| w40_fish7 | 338 / 504 | 251 / 438 |
S2 of w32_fish1 (and less so w40_fish7) has clearly fewer frames with a usable plate: its 1 s windows are spread over 12 short visits where 04c's local plate is worse (frames without bg_ok are trained with the masked loss, see 06's docstring). This is a confound of the S2 arm to keep in mind.
# S2 (GPU: SAM + ViTMatte, 6-35 min per fish), per fish with its FISH_WINDOW python 04d_hifps.py --fish w32_fish1 --out-suffix _s2 --select quality --exclude-heldout --pose-source like_hf # L1 (CPU, < 1 min per fish) python 04d_hifps.py --fish w32_fish1 --out-suffix _l1 --lossless --like _hf --exclude-heldout # S3 needs the S2 frame quality; S1 / C1 only need _hf (no FISH_WINDOW needed) python 12_frame_quality.py --fish w32_fish1 --dataset _s2 python 14_arm_data.py --fish fish2 w12_fish3 w02_fish6 w58_fish8 w32_fish1 w40_fish7 --arms s1,c1,s3 --summary
Date: 2026-11-11. New options of scripts/06_train_gs.py (with gs_model.py, gs_render.py) for the arms. All are off by default; the default run is unchanged (checked below). Tests: scripts/tests/quality_opts_test.py (unit, eval), synthetic data from scripts/tests/make_synth_fish.py (new flags --motion_blur N --exposure e, --water; defaults unchanged, no extra RNG draws).
| option | what it does |
|---|---|
--frame_select [FILE] | reads <dataset dir>/frame_select.json (or FILE, absolute or relative to the data set) = {"frames": {"<id>": weight}}. Weight 0 or missing = not trained (split unselected in poses_refined.json). Weights multiply the per-frame loss together with the frame_filter weight and the robust weight; they are normalised to mean 1 over the training frames (after the internal val split, the exclusion and the frame filter). The internal every-10th val split is unchanged. |
--exclude_frames SPEC | ids never trained and not used as internal val (split excluded). SPEC = inline JSON list "[12,13]", a JSON file with a list (or {"frames": [...]}), a heldout.json of 13_heldout.py (exclude.hf for 30 fps data, exclude.base for 10 fps), or auto = quality_lib.training_exclusion(<base name>) with the base name taken from --fish (w32_fish1_hf, w32_fish1_s2 -> w32_fish1; auto:<name> to give it). The arm folders of section "B. Datasets" already contain no excluded frames, so the flag is a no-op there; it is needed for _hf runs (B0x, B1, M1, C2). |
--water | underwater image formation for the fish pixels (SeaThru-NeRF / WaterSplatting style, premultiplied): I_fish = J exp(-bD z) + alpha Binf (1 - exp(-bB z)), z = scale x expected camera depth of the render (gsplat RGB+ED, detached: colour must not move geometry), learnable global per-RGB bD, bB (> 0, log), Binf (0..1, logit) and scale (m per fish half-length, log, weak prior --lambda_water_scale to --water_scale0 0.1: only b x scale is identifiable). The gaussian colours are then the restored colours J. Compositing over the plate is unchanged (I = I_fish + (1 - alpha) plate; the plate already contains the water), alpha is unchanged. Init: bD = bB = 0.1/m, Binf = median background colour of the crops. Per-frame colour gain / bias (which could absorb the per-frame attenuation and backscatter exactly) are shrunk during training: --water_gain shrink (default, L2 x --water_gain_mult 10), fixed (identity) or free; the held-out test-time fit keeps the normal colour fit. Applied everywhere a render is compared with an image (training, robust scores, pose-hypothesis scoring, TTO). Water params: lr_water 1e-2, optimised after the warm-up. Saved as water in model.pt, poses_refined.json and train_log.json final.water (with bD_per_unit, bB_per_unit). |
--motion_blur N | every TRAINING render is the mean of N renders at times t + s e dt / 2, s in linspace(-1, 1, N), dt = frame interval (1/fps), e = learnable global exposure fraction (init --exposure0 0.5, sigmoid, --lr_exposure 1e-2, from --mb_start, default end of warm-up). Sub-frame states: --mb_motion fit (default): constant-velocity fit of the frame's motion from the refined poses, bending and caudal parameters of the training frames of its segment within +---mb_window 3 intervals (omega = sum tau Log(R_j R_i^T) / sum tau^2, v = sum tau (t_j - t_i) / sum tau^2, likewise bending / caudal; neighbours > --mb_max_deg 30 deg away are skipped), applied to the frame's own (differentiable) pose: R(tau) = Exp(tau omega) R_i, t_i + tau v. --mb_motion nearest: SLERP + lerp toward the previous / next refined training frame (the literal plan; see the test, it is too noisy). Densification uses the sum of the sub-render screen gradients. Held-out TTO, eval and all outputs render sharp (N = 1). Cost ~N x render time (N = 5: 2.4x wall time on _synth_blur_pl, GPU shared with another run). Saved as motion_blur {n, exposure, frame_interval_s} in model.pt / poses_refined.json / train_log.json. |
Rendering a water model elsewhere: gs_model.water_observe(rgb, alpha, depth, bD, bB, Binf, scale) with depth from gs_render.render_ex(..., depth=True) (info["depth"]) or render_batch(..., depth=True). 07_render.py: turntables show the restored colours, sequence.mp4 the observed ones (--seq_water restored to switch). 10_fish_videos.py: FishModel.render(..., observed=True) applies the water; the tracking overlay (render at a real frame's pose) uses it, turntables / poster / swim stay restored. tests/quality_eval.py does not apply the water model yet (not my file): for a fair held-out comparison of a --water model it must render as observed (water from model.pt / poses_refined.json, water_observe with the render depth) before the colour fit and the metrics; otherwise its per-frame colour fit has to absorb the whole water effect and the restored colours are scored against the water-cast frames. Canonical views (views.png) of a water model are restored colours by construction.
Same command at HEAD (copies of the three HEAD files) and with the new code (2000 iterations; gsplat training is not bit-deterministic, so HEAD was run twice to get the noise level):
| data / command | run | train PSNR | val TTO PSNR | val mask PSNR | val IoU | val comp PSNR | gaussians | rot err train (deg) | loss @500 |
|---|---|---|---|---|---|---|---|---|---|
_synth (default flags) | HEAD 1 | 33.33 | 34.83 | 29.65 | 0.983 | - | 27762 | 3.13 | 0.0908 |
| HEAD 2 | 33.32 | 34.86 | 29.70 | 0.983 | - | 27866 | 3.08 | 0.0908 | |
| new | 33.27 | 34.78 | 29.61 | 0.983 | - | 27708 | 3.11 | 0.0908 | |
_synth_fins_4d --fins --deform --hyp_max_frames 300 | HEAD 1 | 25.82 | 28.22 | 27.15 | 0.959 | 30.24 | 17340 | 4.78 | 0.0551 |
| HEAD 2 | 25.90 | 28.40 | 27.22 | 0.960 | 30.02 | 17338 | 4.81 | 0.0551 | |
| new | 25.84 | 28.25 | 27.28 | 0.956 | 30.04 | 17383 | 4.80 | 0.0551 |
The new code is within the HEAD-to-HEAD spread everywhere, with the same loss at iteration 500 and the same pose-repair counts. By construction the default path makes the same calls (the only changed expressions, fw != 1 instead of fw < 1 and filter weight x 1.0, are identical while all weights are <= 1).
quality_opts_test.py unit: SO(3) log / exp round trip (max error 1.5e-11) and a finite gradient at 0, pose interpolation end points / half way / extrapolation, water limits (z = 0 -> J, z -> inf -> alpha Binf), WaterModel round trip, render depth (plane at z = 3 exact in gsplat and torch, rgb / alpha unchanged with depth=True, render_batch = render_ex, backends agree), frame_select / exclusion parsing: all pass.
Smoke runs: --frame_select (weights 2 / 1 / 0) + --exclude_frames (6 ids) on _synth: 6 excluded, 19 unselected, 128 trained, effective weights odd / even frames 1.28 / 0.64 (ratio 2, mean 1); --eval_only --water reloads the water model; --fins --deform --water --motion_blur 3 on _synth_fins_4d (30 fps, caudal) runs; on this blur-free, water-free data e falls to 0.17 and bD x scale to ~0.002-0.003, as it should. Real-data smoke on w12_fish3_hf (--fins --deform --exclude_frames auto --water --motion_blur 3, 1500 iterations): the exclusion resolves to 134 of the data set's frames (train 417 -> 298, internal val 46 -> 31), 277 / 20 / 1 training frames with two / one / no temporal neighbour, peak GPU memory 0.61 GB, 788 s (GPU shared); learned e = 0.27, bD x scale = (.017, .009, .010), bB x scale = (.004, .006, .007), Binf = (.09, .44, .34) (red attenuated most, as expected). A smoke test only, not a result (1500 iterations, no comparison run).
Evaluation (quality_opts_test.py eval): on every 2nd training frame (77 frames) the model is rendered SHARP and RESTORED (no water, gain 1, bias 0) at its refined pose and compared with the GT model at the GT pose (no blur / water / gain), inside the GT mask: PSNR, mean colour error (levels), gradient-energy ratio render / GT (quality_lib.sharpness), LPIPS on the fish box over grey. 3000 iterations, default trainer otherwise. Outputs: work/quality_exp/trainer_opts_tests/eval_*.json and contact sheets eval_*.jpg (crop | GT | models).
Motion blur (make_synth_fish.py --motion_blur 9 --exposure 0.5 [--plates]: each frame = mean of 9 renders of the analytic trajectory over half the frame interval; at 10 fps the fish moves roughly 9-13 px during the exposure in the 256 px crops, estimated from the trajectory speed):
| data | model | PSNR vs sharp GT | colour err | sharpness ratio | LPIPS | learned e (GT 0.5) |
|---|---|---|---|---|---|---|
_synth (no blur, 2000 it., reference) | default | 26.38 | 6.4 | 0.82 | 0.031 | - |
_synth_blur_pl, --bg_composite | base | 22.20 | 11.9 | 0.44 | 0.148 | - |
--motion_blur 5 | 26.10 | 7.3 | 0.74 | 0.048 | 0.418 | |
_synth_blur (masked loss, no plates) | base | 22.06 | 12.0 | 0.44 | 0.105 | - |
--motion_blur 5 --mb_motion nearest | 22.06 | 12.0 | 0.44 | 0.099 | 0.06 (collapsed) | |
--motion_blur 5 (fit), e learnable | killed at it. 1500, e = 0.085 (collapsing) | |||||
--motion_blur 5 --mb_start 0.6 | killed at it. 2000, e = 0.20 (collapsing) | |||||
--motion_blur 5 --lr_exposure 0 (e fixed 0.5) | 21.69 | 10.7 | 0.85 | 0.074 | 0.5 (fixed) |
With the plate-composite loss (the production --fins setting) --motion_blur recovers nearly the sharpness of a model trained on unblurred frames (PSNR 26.1 vs 26.4, LPIPS 0.048 vs 0.031, from 0.148) and learns e = 0.42 for a true 0.5. With the masked loss (no plate) it does not work: the target is crop x hard mask on black, so every blur streak that leaves the SAM mask is penalised and the exposure collapses to ~0 (also with a late start); with e fixed at the true value the model is sharper but grows dark rims at the silhouette (PSNR 21.7). Frames with bg_ok false use the masked loss in fins mode and may pull e down. Use --motion_blur only with --bg_composite / --fins. The nearest interpolation (literal plan) failed even before that: one noisy neighbour pose per side is not a usable velocity at the pose noise of this data (refined rotation error ~2-3 deg vs ~3 deg of motion per frame); the windowed fit is the default.
Water (make_synth_fish.py --water --plates: per fish unit bD = (0.08, 0.03, 0.02), bB = (0.06, 0.05, 0.05), Binf = (0.10, 0.42, 0.50), distance 7-18 fish units; the fish turns cyan as in the real models), --bg_composite:
| model | PSNR restored vs GT J | colour err | LPIPS | sharpness | observed fit PSNR | val TTO PSNR | bD x scale (GT .08 .03 .02) | bB x scale (GT .06 .05 .05) | Binf (GT .10 .42 .50) |
|---|---|---|---|---|---|---|---|---|---|
| base (per-frame gain / bias only) | 12.98 | 42.2 | 0.257 | 0.39 | 29.82 | 34.73 | - | - | - |
--water (gain shrink) | 24.16 | 12.7 | 0.048 | 0.86 | 30.97 | 36.11 | .074 .034 .022 | .031 .025 .025 | .15 .65 .77 |
--water --water_gain fixed | 24.30 | 12.4 | 0.050 | 0.81 | 30.55 | 36.00 | .070 .032 .020 | .021 .025 .026 | .17 .63 .75 |
The attenuation is recovered within ~10 %; backscatter coefficient and veiling light trade off against each other (at these distances Binf (1 - exp(-bB z)) ~ Binf bB z: the products .0047/.016/.019 vs GT .006/.021/.025), so the restored colours are still a little pale (mean restored RGB .54 .57 .46 vs GT .57 .51 .42). The baseline's canonical colours are the observed (water-cast) ones. The water model also fits the observed frames better (+1.1 dB) and its gradient-energy ratio is higher mostly because the restored colours have more contrast. On real data the distance range of a fish is smaller and the gain shrink matters more (identifiability); treat the learned values with care.
--exclude_frames auto on <name>_hf.--motion_blur 5 (--fins is required in practice, see above). The held-out frames are real (blurred) frames rendered sharp, so PSNR on held-out frames can drop for a sharper model (on _synth_blur_pl the internal val TTO PSNR fell from 31.4 to 30.1 while the GT comparison improved by 3.9 dB): judge M1 by LPIPS, the sharpness ratio and the contact sheets.--water; quality_eval.py must render it as observed (see above).--frame_select (normalised to mean 1 here) or --filter_file frame_filter_s3.json, not both. Use --frame_select (see "B. Review" below: the filter-file route lowers the mean loss weight to ~0.6 and with it the densification gradients).Scope: fairness of the evaluation, the arm data sets, the trainer options. Scripts used for the checks are one-off (not committed); the fixes are in tests/quality_eval.py, quality_lib.py, 04d_hifps.py, 14_arm_data.py.
start + (i-1)/10, 30 fps start + (id-1)/30, start = window x 60 s; meta time_s agrees for every frame of every data set): in _s1, _s2, _s3, _l1, _c1 of all 6 fish no frame of meta / poses / frame_filter / frame_select lies within 0.3 s of a held-out frame. For _hf the frames within 0.3 s are exactly heldout.json exclude.hf (fish2 287, w12_fish3 140, w02_fish6 166, w58_fish8 91, w32_fish1 19, w40_fish7 182 ids), which 06_train_gs.py --exclude_frames auto removes (base name parsed correctly for fish2_hf and w32_fish1_hf). Training-eligible frames (valid pose, keep, weight > 0) after the exclusion: _hf 631 / 329 / 218 / 552 / 700 / 429 = S2 (fish2 629) = S3 = L1 = C1; S1 half. The trainer's initialisation, pose-hypothesis repair, robust weights and Binf estimate draw only on train_idx._hf processing on the frame ids both contain (93-700 per fish): identical crop squares, SAM masks (IoU 1.000), soft alpha and plates (MAD 0, fish2 ~1 level), poses within 0.12 deg._render_one = old render_frame, MB = 1, no global optimiser, lam_color_train = lambda_color); the one changed condition (fw != 1 instead of fw < 1) is equivalent because no frame_filter*.json weight in work/ exceeds 1 (135 files checked) and robust weights are <= 1. Together with the HEAD-vs-new runs above (identical loss at iteration 500) no rerun was needed. Unit tests: quality_opts_test.py unit, gs_test_units.py, hifps_unit.py all pass.J e^{-bD z} + alpha Binf (1 - e^{-bB z}) then + (1 - alpha) plate is right for a plate that already contains the water; depth = gsplat RGB+ED expected camera z (= accumulated z / alpha, bounded by the gaussians' z range even where alpha -> 0; unit test: plane at z = 3 exact in both backends), detached in training (colour cannot move geometry or poses); parameters log / logit (positive, Binf in (0, 1)); per-frame gain applied after the water as in the TTO. Works with --fins (composite unchanged, sheet gaussians attenuated like the body) and --deform (depth of the deformed gaussians, also in the batched hypothesis renders).tau = s e dt / 2 (total exposure e x frame interval, e = sigmoid, so 0 < e < 1 without a clamp); R(tau) = Exp(tau omega) R with omega fitted from Log(R_j R_i^T) in camera axes = rotation about the fish origin, consistent with p_cam = R p + t; nearest mode sign of f = s e dt / (2 (t_j - t_i)) correct for both neighbours and for extrapolation; motion detached, gradients reach the frame's pose / bending and e; densification sums the sub-render gradients (each carries 1/N of the loss, so the sum has the scale of a sharp render); neighbours limited to training frames of the same segment and < 30 deg (flips). Evaluation renders sharp.quality_eval.py): the fish box was the bbox of SAM mask OR rendered alpha > 0.5, so a model with a larger / bloated alpha got a larger box with more (easy) background. Now the box is the SAM mask bbox (+10 %), identical for every model; the old value is kept as lpips_fish_union. B0 recomputed from its stored fit (--reuse_tto; the union value reproduces the old numbers exactly; the previous file is kept as eval/B0/metrics_pre_review.json).| fish | sharp_ratio B0 | + noise 3 levels | + noise 6 levels | sharp_match B0 | + noise 6 | blur 1 px |
|---|---|---|---|---|---|---|
| fish2 | 0.119 | 0.142 | 0.202 | 0.279 | 0.281 | 0.240 |
| w12_fish3 | 0.398 | 0.403 | 0.416 | 0.366 | 0.366 | 0.336 |
| w02_fish6 | 0.595 | 0.599 | 0.625 | 0.729 | 0.727 | 0.670 |
| w58_fish8 | 0.180 | 0.802 | 2.594 | 0.126 | 0.125 | 0.127 |
| w32_fish1 | 0.131 | 0.249 | 0.704 | 0.032 | 0.034 | 0.035 |
| w40_fish7 | 0.033 | 0.108 | 0.374 | 0.015 | 0.010 | 0.018 |
(medians over the held-out frames; the photo itself + 3-level noise scores sharp_match 0.997-0.999.) A 3DGS model with speckle / floaters inside the fish could therefore "improve" the sharpness ratio without any real detail. New metrics in quality_eval.py: sharp_match = sum(grad render . grad photo) / sum |grad photo|^2 inside the eroded mask after a sigma 1.5 px pre-blur of both images (only detail in the right place counts; noise cannot raise it) and sharp_ratio_pb (energy ratio after the same pre-blur, less noise-sensitive). Decision rule: a sharp_ratio gain counts only if sharp_match (and LPIPS) do not get worse. For the dark, low-texture fish (w58_fish8, w32_fish1, w40_fish7) sharp_match is near 0 already for B0 (the photo's in-mask gradients are mostly grain and the render's detail is not in register), so there it is a weak signal; LPIPS stays primary.
quality_eval.py now renders a --water model as observed (water_observe with the render depth, parameters from model.pt; checked on w12_fish3_hf/qt_smoke_opts), and maps a model trained on a _c1 data set back with 1 / gains_rgb (found via colour_transform.json next to the model dir or in the data set named in model.pt), because the held-out crops / plates are uncorrected. Without it the TTO colour gain would have to undo a 1.4-1.9x red gain against its prior (0.1 x log(gain)^2: ~0.04 for w12_fish3, comparable to the whole loss) and C1 would lose by construction. Checked: with identity gains the metrics are bit-identical to B0. Note: canonical-view gradient energies (views_*) of C1 / C2 models are in corrected / restored colours (more contrast) and are not comparable with the other arms._hf (arm folders are 30 fps); it now reads the data set's meta fps. (New models store their times, so this only matters for old models.)bg_ok false are trained with the masked loss instead of the plate composite (the production --fins recipe), and the S2 selection ignored plates. Whether a 30 fps frame gets a plate is almost fully determined by its 10 fps frame (P(bg_ok | base bg_ok) 0.96-1.00, P(bg_ok | base not) 0.02-0.18), so it is known before the selection. Fix: 04d_hifps.py --sel-plate-factor F multiplies the arm score of base frames without a plate by F (default 1 = the original S2). New data sets <name>_s2p / <name>_s3p (F = 0.2) for the two fish where S2 had clearly fewer plates than _hf; for the other four S2 already has as many or more plate frames than _hf (fish2 498 vs 503, w12_fish3 188 vs 110, w02_fish6 186 vs 185, w58_fish8 437 vs 436) and _s2 / _s3 stay the arm. Training frames after the exclusion (medians; sharpness = native trimmed Laplacian energy of frame_quality.json):| fish / data set | training frames | with plate (bg_ok) | sharpness | ellipsoid fit | length px | runs |
|---|---|---|---|---|---|---|
w32_fish1 _hf (B0x) | 700 | 652 | 486 | 0.828 | 148 | 1 |
w32_fish1 _s2 | 700 | 379 | 837 (1.72x) | 0.815 | 138 | 24 |
w32_fish1 _s2p | 700 | 694 | 676 (1.39x) | 0.836 | 141 | 21 |
w40_fish7 _hf (B0x) | 429 | 275 | 1106 | 0.774 | 107 | 3 |
w40_fish7 _s2 | 429 | 248 | 1372 (1.24x) | 0.769 | 111 | 17 |
w40_fish7 _s2p | 429 | 302 | 1167 (1.06x) | 0.770 | 105 | 14 |
S2p keeps most of S2's sharpness gain for w32_fish1 with more plates than _hf; for w40_fish7 the sharpness advantage over _hf mostly disappears once plates are required (its sharper visits are the ones without a usable plate). Exclusion re-checked (0 frames near a held-out frame in _s2p / _s3p). _s3p = _s2p with the S3 weights (mean 0.626). Build: FISH_WINDOW=32 04d_hifps.py --fish w32_fish1 --out-suffix _s2p --select quality --exclude-heldout --pose-source like_hf --sel-plate-factor 0.2, 12_frame_quality.py --fish w32_fish1 --dataset _s2p, 14_arm_data.py --fish w32_fish1 --arms s3 --s3_from s2p --s3_to s3p (same for w40_fish7 with window 40). Recommendation: run S2 / S3 on _s2p / _s3p for these two fish (or both variants: the difference isolates the plate effect).
--filter_file frame_filter_s3.json is not equivalent to --frame_select: the filter weights are not normalised (mean 0.59-0.63 vs ~0.95 for the other arms), so every loss gradient, including the screen-space gradients that drive densification (--grad_thresh on their mean), shrinks by ~0.65 relative to the other arms (fewer gaussians, relatively stronger regularisers). Use 06_train_gs.py --fish <name>_s3 --frame_select (weights normalised to mean 1 over the training frames)._hf-based runs with --exclude_frames the excluded frames stay in the data set (untrained); their mask areas still enter the occlusion / area-drop flags of neighbouring frames. Arm folders do not contain them. Negligible information, but not byte-for-byte the same setting.q_arm) and the C1 strength (0.8) are untuned judgement calls.scripts/run_quality_matrix.py: 4 GPU jobs at a time, evaluations before new trainings, 20-minute stale-log watchdog with retry, resumable state in work/quality_exp/matrix/state.json, control.json to skip or add jobs while it runs, and a restarted driver adopts the still-running children of the previous one (it was restarted twice, to add arms; no job was lost or run twice). Every PID is in work/pids_qexp.txt. Report scripts/tests/quality_matrix_report.py writes work/quality_exp/matrix/results.{md,json}, work/quality_exp/<fish>/compare.png (real | B0 | B0x | the 3 arms with the lowest LPIPS, 8 held-out frames on the fish box, plus the canonical flank / top view) and compare_views.png (canonical flank / top / head views of the same models at 384 px).06_train_gs.py --fish <data set> --fins --deform --iters 4000 --hyp_max_frames 300 --exclude_frames auto [--res 384 for fish2 / w02_fish6] --out q_<arm> (the gs_final recipe plus the exclusion), then tests/quality_eval.py --model <dir> --fish <name> --tag <arm> (evaluator after the review fixes of edf1054). The evaluator reports 0 contaminated held-out frames for every trained model._hf), B1 (_hf, 12000 iterations), S1 (_s1), S2 (_s2; _s2p for w32_fish1 / w40_fish7, review fix 5), S3 (_s3 / _s3p, --frame_select), C1 (_c1), C2 (_hf --water), M1 (_hf --motion_blur 3, windowed motion fit), L1 (_l1). B0 is the existing gs_final evaluation (post-review numbers, not retrained)._hf frames with S1's size via --frame_select (scripts/tests/make_s1r_select.py, seed 12345; fish2, w02_fish6, w58_fish8, w32_fish1). S1r tells whether S1's effect comes from choosing good frames or just from having fewer frames, i.e. twice the updates per frame at the same iteration count._s2 / _s3 of w32_fish1 / w40_fish7 (without the plate fix) not run; M1 with 3 sub-renders, not 5.gsplat training is not deterministic. Running the identical B0x command again (B0x2) changes the held-out LPIPS by up to 0.0034, the sharpness ratio by up to x1.14 and masked PSNR by up to 0.15 dB (3 fish). w32_fish1's B0 is a fourth near-rerun: it shares 97 % of B0x's training frames and differs by +0.0018 LPIPS. By the plan's rule a rerun "wins" on 2 of 3 fish, and w32_fish1's rerun is "significantly" worse by the paired Wilcoxon test (p < 0.001, 20 % of frames better). The paired test only covers the sampling of held-out frames, not the training randomness, so per-fish p-values overstate significance. Effects below about 0.0035 LPIPS or x1.15 sharpness on one fish are noise. The table "Beyond the training noise floor" below counts only changes above the largest rerun difference. With 3 reruns that is still a lenient threshold.
On the three fish whose held-out frames were mostly inside gs_final's training window (fish2 16/16, w02_fish6 10/12, w12_fish3 9/24), B0 beats its fair retrain B0x by 0.011-0.020 LPIPS and 0.86-1.35 dB masked PSNR. On the others the gap is 0.004 / 0.2 dB or less. fish2's B0 also had 10000 iterations. Phase A's B0 numbers were optimistic by more than any arm gains, so all comparisons below are against B0x.
_hf frames):_s2p / _s3p) have no bright patch below the belly and a proper caudal peduncle (compare_views.png). B0, B0x, B1, S1r and C2 all keep it; S1 shrinks it; B1 renders it sharper and more prominent (bright fin-like streaks below the belly)._hf does not cover, and on w58_fish8 all of it does; see D2.I looked at compare.png and compare_views.png of all six fish (plus extra sheets with B0x2 / S1r / C2):
work/quality_exp/matrix/results.md, generated by tests/quality_matrix_report.py)Held-out set and protocol of section A (identical for every arm). "win" = the plan's rule vs B0x: LPIPS (fish box, mean) lower AND sharpness ratio (median) higher AND sharp_match (median) not lower AND masked PSNR not more than 0.3 dB lower; adopt at >= 4/6 fish.
| fish | arm | PSNR full | PSNR mask | LPIPS fish | sharp ratio | sharp match | IoU | flank view grad E | gaussians | train min (job wall / optimisation; 4 jobs share the GPU) | contam. |
|---|---|---|---|---|---|---|---|---|---|---|---|
| fish2 | B0 | 24.14 | 21.31 | 0.1694 | 0.119 | 0.279 | 0.897 | 12.51 | 26908 | - / 62 | 16/16 |
| fish2 | B0x | 23.68 | 20.45 | 0.1889 | 0.077 | 0.095 | 0.899 | 8.15 | 20105 | 32 / 24 | 0/16 |
| fish2 | B1 | 23.57 | 20.45 | 0.1781 | 0.152 | 0.163 | 0.892 | 22.62 | 33646 | 90 / 82 | 0/16 |
| fish2 | S1 | 23.58 | 20.44 | 0.1708 | 0.207 | 0.242 | 0.891 | 14.19 | 22850 | 37 / 31 | 0/16 |
| fish2 | S1r | 23.60 | 20.45 | 0.1807 | 0.103 | 0.180 | 0.895 | 9.46 | 22342 | 32 / 23 | 0/16 |
| fish2 | S2 | 23.63 | 20.45 | 0.1890 | 0.085 | 0.104 | 0.899 | 7.00 | 20027 | 33 / 25 | 0/16 |
| fish2 | S3 | 23.65 | 20.50 | 0.1865 | 0.085 | 0.152 | 0.896 | 6.78 | 19894 | 30 / 23 | 0/16 |
| fish2 | C1 | 23.66 | 20.42 | 0.1907 | 0.086 | 0.119 | 0.899 | 5.63 | 20174 | 31 / 24 | 0/16 |
| fish2 | C2 | 23.65 | 20.34 | 0.1981 | 0.068 | 0.106 | 0.896 | 5.57 | 18928 | 33 / 25 | 0/16 |
| fish2 | M1 | 23.62 | 20.34 | 0.1880 | 0.095 | 0.090 | 0.899 | 6.82 | 20671 | 50 / 42 | 0/16 |
| fish2 | L1 | 23.62 | 20.40 | 0.1916 | 0.075 | 0.127 | 0.897 | 6.46 | 20058 | 30 / 23 | 0/16 |
| w12_fish3 | B0 | 21.85 | 17.15 | 0.2142 | 0.398 | 0.366 | 0.885 | 59.32 | 46076 | - / 18 | 9/24 |
| w12_fish3 | B0x | 21.02 | 16.17 | 0.2304 | 0.431 | 0.238 | 0.877 | 56.46 | 46662 | 26 / 22 | 0/24 |
| w12_fish3 | B0x2 | 21.01 | 16.21 | 0.2270 | 0.470 | 0.256 | 0.881 | 70.24 | 46379 | 25 / 21 | 0/24 |
| w12_fish3 | B1 | 20.80 | 15.88 | 0.2251 | 0.476 | 0.232 | 0.874 | 47.71 | 45227 | 78 / 74 | 0/24 |
| w12_fish3 | S1 | 20.51 | 15.54 | 0.2257 | 0.689 | 0.213 | 0.872 | 157.12 | 46212 | 19 / 17 | 0/24 |
| w12_fish3 | S2 | 21.06 | 16.16 | 0.2194 | 0.487 | 0.239 | 0.886 | 90.35 | 46729 | 28 / 23 | 0/24 |
| w12_fish3 | S3 | 21.16 | 16.30 | 0.2135 | 0.520 | 0.244 | 0.875 | 101.37 | 46615 | 25 / 22 | 0/24 |
| w12_fish3 | C1 | 21.03 | 16.20 | 0.2269 | 0.441 | 0.203 | 0.874 | 62.77 | 46777 | 26 / 22 | 0/24 |
| w12_fish3 | C2 | 20.85 | 15.99 | 0.2341 | 0.492 | 0.226 | 0.878 | 119.53 | 45758 | 28 / 24 | 0/24 |
| w12_fish3 | M1 | 20.89 | 16.03 | 0.2286 | 0.564 | 0.211 | 0.882 | 97.82 | 46808 | 43 / 39 | 0/24 |
| w12_fish3 | L1 | 21.04 | 16.24 | 0.2227 | 0.498 | 0.254 | 0.883 | 70.26 | 46702 | 24 / 21 | 0/24 |
| w02_fish6 | B0 | 24.27 | 17.93 | 0.1039 | 0.595 | 0.729 | 0.872 | 280.02 | 24350 | - / 17 | 10/12 |
| w02_fish6 | B0x | 23.20 | 16.58 | 0.1149 | 0.625 | 0.649 | 0.872 | 283.27 | 25096 | 22 / 19 | 0/12 |
| w02_fish6 | B1 | 23.34 | 16.80 | 0.1197 | 0.631 | 0.666 | 0.866 | 280.79 | 38163 | 66 / 63 | 0/12 |
| w02_fish6 | S1 | 23.27 | 16.36 | 0.1130 | 0.693 | 0.732 | 0.872 | 323.45 | 29944 | 17 / 16 | 0/12 |
| w02_fish6 | S1r | 22.92 | 16.10 | 0.1178 | 0.659 | 0.687 | 0.865 | 268.60 | 27931 | 22 / 19 | 0/12 |
| w02_fish6 | S2 | 23.36 | 16.80 | 0.1160 | 0.581 | 0.649 | 0.869 | 278.40 | 26959 | 23 / 21 | 0/12 |
| w02_fish6 | S3 | 23.36 | 16.70 | 0.1165 | 0.592 | 0.670 | 0.875 | 285.41 | 25770 | 22 / 19 | 0/12 |
| w02_fish6 | C1 | 23.20 | 16.58 | 0.1189 | 0.619 | 0.642 | 0.867 | 280.32 | 26095 | 22 / 19 | 0/12 |
| w02_fish6 | C2 | 23.20 | 16.60 | 0.1177 | 0.596 | 0.635 | 0.874 | 284.21 | 25437 | 25 / 22 | 0/12 |
| w02_fish6 | M1 | 23.10 | 16.46 | 0.1251 | 0.681 | 0.618 | 0.873 | 299.55 | 26305 | 61 / 57 | 0/12 |
| w02_fish6 | L1 | 23.16 | 16.56 | 0.1230 | 0.644 | 0.645 | 0.868 | 272.91 | 25501 | 32 / 28 | 0/12 |
| w58_fish8 | B0 | 25.79 | 25.66 | 0.1163 | 0.172 | 0.126 | 0.919 | 0.13 | 19978 | - / 18 | 5/23 |
| w58_fish8 | B0x | 25.69 | 25.45 | 0.1200 | 0.179 | 0.113 | 0.919 | 0.16 | 20262 | 30 / 23 | 0/23 |
| w58_fish8 | B0x2 | 25.71 | 25.48 | 0.1196 | 0.196 | 0.127 | 0.919 | 0.14 | 20364 | 29 / 22 | 0/23 |
| w58_fish8 | B1 | 25.76 | 25.68 | 0.1159 | 0.256 | 0.144 | 0.915 | 0.16 | 26769 | 84 / 77 | 0/23 |
| w58_fish8 | S1 | 25.67 | 25.37 | 0.1176 | 0.261 | 0.113 | 0.916 | 0.11 | 21135 | 23 / 20 | 0/23 |
| w58_fish8 | S1r | 25.80 | 25.74 | 0.1147 | 0.181 | 0.150 | 0.916 | 0.13 | 20524 | 26 / 21 | 0/23 |
| w58_fish8 | S2 | 25.86 | 26.08 | 0.1160 | 0.150 | 0.160 | 0.921 | 0.09 | 21437 | 30 / 23 | 0/23 |
| w58_fish8 | S3 | 25.86 | 26.23 | 0.1142 | 0.147 | 0.172 | 0.923 | 0.09 | 21607 | 29 / 23 | 0/23 |
| w58_fish8 | C1 | 25.76 | 25.54 | 0.1167 | 0.206 | 0.128 | 0.920 | 0.15 | 20211 | 29 / 23 | 0/23 |
| w58_fish8 | C2 | 25.66 | 25.43 | 0.1216 | 0.261 | 0.131 | 0.920 | 0.55 | 19592 | 31 / 25 | 0/23 |
| w58_fish8 | M1 | 25.68 | 25.41 | 0.1181 | 0.223 | 0.122 | 0.919 | 0.38 | 20443 | 47 / 41 | 0/23 |
| w32_fish1 | B0 | 22.22 | 18.68 | 0.1576 | 0.130 | 0.032 | 0.902 | 6.70 | 22914 | - / 23 | 1/40 |
| w32_fish1 | B0x | 22.21 | 18.72 | 0.1558 | 0.121 | 0.039 | 0.900 | 0.29 | 23031 | 36 / 27 | 0/40 |
| w32_fish1 | B0x2 | 22.15 | 18.57 | 0.1586 | 0.138 | 0.035 | 0.901 | 0.30 | 23163 | 31 / 23 | 0/40 |
| w32_fish1 | B1 | 22.18 | 18.57 | 0.1503 | 0.186 | 0.049 | 0.898 | 0.50 | 36961 | 84 / 76 | 0/40 |
| w32_fish1 | S1 | 22.24 | 18.70 | 0.1519 | 0.201 | 0.047 | 0.903 | 12.21 | 24157 | 26 / 22 | 0/40 |
| w32_fish1 | S1r | 22.20 | 18.61 | 0.1532 | 0.118 | 0.035 | 0.902 | 12.72 | 24066 | 24 / 17 | 0/40 |
| w32_fish1 | S2 | 22.63 | 19.02 | 0.1531 | 0.089 | 0.041 | 0.905 | 0.32 | 22238 | 33 / 25 | 0/40 |
| w32_fish1 | S3 | 22.72 | 19.04 | 0.1503 | 0.077 | 0.050 | 0.909 | 0.32 | 22154 | 32 / 24 | 0/40 |
| w32_fish1 | C1 | 22.16 | 18.66 | 0.1586 | 0.127 | 0.048 | 0.900 | 0.35 | 23399 | 31 / 23 | 0/40 |
| w32_fish1 | C2 | 22.20 | 18.67 | 0.1567 | 0.109 | 0.040 | 0.902 | 7.12 | 22427 | 34 / 26 | 0/40 |
| w32_fish1 | M1 | 22.18 | 18.69 | 0.1584 | 0.133 | 0.040 | 0.899 | 0.32 | 23491 | 51 / 42 | 0/40 |
| w40_fish7 | B0 | 20.63 | 17.85 | 0.1983 | 0.033 | 0.015 | 0.870 | 0.63 | 18485 | - / 23 | 10/13 |
| w40_fish7 | B0x | 20.51 | 17.76 | 0.2012 | 0.033 | 0.023 | 0.867 | 0.77 | 17733 | 31 / 26 | 0/13 |
| w40_fish7 | B1 | 20.74 | 17.85 | 0.1981 | 0.141 | 0.031 | 0.871 | 2.51 | 26176 | 74 / 69 | 0/13 |
| w40_fish7 | S1 | 20.35 | 17.62 | 0.2058 | 0.165 | 0.030 | 0.864 | 3.17 | 22100 | 23 / 20 | 0/13 |
| w40_fish7 | S2 | 20.62 | 17.94 | 0.2005 | 0.046 | 0.023 | 0.871 | 1.00 | 18476 | 27 / 23 | 0/13 |
| w40_fish7 | S3 | 20.55 | 17.93 | 0.2031 | 0.070 | 0.018 | 0.870 | 1.01 | 19264 | 27 / 22 | 0/13 |
| w40_fish7 | C1 | 20.49 | 17.65 | 0.2030 | 0.039 | 0.023 | 0.866 | 1.10 | 18292 | 27 / 22 | 0/13 |
| w40_fish7 | C2 | 20.54 | 17.81 | 0.2045 | 0.039 | 0.028 | 0.868 | 1.25 | 17563 | 29 / 24 | 0/13 |
| w40_fish7 | M1 | 20.46 | 17.68 | 0.2036 | 0.036 | 0.028 | 0.865 | 0.91 | 18012 | 45 / 40 | 0/13 |
| fish | arm | dPSNR full | dPSNR mask | dLPIPS | frames better (p) | sharp ratio x | d sharp match | dIoU | flank grad E x | win |
|---|---|---|---|---|---|---|---|---|---|---|
| fish2 | B0 | +0.46 | +0.86 | -0.0195 | 88% (0.000) | 1.55 | 0.184 | -0.002 | 1.53 | yes |
| fish2 | B1 | -0.11 | +0.00 | -0.0108 | 88% (0.002) | 1.97 | 0.069 | -0.007 | 2.78 | yes |
| fish2 | S1 | -0.10 | -0.01 | -0.0181 | 81% (0.001) | 2.69 | 0.147 | -0.008 | 1.74 | yes |
| fish2 | S1r | -0.08 | -0.00 | -0.0082 | 81% (0.006) | 1.34 | 0.085 | -0.004 | 1.16 | yes |
| fish2 | S2 | -0.05 | +0.00 | +0.0001 | 69% (0.404) | 1.10 | 0.009 | -0.000 | 0.86 | no |
| fish2 | S3 | -0.03 | +0.05 | -0.0025 | 69% (0.130) | 1.11 | 0.057 | -0.002 | 0.83 | yes |
| fish2 | C1 | -0.02 | -0.03 | +0.0018 | 31% (0.051) | 1.12 | 0.025 | +0.000 | 0.69 | no |
| fish2 | C2 | -0.03 | -0.11 | +0.0092 | 6% (0.000) | 0.88 | 0.011 | -0.003 | 0.68 | no |
| fish2 | M1 | -0.06 | -0.10 | -0.0009 | 62% (0.375) | 1.24 | -0.005 | +0.000 | 0.84 | no |
| fish2 | L1 | -0.06 | -0.05 | +0.0027 | 31% (0.029) | 0.97 | 0.032 | -0.002 | 0.79 | no |
| w12_fish3 | B0 | +0.84 | +0.98 | -0.0162 | 79% (0.002) | 0.92 | 0.129 | +0.008 | 1.05 | no |
| w12_fish3 | B0x2 | -0.00 | +0.03 | -0.0034 | 58% (0.252) | 1.09 | 0.018 | +0.004 | 1.24 | yes |
| w12_fish3 | B1 | -0.21 | -0.29 | -0.0053 | 62% (0.089) | 1.10 | -0.006 | -0.003 | 0.85 | no |
| w12_fish3 | S1 | -0.51 | -0.63 | -0.0046 | 62% (0.178) | 1.60 | -0.025 | -0.005 | 2.78 | no |
| w12_fish3 | S2 | +0.05 | -0.02 | -0.0110 | 62% (0.049) | 1.13 | 0.002 | +0.009 | 1.60 | yes |
| w12_fish3 | S3 | +0.15 | +0.13 | -0.0169 | 83% (0.001) | 1.21 | 0.006 | -0.001 | 1.80 | yes |
| w12_fish3 | C1 | +0.02 | +0.02 | -0.0035 | 54% (0.218) | 1.02 | -0.035 | -0.003 | 1.11 | no |
| w12_fish3 | C2 | -0.16 | -0.19 | +0.0037 | 33% (0.069) | 1.14 | -0.012 | +0.002 | 2.12 | no |
| w12_fish3 | M1 | -0.12 | -0.14 | -0.0017 | 58% (0.456) | 1.31 | -0.026 | +0.006 | 1.73 | no |
| w12_fish3 | L1 | +0.02 | +0.06 | -0.0076 | 79% (0.002) | 1.16 | 0.017 | +0.006 | 1.24 | yes |
| w02_fish6 | B0 | +1.07 | +1.35 | -0.0109 | 75% (0.021) | 0.95 | 0.080 | +0.000 | 0.99 | no |
| w02_fish6 | B1 | +0.14 | +0.22 | +0.0048 | 67% (0.910) | 1.01 | 0.018 | -0.006 | 0.99 | no |
| w02_fish6 | S1 | +0.08 | -0.22 | -0.0019 | 58% (0.677) | 1.11 | 0.084 | +0.000 | 1.14 | yes |
| w02_fish6 | S1r | -0.27 | -0.48 | +0.0030 | 17% (0.064) | 1.05 | 0.038 | -0.007 | 0.95 | no |
| w02_fish6 | S2 | +0.17 | +0.22 | +0.0011 | 58% (0.791) | 0.93 | 0.001 | -0.003 | 0.98 | no |
| w02_fish6 | S3 | +0.16 | +0.12 | +0.0017 | 50% (0.733) | 0.95 | 0.021 | +0.003 | 1.01 | no |
| w02_fish6 | C1 | -0.00 | -0.00 | +0.0041 | 50% (0.470) | 0.99 | -0.006 | -0.005 | 0.99 | no |
| w02_fish6 | C2 | +0.00 | +0.02 | +0.0028 | 42% (0.301) | 0.95 | -0.014 | +0.002 | 1.00 | no |
| w02_fish6 | M1 | -0.09 | -0.12 | +0.0102 | 8% (0.001) | 1.09 | -0.031 | +0.001 | 1.06 | no |
| w02_fish6 | L1 | -0.04 | -0.02 | +0.0081 | 17% (0.007) | 1.03 | -0.004 | -0.004 | 0.96 | no |
| w58_fish8 | B0 | +0.10 | +0.21 | -0.0037 | 91% (0.000) | 0.96 | 0.013 | +0.000 | 0.81 | no |
| w58_fish8 | B0x2 | +0.01 | +0.03 | -0.0004 | 57% (0.377) | 1.09 | 0.014 | +0.000 | 0.88 | yes |
| w58_fish8 | B1 | +0.07 | +0.24 | -0.0042 | 83% (0.001) | 1.43 | 0.031 | -0.003 | 0.97 | yes |
| w58_fish8 | S1 | -0.02 | -0.08 | -0.0024 | 57% (0.111) | 1.46 | 0.000 | -0.003 | 0.66 | yes |
| w58_fish8 | S1r | +0.11 | +0.29 | -0.0053 | 87% (0.000) | 1.01 | 0.037 | -0.003 | 0.78 | yes |
| w58_fish8 | S2 | +0.17 | +0.64 | -0.0040 | 61% (0.016) | 0.84 | 0.046 | +0.002 | 0.52 | no |
| w58_fish8 | S3 | +0.17 | +0.78 | -0.0058 | 65% (0.048) | 0.82 | 0.059 | +0.004 | 0.56 | no |
| w58_fish8 | C1 | +0.06 | +0.10 | -0.0033 | 78% (0.002) | 1.15 | 0.015 | +0.002 | 0.91 | yes |
| w58_fish8 | C2 | -0.03 | -0.02 | +0.0016 | 26% (0.052) | 1.46 | 0.018 | +0.001 | 3.34 | no |
| w58_fish8 | M1 | -0.01 | -0.03 | -0.0020 | 78% (0.000) | 1.25 | 0.009 | -0.000 | 2.31 | yes |
| w32_fish1 | B0 | +0.01 | -0.04 | +0.0018 | 40% (0.118) | 1.08 | -0.007 | +0.002 | 22.81 | no |
| w32_fish1 | B0x2 | -0.06 | -0.15 | +0.0028 | 20% (0.000) | 1.14 | -0.004 | +0.001 | 1.03 | no |
| w32_fish1 | B1 | -0.03 | -0.15 | -0.0055 | 80% (0.000) | 1.54 | 0.010 | -0.002 | 1.71 | yes |
| w32_fish1 | S1 | +0.03 | -0.02 | -0.0039 | 80% (0.004) | 1.66 | 0.008 | +0.003 | 41.61 | yes |
| w32_fish1 | S1r | -0.01 | -0.11 | -0.0026 | 62% (0.039) | 0.97 | -0.005 | +0.002 | 43.34 | no |
| w32_fish1 | S2 | +0.42 | +0.30 | -0.0027 | 65% (0.094) | 0.73 | 0.002 | +0.005 | 1.10 | no |
| w32_fish1 | S3 | +0.51 | +0.32 | -0.0055 | 68% (0.006) | 0.64 | 0.011 | +0.009 | 1.10 | no |
| w32_fish1 | C1 | -0.05 | -0.06 | +0.0028 | 25% (0.001) | 1.05 | 0.009 | +0.000 | 1.20 | no |
| w32_fish1 | C2 | -0.01 | -0.05 | +0.0009 | 30% (0.044) | 0.90 | 0.001 | +0.002 | 24.27 | no |
| w32_fish1 | M1 | -0.03 | -0.02 | +0.0026 | 40% (0.027) | 1.10 | 0.001 | -0.001 | 1.10 | no |
| w40_fish7 | B0 | +0.12 | +0.09 | -0.0029 | 54% (0.588) | 1.00 | -0.008 | +0.003 | 0.82 | no |
| w40_fish7 | B1 | +0.23 | +0.09 | -0.0030 | 77% (0.127) | 4.32 | 0.007 | +0.004 | 3.27 | yes |
| w40_fish7 | S1 | -0.17 | -0.15 | +0.0046 | 31% (0.305) | 5.05 | 0.007 | -0.003 | 4.13 | no |
| w40_fish7 | S2 | +0.11 | +0.17 | -0.0007 | 38% (0.305) | 1.41 | 0.000 | +0.004 | 1.30 | yes |
| w40_fish7 | S3 | +0.04 | +0.17 | +0.0019 | 46% (0.305) | 2.15 | -0.005 | +0.003 | 1.32 | no |
| w40_fish7 | C1 | -0.02 | -0.11 | +0.0018 | 46% (0.168) | 1.19 | -0.000 | -0.001 | 1.43 | no |
| w40_fish7 | C2 | +0.03 | +0.05 | +0.0033 | 15% (0.003) | 1.18 | 0.004 | +0.001 | 1.63 | no |
| w40_fish7 | M1 | -0.05 | -0.08 | +0.0024 | 15% (0.017) | 1.10 | 0.005 | -0.002 | 1.18 | no |
| arm | fish | wins | LPIPS better | sharp better | match ok | PSNR ok | LPIPS sig. better / worse | mean dLPIPS | mean dPSNR mask | mean dPSNR full | sharp ratio x (geo) | adopt (>= 4/6) | won on |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| B0 | 6 | 1 | 5 | 2 | 4 | 6 | 4 / 0 | -0.0086 | +0.57 | +0.43 | 1.06 | no | fish2 |
| B0x2 | 3 | 2 | 2 | 3 | 2 | 3 | 0 / 1 | -0.0003 | -0.03 | -0.02 | 1.11 | no | w12_fish3, w58_fish8 |
| B1 | 6 | 4 | 5 | 6 | 5 | 6 | 3 / 0 | -0.0040 | +0.02 | +0.01 | 1.66 | yes | fish2, w58_fish8, w32_fish1, w40_fish7 |
| S1 | 6 | 4 | 5 | 6 | 5 | 5 | 2 / 0 | -0.0044 | -0.18 | -0.12 | 1.97 | yes | fish2, w02_fish6, w58_fish8, w32_fish1 |
| S1r | 4 | 2 | 3 | 3 | 3 | 3 | 3 / 0 | -0.0033 | -0.07 | -0.06 | 1.09 | no | fish2, w58_fish8 |
| S2 | 6 | 2 | 4 | 3 | 6 | 6 | 2 / 0 | -0.0028 | +0.22 | +0.14 | 1.00 | no | w12_fish3, w40_fish7 |
| S3 | 6 | 2 | 4 | 3 | 5 | 6 | 3 / 0 | -0.0045 | +0.26 | +0.17 | 1.06 | no | fish2, w12_fish3 |
| C1 | 6 | 1 | 2 | 5 | 3 | 6 | 1 / 1 | +0.0006 | -0.01 | -0.00 | 1.08 | no | w58_fish8 |
| C2 | 6 | 0 | 0 | 3 | 4 | 6 | 0 / 3 | +0.0036 | -0.05 | -0.03 | 1.07 | no | |
| M1 | 6 | 1 | 3 | 6 | 3 | 6 | 1 / 3 | +0.0018 | -0.08 | -0.06 | 1.18 | no | w58_fish8 |
| L1 | 3 | 1 | 1 | 2 | 2 | 3 | 1 / 2 | +0.0011 | -0.00 | -0.03 | 1.05 | no | w12_fish3 |
| arm | fish | LPIPS better by > noise | LPIPS worse by > noise | sharp ratio x > noise | clear wins (both + match ok + PSNR ok) |
|---|---|---|---|---|---|
| B0 | 6 | 4 | 0 | 1 | 1 |
| B0x2 | 3 | 0 | 0 | 0 | 0 |
| B1 | 6 | 4 | 1 | 4 | 3 |
| S1 | 6 | 3 | 1 | 5 | 2 |
| S1r | 4 | 2 | 0 | 1 | 1 |
| S2 | 6 | 2 | 0 | 1 | 0 |
| S3 | 6 | 3 | 0 | 2 | 1 |
| C1 | 6 | 1 | 1 | 2 | 0 |
| C2 | 6 | 0 | 2 | 2 | 0 |
| M1 | 6 | 0 | 1 | 3 | 0 |
| L1 | 3 | 1 | 1 | 1 | 1 |
| fish | arm | exposure | bD x scale (RGB) | bB x scale (RGB) | Binf (RGB) |
|---|---|---|---|---|---|
| fish2 | C2 | - | 0.001 0.002 0.001 | 0.007 0.017 0.017 | 0.196 0.821 0.802 |
| fish2 | M1 | 0.405 | - | - | - |
| w12_fish3 | C2 | - | 0.021 0.006 0.008 | 0.001 0.005 0.006 | 0.032 0.354 0.315 |
| w12_fish3 | M1 | 0.242 | - | - | - |
| w02_fish6 | C2 | - | 0.002 0.002 0.002 | 0.003 0.006 0.009 | 0.065 0.498 0.533 |
| w02_fish6 | M1 | 0.240 | - | - | - |
| w58_fish8 | C2 | - | 0.001 0.004 0.003 | 0.009 0.013 0.013 | 0.161 0.761 0.739 |
| w58_fish8 | M1 | 0.411 | - | - | - |
| w32_fish1 | C2 | - | 0.001 0.002 0.002 | 0.005 0.009 0.008 | 0.230 0.838 0.770 |
| w32_fish1 | M1 | 0.997 | - | - | - |
| w40_fish7 | C2 | - | 0.021 0.005 0.005 | 0.008 0.006 0.006 | 0.240 0.697 0.610 |
| w40_fish7 | M1 | 0.396 | - | - | - |
Review D (below) re-checked these conclusions and gives the go / no-go per change for the rollout (D5).
flank view grad E) is unstable on the dark fish: w32_fish1 jumps x22-43 between models with the belly artifact's edge. C1 / C2 views are in corrected / restored colours. Do not use it for decisions.Reproduce (from scripts/, venv python): python -u run_quality_matrix.py --slots 4 (B0x2 / S1r via work/quality_exp/matrix/control.json {"add": ["w12_fish3:B0x2", ..., "fish2:S1r", ...]} after python tests/make_s1r_select.py --fish fish2 w02_fish6 w58_fish8 w32_fish1), then python tests/quality_matrix_report.py.
A second, independent check of section C before the rollout decision. Script: scripts/tests/quality_review_d.py (reads only metrics.json, train_log.json, meta.json and heldout.json; no GPU).
--res (384 for the two 512 px fish, native otherwise), seed 0, --hyp_max_frames 300 and frame_filter.json. Trained frames after the trainer's validation split: the _hf, S2, S3, C1 and L1 arms are within 2 frames of each other; S1 / S1r have half (by design).B1_rev2 / S3_rev2):_hf never came nearThe held-out set is drawn from all visits (section A). _hf covers 1-3 visits; S2 / S3 draw from all of them. So the S2 data put training frames close in time (beyond the 0.3 s exclusion) to held-out frames whose visit _hf does not contain at all:
| fish | held-out frames with a training frame within 3 s, B0x -> S2/S3 | median distance to the nearest training frame |
|---|---|---|
| w58_fish8 | 5 -> 22 of 23 | 304 s -> 0.7 s |
| w32_fish1 | 2 -> 16 of 40 | 94 s -> 4.2 s |
| w12_fish3 | 7 -> 11 of 24 | 15.8 s -> 3.5 s |
| fish2, w02_fish6, w40_fish7 | unchanged (all near) | unchanged |
These are exactly the three fish where S2 / S3 beat the noise floor. Per held-out frame, dLPIPS vs B0x split by proximity (3 s radius, pooled over fish):
| arm | newly near (arm has a training frame within 3 s, B0x not) | far from both | near both |
|---|---|---|---|
| S3 | n 39, -0.0098 (77 % better) | n 34, -0.0110 (76 %) | n 51, +0.0007 (53 %) |
| S2 | n 39, -0.0039 (62 %) | n 34, -0.0094 (76 %) | n 51, +0.0007 (51 %) |
| B1 | - | n 73, -0.0057 (78 %) | n 55, -0.0033 (75 %) |
| B0x2 (rerun) | - | n 73, +0.0010 (36 %) | n 14, -0.0034 (64 %) |
How to read this:
_hf already had nearby frames, S2 / S3 gain nothing (+0.0007)._hf does not cover.--iters or run at a fixed interval: densification runs until 60 %, hypothesis rounds every 1500 iterations until 70 %, a robust round every 500 iterations. So B1 gets 5 hypothesis rounds instead of 2 and 23 robust rounds instead of 8.--iters 12000 does in production, so it is part of the change, not a defect.sharp_ratio_pb), B1's sharpening is x1.25 as a geometric mean (x1.66 raw).10_fish_videos.py) replay the longest contiguous runs of the data set. S2 replaces those runs with many short ones, so the replayed clips get shorter:| fish | longest run, _hf -> S2 |
|---|---|
| w32_fish1 | 24.0 s -> 3.4 s (9.4 s with _s2p) |
| w58_fish8 | 16.1 s -> 6.6 s |
| w40_fish7 | 14.4 s -> 6.5 s (10.0 s with _s2p) |
| w12_fish3 | 9.5 s -> 5.6 s |
S2 also needs the SAM + matting stage per fish (6-35 min of GPU each) and, for some fish, the plate-aware variant.
| change | verdict | condition / reason |
|---|---|---|
B1 (--iters 12000) | GO | The only gain that holds independently of visit coverage (D2, D4). Cost 3x training (66-90 min per fish with 4 jobs on the GPU). Watch: w02_fish6-like small fish got worse; artifacts get sharper (w32_fish1). Raise --max_gauss for fish near the 50000 cap. |
| S3 (all-visits data + soft weights) | NO-GO for the rollout now; phase C test only | Fails the plan's rule (2/6). Its mean gain is partly visit coverage (D2). It shortens the tracking / swim clips and needs the S2 data stage per fish. Re-test as B1 + S3 vs B1 with 2 seeds, scoring only held-out frames that both data sets cover equally (or far from both). Adopt only if it still wins and the videos are kept on _hf segments. |
| S2 | NO-GO | Superseded by S3 (same frames). |
| S1 (hard halving) | NO-GO | View holes (w12_fish3 -0.63 dB masked PSNR); half of its gain is the fewer-frames effect, which B1 gives without the holes. |
| C1 (plate white balance) | NO-GO | No effect. |
| C2 (water model) | NO-GO | LPIPS worse on 6/6; parameters not identifiable. |
| M1 (motion blur) | NO-GO | LPIPS worse on 3/6, sharp_match lower, 1.7x cost. |
| L1 (lossless crops) | NO-GO | No consistent effect (3 fish). |
This is the "rollout" step of the plan (phase C). Section D was already taken by the review, so it is E here.
Only B1 got a GO in D5, so there was nothing to combine. The v2 recipe is B1 plus the watch-out from D5, a higher gaussian cap:
06_train_gs.py --fish <name>_hf --fins --deform --iters 12000 --hyp_max_frames 300 [--res 384 for 512 px crops] --max_gauss 100000 --out gs_v2
Everything else is as in gs_final: the same data set (<name>_hf), seed and trainer. S3 was not combined (NO-GO in D5), and C1 / C2 / M1 / L1 were not used. There is no --water, so the turntables show the normal gaussian colours.
v2 = q_v2, with the held-out exclusion)run_quality_matrix.py --arms v2 --state work/quality_exp/matrix/state_v2.json (same protocol as section C: --exclude_frames auto, 0 contaminated held-out frames). Report: tests/quality_matrix_report.py --arms B0,B0x,B0x2,B1,v2 --suffix _v2 -> work/quality_exp/matrix/results_v2.{md,json}, work/quality_exp/<fish>/compare_v2.png / compare_views_v2.png (real | B0 | B0x | B1 | v2).
| fish | dLPIPS vs B0x: B1 / v2 | frames better (v2) | sharpness x raw: B1 / v2 | sharpness x pre-blurred: B1 / v2 | dPSNR mask: B1 / v2 | gaussians B1 / v2 | rule win: B1 / v2 |
|---|---|---|---|---|---|---|---|
| fish2 | -0.0108 / -0.0112 | 81 % | 1.97 / 1.95 | 1.28 / 1.55 | +0.00 / +0.01 | 33.6k / 33.9k | yes / yes |
| w12_fish3 | -0.0053 / -0.0054 | 71 % | 1.10 / 1.07 | 1.03 / 1.19 | -0.29 / -0.18 | 45.2k (cap) / 86.4k | no / yes |
| w02_fish6 | +0.0048 / +0.0013 | 75 % | 1.01 / 0.98 | 0.97 / 0.96 | +0.22 / +0.23 | 38.2k / 38.5k | no / no |
| w58_fish8 | -0.0042 / -0.0033 | 78 % | 1.43 / 1.33 | 1.23 / 1.11 | +0.24 / +0.28 | 26.8k / 26.9k | yes / yes |
| w32_fish1 | -0.0055 / -0.0064 | 90 % | 1.54 / 1.45 | 1.23 / 1.14 | -0.15 / -0.08 | 37.0k / 36.9k | yes / yes |
| w40_fish7 | -0.0030 / -0.0012 | 54 % | 4.32 / 3.92 | 2.00 / 1.93 | +0.09 / +0.16 | 26.2k / 26.6k | yes / no |
| all | mean -0.0040 / -0.0044 | geo 1.66 / 1.58 | geo 1.25 / 1.28 | +0.02 / +0.07 | 4/6 / 4/6 |
compare_views_v2.png its flank stripes look about as crisp as B1, perhaps slightly crisper. Nothing got worse, so the cap is kept for all fish. It only binds on fish that reach 50k.compare_v2.png of fish2 and compare_views_v2.png of w12_fish3): v2 looks like B1. The fish2 stripes are slightly crisper than B0x (held-out frames 254, 286 and 3033). Every render remains far softer than the photo.<!-- E3+ (rollout) filled in after the 44-fish run -->
Final production run on 2026-11-10 (13:15 to 20:15, one RTX 4090, at most 4 GPU jobs at once). Method: see docs/method_choice.md. Each fish gets 30 fps data from 04d_hifps.py, capped at 24 s of footage. It is then trained with 4D Gaussian splatting and the fin preset (06_train_gs.py --fins --deform --iters 4000 --hyp_max_frames 300 --out gs_final), and its videos come from 10_fish_videos.py.
gs_fins_4d, copied to gs_final). All other fish were trained here with 4000 iterations.Where to look:
work/gallery.html: one card per fish, with the poster, species label and links to the videos.work/site/ (built by scripts/11_site.py, served by scripts/serve_site.py): the full results website, with per-fish pages and an interactive 3D viewer.work/[w<MM>/]fish/<name>_hf/gs_final/, which holds point_cloud.ply, poses_refined.json, train_log.json, val_grid_comp.png and the videos/ folder. The species file is work/[w<MM>/]fish/<name>/species.json.work/production/results.json. Driver state and logs are in work/production/ and work/logs_produce/.Reproduce: cd scripts; ..\.venv\Scripts\python -u run_production.py --slots 4 (resumable: it skips steps whose outputs exist), then ..\.venv\Scripts\python make_results.py to rebuild the table below and work/gallery.html.
Column notes:
docs/review_notes.json; see "Review" below.<!-- table -->
| # | fish | window | seen (video time, 10 fps visits) | frames 10 fps / 30 fps | species top-1 (prob, confidence) | method | held-out PSNR full / masked | IoU | review | outputs |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | fish1 | 48:00 | 48:17-49:20, 49:58-50:47, 50:49-50:50 (+7 more) | 1728 / 721 | Cephalopholis hemistiktos (Yellowfin hind) 68%, medium | 30 fps fins+4D | 25.3 / 24.3 | 0.926 | fair | turntable turntable_swim tracking poster |
| 2 | fish2 | 48:00 | 48:02-48:12, 48:23-48:29, 49:41-49:47 (+2 more) | 359 / 1146 | Balistapus undulatus (Orange-lined triggerfish) 100%, high | 30 fps fins+4D, 10k iters | 25.4 / 20.3 | 0.889 | good | turntable turntable_swim tracking poster |
| 3 | fish3 | 48:00 | 48:09-48:12, 51:26-51:28, 51:57-51:59 (+25 more) | 910 / 739 | Chromis dimidiata (Chocolate-dip chromis) 99%, high | 30 fps fins+4D, 10k iters | 28.0 / 21.0 | 0.884 | good | turntable turntable_swim tracking poster |
| 4 | w02_fish1 | 02:00 | 02:46-02:50, 04:16-04:19, 05:12-05:17 (+9 more) | 461 / 724 | Halichoeres hortulanus (Checkerboard wrasse) 100%, high | 30 fps fins+4D | 24.9 / 18.2 | 0.883 | good | turntable turntable_swim tracking poster |
| 5 | w02_fish2 | 02:00 | 02:22-02:23, 02:31-02:33, 02:42-02:45 (+13 more) | 611 / 523 | Halichoeres hortulanus (Checkerboard wrasse) 99%, high | 30 fps fins+4D | 27.8 / 20.1 | 0.899 | good | turntable turntable_swim tracking poster |
| 6 | w02_fish4 | 02:00 | 05:01-05:03, 08:09-08:14, 09:03-09:10 (+4 more) | 355 / 715 | Balistapus undulatus (Orange-lined triggerfish) 100%, high | 30 fps fins+4D | 26.6 / 22.2 | 0.879 | good | turntable turntable_swim tracking poster |
| 7 | w02_fish6 | 02:00 | 05:43-05:45, 05:55-05:57, 06:58-07:03 (+3 more) | 166 / 513 | Labroides dimidiatus (Bluestreak cleaner wrasse) 100%, high | 30 fps fins+4D | 24.7 / 16.8 | 0.881 | good | turntable turntable_swim tracking poster |
| 8 | w02_fish7 | 02:00 | 03:22-03:24, 03:28-03:31, 07:37-07:40 (+9 more) | 355 / 720 | Chromis dimidiata (Chocolate-dip chromis) 97%, high | 30 fps fins+4D | 25.8 / 20.4 | 0.895 | good | turntable turntable_swim tracking poster |
| 9 | w02_fish8 | 02:00 | 07:10-07:12, 07:27-07:32, 08:21-08:30 (+2 more) | 206 / 619 | Oxycheilinus digramma (Cheeklined wrasse) 67%, medium | 30 fps fins+4D | 30.2 / 25.5 | 0.929 | fair, species doubtful | turntable turntable_swim tracking poster |
| 10 | w02_fish9 | 02:00 | 02:34-02:37, 05:15-05:17, 06:28-06:30 (+7 more) | 225 / 395 | Oxycheilinus digramma (Cheeklined wrasse) 36%, low | 30 fps fins+4D | 26.3 / 20.5 | 0.856 | fair, species doubtful | turntable turntable_swim tracking poster |
| 11 | w02_fish10 | 02:00 | 02:56-02:58, 03:02-03:05, 04:54-04:56 (+5 more) | 161 / 445 | Oxycheilinus digramma (Cheeklined wrasse) 41%, low | 30 fps fins+4D | 30.7 / 23.4 | 0.880 | fair, species doubtful | turntable turntable_swim tracking poster |
| 12 | w12_fish1 | 12:00 | 12:59-13:24, 13:39-14:03, 14:54-15:23 (+5 more) | 1776 / 721 | Cephalopholis hemistiktos (Yellowfin hind) 86%, high | 30 fps fins+4D | 24.2 / 21.3 | 0.822 | fair | turntable turntable_swim tracking poster |
| 13 | w12_fish2 | 12:00 | 13:23-13:25, 13:43-13:46, 13:53-14:05 (+7 more) | 709 / 677 | Halichoeres hortulanus (Checkerboard wrasse) 100%, high | 30 fps fins+4D | 24.7 / 17.2 | 0.873 | good | turntable turntable_swim tracking poster |
| 14 | w12_fish3 | 12:00 | 14:12-14:14, 14:17-14:25, 14:30-14:32 (+9 more) | 550 / 720 | Chaetodon paucifasciatus (Eritrean butterflyfish) 100%, high | 30 fps fins+4D | 21.7 / 17.1 | 0.872 | good | turntable turntable_swim tracking poster |
| 15 | w12_fish5 | 12:00 | 12:05-12:07, 13:11-13:14, 16:04-16:06 (+9 more) | 220 / 498 | Chromis dimidiata (Chocolate-dip chromis) 98%, high | 30 fps fins+4D | 26.5 / 19.6 | 0.870 | good | turntable turntable_swim tracking poster |
| 16 | w12_fish9 | 12:00 | 12:23-12:25, 12:30-12:36, 13:22-13:24 (+11 more) | 392 / 640 | Oxycheilinus digramma (Cheeklined wrasse) 36%, low | 30 fps fins+4D | 29.1 / 22.1 | 0.888 | fair, species doubtful | turntable turntable_swim tracking poster |
| 17 | w22_fish1 | 22:00 | 23:18-23:31, 24:09-24:13, 24:16-24:21 (+13 more) | 1592 / 721 | Cephalopholis hemistiktos (Yellowfin hind) 83%, high | 30 fps fins+4D | 24.0 / 22.2 | 0.775 | poor | turntable turntable_swim tracking poster |
| 18 | w22_fish2 | 22:00 | 25:02-25:03, 28:13-28:25, 29:28-29:31 (+3 more) | 265 / 704 | Balistapus undulatus (Orange-lined triggerfish) 98%, high | 30 fps fins+4D | 26.7 / 21.4 | 0.881 | good | turntable turntable_swim tracking poster |
| 19 | w22_fish3 | 22:00 | 25:23-25:26, 26:10-26:12, 26:18-26:20 (+8 more) | 218 / 422 | Chromis dimidiata (Chocolate-dip chromis) 92%, high | 30 fps fins+4D | 27.3 / 18.4 | 0.821 | good | turntable turntable_swim tracking poster |
| 20 | w22_fish4 | 22:00 | 29:04-29:11, 29:50-30:02 | 185 / 722 | Chaetodon paucifasciatus (Eritrean butterflyfish) 100%, high | 30 fps fins+4D | 22.7 / 16.8 | 0.879 | good | turntable turntable_swim tracking poster |
| 21 | w32_fish1 | 32:00 | 33:00-33:06, 33:38-33:54, 34:21-34:39 (+9 more) | 2172 / 721 | Cephalopholis hemistiktos (Yellowfin hind) 62%, medium | 30 fps fins+4D | 23.0 / 23.2 | 0.884 | fair | turntable turntable_swim tracking poster |
| 22 | w32_fish3 | 32:00 | 32:26-32:28, 34:53-34:55, 35:10-35:20 (+3 more) | 211 / 636 | Epinephelus fasciatus (Blacktip grouper) 60%, low | 30 fps fins+4D | 29.1 / 23.0 | 0.850 | fair, species doubtful | turntable turntable_swim tracking poster |
| 23 | w32_fish4 | 32:00 | 32:08-32:10, 33:12-33:14, 34:22-34:26 (+8 more) | 194 / 395 | Chromis dimidiata (Chocolate-dip chromis) 91%, high | 30 fps fins+4D | 26.9 / 19.9 | 0.840 | good | turntable turntable_swim tracking poster |
| 24 | w32_fish5 | 32:00 | 37:43-38:02 | 189 / 592 | Cephalopholis hemistiktos (Yellowfin hind) 95%, high | 30 fps fins+4D | 22.4 / 23.5 | 0.843 | poor | turntable turntable_swim tracking poster |
| 25 | w40_fish1 | 40:00 | 40:08-40:11, 40:42-40:51, 41:00-41:00 (+8 more) | 3259 / 721 | Cephalopholis hemistiktos (Yellowfin hind) 85%, high | 30 fps fins+4D | 24.1 / 22.2 | 0.830 | fair | turntable turntable_swim tracking poster |
| 26 | w40_fish2 | 40:00 | 42:07-42:10, 42:33-42:39, 43:15-43:22 (+8 more) | 708 / 723 | Balistapus undulatus (Orange-lined triggerfish) 100%, high | 30 fps fins+4D | 26.6 / 20.7 | 0.874 | good | turntable turntable_swim tracking poster |
| 27 | w40_fish3 | 40:00 | 40:00-40:02, 40:20-40:42, 40:44-40:50 (+1 more) | 341 / 722 | Cephalopholis hemistiktos (Yellowfin hind) 74%, medium | 30 fps fins+4D | 32.4 / 30.5 | 0.842 | fair | turntable turntable_swim tracking poster |
| 28 | w40_fish4 | 40:00 | 40:01-40:05, 40:10-40:12, 40:16-40:22 (+11 more) | 475 / 619 | Chromis dimidiata (Chocolate-dip chromis) 99%, high | 30 fps fins+4D | 26.2 / 19.2 | 0.848 | good | turntable turntable_swim tracking poster |
| 29 | w40_fish5 | 40:00 | 41:25-41:26, 41:42-41:44, 42:01-42:05 (+11 more) | 393 / 722 | Epinephelus fasciatus (Blacktip grouper) 62%, low | 30 fps fins+4D | 26.7 / 22.2 | 0.886 | fair, species doubtful | turntable turntable_swim tracking poster |
| 30 | w40_fish6 | 40:00 | 41:56-42:08, 42:11-42:11, 42:14-42:28 | 230 / 719 | Chaetodon paucifasciatus (Eritrean butterflyfish) 93%, high | 30 fps fins+4D | 25.1 / 18.7 | 0.864 | good | turntable turntable_swim tracking poster |
| 31 | w40_fish7 | 40:00 | 40:27-40:36, 40:38-40:41, 42:10-42:31 | 263 / 723 | Cephalopholis hemistiktos (Yellowfin hind) 73%, medium | 30 fps fins+4D | 19.7 / 22.4 | 0.834 | poor | turntable turntable_swim tracking poster |
| 32 | w58_fish1 | 58:00 | 58:08-58:12, 59:04-59:08, 61:20-61:30 (+6 more) | 698 / 722 | Cephalopholis hemistiktos (Yellowfin hind) 70%, medium | 30 fps fins+4D | 22.2 / 21.8 | 0.855 | poor | turntable turntable_swim tracking poster |
| 33 | w58_fish2 | 58:00 | 59:40-59:41, 59:45-60:06 | 203 / 658 | Cephalopholis hemistiktos (Yellowfin hind) 61%, medium | 30 fps fins+4D | 23.5 / 22.3 | 0.887 | fair | turntable turntable_swim tracking poster |
| 34 | w58_fish3 | 58:00 | 58:45-58:46, 58:49-58:54, 59:20-59:25 (+12 more) | 990 / 1192 | Cephalopholis miniata (Coral hind) 93%, high | 30 fps fins+4D | 26.1 / 21.0 | 0.906 | fair | turntable turntable_swim tracking poster |
| 35 | w58_fish5 | 58:00 | 62:36-63:25 | 450 / 712 | Cephalopholis hemistiktos (Yellowfin hind) 59%, medium | 30 fps fins+4D | 21.0 / 21.6 | 0.833 | poor | turntable turntable_swim tracking poster |
| 36 | w58_fish6 | 58:00 | 58:37-58:39, 58:43-58:47, 59:02-59:07 (+4 more) | 304 / 717 | Balistapus undulatus (Orange-lined triggerfish) 98%, high | 30 fps fins+4D | 30.1 / 23.8 | 0.901 | good | turntable turntable_swim tracking poster |
| 37 | w58_fish7 | 58:00 | 58:10-58:20, 59:02-59:07, 59:12-59:17 (+13 more) | 666 / 655 | Chromis dimidiata (Chocolate-dip chromis) 99%, high | 30 fps fins+4D | 27.6 / 20.5 | 0.882 | good | turntable turntable_swim tracking poster |
| 38 | w58_fish8 | 58:00 | 58:22-58:26, 58:52-59:00, 59:03-59:10 (+3 more) | 395 / 713 | Aethaloperca rogaa (Redmouth grouper) 83%, high | 30 fps fins+4D | 26.5 / 26.2 | 0.914 | good | turntable turntable_swim tracking poster |
| 39 | w58_fish9 | 58:00 | 58:35-58:38, 58:46-58:48, 58:54-58:57 (+12 more) | 478 / 490 | Halichoeres hortulanus (Checkerboard wrasse) 100%, high | 30 fps fins+4D | 28.8 / 20.5 | 0.913 | fair | turntable turntable_swim tracking poster |
| 40 | w58_fish10 | 58:00 | 58:29-58:29, 59:01-59:04, 61:00-61:02 (+11 more) | 332 / 706 | Oxycheilinus digramma (Cheeklined wrasse) 46%, low | 30 fps fins+4D | 27.1 / 20.9 | 0.890 | fair, species doubtful | turntable turntable_swim tracking poster |
| 41 | w58_fish11 | 58:00 | 58:31-58:33, 58:38-58:41, 58:45-58:48 (+8 more) | 307 / 681 | Halichoeres hortulanus (Checkerboard wrasse) 99%, high | 30 fps fins+4D | 27.8 / 18.4 | 0.892 | good | turntable turntable_swim tracking poster |
| 42 | w68_fish1 | 68:00 | 68:00-68:12, 68:29-68:31, 68:34-68:36 (+13 more) | 469 / 595 | Chromis dimidiata (Chocolate-dip chromis) 100%, high | 30 fps fins+4D | 26.5 / 19.4 | 0.874 | good | turntable turntable_swim tracking poster |
| 43 | w68_fish3 | 68:00 | 68:03-68:06, 68:32-68:34, 68:36-68:39 (+5 more) | 209 / 668 | Halichoeres hortulanus (Checkerboard wrasse) 98%, high | 30 fps fins+4D | 29.0 / 22.4 | 0.936 | good | turntable turntable_swim tracking poster |
| 44 | w68_fish5 | 68:00 | 69:53-69:58, 70:29-70:32, 70:45-70:47 (+1 more) | 149 / 439 | Aethaloperca rogaa (Redmouth grouper) 83%, high | 30 fps fins+4D | 26.1 / 24.2 | 0.897 | good | turntable turntable_swim tracking poster |
<!-- /table -->
What works
gs_robust. This is not a like-for-like comparison: the held-out frames, the frame rate and the PSNR definition all differ. The model is still a plain dark body (graded fair).Failure modes and caveats (honest list)
Their 3D bodies look plausible from the side but are flat and featureless. The model at the pose looks slightly thinner than the real fish, and the spots are lost.
The review step (2026-11-10) checked every row of the table against the files on disk and looked at the outputs. The grades and notes are in docs/review_notes.json; make_results.py reads that file for the review column and the gallery.
Checks against the files (all passed):
point_cloud.ply, model.pt, train_log.json, poses_refined.json, species.json, turntable.mp4, turntable_swim.mp4, tracking.mp4, poster.png and videos.json.final.val_tto of each train_log.json to the printed precision.videos.json) are the top-1 of its species.json.gs_final models are byte-identical to their gs_fins_4d. All other logs show --fins --deform, 4000 iterations and --out gs_final.What I looked at: all 44 posters, frames from the tracking, turntable and swim videos of 8 fish (fish2, w02_fish8, w12_fish3, w22_fish1, w32_fish1, w40_fish5, w40_fish7, w58_fish3), the 30 fps crop sheets of w40_fish7 and w32_fish1, and turntable tiles of the weakest hinds.
Grades:
Doubtful species labels (7):
The species pages of work/site show the classifier output without these flags.
Best results to look at first:
Decision (2026-11-10): 4D Gaussian splatting with the fin preset on 30 fps data (06_train_gs.py --fins --deform on the <name>_hf crops of 04d_hifps.py), shortened to 4000 iterations with --hyp_max_frames 300, and 04d capped at 24 s of 30 fps footage per fish (--max-total-hard-s 24, new option). The output folder is gs_final. fish1 is kept.
| name | flags | data | what it adds |
|---|---|---|---|
gs | --legacy-like baseline (sym, pose/colour opt) | 10 fps | binary SAM mask, black background |
gs_robust | default robust mode (hypothesis repair, MAD weights, occlusion-tolerant loss, frame filter) | 10 fps | robustness to toward/away flips, foreign fish, coral occluders |
gs_robust_4d | robust + --deform | 10 fps (or 30 fps for the ablation) | per-frame body bending |
gs_fins | robust + --fins (plate compositing, fin sheet) | 10 fps | translucent fins via compositing over the 04c background plate |
gs_fins_4d | robust + --fins --deform (+ caudal articulation) | 30 fps (*_hf, 04d) or 10 fps | fins + body bending + tail articulation |
Every 10th valid frame is held out; per-frame pose / colour are re-fitted on it (TTO) and the model is frozen. Full = PSNR of the render composited over the plate on the whole crop, masked = PSNR on the SAM mask, IoU against the SAM mask, near-fish = PSNR in the band just outside the mask (where fins live). Rows of one block use the same held-out frames. Sources: work/fish/<f>/eval_cmp/compare.json (round 3b), the review re-evaluation work/fish/<f>_hf/eval_rev/ and the train logs of the runs made for this decision (dec_*, logs in work/logs_decide/).
10 fps data (held-out 10 fps frames)
| fish | run | iters | full | masked | IoU | near-fish |
|---|---|---|---|---|---|---|
| fish2 (triggerfish) | gs | 12000 | 23.0 | 17.8 | 0.884 | 23.8 |
| gs_robust | 12000 | 22.8 | 18.0 | 0.884 | 23.3 | |
| gs_robust_4d | 12000 | 23.7 | 18.4 | 0.915 | 24.7 | |
| gs_fins | 12000 | 24.1 | 18.7 | 0.872 | 24.8 | |
| fish3 (chromis) | gs | 28058 | 24.2 | 17.2 | 0.871 | 27.8 |
| gs_robust | 14000 | 24.1 | 17.3 | 0.868 | 27.6 | |
| gs_robust_4d | 12000 | 25.0 | 18.2 | 0.905 | 28.6 | |
| gs_fins | 14000 | 26.1 | 18.5 | 0.829 | 28.5 | |
fins + deform, 10 fps (dec_fins_4d_x10_6k, new) | 6000 | 26.4 | 19.0 | 0.860 | 29.3 | |
| fish1 (grouper) | gs | 40000 | 24.26 (black-bg PSNR) | - | 0.86 | - |
| gs_robust | 20000 | 24.63 (black-bg PSNR); full 20.9 | 18.1 | 0.86 | 20.8 |
30 fps data (held-out 30 fps frames of fish2_hf / fish3_hf)
| fish | run | iters | full | masked | IoU | near-fish |
|---|---|---|---|---|---|---|
| fish2_hf | 10 fps gs | - | 23.2 | 16.8 | 0.862 | 24.0 |
| 10 fps gs_robust_4d | - | 23.8 | 17.4 | 0.887 | 24.9 | |
| 10 fps gs_fins | - | 24.3 | 18.1 | 0.853 | 24.9 | |
| gs_robust_4d trained at 30 fps (no-fins ablation), composite re-fit (fair) | 10000 | 23.9 | 18.2 | 0.886 | 24.6 | |
| gs_fins_4d | 10000 | 25.4 | 20.3 | 0.889 | 25.8 | |
gs_fins_4d, production preset (dec_final_test) | 4000 | 25.05 | 19.9 | 0.887 | 25.6 | |
| fish3_hf | 10 fps gs | - | 25.2 | 18.0 | 0.858 | 28.1 |
| 10 fps gs_robust_4d | - | 26.0 | 19.0 | 0.886 | 28.7 | |
| 10 fps gs_fins | - | 26.7 | 19.6 | 0.839 | 28.3 | |
| gs_robust_4d at 30 fps (ablation), composite re-fit (fair) | 10000 | 27.2 | 20.1 | 0.890 | 29.4 | |
| gs_fins_4d | 10000 | 28.0 | 21.0 | 0.884 | 29.9 | |
gs_fins_4d (dec_fins_4d_6k) | 6000 | 27.8 | 20.8 | 0.883 | 29.7 | |
| w58_fish3_hf (coral hind, window 58) | production preset (gs_final, end-to-end test of the template) | 4000 | 26.1 | 21.0 | 0.906 | 27.1 |
Notes on fairness: the round-3b table re-fitted each model with its own loss (composite for the fins models, masked for the others), which flatters the fins models. The "fair" rows re-fit the no-fins 30 fps ablation with the composite loss too: the fins advantage is then +0.8 dB full / +0.95 dB masked on fish3_hf and +1.5 / +2.1 dB on fish2_hf. The 10 fps baselines inside the 30 fps blocks were not re-fitted that way (their gap is larger than the fair one). Near-fish values depend on the far_r0 / far_r1 training values; all rows here use the defaults.
gs_robust_4d, gs_fins, and the new 10 fps fins + deform run) all end in a paddle / smeared tail, despite the better 10 fps numbers of the last one.gs_robust is a plausible but blob-like body (no fins, flat ventral side). The composited validation renders align with the crops, the metrics are above the baseline (24.63 vs 24.26 PSNR, IoU 0.86) and the species call is medium confidence (Cephalopholis hemistiktos 68%). The frames are hard (coral in front, cleaner wrasse on the body, other fish crossing), but this is the main resident grouper of the default window, so it is kept and retrained with the final preset like every other fish.FISH_WINDOW=58. The model has the grouper outline with a rounded caudal fin and pectoral / pelvic hints; composites align with the crops (comp 17-29 dB per frame). The canonical colour is cyan-tinted and the spots are smeared (the per-frame colour gain restores the brown in the composites). The poster shows species, confidence and visit times correctly.| step | measured |
|---|---|
| 04d on a window fish (w58_fish3, 1192 frames at 30 fps, 256 px crops) | extract 6 s, SAM 142 s, background / soft alpha (04c adapter) 1209 s (~1 crop/s), ~25 min total |
--fins --deform, 6000 iters, default hypothesis repair (fish3_hf) | 1879 s training; 3 repair rounds of ~230 s each (37 % of the time) |
production preset (4000 iters, --hyp_max_frames 300, --res 384, fish2_hf) | 1030 s training (+ ~3 min final evaluation), 2 repair rounds of ~125 s |
| production preset, w58_fish3_hf (1024 valid poses, 256 px), alone on the GPU | 1015 s training + ~4 min final evaluation |
Expected production cost per new fish: 04d with the 24 s cap (~720 frames) ~14 min, training ~20 min. For the 42 fish to train (fish1 + 41 window fish; fish2 / fish3 reuse their existing 30 fps models; w58_fish3 is already done) that is about 24 GPU-job-hours; with 2 concurrent jobs about 12 h, with 3-4 jobs ~8 h if the GPU throughput scales (it is ~90 % busy with two jobs, so expect less than linear scaling). The data preparation can run in one slot while the other slots train. If this is too slow, use the knobs in that order: drop to --iters 3000, lower the cap to --max-total-hard-s 18, then train the low-priority fish (below) with the 10 fps fallback (no 04d).
Run from scripts/ with the venv python; window fish need FISH_WINDOW=<window minute> (the fish's window in work/fish_index.json; default-window fish fish1-3 run without it).
cd scripts # 1) 30 fps data (once per fish; several fish of one window may share one call) $env:FISH_WINDOW='58' ..\.venv\Scripts\python -u 04d_hifps.py --fish w58_fish5 w58_fish6 --max-total-hard-s 24 # 2) training (add --res 384 when work/w58/fish/<name>_hf/meta.json has crop_res 512) ..\.venv\Scripts\python -u 06_train_gs.py --fish w58_fish5_hf --fins --deform --iters 4000 --hyp_max_frames 300 --out gs_final # 3) videos (species.json is taken from the base fish dir) ..\.venv\Scripts\python -u 10_fish_videos.py --fish w58_fish5_hf --model gs_final --window 58 Remove-Item Env:FISH_WINDOW
Template: 06_train_gs.py --fish <name>_hf --fins --deform --iters 4000 --hyp_max_frames 300 [--res 384] --out gs_final
Special cases:
_hf data and the 10000-iteration gs_fins_4d models exist and are the best models available. Copy work/fish/fish2_hf/gs_fins_4d to work/fish/fish2_hf/gs_final (same for fish3) instead of retraining.04d_hifps.py --fish fish1 --max-total-hard-s 24 (04d's fish1-specific defaults allow at most 2 segments), then the template on fish1_hf (crop_res decides --res).work/w58/fish/w58_fish3_hf/gs_final.hifps_report.json -> poses.valid), no segment of at least 2 s, or fails: 06_train_gs.py --fish <name> --fins --deform --iters 4000 --hyp_max_frames 300 --out gs_final (videos with --fish <name>).06_train_gs.py --fish <name> --iters 6000 --out gs_final.--only: they iterate over every fish dir with a meta.json and would also touch the *_hf dirs.auto: newest gs*/poses_refined.json of the base fish, else the raw 05 poses (the case for all window fish). The robust hypothesis repair of 06 fixes the remaining flips (w58_fish3_hf: 86 % valid interpolated poses, median centre error 0.9 px).Priority order (train the fish with the best material first, so a time-out hits the weakest): species-confident, large fish first; last the drab-wrasse groups that may mix individuals (w02_fish8, w02_fish9, w02_fish10, w12_fish9, w58_fish10) and the heavily coral-occluded fish (w22_fish3, w40_fish7, w12_fish5, w32_fish4, w32_fish5).
Generated by scripts/11_site.py · simple gallery: gallery.html