canns 0.13.1__py3-none-any.whl → 0.14.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.
Files changed (99) hide show
  1. canns/analyzer/data/__init__.py +5 -1
  2. canns/analyzer/data/asa/__init__.py +27 -12
  3. canns/analyzer/data/asa/cohospace.py +336 -10
  4. canns/analyzer/data/asa/config.py +3 -0
  5. canns/analyzer/data/asa/embedding.py +48 -45
  6. canns/analyzer/data/asa/path.py +104 -2
  7. canns/analyzer/data/asa/plotting.py +88 -19
  8. canns/analyzer/data/asa/tda.py +11 -4
  9. canns/analyzer/data/cell_classification/__init__.py +97 -0
  10. canns/analyzer/data/cell_classification/core/__init__.py +26 -0
  11. canns/analyzer/data/cell_classification/core/grid_cells.py +633 -0
  12. canns/analyzer/data/cell_classification/core/grid_modules_leiden.py +288 -0
  13. canns/analyzer/data/cell_classification/core/head_direction.py +347 -0
  14. canns/analyzer/data/cell_classification/core/spatial_analysis.py +431 -0
  15. canns/analyzer/data/cell_classification/io/__init__.py +5 -0
  16. canns/analyzer/data/cell_classification/io/matlab_loader.py +417 -0
  17. canns/analyzer/data/cell_classification/utils/__init__.py +39 -0
  18. canns/analyzer/data/cell_classification/utils/circular_stats.py +383 -0
  19. canns/analyzer/data/cell_classification/utils/correlation.py +318 -0
  20. canns/analyzer/data/cell_classification/utils/geometry.py +442 -0
  21. canns/analyzer/data/cell_classification/utils/image_processing.py +416 -0
  22. canns/analyzer/data/cell_classification/visualization/__init__.py +19 -0
  23. canns/analyzer/data/cell_classification/visualization/grid_plots.py +292 -0
  24. canns/analyzer/data/cell_classification/visualization/hd_plots.py +200 -0
  25. canns/analyzer/metrics/__init__.py +2 -1
  26. canns/analyzer/visualization/core/config.py +46 -4
  27. canns/data/__init__.py +6 -1
  28. canns/data/datasets.py +154 -1
  29. canns/data/loaders.py +37 -0
  30. canns/pipeline/__init__.py +13 -9
  31. canns/pipeline/__main__.py +6 -0
  32. canns/pipeline/asa/runner.py +105 -41
  33. canns/pipeline/asa_gui/__init__.py +68 -0
  34. canns/pipeline/asa_gui/__main__.py +6 -0
  35. canns/pipeline/asa_gui/analysis_modes/__init__.py +42 -0
  36. canns/pipeline/asa_gui/analysis_modes/base.py +39 -0
  37. canns/pipeline/asa_gui/analysis_modes/batch_mode.py +21 -0
  38. canns/pipeline/asa_gui/analysis_modes/cohomap_mode.py +56 -0
  39. canns/pipeline/asa_gui/analysis_modes/cohospace_mode.py +194 -0
  40. canns/pipeline/asa_gui/analysis_modes/decode_mode.py +52 -0
  41. canns/pipeline/asa_gui/analysis_modes/fr_mode.py +81 -0
  42. canns/pipeline/asa_gui/analysis_modes/frm_mode.py +92 -0
  43. canns/pipeline/asa_gui/analysis_modes/gridscore_mode.py +123 -0
  44. canns/pipeline/asa_gui/analysis_modes/pathcompare_mode.py +199 -0
  45. canns/pipeline/asa_gui/analysis_modes/tda_mode.py +112 -0
  46. canns/pipeline/asa_gui/app.py +29 -0
  47. canns/pipeline/asa_gui/controllers/__init__.py +6 -0
  48. canns/pipeline/asa_gui/controllers/analysis_controller.py +59 -0
  49. canns/pipeline/asa_gui/controllers/preprocess_controller.py +89 -0
  50. canns/pipeline/asa_gui/core/__init__.py +15 -0
  51. canns/pipeline/asa_gui/core/cache.py +14 -0
  52. canns/pipeline/asa_gui/core/runner.py +1936 -0
  53. canns/pipeline/asa_gui/core/state.py +324 -0
  54. canns/pipeline/asa_gui/core/worker.py +260 -0
  55. canns/pipeline/asa_gui/main_window.py +184 -0
  56. canns/pipeline/asa_gui/models/__init__.py +7 -0
  57. canns/pipeline/asa_gui/models/config.py +14 -0
  58. canns/pipeline/asa_gui/models/job.py +31 -0
  59. canns/pipeline/asa_gui/models/presets.py +21 -0
  60. canns/pipeline/asa_gui/resources/__init__.py +16 -0
  61. canns/pipeline/asa_gui/resources/dark.qss +167 -0
  62. canns/pipeline/asa_gui/resources/light.qss +163 -0
  63. canns/pipeline/asa_gui/resources/styles.qss +130 -0
  64. canns/pipeline/asa_gui/utils/__init__.py +1 -0
  65. canns/pipeline/asa_gui/utils/formatters.py +15 -0
  66. canns/pipeline/asa_gui/utils/io_adapters.py +40 -0
  67. canns/pipeline/asa_gui/utils/validators.py +41 -0
  68. canns/pipeline/asa_gui/views/__init__.py +1 -0
  69. canns/pipeline/asa_gui/views/help_content.py +171 -0
  70. canns/pipeline/asa_gui/views/pages/__init__.py +6 -0
  71. canns/pipeline/asa_gui/views/pages/analysis_page.py +565 -0
  72. canns/pipeline/asa_gui/views/pages/preprocess_page.py +492 -0
  73. canns/pipeline/asa_gui/views/panels/__init__.py +1 -0
  74. canns/pipeline/asa_gui/views/widgets/__init__.py +21 -0
  75. canns/pipeline/asa_gui/views/widgets/artifacts_tab.py +44 -0
  76. canns/pipeline/asa_gui/views/widgets/drop_zone.py +80 -0
  77. canns/pipeline/asa_gui/views/widgets/file_list.py +27 -0
  78. canns/pipeline/asa_gui/views/widgets/gridscore_tab.py +308 -0
  79. canns/pipeline/asa_gui/views/widgets/help_dialog.py +27 -0
  80. canns/pipeline/asa_gui/views/widgets/image_tab.py +50 -0
  81. canns/pipeline/asa_gui/views/widgets/image_viewer.py +97 -0
  82. canns/pipeline/asa_gui/views/widgets/log_box.py +16 -0
  83. canns/pipeline/asa_gui/views/widgets/pathcompare_tab.py +200 -0
  84. canns/pipeline/asa_gui/views/widgets/popup_combo.py +25 -0
  85. canns/pipeline/gallery/__init__.py +15 -5
  86. canns/pipeline/gallery/__main__.py +11 -0
  87. canns/pipeline/gallery/app.py +705 -0
  88. canns/pipeline/gallery/runner.py +790 -0
  89. canns/pipeline/gallery/state.py +51 -0
  90. canns/pipeline/gallery/styles.tcss +123 -0
  91. canns/pipeline/launcher.py +81 -0
  92. {canns-0.13.1.dist-info → canns-0.14.0.dist-info}/METADATA +11 -1
  93. canns-0.14.0.dist-info/RECORD +163 -0
  94. canns-0.14.0.dist-info/entry_points.txt +5 -0
  95. canns/pipeline/_base.py +0 -50
  96. canns-0.13.1.dist-info/RECORD +0 -89
  97. canns-0.13.1.dist-info/entry_points.txt +0 -3
  98. {canns-0.13.1.dist-info → canns-0.14.0.dist-info}/WHEEL +0 -0
  99. {canns-0.13.1.dist-info → canns-0.14.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,416 @@
1
+ """
2
+ Image Processing Utilities
3
+
4
+ Functions for image manipulation including rotation, filtering, and morphological operations.
5
+ """
6
+
7
+ import numpy as np
8
+ from scipy import ndimage
9
+ from skimage import measure, morphology
10
+
11
+
12
+ def rotate_image(
13
+ image: np.ndarray,
14
+ angle: float,
15
+ output_shape: tuple[int, int] | None = None,
16
+ method: str = "bilinear",
17
+ preserve_range: bool = True,
18
+ ) -> np.ndarray:
19
+ """
20
+ Rotate an image by a given angle.
21
+
22
+ Parameters
23
+ ----------
24
+ image : np.ndarray
25
+ 2D image array to rotate
26
+ angle : float
27
+ Rotation angle in degrees. Positive values rotate counter-clockwise.
28
+ output_shape : tuple of int, optional
29
+ Shape of the output image (height, width). If None, uses input shape.
30
+ method : str, optional
31
+ Interpolation method: 'bilinear' (default), 'nearest', 'cubic'
32
+ preserve_range : bool, optional
33
+ Whether to preserve the original value range. Default is True.
34
+
35
+ Returns
36
+ -------
37
+ rotated : np.ndarray
38
+ Rotated image
39
+
40
+ Examples
41
+ --------
42
+ >>> image = np.random.rand(50, 50)
43
+ >>> rotated = rotate_image(image, 30) # Rotate 30 degrees CCW
44
+ >>> rotated_90 = rotate_image(image, 90) # Rotate 90 degrees
45
+
46
+ Notes
47
+ -----
48
+ Based on MATLAB's imrotate function. Uses scipy.ndimage.rotate.
49
+ The rotation is performed around the center of the image.
50
+ """
51
+ # Map method names to scipy orders
52
+ order_map = {"nearest": 0, "bilinear": 1, "cubic": 3}
53
+ order = order_map.get(method.lower(), 1)
54
+
55
+ # Rotate image (scipy rotates counter-clockwise for positive angles, same as MATLAB)
56
+ rotated = ndimage.rotate(
57
+ image,
58
+ angle,
59
+ order=order,
60
+ reshape=False if output_shape else True,
61
+ mode="constant",
62
+ cval=0.0,
63
+ prefilter=True,
64
+ )
65
+
66
+ # Resize to requested output shape if specified
67
+ if output_shape is not None:
68
+ if rotated.shape != output_shape:
69
+ # Simple crop/pad to match output shape
70
+ h, w = rotated.shape
71
+ oh, ow = output_shape
72
+
73
+ # Calculate center offsets
74
+ start_h = (h - oh) // 2 if h > oh else 0
75
+ start_w = (w - ow) // 2 if w > ow else 0
76
+
77
+ if h >= oh and w >= ow:
78
+ # Crop
79
+ rotated = rotated[start_h : start_h + oh, start_w : start_w + ow]
80
+ elif h <= oh and w <= ow:
81
+ # Pad
82
+ pad_h = (oh - h) // 2
83
+ pad_w = (ow - w) // 2
84
+ rotated = np.pad(
85
+ rotated,
86
+ ((pad_h, oh - h - pad_h), (pad_w, ow - w - pad_w)),
87
+ mode="constant",
88
+ constant_values=0,
89
+ )
90
+ else:
91
+ # Mixed crop/pad - just use zoom
92
+ from scipy.ndimage import zoom
93
+
94
+ zoom_factors = (oh / h, ow / w)
95
+ rotated = zoom(rotated, zoom_factors, order=order)
96
+
97
+ return rotated
98
+
99
+
100
+ def find_regional_maxima(
101
+ image: np.ndarray, connectivity: int = 1, allow_diagonal: bool = False
102
+ ) -> np.ndarray:
103
+ """
104
+ Find regional maxima in an image.
105
+
106
+ A regional maximum is a connected component of pixels with the same value,
107
+ surrounded by pixels with strictly lower values.
108
+
109
+ Parameters
110
+ ----------
111
+ image : np.ndarray
112
+ 2D input image
113
+ connectivity : int, optional
114
+ Connectivity for defining neighbors:
115
+ - 1: 4-connectivity (default, equivalent to MATLAB connectivity=4)
116
+ - 2: 8-connectivity (equivalent to MATLAB connectivity=8)
117
+ allow_diagonal : bool, optional
118
+ If True, uses 8-connectivity. If False, uses 4-connectivity.
119
+ Overrides connectivity parameter if specified.
120
+
121
+ Returns
122
+ -------
123
+ maxima : np.ndarray
124
+ Binary image where True indicates regional maxima
125
+
126
+ Examples
127
+ --------
128
+ >>> # Create image with some peaks
129
+ >>> x = np.linspace(-3, 3, 50)
130
+ >>> xx, yy = np.meshgrid(x, x)
131
+ >>> image = np.exp(-(xx**2 + yy**2)) + 0.5 * np.exp(-((xx-1.5)**2 + (yy-1.5)**2))
132
+ >>> maxima = find_regional_maxima(image)
133
+ >>> print(f"Found {np.sum(maxima)} maxima")
134
+
135
+ Notes
136
+ -----
137
+ Based on MATLAB's imregionalmax function.
138
+
139
+ IMPORTANT: Connectivity mapping differs between MATLAB and Python!
140
+ - MATLAB imregionalmax(image, 4) → Python connectivity=1
141
+ - MATLAB imregionalmax(image, 8) → Python connectivity=2
142
+
143
+ Uses skimage.morphology.local_maxima for detection.
144
+ """
145
+ # Handle connectivity parameter
146
+ if allow_diagonal:
147
+ connectivity = 2
148
+
149
+ # Use skimage to find local maxima
150
+ # Note: skimage uses different connectivity convention than MATLAB
151
+ maxima = morphology.local_maxima(image, connectivity=connectivity)
152
+
153
+ return maxima
154
+
155
+
156
+ def find_contours_at_level(image: np.ndarray, level: float) -> list:
157
+ """
158
+ Find contours in an image at a specific threshold level.
159
+
160
+ Parameters
161
+ ----------
162
+ image : np.ndarray
163
+ 2D input image
164
+ level : float
165
+ Threshold level for contour detection
166
+
167
+ Returns
168
+ -------
169
+ contours : list of np.ndarray
170
+ List of contours. Each contour is an (N, 2) array of (row, col) coordinates.
171
+ Note: Returns (row, col) = (y, x), opposite of MATLAB's (x, y) order!
172
+
173
+ Examples
174
+ --------
175
+ >>> # Create a simple image with a circular feature
176
+ >>> x = np.linspace(-5, 5, 100)
177
+ >>> xx, yy = np.meshgrid(x, x)
178
+ >>> image = np.exp(-(xx**2 + yy**2))
179
+ >>> contours = find_contours_at_level(image, 0.5)
180
+ >>> print(f"Found {len(contours)} contours")
181
+
182
+ Notes
183
+ -----
184
+ Based on MATLAB's contourc function.
185
+ Uses skimage.measure.find_contours.
186
+
187
+ CRITICAL: Coordinate order difference!
188
+ - MATLAB contourc: returns [x; y] (column major)
189
+ - Python find_contours: returns (row, col) = (y, x)
190
+
191
+ For gridness analysis, this coordinate swap must be handled!
192
+ """
193
+ contours = measure.find_contours(image, level)
194
+ return contours
195
+
196
+
197
+ def gaussian_filter_2d(
198
+ image: np.ndarray, sigma: float, mode: str = "reflect", truncate: float = 4.0
199
+ ) -> np.ndarray:
200
+ """
201
+ Apply 2D Gaussian filter to an image.
202
+
203
+ Parameters
204
+ ----------
205
+ image : np.ndarray
206
+ 2D input image
207
+ sigma : float
208
+ Standard deviation of Gaussian kernel
209
+ mode : str, optional
210
+ Boundary handling mode:
211
+ - 'reflect' (default): reflect at boundaries
212
+ - 'constant': pad with zeros
213
+ - 'nearest': replicate edge values
214
+ - 'mirror': mirror at boundaries
215
+ - 'wrap': wrap around
216
+ truncate : float, optional
217
+ Truncate filter at this many standard deviations. Default is 4.0.
218
+
219
+ Returns
220
+ -------
221
+ filtered : np.ndarray
222
+ Filtered image
223
+
224
+ Examples
225
+ --------
226
+ >>> image = np.random.rand(100, 100)
227
+ >>> smoothed = gaussian_filter_2d(image, sigma=2.0)
228
+
229
+ Notes
230
+ -----
231
+ Based on MATLAB's imgaussfilt function.
232
+ Uses scipy.ndimage.gaussian_filter.
233
+ """
234
+ filtered = ndimage.gaussian_filter(image, sigma=sigma, mode=mode, truncate=truncate)
235
+ return filtered
236
+
237
+
238
+ def dilate_image(
239
+ image: np.ndarray,
240
+ footprint: np.ndarray | None = None,
241
+ selem_type: str = "square",
242
+ selem_size: int = 3,
243
+ ) -> np.ndarray:
244
+ """
245
+ Perform morphological dilation on a binary image.
246
+
247
+ Parameters
248
+ ----------
249
+ image : np.ndarray
250
+ Binary input image
251
+ footprint : np.ndarray, optional
252
+ Structuring element. If None, uses selem_type and selem_size.
253
+ selem_type : str, optional
254
+ Type of structuring element: 'square', 'disk', 'diamond'
255
+ Default is 'square'.
256
+ selem_size : int, optional
257
+ Size of structuring element. Default is 3.
258
+
259
+ Returns
260
+ -------
261
+ dilated : np.ndarray
262
+ Dilated image
263
+
264
+ Examples
265
+ --------
266
+ >>> binary_image = (np.random.rand(50, 50) > 0.8)
267
+ >>> dilated = dilate_image(binary_image, selem_type='square', selem_size=3)
268
+
269
+ Notes
270
+ -----
271
+ Based on MATLAB's imdilate function.
272
+ Uses skimage.morphology.dilation.
273
+ """
274
+ if footprint is None:
275
+ if selem_type == "square":
276
+ footprint = morphology.footprint_rectangle((selem_size, selem_size))
277
+ elif selem_type == "disk":
278
+ footprint = morphology.disk(selem_size)
279
+ elif selem_type == "diamond":
280
+ footprint = morphology.diamond(selem_size)
281
+ else:
282
+ raise ValueError(f"Unknown structuring element type: {selem_type}")
283
+
284
+ dilated = morphology.binary_dilation(image, footprint=footprint)
285
+ return dilated
286
+
287
+
288
+ def label_connected_components(
289
+ binary_image: np.ndarray, connectivity: int = 2
290
+ ) -> tuple[np.ndarray, int]:
291
+ """
292
+ Label connected components in a binary image.
293
+
294
+ Parameters
295
+ ----------
296
+ binary_image : np.ndarray
297
+ Binary input image
298
+ connectivity : int, optional
299
+ Connectivity for defining neighbors:
300
+ - 1: 4-connectivity
301
+ - 2: 8-connectivity (default)
302
+
303
+ Returns
304
+ -------
305
+ labels : np.ndarray
306
+ Labeled image where each connected component has a unique integer label
307
+ num_labels : int
308
+ Number of connected components found
309
+
310
+ Examples
311
+ --------
312
+ >>> binary = (np.random.rand(50, 50) > 0.7)
313
+ >>> labels, n = label_connected_components(binary)
314
+ >>> print(f"Found {n} connected components")
315
+
316
+ Notes
317
+ -----
318
+ Based on MATLAB's bwconncomp function.
319
+ Uses skimage.measure.label.
320
+ """
321
+ labels = measure.label(binary_image, connectivity=connectivity)
322
+ num_labels = labels.max()
323
+ return labels, num_labels
324
+
325
+
326
+ def regionprops(labeled_image: np.ndarray, intensity_image: np.ndarray | None = None) -> list:
327
+ """
328
+ Measure properties of labeled image regions.
329
+
330
+ Parameters
331
+ ----------
332
+ labeled_image : np.ndarray
333
+ Labeled image (output from label_connected_components)
334
+ intensity_image : np.ndarray, optional
335
+ Intensity image for computing intensity-based properties
336
+
337
+ Returns
338
+ -------
339
+ properties : list of RegionProperties
340
+ List of region property objects. Each object has attributes like:
341
+ - centroid: (row, col) of region center
342
+ - area: number of pixels in region
343
+ - bbox: bounding box coordinates
344
+ - etc.
345
+
346
+ Examples
347
+ --------
348
+ >>> binary = (np.random.rand(50, 50) > 0.7)
349
+ >>> labels, _ = label_connected_components(binary)
350
+ >>> props = regionprops(labels)
351
+ >>> for prop in props:
352
+ ... print(f"Region at {prop.centroid}, area={prop.area}")
353
+
354
+ Notes
355
+ -----
356
+ Based on MATLAB's regionprops function.
357
+ Uses skimage.measure.regionprops.
358
+ """
359
+ props = measure.regionprops(labeled_image, intensity_image=intensity_image)
360
+ return props
361
+
362
+
363
+ if __name__ == "__main__":
364
+ # Simple tests
365
+ print("Testing image processing functions...")
366
+
367
+ # Test 1: Image rotation
368
+ print("\nTest 1 - Image rotation:")
369
+ image = np.random.rand(50, 50)
370
+ rotated_30 = rotate_image(image, 30)
371
+ rotated_90 = rotate_image(image, 90)
372
+ print(f" Original shape: {image.shape}")
373
+ print(f" Rotated 30° shape: {rotated_30.shape}")
374
+ print(f" Rotated 90° shape: {rotated_90.shape}")
375
+
376
+ # Test 2: Regional maxima
377
+ print("\nTest 2 - Regional maxima:")
378
+ x = np.linspace(-3, 3, 50)
379
+ xx, yy = np.meshgrid(x, x)
380
+ # Two Gaussian peaks
381
+ peaks = np.exp(-(xx**2 + yy**2)) + 0.5 * np.exp(-((xx - 1.5) ** 2 + (yy - 1.5) ** 2))
382
+ maxima = find_regional_maxima(peaks)
383
+ print(f" Found {np.sum(maxima)} maxima")
384
+ print(" Maxima locations:")
385
+ coords = np.argwhere(maxima)
386
+ for i, (y, x) in enumerate(coords[:5]): # Show first 5
387
+ print(f" Peak {i + 1}: ({x}, {y}), value={peaks[y, x]:.3f}")
388
+
389
+ # Test 3: Contour detection
390
+ print("\nTest 3 - Contour detection:")
391
+ circle = np.sqrt(xx**2 + yy**2)
392
+ contours = find_contours_at_level(circle, 1.5)
393
+ print(f" Found {len(contours)} contours at level 1.5")
394
+ if len(contours) > 0:
395
+ print(f" Largest contour has {len(contours[0])} points")
396
+
397
+ # Test 4: Gaussian filtering
398
+ print("\nTest 4 - Gaussian filtering:")
399
+ noisy = image + 0.1 * np.random.randn(*image.shape)
400
+ smoothed = gaussian_filter_2d(noisy, sigma=2.0)
401
+ print(f" Noisy image std: {np.std(noisy):.4f}")
402
+ print(f" Smoothed image std: {np.std(smoothed):.4f}")
403
+
404
+ # Test 5: Connected components
405
+ print("\nTest 5 - Connected components:")
406
+ binary = np.random.rand(50, 50) > 0.7
407
+ labels, n = label_connected_components(binary)
408
+ print(f" Binary image has {np.sum(binary)} True pixels")
409
+ print(f" Found {n} connected components")
410
+
411
+ props = regionprops(labels)
412
+ print(" Region properties for first 3 components:")
413
+ for i, prop in enumerate(props[:3]):
414
+ print(f" Region {i + 1}: centroid={prop.centroid}, area={prop.area}")
415
+
416
+ print("\nAll tests completed!")
@@ -0,0 +1,19 @@
1
+ """Visualization modules."""
2
+
3
+ from .grid_plots import (
4
+ plot_autocorrelogram,
5
+ plot_grid_score_histogram,
6
+ plot_gridness_analysis,
7
+ plot_rate_map,
8
+ )
9
+ from .hd_plots import plot_hd_analysis, plot_polar_tuning, plot_temporal_autocorr
10
+
11
+ __all__ = [
12
+ "plot_autocorrelogram",
13
+ "plot_gridness_analysis",
14
+ "plot_rate_map",
15
+ "plot_grid_score_histogram",
16
+ "plot_polar_tuning",
17
+ "plot_temporal_autocorr",
18
+ "plot_hd_analysis",
19
+ ]