@camstack/types 1.2.404 → 1.2.405

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Small-subject recovery — a second detector pass at NATIVE resolution on
3
+ * the moving regions the full-frame pass did not account for.
4
+ *
5
+ * ## Why
6
+ *
7
+ * The first-level detector sees the frame at its input size (320 px on every
8
+ * hardware-aware default). A 4K frame is reduced 12×: a dog measured at
9
+ * 89–277 px in the source is 7–38 px for the model and is not emitted. Camera 615, six
10
+ * instants with a dog in the scene, five with no `animal` detection at all —
11
+ * nothing downstream (tracker, gate, notifications) ever saw a subject. The
12
+ * confirmation gate, which re-runs the same detector on a 320 px native crop
13
+ * of the PERSON, found the dog at 0.74–0.91 as `bestIncompatibleClass`. The
14
+ * measurement that decided the constants below is
15
+ * `docs/benchmarks/pipeline-frame-model-eval.md`, case `miss-dog-615`: a
16
+ * 640–800 px native square around the moving region brings the dog to
17
+ * 0.82–0.91 with the SAME 320 model; a 1280 px square loses it again.
18
+ *
19
+ * ## What this is NOT (D52)
20
+ *
21
+ * Not a detail/CLIP crop. `deriveDetailCropRect` derives the rectangle a
22
+ * detail or embedding step is fed from a KNOWN subject box, and one vector
23
+ * index tolerates exactly one such derivation. This module derives a square
24
+ * from a MOTION region, before any subject exists, for the FIRST-LEVEL
25
+ * detector; its output is a detection, never a vector. Nothing here may be
26
+ * reused for a detail crop, and nothing here calls the detail derivation.
27
+ *
28
+ * ## When it runs — NOT only when the frame is empty
29
+ *
30
+ * The trigger is an UNCOVERED MOTION REGION, deliberately broader than "the
31
+ * full-frame pass found nothing". Camera 615, 2026-09-06 19:54:33 local: the
32
+ * gate DID confirm an `animal` birth (bestScore 0.8983) — and the track held
33
+ * 9 positions over 1 666 ms, then coasted out its `maxMissedMs` (4 000 ms) and
34
+ * died at 19:54:39, before the dog reached the Aiuola zone. Both of its object
35
+ * events carry `zones: []`, while in the same seconds the animal was in fact
36
+ * at x≈3200, inside the zone. So the defect is not only "the subject is never
37
+ * born" — it is "the subject is born and then the full-frame pass stops seeing
38
+ * it", and a pass gated on "no detections at all" would not have run on a
39
+ * single one of those frames, because the PERSON was still detected.
40
+ *
41
+ * Gating on the motion region instead makes the pass fire in both cases with
42
+ * one rule: a moving thing the full-frame boxes do not account for. A live
43
+ * track feeding on recovered boxes needs one hit per `maxMissedMs` to survive
44
+ * and `minHits` (3) to be born; at {@link RECOVERY_MIN_INTERVAL_MS} the pass
45
+ * supplies 2/s, which is 8× the coasting budget and births a subject in 1.5 s.
46
+ *
47
+ * ## Where
48
+ *
49
+ * In the runner, in-process, on the frame the full pass just ran on. The
50
+ * pixels come from the same process-local `frameRef` lease the full pass
51
+ * used; the crop is resolved as one compressed ≤640 px view. Frames never
52
+ * cross a process boundary at frame rate (D9/D18) — a second crop of the
53
+ * same frame on the same channel does not change that.
54
+ *
55
+ * Pure: no runner state, no IPC, no pixels. The runner wires the lease, the
56
+ * executor call and the rate limit around it.
57
+ *
58
+ * Lives in `@camstack/types` so the live runner (`addon-pipeline`) and the
59
+ * offline accuracy evaluation (`addon-benchmark`) plan the SAME crops: one
60
+ * module, two callers, no copy to drift.
61
+ */
62
+ import type { ObjectDetection } from '../../types/detection.js';
63
+ import type { PipelineStepInputOutput } from '../../capabilities/pipeline-executor.cap.js';
64
+ /** The object-detection step id — the first-level detector both recipes run. */
65
+ export declare const OBJECT_DETECTION_STEP_ID = "object-detection";
66
+ /** Fraction of a frame, `[0, 1]` on both axes. */
67
+ export interface NormalizedRect {
68
+ readonly x: number;
69
+ readonly y: number;
70
+ readonly w: number;
71
+ readonly h: number;
72
+ }
73
+ export interface FrameDims {
74
+ readonly width: number;
75
+ readonly height: number;
76
+ }
77
+ /** A square of the NATIVE frame, in native pixels. */
78
+ export interface RecoveryCrop {
79
+ readonly left: number;
80
+ readonly top: number;
81
+ readonly side: number;
82
+ }
83
+ /**
84
+ * Smallest crop side, native pixels. Below this the model no longer
85
+ * magnifies enough to matter on a 4K frame, and on a 1080p frame 640 is
86
+ * already a third of the picture. Measured: 480–800 px all recover the dog.
87
+ */
88
+ export declare const RECOVERY_CROP_MIN_SIDE_PX = 640;
89
+ /**
90
+ * Largest crop side. A 320 model on a 1024 px square is a 3.2× reduction and
91
+ * scored the dog at 0.52–0.76; at 1280 px it was absent again. A region wider
92
+ * than this is tiled, never zoomed out.
93
+ */
94
+ export declare const RECOVERY_CROP_MAX_SIDE_PX = 800;
95
+ /** Context around the region before squaring, as a fraction of its larger side. */
96
+ export declare const RECOVERY_CROP_PADDING_RATIO = 0.25;
97
+ /**
98
+ * The view the crop is resolved at, compressed. Never larger than the crop
99
+ * (the floor is {@link RECOVERY_CROP_MIN_SIDE_PX}), so it never upscales, and
100
+ * the model letterboxes it to its own input from here as it does the frame.
101
+ */
102
+ export declare const RECOVERY_VIEW_PX = 640;
103
+ /**
104
+ * Crops per frame. Each is one more inference on the same pool (15–25 ms on
105
+ * CoreML for a 640 view); two cover a person-and-dog blob and a second mover.
106
+ */
107
+ export declare const RECOVERY_MAX_CROPS_PER_FRAME = 2;
108
+ /**
109
+ * The pass runs at most this often per camera — at 10 fps that is one frame
110
+ * in five. A subject the full pass misses is missed for seconds, not for one
111
+ * frame, so the recovery does not need every frame to find it.
112
+ *
113
+ * The floor under this number is the tracker: a track coasts for
114
+ * `maxMissedMs` (4 000 ms by default) before it is dropped, so 2 recoveries a
115
+ * second keep a subject the full-frame pass has lost ALIVE with 8× margin,
116
+ * and reach `minHits` (3) in 1.5 s. Raising it past ~1 300 ms would start to
117
+ * lose tracks between passes.
118
+ */
119
+ export declare const RECOVERY_MIN_INTERVAL_MS = 500;
120
+ /**
121
+ * A region larger than this share of the frame is not a small subject: a
122
+ * lighting change, a car filling the picture, a person at the lens. The
123
+ * full-frame pass already sees those at scale.
124
+ */
125
+ export declare const RECOVERY_MAX_REGION_FRAME_FRACTION = 0.25;
126
+ /**
127
+ * How much of a region must lie OUTSIDE every full-frame detection for the
128
+ * pass to run on it. A person's own blob (dilated, with shadow) is covered
129
+ * 70–90% by the person box; a person-and-dog blob is covered ~55% by it.
130
+ */
131
+ export declare const RECOVERY_MIN_UNCOVERED_FRACTION = 0.35;
132
+ /** Motion regions older than this describe a different frame. */
133
+ export declare const RECOVERY_MOTION_MAX_AGE_MS = 2000;
134
+ /** A recovered box overlapping a same-class full-frame box this much is the same subject. */
135
+ export declare const RECOVERY_FUSE_IOU = 0.5;
136
+ /**
137
+ * A recovered box CONTAINED this much in a same-class full-frame box is a part
138
+ * of it — a torso the crop cut out of a person the full pass already has.
139
+ */
140
+ export declare const RECOVERY_FUSE_CONTAINMENT = 0.7;
141
+ /** A box within this many crop-view pixels of the crop edge was clipped by it. */
142
+ export declare const RECOVERY_EDGE_MARGIN_PX = 2;
143
+ /** Area of a normalised rectangle, never negative. Shared with the D396 recipe. */
144
+ export declare function normalizedArea(r: NormalizedRect): number;
145
+ /**
146
+ * THE square-placement rule of this pass — one owner, shared by both recipes.
147
+ *
148
+ * A crop is a square of `side` native pixels centred where it can be and slid
149
+ * inside the frame where it cannot. Both region rules place their squares
150
+ * through this function so a crop means the same rectangle whichever rule
151
+ * chose the region.
152
+ */
153
+ export declare function squareAround(centreX: number, centreY: number, side: number, native: FrameDims): RecoveryCrop;
154
+ export interface RecoveryPlanInput {
155
+ /** Motion regions of this frame, normalised. */
156
+ readonly regions: readonly NormalizedRect[];
157
+ /** The full-frame pass's boxes, normalised. */
158
+ readonly detections: readonly NormalizedRect[];
159
+ /** The NATIVE frame the crops are cut from. */
160
+ readonly native: FrameDims;
161
+ }
162
+ /**
163
+ * The crops the second pass runs on this frame — possibly none.
164
+ *
165
+ * A region qualifies when it is small (`RECOVERY_MAX_REGION_FRAME_FRACTION`)
166
+ * and mostly uncovered by the full-frame boxes
167
+ * (`RECOVERY_MIN_UNCOVERED_FRACTION`). Qualifying regions are served largest
168
+ * uncovered area first under `RECOVERY_MAX_CROPS_PER_FRAME`; a crop whose
169
+ * centre already lies inside a planned crop is folded into it. A frame that
170
+ * cannot hold the smallest crop yields nothing: it is already seen at scale.
171
+ */
172
+ /** Why a motion region is, or is not, worth a native crop. */
173
+ export type RecoveryRegionVerdict = 'eligible' | 'too-large' | 'covered';
174
+ /** How the regions of one frame fell against the two gates. */
175
+ export interface RecoverySkipCounts {
176
+ readonly tooLarge: number;
177
+ readonly covered: number;
178
+ readonly eligible: number;
179
+ }
180
+ /**
181
+ * WHY A FRAME PLANNED NO CROP, AS A NUMBER (D378).
182
+ *
183
+ * The pass report counts the passes that RAN, so "the second pass stops running
184
+ * when a person is beside the dog" — a plausible reading of
185
+ * {@link RECOVERY_MAX_REGION_FRAME_FRACTION} and
186
+ * {@link RECOVERY_MIN_UNCOVERED_FRACTION}, and a live hypothesis on camera 615
187
+ * — could not be confirmed or killed from the logs at all. This classifies the
188
+ * regions a frame offered, against the same predicate the planner filters on.
189
+ */
190
+ export declare function countRecoverySkips(input: RecoveryPlanInput): RecoverySkipCounts;
191
+ export declare function planRecoveryCrops(input: RecoveryPlanInput): readonly RecoveryCrop[];
192
+ /**
193
+ * Boxes the executor produced on the crop VIEW → the result's own space.
194
+ *
195
+ * Three spaces meet here: the view the model saw (`view`, ≤640 px), the native
196
+ * square it was cut from (`crop`, native px) and the space the full-frame
197
+ * result reports in (`result`, native px on the `frameRef` path, the inline
198
+ * ≤640 view otherwise). A box touching the view edge was clipped by the crop
199
+ * and is dropped: half a person is not a person, and the tracker would take
200
+ * the half as the whole.
201
+ */
202
+ export declare function remapCropDetections(detections: readonly ObjectDetection[], crop: RecoveryCrop, view: FrameDims, result: FrameDims, native: FrameDims): readonly ObjectDetection[];
203
+ /**
204
+ * Class-aware NMS of the recovered boxes against the full-frame ones. Every
205
+ * full-frame box is kept as it is — the second pass only ADDS. A recovered box
206
+ * is added when no full-frame box of the same macro class is the same subject
207
+ * (`RECOVERY_FUSE_IOU`) or contains it (`RECOVERY_FUSE_CONTAINMENT`). Ids are
208
+ * re-minted so a crop's `d0` cannot collide with the frame's.
209
+ */
210
+ export declare function fuseRecoveredDetections(full: readonly ObjectDetection[], recovered: readonly ObjectDetection[]): readonly ObjectDetection[];
211
+ /**
212
+ * The step the second pass runs: the enabled first-level object detector of
213
+ * the camera's own tree, with its own model and no children. Same model as
214
+ * the full pass so a recovered box is comparable to a full-frame one.
215
+ */
216
+ export declare function selectRecoveryStep(steps: readonly PipelineStepInputOutput[]): PipelineStepInputOutput | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/types",
3
- "version": "1.2.404",
3
+ "version": "1.2.405",
4
4
  "description": "Shared types, interfaces, and model catalogs for the CamStack detection ecosystem",
5
5
  "keywords": [
6
6
  "camstack",