NEU-CLS steel defect classifier (ResNet-18, ONNX)
ImageNet-pretrained ResNet-18 fine-tuned on the six classes of the NEU-CLS steel surface defect database: crazing, inclusion, patches, pitted surface, rolled-in scale, scratches. Exported to a single ONNX file for CPU or in-browser serving.
Try it in the browser: huggingface.co/spaces/senanurcetin/neu-steel-defect-demo · project and the operator review-queue design it plugs into: github.com/senanurcetin/visual-qc-project.
Results, and what they do not mean
| model | split | accuracy | macro F1 |
|---|---|---|---|
| Random Forest on HOG + Gabor + grid features (the project's model) | random 80/20, seed 42 | 0.939 | 0.939 |
| this ResNet-18 | random 80/20, seed 42 | 1.000 (360/360) | 1.000 |
| this ResNet-18 | blocked: images 241–300 of every class held out | 1.000 (360/360) | 1.000 |
The blocked split exists because NEU images are crops of the same steel strips: neighbouring indices can be near-duplicates, which a random split puts on both sides. The score did not move.
A perfect score means the benchmark is saturated, not that the model is ready for a line. NEU-CLS images are 200×200 grayscale crops taken under one setup. It no longer separates good models from better ones, and it says nothing about different lighting, cameras, steel grades or defect sizes. Validate on your own line's images before relying on it, and keep the project's confidence-based review queue (route low-confidence parts to an operator).
Where the 100% ends. In the Kaggle notebook NEU Steel Defects: 100% Accuracy and Where It Ends, a ResNet-18 trained the same way is tested on the same 360 images, each degraded at test time only:
| test-time condition | accuracy (two GPU runs) |
|---|---|
| clean | 1.000 |
| darker lighting (x0.5) | 1.000 |
| low contrast (x0.5) | 0.947–0.967 |
| brighter lighting (x1.6) | 0.822–0.828 |
| sensor noise, sd 0.05 | 0.692–0.808 |
| JPEG quality 15 | 0.678–0.767 |
| half resolution | 0.642–0.661 |
| slight blur (radius 1.5) | 0.556–0.631 |
| quarter resolution | 0.514–0.594 |
| sensor noise, sd 0.12 | 0.344–0.386 |
| strong blur (radius 3) | 0.356–0.375 |
GPU training is not bit-for-bit reproducible, so the figures move by a few points between runs; the large losses repeat.
Focus, resolution and sensor noise are the conditions to control on the line (or to add to training as augmentation); the clean-image score does not show them.
Use
Input: 1×3×224×224 float32. Convert the grayscale image to 3 channels, resize to 224×224, scale
to [0, 1] and normalise with ImageNet mean/std (labels.json holds the exact values and class
order). Output: logits over the six classes.
import json, numpy as np, onnxruntime as ort
from PIL import Image
meta = json.load(open("labels.json"))
x = np.asarray(Image.open("part.jpg").convert("L").convert("RGB").resize((224, 224)), np.float32) / 255
x = ((x - meta["mean"]) / meta["std"]).transpose(2, 0, 1)[None].astype(np.float32)
logits = ort.InferenceSession("neu_resnet18.onnx").run(None, {"image": x})[0][0]
print(meta["classes"][int(logits.argmax())])
train_cnn.py reproduces both splits (12 epochs, AdamW, flips and small rotations; ~5 min on one GPU).
Data
NEU surface defect database (K. Song and Y. Yan, Northeastern University), 1,800 images, 300 per class. The images are not redistributed here.