← All reports

2026-11-12

SailScan: model accuracy worklist for Skippy

Date: 12 Nov 2026. From Alex, prepared by Claude after reading the whole Boats thread (3 Apr to 12 Nov, 6,582 messages), the Burst 001 report, the 13 Oct accuracy write-up, the 20 Aug guardrails report, and the Ultralytics docs and source.

Scope: things you can check, build or retrain to make the stripe model more accurate. No user taps, no burst capture. Everything is automated.

Rule: this is all sandbox work. Nothing ships to users before METS (17 to 19 Nov) without Alex's OK.

How to read it: Section 1 is what the history shows. Sections 2 to 6 are the work, in the order I would do it. Section 7 lists shelved ideas whose verdicts do not hold. Section 8 turns the older never-run ideas into numbered tasks (the lane-detection model is S1). Section 10 is the checklist to report against: every item in this file has an ID there. Appendix B is the full test ledger so nothing gets re-run by accident.

Revised 12 Nov, afternoon: Section 8 items now have task IDs and specs, the two Section 6 steps are M1 and M2, and Section 10 was added.

Second revision 12 Nov: audited against everything found in the search. Added Section 1.7, checks C9 to C13, retrain T12, pipeline jobs P7 and P8, tasks S11 to S15, two extra fair-test items in Section 6, and Section 11 (things recorded but not asked of you). Companion files in the project, same folder: SailScan-METS-Accuracy-Plan-2026-11-11.md and SailScan-Accuracy-Deep-Dive-2026-11-09.md.

Where I state something about the Ultralytics library I have checked it against their docs or source and say so. Where I state something about our own training config I could not see it, so it is written as a check for you to run.


1. What the full history shows

1.1 The normal score cannot see shape

The 13 Oct accuracy page defines a perfect photo as: every stripe found, no extra lines, and every line "inside a tight band around the person's line (3 percent of the photo's diagonal)". The 20 Aug guardrails page says the same: "closest line within 3% of the photo diagonal".

At the model's working size (960 x 720) that band is 36 px. You measured the tape at about 5 px (27 Oct, scale not stated; if that was at upload size the band is wider still in tape widths). So the band is about seven tape widths each side. A top stripe is about 100 px long with about 10 px of depth, so a dead straight line through it passes.

Almost every promote and reject decision since late August has used this score (a strict score was added on 20 Oct, but the headline number is still this one). It measures stripe count and rough position. It does not measure camber, draft, entry, exit or where the ends are, which is what the customer pays for.

Evidence that it matters: - 18 Oct: scored against the key, SAM 3 off looked better (draft error 8.3 to 4.5, exit 7.6 to 4.8). Alex's blind vote on the 42 most different photos: SAM 3 on 27, off 8. - 20 Oct: Alex marked 20 of 29 right by eye; the scorer passed 29 and 30. - 12 Aug (your own finding): snapping ends to the sail edge did not improve the score "because we grade against human labels, and the human stripes ALSO stop short of the edge. The grader punishes the model for being right."

If the normal scorer has changed since 13 Oct, say so in your report.

1.2 The training loss is just as loose (verified in the library)

Ultralytics pose docs, dataset page: "kpt_oks_sigmas sets custom per-keypoint OKS sigmas used during training and validation... When omitted, the COCO 17-keypoint sigmas are used for kpt_shape: [17, 3] and a uniform 1/N otherwise."

For our 8 points that default is 0.125. The COCO body sigmas run from 0.025 (eyes) to 0.107 (hips), so our stripe points train with a tolerance looser than a human hip.

The loss code (KeypointLoss.forward): e = d / ((2 * sigmas).pow(2) * (area + 1e-9) * 2), loss = 1 - exp(-e), where d is squared pixel distance and area is the target box width times height. In YOLO26 the same sigmas also drive the pose label assigner, and the same sigmas are used for the validation pose mAP.

What that means in pixels at 960 input (my calculation, typical box sizes):

Stripe (box) Error that still scores OKS 0.95, sigma 0.125 Same, sigma 0.05 Same, sigma 0.03 Loss for a 5 px miss, sigma 0.125
Top (100 x 15 px) 3.1 px 1.2 px 0.7 px 0.125
Middle (350 x 45) 10.0 px 4.0 px 2.4 px 0.013
Bottom (650 x 90) 19.4 px 7.7 px 4.6 px 0.003

So with the default, a bottom-stripe point can sit 19 px off (about four tape widths) and still earn 95%, and a 5 px miss on the bottom stripe costs 37 times less than the same miss on the top stripe. The validation mAP saturates at the same place, which is one reason it tells us nothing (1.3).

This fits the oldest complaint in the thread. 16 Jul: the bottom line "rides a few pixels above the real stripe... cuts the corner where the stripe sags hardest". 18 Oct: "in the deepest part of the stripe it sits above where it should, on 82% of bottom stripes". 24 Oct: bottom too flat on 77%.

Hypothesis for why loose means flat: inside the tolerance the loss is a plain squared error, so the model learns the average of where the labels put each point. Labels disagree about where point k sits along the stripe, and the average of positions spread along a curve falls inside the curve, toward the chord. A tight sigma saturates on far-off labels, so the model follows the common position instead of the average. Test T1 settles it.

