Upload folder using huggingface_hub
Browse files- README.md +57 -3
- config.json +28 -0
- inference.py +63 -0
- metadata.json +160 -0
- requirements.txt +3 -0
- shit_detector.onnx +3 -0
README.md
CHANGED
|
@@ -1,3 +1,57 @@
|
|
| 1 |
-
---
|
| 2 |
-
license: mit
|
| 3 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
pipeline_tag: image-classification
|
| 4 |
+
tags:
|
| 5 |
+
- onnx
|
| 6 |
+
- onnxruntime
|
| 7 |
+
- image-classification
|
| 8 |
+
- binary-classification
|
| 9 |
+
library_name: onnx
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# cstria0106/shit-detector
|
| 13 |
+
|
| 14 |
+
ONNX image classifier for detecting whether an image contains feces or toilet waste/staining under this project's labeling policy.
|
| 15 |
+
|
| 16 |
+
## Files
|
| 17 |
+
|
| 18 |
+
- `shit_detector.onnx`: ONNX model exported with dynamic batch axis.
|
| 19 |
+
- `metadata.json`: preprocessing values, threshold, class names, evaluation metrics, and deployment notes.
|
| 20 |
+
- `inference.py`: minimal ONNX Runtime inference helper.
|
| 21 |
+
- `requirements.txt`: runtime dependencies for the helper.
|
| 22 |
+
|
| 23 |
+
## Labels
|
| 24 |
+
|
| 25 |
+
Class order: `['shit', 'not_shit']`.
|
| 26 |
+
|
| 27 |
+
The positive class is `shit`. The default decision threshold is `0.149` on the positive-class probability.
|
| 28 |
+
|
| 29 |
+
## Evaluation Snapshot
|
| 30 |
+
|
| 31 |
+
- ID test precision: 93.51%
|
| 32 |
+
- ID test recall: 91.53%
|
| 33 |
+
- ID test F1: 92.51%
|
| 34 |
+
- ROC-AUC: 99.09%
|
| 35 |
+
- PR-AUC: 97.42%
|
| 36 |
+
- Policy-positive recall: 94.57%
|
| 37 |
+
- Hard-negative guard false-positive rate: 3.95%
|
| 38 |
+
|
| 39 |
+
These metrics come from the local evaluation set recorded in `metadata.json`; they are not a public benchmark.
|
| 40 |
+
|
| 41 |
+
## Intended Use
|
| 42 |
+
|
| 43 |
+
This model is intended for project-specific binary image triage. It may produce false positives on visually similar brown food, stains, mud, and other hard negatives, and false negatives on small, occluded, color-shifted, or unusual positive cases.
|
| 44 |
+
|
| 45 |
+
## Runtime
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
pip install -r requirements.txt
|
| 49 |
+
python inference.py path/to/image.jpg
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## Deployment Target
|
| 53 |
+
|
| 54 |
+
- Target: `lattepanda_n4120`
|
| 55 |
+
- CPU: `Intel Celeron N4120`
|
| 56 |
+
- Model parameters: `4010110`
|
| 57 |
+
- ONNX size MB: `15.341004371643066`
|
config.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"tf_efficientnet_b0.ns_jft_in1k"
|
| 4 |
+
],
|
| 5 |
+
"model_type": "image-classification",
|
| 6 |
+
"library_name": "onnx",
|
| 7 |
+
"task": "image-classification",
|
| 8 |
+
"labels": [
|
| 9 |
+
"shit",
|
| 10 |
+
"not_shit"
|
| 11 |
+
],
|
| 12 |
+
"positive_label": "shit",
|
| 13 |
+
"positive_threshold": 0.149,
|
| 14 |
+
"preprocessor": {
|
| 15 |
+
"input_size": 256,
|
| 16 |
+
"mean": [
|
| 17 |
+
0.485,
|
| 18 |
+
0.456,
|
| 19 |
+
0.406
|
| 20 |
+
],
|
| 21 |
+
"std": [
|
| 22 |
+
0.229,
|
| 23 |
+
0.224,
|
| 24 |
+
0.225
|
| 25 |
+
],
|
| 26 |
+
"resize": "shortest_edge_then_center_crop"
|
| 27 |
+
}
|
| 28 |
+
}
|
inference.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import onnxruntime as ort
|
| 9 |
+
from PIL import Image
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
MODEL_DIR = Path(__file__).resolve().parent
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _resize_center_crop(image: Image.Image, size: int) -> Image.Image:
|
| 16 |
+
resize_short_edge = int(size * 1.14)
|
| 17 |
+
width, height = image.size
|
| 18 |
+
scale = resize_short_edge / min(width, height)
|
| 19 |
+
resized = image.resize((round(width * scale), round(height * scale)))
|
| 20 |
+
left = (resized.width - size) // 2
|
| 21 |
+
top = (resized.height - size) // 2
|
| 22 |
+
return resized.crop((left, top, left + size, top + size))
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _preprocess(image: Image.Image, metadata: dict) -> np.ndarray:
|
| 26 |
+
input_size = int(metadata["input_size"])
|
| 27 |
+
image = _resize_center_crop(image.convert("RGB"), input_size)
|
| 28 |
+
array = np.asarray(image, dtype=np.float32) / 255.0
|
| 29 |
+
array = np.transpose(array, (2, 0, 1))
|
| 30 |
+
mean = np.asarray(metadata["mean"], dtype=np.float32)[:, None, None]
|
| 31 |
+
std = np.asarray(metadata["std"], dtype=np.float32)[:, None, None]
|
| 32 |
+
return ((array - mean) / std)[None, ...]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def predict(image_path: str | Path) -> dict:
|
| 36 |
+
metadata = json.loads((MODEL_DIR / "metadata.json").read_text(encoding="utf-8"))
|
| 37 |
+
session = ort.InferenceSession(
|
| 38 |
+
str(MODEL_DIR / "shit_detector.onnx"),
|
| 39 |
+
providers=["CPUExecutionProvider"],
|
| 40 |
+
)
|
| 41 |
+
batch = _preprocess(Image.open(image_path), metadata)
|
| 42 |
+
logits = session.run(None, {session.get_inputs()[0].name: batch})[0]
|
| 43 |
+
logits = logits * float(metadata.get("logit_scale", 1.0))
|
| 44 |
+
shifted = logits - logits.max(axis=-1, keepdims=True)
|
| 45 |
+
probs = np.exp(shifted) / np.exp(shifted).sum(axis=-1, keepdims=True)
|
| 46 |
+
shit_probability = float(probs[0, 0])
|
| 47 |
+
confidence = float(probs.max(axis=-1)[0])
|
| 48 |
+
threshold = float(metadata.get("shit_threshold", 0.5))
|
| 49 |
+
label = "shit" if shit_probability >= threshold else "not_shit"
|
| 50 |
+
return {
|
| 51 |
+
"status": label,
|
| 52 |
+
"label": label,
|
| 53 |
+
"is_shit": label == "shit",
|
| 54 |
+
"confidence": confidence,
|
| 55 |
+
"shit_probability": shit_probability,
|
| 56 |
+
"threshold": threshold,
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
if __name__ == "__main__":
|
| 61 |
+
if len(sys.argv) != 2:
|
| 62 |
+
raise SystemExit("Usage: python inference.py path/to/image")
|
| 63 |
+
print(json.dumps(predict(sys.argv[1]), indent=2))
|
metadata.json
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "tf_efficientnet_b0.ns_jft_in1k",
|
| 3 |
+
"input_size": 256,
|
| 4 |
+
"mean": [
|
| 5 |
+
0.485,
|
| 6 |
+
0.456,
|
| 7 |
+
0.406
|
| 8 |
+
],
|
| 9 |
+
"std": [
|
| 10 |
+
0.229,
|
| 11 |
+
0.224,
|
| 12 |
+
0.225
|
| 13 |
+
],
|
| 14 |
+
"class_names": [
|
| 15 |
+
"shit",
|
| 16 |
+
"not_shit"
|
| 17 |
+
],
|
| 18 |
+
"model_params": 4010110,
|
| 19 |
+
"deployment": {
|
| 20 |
+
"target_name": "lattepanda_n4120",
|
| 21 |
+
"cpu": "Intel Celeron N4120",
|
| 22 |
+
"cpu_threads": 4,
|
| 23 |
+
"gpu": "Intel UHD Graphics 600",
|
| 24 |
+
"memory_gib": 3.66,
|
| 25 |
+
"max_model_params": 6000000,
|
| 26 |
+
"max_onnx_size_mb": 32,
|
| 27 |
+
"max_cpu_latency_ms_p95": 1500,
|
| 28 |
+
"max_process_rss_mb": 1024
|
| 29 |
+
},
|
| 30 |
+
"shit_threshold": 0.149,
|
| 31 |
+
"f1_shit_threshold": 0.149,
|
| 32 |
+
"logit_scale": 1.0,
|
| 33 |
+
"threshold_policy": "shit_threshold is selected on validation F1.",
|
| 34 |
+
"quality_targets": {
|
| 35 |
+
"precision": 0.99,
|
| 36 |
+
"recall": 0.8,
|
| 37 |
+
"f1": 0.9,
|
| 38 |
+
"roc_auc": 0.95,
|
| 39 |
+
"pr_auc": 0.95
|
| 40 |
+
},
|
| 41 |
+
"delete_allowed_negative_fragments": [],
|
| 42 |
+
"last_eval": {
|
| 43 |
+
"val_f1_threshold": {
|
| 44 |
+
"precision": 0.9727272727272728,
|
| 45 |
+
"recall": 0.8916666666666667,
|
| 46 |
+
"f1": 0.9304347826086957,
|
| 47 |
+
"false_positive_rate": 0.004373177842565598,
|
| 48 |
+
"tp": 107,
|
| 49 |
+
"shit_tp": 107,
|
| 50 |
+
"delete_allowed_tp": 0,
|
| 51 |
+
"fp": 3,
|
| 52 |
+
"fn": 13,
|
| 53 |
+
"tn": 683,
|
| 54 |
+
"delete_allowed_count": 0,
|
| 55 |
+
"delete_allowed_predicted_shit_count": 0
|
| 56 |
+
},
|
| 57 |
+
"id_test_at_val_threshold": {
|
| 58 |
+
"precision": 0.9351351351351351,
|
| 59 |
+
"recall": 0.9153439153439153,
|
| 60 |
+
"f1": 0.9251336898395722,
|
| 61 |
+
"false_positive_rate": 0.011964107676969093,
|
| 62 |
+
"tp": 173,
|
| 63 |
+
"shit_tp": 173,
|
| 64 |
+
"delete_allowed_tp": 0,
|
| 65 |
+
"fp": 12,
|
| 66 |
+
"fn": 16,
|
| 67 |
+
"tn": 991,
|
| 68 |
+
"delete_allowed_count": 0,
|
| 69 |
+
"delete_allowed_predicted_shit_count": 0
|
| 70 |
+
},
|
| 71 |
+
"id_test_oracle_f1": {
|
| 72 |
+
"precision": 0.9558011049723757,
|
| 73 |
+
"recall": 0.9153439153439153,
|
| 74 |
+
"f1": 0.9351351351351351,
|
| 75 |
+
"false_positive_rate": 0.007976071784646061,
|
| 76 |
+
"tp": 173,
|
| 77 |
+
"shit_tp": 173,
|
| 78 |
+
"delete_allowed_tp": 0,
|
| 79 |
+
"fp": 8,
|
| 80 |
+
"fn": 16,
|
| 81 |
+
"tn": 995,
|
| 82 |
+
"delete_allowed_count": 0,
|
| 83 |
+
"delete_allowed_predicted_shit_count": 0
|
| 84 |
+
},
|
| 85 |
+
"roc_auc": 0.9909029525181069,
|
| 86 |
+
"pr_auc": 0.9741673091947349,
|
| 87 |
+
"hard_negative_guard": {
|
| 88 |
+
"false_shit_rate": 0.03954802259887006,
|
| 89 |
+
"false_shit_count": 14,
|
| 90 |
+
"not_shit_count": 340,
|
| 91 |
+
"total": 354
|
| 92 |
+
},
|
| 93 |
+
"policy_positive": {
|
| 94 |
+
"recall": 0.9456521739130435,
|
| 95 |
+
"not_shit_miss_count": 20,
|
| 96 |
+
"total": 368,
|
| 97 |
+
"by_group": {
|
| 98 |
+
"brown_water_floating": {
|
| 99 |
+
"total": 28,
|
| 100 |
+
"predicted_shit_count": 27,
|
| 101 |
+
"recall": 0.9642857142857143
|
| 102 |
+
},
|
| 103 |
+
"clogged_toilet": {
|
| 104 |
+
"total": 19,
|
| 105 |
+
"predicted_shit_count": 17,
|
| 106 |
+
"recall": 0.8947368421052632
|
| 107 |
+
},
|
| 108 |
+
"color_shifted": {
|
| 109 |
+
"total": 20,
|
| 110 |
+
"predicted_shit_count": 19,
|
| 111 |
+
"recall": 0.95
|
| 112 |
+
},
|
| 113 |
+
"manual_new_shits": {
|
| 114 |
+
"total": 6,
|
| 115 |
+
"predicted_shit_count": 6,
|
| 116 |
+
"recall": 1.0
|
| 117 |
+
},
|
| 118 |
+
"other_shit": {
|
| 119 |
+
"total": 29,
|
| 120 |
+
"predicted_shit_count": 26,
|
| 121 |
+
"recall": 0.896551724137931
|
| 122 |
+
},
|
| 123 |
+
"subagent_fp_policy_positive": {
|
| 124 |
+
"total": 2,
|
| 125 |
+
"predicted_shit_count": 2,
|
| 126 |
+
"recall": 1.0
|
| 127 |
+
},
|
| 128 |
+
"toilet_feces_general": {
|
| 129 |
+
"total": 171,
|
| 130 |
+
"predicted_shit_count": 164,
|
| 131 |
+
"recall": 0.9590643274853801
|
| 132 |
+
},
|
| 133 |
+
"toilet_soiling": {
|
| 134 |
+
"total": 50,
|
| 135 |
+
"predicted_shit_count": 44,
|
| 136 |
+
"recall": 0.88
|
| 137 |
+
},
|
| 138 |
+
"toilet_urine_soiling": {
|
| 139 |
+
"total": 43,
|
| 140 |
+
"predicted_shit_count": 43,
|
| 141 |
+
"recall": 1.0
|
| 142 |
+
}
|
| 143 |
+
},
|
| 144 |
+
"min_group_recall": 0.88
|
| 145 |
+
}
|
| 146 |
+
},
|
| 147 |
+
"onnx": {
|
| 148 |
+
"path": "models\\shit_detector.onnx",
|
| 149 |
+
"size_mb": 15.341004371643066,
|
| 150 |
+
"opset_version": 17,
|
| 151 |
+
"providers": [
|
| 152 |
+
"CPUExecutionProvider"
|
| 153 |
+
]
|
| 154 |
+
},
|
| 155 |
+
"checkpoint": {
|
| 156 |
+
"path": "checkpoints\\best.pt",
|
| 157 |
+
"selection_metric": "shit_f1",
|
| 158 |
+
"selection_score": 0.9304347826086957
|
| 159 |
+
}
|
| 160 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
numpy>=1.26
|
| 2 |
+
onnxruntime>=1.26
|
| 3 |
+
pillow>=10.0
|
shit_detector.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ffed6c44cbeb8d973f8c91daa04dc70da5fd638eb27fc4add50f425791d9056a
|
| 3 |
+
size 16086209
|