farouk04 commited on
Commit
9cf8add
·
verified ·
1 Parent(s): 0dff4ee

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +27 -0
  2. README.md +40 -5
  3. app.py +217 -0
  4. detectors/__init__.py +1 -0
  5. detectors/__pycache__/__init__.cpython-312.pyc +0 -0
  6. detectors/__pycache__/cloth_detection.cpython-312.pyc +0 -0
  7. detectors/__pycache__/glove_type_detector.cpython-312.pyc +0 -0
  8. detectors/__pycache__/nitrile_detection.cpython-312.pyc +0 -0
  9. detectors/__pycache__/rubber_detection.cpython-312.pyc +0 -0
  10. detectors/__pycache__/utils.cpython-312.pyc +0 -0
  11. detectors/__pycache__/vinyl_detection.cpython-312.pyc +0 -0
  12. detectors/cloth_detection.py +116 -0
  13. detectors/glove_type_detector.py +83 -0
  14. detectors/nitrile_detection.py +112 -0
  15. detectors/rubber_detection.py +119 -0
  16. detectors/utils.py +170 -0
  17. detectors/vinyl_detection.py +111 -0
  18. examples/DC_1.jpeg +0 -0
  19. examples/DC_2.jpeg +0 -0
  20. examples/Fold_1.png +3 -0
  21. examples/Fold_2.png +3 -0
  22. examples/Fold_3.png +3 -0
  23. examples/H(2).jpeg +0 -0
  24. examples/H(3).jpeg +0 -0
  25. examples/H(5).jpeg +0 -0
  26. examples/H(6).jpeg +0 -0
  27. examples/H(7).jpeg +0 -0
  28. examples/Holes_1.png +3 -0
  29. examples/Holes_2.png +3 -0
  30. examples/Holes_3.png +3 -0
  31. examples/Holes_4.png +3 -0
  32. examples/MF(2).jpeg +0 -0
  33. examples/MF(3).jpeg +0 -0
  34. examples/MF(4).jpeg +0 -0
  35. examples/MF(5).jpeg +0 -0
  36. examples/MF(6).jpeg +0 -0
  37. examples/MF(7).jpeg +0 -0
  38. examples/Normal_1.jpg +0 -0
  39. examples/Normal_2.jpg +0 -0
  40. examples/Original_1.jpeg +0 -0
  41. examples/Original_2.jpeg +0 -0
  42. examples/Original_3.jpeg +0 -0
  43. examples/PH_1.jpeg +0 -0
  44. examples/PH_2.jpeg +0 -0
  45. examples/PH_3.jpeg +0 -0
  46. examples/S(1).jpeg +0 -0
  47. examples/S(10).jpeg +0 -0
  48. examples/S(7).jpeg +0 -0
  49. examples/S(8).jpeg +0 -0
  50. 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: indigo
5
- colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 6.12.0
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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

  • SHA256: c043246b23c4c52d57811dee286be67233ed38fa46a8438faa2e4dddebcd2c54
  • Pointer size: 132 Bytes
  • Size of remote file: 4.61 MB
examples/Fold_2.png ADDED

Git LFS Details

  • SHA256: b0ab3407af03c4c840360b654fe9e1cba0d954716166558e9411cfb4ece6d65e
  • Pointer size: 132 Bytes
  • Size of remote file: 4.86 MB
examples/Fold_3.png ADDED

Git LFS Details

  • SHA256: 28aed4fa85c0ca4f431c28c86cd8bc11dfd31c98b4f2a89066a01d465aea8f87
  • Pointer size: 132 Bytes
  • Size of remote file: 4.99 MB
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

  • SHA256: e4655841992af8ed026f1d0152aa454390e3186d20b21bdefe46ff08622c4278
  • Pointer size: 132 Bytes
  • Size of remote file: 4.92 MB
examples/Holes_2.png ADDED

Git LFS Details

  • SHA256: 8ea48fc1dd362b41691e1b89ec2cc0e4b15cf2459be6418d551b8d96ca8fb370
  • Pointer size: 132 Bytes
  • Size of remote file: 4.92 MB
examples/Holes_3.png ADDED

Git LFS Details

  • SHA256: 3e8b39670e8d4c4e0a8f62ba06d43b66d9d51f0c344e9c60031f657d2b2f8288
  • Pointer size: 132 Bytes
  • Size of remote file: 4.69 MB
examples/Holes_4.png ADDED

Git LFS Details

  • SHA256: 31a5110ea6a3e0ba2e78dfb3fe490b2001ac51a60ddc878544a84229a9af1282
  • Pointer size: 131 Bytes
  • Size of remote file: 981 kB
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