Spaces:
Running on Zero
Running on Zero
File size: 8,770 Bytes
4b98524 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | """The deterministic methods, as first-class adapters.
Directive §4, and it is the sentence most easily skipped in the whole document:
> Do not use a neural model when deterministic signal processing is better.
So optical flow, FFT periodicity, contour measurement and reference-marker
calibration are registered here alongside SAM and DINOv3 rather than living in a
utilities module. They sit in the same registry, answer the same
`availability()`, and appear in the same listing, because a reader comparing the
stack should see that two of the capabilities with the clearest path forward
need no weights at all.
**These adapters are always available**, which no other adapter in this package
can say. There is no artefact to be absent, no card to be checksummed, no
licence to be refused and no gate to fail — which is most of the argument for
preferring them. `availability()` still exists and still answers, because a
caller should not have to know which kind of adapter it is holding.
The honesty property is not weaker here, it is only located differently. A
neural adapter refuses by having no model; these refuse by measuring whether the
signal was present, in `periodicity`'s two gates and in `geometry`'s refusal to
invent a scale.
"""
from __future__ import annotations
from pathlib import Path
from app.adapters.base import (
Adapter,
AdapterSpec,
Availability,
MeasuredCost,
Measurement,
Modality,
Placement,
Task,
)
from app.adapters.signal.geometry import (
NoReference,
Scale,
measure_region,
scale_from_marker,
)
from app.adapters.signal.respiration import RespirationResult, respiratory_rate
RESPIRATION_SPEC = AdapterSpec(
adapter_id="respiration-flow-fft",
runtime="opencv-numpy",
tasks=(Task.MEASURE,),
modalities=(Modality.VIDEO,),
directive_role=(
"§14 cattle respiratory rate — video, flank region, optical flow, "
"periodicity, FFT, breaths per minute. §4 names optical flow, FFT and "
"periodic motion analysis as OpenCV work rather than model work."
),
requires_artefact=False,
placement=Placement.CPU_SERVICE,
placement_reason=(
"No weights, and the arithmetic is cheap — but the clip is long. Dense "
"flow costs 4.3 ms per frame pair at 320 px, so §14's 30–60 second "
"capture is 4–8 seconds of flow plus decode, measured at 9.7 s median "
"for a 31-second clip. **That is past the inline ceiling**, so this "
"capability needs a queue rather than a bigger box. It is also the "
"strongest on-device candidate in the stack: OpenCV is on the phone "
"already, the video never has to leave it, and ADR 0002's offline-first "
"promise is kept for free."
),
measured=MeasuredCost(
hardware=(
"Apple M-series laptop (NOT the target container). OpenCV 5.0.0 "
"reports 11 threads and ignores setNumThreads(), so a "
"single-threaded figure could not be taken on this build"
),
threads=11,
sample=(
"Cow_crosses_cattle_grid.webm, 925 frames, 30.86 s at 29.97 fps, "
"whole frame, decode plus flow plus spectrum"
),
runs=7,
median_seconds=9.68,
peak_rss_mb=239.0,
measured_on="2026-08-21",
),
notes=(
"**The latency figure is load-sensitive and should be read as a band, "
"not a point.** Seven runs give a 9.68 s median over a 9.29–12.58 s "
"spread, and separate sessions on the same machine and the same clip "
"produced medians of 12.15 s and 14.17 s. The previously recorded "
"9.07 s / 331 MB does not reproduce in any configuration tried: memory "
"is consistently around 239 MB, and no threading setting moves the "
"latency, because this OpenCV build does not honour setNumThreads. "
"What survives all of it is the conclusion — every measurement is past "
"the 8 s inline ceiling, so this capability needs a queue.\n\n"
"**What the metronome validates is the extractor, not this adapter.** "
"On footage whose Commons description states 96 beats per minute, "
"`signal.dominant_rate` returns 96.48, and 48.38 on a crop of the "
"pendulum alone, the swing being half the tick rate — a 0.5% error "
"against a stated rate on real video. But `Metronome.webm` is 11.71 "
"seconds, and `respiration.MIN_CAPTURE_SECONDS` is 20, so "
"`measure()` refuses all three of those regions before any signal "
"processing runs. The only clip with a ground truth cannot reach the "
"code path this adapter exposes, and an earlier version of this note "
"read as though it had. `tests/test_adapters.py` asserts the gap so it "
"cannot be quietly re-closed in prose.\n\n"
"**No cattle rate is validated** — all three real cattle clips are "
"refused, two for being shorter than the capture protocol and one for "
"having no clear rhythm. What is missing is not model work: it is a "
"thirty-second clip of a cow's flank with somebody's counted breath "
"rate beside it."
),
)
GEOMETRY_SPEC = AdapterSpec(
adapter_id="marker-geometry",
runtime="opencv-numpy",
tasks=(Task.MEASURE,),
modalities=(Modality.IMAGE,),
directive_role=(
"§4 geometry, contour measurement and reference-marker calibration; "
"§9's 'approximate visible area: 12–16 cm²' for a wound, and §22's "
"fallback scale when metric depth is unreliable."
),
requires_artefact=False,
placement=Placement.ON_DEVICE,
placement_reason=(
"Marker detection and a contour area are microseconds of arithmetic on "
"a phone. Running it on the device means the farmer learns the card was "
"not in shot while still standing next to the animal, which is the "
"difference between a re-capture and a lost record."
),
notes=(
"**Unmeasured, and unexercised on a real photograph.** No image "
"available to this project contains an Animap reference marker, so the "
"marker-detection half has never run on anything real. The arithmetic "
"either side of it is exercised by unit tests. Do not quote an area "
"from this until somebody has photographed a printed card beside a "
"ruler."
),
)
class DeterministicAdapter(Adapter):
"""Signal processing and geometry. Always available, never guessing."""
def __init__(self, spec: AdapterSpec) -> None:
self.spec = spec
def availability(self) -> Availability:
# OpenCV and NumPy are production dependencies, so there is genuinely
# nothing to check. Importing cv2 here to prove it would make a health
# probe pay for a 60 MB import.
return Availability(True)
def load(self) -> "DeterministicAdapter":
return self
class RespirationAdapter(DeterministicAdapter):
"""§14, end to end."""
def __init__(self) -> None:
super().__init__(RESPIRATION_SPEC)
def measure(
self,
video_path: Path | str,
*,
region: tuple[float, float, float, float] | None = None,
) -> RespirationResult:
return respiratory_rate(video_path, region=region)
class GeometryAdapter(DeterministicAdapter):
"""§9 and §4, once something in the frame has a known size."""
def __init__(self) -> None:
super().__init__(GEOMETRY_SPEC)
def scale(self, image, marker_side_mm: float) -> Scale:
"""Pixels per millimetre from a printed marker.
Propagates `NoReference` rather than returning a default. A frame with
no marker has no scale, and the honest answer is a re-capture prompt.
"""
return scale_from_marker(image, marker_side_mm)
def region_size(self, mask, scale: Scale) -> dict:
return measure_region(mask, scale)
def try_scale(self, image, marker_side_mm: float) -> Measurement:
"""The same thing, as a `Measurement` a runner can put in a result."""
try:
found = self.scale(image, marker_side_mm)
except NoReference as absent:
return Measurement(
kind="scale", value=None, unit="px/mm", usable=False,
detail=str(absent),
)
return Measurement(
kind="scale", value=round(found.pixels_per_mm, 4), unit="px/mm",
usable=True,
support={"relative_error": round(found.relative_error, 4)},
detail=found.source,
)
def deterministic_adapters() -> list[Adapter]:
return [RespirationAdapter(), GeometryAdapter()]
|