cstria0106 commited on
Commit
c2d0005
·
verified ·
1 Parent(s): a88d190

Upload folder using huggingface_hub

Browse files
Files changed (6) hide show
  1. README.md +57 -3
  2. config.json +28 -0
  3. inference.py +63 -0
  4. metadata.json +160 -0
  5. requirements.txt +3 -0
  6. 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