I found no message in the thread where sigmas were set or discussed.

1.3 Burst 001 already answered two questions that were not read off it

I put the 30 finished runs from burst-001.sailscanapp.pages.dev through a regression (all recipe settings at once, the failed roll 12 at 166 left out).

Setting Effect on exam score (photos of 332) Std error
Flip 0 to 0.3 +18.8 4.3
HSV-V +0.6 +5.8 4.9
Rotation +30 deg +5.0 6.6
960 vs 640 -2.1 3.9
Erase +0.4 -2.7 4.8
Epochs +40 -8.3 4.6

Caveat: Burst 001 was the August medium model on the 3% score. Treat these as strong leads, not as settled for v39.

1.4 The 332 is now a practice set

20 Oct: "I traced each miss on the test photos to the step causing it, tried a fix in the cloud, and only shipped what beat the live app on all 327 photos." Good engineering, but it means the clean-up rules have been fitted to those photos one miss at a time. 317 of 332 no longer predicts what a new customer photo does.

Evidence: on 29 Oct Alex marked both versions wrong on 6 of 30 recent customer sails. RTMO raw beat v39 raw (291 v 276) and RF-DETR raw beat ours raw (272 v 266), and both lost after clean-up, which you described as "all tuned on v39's output" (28 Oct).

1.5 Point order: a possible cause of lines stopping short

What the thread says: - 4 Oct: "The labels are stored left to right." - 20 Oct: some hand-corrected stripes were found to have been drawn leech-first (paraphrase). - 16 Oct: 117 archive training labels and 10 test answers were mirror images. Fixed for archive photos. App labels were not checked. - 26 Aug: the QC page inserted a clicked dot before the last one, scrambling order. Saved labels were not audited. - 26 Oct: the main model trains with "heavy rotations and upside-down flips". RF-DETR trained with left-right flips only; when given the main model's augmentation set it came out much worse.

What the library does (verified in v8_transforms and RandomFlip): - If flip_idx is missing from the dataset file, both flips are switched off with the warning "No 'flip_idx' array defined in data.yaml, disabling 'fliplr' and 'flipud' augmentations." - Both the vertical and the horizontal flip are built with the same flip_idx. - Rotation never reorders keypoints.

Why that matters for points ordered left to right: - A left-right flip reverses the order, so it needs flip_idx: [7,6,5,4,3,2,1,0]. - An up-down flip does not change left-to-right order, so it must not reorder. If the installed version applies that same flip_idx on up-down flips, every upside-down-flipped sample is stored right to left. - A rotation past 90 degrees reverses the order with no reindexing.

One flip_idx cannot be right for both flips under a left-to-right rule.

What reversed labels do under an averaging loss: point k is pulled toward where point 7-k sits. Each 1% of samples with reversed order pulls both ends of every line in by 1% of its length.

Share of training samples with reversed order Line length as share of the true chord
0% 100%
2% 96%
5% 90%
10% 80%

The model can partly work out orientation from the picture, so the real effect is smaller than the table. But the symptom matches Alex's most repeated note (29 Oct: "top and bottom stopping short of luff and leech"; 19 Oct: lengthening the top line won 37 to 0). This is unproven. Check C2 settles it in an hour.

1.6 The model already says which points are off the photo, and we ignore it

Verified in RandomPerspective.apply_keypoints: visible[out_mask] = 0 for any keypoint that lands outside the frame after augmentation. The loss then skips those points. So although every point in our labels is marked visible, the YOLO model has been trained on augmented samples where cut stripes had their off-frame points unsupervised and flagged invisible. How many depends on the mosaic, scale and translate settings (C1).

27 Aug, your config check: the per-point visibility output is ignored at inference.

So when a stripe leaves the photo, the model's coordinates for the off-frame points are untrained, its visibility output probably says so, and the pipeline draws them anyway. That is the shape of the bug Alex hand-corrected "over 500 times" (2 Oct), and of the three rule patches since (16 Oct corner-to-corner bottom stripe, 19 to 20 Oct bent tail, 27 Oct fold-back).

There is also a mismatch in convention. A human label for a stripe that leaves the photo is 8 points spread along the visible part, ending at the border. An augmented crop keeps the original 8 positions and hides some. The model sees both.

Check C3 and test T4 cover this.

1.7 What customer photos and customer corrections show (my 11 Nov measurements)

From the 134 saved customer corrections: - Stripe size at 960 input: top stripe about 100 px long, bottom about 648 px. Uploads are 1,500 to 2,000 px on the long side. - draft and draftStable differ by a median of 2.4 points and by 6.3 at the 90th percentile. - On 117 stripes the customer nudged by less than about 2 px, draft moved by a median of 0.7 points and 4.2 at the 90th percentile when read from the curve through the points, against 0.3 and 2.0 from a smooth fit. Camber moved the same either way. So a 2 px nudge can move the draft we report by 4 points. - Where customers did move a stripe, a quarter of the time an end moved by more than 14 to 21% of the chord. Ends are where customers correct us most. - 52 of the 134 carry the camber-over-limit flag.

