Image Classification
timm
English
gravitational-waves
ligo
vision-transformer
glitch-classification
gravity-spy
physics
deep-learning
spectrograms
continuous-gravitational-waves
resnet
detector-characterization
Eval Results (legacy)
Instructions to use JesseWeigel/ligo-glitch-vit-cnn with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- timm
How to use JesseWeigel/ligo-glitch-vit-cnn with timm:
import timm model = timm.create_model("hf_hub:JesseWeigel/ligo-glitch-vit-cnn", pretrained=True) - Notebooks
- Google Colab
- Kaggle
Initial release: ViT-B/16 and ResNet-50v2 for Gravity Spy glitch classification
Browse files- README.md +173 -3
- checkpoints/checksums.sha256 +2 -0
- checkpoints/resnet50v2_gravityspy_o3.pt +3 -0
- checkpoints/vit_b16_gravityspy_o3.pt +3 -0
- examples/expected_output.json +217 -0
- src/__pycache__/inference.cpython-312.pyc +0 -0
- src/__pycache__/preprocessing.cpython-312.pyc +0 -0
- src/class_labels.json +1 -0
- src/inference.py +192 -0
- src/model_config.json +35 -0
- src/preprocessing.py +66 -0
README.md
CHANGED
|
@@ -1,3 +1,173 @@
|
|
| 1 |
-
--
|
| 2 |
-
|
| 3 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Gravity Spy Glitch Classifier: ViT-B/16 and ResNet-50v2 BiT
|
| 2 |
+
|
| 3 |
+
Two deep learning models for classifying LIGO gravitational-wave detector
|
| 4 |
+
glitch morphologies from Q-transform spectrograms, trained on Gravity Spy O3
|
| 5 |
+
data (Zevin et al. 2017, CQG 34 064003).
|
| 6 |
+
|
| 7 |
+
## Model Overview
|
| 8 |
+
|
| 9 |
+
| Property | ViT-B/16 | ResNet-50v2 BiT |
|
| 10 |
+
|---|---|---|
|
| 11 |
+
| Architecture | Vision Transformer (patch 16, 224px) | Big Transfer ResNet-50v2 |
|
| 12 |
+
| Pretrained weights | AugReg ImageNet-21k + ImageNet-1k | ImageNet-21k + ImageNet-1k |
|
| 13 |
+
| Parameters | ~86M | ~25M |
|
| 14 |
+
| Framework | timm | timm |
|
| 15 |
+
|
| 16 |
+
Both models classify 224x224 RGB Q-transform spectrograms into 23 Gravity Spy
|
| 17 |
+
glitch classes (22 glitch morphologies + No_Glitch). The class taxonomy follows
|
| 18 |
+
Zevin et al. (2017).
|
| 19 |
+
|
| 20 |
+
## Training Data
|
| 21 |
+
|
| 22 |
+
- **Source:** Gravity Spy O3 (H1 + L1), filtered to ml_confidence > 0.9
|
| 23 |
+
- **Total samples:** 227,943 training / 48,844 validation / 48,845 test
|
| 24 |
+
- **Split:** Temporal split (70/15/15%) with 60-second gap enforcement
|
| 25 |
+
- **Classes:** 23 (see `src/class_labels.json`)
|
| 26 |
+
- **Preprocessing:** Q-transform spectrograms resized to 224x224, normalized with ImageNet statistics
|
| 27 |
+
- **Rare classes:** Chirp (11 train), Wandering_Line (30), Helix (33), Light_Modulation (142)
|
| 28 |
+
|
| 29 |
+
## Performance (O3 Test Set)
|
| 30 |
+
|
| 31 |
+
**Primary metric: macro-F1** (averaged equally across all 23 classes).
|
| 32 |
+
|
| 33 |
+
| Metric | ViT-B/16 | ResNet-50v2 BiT |
|
| 34 |
+
|---|---|---|
|
| 35 |
+
| **Macro-F1 (PRIMARY)** | **0.7230** [0.7031, 0.7397] | **0.6786** [0.6598, 0.6944] |
|
| 36 |
+
| Rare-class macro-F1 | 0.2412 [0.2019, 0.2957] | 0.3028 [0.2085, 0.3751] |
|
| 37 |
+
| Overall accuracy (secondary) | 0.9343 | 0.9181 |
|
| 38 |
+
|
| 39 |
+
*95% bootstrap confidence intervals (10,000 resamples). Macro-F1 difference is
|
| 40 |
+
statistically significant (p = 0.0002).*
|
| 41 |
+
|
| 42 |
+
> **Note:** Overall accuracy is reported as a secondary sanity check only.
|
| 43 |
+
> It is not the primary metric because it masks rare-class performance
|
| 44 |
+
> differences (see "Limitations" below).
|
| 45 |
+
|
| 46 |
+
### Per-Class Highlights
|
| 47 |
+
|
| 48 |
+
Architecture preference is **class-morphology-dependent**, not a uniform
|
| 49 |
+
advantage for either model:
|
| 50 |
+
|
| 51 |
+
| Class | ViT F1 | CNN F1 | Difference | Favors |
|
| 52 |
+
|---|---|---|---|---|
|
| 53 |
+
| Power_Line | 0.742 | 0.235 | **+0.507** | ViT |
|
| 54 |
+
| Light_Modulation | 0.859 | 0.691 | +0.168 | ViT |
|
| 55 |
+
| Scratchy | 0.875 | 0.503 | +0.372 | ViT |
|
| 56 |
+
| Chirp | 0.000 | 0.471 | **+0.471** | CNN |
|
| 57 |
+
| Scattered_Light | 0.719 | 0.811 | +0.092 | CNN |
|
| 58 |
+
| Paired_Doves | 0.613 | 0.099 | +0.514 | ViT |
|
| 59 |
+
| Violin_Mode | 0.544 | 0.683 | +0.139 | CNN |
|
| 60 |
+
|
| 61 |
+
The ViT achieves higher overall macro-F1 but performs **worse on rare classes**
|
| 62 |
+
(macro-F1 0.241 vs CNN 0.303). This is a forbidden-proxy scenario: overall
|
| 63 |
+
accuracy improvement does not extend to rare classes.
|
| 64 |
+
|
| 65 |
+
## O4 Generalization
|
| 66 |
+
|
| 67 |
+
Both models were evaluated on 38,587 O4a Gravity Spy spectrograms:
|
| 68 |
+
|
| 69 |
+
| Metric | ViT-B/16 | ResNet-50v2 BiT |
|
| 70 |
+
|---|---|---|
|
| 71 |
+
| O4 macro-F1 | 0.6695 [0.6555, 0.6816] | 0.6674 [0.6567, 0.6765] |
|
| 72 |
+
| Relative degradation from O3 | -7.4% | -1.7% |
|
| 73 |
+
|
| 74 |
+
Both models pass the <20% degradation threshold. The CNN shows better
|
| 75 |
+
generalization stability across observing runs.
|
| 76 |
+
|
| 77 |
+
## Limitations
|
| 78 |
+
|
| 79 |
+
- **O3-trained only:** Models have not been fine-tuned on O4 data.
|
| 80 |
+
Performance may degrade further on later observing runs with new
|
| 81 |
+
instrumental configurations.
|
| 82 |
+
- **Single-view:** Models use only the 1.0-second duration Q-transform view.
|
| 83 |
+
Gravity Spy's full pipeline uses four duration views (0.5s, 1.0s, 2.0s, 4.0s).
|
| 84 |
+
- **23 classes:** New glitch morphologies appearing in O4+ will be
|
| 85 |
+
misclassified into existing categories.
|
| 86 |
+
- **Rare-class regression for ViT:** The ViT's macro-F1 advantage does not
|
| 87 |
+
extend to rare classes (< 200 training samples). The CNN outperforms on
|
| 88 |
+
Chirp, Helix, and Violin_Mode.
|
| 89 |
+
- **Label quality:** Training labels are from ml_confidence > 0.9 filtering
|
| 90 |
+
of Gravity Spy citizen science classifications, not expert-reviewed labels.
|
| 91 |
+
|
| 92 |
+
## Usage
|
| 93 |
+
|
| 94 |
+
```bash
|
| 95 |
+
# Install dependencies
|
| 96 |
+
pip install torch timm albumentations numpy Pillow
|
| 97 |
+
|
| 98 |
+
# Classify a spectrogram with ViT
|
| 99 |
+
python release/src/inference.py --model vit --image path/to/spectrogram.png
|
| 100 |
+
|
| 101 |
+
# Classify with CNN, showing top-5 predictions
|
| 102 |
+
python release/src/inference.py --model cnn --image path/to/spectrogram.png --top-k 5
|
| 103 |
+
|
| 104 |
+
# JSON output
|
| 105 |
+
python release/src/inference.py --model vit --image path/to/spectrogram.png --json
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
### Input Format
|
| 109 |
+
|
| 110 |
+
- PNG or JPG image of a Q-transform spectrogram
|
| 111 |
+
- Any resolution (automatically resized to 224x224)
|
| 112 |
+
- RGB color (grayscale images are converted to 3-channel)
|
| 113 |
+
|
| 114 |
+
### Output
|
| 115 |
+
|
| 116 |
+
```
|
| 117 |
+
Model: ViT-B/16
|
| 118 |
+
Image: spectrogram.png
|
| 119 |
+
|
| 120 |
+
Predictions:
|
| 121 |
+
Rank Class Probability
|
| 122 |
+
-------------------------------------------
|
| 123 |
+
1 Blip 0.9823
|
| 124 |
+
2 Blip_Low_Frequency 0.0091
|
| 125 |
+
3 Koi_Fish 0.0034
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
## File Structure
|
| 129 |
+
|
| 130 |
+
```
|
| 131 |
+
release/
|
| 132 |
+
README.md # This file (model card)
|
| 133 |
+
checkpoints/
|
| 134 |
+
vit_b16_gravityspy_o3.pt # ViT-B/16 weights (~983 MB)
|
| 135 |
+
resnet50v2_gravityspy_o3.pt # ResNet-50v2 BiT weights (~270 MB)
|
| 136 |
+
checksums.sha256 # SHA-256 checksums for integrity
|
| 137 |
+
src/
|
| 138 |
+
inference.py # Standalone CLI inference script
|
| 139 |
+
preprocessing.py # Locked evaluation transforms
|
| 140 |
+
class_labels.json # 23-class index-to-label mapping
|
| 141 |
+
model_config.json # Architecture and training config
|
| 142 |
+
examples/
|
| 143 |
+
expected_output.json # Validated predictions for test images
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
## Verifying Checkpoint Integrity
|
| 147 |
+
|
| 148 |
+
```bash
|
| 149 |
+
cd release/checkpoints
|
| 150 |
+
sha256sum -c checksums.sha256
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
## Citation
|
| 154 |
+
|
| 155 |
+
```bibtex
|
| 156 |
+
@misc{gravitspy_vit_cnn_2026,
|
| 157 |
+
title={Vision Transformer vs. CNN for LIGO Glitch Classification:
|
| 158 |
+
Class-Morphology-Dependent Architecture Preferences},
|
| 159 |
+
author={[Authors]},
|
| 160 |
+
year={2026},
|
| 161 |
+
note={Models trained on Gravity Spy O3 data (Zevin et al. 2017, CQG 34 064003)}
|
| 162 |
+
}
|
| 163 |
+
```
|
| 164 |
+
|
| 165 |
+
## License
|
| 166 |
+
|
| 167 |
+
MIT
|
| 168 |
+
|
| 169 |
+
## References
|
| 170 |
+
|
| 171 |
+
- Zevin, M. et al. (2017). "Gravity Spy: integrating advanced LIGO detector
|
| 172 |
+
characterization, machine learning, and citizen science." *Classical and
|
| 173 |
+
Quantum Gravity*, 34(6), 064003.
|
checkpoints/checksums.sha256
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ca5635865a9ad8886e7eed2022b4b2005e439bd9033ce177d656c5068867461d vit_b16_gravityspy_o3.pt
|
| 2 |
+
65bfe7db29dce148b45dc2eba1d8a728c947c6b2432bbfc741a407801c67e9b3 resnet50v2_gravityspy_o3.pt
|
checkpoints/resnet50v2_gravityspy_o3.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:65bfe7db29dce148b45dc2eba1d8a728c947c6b2432bbfc741a407801c67e9b3
|
| 3 |
+
size 282766133
|
checkpoints/vit_b16_gravityspy_o3.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ca5635865a9ad8886e7eed2022b4b2005e439bd9033ce177d656c5068867461d
|
| 3 |
+
size 1029991217
|
examples/expected_output.json
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"filename": "qnPsv3hL2x_1.0s.png",
|
| 4 |
+
"true_label": "Blip",
|
| 5 |
+
"image_path": "data/spectrograms/Blip/qnPsv3hL2x_1.0s.png",
|
| 6 |
+
"vit_prediction": "Blip",
|
| 7 |
+
"vit_confidence": 0.990561,
|
| 8 |
+
"vit_top3": [
|
| 9 |
+
{
|
| 10 |
+
"rank": 1,
|
| 11 |
+
"class": "Blip",
|
| 12 |
+
"probability": 0.990561
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"rank": 2,
|
| 16 |
+
"class": "Blip_Low_Frequency",
|
| 17 |
+
"probability": 0.003823
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"rank": 3,
|
| 21 |
+
"class": "Repeating_Blips",
|
| 22 |
+
"probability": 0.001406
|
| 23 |
+
}
|
| 24 |
+
],
|
| 25 |
+
"cnn_prediction": "Blip",
|
| 26 |
+
"cnn_confidence": 0.655181,
|
| 27 |
+
"cnn_top3": [
|
| 28 |
+
{
|
| 29 |
+
"rank": 1,
|
| 30 |
+
"class": "Blip",
|
| 31 |
+
"probability": 0.655181
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"rank": 2,
|
| 35 |
+
"class": "Repeating_Blips",
|
| 36 |
+
"probability": 0.323093
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"rank": 3,
|
| 40 |
+
"class": "Extremely_Loud",
|
| 41 |
+
"probability": 0.002679
|
| 42 |
+
}
|
| 43 |
+
]
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"filename": "EMxVe5MHAz_1.0s.png",
|
| 47 |
+
"true_label": "Power_Line",
|
| 48 |
+
"image_path": "data/spectrograms/Power_Line/EMxVe5MHAz_1.0s.png",
|
| 49 |
+
"vit_prediction": "Power_Line",
|
| 50 |
+
"vit_confidence": 0.997734,
|
| 51 |
+
"vit_top3": [
|
| 52 |
+
{
|
| 53 |
+
"rank": 1,
|
| 54 |
+
"class": "Power_Line",
|
| 55 |
+
"probability": 0.997734
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"rank": 2,
|
| 59 |
+
"class": "No_Glitch",
|
| 60 |
+
"probability": 0.000965
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"rank": 3,
|
| 64 |
+
"class": "Blip_Low_Frequency",
|
| 65 |
+
"probability": 0.000165
|
| 66 |
+
}
|
| 67 |
+
],
|
| 68 |
+
"cnn_prediction": "Power_Line",
|
| 69 |
+
"cnn_confidence": 0.912969,
|
| 70 |
+
"cnn_top3": [
|
| 71 |
+
{
|
| 72 |
+
"rank": 1,
|
| 73 |
+
"class": "Power_Line",
|
| 74 |
+
"probability": 0.912969
|
| 75 |
+
},
|
| 76 |
+
{
|
| 77 |
+
"rank": 2,
|
| 78 |
+
"class": "Air_Compressor",
|
| 79 |
+
"probability": 0.057046
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"rank": 3,
|
| 83 |
+
"class": "No_Glitch",
|
| 84 |
+
"probability": 0.007583
|
| 85 |
+
}
|
| 86 |
+
]
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
"filename": "kwYnhvabQ8_1.0s.png",
|
| 90 |
+
"true_label": "Chirp",
|
| 91 |
+
"image_path": "data/spectrograms/Chirp/kwYnhvabQ8_1.0s.png",
|
| 92 |
+
"vit_prediction": "Blip_Low_Frequency",
|
| 93 |
+
"vit_confidence": 0.944321,
|
| 94 |
+
"vit_top3": [
|
| 95 |
+
{
|
| 96 |
+
"rank": 1,
|
| 97 |
+
"class": "Blip_Low_Frequency",
|
| 98 |
+
"probability": 0.944321
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"rank": 2,
|
| 102 |
+
"class": "Blip",
|
| 103 |
+
"probability": 0.033233
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
"rank": 3,
|
| 107 |
+
"class": "Repeating_Blips",
|
| 108 |
+
"probability": 0.005682
|
| 109 |
+
}
|
| 110 |
+
],
|
| 111 |
+
"cnn_prediction": "Chirp",
|
| 112 |
+
"cnn_confidence": 0.514547,
|
| 113 |
+
"cnn_top3": [
|
| 114 |
+
{
|
| 115 |
+
"rank": 1,
|
| 116 |
+
"class": "Chirp",
|
| 117 |
+
"probability": 0.514547
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
"rank": 2,
|
| 121 |
+
"class": "Blip_Low_Frequency",
|
| 122 |
+
"probability": 0.274587
|
| 123 |
+
},
|
| 124 |
+
{
|
| 125 |
+
"rank": 3,
|
| 126 |
+
"class": "Blip",
|
| 127 |
+
"probability": 0.14339
|
| 128 |
+
}
|
| 129 |
+
]
|
| 130 |
+
},
|
| 131 |
+
{
|
| 132 |
+
"filename": "mRGHzX9W9X_1.0s.png",
|
| 133 |
+
"true_label": "No_Glitch",
|
| 134 |
+
"image_path": "data/spectrograms/No_Glitch/mRGHzX9W9X_1.0s.png",
|
| 135 |
+
"vit_prediction": "No_Glitch",
|
| 136 |
+
"vit_confidence": 0.991169,
|
| 137 |
+
"vit_top3": [
|
| 138 |
+
{
|
| 139 |
+
"rank": 1,
|
| 140 |
+
"class": "No_Glitch",
|
| 141 |
+
"probability": 0.991169
|
| 142 |
+
},
|
| 143 |
+
{
|
| 144 |
+
"rank": 2,
|
| 145 |
+
"class": "1400Ripples",
|
| 146 |
+
"probability": 0.004831
|
| 147 |
+
},
|
| 148 |
+
{
|
| 149 |
+
"rank": 3,
|
| 150 |
+
"class": "Power_Line",
|
| 151 |
+
"probability": 0.000853
|
| 152 |
+
}
|
| 153 |
+
],
|
| 154 |
+
"cnn_prediction": "No_Glitch",
|
| 155 |
+
"cnn_confidence": 0.989384,
|
| 156 |
+
"cnn_top3": [
|
| 157 |
+
{
|
| 158 |
+
"rank": 1,
|
| 159 |
+
"class": "No_Glitch",
|
| 160 |
+
"probability": 0.989384
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"rank": 2,
|
| 164 |
+
"class": "1400Ripples",
|
| 165 |
+
"probability": 0.003378
|
| 166 |
+
},
|
| 167 |
+
{
|
| 168 |
+
"rank": 3,
|
| 169 |
+
"class": "Helix",
|
| 170 |
+
"probability": 0.002471
|
| 171 |
+
}
|
| 172 |
+
]
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
"filename": "XvHH2XRheR_1.0s.png",
|
| 176 |
+
"true_label": "Scattered_Light",
|
| 177 |
+
"image_path": "data/spectrograms/Scattered_Light/XvHH2XRheR_1.0s.png",
|
| 178 |
+
"vit_prediction": "Scattered_Light",
|
| 179 |
+
"vit_confidence": 0.995357,
|
| 180 |
+
"vit_top3": [
|
| 181 |
+
{
|
| 182 |
+
"rank": 1,
|
| 183 |
+
"class": "Scattered_Light",
|
| 184 |
+
"probability": 0.995357
|
| 185 |
+
},
|
| 186 |
+
{
|
| 187 |
+
"rank": 2,
|
| 188 |
+
"class": "Extremely_Loud",
|
| 189 |
+
"probability": 0.001687
|
| 190 |
+
},
|
| 191 |
+
{
|
| 192 |
+
"rank": 3,
|
| 193 |
+
"class": "Fast_Scattering",
|
| 194 |
+
"probability": 0.000669
|
| 195 |
+
}
|
| 196 |
+
],
|
| 197 |
+
"cnn_prediction": "Scattered_Light",
|
| 198 |
+
"cnn_confidence": 0.962074,
|
| 199 |
+
"cnn_top3": [
|
| 200 |
+
{
|
| 201 |
+
"rank": 1,
|
| 202 |
+
"class": "Scattered_Light",
|
| 203 |
+
"probability": 0.962074
|
| 204 |
+
},
|
| 205 |
+
{
|
| 206 |
+
"rank": 2,
|
| 207 |
+
"class": "Low_Frequency_Lines",
|
| 208 |
+
"probability": 0.017069
|
| 209 |
+
},
|
| 210 |
+
{
|
| 211 |
+
"rank": 3,
|
| 212 |
+
"class": "Extremely_Loud",
|
| 213 |
+
"probability": 0.004967
|
| 214 |
+
}
|
| 215 |
+
]
|
| 216 |
+
}
|
| 217 |
+
]
|
src/__pycache__/inference.cpython-312.pyc
ADDED
|
Binary file (7.76 kB). View file
|
|
|
src/__pycache__/preprocessing.cpython-312.pyc
ADDED
|
Binary file (2.53 kB). View file
|
|
|
src/class_labels.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
["1080Lines", "1400Ripples", "Air_Compressor", "Blip", "Blip_Low_Frequency", "Chirp", "Extremely_Loud", "Fast_Scattering", "Helix", "Koi_Fish", "Light_Modulation", "Low_Frequency_Burst", "Low_Frequency_Lines", "No_Glitch", "Paired_Doves", "Power_Line", "Repeating_Blips", "Scattered_Light", "Scratchy", "Tomte", "Violin_Mode", "Wandering_Line", "Whistle"]
|
src/inference.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Standalone inference script for Gravity Spy glitch classification.
|
| 3 |
+
|
| 4 |
+
Classifies LIGO glitch spectrograms using either a ViT-B/16 or ResNet-50v2 BiT
|
| 5 |
+
model trained on O3 Gravity Spy data (23 classes).
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
python inference.py --model vit --image path/to/spectrogram.png
|
| 9 |
+
python inference.py --model cnn --image path/to/spectrogram.png --top-k 5
|
| 10 |
+
|
| 11 |
+
Requirements: torch, timm, albumentations, numpy, Pillow
|
| 12 |
+
No training code imports required.
|
| 13 |
+
"""
|
| 14 |
+
# ASSERT_CONVENTION: primary_metric=macro_f1, input_format=224x224_RGB_PNG_0to1
|
| 15 |
+
|
| 16 |
+
import argparse
|
| 17 |
+
import json
|
| 18 |
+
import sys
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
|
| 21 |
+
import numpy as np
|
| 22 |
+
import torch
|
| 23 |
+
import timm
|
| 24 |
+
|
| 25 |
+
from preprocessing import load_image
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def load_model(model_key, config_path=None, checkpoint_dir=None):
|
| 29 |
+
"""Load a model from config and checkpoint.
|
| 30 |
+
|
| 31 |
+
Parameters
|
| 32 |
+
----------
|
| 33 |
+
model_key : str
|
| 34 |
+
Either 'vit' or 'cnn'.
|
| 35 |
+
config_path : Path or None
|
| 36 |
+
Path to model_config.json. Defaults to same directory as this script.
|
| 37 |
+
checkpoint_dir : Path or None
|
| 38 |
+
Path to checkpoints directory. Defaults to ../checkpoints/ relative to this script.
|
| 39 |
+
|
| 40 |
+
Returns
|
| 41 |
+
-------
|
| 42 |
+
model : torch.nn.Module
|
| 43 |
+
Loaded model in mode for inference.
|
| 44 |
+
class_labels : list of str
|
| 45 |
+
Ordered class label names (index -> label).
|
| 46 |
+
"""
|
| 47 |
+
script_dir = Path(__file__).resolve().parent
|
| 48 |
+
if config_path is None:
|
| 49 |
+
config_path = script_dir / "model_config.json"
|
| 50 |
+
if checkpoint_dir is None:
|
| 51 |
+
checkpoint_dir = script_dir.parent / "checkpoints"
|
| 52 |
+
|
| 53 |
+
with open(config_path) as f:
|
| 54 |
+
config = json.load(f)
|
| 55 |
+
|
| 56 |
+
if model_key not in config["models"]:
|
| 57 |
+
available = list(config["models"].keys())
|
| 58 |
+
raise ValueError(f"Unknown model key '{model_key}'. Choose from: {available}")
|
| 59 |
+
|
| 60 |
+
model_cfg = config["models"][model_key]
|
| 61 |
+
|
| 62 |
+
# Build architecture via timm (no pretrained weights -- we load our own)
|
| 63 |
+
model = timm.create_model(
|
| 64 |
+
model_cfg["model_name"],
|
| 65 |
+
pretrained=False,
|
| 66 |
+
num_classes=model_cfg["num_classes"],
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
# Load trained weights
|
| 70 |
+
checkpoint_path = checkpoint_dir / model_cfg["checkpoint_file"]
|
| 71 |
+
if not checkpoint_path.exists():
|
| 72 |
+
raise FileNotFoundError(
|
| 73 |
+
f"Checkpoint not found: {checkpoint_path}\n"
|
| 74 |
+
f"Expected file: {model_cfg['checkpoint_file']}"
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
|
| 78 |
+
# Handle both full training checkpoints (with 'model_state_dict' key)
|
| 79 |
+
# and bare state_dict files
|
| 80 |
+
if isinstance(checkpoint, dict) and "model_state_dict" in checkpoint:
|
| 81 |
+
state_dict = checkpoint["model_state_dict"]
|
| 82 |
+
else:
|
| 83 |
+
state_dict = checkpoint
|
| 84 |
+
model.load_state_dict(state_dict)
|
| 85 |
+
|
| 86 |
+
# Set to inference mode
|
| 87 |
+
model.requires_grad_(False)
|
| 88 |
+
model.eval()
|
| 89 |
+
|
| 90 |
+
# Load class labels
|
| 91 |
+
labels_path = script_dir / "class_labels.json"
|
| 92 |
+
with open(labels_path) as f:
|
| 93 |
+
class_labels = json.load(f)
|
| 94 |
+
|
| 95 |
+
return model, class_labels
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def predict(model, image_tensor, class_labels, top_k=3):
|
| 99 |
+
"""Run inference on a preprocessed image tensor.
|
| 100 |
+
|
| 101 |
+
Parameters
|
| 102 |
+
----------
|
| 103 |
+
model : torch.nn.Module
|
| 104 |
+
Model in inference mode.
|
| 105 |
+
image_tensor : torch.Tensor
|
| 106 |
+
Preprocessed image of shape (3, 224, 224).
|
| 107 |
+
class_labels : list of str
|
| 108 |
+
Ordered class label names.
|
| 109 |
+
top_k : int
|
| 110 |
+
Number of top predictions to return.
|
| 111 |
+
|
| 112 |
+
Returns
|
| 113 |
+
-------
|
| 114 |
+
predictions : list of dict
|
| 115 |
+
Top-k predictions, each with 'rank', 'class', 'probability'.
|
| 116 |
+
"""
|
| 117 |
+
with torch.no_grad():
|
| 118 |
+
logits = model(image_tensor.unsqueeze(0)) # (1, num_classes)
|
| 119 |
+
probs = torch.softmax(logits, dim=1).squeeze(0) # (num_classes,)
|
| 120 |
+
|
| 121 |
+
top_k = min(top_k, len(class_labels))
|
| 122 |
+
top_probs, top_indices = torch.topk(probs, top_k)
|
| 123 |
+
|
| 124 |
+
predictions = []
|
| 125 |
+
for rank, (prob, idx) in enumerate(zip(top_probs, top_indices), 1):
|
| 126 |
+
predictions.append({
|
| 127 |
+
"rank": rank,
|
| 128 |
+
"class": class_labels[idx.item()],
|
| 129 |
+
"probability": round(prob.item(), 6),
|
| 130 |
+
})
|
| 131 |
+
|
| 132 |
+
return predictions
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def main():
|
| 136 |
+
parser = argparse.ArgumentParser(
|
| 137 |
+
description="Gravity Spy glitch classification inference",
|
| 138 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 139 |
+
epilog=(
|
| 140 |
+
"Examples:\n"
|
| 141 |
+
" python inference.py --model vit --image spectrogram.png\n"
|
| 142 |
+
" python inference.py --model cnn --image spectrogram.png --top-k 5\n"
|
| 143 |
+
" python inference.py --model vit --image spectrogram.png --json\n"
|
| 144 |
+
),
|
| 145 |
+
)
|
| 146 |
+
parser.add_argument("--model", required=True, choices=["vit", "cnn"],
|
| 147 |
+
help="Model to use: vit (ViT-B/16) or cnn (ResNet-50v2 BiT)")
|
| 148 |
+
parser.add_argument("--image", required=True, type=str,
|
| 149 |
+
help="Path to spectrogram image (PNG/JPG)")
|
| 150 |
+
parser.add_argument("--top-k", type=int, default=3,
|
| 151 |
+
help="Number of top predictions to show (default: 3)")
|
| 152 |
+
parser.add_argument("--json", action="store_true",
|
| 153 |
+
help="Output predictions as JSON")
|
| 154 |
+
args = parser.parse_args()
|
| 155 |
+
|
| 156 |
+
# Validate image path
|
| 157 |
+
image_path = Path(args.image)
|
| 158 |
+
if not image_path.exists():
|
| 159 |
+
print(f"Error: Image not found: {image_path}", file=sys.stderr)
|
| 160 |
+
sys.exit(1)
|
| 161 |
+
|
| 162 |
+
# Load model
|
| 163 |
+
model, class_labels = load_model(args.model)
|
| 164 |
+
|
| 165 |
+
# Preprocess image
|
| 166 |
+
image_tensor = load_image(str(image_path))
|
| 167 |
+
|
| 168 |
+
# Run inference
|
| 169 |
+
predictions = predict(model, image_tensor, class_labels, top_k=args.top_k)
|
| 170 |
+
|
| 171 |
+
# Output results
|
| 172 |
+
if args.json:
|
| 173 |
+
result = {
|
| 174 |
+
"model": args.model,
|
| 175 |
+
"image": str(image_path),
|
| 176 |
+
"predictions": predictions,
|
| 177 |
+
}
|
| 178 |
+
print(json.dumps(result, indent=2))
|
| 179 |
+
else:
|
| 180 |
+
model_name = "ViT-B/16" if args.model == "vit" else "ResNet-50v2 BiT"
|
| 181 |
+
print(f"\nModel: {model_name}")
|
| 182 |
+
print(f"Image: {image_path}")
|
| 183 |
+
print(f"\nPredictions:")
|
| 184 |
+
print(f"{'Rank':<6} {'Class':<25} {'Probability':<12}")
|
| 185 |
+
print("-" * 43)
|
| 186 |
+
for pred in predictions:
|
| 187 |
+
print(f"{pred['rank']:<6} {pred['class']:<25} {pred['probability']:.4f}")
|
| 188 |
+
print()
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
if __name__ == "__main__":
|
| 192 |
+
main()
|
src/model_config.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"models": {
|
| 3 |
+
"vit": {
|
| 4 |
+
"model_name": "vit_base_patch16_224.augreg_in21k_ft_in1k",
|
| 5 |
+
"architecture": "ViT-B/16",
|
| 6 |
+
"num_classes": 23,
|
| 7 |
+
"pretrained_source": "AugReg ImageNet-21k fine-tuned on ImageNet-1k",
|
| 8 |
+
"checkpoint_file": "vit_b16_gravityspy_o3.pt",
|
| 9 |
+
"input_size": 224,
|
| 10 |
+
"normalization": {
|
| 11 |
+
"mean": [0.485, 0.456, 0.406],
|
| 12 |
+
"std": [0.229, 0.224, 0.225]
|
| 13 |
+
}
|
| 14 |
+
},
|
| 15 |
+
"cnn": {
|
| 16 |
+
"model_name": "resnetv2_50x1_bit.goog_in21k_ft_in1k",
|
| 17 |
+
"architecture": "ResNet-50v2 BiT",
|
| 18 |
+
"num_classes": 23,
|
| 19 |
+
"pretrained_source": "ImageNet-21k fine-tuned on ImageNet-1k",
|
| 20 |
+
"checkpoint_file": "resnet50v2_gravityspy_o3.pt",
|
| 21 |
+
"input_size": 224,
|
| 22 |
+
"normalization": {
|
| 23 |
+
"mean": [0.485, 0.456, 0.406],
|
| 24 |
+
"std": [0.229, 0.224, 0.225]
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
},
|
| 28 |
+
"training": {
|
| 29 |
+
"dataset": "Gravity Spy O3",
|
| 30 |
+
"num_classes": 23,
|
| 31 |
+
"split": "temporal 70/15/15% with 60s gap enforcement",
|
| 32 |
+
"train_samples": 227943,
|
| 33 |
+
"seed": 42
|
| 34 |
+
}
|
| 35 |
+
}
|
src/preprocessing.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Standalone preprocessing for Gravity Spy spectrogram inference.
|
| 2 |
+
|
| 3 |
+
Extracts eval_transforms() from the training pipeline with NO training
|
| 4 |
+
dependencies (no wandb, no dataloader, no training-specific imports).
|
| 5 |
+
|
| 6 |
+
Preprocessing is locked to match training exactly:
|
| 7 |
+
- Resize to 224x224
|
| 8 |
+
- Normalize with ImageNet statistics
|
| 9 |
+
- Convert to PyTorch tensor
|
| 10 |
+
"""
|
| 11 |
+
# ASSERT_CONVENTION: primary_metric=macro_f1, input_format=224x224_RGB_PNG_0to1
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
from PIL import Image
|
| 15 |
+
import albumentations as A
|
| 16 |
+
from albumentations.pytorch import ToTensorV2
|
| 17 |
+
|
| 18 |
+
# ImageNet statistics for pretrained model normalization
|
| 19 |
+
IMAGENET_MEAN = [0.485, 0.456, 0.406]
|
| 20 |
+
IMAGENET_STD = [0.229, 0.224, 0.225]
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def eval_transforms(image_size=224):
|
| 24 |
+
"""Evaluation transform -- resize + normalize only, no augmentation.
|
| 25 |
+
|
| 26 |
+
This is identical to the eval_transforms used during training/validation.
|
| 27 |
+
Input images are expected to be RGB numpy arrays with pixel values in [0, 255].
|
| 28 |
+
Output tensors have pixel values normalized by ImageNet statistics.
|
| 29 |
+
|
| 30 |
+
Parameters
|
| 31 |
+
----------
|
| 32 |
+
image_size : int
|
| 33 |
+
Target spatial dimension (default 224 for ViT-B/16 and ResNet-50v2).
|
| 34 |
+
|
| 35 |
+
Returns
|
| 36 |
+
-------
|
| 37 |
+
transform : albumentations.Compose
|
| 38 |
+
Evaluation transform pipeline.
|
| 39 |
+
"""
|
| 40 |
+
return A.Compose([
|
| 41 |
+
A.Resize(image_size, image_size),
|
| 42 |
+
A.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
|
| 43 |
+
ToTensorV2(),
|
| 44 |
+
])
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def load_image(image_path, image_size=224):
|
| 48 |
+
"""Load an image file and apply evaluation transforms.
|
| 49 |
+
|
| 50 |
+
Parameters
|
| 51 |
+
----------
|
| 52 |
+
image_path : str
|
| 53 |
+
Path to a PNG/JPG spectrogram image.
|
| 54 |
+
image_size : int
|
| 55 |
+
Target spatial dimension (default 224).
|
| 56 |
+
|
| 57 |
+
Returns
|
| 58 |
+
-------
|
| 59 |
+
tensor : torch.Tensor
|
| 60 |
+
Preprocessed image tensor of shape (3, image_size, image_size).
|
| 61 |
+
"""
|
| 62 |
+
img = Image.open(image_path).convert("RGB")
|
| 63 |
+
img_np = np.array(img) # shape (H, W, 3), dtype uint8, values [0, 255]
|
| 64 |
+
transform = eval_transforms(image_size)
|
| 65 |
+
transformed = transform(image=img_np)
|
| 66 |
+
return transformed["image"] # torch.Tensor (3, 224, 224)
|