fix(hints): validate and clamp detector boxes to the frame
- commit
- bb0e85367721c36dc605dce2ab01c1f9fe8972c2
- author
- brice · · 2026-10-21 13:38 UTC
- parent
- 7e6b6a6
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +63 -24
M internal/adapters/hints/hints.go +16 -9
| @@ -223,7 +223,7 @@ func (w *worker) scan(ctx context.Context, req ports.HintScanRequest, fr frame, | |||
| 223 | 223 | res.Err = err | |
| 224 | 224 | return res | |
| 225 | 225 | } | |
| 226 | - | res.Targets = targetsOf(dets, req.Scale) | |
| 226 | + | res.Targets = targetsOf(dets, req.Scale, req.Width, req.Height) | |
| 227 | 227 | if w.dumpDir != "" { | |
| 228 | 228 | // The dump reads the frame after the result is sent and releases it. | |
| 229 | 229 | w.pending = &dumpJob{when: time.Now(), req: req, pix: fr.pix, m: fr.m, stride: fr.stride, targets: res.Targets, dets: dets, captureD: captureDur, detectD: detectDur} | |
| @@ -236,27 +236,34 @@ func (w *worker) scan(ctx context.Context, req ports.HintScanRequest, fr frame, | |||
| 236 | 236 | } | |
| 237 | 237 | ||
| 238 | 238 | // targetsOf converts boxes in physical pixels to logical output-local | |
| 239 | - | // targets, rounding outwards, and drops the ones under minTarget on a side. | |
| 240 | - | func targetsOf(dets []detection, scale float64) []ports.HintTarget { | |
| 239 | + | // targets. A box with a non-finite coordinate or score, a score outside | |
| 240 | + | // [0,1] or no area is dropped; the others are clamped to the width x height | |
| 241 | + | // frame (physical), rounded outwards to logical pixels, and dropped when | |
| 242 | + | // under minTarget on a side. | |
| 243 | + | func targetsOf(dets []detection, scale float64, width, height int) []ports.HintTarget { | |
| 241 | 244 | if scale <= 0 { | |
| 242 | 245 | scale = 1 | |
| 243 | 246 | } | |
| 247 | + | fw, fh := float64(width), float64(height) | |
| 244 | 248 | out := make([]ports.HintTarget, 0, len(dets)) | |
| 245 | 249 | for _, d := range dets { | |
| 246 | 250 | v := [4]float64{float64(d.x1), float64(d.y1), float64(d.x2), float64(d.y2)} | |
| 247 | - | ok := true | |
| 251 | + | ok := d.score >= 0 && d.score <= 1 // false for NaN too | |
| 248 | 252 | for _, f := range v { | |
| 249 | 253 | ok = ok && !math.IsNaN(f) && !math.IsInf(f, 0) | |
| 250 | 254 | } | |
| 251 | - | if !ok { | |
| 255 | + | if !ok || v[2] <= v[0] || v[3] <= v[1] { | |
| 252 | 256 | continue | |
| 253 | 257 | } | |
| 254 | - | x1, y1 := int(math.Floor(v[0]/scale)), int(math.Floor(v[1]/scale)) | |
| 255 | - | x2, y2 := int(math.Ceil(v[2]/scale)), int(math.Ceil(v[3]/scale)) | |
| 256 | - | if x2-x1 < minTarget || y2-y1 < minTarget { | |
| 258 | + | // Clamp before any int conversion: huge finite values stay safe. | |
| 259 | + | x1, y1 := math.Max(0, math.Min(v[0], fw)), math.Max(0, math.Min(v[1], fh)) | |
| 260 | + | x2, y2 := math.Max(0, math.Min(v[2], fw)), math.Max(0, math.Min(v[3], fh)) | |
| 261 | + | lx1, ly1 := int(math.Floor(x1/scale)), int(math.Floor(y1/scale)) | |
| 262 | + | lx2, ly2 := int(math.Ceil(x2/scale)), int(math.Ceil(y2/scale)) | |
| 263 | + | if lx2-lx1 < minTarget || ly2-ly1 < minTarget { | |
| 257 | 264 | continue | |
| 258 | 265 | } | |
| 259 | - | out = append(out, ports.HintTarget{Rect: ports.Rect{X: x1, Y: y1, W: x2 - x1, H: y2 - y1}, Score: d.score}) | |
| 266 | + | out = append(out, ports.HintTarget{Rect: ports.Rect{X: lx1, Y: ly1, W: lx2 - lx1, H: ly2 - ly1}, Score: d.score}) | |
| 260 | 267 | } | |
| 261 | 268 | return out | |
| 262 | 269 | } | |
| @@ -223,7 +223,7 @@ func (w *worker) scan(ctx context.Context, req ports.HintScanRequest, fr frame, | |||||
| 223 | res.Err = err | 223 | res.Err = err | ||
| 224 | return res | 224 | return res | ||
| 225 | } | 225 | } | ||
| 226 | - | res.Targets = targetsOf(dets, req.Scale) | 226 | + | res.Targets = targetsOf(dets, req.Scale, req.Width, req.Height) |
| 227 | if w.dumpDir != "" { | 227 | if w.dumpDir != "" { | ||
| 228 | // The dump reads the frame after the result is sent and releases it. | 228 | // The dump reads the frame after the result is sent and releases it. | ||
| 229 | w.pending = &dumpJob{when: time.Now(), req: req, pix: fr.pix, m: fr.m, stride: fr.stride, targets: res.Targets, dets: dets, captureD: captureDur, detectD: detectDur} | 229 | w.pending = &dumpJob{when: time.Now(), req: req, pix: fr.pix, m: fr.m, stride: fr.stride, targets: res.Targets, dets: dets, captureD: captureDur, detectD: detectDur} | ||
| @@ -236,27 +236,34 @@ func (w *worker) scan(ctx context.Context, req ports.HintScanRequest, fr frame, | |||||
| 236 | } | 236 | } | ||
| 237 | 237 | ||||
| 238 | // targetsOf converts boxes in physical pixels to logical output-local | 238 | // targetsOf converts boxes in physical pixels to logical output-local | ||
| 239 | - | // targets, rounding outwards, and drops the ones under minTarget on a side. | 239 | + | // targets. A box with a non-finite coordinate or score, a score outside |
| 240 | - | func targetsOf(dets []detection, scale float64) []ports.HintTarget { | 240 | + | // [0,1] or no area is dropped; the others are clamped to the width x height |
| 241 | + | // frame (physical), rounded outwards to logical pixels, and dropped when | |||
| 242 | + | // under minTarget on a side. | |||
| 243 | + | func targetsOf(dets []detection, scale float64, width, height int) []ports.HintTarget { | |||
| 241 | if scale <= 0 { | 244 | if scale <= 0 { | ||
| 242 | scale = 1 | 245 | scale = 1 | ||
| 243 | } | 246 | } | ||
| 247 | + | fw, fh := float64(width), float64(height) | |||
| 244 | out := make([]ports.HintTarget, 0, len(dets)) | 248 | out := make([]ports.HintTarget, 0, len(dets)) | ||
| 245 | for _, d := range dets { | 249 | for _, d := range dets { | ||
| 246 | v := [4]float64{float64(d.x1), float64(d.y1), float64(d.x2), float64(d.y2)} | 250 | v := [4]float64{float64(d.x1), float64(d.y1), float64(d.x2), float64(d.y2)} | ||
| 247 | - | ok := true | 251 | + | ok := d.score >= 0 && d.score <= 1 // false for NaN too |
| 248 | for _, f := range v { | 252 | for _, f := range v { | ||
| 249 | ok = ok && !math.IsNaN(f) && !math.IsInf(f, 0) | 253 | ok = ok && !math.IsNaN(f) && !math.IsInf(f, 0) | ||
| 250 | } | 254 | } | ||
| 251 | - | if !ok { | 255 | + | if !ok || v[2] <= v[0] || v[3] <= v[1] { |
| 252 | continue | 256 | continue | ||
| 253 | } | 257 | } | ||
| 254 | - | x1, y1 := int(math.Floor(v[0]/scale)), int(math.Floor(v[1]/scale)) | 258 | + | // Clamp before any int conversion: huge finite values stay safe. |
| 255 | - | x2, y2 := int(math.Ceil(v[2]/scale)), int(math.Ceil(v[3]/scale)) | 259 | + | x1, y1 := math.Max(0, math.Min(v[0], fw)), math.Max(0, math.Min(v[1], fh)) |
| 256 | - | if x2-x1 < minTarget || y2-y1 < minTarget { | 260 | + | x2, y2 := math.Max(0, math.Min(v[2], fw)), math.Max(0, math.Min(v[3], fh)) |
| 261 | + | lx1, ly1 := int(math.Floor(x1/scale)), int(math.Floor(y1/scale)) | |||
| 262 | + | lx2, ly2 := int(math.Ceil(x2/scale)), int(math.Ceil(y2/scale)) | |||
| 263 | + | if lx2-lx1 < minTarget || ly2-ly1 < minTarget { | |||
| 257 | continue | 264 | continue | ||
| 258 | } | 265 | } | ||
| 259 | - | out = append(out, ports.HintTarget{Rect: ports.Rect{X: x1, Y: y1, W: x2 - x1, H: y2 - y1}, Score: d.score}) | 266 | + | out = append(out, ports.HintTarget{Rect: ports.Rect{X: lx1, Y: ly1, W: lx2 - lx1, H: ly2 - ly1}, Score: d.score}) |
| 260 | } | 267 | } | ||
| 261 | return out | 268 | return out | ||
| 262 | } | 269 | } | ||
M internal/adapters/hints/hints_test.go +47 -15
| @@ -67,7 +67,7 @@ func TestHintScan(t *testing.T) { | |||
| 67 | 67 | var got detFrame | |
| 68 | 68 | det.EXPECT().Detect(mock.Anything, mock.Anything).RunAndReturn(func(_ context.Context, f detFrame) ([]detection, error) { | |
| 69 | 69 | got = f | |
| 70 | - | return []detection{{x1: 10, y1: 20, x2: 110, y2: 60, score: 0.8}}, nil | |
| 70 | + | return []detection{{x1: 10, y1: 20, x2: 50, y2: 30, score: 0.8}}, nil | |
| 71 | 71 | }).Once() | |
| 72 | 72 | h := startWith(t, 5*time.Second, Options{detector: det}) | |
| 73 | 73 | go func() { | |
| @@ -79,26 +79,58 @@ func TestHintScan(t *testing.T) { | |||
| 79 | 79 | require.NoError(t, res.Err) | |
| 80 | 80 | require.Equal(t, uint64(7), res.Seq) | |
| 81 | 81 | require.Equal(t, "HEADLESS-1", res.Output) | |
| 82 | - | require.Equal(t, []ports.HintTarget{{Rect: ports.Rect{X: 10, Y: 20, W: 100, H: 40}, Score: 0.8}}, res.Targets) | |
| 82 | + | require.Equal(t, []ports.HintTarget{{Rect: ports.Rect{X: 10, Y: 20, W: 40, H: 10}, Score: 0.8}}, res.Targets) | |
| 83 | 83 | require.Equal(t, 64, got.width) | |
| 84 | 84 | require.Equal(t, 32, got.height) | |
| 85 | 85 | require.Equal(t, 64*4, got.stride) | |
| 86 | 86 | } | |
| 87 | 87 | ||
| 88 | 88 | func TestTargetsOf(t *testing.T) { | |
| 89 | - | got := targetsOf([]detection{ | |
| 90 | - | {x1: 11, y1: 21, x2: 100.2, y2: 61, score: 0.5}, // scale 2: x 5..51, y 10..31 | |
| 91 | - | {x1: 0, y1: 0, x2: 5, y2: 40, score: 0.9}, // 2.5 -> 3 logical wide: dropped | |
| 92 | - | {x1: 0, y1: 0, x2: 40, y2: 5, score: 0.9}, // 3 logical high: dropped | |
| 93 | - | {x1: 0, y1: 0, x2: 8, y2: 8, score: 0.4}, // exactly 4x4: kept | |
| 94 | - | {x1: float32(math.NaN()), y1: 0, x2: 50, y2: 50}, | |
| 95 | - | }, 2) | |
| 96 | - | require.Equal(t, []ports.HintTarget{ | |
| 97 | - | {Rect: ports.Rect{X: 5, Y: 10, W: 46, H: 21}, Score: 0.5}, | |
| 98 | - | {Rect: ports.Rect{X: 0, Y: 0, W: 4, H: 4}, Score: 0.4}, | |
| 99 | - | }, got) | |
| 100 | - | // A zero scale reads as 1. | |
| 101 | - | require.Len(t, targetsOf([]detection{{x2: 10, y2: 10}}, 0), 1) | |
| 89 | + | nan := float32(math.NaN()) | |
| 90 | + | inf := float32(math.Inf(1)) | |
| 91 | + | r := func(x, y, w, h int, score float32) []ports.HintTarget { | |
| 92 | + | return []ports.HintTarget{{Rect: ports.Rect{X: x, Y: y, W: w, H: h}, Score: score}} | |
| 93 | + | } | |
| 94 | + | for _, tc := range []struct { | |
| 95 | + | name string | |
| 96 | + | det detection | |
| 97 | + | scale float64 | |
| 98 | + | want []ports.HintTarget | |
| 99 | + | }{ | |
| 100 | + | {"inside", detection{10, 20, 50, 40, 0.5}, 1, r(10, 20, 40, 20, 0.5)}, | |
| 101 | + | {"scale 2 rounds outwards, clamped at 100", detection{11, 21, 100.2, 61, 0.5}, 2, r(5, 10, 45, 21, 0.5)}, | |
| 102 | + | {"zero scale reads as 1", detection{0, 0, 10, 10, 0.5}, 0, r(0, 0, 10, 10, 0.5)}, | |
| 103 | + | {"exactly 4x4 kept", detection{0, 0, 8, 8, 0.4}, 2, r(0, 0, 4, 4, 0.4)}, | |
| 104 | + | {"3 logical wide dropped", detection{0, 0, 5, 40, 0.9}, 2, nil}, | |
| 105 | + | {"3 logical high dropped", detection{0, 0, 40, 5, 0.9}, 2, nil}, | |
| 106 | + | {"fully outside", detection{200, 10, 300, 50, 0.5}, 1, nil}, | |
| 107 | + | {"fully before origin", detection{-50, -50, -10, -10, 0.5}, 1, nil}, | |
| 108 | + | {"partly outside is clamped", detection{-20, 70, 150, 120, 0.5}, 1, r(0, 70, 100, 30, 0.5)}, | |
| 109 | + | {"partly outside, scale 2", detection{-20, 70, 150, 120, 0.5}, 2, r(0, 35, 50, 15, 0.5)}, | |
| 110 | + | {"inverted x", detection{50, 10, 20, 40, 0.5}, 1, nil}, | |
| 111 | + | {"inverted y", detection{10, 40, 50, 10, 0.5}, 1, nil}, | |
| 112 | + | {"empty x", detection{10, 10, 10, 40, 0.5}, 1, nil}, | |
| 113 | + | {"empty y", detection{10, 10, 40, 10, 0.5}, 1, nil}, | |
| 114 | + | {"NaN coordinate", detection{nan, 0, 50, 50, 0.5}, 1, nil}, | |
| 115 | + | {"Inf coordinate", detection{0, 0, inf, 50, 0.5}, 1, nil}, | |
| 116 | + | {"NaN score", detection{0, 0, 50, 50, nan}, 1, nil}, | |
| 117 | + | {"Inf score", detection{0, 0, 50, 50, inf}, 1, nil}, | |
| 118 | + | {"score above 1", detection{0, 0, 50, 50, 1.5}, 1, nil}, | |
| 119 | + | {"negative score", detection{0, 0, 50, 50, -0.1}, 1, nil}, | |
| 120 | + | {"score 0 and 1 kept", detection{0, 0, 50, 50, 1}, 1, r(0, 0, 50, 50, 1)}, | |
| 121 | + | {"huge coordinates", detection{-1e30, -1e30, 1e30, 1e30, 0.5}, 1, r(0, 0, 100, 100, 0.5)}, | |
| 122 | + | {"huge positive only", detection{10, 10, 1e30, 1e30, 0.5}, 2, r(5, 5, 45, 45, 0.5)}, | |
| 123 | + | {"huge outside", detection{1e30, 1e30, 3e30, 3e30, 0.5}, 1, nil}, | |
| 124 | + | } { | |
| 125 | + | t.Run(tc.name, func(t *testing.T) { | |
| 126 | + | got := targetsOf([]detection{tc.det}, tc.scale, 100, 100) | |
| 127 | + | if tc.want == nil { | |
| 128 | + | require.Empty(t, got) | |
| 129 | + | return | |
| 130 | + | } | |
| 131 | + | require.Equal(t, tc.want, got) | |
| 132 | + | }) | |
| 133 | + | } | |
| 102 | 134 | } | |
| 103 | 135 | ||
| 104 | 136 | // A detector error fails the scan; the worker goes on and the buffer is released. | |
| @@ -67,7 +67,7 @@ func TestHintScan(t *testing.T) { | |||||
| 67 | var got detFrame | 67 | var got detFrame | ||
| 68 | det.EXPECT().Detect(mock.Anything, mock.Anything).RunAndReturn(func(_ context.Context, f detFrame) ([]detection, error) { | 68 | det.EXPECT().Detect(mock.Anything, mock.Anything).RunAndReturn(func(_ context.Context, f detFrame) ([]detection, error) { | ||
| 69 | got = f | 69 | got = f | ||
| 70 | - | return []detection{{x1: 10, y1: 20, x2: 110, y2: 60, score: 0.8}}, nil | 70 | + | return []detection{{x1: 10, y1: 20, x2: 50, y2: 30, score: 0.8}}, nil |
| 71 | }).Once() | 71 | }).Once() | ||
| 72 | h := startWith(t, 5*time.Second, Options{detector: det}) | 72 | h := startWith(t, 5*time.Second, Options{detector: det}) | ||
| 73 | go func() { | 73 | go func() { | ||
| @@ -79,26 +79,58 @@ func TestHintScan(t *testing.T) { | |||||
| 79 | require.NoError(t, res.Err) | 79 | require.NoError(t, res.Err) | ||
| 80 | require.Equal(t, uint64(7), res.Seq) | 80 | require.Equal(t, uint64(7), res.Seq) | ||
| 81 | require.Equal(t, "HEADLESS-1", res.Output) | 81 | require.Equal(t, "HEADLESS-1", res.Output) | ||
| 82 | - | require.Equal(t, []ports.HintTarget{{Rect: ports.Rect{X: 10, Y: 20, W: 100, H: 40}, Score: 0.8}}, res.Targets) | 82 | + | require.Equal(t, []ports.HintTarget{{Rect: ports.Rect{X: 10, Y: 20, W: 40, H: 10}, Score: 0.8}}, res.Targets) |
| 83 | require.Equal(t, 64, got.width) | 83 | require.Equal(t, 64, got.width) | ||
| 84 | require.Equal(t, 32, got.height) | 84 | require.Equal(t, 32, got.height) | ||
| 85 | require.Equal(t, 64*4, got.stride) | 85 | require.Equal(t, 64*4, got.stride) | ||
| 86 | } | 86 | } | ||
| 87 | 87 | ||||
| 88 | func TestTargetsOf(t *testing.T) { | 88 | func TestTargetsOf(t *testing.T) { | ||
| 89 | - | got := targetsOf([]detection{ | 89 | + | nan := float32(math.NaN()) |
| 90 | - | {x1: 11, y1: 21, x2: 100.2, y2: 61, score: 0.5}, // scale 2: x 5..51, y 10..31 | 90 | + | inf := float32(math.Inf(1)) |
| 91 | - | {x1: 0, y1: 0, x2: 5, y2: 40, score: 0.9}, // 2.5 -> 3 logical wide: dropped | 91 | + | r := func(x, y, w, h int, score float32) []ports.HintTarget { |
| 92 | - | {x1: 0, y1: 0, x2: 40, y2: 5, score: 0.9}, // 3 logical high: dropped | 92 | + | return []ports.HintTarget{{Rect: ports.Rect{X: x, Y: y, W: w, H: h}, Score: score}} |
| 93 | - | {x1: 0, y1: 0, x2: 8, y2: 8, score: 0.4}, // exactly 4x4: kept | 93 | + | } |
| 94 | - | {x1: float32(math.NaN()), y1: 0, x2: 50, y2: 50}, | 94 | + | for _, tc := range []struct { |
| 95 | - | }, 2) | 95 | + | name string |
| 96 | - | require.Equal(t, []ports.HintTarget{ | 96 | + | det detection |
| 97 | - | {Rect: ports.Rect{X: 5, Y: 10, W: 46, H: 21}, Score: 0.5}, | 97 | + | scale float64 |
| 98 | - | {Rect: ports.Rect{X: 0, Y: 0, W: 4, H: 4}, Score: 0.4}, | 98 | + | want []ports.HintTarget |
| 99 | - | }, got) | 99 | + | }{ |
| 100 | - | // A zero scale reads as 1. | 100 | + | {"inside", detection{10, 20, 50, 40, 0.5}, 1, r(10, 20, 40, 20, 0.5)}, |
| 101 | - | require.Len(t, targetsOf([]detection{{x2: 10, y2: 10}}, 0), 1) | 101 | + | {"scale 2 rounds outwards, clamped at 100", detection{11, 21, 100.2, 61, 0.5}, 2, r(5, 10, 45, 21, 0.5)}, |
| 102 | + | {"zero scale reads as 1", detection{0, 0, 10, 10, 0.5}, 0, r(0, 0, 10, 10, 0.5)}, | |||
| 103 | + | {"exactly 4x4 kept", detection{0, 0, 8, 8, 0.4}, 2, r(0, 0, 4, 4, 0.4)}, | |||
| 104 | + | {"3 logical wide dropped", detection{0, 0, 5, 40, 0.9}, 2, nil}, | |||
| 105 | + | {"3 logical high dropped", detection{0, 0, 40, 5, 0.9}, 2, nil}, | |||
| 106 | + | {"fully outside", detection{200, 10, 300, 50, 0.5}, 1, nil}, | |||
| 107 | + | {"fully before origin", detection{-50, -50, -10, -10, 0.5}, 1, nil}, | |||
| 108 | + | {"partly outside is clamped", detection{-20, 70, 150, 120, 0.5}, 1, r(0, 70, 100, 30, 0.5)}, | |||
| 109 | + | {"partly outside, scale 2", detection{-20, 70, 150, 120, 0.5}, 2, r(0, 35, 50, 15, 0.5)}, | |||
| 110 | + | {"inverted x", detection{50, 10, 20, 40, 0.5}, 1, nil}, | |||
| 111 | + | {"inverted y", detection{10, 40, 50, 10, 0.5}, 1, nil}, | |||
| 112 | + | {"empty x", detection{10, 10, 10, 40, 0.5}, 1, nil}, | |||
| 113 | + | {"empty y", detection{10, 10, 40, 10, 0.5}, 1, nil}, | |||
| 114 | + | {"NaN coordinate", detection{nan, 0, 50, 50, 0.5}, 1, nil}, | |||
| 115 | + | {"Inf coordinate", detection{0, 0, inf, 50, 0.5}, 1, nil}, | |||
| 116 | + | {"NaN score", detection{0, 0, 50, 50, nan}, 1, nil}, | |||
| 117 | + | {"Inf score", detection{0, 0, 50, 50, inf}, 1, nil}, | |||
| 118 | + | {"score above 1", detection{0, 0, 50, 50, 1.5}, 1, nil}, | |||
| 119 | + | {"negative score", detection{0, 0, 50, 50, -0.1}, 1, nil}, | |||
| 120 | + | {"score 0 and 1 kept", detection{0, 0, 50, 50, 1}, 1, r(0, 0, 50, 50, 1)}, | |||
| 121 | + | {"huge coordinates", detection{-1e30, -1e30, 1e30, 1e30, 0.5}, 1, r(0, 0, 100, 100, 0.5)}, | |||
| 122 | + | {"huge positive only", detection{10, 10, 1e30, 1e30, 0.5}, 2, r(5, 5, 45, 45, 0.5)}, | |||
| 123 | + | {"huge outside", detection{1e30, 1e30, 3e30, 3e30, 0.5}, 1, nil}, | |||
| 124 | + | } { | |||
| 125 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 126 | + | got := targetsOf([]detection{tc.det}, tc.scale, 100, 100) | |||
| 127 | + | if tc.want == nil { | |||
| 128 | + | require.Empty(t, got) | |||
| 129 | + | return | |||
| 130 | + | } | |||
| 131 | + | require.Equal(t, tc.want, got) | |||
| 132 | + | }) | |||
| 133 | + | } | |||
| 102 | } | 134 | } | ||
| 103 | 135 | ||||
| 104 | // A detector error fails the scan; the worker goes on and the buffer is released. | 136 | // A detector error fails the scan; the worker goes on and the buffer is released. | ||