From v39's finder stage on 25 customer photos sampled in the QC app (judged by eye): - 16 were proper look-up shots and the lines sat on the stripes on all 16. Two blemishes: a fifth line along the foot on one, and a clipped piece of the bottom stripe measured as a whole stripe on another. - 4 of those 16 read camber of 14 to 20% with entry angles of 34 to 42 degrees. - 9 were not measurable (1 far away, 2 edge-on, 3 sideways, 2 small-boat sails where the lines went on battens, 1 sail on the ground). Only the far-away one returned nothing. One sideways photo returned 25% camber and a 150 degree exit.

Also on 11 Nov: the QC Combined Review page was seeding its lines from a cached prediction (recipe finder_v27_flow_v26_refine_1, computed between 1 Oct and 4 Nov), not from the live v39 pipeline. See C11.

Simulation, beyond the table in Section 5: at 250 px with 2 px of point noise, camber scatter is 0.9 and draft scatter 7.6 (curve through points) against 3.5 to 4 (smooth fit). A line that stops 10% short at one end reads camber 0.7 to 1.4 points low and turns the chord 2 to 2.5 degrees.


2. Zero-cost checks (no training, do these first)

ID Check How Report
C1 v39's real recipe Load the v39 .pt and print train_args, version, date; print the dataset yaml (kpt_shape, flip_idx, kpt_oks_sigmas); say whether the shipped weights are best.pt or last.pt fliplr, flipud, degrees, mosaic, close_mosaic, scale, translate, pose, kobj, rle, imgsz, epochs, seed, Ultralytics version
C2a Point order in stored labels For every stripe in the training export and the test key, in the upright frame: is the first point left of the last, and do the points advance monotonically along the chord Count reversed and count scrambled, split by source (archive, app, customer correction, reviewer)
C2b Point order after augmentation Build the v39 training dataloader, pull 2,000 augmented samples, run the same test on visible points. Repeat with fliplr only, flipud only, rotation only Share reversed for each. Target is 0%
C3 Visibility on cut stripes On photos where a stripe leaves the frame, or where the bend or hook fix fired, dump v39's 8 per-point visibility scores Are the points the fix removed the low-visibility ones
C4 Jitter test Scan each stored customer photo 10 times with small changes (scale 0.9 to 1.1, turn up to 3 deg, shift up to 2%, JPEG quality 80 to 95), full pipeline each time Per stripe: spread of camber, draft, draftStable, entry, exit, twist, by top, middle, bottom. Share of photos where the stripe count changes
C5 Per-point confidence YOLO26 pose trains a per-keypoint sigma (RLE). Check whether v39 has it and whether the export keeps it If present: does sigma track real error on the practice set
C6 Training set facts Count images with no stripes (empty labels); are the 421 duplicates still in; share of labels on the bottom edge; labels by source Counts
C7 Which draft customers see The server returns draft and draftStable Which one the app shows. On 20 Aug you measured the stable one as 3.6x more consistent (error 4.15 to 3.09)
C8 Half-pixel fixes The 10 Nov coordinate audit found two suspects (SAM 3 hand-off, ONNX padding) Full-pipeline re-score with both fixed
C9 How a hand label becomes 8 training points Read the export code. The 20 Aug guardrails page says a spline moved training targets by 3.05 draft points; 18 Oct says v39 has a "smoothing fix" The exact rule today: how points are resampled, whether any smoothing or spline is still applied, and to which label sources
C10 Labels saved by the 29 Sep labeller bug That bug saved the model's own 8-point line as the human line whenever a boom or outline was added. The count was "being counted" and never posted How many, whether they are tagged, whether they are in training
C11 What the reviewer sees and saves On 11 Nov the Combined Review page seeded lines from a cached prediction (recipe finder_v27_flow_v26_refine_1), not live v39. Since 3 Nov reviewer saves go "straight into the training labels" Which recipe seeds the reviewer's lines today. Whether a seed saved without edits can enter training as a human label
C12 Missed stripes below the confidence cut-off Planned on 24 Oct, no result posted. For every missed stripe on the practice set, look in the raw candidates How many missed stripes were found but dropped, and at what confidence
C13 Boom and twist Twist is measured from the boom. 16 Aug: typical boom error about 9 degrees. 21 Oct: Alex marked both boom models wrong on 25 of 59. Only 44 of the 332 have a boom label Boom angle error on every photo with a boom label. How often twist is shown with no boom found

C4 needs no labels and gives a repeatability number for every customer photo we hold. It is also the cheapest health check of the clean-up stack: if a 2 degree turn changes the stripe count, a rule is on a knife edge.

One note for C1: Burst 001 varied an "erase" setting and it showed no effect (-2.7 photos, std error 4.8). The Ultralytics erasing setting is described in their docs as applying to classification training. I did not verify this in source. Confirm whether it does anything in a pose run before spending a recipe slot on it again.


3. Fix the ruler before any retrain

