- clinical-attire-nano
- Read this first: the labels are machine-generated
- Results β and pick your resolution
- Where to put the camera β computed, not guessed
- Scope: operations, never diagnosis
- The taxonomy came from an audit, and the audit changed it
- Usage
- Known failure modes, measured
- The labels are published too
- Related
- Read this first: the labels are machine-generated
β Not validated on camera footage β and the 256 px recommendation is withdrawn
This model is trained on Places365 photographs. Tested against 1,684 frames of fixed-camera hospital-room video containing a gowned patient in nearly every frame, with staff in scrubs entering in a minority:
variant patientfiresstafffiresvisitorfires64Γ64 0.4% 19.2% 4.1% 256Γ256 5.6% 1.7% 68.8% It essentially never detects the patient, and at 256 px labels 68.8% of frames
visitorβ street clothes that appear nowhere in the footage. Neither variant is validated on wall- or ceiling-mounted camera footage, and neither should be relied on for it.The recommendation to prefer the 256 px variant is withdrawn. It was based on Places365 validation (
maskAP 0.401 β 0.587), which is real but did not predict deployment: on camera footage the 256 px model is worse, and itsvisitorhead fails catastrophically.The numbers further down remain accurate for photographs. Use this model on photographic inputs, or validate on your own footage first.
Test caveat: generated rather than filmed footage, one room, roughly two people β so it cannot measure cross-ward generalisation. The ground truth is nonetheless certain.
clinical-attire-nano
47,252 parameters. 189 KB. Two input resolutions β 64x64 at 2,800 fps, or 256x256 at 219 fps.
Four independent heads: staff Β· patient Β· visitor Β· mask
Answers "who is in this room, and is a mask being worn" from a 64Γ64 frame β fast enough to run on every frame of a video stream on a CPU.
Domain measured / deployment domain tested: measured on Places365 handheld photographs with VL-model labels; deployment domain: TESTED on fixed-camera frames and NOT validated there; see the notice above. (Fifth line of the card standard, added 2026-09-02: a number is only as good as the domain it was measured in.)
Read this first: the labels are machine-generated
No human labelled this data. There is no public dataset for scrubs, hospital gowns or lab coats β
Fashionpedia has none of the 46 categories, and searches return only face-mask corpora and
construction PPE. So the labels were produced by qwen2.5vl:7b on Places365 clinical and
domestic scenes:
- Every image asked twice with different wording; only agreeing answers kept β 2,966 of 4,000 (74%).
- Cleaned against an independent person detector (yolov8n-pose, sharing no failure mode with the VL model): images where the two disagreed on whether a person is present were dropped. 229 removed, 2,737 kept.
- A 16-image hand audit put teacher accuracy at roughly 78%.
This model therefore cannot exceed a ~78%-accurate teacher, and every number below measures
agreement with that teacher rather than with reality. Treat it as a fast approximation of
qwen2.5vl:7b at 1/150,000th the parameters, not as a validated classifier.
Results β and pick your resolution
Two variants ship, identical in every way except input size. Nano global-average-pools, so the parameter count is 47,252 at both; only the pixels differ.
clinical_attire.onnx (64Γ64) |
clinical_attire_256.onnx (256Γ256) |
|
|---|---|---|
| latency, 1 CPU thread | 0.36 ms (2,800 fps) | 4.56 ms (219 fps) |
staff AP |
0.466 | 0.605 |
patient AP |
0.382 | 0.479 |
visitor AP |
0.491 | 0.604 |
mask AP |
0.401 | 0.587 |
For photographic input, use 256Γ256 unless severely compute-bound β but see the warning at the top: on camera footage the 256 px variant is worse, and neither variant is validated there. 219 fps is still 7Γ what a 30 fps camera
needs, and the accuracy gain is large β mask improves most (+0.186), which is expected, since a
surgical mask is a handful of pixels at 64Γ64 and a recognisable object at 256Γ256.
The margin roughly doubles, because the baseline cannot use the pixels
Margin over the cheap baseline (best of ten cheap statistics and the three raw channel means β colour is included deliberately, since scrubs are teal and lab coats white):
| head | margin @64 | margin @256 | cheap AP @64 β @256 |
|---|---|---|---|
staff |
+0.140 | +0.278 | 0.326 β 0.327 |
patient |
+0.193 | +0.256 | 0.189 β 0.223 |
visitor |
+0.188 | +0.303 | 0.302 β 0.300 |
mask |
+0.121 | +0.305 | 0.280 β 0.282 |
The cheap baseline is flat with resolution β global aggregates wash out detail however many pixels they are given β while the model gains steadily. Higher resolution is exactly where a learned model earns its keep over a threshold.
Full sweep, constant parameters
| resolution | ms | fps | staff | patient | visitor | mask |
|---|---|---|---|---|---|---|
| 64 | 0.31 | 3,200 | 0.476 | 0.376 | 0.510 | 0.439 |
| 128 | 1.16 | 859 | 0.540 | 0.413 | 0.537 | 0.506 |
| 192 | 2.62 | 382 | 0.574 | 0.471 | 0.589 | 0.542 |
| 256 | 4.51 | 222 | 0.610 | 0.477 | 0.595 | 0.596 |
Still rising at 256, and 256 is the ceiling of the DATA, not of the method β Places365-Standard ships at 256Γ256. A 720p deployment offers more detail than these labels can describe, and whether the trend continues past 256 is unmeasured. Do not extrapolate it.
The cheap baseline is the best of ten cheap image statistics and the three raw channel means, with threshold direction fitted on train β colour is included deliberately, because scrubs are solid teal and lab coats are white, and a model that only beat a greyscale baseline would be winning on colour it has no credit for.
Training set: 2,737 images; positives 575 staff, 361 patient, 524 visitor, 276 mask.
Where to put the camera β computed, not guessed
Whether a feature is answerable depends on how many pixels it spans, and that follows from physical size. A face mask is ~150 mm on a ~1700 mm person; a patient gown is ~1200 mm. Roughly 8 px on the feature is the threshold for classifiability, 16 px comfortable.
This model's training photos are portraits β a person fills a median 81% of frame height (measured with a pose detector over 472 labelled images). A wall camera sees nothing like that, so the table below is what matters for deployment.
Pixels on feature at 720p, by how much of the frame height a person occupies:
| feature | mm | portrait 81% | seated 45% | bedside 25% | corner 13% | wide ward 7% |
|---|---|---|---|---|---|---|
| patient gown | 1200 | 412 | 229 | 127 | 66 | 36 |
| street clothes | 700 | 240 | 133 | 74 | 39 | 21 |
| lab coat | 900 | 309 | 172 | 95 | 50 | 27 |
| scrubs top | 500 | 172 | 95 | 53 | 28 | 15 |
| face mask | 150 | 52 | 29 | 16 | 8 | 4 β |
| ID badge | 85 | 29 | 16 | 9 | 5 β | 3 β |
| wristband | 60 | 21 | 11 | 6 β | 3 β | 2 β |
| nasal cannula | 6 | 2 β | 1 β | 0.6 β | β | β |
β = under 8 px, not answerable. Bold = marginal.
What follows, without running anything:
- Role and attire work from any realistic room camera at 720p. Gowns, scrubs and street clothes are large features; even a wide ward view keeps them above threshold.
- Mask compliance needs the person at β₯13% of frame height at 720p. A wide ward view will not support it. A bedside or doorway camera will.
- Wristbands and ID badges are marginal even at bedside framing β 6β9 px. Do not build on them.
- IV lines and nasal cannulae are hopeless from a room camera at any practical resolution. Putting 8 px on a 6 mm cannula at bedside framing needs a 9,067-pixel-tall sensor. That is not a model problem and no amount of training fixes it; it needs a bedside close-up, a different sensor, or a different approach entirely.
This also explains the resolution result above: between a 64 and 256 px input, face mask is the
only feature that crosses the 8 px threshold (4.6 β 18.2 px). It gained most (+0.186). The others
were already above it and gained less. The law predicted the ordering before it was measured.
Prior art: this is Johnson's criteria, rediscovered
The ~8 px threshold above was derived empirically from average-precision curves. It then turned out to match Johnson's criteria (John Johnson, 1958), the standard used in surveillance and thermal optics for over half a century:
| task | Johnson (1958) | measured here |
|---|---|---|
| detection β something is there | 2 px | β |
| recognition β what kind of thing | 8 px | ~8 px |
| identification β which specific one | 12.8 px | β |
The empirical threshold for "can a CNN classify this attribute" lands on Johnson's recognition value. That convergence is worth stating plainly: the principle is not new, and credit belongs to Johnson.
What is offered here is the application rather than the principle β Johnson's criteria describe a human observer detecting or recognising a whole object, whereas the numbers above are a CNN classifying fine-grained attributes of an object it is already looking at, validated per-attribute against training support as a confound. The zoom calculation and the a-priori camera spec follow from that, and the fact that a 1958 optics rule predicts modern CNN behaviour this well is arguably the more useful finding.
Scope: operations, never diagnosis
Who is present and whether PPE is worn are staffing, workflow and compliance questions. This model says nothing clinical about any person, and cannot β it sees a 64Γ64 thumbnail. Not a medical device.
The taxonomy came from an audit, and the audit changed it
The first class list said "hospital gown". Inspecting labelled examples by eye showed its only failures were staff in blue surgical gowns labelled as such β correct English, operationally backwards, since a clinician's sterile gown and a patient's gown mean opposite things. The heads below separate the evidence and report role:
staff scrubs, surgical gown, lab coat
patient patient gown
visitor street clothes
mask orthogonal β a compliance signal, not a role
Usage
import numpy as np, onnxruntime as ort, cv2
so = ort.SessionOptions(); so.intra_op_num_threads = 1
so.add_session_config_entry("session.intra_op.allow_spinning", "0") # else it burns cores idling
sess = ort.InferenceSession("clinical_attire_256.onnx", sess_options=so, # or the 64x64 variant
providers=["CPUExecutionProvider"])
RES = 256 # must match the file you loaded
HEADS = ["staff", "patient", "visitor", "mask"]
frame = cv2.imread("room.jpg") # BGR, any size
x = cv2.resize(frame, (RES, RES), interpolation=cv2.INTER_AREA).astype(np.float32) / 255.0
x = ((x - x.mean()) / (x.std() + 1e-6)).transpose(2, 0, 1)[None]
logits = sess.run(None, {"image": x})[0][0]
probs = 1 / (1 + np.exp(-logits)) # independent sigmoids, NOT a softmax
print(dict(zip(HEADS, probs.round(3))))
Input is BGR, per-image standardised. Heads are independent β a room can contain staff and a patient and a visitor at once, which 180 training images do.
Aggregate over frames. Single-frame AP of 0.34β0.49 is a triage signal, not a decision. On video, average across a second or more.
Known failure modes, measured
- Hallucinated attire: 3.9% of attire claims had no person detected by the independent detector
(
patient_gownworst at 7.1% β bedding mistaken for a gown;lab_coat0%). Those were removed from training but the tendency is inherited. - Missed people: 12.3% of "nobody visible" labels did contain a detected person.
- Oxygen masks count as
mask. The audit found a child on a nebuliser labelled mask-positive. For PPE-compliance use this is a real confusion: infection control and respiratory support are opposite situations. - Places365 contains illustrations. One audited example was a cartoon of nurses. Drawings are in the training set.
- Trained on photographs, not camera footage. A wall-mounted camera sees wider, higher, emptier scenes; that gap is unmeasured.
- Resolution matters more than anything else here, for photographs. On camera footage the ordering reverses β see the warning at the top. At 64Γ64 a mask is a few pixels; the 256Γ256
variant lifts
maskAP from 0.401 to 0.587 at the same parameter count. If you can afford 4.56 ms, use it.
The labels are published too
resoajoe/clinical-attire-labels
β all 4,000 rows including the 1,034 where the two prompts disagreed, with both raw answers retained
so you can re-parse or re-adjudicate rather than take my parsing on trust.
Related
resoajoe/clinical-scene-nanoβ is this a patient room? (the scene rung beneath this one)resoajoe/room-audio-ladder-nanoβ the audio equivalent