bhavibhatt's picture
Upload 2 files
0119666 verified
Raw
History Blame Contribute Delete
7.07 kB
import cv2
import numpy as np
from pathlib import Path
from ultralytics import YOLO
from huggingface_hub import hf_hub_download
class WeldVision:
"""
Local four-model WeldVision ensemble.
Usage:
model = WeldVision.from_pretrained(
"bhavibhatt/weldvision-ensemble"
)
result = model.predict("weld.jpg")
"""
CLASS_NAMES = {
0: "Bad Welding",
1: "Crack",
2: "Excess Reinforcement",
3: "Good Welding",
4: "Porosity",
5: "Spatters",
}
PENALTIES = {
"Crack": 40,
"Porosity": 15,
"Spatters": 5,
"Excess Reinforcement": 20,
"Bad Welding": 50,
"Good Welding": 0,
}
def __init__(self, model_paths, conf=0.25, ensemble_iou=0.50, imgsz=640):
self.conf = conf
self.ensemble_iou = ensemble_iou
self.imgsz = imgsz
self.base_1 = YOLO(str(model_paths["best.pt"]))
self.base_2 = YOLO(str(model_paths["best_v0.pt"]))
self.crack = YOLO(str(model_paths["crack_specialist.pt"]))
self.spatters = YOLO(str(model_paths["spatters_specialist.pt"]))
@classmethod
def from_pretrained(
cls,
repo_id,
revision=None,
cache_dir=None,
conf=0.25,
ensemble_iou=0.50,
imgsz=640,
):
"""
Download the four weights from Hugging Face Hub and load them locally.
"""
names = [
"best.pt",
"best_v0.pt",
"crack_specialist.pt",
"spatters_specialist.pt",
]
paths = {}
for name in names:
paths[name] = hf_hub_download(
repo_id=repo_id,
filename=f"weights/{name}",
revision=revision,
cache_dir=cache_dir,
)
return cls(
paths,
conf=conf,
ensemble_iou=ensemble_iou,
imgsz=imgsz,
)
@staticmethod
def _load_image(image):
if isinstance(image, (str, Path)):
image = cv2.imread(str(image))
if image is None:
raise ValueError(f"Could not read image: {image}")
return image
if isinstance(image, np.ndarray):
if image.ndim != 3 or image.shape[2] != 3:
raise ValueError("Image must have shape H x W x 3")
return image
raise TypeError("image must be a file path or HxWx3 numpy array")
@staticmethod
def _mask_iou(a, b):
a = a.astype(bool)
b = b.astype(bool)
inter = np.logical_and(a, b).sum()
union = np.logical_or(a, b).sum()
return float(inter / union) if union else 0.0
def _extract(self, result, source):
if result.boxes is None or result.masks is None:
return []
boxes = result.boxes.data.cpu().numpy()
masks = result.masks.data.cpu().numpy()
out = []
for box, mask in zip(boxes, masks):
x1, y1, x2, y2, conf, cls_id = box
cls_id = int(cls_id)
if source == "crack_specialist":
class_name = "Crack"
elif source == "spatters_specialist":
class_name = "Spatters"
else:
class_name = self.CLASS_NAMES.get(cls_id, str(cls_id))
out.append({
"box": np.array([x1, y1, x2, y2], dtype=np.float32),
"conf": float(conf),
"class_name": class_name,
"mask": mask.astype(np.float32),
"source": source,
})
return out
def _run(self, model, image, source):
result = model.predict(
image,
conf=self.conf,
imgsz=self.imgsz,
verbose=False,
)[0]
return self._extract(result, source)
def _merge(self, predictions):
predictions = sorted(
predictions,
key=lambda p: p["conf"],
reverse=True,
)
selected = []
for candidate in predictions:
duplicate = False
for existing in selected:
if candidate["class_name"] != existing["class_name"]:
continue
if self._mask_iou(
candidate["mask"],
existing["mask"],
) >= self.ensemble_iou:
duplicate = True
break
if not duplicate:
selected.append(candidate)
return selected
def _severity(self, name):
penalty = self.PENALTIES.get(name, 0)
if penalty >= 30:
return "HIGH"
if penalty >= 15:
return "MEDIUM"
if penalty > 0:
return "LOW"
return "NONE"
def predict(self, image):
"""
Run the four-model ensemble.
Returns a JSON-serializable dictionary.
"""
image_bgr = self._load_image(image)
h, w = image_bgr.shape[:2]
p1 = self._run(self.base_1, image_bgr, "best.pt")
p2 = self._run(self.base_2, image_bgr, "best_v0.pt")
pc = self._run(self.crack, image_bgr, "crack_specialist")
ps = self._run(self.spatters, image_bgr, "spatters_specialist")
merged = self._merge(p1 + p2 + pc + ps)
detections = []
score = 100
highest = "NONE"
rank = {"NONE": 0, "LOW": 1, "MEDIUM": 2, "HIGH": 3}
for p in merged:
name = p["class_name"]
if name == "Good Welding":
continue
severity = self._severity(name)
score -= self.PENALTIES.get(name, 0)
if rank[severity] > rank[highest]:
highest = severity
x1, y1, x2, y2 = p["box"]
detections.append({
"class": name,
"confidence": round(float(p["conf"]), 4),
"severity": severity,
"box": [
round(float(max(0, min(w, x1))), 2),
round(float(max(0, min(h, y1))), 2),
round(float(max(0, min(w, x2))), 2),
round(float(max(0, min(h, y2))), 2),
],
"source": p["source"],
})
score = max(0, score)
if score < 70 or highest == "HIGH":
decision = "FAIL"
elif score < 85 or highest == "MEDIUM":
decision = "REVIEW"
else:
decision = "PASS"
return {
"model": "WeldVision-Ensemble",
"version": "1.0",
"decision": decision,
"score": score,
"highest_severity": highest,
"model_counts": {
"best.pt": len(p1),
"best_v0.pt": len(p2),
"crack_specialist.pt": len(pc),
"spatters_specialist.pt": len(ps),
"ensemble": len(merged),
},
"detections": detections,
}