R1. Measurement scorecard. Keep the count check. Add, per matched stripe: - distance from the answer line in pixels at stored-upload size and as % of chord (mean and worst point) - luff-end error and leech-end error, along the stripe, as % of chord - camber, draft (stable method), entry, exit, twist error - all of it split by top, middle, bottom; customer or archive; main or jib; portrait or landscape; stripe leaves the frame or not - report median and 90th percentile, never just a pass count

R2. Practice set and sealed set. Take the customer candidates (custset, 180 waiting on Alex's keep or drop) and split by boat and day, so one sail never sits on both sides. Half is the practice set: open every miss, tune every rule on it. Half is sealed: only totals are ever looked at, nobody opens a photo, and no rule is tuned on it. If it is ever opened, it becomes practice and a new sealed half is drawn from newer scans (the keep-unsaved-scans change in 1.0.13 gives a steady supply). The 332 stays as a regression check only.

R3. Gold answers. For the practice and sealed sets, two independent traces per photo, drawn in a zoomed view with no model line showing, bottom edge of the tape, luff to leech (the label guide rule). The reviewer role from 3 Nov can do this. The gap between the two traces is the human noise floor, which was proposed on 20 Aug and never measured. No model can be shown to beat it, so it tells us when to stop.

One labelling rule is still open and must be settled in the label guide before gold tracing starts, because it decides whether the model or the key is wrong on a class of misses: "very short stripes right at the head: always label, or never?" (27 Oct, never answered). Alex decides; the guide and the scorer then follow it.

R4. Repeats and tests. Any retrain is run 3 times with different seeds. Compare to the T0 mean, not to the shipped v39 (v39 was picked from several tuned runs, so its own score is flattered). For photo-level pass or fail use a paired test (McNemar). For measurements use a paired bootstrap. Example of why: the 16 Oct retrain on the mirror-fixed labels "fixed 6, broke 15" and was dropped; exact McNemar on 6 v 15 is p = 0.078.

R5. Win rule, fixed now. A challenger replaces the baseline only if, on the sealed set: 1. median camber error is lower and the 90th percentile is no worse (paired bootstrap 95% interval excludes zero) 2. end error is lower 3. draft error is no worse 4. exact stripe count is no worse (paired test) 5. jitter spread (C4) is no worse 6. Alex's blind vote on 40 photos where the two differ goes its way

R6. Results cannot be lost. Every run writes its config, seed, per-photo results and scorecard to storage before the GPU shuts down. On 24 Oct two results could not be found and had to be treated as unproven.


4. Retrains

One change at a time against T0, three seeds each, scored with R1 on practice then sealed. Burst 001 runs cost about $2 each on the medium model; the large model costs more.

ID Change Why Notes
T0 v39's exact recipe, 3 seeds Measures run-to-run noise on every metric. Becomes the baseline Also score last.pt against best.pt for each
T1 kpt_oks_sigmas at 0.05, then 0.03 Section 1.2 One line in the dataset yaml. Warm-start from v39 so tight sigmas have a gradient from the first step. Expect validation mAP to fall; ignore it
T2 Flip rate 0.5 Section 1.3 Only after C1 and C2b say which flip is in use and that order survives it
T3 Order-safe training Section 1.5 Option A: after all geometric augmentation, re-sort each stripe's points left to right; cap rotation at 45 deg; no up-down flip unless it is proven order-safe. Option B (better): make the keypoint loss and the assigner accept either direction, taking the smaller of forward and reversed, as MapTR does for open polylines. B also makes rotation augmentation safe, which could retire the try-four-rotations step at inference
T4 Cut-stripe augmentation that matches the label rule Section 1.6 When a crop cuts a stripe, resample the visible part to 8 even points ending at the frame border, all marked visible, the same as a human label. Compare the hook, bend and fold fix rates with the rules switched off
T5 Train on bottom-edge labels 27 Oct: training labels are 56% mid-tape and 22% bottom edge; the strict answers were moved to the bottom edge by a script. You named this option and it was not run Run the same script over the training labels. Spot-check 100 shifted labels by eye first, split by sail colour, because the script has only been judged on test answers
T6 Clean finish 21 Oct audit: of 5,459 labels, 1,002 are drawn or approved by Alex and 2,780 are archive lines from another system that cannot be proven. 13 Sep: 70 of 152 audited labels were bad (audit set was half suspect picks, half random) Train on everything as now, then a short low-learning-rate final stage on trusted labels only (Alex-drawn or approved, plus approved customer corrections). Standard for noisy labels. Not the same as the 22 Oct "remove unapproved" test, whose result was lost
T7 No-stripe photos as background 13 Oct: invented stripes on 21 of 53 misses. 24 Oct: 6 of 19 misses were lines on seams or graphics. 29 Oct, Alex: sails with no stripes still get lines Add customer photos that have no readable stripes with empty label files (sail on the ground, edge-on, far off, plain sails). Start at 5 to 10% of the set. If seams and battens still fire, second step: label battens, seams and shrouds as their own classes (SAM 3 text prompts to pre-label, reviewer to check) so they have somewhere to go
T8 Remove the 421 duplicates and group near-copies 24 Oct: still in the training set. 20 Oct: the newer-label retrain lost on "near-copies of the same up-the-mast shots" Group by sail and session before splitting train and validation too, so the validation number stops rewarding memory
T9 Choose the checkpoint on our own score Section 1.3 Add a callback that runs the R1 scorecard on the practice set every few epochs and keeps the best by camber and end error. Ultralytics has no setting for this; it needs a callback or a source edit
T10 More pixels on the top stripe 24 Oct: "the top stripe is actually off by more". 18 Oct: 20 of 37 misses were tiny top stripes 1280 input, and a stride-4 (P2) head if the installed version has that config for YOLO26 pose. The earlier 1280 run (293 v 295) was one run on the 3% score, so it decided nothing
T11 Stack the winners Only changes that passed R5 on their own
T12 Curve-distance loss on the current model Sections 1.2 and 1.5. Today each predicted point is pulled toward the label point with the same number. If labels disagree about where point 3 sits along the stripe, the model averages, and the average falls inside the curve Change the keypoint loss to measure the distance from each predicted point to the labelled curve (the label drawn as a polyline), plus separate terms for the two end points and for even spacing along the stripe. Keep the standard matching for assigning stripes. This removes sliding along the stripe directly, where T1 only reduces it. Code change in the loss; no new architecture

Order: T0 first. T1, T3 and T5 next because they aim straight at flat lines, short ends and the mid-tape offset. T2 once C2b is clean. T12 if T1 helps but does not close the gap.


5. Pipeline work that needs no retraining

ID Work Why
P1 Average the curves of the finders we already have v39, RF-DETR and RTMO are three different architectures trained on the same labels. Today RF-DETR only adds missed stripes. Where two or three find the same stripe, fit one curve to all their points. Independent errors cancel. The 3% score could not have seen the gain, which is why this needs R1 first
P2 Stop lines at the last confident point If C3 shows visibility separates good points from off-frame ones: end the line at the last confident point and carry it along its own fitted curve to the frame edge. Should replace three rule patches with one mechanism
P3 Turn the photo upright from the phone's tilt reading Phone tilt and lens info were added to the photo record in the 3 Nov app work (confirm it is in the shipped build). Tilt tells you which way is up directly. That removes the try-four-rotations search, where RF-DETR invented stripes (17 Oct) and which turns about 1 in 100 upright photos sideways (21 Oct)
P4 Average over jitters If C4 shows real spread, run the finder on 3 jittered copies and average the curves. Finder time is about 3 s, so this is affordable on that stage only. Earlier test-time augmentation (212, negative) was multi-scale on the count score, which is a different test
P5 Clean-up tuned per finder, on the new scorecard RTMO raw 291 v 276 and RF-DETR raw 272 v 266 both beat v39 before clean-up. Retune the clean-up thresholds for each on the practice set, then compare best against best on the sealed set. Include the two gates that were set on the 3% score: the SAM 3 guard (keep SAM 3's line only if no point moves more than 3% of the diagonal and it is at least 95% as long) and the RF-DETR gate (0.66). A 3% of diagonal move limit is the same seven tape widths as the old score
P6 Measure on the largest image we hold, not the 960 copy The finder works at 960. Uploads are stored at about 1,500 to 2,000 px on the long side, so every stripe has about 1.5 to 2 times the linear resolution sitting unused. Step 1, no app change: run the measuring stage (Section 6) on the stored upload. Step 2, app change, automatic, needs Alex's OK: after the first pass the app sends a full-size crop round each stripe (phone originals are 4,032 or 8,064 px wide). Not the same as the 16 Oct zoom test, which re-ran the finder on an upscaled copy and so added no information

| P7 | Twist that does not depend on a bad boom | If C13 confirms the boom is unreliable: show twist only when the boom passes a confidence check, and offer twist relative to the lowest stripe's chord as a fallback that needs no boom. How twist is defined with no boom is Alex's call | | P8 | Use the whole sail to check the top stripe | Camber, draft and twist change smoothly up a sail. The top stripe is the shortest and the noisiest (about 100 px). Fit the trend through the lower stripes and flag a top stripe that sits outside its own error bar from that trend. Flag only, do not overwrite: a top stripe that really is odd is exactly what the sailor wants to see. Needs error bars first (C5 or M2) |

Simulation behind P6 (10% camber, 45% draft, 8 points, 1 px of point noise):

Stripe length in pixels Draft scatter, curve through the 8 points Draft scatter, smooth fit Camber scatter
100 8.5 points 4 to 5 1.0
250 4.6 1.9 0.4
650 1.8 0.7 0.2

A top stripe is about 100 px at 960. At full phone resolution it is 400 to 800 px.


6. The challenger: a measuring stage that works on a straightened strip

This is the new model to train and sandbox against the current one. The finder stays. What changes is how each line is placed and measured.

The idea has been on our own lists three times and never run: guardrails playbook 20 Aug ("Full-resolution line refinement", Steger sub-pixel ridge snap with a max-move budget; "Bottom-stripe crop pass at high res"), Alex's 22 Oct note (held), and 3 Nov ("next, not started").

Why the earlier "snapping made it worse" results do not rule it out: - 16 Jul: implementation failure ("snapping everything further away"), no numbers. - 14 Aug colour snap and 20 Aug ridge polish: graded against labels that sit mid-tape or nowhere fixed, stop short of the edges, and that training had smoothed (3.05 draft points, guardrails page). A step that moves a line onto the real tape scores worse against an answer line that is off the tape. The 20 Aug loss was on draft alone (4.0 to 4.6), the number you showed the same day to be unstable. - SAM 3, once pointed at the bottom edge of its mask, won Alex's blind vote 19 to 1 (20 Oct). So putting the line on the tape is what the eye wants. SAM 3 does it at about 1,000 px internally with a mask; this does it at full size with sub-pixel output.

Build, in two steps.

Step A (ID M1), no training (a few days): 1. For each found stripe, cut a band from the largest stored image along the finder's line, wide enough to hold the tape plus a margin either side. 2. Straighten it so the finder's line is the horizontal axis. 3. Normalise so the tape is always dark on light. 4. In every column find the bottom edge of the tape to sub-pixel precision (Steger-style ridge and edge response). 5. Link the columns with a dynamic-programming path that cannot jump, with a cap on how far it may move from the finder's line. 6. Fit one smooth section curve to all column positions, weighted by edge strength. Measure camber, draft and angles on that curve, not on a Catmull-Rom curve through 8 points. Use a spline with a smoothness penalty, not a fixed low-order shape: in my simulation a simple polynomial section read a true 35% draft as 39 to 41%. Check the fit for bias on synthetic strips with known shape before trusting it. 7. Columns with no clear tape (glare, logos, numbers) get low weight and are bridged by the fit, not guessed.

Step B (ID M2), a small trained model (if M1 beats the baseline on R1): - Input: the straightened strip. Output per column: tape bottom-edge offset (sub-pixel), visible or not, and an uncertainty. Plus two end outputs: where the tape meets the luff and where it meets the leech. - Training data: (1) stripes a human drew or moved, bottom edge only (the label's seedCorrection.perStripe.dist says which stripes were moved); (2) synthetic tape drawn onto real sailcloth strips with exact known shape, covering white on white, glare, seams crossing, numbers and blur; (3) SAM 3 bottom-edge masks as low-weight labels; (4) unlabelled customer strips with a consistency loss between two jittered views. - Per-column uncertainty gives an error bar on every number we show.

Why a second stage at all: the finder places all 8 points of a stripe from a single grid cell. On a 650 px bottom stripe the end points are hundreds of pixels from that cell, so fine placement at the ends is the hardest thing for that design. A stage that looks only at the strip, at full size, does not have that limit.

Fair test. Baseline is today's pipeline. Score both on: - the sealed set with gold answers (R3) and the R5 win rule - the synthetic strips, where truth is exact - the C4 jitter spread - repeat pairs: two photos of the same sail taken moments apart should give the same numbers. Mine these from customers who scanned the same sail twice in one session; no labels needed - any sail we hold both a SailScan photo and a reading from another sail-vision system for. A cross-check only, since the other system is not truth either. I found no published accuracy figure for any competing system.


7. Shelved ideas whose verdicts do not hold

Each was dropped on a gap inside single-run noise, on the 3% score, with no repeat. Do not re-run them all. Re-judge any that come up again with R1 and R4.

Idea Result at the time Date
1280 px training 293 v 295 18 Oct
Zoom on the head 306 v 309; earlier 285 v 286 20 Oct, 16 Oct
Bigger retrained RF-DETR 305 v 308 20 Oct
Top-stripe-weighted retrain 296 v 308 20 Oct
Retrain with newer labels 293 v 309 20 Oct
Retrain on mirror-fixed labels 285 v 294 (fixed 6, broke 15, p = 0.078) 16 Oct
Retrains on the 44 retraced missing stripes 275 and 267 v 279 15 Oct
12 points per stripe 284 v 296 of 329 18 Oct
More training data, three runs 246, 256, 258 v 264 Sep
Edge extension to the sail outline Switched off 28 Sep because it lowered the score, on answer lines that stop short of the edge 28 Sep

The clean-up rule changes are different: they are deterministic, so their gains on the 332 are real on the 332. The open question for them is whether they hold on the sealed set.


8. Suggested before and never run: now numbered tasks

These were each proposed at least once in the thread and never run. They are tasks, not reminders. Report on each by ID.

S1. Lane-detection model as a challenger finder

Proposed 5 Sep ("lane detection is the closest field, train one as a challenger"), 17 Sep (test matrix row 3.2) and 2 Nov. Never trained.

S2 to S10

ID Task What to do When
S2 Pseudo-labels from unlabelled uploads (5 Sep) Run the full pipeline on customer photos with no label. Keep only stripes where v39, RF-DETR and RTMO agree and the jitter spread (C4) is small. Add as low-weight training data. Judge by R5 After T6
S3 Agreement across boom-cam frames (5 Sep) Frames a few seconds apart show the same sail at the same trim, so stripe count and shape should match their neighbours. Use disagreement to flag bad frames and as a free repeatability measure. Uses frames the boom cam already takes; not a capture change After C4
S4 Photo-quality gate (17 Jul, 6 Oct, 22 Oct) A check that returns "cannot measure this photo" for edge-on, far-off, on-the-ground, blurred or stripe-free sails instead of drawing lines and numbers. Train it from the T7 no-stripe set. In my 11 Nov sample 9 of 25 customer photos were unmeasurable and most still got numbers. Needs Alex's OK to ship After T7
S5 Lens distortion check (22 Oct) Lens info is now in the photo record. Undistort wide-lens photos before measuring and see whether the numbers move on the practice set After R1
S6 Automatic bad-label ranking (3 Nov, "next, not started") Rank training labels by loss under the current model and by disagreement between the three finders. Send the top of the list to the reviewer. On 5 Sep the top-100 suspects scored about 14x worse than random Now
S7 SAM 3's sail mask for the edge (13 Oct, "has not been tried") The mask is computed on every scan and thrown away. Use it for the luff and leech ends in place of the low-resolution edge model. Judge by end error on R1 After R1
S8 Camera-angle correction (24 Oct: twist "is measured flat in the photo... it isn't true 3D twist") Use phone tilt, lens focal length and the mast line to estimate the viewing angle. Correct twist first, then camber. One method to test: the stripes sit at evenly spaced heights (about 25, 50 and 75%, Alex 29 Oct), so the three points where they meet the luff are evenly spaced in real life. In the photo their uneven spacing gives the mast's vanishing point (it is the harmonic conjugate of the middle point with respect to the outer two). With the focal length that gives the viewing angle, and each stripe can then be measured in its own plane. Sensitive to mast bend and to stripes that are not exactly evenly spaced, so treat it as a test, not a given. Test: photos of the same sail from different spots should give the same numbers after correction After R1
S9 Synthetic sails 50 Blender images were made on 5 Nov; your own verdict was cloth too clean and light too flat. Do synthetic tape on real cloth strips first (M2, item 2). Go back to full Blender scenes only if that helps With M2
S10 Human re-trace noise floor (20 Aug) Covered by R3, without Alex With R3
S11 Learned keep or drop judge over the model's own candidates (5 Sep junk-or-stripe classifier; 6 Oct line-quality judge, "not built") 26 Sep oracle: a perfect judge over the kept guesses scores 288 and over all guesses 311, against 252 that day. 24 Oct: a perfect "drop the wrong line" step "was the biggest gain on the table". One trained judge was tried once (+4 photos, fragile). Build it properly: a small classifier on a crop along each candidate line plus geometry (place in the sail, spacing to neighbours, length, confidence, whether the other finders agree). Train on labelled photos: a candidate that matches a label is keep, one that matches nothing is drop. It replaces hand thresholds for duplicates, seams and battens. Judge on missed and extra stripes on the sealed set After R2
S12 3D from the photo itself Depth Anything 3 class models estimate surface shape from one or more photos. One photo: use it only to estimate which way the sail faces the camera, as an input to S8. Test on 50 photos: do two viewpoints of one sail agree better after the correction. Speculative on white cloth against sky. Several photos per scan would give true 3D twist but is a capture change, so it is recorded in Section 11 and not asked here After S8
S13 Carry labels along boom-cam sequences Alex could not hand-edit about 2,000 boom-cam frames (13 Sep). Label one frame, carry its lines to neighbouring frames with a point tracker, reviewer spot-checks. Caution: on 16 Sep adding boom-cam frames made counts worse (86 to 75 of 93) and they are near-copies, so group them by sequence (T8) and weight them down After T8
S14 Choose what to label next by disagreement On unlabelled customer scans (kept since 1.0.13), rank by disagreement between v39, RF-DETR and RTMO and by jitter spread (C4). Send the top of that list to the reviewer. Those are the photos a new label teaches the most After C4
S15 Automatic exposure for the tape (app, no user action) White cloth blows out in bright sun and tape contrast drops. Meter on the sail and bias exposure slightly down. Speculative: test on 20 photo pairs before building anything. Needs Alex's OK to ship Later

9. What to send back

First, a status line for every ID in Section 10: done, running, waiting on what, or not doing and why. An item nobody reports on is assumed dropped.

Then one page, in this order: 1. C1 to C8 answers, as a table, with the actual values found (the sigma in use, the flip settings, the share of reversed labels), not only that the check was run. 2. T0 noise: mean and spread over 3 seeds for every R1 metric. 3. One row per test: what changed, seeds, practice and sealed results, paired p-value, pass or fail on R5. 4. Anything in Section 1 that your code shows is wrong. I could not see the training config, so corrections are expected.

Two things already waiting on Alex gate R2 and R3: the custset keep or drop pass, and approval of the label guide. Nothing new is asked of him here.


10. Checklist: every item in this file

ID Item Can start
C1 v39's real recipe Now
C2a Point order in stored labels Now
C2b Point order after augmentation Now
C3 Visibility on cut stripes Now
C4 Jitter test Now
C5 Per-point confidence (RLE sigma) Now
C6 Training set facts Now
C7 Which draft customers see Now
C8 Half-pixel fixes re-scored Now
C9 How a label becomes 8 training points Now
C10 Labels saved by the 29 Sep labeller bug Now
C11 What the reviewer sees and saves Now
C12 Missed stripes below the cut-off After R2 (332 now)
C13 Boom and twist error Now
R1 Measurement scorecard Now
R2 Practice set and sealed set After Alex's custset pass
R3 Gold answers, two traces After label guide approval
R4 Three seeds and paired tests With T0
R5 Win rule Fixed in this file
R6 Results saved before shutdown Now
T0 Baseline, 3 seeds After C1
T1 Tighter sigmas After T0
T2 Flip rate 0.5 After C1 and C2b
T3 Order-safe training After C2a and C2b
T4 Cut-stripe augmentation After C3
T5 Bottom-edge labels After T0
T6 Clean finish on trusted labels After T0
T7 No-stripe photos as background After C6
T8 Remove duplicates, group near-copies After C6
T9 Checkpoint chosen on our own score After R1
T10 More pixels on the top stripe After T0
T11 Stack the winners Last
T12 Curve-distance loss After T1
P1 Average the finders' curves Build now, judge on R1
P2 Stop lines at the last confident point After C3
P3 Upright from phone tilt After confirming tilt is in the shipped build
P4 Average over jitters After C4
P5 Clean-up tuned per finder After R2
P6 Measure on the largest stored image With M1
P7 Twist without a bad boom After C13
P8 Whole-sail check on the top stripe After C5 or M2
M1 Strip measuring stage, no training Build now, judge on R1 and R3
M2 Strip measuring stage, trained After M1 passes
S1 Lane-detection challenger After R1 and T0
S2 Pseudo-labels After T6
S3 Boom-cam frame agreement After C4
S4 Photo-quality gate After T7
S5 Lens distortion check After R1
S6 Bad-label ranking Now
S7 SAM 3 sail mask for the edge After R1
S8 Camera-angle correction After R1
S9 Synthetic sails With M2
S10 Human re-trace noise floor With R3
S11 Learned keep or drop judge After R2
S12 3D from the photo After S8
S13 Labels carried along boom-cam sequences After T8
S14 Choose what to label by disagreement After C4
S15 Automatic exposure for the tape Later

Holding retrains until the checks and T0 are in is right. It is not a reason to hold the rows marked "Now" or "Build now": C4 to C11, C13, R1, R6, P1, M1 and S6 need no retraining and no answer from the first three checks.


11. Recorded so it is not lost, not asked of you


Appendix A. Sources for the library facts

Not verified by me: whether the installed Ultralytics version reorders keypoints on up-down flips (the current source passes flip_idx to both flips; older versions applied it to the left-right flip only), and whether a P2 config exists for YOLO26 pose. C1 and C2b answer the first.


Appendix B. Test ledger, 3 Apr to 12 Nov 2026

Every accuracy test found in the Boats thread, in date order. Dates are 2026, month-day. Lines marked MY READ or starting with => are Claude's comments, not things said in the thread. Quoted text is from the thread. Version numbers are as posted at the time ("v27" was used for two different models, in August and in September).

Chunk 01: 3 Apr to 16 Apr 2026

Chunk 02: 17 Apr to 31 May 2026

Chunk 03: 1 Jun to 7 Jul 2026

Chunk 04: 8 Jul to 17 Jul 2026

Chunk 05: 18 Jul to 27 Jul 2026

Chunk 06: 28 Jul to 7 Aug 2026

Chunk 07: 8 Aug to 15 Aug 2026

Chunk 08/09: 16 Aug to 19 Aug 2026

From project docs (second-hand), 16 Sep to 27 Oct 2026

FULL THREAD, first-hand. bb-01: 19 Aug to 21 Aug 2026

bb-02: 22 to 31 Aug 2026 (first-hand)

bb-03: 1 to 6 Sep 2026 (first-hand)

bb-04: 7 to 20 Sep 2026 (first-hand)

bb-05: 22 Sep to 7 Oct 2026 (first-hand)

bb-06: 8 to 16 Oct 2026 (first-hand)

bb-07: 17 to 21 Oct 2026 (first-hand)

bb-08: 22 to 27 Oct 2026 (first-hand)

bb-09 to bb-11: 27 Oct to 12 Nov 2026 (first-hand; most of bb-09 is pasted documents)

THINGS THE THREAD SHOWS WERE NEVER RUN (as of 12 Nov)

lane-detection model (CLRerNet etc.); DINOv3 probe / DINOv3 for picking what to label; full-resolution straightened-strip refiner; pseudo-labels on the unlabelled uploads; temporal agreement across boom-cam bursts; photo-quality gate; lens-distortion check; human re-trace noise floor; seed-repeat variance study; curve-format model head (Arc 32 was only ever a label format test); training with the labels moved to the bottom edge; visibility flags on stripe ends; mast/forestay as a trained class; any camera-geometry correction; any multi-photo capture.