dropdrop 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dropdrop/__init__.py +16 -0
- dropdrop/cache.py +133 -0
- dropdrop/cli.py +252 -0
- dropdrop/config.py +67 -0
- dropdrop/pipeline.py +400 -0
- dropdrop/stats.py +299 -0
- dropdrop/ui.py +441 -0
- dropdrop-1.1.0.dist-info/METADATA +179 -0
- dropdrop-1.1.0.dist-info/RECORD +12 -0
- dropdrop-1.1.0.dist-info/WHEEL +4 -0
- dropdrop-1.1.0.dist-info/entry_points.txt +2 -0
- dropdrop-1.1.0.dist-info/licenses/LICENSE +21 -0
dropdrop/ui.py
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
"""User interface components for visualization and editing."""
|
|
2
|
+
|
|
3
|
+
import cv2
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseWindow:
|
|
8
|
+
"""Base class for all window-based interfaces."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, visualization_data):
|
|
11
|
+
self.visualization_data = visualization_data
|
|
12
|
+
self.frames = sorted(visualization_data.keys())
|
|
13
|
+
self.current_index = 0
|
|
14
|
+
self.window_name = "Window"
|
|
15
|
+
|
|
16
|
+
def navigate(self):
|
|
17
|
+
"""Handle keyboard navigation - common for all windows."""
|
|
18
|
+
key = cv2.waitKey(1) & 0xFF
|
|
19
|
+
|
|
20
|
+
if key == ord("q") or key == 27: # q or ESC
|
|
21
|
+
return False
|
|
22
|
+
elif key == 83 or key == ord(" "): # Right arrow or space
|
|
23
|
+
self.current_index = (self.current_index + 1) % len(self.frames)
|
|
24
|
+
elif key == 81: # Left arrow
|
|
25
|
+
self.current_index = (self.current_index - 1) % len(self.frames)
|
|
26
|
+
elif key == 13: # Enter
|
|
27
|
+
if self.current_index < len(self.frames) - 1:
|
|
28
|
+
self.current_index += 1
|
|
29
|
+
else:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
def get_current_frame_data(self):
|
|
35
|
+
"""Get current frame visualization data."""
|
|
36
|
+
return self.visualization_data[self.frames[self.current_index]]
|
|
37
|
+
|
|
38
|
+
def run(self):
|
|
39
|
+
"""Main window loop - to be overridden."""
|
|
40
|
+
raise NotImplementedError
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Viewer(BaseWindow):
|
|
44
|
+
"""Interactive viewer for detection results."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, visualization_data, results_df):
|
|
47
|
+
super().__init__(visualization_data)
|
|
48
|
+
self.df = results_df
|
|
49
|
+
self.mode = "steps"
|
|
50
|
+
self.window_name = "Droplet Detection Viewer"
|
|
51
|
+
|
|
52
|
+
def create_overlay(self, frame_idx):
|
|
53
|
+
"""Create overlay visualization from stored data."""
|
|
54
|
+
frame_data = self.visualization_data[frame_idx]
|
|
55
|
+
min_proj = frame_data["min_projection"]
|
|
56
|
+
|
|
57
|
+
overlay = cv2.cvtColor(min_proj, cv2.COLOR_GRAY2BGR)
|
|
58
|
+
|
|
59
|
+
for i, droplet_info in enumerate(frame_data["droplet_masks"]):
|
|
60
|
+
cx, cy = droplet_info["center"]
|
|
61
|
+
radius = int(droplet_info["radius"])
|
|
62
|
+
inclusions = droplet_info["inclusions"]
|
|
63
|
+
|
|
64
|
+
color = (0, 0, 255) if inclusions > 0 else (0, 255, 0)
|
|
65
|
+
cv2.circle(overlay, (int(cx), int(cy)), radius, color, 2)
|
|
66
|
+
|
|
67
|
+
eroded_radius = radius - self.df.iloc[0].get("erosion_pixels", 10)
|
|
68
|
+
if eroded_radius > 0:
|
|
69
|
+
cv2.circle(overlay, (int(cx), int(cy)), eroded_radius, (0, 255, 255), 1)
|
|
70
|
+
|
|
71
|
+
cv2.circle(overlay, (int(cx), int(cy)), 3, (255, 0, 0), -1)
|
|
72
|
+
|
|
73
|
+
if inclusions > 0:
|
|
74
|
+
cv2.putText(
|
|
75
|
+
overlay,
|
|
76
|
+
str(inclusions),
|
|
77
|
+
(int(cx) - 10, int(cy) + 5),
|
|
78
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
|
79
|
+
0.5,
|
|
80
|
+
(255, 255, 255),
|
|
81
|
+
2,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
frame_df = self.df[self.df["frame"] == frame_idx]
|
|
85
|
+
total_droplets = len(frame_df)
|
|
86
|
+
total_inclusions = frame_df["inclusions"].sum()
|
|
87
|
+
|
|
88
|
+
info_text = f"Frame {frame_idx} | Droplets: {total_droplets} | Inclusions: {int(total_inclusions)}"
|
|
89
|
+
cv2.putText(
|
|
90
|
+
overlay, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return overlay
|
|
94
|
+
|
|
95
|
+
def create_steps(self, frame_idx):
|
|
96
|
+
"""Create processing steps visualization from stored data."""
|
|
97
|
+
frame_data = self.visualization_data[frame_idx]
|
|
98
|
+
min_proj = frame_data["min_projection"]
|
|
99
|
+
h, w = min_proj.shape
|
|
100
|
+
|
|
101
|
+
images = []
|
|
102
|
+
|
|
103
|
+
min_bgr = cv2.cvtColor(min_proj, cv2.COLOR_GRAY2BGR)
|
|
104
|
+
images.append(("Min Projection", min_bgr))
|
|
105
|
+
|
|
106
|
+
droplet_overlay = np.zeros((h, w, 3), dtype=np.uint8)
|
|
107
|
+
for i, mask in enumerate(frame_data["droplet_masks"]):
|
|
108
|
+
droplet_mask = mask["mask"]
|
|
109
|
+
color_val = (i * 30) % 200 + 55
|
|
110
|
+
droplet_overlay[droplet_mask > 0] = [color_val, color_val, 0]
|
|
111
|
+
images.append(("Cellpose Detection", droplet_overlay))
|
|
112
|
+
|
|
113
|
+
eroded_overlay = np.zeros((h, w, 3), dtype=np.uint8)
|
|
114
|
+
for eroded_mask in frame_data["eroded_masks"]:
|
|
115
|
+
eroded_overlay[eroded_mask > 0] = [0, 200, 200]
|
|
116
|
+
images.append(("Eroded Masks", eroded_overlay))
|
|
117
|
+
|
|
118
|
+
if "masked_images" in frame_data and frame_data["masked_images"]:
|
|
119
|
+
blackhat_combined = np.zeros((h, w), dtype=np.uint8)
|
|
120
|
+
for masked_blackhat in frame_data["masked_images"]:
|
|
121
|
+
blackhat_combined = cv2.bitwise_or(blackhat_combined, masked_blackhat)
|
|
122
|
+
blackhat_bgr = cv2.cvtColor(blackhat_combined, cv2.COLOR_GRAY2BGR)
|
|
123
|
+
images.append(("Black-hat (Masked)", blackhat_bgr))
|
|
124
|
+
|
|
125
|
+
inclusion_overlay = np.zeros((h, w, 3), dtype=np.uint8)
|
|
126
|
+
for inclusion_mask in frame_data["inclusion_masks"]:
|
|
127
|
+
inclusion_overlay[:, :, 2] = cv2.bitwise_or(
|
|
128
|
+
inclusion_overlay[:, :, 2], inclusion_mask
|
|
129
|
+
)
|
|
130
|
+
images.append(("Detected Inclusions", inclusion_overlay))
|
|
131
|
+
|
|
132
|
+
final_overlay = self.create_overlay(frame_idx)
|
|
133
|
+
images.append(("Final Result", final_overlay))
|
|
134
|
+
|
|
135
|
+
cols = 3
|
|
136
|
+
rows = 2
|
|
137
|
+
collage = np.ones((rows * h, cols * w, 3), dtype=np.uint8) * 240
|
|
138
|
+
|
|
139
|
+
for idx, (title, img) in enumerate(images[:6]):
|
|
140
|
+
row = idx // cols
|
|
141
|
+
col = idx % cols
|
|
142
|
+
|
|
143
|
+
if img.shape[:2] != (h, w):
|
|
144
|
+
img = cv2.resize(img, (w, h))
|
|
145
|
+
|
|
146
|
+
img_copy = img.copy()
|
|
147
|
+
cv2.putText(
|
|
148
|
+
img_copy, title, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
collage[row * h : (row + 1) * h, col * w : (col + 1) * w] = img_copy
|
|
152
|
+
|
|
153
|
+
cv2.putText(
|
|
154
|
+
collage,
|
|
155
|
+
f"Frame {frame_idx}/{max(self.frames)}",
|
|
156
|
+
(10, rows * h - 10),
|
|
157
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
|
158
|
+
0.6,
|
|
159
|
+
(255, 0, 0),
|
|
160
|
+
2,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return collage
|
|
164
|
+
|
|
165
|
+
def run(self):
|
|
166
|
+
"""Run viewer with mode switching."""
|
|
167
|
+
cv2.namedWindow(self.window_name, cv2.WINDOW_NORMAL)
|
|
168
|
+
|
|
169
|
+
def mouse_callback(event, x, y, flags, param):
|
|
170
|
+
if event == cv2.EVENT_LBUTTONDOWN:
|
|
171
|
+
self.current_index = (self.current_index + 1) % len(self.frames)
|
|
172
|
+
|
|
173
|
+
cv2.setMouseCallback(self.window_name, mouse_callback)
|
|
174
|
+
|
|
175
|
+
while True:
|
|
176
|
+
frame_idx = self.frames[self.current_index]
|
|
177
|
+
|
|
178
|
+
if self.mode == "overlay":
|
|
179
|
+
display_img = self.create_overlay(frame_idx)
|
|
180
|
+
else:
|
|
181
|
+
display_img = self.create_steps(frame_idx)
|
|
182
|
+
|
|
183
|
+
cv2.imshow(self.window_name, display_img)
|
|
184
|
+
|
|
185
|
+
key = cv2.waitKey(1) & 0xFF
|
|
186
|
+
if key == ord("m"):
|
|
187
|
+
self.mode = "overlay" if self.mode == "steps" else "steps"
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
if not self.navigate():
|
|
191
|
+
break
|
|
192
|
+
|
|
193
|
+
cv2.destroyAllWindows()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class InclusionEditor(BaseWindow):
|
|
197
|
+
"""Interactive editor for inclusion corrections."""
|
|
198
|
+
|
|
199
|
+
def __init__(self, visualization_data, results_data):
|
|
200
|
+
super().__init__(visualization_data)
|
|
201
|
+
self.results_data = results_data
|
|
202
|
+
self.window_name = "Inclusion Editor"
|
|
203
|
+
self.inclusions = {}
|
|
204
|
+
self.undo_stack = {} # {frame_idx: [(action, data), ...]}
|
|
205
|
+
self.disabled_droplets = {} # {frame_idx: set of droplet indices}
|
|
206
|
+
self.right_mouse_down = False
|
|
207
|
+
self.mouse_pos = (0, 0)
|
|
208
|
+
self.show_droplets = True
|
|
209
|
+
self.initialize_inclusions()
|
|
210
|
+
|
|
211
|
+
def initialize_inclusions(self):
|
|
212
|
+
"""Initialize inclusions from detected masks - use centroids only."""
|
|
213
|
+
for frame_idx in self.frames:
|
|
214
|
+
self.inclusions[frame_idx] = []
|
|
215
|
+
self.undo_stack[frame_idx] = []
|
|
216
|
+
self.disabled_droplets[frame_idx] = set()
|
|
217
|
+
frame_data = self.visualization_data[frame_idx]
|
|
218
|
+
|
|
219
|
+
if "inclusion_masks" in frame_data:
|
|
220
|
+
for mask in frame_data["inclusion_masks"]:
|
|
221
|
+
if np.any(mask):
|
|
222
|
+
num_labels, labels, stats, centroids = (
|
|
223
|
+
cv2.connectedComponentsWithStats(
|
|
224
|
+
mask.astype(np.uint8), connectivity=8
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
for i in range(1, num_labels):
|
|
228
|
+
cx, cy = centroids[i]
|
|
229
|
+
self.inclusions[frame_idx].append((int(cx), int(cy)))
|
|
230
|
+
|
|
231
|
+
def get_droplet_at(self, x, y):
|
|
232
|
+
"""Get droplet index at position, or None if not found."""
|
|
233
|
+
frame_idx = self.frames[self.current_index]
|
|
234
|
+
frame_data = self.visualization_data[frame_idx]
|
|
235
|
+
|
|
236
|
+
for i, droplet_info in enumerate(frame_data.get("droplet_masks", [])):
|
|
237
|
+
cx, cy = droplet_info["center"]
|
|
238
|
+
radius = droplet_info["radius"]
|
|
239
|
+
dist = np.sqrt((x - cx) ** 2 + (y - cy) ** 2)
|
|
240
|
+
if dist <= radius:
|
|
241
|
+
return i
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
def toggle_droplet(self, droplet_idx):
|
|
245
|
+
"""Toggle droplet enabled/disabled state."""
|
|
246
|
+
frame_idx = self.frames[self.current_index]
|
|
247
|
+
if droplet_idx in self.disabled_droplets[frame_idx]:
|
|
248
|
+
self.disabled_droplets[frame_idx].remove(droplet_idx)
|
|
249
|
+
print(f"Enabled droplet {droplet_idx}")
|
|
250
|
+
else:
|
|
251
|
+
self.disabled_droplets[frame_idx].add(droplet_idx)
|
|
252
|
+
print(f"Disabled droplet {droplet_idx}")
|
|
253
|
+
|
|
254
|
+
def add_inclusion(self, x, y):
|
|
255
|
+
"""Add inclusion at position with undo tracking."""
|
|
256
|
+
frame_idx = self.frames[self.current_index]
|
|
257
|
+
pos = (x, y)
|
|
258
|
+
self.inclusions[frame_idx].append(pos)
|
|
259
|
+
self.undo_stack[frame_idx].append(("add", pos))
|
|
260
|
+
print(f"Added inclusion at: {x},{y}")
|
|
261
|
+
|
|
262
|
+
def remove_inclusion_at(self, x, y):
|
|
263
|
+
"""Remove inclusion nearest to position if within threshold."""
|
|
264
|
+
frame_idx = self.frames[self.current_index]
|
|
265
|
+
if self.inclusions[frame_idx]:
|
|
266
|
+
distances = [
|
|
267
|
+
np.sqrt((x - ix) ** 2 + (y - iy) ** 2)
|
|
268
|
+
for ix, iy in self.inclusions[frame_idx]
|
|
269
|
+
]
|
|
270
|
+
min_dist = min(distances)
|
|
271
|
+
if min_dist < 20:
|
|
272
|
+
idx = distances.index(min_dist)
|
|
273
|
+
pos = self.inclusions[frame_idx].pop(idx)
|
|
274
|
+
self.undo_stack[frame_idx].append(("remove", pos))
|
|
275
|
+
print(f"Removed inclusion at: {pos}")
|
|
276
|
+
return True
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
def clear_inclusions(self):
|
|
280
|
+
"""Clear all inclusions in current frame with undo tracking."""
|
|
281
|
+
frame_idx = self.frames[self.current_index]
|
|
282
|
+
if self.inclusions[frame_idx]:
|
|
283
|
+
old_inclusions = self.inclusions[frame_idx].copy()
|
|
284
|
+
self.undo_stack[frame_idx].append(("clear", old_inclusions))
|
|
285
|
+
self.inclusions[frame_idx] = []
|
|
286
|
+
print(f"Cleared {len(old_inclusions)} inclusions from frame {frame_idx}")
|
|
287
|
+
|
|
288
|
+
def undo(self):
|
|
289
|
+
"""Undo last action in current frame."""
|
|
290
|
+
frame_idx = self.frames[self.current_index]
|
|
291
|
+
if not self.undo_stack[frame_idx]:
|
|
292
|
+
print("Nothing to undo")
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
action, data = self.undo_stack[frame_idx].pop()
|
|
296
|
+
|
|
297
|
+
if action == "add":
|
|
298
|
+
self.inclusions[frame_idx].remove(data)
|
|
299
|
+
print(f"Undo: removed inclusion at {data}")
|
|
300
|
+
elif action == "remove":
|
|
301
|
+
self.inclusions[frame_idx].append(data)
|
|
302
|
+
print(f"Undo: restored inclusion at {data}")
|
|
303
|
+
elif action == "clear":
|
|
304
|
+
self.inclusions[frame_idx] = data
|
|
305
|
+
print(f"Undo: restored {len(data)} inclusions")
|
|
306
|
+
|
|
307
|
+
def draw_frame(self):
|
|
308
|
+
"""Draw current frame with droplet masks and inclusions."""
|
|
309
|
+
frame_data = self.get_current_frame_data()
|
|
310
|
+
min_proj = frame_data["min_projection"]
|
|
311
|
+
frame_idx = self.frames[self.current_index]
|
|
312
|
+
|
|
313
|
+
display = cv2.cvtColor(min_proj, cv2.COLOR_GRAY2BGR)
|
|
314
|
+
|
|
315
|
+
# Draw droplet boundaries
|
|
316
|
+
if self.show_droplets:
|
|
317
|
+
for i, droplet_info in enumerate(frame_data.get("droplet_masks", [])):
|
|
318
|
+
cx, cy = droplet_info["center"]
|
|
319
|
+
radius = int(droplet_info["radius"])
|
|
320
|
+
is_disabled = i in self.disabled_droplets[frame_idx]
|
|
321
|
+
color = (128, 128, 128) if is_disabled else (0, 255, 0)
|
|
322
|
+
thickness = 1 if is_disabled else 2
|
|
323
|
+
cv2.circle(display, (int(cx), int(cy)), radius, color, thickness)
|
|
324
|
+
cv2.circle(display, (int(cx), int(cy)), 3, color, -1)
|
|
325
|
+
if is_disabled:
|
|
326
|
+
cv2.putText(display, "X", (int(cx) - 8, int(cy) + 8),
|
|
327
|
+
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (128, 128, 128), 2)
|
|
328
|
+
|
|
329
|
+
# Draw inclusions
|
|
330
|
+
for x, y in self.inclusions[frame_idx]:
|
|
331
|
+
overlay = display.copy()
|
|
332
|
+
cv2.circle(overlay, (x, y), 7, (0, 0, 255), -1)
|
|
333
|
+
display = cv2.addWeighted(display, 0.5, overlay, 0.5, 0)
|
|
334
|
+
|
|
335
|
+
# Status bar
|
|
336
|
+
total_droplets = len(frame_data.get("droplet_masks", []))
|
|
337
|
+
active_droplets = total_droplets - len(self.disabled_droplets[frame_idx])
|
|
338
|
+
count = len(self.inclusions[frame_idx])
|
|
339
|
+
status = f"Frame {frame_idx} | Droplets: {active_droplets}/{total_droplets} | Inclusions: {count}"
|
|
340
|
+
cv2.putText(
|
|
341
|
+
display, status, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
hint = "Left: Add | Right: Remove | s: Toggle droplet | u: Undo | c: Clear | d: Droplets | q: Exit"
|
|
345
|
+
cv2.putText(
|
|
346
|
+
display,
|
|
347
|
+
hint,
|
|
348
|
+
(10, display.shape[0] - 10),
|
|
349
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
|
350
|
+
0.5,
|
|
351
|
+
(0, 255, 0),
|
|
352
|
+
1,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
return display
|
|
356
|
+
|
|
357
|
+
def update_results_with_inclusions(self):
|
|
358
|
+
"""Update results with correct per-droplet inclusion counts."""
|
|
359
|
+
# Filter out disabled droplets
|
|
360
|
+
filtered_results = []
|
|
361
|
+
|
|
362
|
+
for row in self.results_data:
|
|
363
|
+
frame_idx = row["frame"]
|
|
364
|
+
droplet_id = row["droplet_id"]
|
|
365
|
+
|
|
366
|
+
# Skip disabled droplets
|
|
367
|
+
if droplet_id in self.disabled_droplets.get(frame_idx, set()):
|
|
368
|
+
continue
|
|
369
|
+
|
|
370
|
+
# Count inclusions for this droplet
|
|
371
|
+
droplet_inclusions = 0
|
|
372
|
+
if frame_idx in self.inclusions:
|
|
373
|
+
cx, cy = row["center_x"], row["center_y"]
|
|
374
|
+
radius = row["diameter_px"] / 2
|
|
375
|
+
|
|
376
|
+
for ix, iy in self.inclusions[frame_idx]:
|
|
377
|
+
dist = np.sqrt((ix - cx) ** 2 + (iy - cy) ** 2)
|
|
378
|
+
if dist <= radius:
|
|
379
|
+
droplet_inclusions += 1
|
|
380
|
+
|
|
381
|
+
row["inclusions"] = droplet_inclusions
|
|
382
|
+
row["detected"] = False
|
|
383
|
+
filtered_results.append(row)
|
|
384
|
+
|
|
385
|
+
return filtered_results
|
|
386
|
+
|
|
387
|
+
def run(self):
|
|
388
|
+
"""Run interactive editor."""
|
|
389
|
+
print("\nINTERACTIVE INCLUSION EDITOR")
|
|
390
|
+
print("Left: Add | Right: Remove | s: Toggle droplet | u: Undo | c: Clear | d: Droplets | q: Exit\n")
|
|
391
|
+
|
|
392
|
+
cv2.namedWindow(self.window_name, cv2.WINDOW_NORMAL)
|
|
393
|
+
|
|
394
|
+
def mouse_callback(event, x, y, flags, param):
|
|
395
|
+
self.mouse_pos = (x, y)
|
|
396
|
+
frame_idx = self.frames[self.current_index]
|
|
397
|
+
|
|
398
|
+
if event == cv2.EVENT_LBUTTONDOWN:
|
|
399
|
+
self.add_inclusion(x, y)
|
|
400
|
+
elif event == cv2.EVENT_RBUTTONDOWN:
|
|
401
|
+
self.right_mouse_down = True
|
|
402
|
+
self.remove_inclusion_at(x, y)
|
|
403
|
+
elif event == cv2.EVENT_RBUTTONUP:
|
|
404
|
+
self.right_mouse_down = False
|
|
405
|
+
elif event == cv2.EVENT_MOUSEMOVE and self.right_mouse_down:
|
|
406
|
+
self.remove_inclusion_at(x, y)
|
|
407
|
+
|
|
408
|
+
cv2.setMouseCallback(self.window_name, mouse_callback)
|
|
409
|
+
|
|
410
|
+
while True:
|
|
411
|
+
display = self.draw_frame()
|
|
412
|
+
cv2.imshow(self.window_name, display)
|
|
413
|
+
|
|
414
|
+
key = cv2.waitKey(30) & 0xFF
|
|
415
|
+
|
|
416
|
+
if key == ord("c"):
|
|
417
|
+
self.clear_inclusions()
|
|
418
|
+
elif key == ord("u"):
|
|
419
|
+
self.undo()
|
|
420
|
+
elif key == ord("d"):
|
|
421
|
+
self.show_droplets = not self.show_droplets
|
|
422
|
+
print(f"Droplet visibility: {'ON' if self.show_droplets else 'OFF'}")
|
|
423
|
+
elif key == ord("s"):
|
|
424
|
+
droplet_idx = self.get_droplet_at(*self.mouse_pos)
|
|
425
|
+
if droplet_idx is not None:
|
|
426
|
+
self.toggle_droplet(droplet_idx)
|
|
427
|
+
elif key == ord("q") or key == 27:
|
|
428
|
+
break
|
|
429
|
+
elif key == 83 or key == ord(" "):
|
|
430
|
+
self.current_index = (self.current_index + 1) % len(self.frames)
|
|
431
|
+
elif key == 81:
|
|
432
|
+
self.current_index = (self.current_index - 1) % len(self.frames)
|
|
433
|
+
elif key == 13:
|
|
434
|
+
if self.current_index < len(self.frames) - 1:
|
|
435
|
+
self.current_index += 1
|
|
436
|
+
else:
|
|
437
|
+
break
|
|
438
|
+
|
|
439
|
+
cv2.destroyAllWindows()
|
|
440
|
+
|
|
441
|
+
return self.update_results_with_inclusions()
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dropdrop
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Python pipeline script for detecting droplets with beads and other inclusions via cellpose
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: cellpose>=4.0.6
|
|
9
|
+
Requires-Dist: matplotlib>=3.10.6
|
|
10
|
+
Requires-Dist: numpy>=2.3.3
|
|
11
|
+
Requires-Dist: opencv-python>=4.11.0.86
|
|
12
|
+
Requires-Dist: pandas>=2.3.3
|
|
13
|
+
Requires-Dist: scipy>=1.16.2
|
|
14
|
+
Requires-Dist: seaborn>=0.13.2
|
|
15
|
+
Requires-Dist: tqdm>=4.67.1
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# DropDrop
|
|
21
|
+
|
|
22
|
+
Automated Python pipeline for detecting droplets and inclusions (beads) in microscopy z-stacks using Cellpose segmentation and morphological analysis.
|
|
23
|
+
|
|
24
|
+
Tailored for the EVOS M5000 Imaging System.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Using uv (recommended)
|
|
30
|
+
uv pip install dropdrop
|
|
31
|
+
|
|
32
|
+
# Using pip
|
|
33
|
+
pip install dropdrop
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### From source
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/yourusername/dropdrop.git
|
|
40
|
+
cd dropdrop
|
|
41
|
+
uv pip install -e .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Run with interactive prompts
|
|
48
|
+
dropdrop ./images
|
|
49
|
+
|
|
50
|
+
# Run with settings
|
|
51
|
+
dropdrop ./images --settings "d=1000,p=on,l=experiment1"
|
|
52
|
+
|
|
53
|
+
# Process only first 5 frames (for testing)
|
|
54
|
+
dropdrop ./images -n 5
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
### Basic Commands
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Run pipeline with compact settings
|
|
63
|
+
dropdrop ./images --settings "d=1000,p=on,c=6.5e5,l=experiment1"
|
|
64
|
+
|
|
65
|
+
# Custom output directory
|
|
66
|
+
dropdrop ./images ./results/my_project --settings "d=500"
|
|
67
|
+
|
|
68
|
+
# Interactive viewer (view results after processing)
|
|
69
|
+
dropdrop ./images --view
|
|
70
|
+
|
|
71
|
+
# Interactive editor (manually correct inclusions)
|
|
72
|
+
dropdrop ./images --interactive
|
|
73
|
+
|
|
74
|
+
# Archive output as tar.gz
|
|
75
|
+
dropdrop ./images -z
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Settings Format
|
|
79
|
+
|
|
80
|
+
Compact settings string: `d=dilution,p=poisson,c=count,l=label`
|
|
81
|
+
|
|
82
|
+
| Key | Full name | Description | Default |
|
|
83
|
+
|-----|-----------|-------------|---------|
|
|
84
|
+
| `d` | `dilution` | Dilution factor | 500 |
|
|
85
|
+
| `p` | `poisson` | Enable Poisson analysis (on/off) | on |
|
|
86
|
+
| `c` | `count` | Stock bead count per uL | 6.5e5 |
|
|
87
|
+
| `l` | `label` | Project label for output naming | None |
|
|
88
|
+
|
|
89
|
+
### Cache Control
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
dropdrop ./images --no-cache # Disable caching
|
|
93
|
+
dropdrop ./images --clear-cache # Clear cache before run
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Interactive Editor
|
|
97
|
+
|
|
98
|
+
The editor allows manual correction of detected inclusions:
|
|
99
|
+
|
|
100
|
+
| Key | Action |
|
|
101
|
+
|-----|--------|
|
|
102
|
+
| Left-click | Add inclusion |
|
|
103
|
+
| Right-click (hold) | Remove inclusions |
|
|
104
|
+
| `s` | Toggle droplet selection (hover over droplet) |
|
|
105
|
+
| `u` | Undo last action |
|
|
106
|
+
| `c` | Clear all inclusions in frame |
|
|
107
|
+
| `d` | Toggle droplet visibility |
|
|
108
|
+
| Arrow keys / Space | Navigate frames |
|
|
109
|
+
| `q` / Esc | Exit |
|
|
110
|
+
|
|
111
|
+
Disabled droplets (gray with X) are excluded from the final results.
|
|
112
|
+
|
|
113
|
+
## Output Structure
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
results/<YYYYMMDD>_<label>/
|
|
117
|
+
data.csv # Raw detection data
|
|
118
|
+
summary.txt # Settings and statistics
|
|
119
|
+
size_distribution.png # Droplet diameter histogram
|
|
120
|
+
poisson_comparison.png # Bead distribution vs theoretical
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### data.csv columns
|
|
124
|
+
|
|
125
|
+
| Column | Description |
|
|
126
|
+
|--------|-------------|
|
|
127
|
+
| `frame` | Frame index |
|
|
128
|
+
| `droplet_id` | Droplet ID within frame |
|
|
129
|
+
| `center_x`, `center_y` | Droplet center coordinates (px) |
|
|
130
|
+
| `diameter_px`, `diameter_um` | Droplet diameter |
|
|
131
|
+
| `area_px`, `area_um2` | Droplet area |
|
|
132
|
+
| `inclusions` | Number of inclusions detected |
|
|
133
|
+
|
|
134
|
+
## Configuration
|
|
135
|
+
|
|
136
|
+
Create `config.json` in your working directory to customize detection parameters:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"cellpose_flow_threshold": 0.4,
|
|
141
|
+
"cellpose_cellprob_threshold": 0.0,
|
|
142
|
+
"erosion_pixels": 5,
|
|
143
|
+
"kernel_size": 7,
|
|
144
|
+
"tophat_threshold": 30,
|
|
145
|
+
"min_inclusion_area": 7,
|
|
146
|
+
"max_inclusion_area": 50,
|
|
147
|
+
"edge_buffer": 5,
|
|
148
|
+
"min_droplet_diameter": 80,
|
|
149
|
+
"max_droplet_diameter": 200,
|
|
150
|
+
"px_to_um": 1.14,
|
|
151
|
+
"cache": {
|
|
152
|
+
"enabled": true,
|
|
153
|
+
"max_frames": 100
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Parameters
|
|
159
|
+
|
|
160
|
+
| Parameter | Description |
|
|
161
|
+
|-----------|-------------|
|
|
162
|
+
| `cellpose_flow_threshold` | Cellpose flow threshold for segmentation |
|
|
163
|
+
| `cellpose_cellprob_threshold` | Cellpose cell probability threshold |
|
|
164
|
+
| `erosion_pixels` | Pixels to erode droplet mask before inclusion detection |
|
|
165
|
+
| `kernel_size` | Morphological kernel size for black-hat transform |
|
|
166
|
+
| `tophat_threshold` | Threshold for inclusion detection |
|
|
167
|
+
| `min/max_inclusion_area` | Inclusion size constraints (px) |
|
|
168
|
+
| `edge_buffer` | Buffer from image edge to ignore inclusions |
|
|
169
|
+
| `min/max_droplet_diameter` | Droplet size constraints (px) |
|
|
170
|
+
| `px_to_um` | Pixel to micrometer conversion factor |
|
|
171
|
+
|
|
172
|
+
## Requirements
|
|
173
|
+
|
|
174
|
+
- Python 3.12+
|
|
175
|
+
- CUDA-capable GPU (recommended for Cellpose)
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
dropdrop/__init__.py,sha256=R1qfIhy13MXKmrJ-PNbHruI7INNJ49FYp4IAI1ytnFc,408
|
|
2
|
+
dropdrop/cache.py,sha256=T7BMikM35239SOBWQr3hLjNvYYgnxUuRO3H1GFl1qaY,4931
|
|
3
|
+
dropdrop/cli.py,sha256=FWjYRXHvRgGZhN9Ajo8lLwCva9eeOOQPrgQeXU3hHnc,7714
|
|
4
|
+
dropdrop/config.py,sha256=U-9cKd76naOlQQm1vxO5XV3DDMQtopGYBXVKmzuv_mw,1893
|
|
5
|
+
dropdrop/pipeline.py,sha256=RSSsXWfEZCqioWdeLqbnkePw4VHzCTF-iwGYZhHS8rI,14106
|
|
6
|
+
dropdrop/stats.py,sha256=8ahnu8lChxhXGNPHIBjR7nr2OtrV0T60mx03zJF4EtY,10084
|
|
7
|
+
dropdrop/ui.py,sha256=KBQvzxx_Z3zJgYTif7yoGLJ40aKY1DnHY5MMpwLtFHw,16863
|
|
8
|
+
dropdrop-1.1.0.dist-info/METADATA,sha256=ChKsLzHrZNKGNj9FDMomt1nPtU374vX3g5HVNXWYWpQ,4568
|
|
9
|
+
dropdrop-1.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
10
|
+
dropdrop-1.1.0.dist-info/entry_points.txt,sha256=-XsWGgKgxvNMVTfcYrWGbaHHBI7Nm89hKtCmCC68YjM,47
|
|
11
|
+
dropdrop-1.1.0.dist-info/licenses/LICENSE,sha256=ou8INuvxf2J0kvMwEWYadOefpGT_YvuUZ-vp5fgzXtg,1074
|
|
12
|
+
dropdrop-1.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Oleksii Stroganov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|