feat: chain track 5 frames so a moving object is re-seeded (REQ-189)

trackForward sent the identical box to all five target frames. On batch 19
(extracted at 0.5 fps - one frame every 2 s) a truck entering the frame
outruns a static seed, so the action either missed it or grabbed the wrong
part.

- geometry handling lifted to module-level shapeBox(), reused for the drawn
  shape and for each assist result
- first frame still uses the drawn box exactly; frames 2-5 are seeded with
  the previous run's shape grown x1.5 about its centre (SEED_GROWTH)
- a frame SAM3 refuses keeps the last good seed for the next attempt
- REQ-189 rewritten to the chaining contract, including the accepted cost:
  a bad read can carry forward; this is not tracking, no motion model
This commit is contained in:
asus committed 2026-10-05 12:21:18 +07:00
1 parent b71f1d4bf2
commit 1530bbc999
4 files changed
+73 -26

No files matched your search

+41 -18
View File
@@ -9,6 +9,37 @@ import ReviewSidebar from '../components/ReviewSidebar'
import ExemplarFilterPanel from '../components/ExemplarFilterPanel'
import useExemplarPool from '../hooks/useExemplarPool'
// How much bigger the seed box gets before each frame after the first, so the
// object has room to have moved. Hand-tuned, not learned (REQ-189).
const SEED_GROWTH = 1.5
// SAM3 assist wants one normalized [x0, y0, x1, y1] box (REQ-043). A bbox
// shape already is that; a polygon is reduced to its bounding box.
function shapeBox(geometry) {
const pts = geometry?.points
if (geometry?.type === 'bbox' && pts?.length === 4) return pts
if (geometry?.type === 'polygon' && pts?.length) {
let minX = 1, minY = 1, maxX = 0, maxY = 0
for (const [x, y] of pts) {
if (x < minX) minX = x
if (x > maxX) maxX = x
if (y < minY) minY = y
if (y > maxY) maxY = y
}
return [minX, minY, maxX, maxY]
}
return null
}
function growBox([x0, y0, x1, y1], k) {
const cx = (x0 + x1) / 2
const cy = (y0 + y1) / 2
const hw = ((x1 - x0) * k) / 2
const hh = ((y1 - y0) * k) / 2
const clamp = (v) => Math.min(1, Math.max(0, v))
return [clamp(cx - hw), clamp(cy - hh), clamp(cx + hw), clamp(cy + hh)]
}
export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }) {
const [batch, setBatch] = useState(null)
const [project, setProject] = useState(null)
@@ -367,23 +398,8 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
return
}
// SAM3 assist wants one normalized [x0, y0, x1, y1] box (REQ-043). A bbox
// shape already is that; a polygon is reduced to its bounding box.
const geometry = current.geometry
let box = null
if (geometry?.type === 'bbox' && geometry.points?.length === 4) {
box = geometry.points
} else if (geometry?.type === 'polygon' && geometry.points?.length) {
let minX = 1, minY = 1, maxX = 0, maxY = 0
for (const [x, y] of geometry.points) {
if (x < minX) minX = x
if (x > maxX) maxX = x
if (y < minY) minY = y
if (y > maxY) maxY = y
}
box = [minX, minY, maxX, maxY]
}
if (!box) {
const firstBox = shapeBox(current.geometry)
if (!firstBox) {
setError('That shape has no usable box — Track 5 Frames needs one.')
return
}
@@ -397,12 +413,19 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
setError('')
try {
let tracked = 0
let seed = firstBox
const failures = []
for (let i = 0; i < ahead.length; i++) {
const targetFrame = ahead[i]
// The first frame uses the drawn box untouched. After that the object
// has had time to move, so the last result seeds a wider box.
const seedBox = i === 0 ? seed : growBox(seed, SEED_GROWTH)
try {
await api.assist(targetFrame.id, { box, class_id: current.class_id })
const created = await api.assist(targetFrame.id, {
box: seedBox, class_id: current.class_id,
})
tracked++
seed = shapeBox(created?.geometry) ?? seed
patchFrameLocally(targetFrame.id, { annotation_count: (targetFrame.annotation_count ?? 0) + 1 })
} catch (exc) {
failures.push(`frame ${index + i + 2}: ${exc.message}`)