Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +27 -0
- README.md +40 -5
- app.py +217 -0
- detectors/__init__.py +1 -0
- detectors/__pycache__/__init__.cpython-312.pyc +0 -0
- detectors/__pycache__/cloth_detection.cpython-312.pyc +0 -0
- detectors/__pycache__/glove_type_detector.cpython-312.pyc +0 -0
- detectors/__pycache__/nitrile_detection.cpython-312.pyc +0 -0
- detectors/__pycache__/rubber_detection.cpython-312.pyc +0 -0
- detectors/__pycache__/utils.cpython-312.pyc +0 -0
- detectors/__pycache__/vinyl_detection.cpython-312.pyc +0 -0
- detectors/cloth_detection.py +116 -0
- detectors/glove_type_detector.py +83 -0
- detectors/nitrile_detection.py +112 -0
- detectors/rubber_detection.py +119 -0
- detectors/utils.py +170 -0
- detectors/vinyl_detection.py +111 -0
- examples/DC_1.jpeg +0 -0
- examples/DC_2.jpeg +0 -0
- examples/Fold_1.png +3 -0
- examples/Fold_2.png +3 -0
- examples/Fold_3.png +3 -0
- examples/H(2).jpeg +0 -0
- examples/H(3).jpeg +0 -0
- examples/H(5).jpeg +0 -0
- examples/H(6).jpeg +0 -0
- examples/H(7).jpeg +0 -0
- examples/Holes_1.png +3 -0
- examples/Holes_2.png +3 -0
- examples/Holes_3.png +3 -0
- examples/Holes_4.png +3 -0
- examples/MF(2).jpeg +0 -0
- examples/MF(3).jpeg +0 -0
- examples/MF(4).jpeg +0 -0
- examples/MF(5).jpeg +0 -0
- examples/MF(6).jpeg +0 -0
- examples/MF(7).jpeg +0 -0
- examples/Normal_1.jpg +0 -0
- examples/Normal_2.jpg +0 -0
- examples/Original_1.jpeg +0 -0
- examples/Original_2.jpeg +0 -0
- examples/Original_3.jpeg +0 -0
- examples/PH_1.jpeg +0 -0
- examples/PH_2.jpeg +0 -0
- examples/PH_3.jpeg +0 -0
- examples/S(1).jpeg +0 -0
- examples/S(10).jpeg +0 -0
- examples/S(7).jpeg +0 -0
- examples/S(8).jpeg +0 -0
- examples/S(9).jpeg +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,30 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
examples/Fold_1.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
examples/Fold_2.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
examples/Fold_3.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
examples/Holes_1.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
examples/Holes_2.png filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
examples/Holes_3.png filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
examples/Holes_4.png filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
examples/Spots_1.png filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
examples/Spots_2.png filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
examples/Spots_3.png filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
examples/Spots_4.png filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
examples/Spots_5.png filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
examples/vinyl_cuff_1.jpg filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
examples/vinyl_cuff_2.jpg filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
examples/vinyl_cuff_3.jpg filter=lfs diff=lfs merge=lfs -text
|
| 51 |
+
examples/vinyl_cuff_4.jpg filter=lfs diff=lfs merge=lfs -text
|
| 52 |
+
examples/vinyl_cuff_5.jpg filter=lfs diff=lfs merge=lfs -text
|
| 53 |
+
examples/vinyl_foreign_object_1.jpg filter=lfs diff=lfs merge=lfs -text
|
| 54 |
+
examples/vinyl_foreign_object_2.jpg filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
examples/vinyl_foreign_object_3.jpg filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
examples/vinyl_foreign_object_4.jpg filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
examples/vinyl_foreign_object_5.jpg filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
examples/vinyl_hole_1.jpg filter=lfs diff=lfs merge=lfs -text
|
| 59 |
+
examples/vinyl_hole_2.jpg filter=lfs diff=lfs merge=lfs -text
|
| 60 |
+
examples/vinyl_hole_3.jpg filter=lfs diff=lfs merge=lfs -text
|
| 61 |
+
examples/vinyl_hole_4.jpg filter=lfs diff=lfs merge=lfs -text
|
| 62 |
+
examples/vinyl_hole_5.jpg filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,13 +1,48 @@
|
|
| 1 |
---
|
| 2 |
title: Glove Defect Detection
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Glove Defect Detection
|
| 3 |
+
emoji: 🧤
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 4.44.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# 🧤 Glove Defect Detection System
|
| 14 |
+
|
| 15 |
+
An image processing app that automatically **classifies safety glove types** and **detects surface defects** — no deep learning required, purely classical computer vision (HSV segmentation + morphological operations).
|
| 16 |
+
|
| 17 |
+
## How it works
|
| 18 |
+
|
| 19 |
+
1. **Upload** a photo of a glove (rubber, cloth, nitrile, or vinyl).
|
| 20 |
+
2. The app **classifies the glove type** by analysing dominant HSV colour features.
|
| 21 |
+
3. It then runs the **type-specific defect detector** and returns an annotated image.
|
| 22 |
+
|
| 23 |
+
## Supported glove types & defects
|
| 24 |
+
|
| 25 |
+
| Glove Type | Detected Defects |
|
| 26 |
+
|---------------|---------------------------------------------|
|
| 27 |
+
| Rubber | Holes · Discoloration · Folds |
|
| 28 |
+
| Cloth | Missing Fingers · Holes · Stains |
|
| 29 |
+
| Nitrile | Spots · Holes · Tears |
|
| 30 |
+
| Vinyl | Palm Holes · Cuff Tears · Foreign Objects |
|
| 31 |
+
|
| 32 |
+
## Technical stack
|
| 33 |
+
|
| 34 |
+
- **OpenCV** — image preprocessing, morphological operations, contour drawing
|
| 35 |
+
- **scikit-image** — connected-component region properties (area, eccentricity, etc.)
|
| 36 |
+
- **Gradio** — web interface
|
| 37 |
+
|
| 38 |
+
## Project
|
| 39 |
+
|
| 40 |
+
IPPR (Image Processing & Pattern Recognition) Assignment
|
| 41 |
+
Asia Pacific University of Technology & Innovation (APU)
|
| 42 |
+
|
| 43 |
+
| Contributor | Module |
|
| 44 |
+
|------------------------------------------|--------------------------------------------|
|
| 45 |
+
| Chiew Wen Qian (TP084377) | Rubber detector · Code & GUI integration |
|
| 46 |
+
| Farouk Elouzzani (TP075438) | Glove type classifier · Vinyl detector |
|
| 47 |
+
| Puteri Hannah binti Mohamed Daud Ibrahim (TP077710) | Nitrile detector |
|
| 48 |
+
| Wendy Koh Xin Hui (TP077721) | Cloth detector |
|
app.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Glove Defect Detection — Gradio app entry point.
|
| 3 |
+
|
| 4 |
+
Upload a glove image → automatic type classification → defect analysis.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import gradio as gr
|
| 9 |
+
|
| 10 |
+
from detectors.glove_type_detector import detect_glove_type
|
| 11 |
+
from detectors.rubber_detection import detect_rubber
|
| 12 |
+
from detectors.cloth_detection import detect_cloth
|
| 13 |
+
from detectors.nitrile_detection import detect_nitrile
|
| 14 |
+
from detectors.vinyl_detection import detect_vinyl
|
| 15 |
+
|
| 16 |
+
DETECTORS = {
|
| 17 |
+
"Rubber Glove": detect_rubber,
|
| 18 |
+
"Cloth Glove": detect_cloth,
|
| 19 |
+
"Nitrile Glove": detect_nitrile,
|
| 20 |
+
"Vinyl Glove": detect_vinyl,
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def analyse(image: np.ndarray):
|
| 25 |
+
if image is None:
|
| 26 |
+
return None, "—", "—"
|
| 27 |
+
img = image.astype(np.uint8)
|
| 28 |
+
_, glove_type = detect_glove_type(img)
|
| 29 |
+
detector = DETECTORS.get(glove_type, detect_rubber)
|
| 30 |
+
result_img, raw_status = detector(img)
|
| 31 |
+
label = "✅ Passed" if raw_status == "Passed" else "⚠️ Defective"
|
| 32 |
+
return result_img, glove_type, label
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def load_from_gallery(evt: gr.SelectData):
|
| 36 |
+
"""Pass the clicked gallery image into the input component."""
|
| 37 |
+
val = evt.value
|
| 38 |
+
# Gradio 4+ returns {"image": {"path": ...}, "caption": ...} for (path, caption) tuples
|
| 39 |
+
if isinstance(val, dict):
|
| 40 |
+
img = val.get("image", val)
|
| 41 |
+
if isinstance(img, dict):
|
| 42 |
+
return img.get("path") or img.get("url")
|
| 43 |
+
return img
|
| 44 |
+
return val
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# ── Sample image lists — (file_path, caption) ────────────────────────────────
|
| 48 |
+
# Corrected groupings per dataset naming convention:
|
| 49 |
+
# DC_* → Rubber + Discoloration
|
| 50 |
+
# Fold_* → Rubber + Fold
|
| 51 |
+
# Original_* → Rubber + Normal
|
| 52 |
+
# PH_* → Rubber + Hole (Palm)
|
| 53 |
+
# H(*) → Cloth + Hole
|
| 54 |
+
# MF(*) → Cloth + Missing Finger
|
| 55 |
+
# S(*) → Cloth + Stain
|
| 56 |
+
# Holes_* → Nitrile + Hole
|
| 57 |
+
# Normal_* → Nitrile + Normal
|
| 58 |
+
# Spots_* → Nitrile + Spot
|
| 59 |
+
# Tear_* → Nitrile + Tear
|
| 60 |
+
# vinyl_hole_* → Vinyl + Palm Hole
|
| 61 |
+
# vinyl_cuff_* → Vinyl + Cuff Tear
|
| 62 |
+
# vinyl_foreign_* → Vinyl + Foreign Object
|
| 63 |
+
|
| 64 |
+
RUBBER = [
|
| 65 |
+
("examples/Original_1.jpeg", "Normal"),
|
| 66 |
+
("examples/Original_2.jpeg", "Normal"),
|
| 67 |
+
("examples/Original_3.jpeg", "Normal"),
|
| 68 |
+
("examples/PH_1.jpeg", "Hole"),
|
| 69 |
+
("examples/PH_2.jpeg", "Hole"),
|
| 70 |
+
("examples/PH_3.jpeg", "Hole"),
|
| 71 |
+
("examples/DC_1.jpeg", "Discoloration"),
|
| 72 |
+
("examples/DC_2.jpeg", "Discoloration"),
|
| 73 |
+
("examples/Fold_1.png", "Fold"),
|
| 74 |
+
("examples/Fold_2.png", "Fold"),
|
| 75 |
+
("examples/Fold_3.png", "Fold"),
|
| 76 |
+
]
|
| 77 |
+
|
| 78 |
+
CLOTH = [
|
| 79 |
+
("examples/H(2).jpeg", "Hole"),
|
| 80 |
+
("examples/H(3).jpeg", "Hole"),
|
| 81 |
+
("examples/H(5).jpeg", "Hole"),
|
| 82 |
+
("examples/H(6).jpeg", "Hole"),
|
| 83 |
+
("examples/H(7).jpeg", "Hole"),
|
| 84 |
+
("examples/MF(2).jpeg", "Missing Finger"),
|
| 85 |
+
("examples/MF(3).jpeg", "Missing Finger"),
|
| 86 |
+
("examples/MF(4).jpeg", "Missing Finger"),
|
| 87 |
+
("examples/MF(5).jpeg", "Missing Finger"),
|
| 88 |
+
("examples/MF(6).jpeg", "Missing Finger"),
|
| 89 |
+
("examples/MF(7).jpeg", "Missing Finger"),
|
| 90 |
+
("examples/S(1).jpeg", "Stain"),
|
| 91 |
+
("examples/S(7).jpeg", "Stain"),
|
| 92 |
+
("examples/S(8).jpeg", "Stain"),
|
| 93 |
+
("examples/S(9).jpeg", "Stain"),
|
| 94 |
+
("examples/S(10).jpeg", "Stain"),
|
| 95 |
+
]
|
| 96 |
+
|
| 97 |
+
NITRILE = [
|
| 98 |
+
("examples/Normal_1.jpg", "Normal"),
|
| 99 |
+
("examples/Normal_2.jpg", "Normal"),
|
| 100 |
+
("examples/Holes_1.png", "Hole"),
|
| 101 |
+
("examples/Holes_2.png", "Hole"),
|
| 102 |
+
("examples/Holes_3.png", "Hole"),
|
| 103 |
+
("examples/Holes_4.png", "Hole"),
|
| 104 |
+
("examples/Spots_1.png", "Spot"),
|
| 105 |
+
("examples/Spots_2.png", "Spot"),
|
| 106 |
+
("examples/Spots_3.png", "Spot"),
|
| 107 |
+
("examples/Spots_4.png", "Spot"),
|
| 108 |
+
("examples/Spots_5.png", "Spot"),
|
| 109 |
+
("examples/Tear_1.jpg", "Tear"),
|
| 110 |
+
("examples/Tear_2.jpg", "Tear"),
|
| 111 |
+
("examples/Tear_3.jpg", "Tear"),
|
| 112 |
+
]
|
| 113 |
+
|
| 114 |
+
VINYL = [
|
| 115 |
+
("examples/vinyl_hole_1.jpg", "Palm Hole"),
|
| 116 |
+
("examples/vinyl_hole_2.jpg", "Palm Hole"),
|
| 117 |
+
("examples/vinyl_hole_3.jpg", "Palm Hole"),
|
| 118 |
+
("examples/vinyl_hole_4.jpg", "Palm Hole"),
|
| 119 |
+
("examples/vinyl_hole_5.jpg", "Palm Hole"),
|
| 120 |
+
("examples/vinyl_cuff_1.jpg", "Cuff Tear"),
|
| 121 |
+
("examples/vinyl_cuff_2.jpg", "Cuff Tear"),
|
| 122 |
+
("examples/vinyl_cuff_3.jpg", "Cuff Tear"),
|
| 123 |
+
("examples/vinyl_cuff_4.jpg", "Cuff Tear"),
|
| 124 |
+
("examples/vinyl_cuff_5.jpg", "Cuff Tear"),
|
| 125 |
+
("examples/vinyl_foreign_object_1.jpg", "Foreign Object"),
|
| 126 |
+
("examples/vinyl_foreign_object_2.jpg", "Foreign Object"),
|
| 127 |
+
("examples/vinyl_foreign_object_3.jpg", "Foreign Object"),
|
| 128 |
+
("examples/vinyl_foreign_object_4.jpg", "Foreign Object"),
|
| 129 |
+
("examples/vinyl_foreign_object_5.jpg", "Foreign Object"),
|
| 130 |
+
]
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
# ── UI ──────────────────────────────��─────────────────────────────────────────
|
| 134 |
+
|
| 135 |
+
with gr.Blocks(
|
| 136 |
+
title="Glove Defect Inspector",
|
| 137 |
+
theme=gr.themes.Soft(primary_hue="blue"),
|
| 138 |
+
) as demo:
|
| 139 |
+
|
| 140 |
+
gr.Markdown(
|
| 141 |
+
"""
|
| 142 |
+
# 🧤 Glove Defect Detection System
|
| 143 |
+
Upload a photo of a safety glove OR pick one from the sample gallery below.
|
| 144 |
+
The system will automatically identify the **glove type** and scan for **defects**.
|
| 145 |
+
|
| 146 |
+
| Glove Type | Detectable Defects |
|
| 147 |
+
|------------|-----------------------------------------------|
|
| 148 |
+
| Rubber | Holes · Discoloration · Folds |
|
| 149 |
+
| Cloth | Missing Fingers · Holes · Stains |
|
| 150 |
+
| Nitrile | Spots · Holes · Tears |
|
| 151 |
+
| Vinyl | Palm Holes · Cuff Tears · Foreign Objects |
|
| 152 |
+
"""
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
with gr.Row():
|
| 156 |
+
with gr.Column(scale=1):
|
| 157 |
+
inp = gr.Image(
|
| 158 |
+
type="numpy",
|
| 159 |
+
label="Input Image",
|
| 160 |
+
sources=["upload", "webcam"],
|
| 161 |
+
height=340,
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
with gr.Column(scale=1):
|
| 165 |
+
out_img = gr.Image(
|
| 166 |
+
type="numpy",
|
| 167 |
+
label="Annotated Result",
|
| 168 |
+
height=340,
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
with gr.Row():
|
| 172 |
+
out_type = gr.Textbox(label="Detected Glove Type", interactive=False)
|
| 173 |
+
out_status = gr.Textbox(label="Defect Status", interactive=False)
|
| 174 |
+
|
| 175 |
+
inp.change(fn=analyse, inputs=inp, outputs=[out_img, out_type, out_status])
|
| 176 |
+
|
| 177 |
+
# ── Sample gallery ────────────────────────────────────────────────────────
|
| 178 |
+
gr.Markdown("### 🖼️ Sample Images | Click any thumbnail to load & analyse it")
|
| 179 |
+
|
| 180 |
+
with gr.Accordion("🟢 Rubber Glove (11 samples)", open=False):
|
| 181 |
+
rubber_gallery = gr.Gallery(
|
| 182 |
+
value=RUBBER, columns=6, height=200,
|
| 183 |
+
show_label=False, allow_preview=False, object_fit="cover",
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
with gr.Accordion("⚪ Cloth Glove (16 samples)", open=False):
|
| 187 |
+
cloth_gallery = gr.Gallery(
|
| 188 |
+
value=CLOTH, columns=6, height=200,
|
| 189 |
+
show_label=False, allow_preview=False, object_fit="cover",
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
with gr.Accordion("🟣 Nitrile Glove (14 samples)", open=False):
|
| 193 |
+
nitrile_gallery = gr.Gallery(
|
| 194 |
+
value=NITRILE, columns=6, height=200,
|
| 195 |
+
show_label=False, allow_preview=False, object_fit="cover",
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
with gr.Accordion("⚫ Vinyl Glove (15 samples)", open=False):
|
| 199 |
+
vinyl_gallery = gr.Gallery(
|
| 200 |
+
value=VINYL, columns=6, height=200,
|
| 201 |
+
show_label=False, allow_preview=False, object_fit="cover",
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
# Clicking any gallery thumbnail → loads image into inp → auto-analyses
|
| 205 |
+
for gal in [rubber_gallery, cloth_gallery, nitrile_gallery, vinyl_gallery]:
|
| 206 |
+
gal.select(fn=load_from_gallery, outputs=inp)
|
| 207 |
+
|
| 208 |
+
gr.Markdown(
|
| 209 |
+
"""
|
| 210 |
+
---
|
| 211 |
+
**Project**: IPPR Assignment — Asia Pacific University (APU)
|
| 212 |
+
**Team**: Chiew Wen Qian · Farouk Elouzzani · Puteri Hannah · Wendy Koh Xin Hui
|
| 213 |
+
"""
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
if __name__ == "__main__":
|
| 217 |
+
demo.launch()
|
detectors/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# detectors package
|
detectors/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (256 Bytes). View file
|
|
|
detectors/__pycache__/cloth_detection.cpython-312.pyc
ADDED
|
Binary file (5.88 kB). View file
|
|
|
detectors/__pycache__/glove_type_detector.cpython-312.pyc
ADDED
|
Binary file (3.7 kB). View file
|
|
|
detectors/__pycache__/nitrile_detection.cpython-312.pyc
ADDED
|
Binary file (5.21 kB). View file
|
|
|
detectors/__pycache__/rubber_detection.cpython-312.pyc
ADDED
|
Binary file (6.54 kB). View file
|
|
|
detectors/__pycache__/utils.cpython-312.pyc
ADDED
|
Binary file (10.5 kB). View file
|
|
|
detectors/__pycache__/vinyl_detection.cpython-312.pyc
ADDED
|
Binary file (5.22 kB). View file
|
|
|
detectors/cloth_detection.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Cloth glove defect detector — ported from Cloth_Detection.m (Wendy Koh Xin Hui).
|
| 3 |
+
|
| 4 |
+
Detects:
|
| 5 |
+
MISSING FINGER — long, eccentric skin-coloured region (torn/missing finger)
|
| 6 |
+
HOLE — smaller skin-coloured spot inside the glove
|
| 7 |
+
STAIN — orange-yellow patch with low eccentricity
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import cv2
|
| 11 |
+
import numpy as np
|
| 12 |
+
from skimage.measure import regionprops, label as sk_label
|
| 13 |
+
from .utils import fill_holes, remove_small_blobs, get_hsv, disk_kernel, rect_kernel
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def detect_cloth(img_rgb: np.ndarray):
|
| 17 |
+
"""
|
| 18 |
+
Parameters
|
| 19 |
+
----------
|
| 20 |
+
img_rgb : np.ndarray (H, W, 3) uint8 RGB
|
| 21 |
+
|
| 22 |
+
Returns
|
| 23 |
+
-------
|
| 24 |
+
result : np.ndarray — annotated RGB image
|
| 25 |
+
status : str — 'Passed' or 'Defective'
|
| 26 |
+
"""
|
| 27 |
+
rows, cols = img_rgb.shape[:2]
|
| 28 |
+
defect_status = "Passed"
|
| 29 |
+
|
| 30 |
+
# ── Glove mask via Otsu ───────────────────────────────────────────────────
|
| 31 |
+
gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)
|
| 32 |
+
_, glove_mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
| 33 |
+
glove_mask = fill_holes(glove_mask)
|
| 34 |
+
|
| 35 |
+
H, S, V = get_hsv(img_rgb)
|
| 36 |
+
|
| 37 |
+
# ── Pipeline A: stain detection (orange-yellow H ∈ [0.07, 0.17]) ─────────
|
| 38 |
+
stain_raw = (
|
| 39 |
+
(H >= 0.07) & (H <= 0.17) & (S >= 0.40) & (V >= 0.30)
|
| 40 |
+
).astype(np.uint8) * 255
|
| 41 |
+
stain_raw = stain_raw & glove_mask
|
| 42 |
+
se_stain = disk_kernel(2)
|
| 43 |
+
stain_mask = cv2.dilate(cv2.erode(stain_raw, se_stain), se_stain)
|
| 44 |
+
stain_mask = remove_small_blobs(stain_mask, 60)
|
| 45 |
+
|
| 46 |
+
# ── Pipeline B: skin / hole detection (pink-red H ∈ [0.0, 0.06]) ─────────
|
| 47 |
+
skin_raw = (
|
| 48 |
+
(H >= 0.0) & (H <= 0.06) & (S >= 0.25) & (S <= 0.70) & (V >= 0.15)
|
| 49 |
+
).astype(np.uint8) * 255
|
| 50 |
+
se_clean = disk_kernel(3)
|
| 51 |
+
cleaned = cv2.dilate(cv2.erode(skin_raw & glove_mask, se_clean), se_clean)
|
| 52 |
+
# Close with a tall rectangle to bridge finger-width gaps
|
| 53 |
+
se_finger = rect_kernel(150, 20)
|
| 54 |
+
finger_mask = cv2.morphologyEx(cleaned, cv2.MORPH_CLOSE, se_finger)
|
| 55 |
+
finger_mask = finger_mask & ~stain_mask # stains must not bleed into finger mask
|
| 56 |
+
|
| 57 |
+
# ── Annotate ──────────────────────────────────────────────────────────────
|
| 58 |
+
result = img_rgb.copy()
|
| 59 |
+
|
| 60 |
+
# Finger / hole regions
|
| 61 |
+
n_labels, labels, stats, _ = cv2.connectedComponentsWithStats(
|
| 62 |
+
finger_mask, connectivity=8
|
| 63 |
+
)
|
| 64 |
+
for i in range(1, n_labels):
|
| 65 |
+
bx = stats[i, cv2.CC_STAT_LEFT]
|
| 66 |
+
by = stats[i, cv2.CC_STAT_TOP]
|
| 67 |
+
bw = stats[i, cv2.CC_STAT_WIDTH]
|
| 68 |
+
bh = stats[i, cv2.CC_STAT_HEIGHT]
|
| 69 |
+
area = stats[i, cv2.CC_STAT_AREA]
|
| 70 |
+
|
| 71 |
+
comp = (labels == i).astype(np.uint8) * 255
|
| 72 |
+
lf = sk_label(comp > 0)
|
| 73 |
+
props = regionprops(lf)
|
| 74 |
+
if not props:
|
| 75 |
+
continue
|
| 76 |
+
ecc = props[0].eccentricity
|
| 77 |
+
ratio = bh / max(bw, 1)
|
| 78 |
+
|
| 79 |
+
if (bh > 80 or ratio > 1.6) and ecc > 0.85:
|
| 80 |
+
cv2.rectangle(result, (bx, by), (bx + bw, by + bh), (255, 0, 0), 4)
|
| 81 |
+
cv2.putText(
|
| 82 |
+
result, "MISSING FINGER", (bx, max(by - 25, 12)),
|
| 83 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0), 2, cv2.LINE_AA,
|
| 84 |
+
)
|
| 85 |
+
defect_status = "Defective"
|
| 86 |
+
elif area > 80:
|
| 87 |
+
cv2.rectangle(result, (bx, by), (bx + bw, by + bh), (255, 255, 0), 2)
|
| 88 |
+
cv2.putText(
|
| 89 |
+
result, "HOLE", (bx, max(by - 20, 12)),
|
| 90 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 0), 2, cv2.LINE_AA,
|
| 91 |
+
)
|
| 92 |
+
defect_status = "Defective"
|
| 93 |
+
|
| 94 |
+
# Stain regions
|
| 95 |
+
n_s, labs_s, stats_s, _ = cv2.connectedComponentsWithStats(
|
| 96 |
+
stain_mask, connectivity=8
|
| 97 |
+
)
|
| 98 |
+
for j in range(1, n_s):
|
| 99 |
+
comp = (labs_s == j).astype(np.uint8) * 255
|
| 100 |
+
lf = sk_label(comp > 0)
|
| 101 |
+
props = regionprops(lf)
|
| 102 |
+
if not props:
|
| 103 |
+
continue
|
| 104 |
+
if props[0].eccentricity < 0.85:
|
| 105 |
+
bx = stats_s[j, cv2.CC_STAT_LEFT]
|
| 106 |
+
by = stats_s[j, cv2.CC_STAT_TOP]
|
| 107 |
+
bw = stats_s[j, cv2.CC_STAT_WIDTH]
|
| 108 |
+
bh = stats_s[j, cv2.CC_STAT_HEIGHT]
|
| 109 |
+
cv2.rectangle(result, (bx, by), (bx + bw, by + bh), (255, 128, 0), 3)
|
| 110 |
+
cv2.putText(
|
| 111 |
+
result, "STAIN", (bx, by + bh + 20),
|
| 112 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 128, 0), 2, cv2.LINE_AA,
|
| 113 |
+
)
|
| 114 |
+
defect_status = "Defective"
|
| 115 |
+
|
| 116 |
+
return result, defect_status
|
detectors/glove_type_detector.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Glove type classifier — ported from GloveTypeDetector.m (Farouk Elouzzani).
|
| 3 |
+
|
| 4 |
+
Pipeline:
|
| 5 |
+
1. Otsu threshold on grayscale → binary foreground mask
|
| 6 |
+
2. Invert mask if the border is mostly foreground (background/foreground swap)
|
| 7 |
+
3. Fill holes + keep largest blob
|
| 8 |
+
4. Extract HSV from foreground pixels, classify by dominant hue/saturation/value
|
| 9 |
+
Green → Rubber Glove
|
| 10 |
+
White → Cloth Glove
|
| 11 |
+
Purple → Nitrile Glove
|
| 12 |
+
Black → Vinyl Glove
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import cv2
|
| 16 |
+
import numpy as np
|
| 17 |
+
from .utils import fill_holes, keep_largest_blob, get_hsv
|
| 18 |
+
|
| 19 |
+
GLOVE_TYPES = {
|
| 20 |
+
1: "Rubber Glove",
|
| 21 |
+
2: "Cloth Glove",
|
| 22 |
+
3: "Nitrile Glove",
|
| 23 |
+
4: "Vinyl Glove",
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def detect_glove_type(img_rgb: np.ndarray):
|
| 28 |
+
"""
|
| 29 |
+
Classify the glove type in img_rgb.
|
| 30 |
+
|
| 31 |
+
Parameters
|
| 32 |
+
----------
|
| 33 |
+
img_rgb : np.ndarray (H, W, 3) uint8 RGB
|
| 34 |
+
|
| 35 |
+
Returns
|
| 36 |
+
-------
|
| 37 |
+
masked_glove : np.ndarray — input image with background zeroed out
|
| 38 |
+
glove_type : str — one of the GLOVE_TYPES values
|
| 39 |
+
"""
|
| 40 |
+
if img_rgb.ndim == 2:
|
| 41 |
+
img_rgb = cv2.cvtColor(img_rgb, cv2.COLOR_GRAY2RGB)
|
| 42 |
+
|
| 43 |
+
# --- Foreground / background separation via Otsu ---
|
| 44 |
+
gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)
|
| 45 |
+
_, mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
| 46 |
+
|
| 47 |
+
# If the border pixels are mostly bright, the glove is the dark object → invert
|
| 48 |
+
border = np.concatenate(
|
| 49 |
+
[mask[0, :], mask[-1, :], mask[:, 0], mask[:, -1]]
|
| 50 |
+
)
|
| 51 |
+
if np.sum(border > 0) > len(border) / 2:
|
| 52 |
+
mask = cv2.bitwise_not(mask)
|
| 53 |
+
|
| 54 |
+
mask = fill_holes(mask)
|
| 55 |
+
mask = keep_largest_blob(mask, n=1)
|
| 56 |
+
|
| 57 |
+
# --- HSV feature extraction from foreground ---
|
| 58 |
+
H, S, V = get_hsv(img_rgb)
|
| 59 |
+
fg = mask > 0
|
| 60 |
+
if not np.any(fg):
|
| 61 |
+
fg = np.ones_like(mask, dtype=bool)
|
| 62 |
+
|
| 63 |
+
fH, fS, fV = H[fg], S[fg], V[fg]
|
| 64 |
+
|
| 65 |
+
is_black = fV < 0.25
|
| 66 |
+
is_white = (fS < 0.10) & ~is_black
|
| 67 |
+
is_purple = (fH > 0.60) & (fH < 0.90) & ~is_white & ~is_black
|
| 68 |
+
is_green = (fH > 0.20) & (fH < 0.55) & ~is_white & ~is_black
|
| 69 |
+
|
| 70 |
+
# Default class = 2 (Cloth / white)
|
| 71 |
+
classes = np.full(len(fH), 2, dtype=np.int32)
|
| 72 |
+
classes[is_green] = 1
|
| 73 |
+
classes[is_white] = 2
|
| 74 |
+
classes[is_purple] = 3
|
| 75 |
+
classes[is_black] = 4
|
| 76 |
+
|
| 77 |
+
counts = {k: int(np.sum(classes == k)) for k in [1, 2, 3, 4]}
|
| 78 |
+
dominant = max(counts, key=counts.get)
|
| 79 |
+
|
| 80 |
+
masked = img_rgb.copy()
|
| 81 |
+
masked[~fg] = 0
|
| 82 |
+
|
| 83 |
+
return masked, GLOVE_TYPES[dominant]
|
detectors/nitrile_detection.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Nitrile glove defect detector — ported from Nitrile_Detection.m
|
| 3 |
+
(Puteri Hannah binti Mohamed Daud Ibrahim).
|
| 4 |
+
|
| 5 |
+
Detects:
|
| 6 |
+
SPOT — dark enclosed region (brightness < 0.25)
|
| 7 |
+
HOLE — bright enclosed gap away from the palm edge
|
| 8 |
+
TEAR — bright gap that touches the glove edge AND lies within the palm ROI
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import cv2
|
| 12 |
+
import numpy as np
|
| 13 |
+
from .utils import (
|
| 14 |
+
fill_holes, keep_largest_blob, remove_small_blobs,
|
| 15 |
+
im_dilate, get_hsv, resize_to_height, disk_kernel,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def detect_nitrile(img_rgb: np.ndarray):
|
| 20 |
+
"""
|
| 21 |
+
Parameters
|
| 22 |
+
----------
|
| 23 |
+
img_rgb : np.ndarray (H, W, 3) uint8 RGB
|
| 24 |
+
|
| 25 |
+
Returns
|
| 26 |
+
-------
|
| 27 |
+
result : np.ndarray — annotated RGB image
|
| 28 |
+
status : str — 'Passed' or 'Defective'
|
| 29 |
+
"""
|
| 30 |
+
img = resize_to_height(img_rgb, 800)
|
| 31 |
+
rows, cols = img.shape[:2]
|
| 32 |
+
|
| 33 |
+
blurred = cv2.GaussianBlur(img, (0, 0), 1.5)
|
| 34 |
+
H, S, V = get_hsv(blurred)
|
| 35 |
+
|
| 36 |
+
# ── Glove segmentation (purple H ∈ [0.55, 0.95]) ─────────────────────────
|
| 37 |
+
raw_mask = (
|
| 38 |
+
(H > 0.55) & (H < 0.95) & (S > 0.10) & (V > 0.15)
|
| 39 |
+
).astype(np.uint8) * 255
|
| 40 |
+
raw_mask = remove_small_blobs(raw_mask, 500)
|
| 41 |
+
|
| 42 |
+
solid = fill_holes(raw_mask)
|
| 43 |
+
silhouette = keep_largest_blob(solid, n=1)
|
| 44 |
+
|
| 45 |
+
# ── Palm ROI: circle centred on the glove, radius = 23 % of image width ──
|
| 46 |
+
palm_roi = np.zeros((rows, cols), dtype=bool)
|
| 47 |
+
n_lbl, lbl, stats, centroids = cv2.connectedComponentsWithStats(
|
| 48 |
+
silhouette, connectivity=8
|
| 49 |
+
)
|
| 50 |
+
if n_lbl > 1:
|
| 51 |
+
cx = float(centroids[1, 0])
|
| 52 |
+
cy = float(centroids[1, 1])
|
| 53 |
+
Y, X = np.mgrid[0:rows, 0:cols]
|
| 54 |
+
palm_roi = np.sqrt((X - cx) ** 2 + (Y - cy) ** 2) < (cols * 0.23)
|
| 55 |
+
|
| 56 |
+
# ── Defect masks ──────────────────────────────────────────────────────────
|
| 57 |
+
# Internal gaps: fully enclosed holes within the silhouette
|
| 58 |
+
internal = (silhouette > 0) & ~(raw_mask > 0)
|
| 59 |
+
|
| 60 |
+
# Edge tears: skin-tone pixels in a 15-px halo around the glove, inside palm ROI
|
| 61 |
+
skin_mask = ((H < 0.1) | (H > 0.90)) & (S > 0.15) & (V > 0.15)
|
| 62 |
+
halo = im_dilate(silhouette, 15) & ~(silhouette > 0)
|
| 63 |
+
edge_tears = skin_mask & (halo > 0) & palm_roi
|
| 64 |
+
|
| 65 |
+
all_defects = (internal | edge_tears).astype(np.uint8) * 255
|
| 66 |
+
all_defects = remove_small_blobs(all_defects, 40)
|
| 67 |
+
|
| 68 |
+
# ── Classification & annotation ───────────────────────────────────────────
|
| 69 |
+
glove_boundary = cv2.Canny(silhouette, 100, 200)
|
| 70 |
+
thick_boundary = im_dilate(glove_boundary, 12)
|
| 71 |
+
|
| 72 |
+
n_lbl, labels, stats, _ = cv2.connectedComponentsWithStats(
|
| 73 |
+
all_defects, connectivity=8
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
result = img.copy()
|
| 77 |
+
any_defect = False
|
| 78 |
+
|
| 79 |
+
for k in range(1, n_lbl):
|
| 80 |
+
idx = np.where(labels == k)
|
| 81 |
+
bb = stats[k]
|
| 82 |
+
bx, by = bb[cv2.CC_STAT_LEFT], bb[cv2.CC_STAT_TOP]
|
| 83 |
+
bw, bh = bb[cv2.CC_STAT_WIDTH], bb[cv2.CC_STAT_HEIGHT]
|
| 84 |
+
|
| 85 |
+
# Centroid bounds-check
|
| 86 |
+
cx_d = int(np.mean(idx[1]))
|
| 87 |
+
cy_d = int(np.mean(idx[0]))
|
| 88 |
+
cx_d = min(max(cx_d, 0), cols - 1)
|
| 89 |
+
cy_d = min(max(cy_d, 0), rows - 1)
|
| 90 |
+
|
| 91 |
+
brightness = float(np.median(V[idx]))
|
| 92 |
+
|
| 93 |
+
if brightness < 0.25:
|
| 94 |
+
label, color = "SPOT", (255, 255, 0)
|
| 95 |
+
else:
|
| 96 |
+
touches_edge = bool(
|
| 97 |
+
np.any(thick_boundary[idx]) or np.any(edge_tears[idx])
|
| 98 |
+
)
|
| 99 |
+
in_palm = bool(palm_roi[cy_d, cx_d])
|
| 100 |
+
if touches_edge and in_palm:
|
| 101 |
+
label, color = "TEAR", (255, 50, 50)
|
| 102 |
+
else:
|
| 103 |
+
label, color = "HOLE", (255, 0, 255)
|
| 104 |
+
|
| 105 |
+
any_defect = True
|
| 106 |
+
cv2.rectangle(result, (bx, by), (bx + bw, by + bh), color, 3)
|
| 107 |
+
cv2.putText(
|
| 108 |
+
result, label, (bx, max(by - 8, 12)),
|
| 109 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.65, color, 2, cv2.LINE_AA,
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
return result, "Defective" if any_defect else "Passed"
|
detectors/rubber_detection.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Rubber glove defect detector — ported from Rubber_Detection.m (Chiew Wen Qian).
|
| 3 |
+
|
| 4 |
+
Detects:
|
| 5 |
+
HOLE — enclosed regions whose colour does not match the glove
|
| 6 |
+
DISCOLORATION — abnormally vivid or dark patches inside the glove
|
| 7 |
+
FOLD — elongated dark valleys with a sharp brightness gradient
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import cv2
|
| 11 |
+
import numpy as np
|
| 12 |
+
from skimage.measure import regionprops, label as sk_label
|
| 13 |
+
from .utils import (
|
| 14 |
+
fill_holes, keep_largest_blob, remove_small_blobs,
|
| 15 |
+
im_close, im_erode, clean_mask, get_hsv,
|
| 16 |
+
resize_to_max, draw_labeled_regions,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def detect_rubber(img_rgb: np.ndarray):
|
| 21 |
+
"""
|
| 22 |
+
Parameters
|
| 23 |
+
----------
|
| 24 |
+
img_rgb : np.ndarray (H, W, 3) uint8 RGB
|
| 25 |
+
|
| 26 |
+
Returns
|
| 27 |
+
-------
|
| 28 |
+
result : np.ndarray — annotated RGB image
|
| 29 |
+
status : str — 'Passed' or 'Defective'
|
| 30 |
+
"""
|
| 31 |
+
img = resize_to_max(img_rgb, 800)
|
| 32 |
+
rows, cols = img.shape[:2]
|
| 33 |
+
|
| 34 |
+
# ── 1. HSV segmentation ───────────────────────────────────────────────────
|
| 35 |
+
blurred = cv2.GaussianBlur(img, (0, 0), 1.5)
|
| 36 |
+
H, S, V = get_hsv(blurred)
|
| 37 |
+
|
| 38 |
+
raw_glove = (
|
| 39 |
+
(H > 0.40) & (H < 0.68) & (S > 0.08) & (V > 0.20) & (V < 0.92)
|
| 40 |
+
).astype(np.uint8) * 255
|
| 41 |
+
raw_glove = remove_small_blobs(raw_glove, 800)
|
| 42 |
+
|
| 43 |
+
glove_mask = im_close(raw_glove, 6)
|
| 44 |
+
glove_mask = keep_largest_blob(glove_mask)
|
| 45 |
+
glove_mask = fill_holes(glove_mask)
|
| 46 |
+
safe_zone = im_erode(glove_mask, 8)
|
| 47 |
+
|
| 48 |
+
sz = safe_zone > 0
|
| 49 |
+
Vs, Ss = V[sz], S[sz]
|
| 50 |
+
medV = float(np.median(Vs)) if Vs.size else 0.5
|
| 51 |
+
medS = float(np.median(Ss)) if Ss.size else 0.2
|
| 52 |
+
stdV = float(np.std(Vs)) if Vs.size else 0.05
|
| 53 |
+
|
| 54 |
+
# ── 2. Hole detection ─────────────────────────────────────────────────────
|
| 55 |
+
# Enclosed regions = filled glove minus raw glove (dark interior gaps)
|
| 56 |
+
enclosed = (fill_holes(raw_glove) > 0) & ~(raw_glove > 0)
|
| 57 |
+
enclosed = enclosed & (im_erode(glove_mask, 8) > 0)
|
| 58 |
+
enclosed = remove_small_blobs(enclosed.astype(np.uint8) * 255, 30)
|
| 59 |
+
|
| 60 |
+
labeled = sk_label(enclosed > 0)
|
| 61 |
+
props = regionprops(labeled)
|
| 62 |
+
|
| 63 |
+
holes_bool = np.zeros((rows, cols), dtype=bool)
|
| 64 |
+
for p in props:
|
| 65 |
+
if p.area < 80 or p.eccentricity >= 0.98:
|
| 66 |
+
continue
|
| 67 |
+
r_idx, c_idx = p.coords[:, 0], p.coords[:, 1]
|
| 68 |
+
rH = float(H[r_idx, c_idx].mean())
|
| 69 |
+
rS = float(S[r_idx, c_idx].mean())
|
| 70 |
+
rV = float(V[r_idx, c_idx].mean())
|
| 71 |
+
|
| 72 |
+
is_glove_color = (0.40 < rH < 0.68) and rS > 0.08 and (0.20 < rV < 0.92)
|
| 73 |
+
is_dark_stain = (rV < medV - 0.12) and (rS < medS)
|
| 74 |
+
if is_glove_color or is_dark_stain:
|
| 75 |
+
continue
|
| 76 |
+
holes_bool[r_idx, c_idx] = True
|
| 77 |
+
|
| 78 |
+
holes_mask = clean_mask(holes_bool.astype(np.uint8) * 255, 6, 80)
|
| 79 |
+
|
| 80 |
+
# ── 3. Discoloration detection ────────────────────────────────────────────
|
| 81 |
+
vivid = (S > medS + 0.18) & sz
|
| 82 |
+
dark_mark = (V < medV - max(3.5 * stdV, 0.15)) & (S < medS) & sz
|
| 83 |
+
dcol_bool = (vivid | dark_mark) & ~(holes_mask > 0)
|
| 84 |
+
dcol_mask = clean_mask(dcol_bool.astype(np.uint8) * 255, 4, 120)
|
| 85 |
+
|
| 86 |
+
# ── 4. Fold detection ─────────────────────────────────────────────────────
|
| 87 |
+
interior = im_erode(glove_mask, 20) > 0
|
| 88 |
+
Vs_smooth = cv2.GaussianBlur(V.astype(np.float32), (0, 0), 1.5)
|
| 89 |
+
neigh_avg = cv2.blur(Vs_smooth, (25, 25))
|
| 90 |
+
valley = (Vs_smooth < neigh_avg - 0.04) & interior
|
| 91 |
+
|
| 92 |
+
gx = cv2.Sobel(Vs_smooth, cv2.CV_32F, 1, 0, ksize=3)
|
| 93 |
+
gy = cv2.Sobel(Vs_smooth, cv2.CV_32F, 0, 1, ksize=3)
|
| 94 |
+
grad = np.sqrt(gx ** 2 + gy ** 2)
|
| 95 |
+
g_max = grad.max()
|
| 96 |
+
grad_norm = grad / g_max if g_max > 0 else grad
|
| 97 |
+
sharp_edge = grad_norm > 0.18
|
| 98 |
+
|
| 99 |
+
fold_cand = (
|
| 100 |
+
valley & sharp_edge & interior
|
| 101 |
+
& ~(holes_mask > 0) & ~(dcol_mask > 0)
|
| 102 |
+
)
|
| 103 |
+
fold_cand = clean_mask(fold_cand.astype(np.uint8) * 255, 14, 500)
|
| 104 |
+
|
| 105 |
+
lf = sk_label(fold_cand > 0)
|
| 106 |
+
fp = regionprops(lf)
|
| 107 |
+
folds_bool = np.zeros((rows, cols), dtype=bool)
|
| 108 |
+
for p in fp:
|
| 109 |
+
if p.eccentricity > 0.80 and p.area > 500:
|
| 110 |
+
folds_bool[p.coords[:, 0], p.coords[:, 1]] = True
|
| 111 |
+
|
| 112 |
+
# ── 5. Annotate ───────────────────────────────────────────────────────────
|
| 113 |
+
result = img.copy()
|
| 114 |
+
found = False
|
| 115 |
+
found = draw_labeled_regions(result, holes_mask, (255, 50, 50), "HOLE", 40) or found
|
| 116 |
+
found = draw_labeled_regions(result, dcol_mask, (255, 255, 0), "DISCOLORATION", 100) or found
|
| 117 |
+
found = draw_labeled_regions(result, folds_bool.astype(np.uint8)*255, (0, 255, 255), "FOLD", 400) or found
|
| 118 |
+
|
| 119 |
+
return result, "Defective" if found else "Passed"
|
detectors/utils.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Shared image-processing helpers used across all glove detectors.
|
| 3 |
+
All functions accept and return uint8 binary masks (0 / 255) unless noted.
|
| 4 |
+
HSV values are normalised to [0, 1] throughout.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import cv2
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# ── Structuring elements ──────────────────────────────────────────────────────
|
| 12 |
+
|
| 13 |
+
def disk_kernel(r: int) -> np.ndarray:
|
| 14 |
+
"""Disk-shaped SE — equivalent to MATLAB strel('disk', r)."""
|
| 15 |
+
return cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * r + 1, 2 * r + 1))
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def rect_kernel(rows: int, cols: int) -> np.ndarray:
|
| 19 |
+
"""Rectangular SE — equivalent to MATLAB strel('rectangle', [rows, cols])."""
|
| 20 |
+
return cv2.getStructuringElement(cv2.MORPH_RECT, (cols, rows))
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# ── Morphological wrappers ────────────────────────────────────────────────────
|
| 24 |
+
|
| 25 |
+
def im_close(mask: np.ndarray, r: int) -> np.ndarray:
|
| 26 |
+
return cv2.morphologyEx(mask, cv2.MORPH_CLOSE, disk_kernel(r))
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def im_open(mask: np.ndarray, r: int) -> np.ndarray:
|
| 30 |
+
return cv2.morphologyEx(mask, cv2.MORPH_OPEN, disk_kernel(r))
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def im_erode(mask: np.ndarray, r: int) -> np.ndarray:
|
| 34 |
+
return cv2.erode(mask, disk_kernel(r))
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def im_dilate(mask: np.ndarray, r: int) -> np.ndarray:
|
| 38 |
+
return cv2.dilate(mask, disk_kernel(r))
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# ── Binary mask helpers ───────────────────────────────────────────────────────
|
| 42 |
+
|
| 43 |
+
def fill_holes(binary_mask: np.ndarray) -> np.ndarray:
|
| 44 |
+
"""Fill enclosed holes — equivalent to MATLAB imfill(mask, 'holes')."""
|
| 45 |
+
h, w = binary_mask.shape
|
| 46 |
+
flood = binary_mask.copy()
|
| 47 |
+
flood_mask = np.zeros((h + 2, w + 2), np.uint8)
|
| 48 |
+
cv2.floodFill(flood, flood_mask, (0, 0), 255)
|
| 49 |
+
return binary_mask | cv2.bitwise_not(flood)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def keep_largest_blob(binary_mask: np.ndarray, n: int = 1) -> np.ndarray:
|
| 53 |
+
"""Keep the n largest connected components — equivalent to bwareafilt(mask, n)."""
|
| 54 |
+
n_labels, labels, stats, _ = cv2.connectedComponentsWithStats(
|
| 55 |
+
binary_mask, connectivity=8
|
| 56 |
+
)
|
| 57 |
+
if n_labels <= 1:
|
| 58 |
+
return binary_mask
|
| 59 |
+
areas = stats[1:, cv2.CC_STAT_AREA]
|
| 60 |
+
top_labels = np.argsort(areas)[::-1][:n] + 1
|
| 61 |
+
result = np.zeros_like(binary_mask)
|
| 62 |
+
for lbl in top_labels:
|
| 63 |
+
result[labels == lbl] = 255
|
| 64 |
+
return result
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def remove_small_blobs(binary_mask: np.ndarray, min_area: int) -> np.ndarray:
|
| 68 |
+
"""Remove components smaller than min_area — equivalent to bwareaopen."""
|
| 69 |
+
n_labels, labels, stats, _ = cv2.connectedComponentsWithStats(
|
| 70 |
+
binary_mask, connectivity=8
|
| 71 |
+
)
|
| 72 |
+
result = np.zeros_like(binary_mask)
|
| 73 |
+
for i in range(1, n_labels):
|
| 74 |
+
if stats[i, cv2.CC_STAT_AREA] >= min_area:
|
| 75 |
+
result[labels == i] = 255
|
| 76 |
+
return result
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def imclearborder(binary_mask: np.ndarray) -> np.ndarray:
|
| 80 |
+
"""Remove blobs touching the image border — equivalent to MATLAB imclearborder."""
|
| 81 |
+
n_labels, labels, _, _ = cv2.connectedComponentsWithStats(
|
| 82 |
+
binary_mask, connectivity=8
|
| 83 |
+
)
|
| 84 |
+
border_labels = set()
|
| 85 |
+
for edge in (labels[0, :], labels[-1, :], labels[:, 0], labels[:, -1]):
|
| 86 |
+
border_labels.update(np.unique(edge).tolist())
|
| 87 |
+
border_labels.discard(0)
|
| 88 |
+
result = binary_mask.copy()
|
| 89 |
+
for lbl in border_labels:
|
| 90 |
+
result[labels == lbl] = 0
|
| 91 |
+
return result
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def clean_mask(mask: np.ndarray, disk_r: int, min_area: int) -> np.ndarray:
|
| 95 |
+
"""im_close then remove_small_blobs — matches MATLAB cleanMask helper."""
|
| 96 |
+
return remove_small_blobs(im_close(mask, disk_r), min_area)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
# ── Colour / resize helpers ───────────────────────────────────────────────────
|
| 100 |
+
|
| 101 |
+
def get_hsv(img_rgb: np.ndarray):
|
| 102 |
+
"""
|
| 103 |
+
Convert RGB image to HSV and return H, S, V as float32 arrays in [0, 1].
|
| 104 |
+
OpenCV uses H∈[0,179], S∈[0,255], V∈[0,255] — we normalise here.
|
| 105 |
+
"""
|
| 106 |
+
hsv = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2HSV).astype(np.float32)
|
| 107 |
+
return hsv[:, :, 0] / 179.0, hsv[:, :, 1] / 255.0, hsv[:, :, 2] / 255.0
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def resize_to_max(img: np.ndarray, max_px: int = 800) -> np.ndarray:
|
| 111 |
+
"""Resize so that the longer side equals max_px (preserves aspect ratio)."""
|
| 112 |
+
h, w = img.shape[:2]
|
| 113 |
+
if h >= w:
|
| 114 |
+
new_h, new_w = max_px, max(1, int(w * max_px / h))
|
| 115 |
+
else:
|
| 116 |
+
new_h, new_w = max(1, int(h * max_px / w)), max_px
|
| 117 |
+
return cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def resize_to_height(img: np.ndarray, target_h: int = 800) -> np.ndarray:
|
| 121 |
+
"""Resize so height == target_h — matches imresize(img, [800, NaN])."""
|
| 122 |
+
h, w = img.shape[:2]
|
| 123 |
+
new_w = max(1, int(w * target_h / h))
|
| 124 |
+
return cv2.resize(img, (new_w, target_h), interpolation=cv2.INTER_LINEAR)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def resize_to_width(img: np.ndarray, target_w: int = 800) -> np.ndarray:
|
| 128 |
+
"""Resize so width == target_w — matches imresize(img, scale) by width."""
|
| 129 |
+
h, w = img.shape[:2]
|
| 130 |
+
new_h = max(1, int(h * target_w / w))
|
| 131 |
+
return cv2.resize(img, (target_w, new_h), interpolation=cv2.INTER_LINEAR)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
# ── Drawing helper ────────────────────────────────────────────────────────────
|
| 135 |
+
|
| 136 |
+
def draw_labeled_regions(
|
| 137 |
+
img: np.ndarray,
|
| 138 |
+
mask: np.ndarray,
|
| 139 |
+
color: tuple,
|
| 140 |
+
label: str,
|
| 141 |
+
min_area: int,
|
| 142 |
+
) -> bool:
|
| 143 |
+
"""
|
| 144 |
+
Draw a coloured outline + text label for every region in mask that is
|
| 145 |
+
larger than min_area. img is modified in place (RGB uint8).
|
| 146 |
+
Returns True if at least one region was drawn.
|
| 147 |
+
"""
|
| 148 |
+
if mask is None or not np.any(mask > 0):
|
| 149 |
+
return False
|
| 150 |
+
if mask.dtype != np.uint8:
|
| 151 |
+
mask = (mask > 0).astype(np.uint8) * 255
|
| 152 |
+
|
| 153 |
+
n_labels, labels, stats, _ = cv2.connectedComponentsWithStats(
|
| 154 |
+
mask, connectivity=8
|
| 155 |
+
)
|
| 156 |
+
found = False
|
| 157 |
+
for i in range(1, n_labels):
|
| 158 |
+
if stats[i, cv2.CC_STAT_AREA] < min_area:
|
| 159 |
+
continue
|
| 160 |
+
found = True
|
| 161 |
+
comp = ((labels == i).astype(np.uint8) * 255)
|
| 162 |
+
perim = im_dilate(comp, 3) - comp
|
| 163 |
+
img[perim > 0] = color
|
| 164 |
+
x = stats[i, cv2.CC_STAT_LEFT]
|
| 165 |
+
y = max(stats[i, cv2.CC_STAT_TOP] - 8, 12)
|
| 166 |
+
cv2.putText(
|
| 167 |
+
img, label, (x, y),
|
| 168 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.65, color, 2, cv2.LINE_AA,
|
| 169 |
+
)
|
| 170 |
+
return found
|
detectors/vinyl_detection.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Vinyl glove defect detector — ported from Vinyl_Detection.m (Farouk Elouzzani).
|
| 3 |
+
|
| 4 |
+
Detects (blobs with area > 800 px after morphological cleaning):
|
| 5 |
+
PALM HOLE — skin-coloured, circular (circularity > 0.8)
|
| 6 |
+
CUFF TEAR — skin-coloured, elongated (circularity ≤ 0.8)
|
| 7 |
+
FOREIGN OBJ — non-skin-coloured anomaly
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import math
|
| 11 |
+
import cv2
|
| 12 |
+
import numpy as np
|
| 13 |
+
from skimage.measure import regionprops, label as sk_label
|
| 14 |
+
from .utils import imclearborder, get_hsv, resize_to_width, im_open, im_close
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def detect_vinyl(img_rgb: np.ndarray):
|
| 18 |
+
"""
|
| 19 |
+
Parameters
|
| 20 |
+
----------
|
| 21 |
+
img_rgb : np.ndarray (H, W, 3) uint8 RGB
|
| 22 |
+
|
| 23 |
+
Returns
|
| 24 |
+
-------
|
| 25 |
+
result : np.ndarray — annotated RGB image
|
| 26 |
+
status : str — 'Passed' or 'Defective'
|
| 27 |
+
"""
|
| 28 |
+
img = resize_to_width(img_rgb, 800)
|
| 29 |
+
rows, cols = img.shape[:2]
|
| 30 |
+
|
| 31 |
+
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
|
| 32 |
+
H, S, V = get_hsv(img)
|
| 33 |
+
|
| 34 |
+
# ── Masks ─────────────────────────────────────────────────────────────────
|
| 35 |
+
# Foreground: non-white pixels; Glove: dark (V < 0.35 = black vinyl body)
|
| 36 |
+
fg_mask = (gray < 210).astype(np.uint8) * 255
|
| 37 |
+
glove_mask = (V < 0.35).astype(np.uint8) * 255
|
| 38 |
+
|
| 39 |
+
# Defects are foreground pixels that are NOT the dark glove but have colour
|
| 40 |
+
defect_mask = fg_mask & ~glove_mask & ((S > 0.15).astype(np.uint8) * 255)
|
| 41 |
+
|
| 42 |
+
# ── Morphological refinement ──────────────────────────────────────────────
|
| 43 |
+
defect_mask = im_open(defect_mask, 3)
|
| 44 |
+
defect_mask = im_close(defect_mask, 3)
|
| 45 |
+
defect_mask = imclearborder(defect_mask)
|
| 46 |
+
|
| 47 |
+
# ── Blob analysis ─────────────────────────────────────────────────────────
|
| 48 |
+
lf = sk_label(defect_mask > 0)
|
| 49 |
+
props = regionprops(lf)
|
| 50 |
+
|
| 51 |
+
result = img.copy()
|
| 52 |
+
defect_status = "Passed"
|
| 53 |
+
|
| 54 |
+
for p in props:
|
| 55 |
+
if p.area <= 800:
|
| 56 |
+
continue
|
| 57 |
+
|
| 58 |
+
defect_status = "Defective"
|
| 59 |
+
bx, by, bx2, by2 = (
|
| 60 |
+
p.bbox[1], p.bbox[0], p.bbox[3], p.bbox[2]
|
| 61 |
+
)
|
| 62 |
+
bw, bh = bx2 - bx, by2 - by
|
| 63 |
+
label_x = bx + bw // 2
|
| 64 |
+
label_y = max(by - 40, 15)
|
| 65 |
+
|
| 66 |
+
# Per-blob colour sampling
|
| 67 |
+
r_idx, c_idx = p.coords[:, 0], p.coords[:, 1]
|
| 68 |
+
meanH = float(H[r_idx, c_idx].mean())
|
| 69 |
+
meanS = float(S[r_idx, c_idx].mean())
|
| 70 |
+
meanV = float(V[r_idx, c_idx].mean())
|
| 71 |
+
|
| 72 |
+
is_skin = (
|
| 73 |
+
0.0 <= meanH <= 0.12
|
| 74 |
+
and 0.20 <= meanS <= 0.70
|
| 75 |
+
and 0.30 <= meanV <= 1.00
|
| 76 |
+
)
|
| 77 |
+
circularity = (
|
| 78 |
+
(4 * math.pi * p.area) / (p.perimeter ** 2)
|
| 79 |
+
if p.perimeter > 0 else 0.0
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
if is_skin:
|
| 83 |
+
if circularity > 0.8:
|
| 84 |
+
label, outline, bg = "PALM HOLE", (255, 255, 0), (0, 0, 0)
|
| 85 |
+
else:
|
| 86 |
+
label, outline, bg = "CUFF TEAR", (255, 50, 50), (255, 255, 255)
|
| 87 |
+
else:
|
| 88 |
+
label, outline, bg = "FOREIGN OBJ", (50, 255, 50), (0, 0, 0)
|
| 89 |
+
|
| 90 |
+
# Draw boundary contour
|
| 91 |
+
comp = (lf == p.label).astype(np.uint8) * 255
|
| 92 |
+
contours, _ = cv2.findContours(comp, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 93 |
+
cv2.drawContours(result, contours, -1, outline, 3)
|
| 94 |
+
|
| 95 |
+
# Draw label with background box
|
| 96 |
+
font = cv2.FONT_HERSHEY_SIMPLEX
|
| 97 |
+
font_scale = 0.6
|
| 98 |
+
thickness = 2
|
| 99 |
+
(tw, th), _ = cv2.getTextSize(label, font, font_scale, thickness)
|
| 100 |
+
tx = label_x - tw // 2
|
| 101 |
+
ty = label_y
|
| 102 |
+
cv2.rectangle(result, (tx - 2, ty - th - 2), (tx + tw + 2, ty + 2), bg, -1)
|
| 103 |
+
cv2.putText(result, label, (tx, ty), font, font_scale, outline, thickness, cv2.LINE_AA)
|
| 104 |
+
|
| 105 |
+
if defect_status == "Passed":
|
| 106 |
+
cv2.putText(
|
| 107 |
+
result, "NO DEFECTS DETECTED", (20, 40),
|
| 108 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2, cv2.LINE_AA,
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
return result, defect_status
|
examples/DC_1.jpeg
ADDED
|
examples/DC_2.jpeg
ADDED
|
examples/Fold_1.png
ADDED
|
Git LFS Details
|
examples/Fold_2.png
ADDED
|
Git LFS Details
|
examples/Fold_3.png
ADDED
|
Git LFS Details
|
examples/H(2).jpeg
ADDED
|
examples/H(3).jpeg
ADDED
|
examples/H(5).jpeg
ADDED
|
examples/H(6).jpeg
ADDED
|
examples/H(7).jpeg
ADDED
|
examples/Holes_1.png
ADDED
|
Git LFS Details
|
examples/Holes_2.png
ADDED
|
Git LFS Details
|
examples/Holes_3.png
ADDED
|
Git LFS Details
|
examples/Holes_4.png
ADDED
|
Git LFS Details
|
examples/MF(2).jpeg
ADDED
|
examples/MF(3).jpeg
ADDED
|
examples/MF(4).jpeg
ADDED
|
examples/MF(5).jpeg
ADDED
|
examples/MF(6).jpeg
ADDED
|
examples/MF(7).jpeg
ADDED
|
examples/Normal_1.jpg
ADDED
|
examples/Normal_2.jpg
ADDED
|
examples/Original_1.jpeg
ADDED
|
examples/Original_2.jpeg
ADDED
|
examples/Original_3.jpeg
ADDED
|
examples/PH_1.jpeg
ADDED
|
examples/PH_2.jpeg
ADDED
|
examples/PH_3.jpeg
ADDED
|
examples/S(1).jpeg
ADDED
|
examples/S(10).jpeg
ADDED
|
examples/S(7).jpeg
ADDED
|
examples/S(8).jpeg
ADDED
|
examples/S(9).jpeg
ADDED
|