depth-nano
47,187 parameters. 189 KB of ONNX. Given one frame from a generated video, estimate how many generations it is removed from a real anchor image.
When you generate long video by chaining β each chunk conditioned on the last frame of the previous one β error accumulates. This model reads that accumulation off a single 64Γ64 frame.
Scope β read this first
What it is for: monitoring chained video generation. Answers "how degraded is this frame, in units of generations from ground truth?" so a pipeline can decide when to re-anchor.
What it must not be used for:
- Not an image-quality or aesthetic score. It estimates chaining depth, which correlates with degradation in this pipeline but is not a general quality measure.
- Not a deepfake, provenance, or AI-generated-content detector. Every training frame is AI-generated. It has never seen a real photograph and says nothing about whether an image is real.
- Not a measure of how many times an image was compressed, resaved, or edited.
- Not for any decision about a person.
What it predicts
Three ordinal bins over chaining depth:
| class | depth | chunks |
|---|---|---|
fresh |
0 β generated directly from a real anchor | 932 |
shallow |
1β5 generations removed | 264 |
deep |
6+ generations removed | 644 |
Ordinal bins, not regression. The depth distribution is bimodal β 932 chunks at depth 0, 547 at 12+, a thin tail between. A regressor scores well on that by learning "zero or large" and never resolving the middle, and MAE hides the failure. Binning forces the middle class to show its own recall, which is where the difficulty actually is.
Measured performance
Stratified split by ARM, 34 arms, ~30% held out, 3 seeds. Splitting by arm is mandatory here (frames in an arm share a seed and a chaining config), and because depth is confounded with arm identity β the heavily-chained arms are deep throughout β shuffles are retried until both sides contain all three classes.
| seed | accuracy | majority | lift | ordinal MAE | fresh | shallow | deep |
|---|---|---|---|---|---|---|---|
| 0 | 0.946 | 0.434 | +0.512 | 0.055 | 0.979 | 0.772 | 0.974 |
| 1 | 0.965 | 0.696 | +0.268 | 0.035 | 0.992 | 0.877 | 0.917 |
| 2 | 0.947 | 0.471 | +0.476 | 0.057 | 0.995 | 0.520 | 0.956 |
| mean | +0.419 Β± 0.108 | 0.049 Β± 0.010 | 0.989 | 0.723 | 0.949 |
Ordinal MAE of 0.049 bins means that when it is wrong it is almost always wrong by one adjacent
bin, never by two. shallow is the hard class and the one that varies (0.520β0.877); the two
extremes are close to solved.
The ablation that makes this believable
+0.42 lift is much higher than anything else in this family, which is exactly when to suspect a
trivial cue. The obvious one: fresh chunks are generated directly from the real anchor image,
so a model could score well by learning "does this look like the anchor photo?" β which would be a
real signal, but not depth estimation.
So I removed fresh entirely and asked only shallow (1β5) vs deep (6+), where no anchor-
similarity cue exists. 908 chunks, 27 arms, 5 seeds:
| seed | accuracy | majority | lift |
|---|---|---|---|
| 0 | 0.919 | 0.555 | +0.365 |
| 1 | 0.953 | 0.560 | +0.393 |
| 2 | 0.902 | 0.624 | +0.278 |
| 3 | 0.957 | 0.574 | +0.383 |
| 4 | 0.952 | 0.733 | +0.218 |
| mean | +0.328 Β± 0.068 |
It still works. The model is reading accumulated generation damage, not recognising the anchor.
Label-shuffle control: with depth labels permuted across chunks, the same pipeline scores β0.097 and β0.042 lift. The harness does not manufacture signal.
The crop assumption
Trained on full frames resized to 64Γ64, no crop. Chaining damage is a global property of the frame, so unlike most nano models in this family there is no object-to-frame ratio to preserve and no clutter wall. The corollary: do not feed it a crop β the degradation statistics of a region are not those of the frame. Whole frames, resized, letterboxed if the aspect differs.
Known failure modes
- Two generators, one scene. LTX-Video-2B and Wan 2.2 TI2V-5B, an interior room with a person. Chaining artefacts are model-specific; expect poor transfer to other architectures.
shallowrecall swings from 0.52 to 0.88 depending on which arms are held out. The middle of the depth range is genuinely under-sampled (264 chunks vs 932 and 644).- Depth is a proxy for damage, not damage itself. A well-behaved chain at depth 8 may look better than a bad one at depth 3. The model predicts the label it was given.
- It cannot exceed the bins. "6+" covers depth 6 and depth 40 identically.
- Frame-level inference, chunk-level labels. Every frame in a chunk inherits the chunk's depth, which is correct here (depth is constant within a chunk) β but the visual damage is not constant within a chunk, adding label noise.
Usage
import cv2, numpy as np, onnxruntime as ort
CLASSES = ["fresh", "shallow", "deep"]
sess = ort.InferenceSession("depth.onnx", providers=["CPUExecutionProvider"])
img = cv2.imread("frame.jpg") # whole frame, BGR, as cv2.imread returns it
x = cv2.resize(img, (64, 64), interpolation=cv2.INTER_AREA)
x = x.astype(np.float32).transpose(2, 0, 1)[None] / 255.0
logits = sess.run(None, {"image": x})[0][0]
print(CLASSES[int(logits.argmax())])
Input is BGR, 0β1, NCHW β the channel order cv2.imread returns, not RGB. Training used BGR
throughout; feeding RGB swaps two channels silently and degrades output without erroring.
Verification
ONNX vs PyTorch, identical weights, both on CPU, 512 real frames:
- max relative logit difference 3.0e-07 (logits reach Β±19.2)
- 100% argmax agreement
Tolerance is relative, not absolute. An absolute 1e-3 check flags a bit-accurate export of this model as broken β it did, the first time, on the sibling model.
What this replaced
The original plan was a model predicting chunks until collapse. It was killed before training: collapse onset turns out not to be a well-defined scalar. Only 12 of 34 arms end in an absorbing failure run, and on 8 of those, two reasonable definitions of onset disagree by more than 2 chunks β in one arm by 44, because a single chunk flickered past the quality checker. The label's variance was dominated by the checker, not by the process. Chaining depth needs no changepoint and no quality signal, so it replaced it.
Training
- 1,840 chunks from 34 arms, 6 frames per chunk = 11,040 frames
- 4 conv layers (16β32β48β64), BatchNorm, global average pool, 3-way linear head
- Cross-entropy with inverse-frequency class weights capped at 10Γ
- Adam 3e-3, 30 epochs, batch 32
- Shipped weights are fit on all 34 arms; every table above is from held-out arm splits
Trained on an NVIDIA Jetson AGX Orin. Both depth.onnx and depth.pt included.
Related
resoajoe/quality-gate-nanoβ is this chunk bad?resoajoe/failure-type-nanoβ why is it bad?- this β how far gone is it?
Deployment note: cap the ONNX Runtime thread pool
Measured on a Jetson AGX Orin. ONNX Runtime sizes its intra-op thread pool to the CPU core count, and those workers spin-wait between inferences. Running two 47K-parameter models this way left ~18 threads busy-waiting at ~10.6% of a core each β about 1.9 cores burned continuously to run inferences that take 0.28 ms. A model this small cannot use intra-op parallelism at all.
so = ort.SessionOptions()
so.intra_op_num_threads = 1
so.inter_op_num_threads = 1
so.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
so.add_session_config_entry("session.intra_op.allow_spinning", "0")
sess = ort.InferenceSession("depth.onnx", sess_options=so, providers=["CPUExecutionProvider"])
Measured effect on the same workload: idle CPU 192% β 16.5% of one core, active 231% β 88%, thread count 45 β 22, throughput unchanged. On edge hardware this is the difference between "runs alongside everything else" and "saturates the machine".
What "scalar baseline" means on this card
Every margin quoted here is against a stated baseline, because a margin without one is not a measurement. The baseline is the best single-threshold classifier over ten cheap statistics, fitted optimistically:
mean Β· std Β· lapvar Β· hf (high-frequency energy ratio) Β· grad (Sobel magnitude) Β·
entropy Β· centre_edge Β· radial_slope Β· row_fft_peak Β· col_fft_peak
The last four are spatially aware, added after an earlier six-statistic baseline β all global aggregates β was found to systematically overstate model value on spatially structured tasks. A baseline that cannot see where anything is loses to a CNN by default. On one test task that flaw inflated an apparent margin from +0.060 to +0.261.
Two questions are asked with it, and they disagree:
- in-sample β threshold fitted on the data it is scored on. Deliberately generous. Answers is there structure beyond a low-order statistic?
- transferred β threshold fitted on the training corpus, applied unchanged to the target. Answers what should I ship? On one task the in-sample figure was 0.954 and the transferred figure 0.565.
Where this card quotes a single scalar figure without qualification, it is the in-sample one.
Provenance and privacy
Training frames are model-generated video of a synthetic interior scene. No real patients, no real footage, and no personal data are involved anywhere in this work.