| { |
| "schema_version": "1.0.0", |
| "note": "The single source of truth for pixel handling. scripts/build_frame_dataset.py, scripts/train.py, src/organscan/preprocess.py and the exported ONNX graph all read this file, and scripts/export_onnx.py copies it into models/ so the shipped artifact carries its own contract. If training and serving ever disagree about normalisation, the model silently loses several points and nothing errors - hence one file, not four constants.", |
|
|
| "input_name": "pixel_values", |
| "dtype": "float32", |
| "dtype_note": "float32 in, even for the INT8 model. Static quantization inserts QuantizeLinear at the graph head; handing it uint8 is a silent accuracy loss.", |
| "layout": "NCHW", |
| "channels": 3, |
| "channels_note": "Always 3. Grayscale B-mode replicates across channels; colour Doppler uses them for real.", |
| "image_size": 224, |
| "dynamic_axes": { "batch": 0 }, |
| "spatial_dims_fixed": true, |
| "spatial_dims_note": "H and W are fixed at 224. Dynamic spatial dims block ORT/NNAPI kernel selection and cost real latency on edge hardware.", |
|
|
| "pipeline": [ |
| { "step": 1, "op": "decode", "detail": "Honour TransferSyntaxUID. Multi-frame ultrasound is sampled by temporal stride, never frame-by-frame." }, |
| { "step": 2, "op": "photometric", "detail": "Invert when PhotometricInterpretation == MONOCHROME1." }, |
| { "step": 3, "op": "crop_scan_region", "detail": "Ultrasound: crop to SequenceOfUltrasoundRegions (0018,6011) RegionLocationMinX0/MinY0/MaxX1/MaxY1. Non-negotiable - vendor UI text often reads the organ name outright." }, |
| { "step": 4, "op": "intensity", "detail": "US is already 8-bit. CT/MR apply RescaleSlope/RescaleIntercept, then the fixed window below." }, |
| { "step": 5, "op": "resize", "detail": "Shortest side to 224, bilinear, antialias on." }, |
| { "step": 6, "op": "center_crop", "detail": "224 x 224." }, |
| { "step": 7, "op": "to_float", "detail": "Divide by 255 into [0, 1]." }, |
| { "step": 8, "op": "normalize", "detail": "Subtract mean, divide by std." }, |
| { "step": 9, "op": "layout", "detail": "HWC -> CHW, stack to [B, 3, 224, 224]." } |
| ], |
|
|
| "resize": { |
| "shortest_side": 224, |
| "interpolation": "bilinear", |
| "antialias": true |
| }, |
| "center_crop": 224, |
| "rescale_factor": 0.00392156862745098, |
| "image_mean": [0.485, 0.456, 0.406], |
| "image_std": [0.229, 0.224, 0.225], |
| "normalization_note": "ImageNet statistics, because the student is ImageNet-initialised. scripts/export_onnx.py re-reads the chosen timm config at export time and fails if it disagrees with these values rather than letting them drift.", |
|
|
| "ct_windows": { |
| "note": "Fixed window per region, applied to Hounsfield units after rescale. `pseudo_rgb` stacks three windows into the three channels instead of replicating one - free accuracy on CT, and the reason channels is 3 rather than 1.", |
| "default": { "center": 40, "width": 400 }, |
| "abdomen": { "center": 40, "width": 400 }, |
| "chest": { "center": -600, "width": 1500 }, |
| "head": { "center": 40, "width": 80 }, |
| "extremity": { "center": 300, "width": 1500 }, |
| "pseudo_rgb": { |
| "enabled": true, |
| "channels": [ |
| { "center": 40, "width": 400, "label": "soft_tissue" }, |
| { "center": -600, "width": 1500, "label": "lung" }, |
| { "center": 300, "width": 1500, "label": "bone" } |
| ] |
| } |
| }, |
|
|
| "mr_intensity": { |
| "note": "MR has no absolute scale, so a fixed window is meaningless. Percentile clip then min-max into [0,1].", |
| "clip_percentiles": [0.5, 99.5] |
| }, |
|
|
| "ultrasound": { |
| "require_region_crop": true, |
| "require_region_crop_note": "A US frame without a successful region crop is rejected by scripts/build_frame_dataset.py rather than used. Burned-in annotation is a label leak that produces a 99% model which fails on every other scanner (plan section 8.1).", |
| "frames_per_loop": { "min": 5, "max": 20, "selection": "temporal_stride" } |
| }, |
|
|
| "train_augmentation": { |
| "note": "Deliberately mild. Anatomy classification is left/right sensitive - a horizontal flip turns KIDNEY_LEFT into KIDNEY_RIGHT, so it is off everywhere.", |
| "horizontal_flip": 0.0, |
| "horizontal_flip_note": "Zero, permanently. Enabling it silently destroys every laterality label in the corpus.", |
| "vertical_flip": 0.0, |
| "random_resized_crop": { "enabled": true, "scale": [0.7, 1.0], "ratio": [0.85, 1.18] }, |
| "rotation_degrees": 10, |
| "brightness": 0.2, |
| "contrast": 0.2, |
| "gamma": [0.8, 1.25], |
| "gaussian_noise_std": 0.02, |
| "speckle_noise": { "enabled": true, "modalities": ["US"], "std": 0.05 }, |
| "random_erasing": 0.1 |
| } |
| } |
|
|