Upload 5 files
Browse files- README.md +19 -0
- config.json +1 -0
- ensemble.py +43 -0
- handler.py +13 -0
- requirements.txt +6 -0
README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
tags:
|
| 4 |
+
- computer-vision
|
| 5 |
+
- image-segmentation
|
| 6 |
+
- welding
|
| 7 |
+
- weld-inspection
|
| 8 |
+
- yolo
|
| 9 |
+
pipeline_tag: image-segmentation
|
| 10 |
+
---
|
| 11 |
+
# WeldVision Ensemble
|
| 12 |
+
|
| 13 |
+
Four-model YOLO segmentation ensemble for prototype weld-surface defect inspection.
|
| 14 |
+
|
| 15 |
+
Models: `best.pt`, `best_v0.pt`, `crack_specialist.pt`, `spatters_specialist.pt`.
|
| 16 |
+
|
| 17 |
+
The custom `handler.py` loads all four weights and performs class-aware mask-IoU merging.
|
| 18 |
+
|
| 19 |
+
**Prototype only:** the score and PASS/REVIEW/FAIL rules are not welding-code acceptance criteria and do not replace qualified human inspection.
|
config.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"name":"WeldVision-Ensemble","version":"1.0","models":["best.pt","best_v0.pt","crack_specialist.pt","spatters_specialist.pt"],"confidence":0.25,"ensemble_mask_iou":0.50,"image_size":640}
|
ensemble.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
from ultralytics import YOLO
|
| 4 |
+
|
| 5 |
+
class WeldVisionEnsemble:
|
| 6 |
+
CLASS_NAMES={0:'Bad Welding',1:'Crack',2:'Excess Reinforcement',3:'Good Welding',4:'Porosity',5:'Spatters'}
|
| 7 |
+
PENALTIES={'Crack':40,'Porosity':15,'Spatters':5,'Excess Reinforcement':20,'Bad Welding':50,'Good Welding':0}
|
| 8 |
+
def __init__(self, model_dir, conf=.25, ensemble_iou=.50, imgsz=640):
|
| 9 |
+
self.conf=conf; self.ensemble_iou=ensemble_iou; self.imgsz=imgsz
|
| 10 |
+
self.base1=YOLO(f'{model_dir}/best.pt'); self.base2=YOLO(f'{model_dir}/best_v0.pt')
|
| 11 |
+
self.crack=YOLO(f'{model_dir}/crack_specialist.pt'); self.spatters=YOLO(f'{model_dir}/spatters_specialist.pt')
|
| 12 |
+
@staticmethod
|
| 13 |
+
def mask_iou(a,b):
|
| 14 |
+
a=a.astype(bool); b=b.astype(bool); u=np.logical_or(a,b).sum(); return float(np.logical_and(a,b).sum()/u) if u else 0.0
|
| 15 |
+
def extract(self,r,source):
|
| 16 |
+
if r.boxes is None or r.masks is None: return []
|
| 17 |
+
out=[]
|
| 18 |
+
for box,mask in zip(r.boxes.data.cpu().numpy(),r.masks.data.cpu().numpy()):
|
| 19 |
+
x1,y1,x2,y2,conf,cls=box; cls=int(cls)
|
| 20 |
+
name='Crack' if source=='crack' else 'Spatters' if source=='spatters' else self.CLASS_NAMES.get(cls,str(cls))
|
| 21 |
+
out.append({'box':np.array([x1,y1,x2,y2],dtype=np.float32),'conf':float(conf),'class_name':name,'mask':mask.astype(np.float32),'source':source})
|
| 22 |
+
return out
|
| 23 |
+
def run(self,model,img,source):
|
| 24 |
+
return self.extract(model.predict(img,conf=self.conf,imgsz=self.imgsz,verbose=False)[0],source)
|
| 25 |
+
def merge(self,preds):
|
| 26 |
+
selected=[]
|
| 27 |
+
for p in sorted(preds,key=lambda x:x['conf'],reverse=True):
|
| 28 |
+
if not any(p['class_name']==q['class_name'] and self.mask_iou(p['mask'],q['mask'])>=self.ensemble_iou for q in selected): selected.append(p)
|
| 29 |
+
return selected
|
| 30 |
+
def predict(self,image):
|
| 31 |
+
image=np.asarray(image)
|
| 32 |
+
if image.ndim!=3 or image.shape[2]!=3: raise ValueError('image must be HxWx3 RGB')
|
| 33 |
+
bgr=cv2.cvtColor(image,cv2.COLOR_RGB2BGR)
|
| 34 |
+
p1=self.run(self.base1,bgr,'base_1'); p2=self.run(self.base2,bgr,'base_2'); pc=self.run(self.crack,bgr,'crack'); ps=self.run(self.spatters,bgr,'spatters')
|
| 35 |
+
merged=self.merge(p1+p2+pc+ps); detections=[]; h,w=bgr.shape[:2]
|
| 36 |
+
for p in merged:
|
| 37 |
+
c=p['class_name']
|
| 38 |
+
if c=='Good Welding': continue
|
| 39 |
+
sev='HIGH' if self.PENALTIES.get(c,0)>=30 else 'MEDIUM' if self.PENALTIES.get(c,0)>=15 else 'LOW' if self.PENALTIES.get(c,0)>0 else 'NONE'
|
| 40 |
+
x1,y1,x2,y2=p['box']; detections.append({'class':c,'confidence':round(p['conf'],4),'severity':sev,'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']})
|
| 41 |
+
score=max(0,100-sum(self.PENALTIES.get(d['class'],0) for d in detections)); high=any(d['severity']=='HIGH' for d in detections); med=any(d['severity']=='MEDIUM' for d in detections)
|
| 42 |
+
decision='FAIL' if score<70 or high else 'REVIEW' if score<85 or med else 'PASS'
|
| 43 |
+
return {'model':'WeldVision-Ensemble','version':'1.0','decision':decision,'score':score,'detections':detections,'model_counts':{'best.pt':len(p1),'best_v0.pt':len(p2),'crack_specialist.pt':len(pc),'spatters_specialist.pt':len(ps),'ensemble':len(merged)},'recommendation':'Significant visible defect detected. Human inspection required.' if decision=='FAIL' else 'Moderate defect or quality concern detected. Human review required.' if decision=='REVIEW' else 'No significant visible defect detected.'}
|
handler.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io, os
|
| 2 |
+
import numpy as np
|
| 3 |
+
from PIL import Image
|
| 4 |
+
from ensemble import WeldVisionEnsemble
|
| 5 |
+
MODEL=WeldVisionEnsemble(os.path.join(os.path.dirname(__file__),'models'))
|
| 6 |
+
def _image(data):
|
| 7 |
+
if isinstance(data,dict): data=data.get('image',data.get('inputs'))
|
| 8 |
+
if isinstance(data,str):
|
| 9 |
+
import base64; data=base64.b64decode(data)
|
| 10 |
+
if isinstance(data,(bytes,bytearray)): return np.array(Image.open(io.BytesIO(data)).convert('RGB'))
|
| 11 |
+
if isinstance(data,np.ndarray): return data
|
| 12 |
+
raise ValueError('Expected image bytes, base64, or numpy array')
|
| 13 |
+
def handler(data): return MODEL.predict(_image(data))
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ultralytics==8.4.126
|
| 2 |
+
opencv-python-headless
|
| 3 |
+
numpy
|
| 4 |
+
Pillow
|
| 5 |
+
torch
|
| 6 |
+
torchvision
|