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/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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dropdrop = dropdrop.cli:main
@@ -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.