smiler488 commited on
Commit
90297ca
·
verified ·
1 Parent(s): c39dcb1

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +244 -0
  2. requirements.txt +6 -0
app.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import numpy as np
3
+ import cv2
4
+ import pandas as pd
5
+ from skimage import measure, morphology
6
+ import tempfile
7
+
8
+ MAX_SIDE = 2048
9
+
10
+ def downscale_bgr(img):
11
+ h, w = img.shape[:2]
12
+ scale = min(1.0, MAX_SIDE / max(h, w))
13
+ if scale < 1.0:
14
+ img = cv2.resize(img, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA)
15
+ return img, scale
16
+
17
+ def normalize_angle(angle, size_w, size_h):
18
+ a = angle
19
+ if size_w < size_h:
20
+ a += 90.0
21
+ a = ((a % 180.0) + 180.0) % 180.0
22
+ return a
23
+
24
+ def detect_reference(img_bgr, mode, ref_size_mm):
25
+ h, w = img_bgr.shape[:2]
26
+ roi_w = int(w * 0.25)
27
+ roi_h = int(h * 0.25)
28
+ roi = img_bgr[0:roi_h, 0:roi_w]
29
+ gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
30
+ gray = cv2.medianBlur(gray, 5)
31
+ px_per_mm = None
32
+ center = None
33
+ ref_type = None
34
+ if mode in ["auto", "coin"]:
35
+ circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, dp=1.2, minDist=20, param1=120, param2=35, minRadius=8, maxRadius=min(roi_w, roi_h) // 2)
36
+ if circles is not None and len(circles) > 0:
37
+ c = circles[0][0]
38
+ r = float(c[2])
39
+ d_px = 2.0 * r
40
+ d_mm = ref_size_mm if ref_size_mm and ref_size_mm > 0 else 25.0
41
+ px_per_mm = d_px / d_mm
42
+ center = (int(c[0]), int(c[1]))
43
+ ref_type = "coin"
44
+ if px_per_mm is None and mode in ["auto", "square"]:
45
+ edges = cv2.Canny(gray, 80, 160)
46
+ cnts, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
47
+ best = None
48
+ best_score = 0.0
49
+ for cnt in cnts:
50
+ x, y, ww, hh = cv2.boundingRect(cnt)
51
+ area = ww * hh
52
+ if area < 225:
53
+ continue
54
+ score = min(ww, hh) / max(ww, hh)
55
+ if score > best_score:
56
+ best_score = score
57
+ best = (x, y, ww, hh)
58
+ if best is not None and best_score > 0.6:
59
+ ww, hh = best[2], best[3]
60
+ s_px = float(max(ww, hh))
61
+ s_mm = ref_size_mm if ref_size_mm and ref_size_mm > 0 else 20.0
62
+ px_per_mm = s_px / s_mm
63
+ center = (best[0] + ww // 2, best[1] + hh // 2)
64
+ ref_type = "square"
65
+ if px_per_mm is None:
66
+ px_per_mm = 5.0 if ref_size_mm and ref_size_mm < 10 else 3.0
67
+ return px_per_mm, center, ref_type
68
+
69
+ def build_mask_hsv(img_bgr, sample_type, hsv_low_h, hsv_high_h, color_tol):
70
+ hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
71
+ h = hsv[:, :, 0]
72
+ s = hsv[:, :, 1]
73
+ v = hsv[:, :, 2]
74
+ if sample_type == "leaves":
75
+ low_h = int(max(0, hsv_low_h))
76
+ high_h = int(min(179, hsv_high_h))
77
+ mask_h = cv2.inRange(h, low_h, high_h)
78
+ mask_s = cv2.inRange(s, 30, 255)
79
+ mask_v = cv2.inRange(v, 30, 255)
80
+ mask = cv2.bitwise_and(mask_h, cv2.bitwise_and(mask_s, mask_v))
81
+ else:
82
+ mask_s = cv2.inRange(s, 20, 255)
83
+ mask_v = cv2.inRange(v, 20, 255)
84
+ mask = cv2.bitwise_and(mask_s, mask_v)
85
+ kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
86
+ mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
87
+ mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
88
+ return mask
89
+
90
+ def segment(img_bgr, sample_type, hsv_low_h, hsv_high_h, color_tol, min_area_px, max_area_px):
91
+ mask = build_mask_hsv(img_bgr, sample_type, hsv_low_h, hsv_high_h, color_tol)
92
+ cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
93
+ comps = []
94
+ for cnt in cnts:
95
+ area_px = cv2.contourArea(cnt)
96
+ if area_px < float(min_area_px) or area_px > float(max_area_px):
97
+ continue
98
+ rect = cv2.minAreaRect(cnt)
99
+ box = cv2.boxPoints(rect)
100
+ box = np.int0(box)
101
+ m = cv2.moments(cnt)
102
+ if m["m00"] == 0:
103
+ cx, cy = 0, 0
104
+ else:
105
+ cx = int(m["m10"] / m["m00"])
106
+ cy = int(m["m01"] / m["m00"])
107
+ peri = cv2.arcLength(cnt, True)
108
+ circ = (4.0 * np.pi * area_px) / (peri * peri + 1e-6)
109
+ hull = cv2.convexHull(cnt)
110
+ hull_area = cv2.contourArea(hull)
111
+ solidity = area_px / (hull_area + 1e-6)
112
+ angle = normalize_angle(rect[2], rect[1][0], rect[1][1])
113
+ comps.append({
114
+ "contour": cnt,
115
+ "rect": rect,
116
+ "box": box,
117
+ "area_px": area_px,
118
+ "peri_px": peri,
119
+ "center": (cx, cy),
120
+ "angle": angle,
121
+ "solidity": solidity
122
+ })
123
+ return comps
124
+
125
+ def compute_color_metrics(img_bgr, mask):
126
+ mean_bgr = cv2.mean(img_bgr, mask=mask)
127
+ mean_b, mean_g, mean_r = mean_bgr[0], mean_bgr[1], mean_bgr[2]
128
+ rgb = np.array([[[mean_r, mean_g, mean_b]]], dtype=np.uint8)
129
+ hsv = cv2.cvtColor(rgb, cv2.COLOR_RGB2HSV)[0, 0]
130
+ h, s, v = int(hsv[0]), int(hsv[1]), int(hsv[2])
131
+ denom = (mean_r + mean_g + mean_b + 1e-6)
132
+ green_index = (2.0 * mean_g - mean_r - mean_b) / denom
133
+ brown_index = (mean_r - mean_b) / denom
134
+ return mean_r, mean_g, mean_b, h, s, v, green_index, brown_index
135
+
136
+ def compute_metrics(img_bgr, comps, px_per_mm):
137
+ rows = []
138
+ for i, c in enumerate(comps, start=1):
139
+ w_px = max(c["rect"][1][0], c["rect"][1][1])
140
+ h_px = min(c["rect"][1][0], c["rect"][1][1])
141
+ length_mm = w_px / px_per_mm
142
+ width_mm = h_px / px_per_mm
143
+ area_mm2 = c["area_px"] / (px_per_mm * px_per_mm)
144
+ perimeter_mm = c["peri_px"] / px_per_mm
145
+ aspect_ratio = length_mm / (width_mm + 1e-6)
146
+ circularity = (4.0 * np.pi * area_mm2) / (perimeter_mm * perimeter_mm + 1e-6)
147
+ mask_single = np.zeros(img_bgr.shape[:2], dtype=np.uint8)
148
+ cv2.drawContours(mask_single, [c["contour"]], -1, 255, thickness=-1)
149
+ mean_r, mean_g, mean_b, h, s, v, gi, bi = compute_color_metrics(img_bgr, mask_single)
150
+ rows.append({
151
+ "label": f"s{i}",
152
+ "centerX_px": int(c["center"][0]),
153
+ "centerY_px": int(c["center"][1]),
154
+ "length_mm": round(length_mm, 2),
155
+ "width_mm": round(width_mm, 2),
156
+ "area_mm2": round(area_mm2, 2),
157
+ "perimeter_mm": round(perimeter_mm, 2),
158
+ "aspect_ratio": round(aspect_ratio, 2),
159
+ "circularity": round(circularity, 3),
160
+ "angle_deg": round(float(c["angle"]), 1),
161
+ "meanR": int(round(mean_r)),
162
+ "meanG": int(round(mean_g)),
163
+ "meanB": int(round(mean_b)),
164
+ "hue": h,
165
+ "saturation": s,
166
+ "value": v,
167
+ "greenIndex": round(float(gi), 3),
168
+ "brownIndex": round(float(bi), 3)
169
+ })
170
+ return pd.DataFrame(rows)
171
+
172
+ def render_overlay(img_bgr, px_per_mm, ref, comps, df):
173
+ out = img_bgr.copy()
174
+ if ref and ref[0] is not None:
175
+ cx, cy = ref[0]
176
+ cv2.circle(out, (int(cx), int(cy)), 16, (0, 0, 0), -1)
177
+ cv2.putText(out, "s0", (int(cx) + 20, int(cy) - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA)
178
+ for i, c in enumerate(comps, start=1):
179
+ box = c["box"]
180
+ cv2.drawContours(out, [box], 0, (0, 0, 0), 2)
181
+ cv2.drawContours(out, [c["contour"]], -1, (0, 0, 0), 1)
182
+ cx, cy = c["center"][0], c["center"][1]
183
+ cv2.circle(out, (int(cx), int(cy)), 14, (0, 0, 0), -1)
184
+ cv2.putText(out, f"s{i}", (int(cx) - 8, int(cy) + 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1, cv2.LINE_AA)
185
+ row = df.iloc[i - 1]
186
+ tx = int(cx) + 22
187
+ ty = int(cy) - 12
188
+ cv2.putText(out, f"L:{row['length_mm']}mm", (tx, ty), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA)
189
+ cv2.putText(out, f"W:{row['width_mm']}mm", (tx, ty + 12), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA)
190
+ cv2.putText(out, f"A:{row['area_mm2']}mm2", (tx, ty + 24), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA)
191
+ cv2.putText(out, f"θ:{row['angle_deg']}°", (tx, ty + 36), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA)
192
+ return cv2.cvtColor(out, cv2.COLOR_BGR2RGB)
193
+
194
+ def analyze(image, sample_type, expected_count, ref_mode, ref_size_mm, min_area_px, max_area_px, color_tol, hsv_low_h, hsv_high_h):
195
+ if image is None:
196
+ return None, pd.DataFrame(), None, []
197
+ img_rgb = np.array(image)
198
+ img_bgr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR)
199
+ img_bgr, scale = downscale_bgr(img_bgr)
200
+ px_per_mm, ref_center, ref_type = detect_reference(img_bgr, ref_mode, ref_size_mm)
201
+ comps = segment(img_bgr, sample_type, hsv_low_h, hsv_high_h, color_tol, min_area_px, max_area_px)
202
+ if sample_type == "leaves":
203
+ comps.sort(key=lambda c: c["center"][0])
204
+ else:
205
+ comps.sort(key=lambda c: c["center"][1] * 0.3 + c["center"][0] * 0.7)
206
+ if expected_count and expected_count > 0:
207
+ comps = comps[:int(expected_count)]
208
+ df = compute_metrics(img_bgr, comps, px_per_mm)
209
+ overlay = render_overlay(img_bgr.copy(), px_per_mm, (ref_center, ref_type), comps, df)
210
+ csv = df.to_csv(index=False)
211
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv")
212
+ tmp.write(csv.encode("utf-8"))
213
+ tmp.close()
214
+ js = df.to_dict(orient="records")
215
+ return overlay, df, tmp.name, js
216
+
217
+ with gr.Blocks(theme=gr.themes.Default()) as demo:
218
+ gr.Markdown("# Biological Sample Quantifier (Leaves / Seeds)")
219
+ with gr.Row():
220
+ with gr.Column(scale=1):
221
+ image = gr.Image(type="numpy", label="Upload image")
222
+ sample_type = gr.Radio(["leaves", "seeds-grains"], value="leaves", label="Sample type")
223
+ expected = gr.Slider(1, 500, value=5, step=1, label="Expected count")
224
+ ref_mode = gr.Radio(["auto", "coin", "square"], value="auto", label="Reference mode")
225
+ ref_size = gr.Slider(1, 100, value=25.0, step=0.1, label="Reference size (mm)")
226
+ min_area = gr.Slider(10, 5000, value=500, step=10, label="Min area (px²)")
227
+ max_area = gr.Slider(1000, 200000, value=50000, step=1000, label="Max area (px²)")
228
+ color_tol = gr.Slider(5, 100, value=40, step=1, label="Color tolerance")
229
+ hsv_low = gr.Slider(0, 179, value=35, step=1, label="HSV H lower (leaves)")
230
+ hsv_high = gr.Slider(0, 179, value=85, step=1, label="HSV H upper (leaves)")
231
+ run = gr.Button("Analyze")
232
+ reset = gr.Button("Reset")
233
+ with gr.Column(scale=2):
234
+ overlay = gr.Image(label="Annotated")
235
+ table = gr.Dataframe(label="Metrics", wrap=True)
236
+ csv_out = gr.File(label="CSV export")
237
+ json_out = gr.JSON(label="JSON preview")
238
+ def _analyze(image, sample_type, expected, ref_mode, ref_size, min_area, max_area, color_tol, hsv_low, hsv_high):
239
+ return analyze(image, sample_type, expected, ref_mode, ref_size, min_area, max_area, color_tol, hsv_low, hsv_high)
240
+ run.click(_analyze, [image, sample_type, expected, ref_mode, ref_size, min_area, max_area, color_tol, hsv_low, hsv_high], [overlay, table, csv_out, json_out])
241
+ reset.click(lambda: (None, pd.DataFrame(), None, []), None, [overlay, table, csv_out, json_out])
242
+
243
+ if __name__ == "__main__":
244
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio>=4.34.0
2
+ opencv-python-headless>=4.10.0.84
3
+ numpy>=1.26.4
4
+ pillow>=10.4.0
5
+ scikit-image>=0.24.0
6
+ pandas>=2.2.2