bhavibhatt commited on
Commit
0119666
·
verified ·
1 Parent(s): fda0c8d

Upload 2 files

Browse files
Files changed (2) hide show
  1. weldvision/__init__.py +3 -0
  2. weldvision/ensemble.py +249 -0
weldvision/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .ensemble import WeldVision
2
+
3
+ __all__ = ["WeldVision"]
weldvision/ensemble.py ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ from pathlib import Path
4
+ from ultralytics import YOLO
5
+ from huggingface_hub import hf_hub_download
6
+
7
+
8
+ class WeldVision:
9
+ """
10
+ Local four-model WeldVision ensemble.
11
+
12
+ Usage:
13
+ model = WeldVision.from_pretrained(
14
+ "bhavibhatt/weldvision-ensemble"
15
+ )
16
+ result = model.predict("weld.jpg")
17
+ """
18
+
19
+ CLASS_NAMES = {
20
+ 0: "Bad Welding",
21
+ 1: "Crack",
22
+ 2: "Excess Reinforcement",
23
+ 3: "Good Welding",
24
+ 4: "Porosity",
25
+ 5: "Spatters",
26
+ }
27
+
28
+ PENALTIES = {
29
+ "Crack": 40,
30
+ "Porosity": 15,
31
+ "Spatters": 5,
32
+ "Excess Reinforcement": 20,
33
+ "Bad Welding": 50,
34
+ "Good Welding": 0,
35
+ }
36
+
37
+ def __init__(self, model_paths, conf=0.25, ensemble_iou=0.50, imgsz=640):
38
+ self.conf = conf
39
+ self.ensemble_iou = ensemble_iou
40
+ self.imgsz = imgsz
41
+
42
+ self.base_1 = YOLO(str(model_paths["best.pt"]))
43
+ self.base_2 = YOLO(str(model_paths["best_v0.pt"]))
44
+ self.crack = YOLO(str(model_paths["crack_specialist.pt"]))
45
+ self.spatters = YOLO(str(model_paths["spatters_specialist.pt"]))
46
+
47
+ @classmethod
48
+ def from_pretrained(
49
+ cls,
50
+ repo_id,
51
+ revision=None,
52
+ cache_dir=None,
53
+ conf=0.25,
54
+ ensemble_iou=0.50,
55
+ imgsz=640,
56
+ ):
57
+ """
58
+ Download the four weights from Hugging Face Hub and load them locally.
59
+ """
60
+ names = [
61
+ "best.pt",
62
+ "best_v0.pt",
63
+ "crack_specialist.pt",
64
+ "spatters_specialist.pt",
65
+ ]
66
+
67
+ paths = {}
68
+ for name in names:
69
+ paths[name] = hf_hub_download(
70
+ repo_id=repo_id,
71
+ filename=f"weights/{name}",
72
+ revision=revision,
73
+ cache_dir=cache_dir,
74
+ )
75
+
76
+ return cls(
77
+ paths,
78
+ conf=conf,
79
+ ensemble_iou=ensemble_iou,
80
+ imgsz=imgsz,
81
+ )
82
+
83
+ @staticmethod
84
+ def _load_image(image):
85
+ if isinstance(image, (str, Path)):
86
+ image = cv2.imread(str(image))
87
+ if image is None:
88
+ raise ValueError(f"Could not read image: {image}")
89
+ return image
90
+
91
+ if isinstance(image, np.ndarray):
92
+ if image.ndim != 3 or image.shape[2] != 3:
93
+ raise ValueError("Image must have shape H x W x 3")
94
+ return image
95
+
96
+ raise TypeError("image must be a file path or HxWx3 numpy array")
97
+
98
+ @staticmethod
99
+ def _mask_iou(a, b):
100
+ a = a.astype(bool)
101
+ b = b.astype(bool)
102
+ inter = np.logical_and(a, b).sum()
103
+ union = np.logical_or(a, b).sum()
104
+ return float(inter / union) if union else 0.0
105
+
106
+ def _extract(self, result, source):
107
+ if result.boxes is None or result.masks is None:
108
+ return []
109
+
110
+ boxes = result.boxes.data.cpu().numpy()
111
+ masks = result.masks.data.cpu().numpy()
112
+ out = []
113
+
114
+ for box, mask in zip(boxes, masks):
115
+ x1, y1, x2, y2, conf, cls_id = box
116
+ cls_id = int(cls_id)
117
+
118
+ if source == "crack_specialist":
119
+ class_name = "Crack"
120
+ elif source == "spatters_specialist":
121
+ class_name = "Spatters"
122
+ else:
123
+ class_name = self.CLASS_NAMES.get(cls_id, str(cls_id))
124
+
125
+ out.append({
126
+ "box": np.array([x1, y1, x2, y2], dtype=np.float32),
127
+ "conf": float(conf),
128
+ "class_name": class_name,
129
+ "mask": mask.astype(np.float32),
130
+ "source": source,
131
+ })
132
+
133
+ return out
134
+
135
+ def _run(self, model, image, source):
136
+ result = model.predict(
137
+ image,
138
+ conf=self.conf,
139
+ imgsz=self.imgsz,
140
+ verbose=False,
141
+ )[0]
142
+ return self._extract(result, source)
143
+
144
+ def _merge(self, predictions):
145
+ predictions = sorted(
146
+ predictions,
147
+ key=lambda p: p["conf"],
148
+ reverse=True,
149
+ )
150
+
151
+ selected = []
152
+ for candidate in predictions:
153
+ duplicate = False
154
+
155
+ for existing in selected:
156
+ if candidate["class_name"] != existing["class_name"]:
157
+ continue
158
+
159
+ if self._mask_iou(
160
+ candidate["mask"],
161
+ existing["mask"],
162
+ ) >= self.ensemble_iou:
163
+ duplicate = True
164
+ break
165
+
166
+ if not duplicate:
167
+ selected.append(candidate)
168
+
169
+ return selected
170
+
171
+ def _severity(self, name):
172
+ penalty = self.PENALTIES.get(name, 0)
173
+ if penalty >= 30:
174
+ return "HIGH"
175
+ if penalty >= 15:
176
+ return "MEDIUM"
177
+ if penalty > 0:
178
+ return "LOW"
179
+ return "NONE"
180
+
181
+ def predict(self, image):
182
+ """
183
+ Run the four-model ensemble.
184
+
185
+ Returns a JSON-serializable dictionary.
186
+ """
187
+ image_bgr = self._load_image(image)
188
+ h, w = image_bgr.shape[:2]
189
+
190
+ p1 = self._run(self.base_1, image_bgr, "best.pt")
191
+ p2 = self._run(self.base_2, image_bgr, "best_v0.pt")
192
+ pc = self._run(self.crack, image_bgr, "crack_specialist")
193
+ ps = self._run(self.spatters, image_bgr, "spatters_specialist")
194
+
195
+ merged = self._merge(p1 + p2 + pc + ps)
196
+
197
+ detections = []
198
+ score = 100
199
+ highest = "NONE"
200
+ rank = {"NONE": 0, "LOW": 1, "MEDIUM": 2, "HIGH": 3}
201
+
202
+ for p in merged:
203
+ name = p["class_name"]
204
+ if name == "Good Welding":
205
+ continue
206
+
207
+ severity = self._severity(name)
208
+ score -= self.PENALTIES.get(name, 0)
209
+ if rank[severity] > rank[highest]:
210
+ highest = severity
211
+
212
+ x1, y1, x2, y2 = p["box"]
213
+ detections.append({
214
+ "class": name,
215
+ "confidence": round(float(p["conf"]), 4),
216
+ "severity": severity,
217
+ "box": [
218
+ round(float(max(0, min(w, x1))), 2),
219
+ round(float(max(0, min(h, y1))), 2),
220
+ round(float(max(0, min(w, x2))), 2),
221
+ round(float(max(0, min(h, y2))), 2),
222
+ ],
223
+ "source": p["source"],
224
+ })
225
+
226
+ score = max(0, score)
227
+
228
+ if score < 70 or highest == "HIGH":
229
+ decision = "FAIL"
230
+ elif score < 85 or highest == "MEDIUM":
231
+ decision = "REVIEW"
232
+ else:
233
+ decision = "PASS"
234
+
235
+ return {
236
+ "model": "WeldVision-Ensemble",
237
+ "version": "1.0",
238
+ "decision": decision,
239
+ "score": score,
240
+ "highest_severity": highest,
241
+ "model_counts": {
242
+ "best.pt": len(p1),
243
+ "best_v0.pt": len(p2),
244
+ "crack_specialist.pt": len(pc),
245
+ "spatters_specialist.pt": len(ps),
246
+ "ensemble": len(merged),
247
+ },
248
+ "detections": detections,
249
+ }