objscale 1.2.0__tar.gz → 1.3.0__tar.gz
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.
- {objscale-1.2.0 → objscale-1.3.0}/PKG-INFO +6 -6
- {objscale-1.2.0 → objscale-1.3.0}/README.md +5 -5
- {objscale-1.2.0 → objscale-1.3.0}/objscale/__init__.py +3 -3
- {objscale-1.2.0 → objscale-1.3.0}/objscale/_fractal_dimensions.py +31 -9
- {objscale-1.2.0 → objscale-1.3.0}/objscale/_object_analysis.py +163 -143
- {objscale-1.2.0 → objscale-1.3.0}/objscale/_size_distributions.py +13 -5
- {objscale-1.2.0 → objscale-1.3.0}/objscale.egg-info/PKG-INFO +6 -6
- {objscale-1.2.0 → objscale-1.3.0}/pyproject.toml +1 -1
- {objscale-1.2.0 → objscale-1.3.0}/LICENSE +0 -0
- {objscale-1.2.0 → objscale-1.3.0}/objscale/_utils.py +0 -0
- {objscale-1.2.0 → objscale-1.3.0}/objscale.egg-info/SOURCES.txt +0 -0
- {objscale-1.2.0 → objscale-1.3.0}/objscale.egg-info/dependency_links.txt +0 -0
- {objscale-1.2.0 → objscale-1.3.0}/objscale.egg-info/requires.txt +0 -0
- {objscale-1.2.0 → objscale-1.3.0}/objscale.egg-info/top_level.txt +0 -0
- {objscale-1.2.0 → objscale-1.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: objscale
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Object-based analysis functions for fractal dimensions and size distributions
|
|
5
5
|
Author-email: Thomas DeWitt <thomas.dewitt@utah.edu>
|
|
6
6
|
License: MIT
|
|
@@ -162,10 +162,11 @@ ind_dim, ind_error = objscale.individual_fractal_dimension(arrays)
|
|
|
162
162
|
|
|
163
163
|
### Object Analysis
|
|
164
164
|
|
|
165
|
-
- `
|
|
166
|
-
- `
|
|
167
|
-
- `
|
|
168
|
-
- `
|
|
165
|
+
- `label_structures` - Label connected components (wraps scipy.ndimage.label with NaN handling and periodic boundaries)
|
|
166
|
+
- `get_structure_areas` - Calculate areas of labelled structures (O(n), fast)
|
|
167
|
+
- `get_structure_perimeters` - Calculate perimeters of labelled structures (O(n), fast)
|
|
168
|
+
- `get_structure_height_width` - Calculate height and width of labelled structures
|
|
169
|
+
- `get_structure_props` - Calculate perimeter, area, width, height from a binary array (convenience wrapper)
|
|
169
170
|
- `get_every_boundary_perimeter` - Perimeters of every boundary including nested holes
|
|
170
171
|
- `total_perimeter` - Total perimeter of all objects
|
|
171
172
|
- `total_number` - Count number of structures
|
|
@@ -173,7 +174,6 @@ ind_dim, ind_error = objscale.individual_fractal_dimension(arrays)
|
|
|
173
174
|
- `remove_structures_touching_border_nan` - Remove border-touching structures
|
|
174
175
|
- `remove_structure_holes` - Fill holes in structures
|
|
175
176
|
- `label_size` - Label each structure with its size value
|
|
176
|
-
- `label_periodic_boundaries` - Merge labels across periodic boundaries
|
|
177
177
|
- `clear_border_adjacent` - Clear structures touching array edges
|
|
178
178
|
|
|
179
179
|
### Utilities
|
|
@@ -121,10 +121,11 @@ ind_dim, ind_error = objscale.individual_fractal_dimension(arrays)
|
|
|
121
121
|
|
|
122
122
|
### Object Analysis
|
|
123
123
|
|
|
124
|
-
- `
|
|
125
|
-
- `
|
|
126
|
-
- `
|
|
127
|
-
- `
|
|
124
|
+
- `label_structures` - Label connected components (wraps scipy.ndimage.label with NaN handling and periodic boundaries)
|
|
125
|
+
- `get_structure_areas` - Calculate areas of labelled structures (O(n), fast)
|
|
126
|
+
- `get_structure_perimeters` - Calculate perimeters of labelled structures (O(n), fast)
|
|
127
|
+
- `get_structure_height_width` - Calculate height and width of labelled structures
|
|
128
|
+
- `get_structure_props` - Calculate perimeter, area, width, height from a binary array (convenience wrapper)
|
|
128
129
|
- `get_every_boundary_perimeter` - Perimeters of every boundary including nested holes
|
|
129
130
|
- `total_perimeter` - Total perimeter of all objects
|
|
130
131
|
- `total_number` - Count number of structures
|
|
@@ -132,7 +133,6 @@ ind_dim, ind_error = objscale.individual_fractal_dimension(arrays)
|
|
|
132
133
|
- `remove_structures_touching_border_nan` - Remove border-touching structures
|
|
133
134
|
- `remove_structure_holes` - Fill holes in structures
|
|
134
135
|
- `label_size` - Label each structure with its size value
|
|
135
|
-
- `label_periodic_boundaries` - Merge labels across periodic boundaries
|
|
136
136
|
- `clear_border_adjacent` - Clear structures touching array edges
|
|
137
137
|
|
|
138
138
|
### Utilities
|
|
@@ -20,13 +20,13 @@ from ._fractal_dimensions import (
|
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
from ._object_analysis import (
|
|
23
|
+
label_structures,
|
|
23
24
|
get_structure_props,
|
|
24
25
|
get_structure_areas,
|
|
25
26
|
get_structure_perimeters,
|
|
26
27
|
get_structure_height_width,
|
|
27
28
|
get_every_boundary_perimeter,
|
|
28
29
|
remove_structures_touching_border_nan,
|
|
29
|
-
label_periodic_boundaries,
|
|
30
30
|
remove_structure_holes,
|
|
31
31
|
clear_border_adjacent,
|
|
32
32
|
)
|
|
@@ -58,7 +58,7 @@ __all__ = [
|
|
|
58
58
|
'get_structure_height_width',
|
|
59
59
|
'get_every_boundary_perimeter',
|
|
60
60
|
'remove_structures_touching_border_nan',
|
|
61
|
-
'
|
|
61
|
+
'label_structures',
|
|
62
62
|
'remove_structure_holes',
|
|
63
63
|
'clear_border_adjacent',
|
|
64
64
|
'linear_regression',
|
|
@@ -66,7 +66,7 @@ __all__ = [
|
|
|
66
66
|
'set_num_threads',
|
|
67
67
|
]
|
|
68
68
|
|
|
69
|
-
__version__ = "1.
|
|
69
|
+
__version__ = "1.3.0"
|
|
70
70
|
__author__ = "Thomas DeWitt"
|
|
71
71
|
__email__ = "thomas.dewitt@utah.edu"
|
|
72
72
|
__description__ = "Object-based analysis functions for fractal dimensions and size distributions"
|
|
@@ -7,11 +7,12 @@ from numba.typed import List
|
|
|
7
7
|
from scipy.ndimage import label
|
|
8
8
|
from warnings import warn
|
|
9
9
|
from ._object_analysis import (
|
|
10
|
+
label_structures,
|
|
11
|
+
_merge_periodic_labels,
|
|
10
12
|
remove_structures_touching_border_nan,
|
|
11
13
|
remove_structure_holes,
|
|
12
14
|
get_structure_areas,
|
|
13
15
|
get_structure_perimeters,
|
|
14
|
-
label_periodic_boundaries,
|
|
15
16
|
)
|
|
16
17
|
from ._utils import linear_regression, encase_in_value
|
|
17
18
|
|
|
@@ -233,6 +234,7 @@ def individual_correlation_dimension(
|
|
|
233
234
|
return_C_l: bool = False,
|
|
234
235
|
point_reduction_factor: float = 1,
|
|
235
236
|
nbins: int = 50,
|
|
237
|
+
filled: bool = True,
|
|
236
238
|
) -> tuple[float, float] | tuple[float, float, NDArray, NDArray]:
|
|
237
239
|
"""
|
|
238
240
|
Calculate the correlation dimension of the Nth largest structure in an array.
|
|
@@ -262,6 +264,10 @@ def individual_correlation_dimension(
|
|
|
262
264
|
return_C_l : bool, default=False
|
|
263
265
|
If True, return dimension, error, bins, C_l. Otherwise, return dimension,
|
|
264
266
|
error.
|
|
267
|
+
filled : bool, default=True
|
|
268
|
+
If True, fill interior holes in the isolated structure before computing
|
|
269
|
+
the correlation dimension, so that only the outer boundary contributes.
|
|
270
|
+
If False, holes are left as-is and interior boundaries are included.
|
|
265
271
|
point_reduction_factor : float, default=1
|
|
266
272
|
Draw N/point_reduction_factor circles, where N is the total number of
|
|
267
273
|
available circles. Must be >= 1.
|
|
@@ -306,6 +312,10 @@ def individual_correlation_dimension(
|
|
|
306
312
|
# Isolate the Nth largest structure
|
|
307
313
|
isolated = isolate_nth_largest_structure(binary, n=n)
|
|
308
314
|
|
|
315
|
+
# Optionally fill interior holes so only the outer boundary contributes
|
|
316
|
+
if filled:
|
|
317
|
+
isolated = remove_structure_holes(isolated.astype(float)).astype(bool)
|
|
318
|
+
|
|
309
319
|
# Crop to bounding box
|
|
310
320
|
rows = np.any(isolated, axis=1)
|
|
311
321
|
cols = np.any(isolated, axis=0)
|
|
@@ -582,7 +592,8 @@ def individual_fractal_dimension(
|
|
|
582
592
|
min_a: float = 10,
|
|
583
593
|
max_a: float = np.inf,
|
|
584
594
|
bins: int | None = 30,
|
|
585
|
-
return_values: bool = False
|
|
595
|
+
return_values: bool = False,
|
|
596
|
+
filled: bool = True,
|
|
586
597
|
) -> tuple[float, float] | tuple[float, float, NDArray, NDArray]:
|
|
587
598
|
"""
|
|
588
599
|
Calculate the individual fractal dimension Df of objects within arrays.
|
|
@@ -613,6 +624,10 @@ def individual_fractal_dimension(
|
|
|
613
624
|
points without binning.
|
|
614
625
|
return_values : bool, default=False
|
|
615
626
|
If True, return additional data used in the calculation.
|
|
627
|
+
filled : bool, default=True
|
|
628
|
+
If True, fill interior holes in structures before computing areas and
|
|
629
|
+
perimeters. If False, holes are left as-is, so perimeters include
|
|
630
|
+
interior boundaries and areas exclude hole pixels.
|
|
616
631
|
|
|
617
632
|
Returns
|
|
618
633
|
-------
|
|
@@ -656,14 +671,21 @@ def individual_fractal_dimension(
|
|
|
656
671
|
raise ValueError('Each array shape must match corresponding pixel sizes shape')
|
|
657
672
|
|
|
658
673
|
array = remove_structures_touching_border_nan(array)
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
674
|
+
if filled:
|
|
675
|
+
array = remove_structure_holes(array)
|
|
676
|
+
lab, nm, nl = label_structures(array, wrap='both')
|
|
677
|
+
if lab is None:
|
|
678
|
+
continue
|
|
679
|
+
new_a = get_structure_areas(lab, nm, nl, xs, ys)
|
|
680
|
+
new_p = get_structure_perimeters(lab, nm, nl, xs, ys)
|
|
681
|
+
# Filter out labels with zero area or zero perimeter (e.g. NaN-surrounded)
|
|
682
|
+
valid = (new_a > 0) & (new_p > 0)
|
|
683
|
+
areas.extend(new_a[valid])
|
|
684
|
+
perimeters.extend(new_p[valid])
|
|
664
685
|
|
|
665
686
|
areas, perimeters = np.array(areas), np.array(perimeters)
|
|
666
|
-
|
|
687
|
+
mask = (areas > min_a) & (areas < max_a)
|
|
688
|
+
areas, perimeters = areas[mask], perimeters[mask]
|
|
667
689
|
|
|
668
690
|
log_sqrt_a = np.log10(np.sqrt(areas))
|
|
669
691
|
log_p = np.log10(perimeters)
|
|
@@ -1088,7 +1110,7 @@ def label_size(
|
|
|
1088
1110
|
if wrap is None:
|
|
1089
1111
|
pass
|
|
1090
1112
|
elif wrap == 'both' or wrap == 'sides':
|
|
1091
|
-
labelled_array =
|
|
1113
|
+
labelled_array = _merge_periodic_labels(labelled_array, wrap)
|
|
1092
1114
|
else:
|
|
1093
1115
|
raise ValueError(f'wrap={wrap} not supported')
|
|
1094
1116
|
|
|
@@ -10,29 +10,58 @@ from skimage.segmentation import clear_border
|
|
|
10
10
|
from ._utils import encase_in_value
|
|
11
11
|
|
|
12
12
|
__all__ = [
|
|
13
|
+
'label_structures',
|
|
13
14
|
'get_structure_props',
|
|
14
15
|
'get_structure_areas',
|
|
15
16
|
'get_structure_perimeters',
|
|
16
17
|
'get_structure_height_width',
|
|
17
|
-
'label_periodic_boundaries',
|
|
18
18
|
'get_every_boundary_perimeter',
|
|
19
19
|
'remove_structures_touching_border_nan',
|
|
20
20
|
'clear_border_adjacent',
|
|
21
21
|
'remove_structure_holes',
|
|
22
22
|
]
|
|
23
23
|
|
|
24
|
+
DEFAULT_STRUCTURE = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]])
|
|
25
|
+
|
|
24
26
|
|
|
25
27
|
# =============================================================================
|
|
26
|
-
#
|
|
28
|
+
# Labeling
|
|
27
29
|
# =============================================================================
|
|
28
30
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
+
def label_structures(
|
|
32
|
+
array: NDArray,
|
|
33
|
+
structure: NDArray = DEFAULT_STRUCTURE,
|
|
34
|
+
wrap: str | None = 'both',
|
|
35
|
+
) -> tuple[NDArray | None, NDArray | None, int]:
|
|
36
|
+
"""
|
|
37
|
+
Label connected components in a binary array.
|
|
38
|
+
|
|
39
|
+
Wrapper on ``scipy.ndimage.label`` with NaN handling and optional
|
|
40
|
+
periodic boundary merging.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
array : np.ndarray
|
|
45
|
+
2-D binary array (0s, 1s, and optionally NaN).
|
|
46
|
+
structure : np.ndarray, default=4-connectivity cross
|
|
47
|
+
Connectivity kernel passed to ``scipy.ndimage.label``.
|
|
48
|
+
wrap : str or None, default='both'
|
|
49
|
+
Periodic boundary handling:
|
|
50
|
+
- ``'both'``: merge labels across left-right and top-bottom edges.
|
|
51
|
+
- ``'sides'``: merge labels across left-right edges only.
|
|
52
|
+
- ``None``: no periodic merging.
|
|
31
53
|
|
|
32
|
-
Returns
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
labelled_array : np.ndarray or None
|
|
57
|
+
Float32 array where each unique positive value is a connected
|
|
58
|
+
component label. Pixels that were NaN in the input are 0.
|
|
59
|
+
``None`` if no structures exist.
|
|
60
|
+
nan_mask : np.ndarray or None
|
|
61
|
+
Boolean array indicating NaN locations in the input.
|
|
62
|
+
``None`` if no structures exist.
|
|
63
|
+
n_labels : int
|
|
64
|
+
Number of connected components found (0 if none).
|
|
36
65
|
"""
|
|
37
66
|
nan_mask = np.isnan(array)
|
|
38
67
|
no_nans = array.copy()
|
|
@@ -41,18 +70,31 @@ def _label_array(array, structure, wrap):
|
|
|
41
70
|
return None, None, 0
|
|
42
71
|
labelled_array, n_labels = label(no_nans.astype(bool), structure, output=np.float32)
|
|
43
72
|
|
|
44
|
-
if wrap
|
|
45
|
-
|
|
46
|
-
elif wrap
|
|
47
|
-
|
|
48
|
-
else:
|
|
49
|
-
raise ValueError(f'wrap={wrap} not supported')
|
|
73
|
+
if wrap == 'both' or wrap == 'sides':
|
|
74
|
+
labelled_array = _merge_periodic_labels(labelled_array, wrap)
|
|
75
|
+
elif wrap is not None:
|
|
76
|
+
raise ValueError(f'wrap={wrap!r} not supported')
|
|
50
77
|
|
|
51
|
-
# NaN pixels get label 0 (already the case from no_nans). Do NOT restore
|
|
52
|
-
# nans into the labeled array — callers use nan_mask where needed.
|
|
53
78
|
return labelled_array, nan_mask, n_labels
|
|
54
79
|
|
|
55
80
|
|
|
81
|
+
def _merge_periodic_labels(labelled_array: NDArray, wrap: str) -> NDArray:
|
|
82
|
+
"""Merge labels that span periodic boundaries (internal helper)."""
|
|
83
|
+
if wrap == 'sides' or wrap == 'both':
|
|
84
|
+
for j, value in enumerate(labelled_array[:, 0]):
|
|
85
|
+
if value != 0:
|
|
86
|
+
if labelled_array[j, labelled_array.shape[1] - 1] != 0 and labelled_array[j, labelled_array.shape[1] - 1] != value:
|
|
87
|
+
labelled_array[labelled_array == labelled_array[j, labelled_array.shape[1] - 1]] = value
|
|
88
|
+
|
|
89
|
+
if wrap == 'both':
|
|
90
|
+
for i, value in enumerate(labelled_array[0, :]):
|
|
91
|
+
if value != 0:
|
|
92
|
+
if labelled_array[labelled_array.shape[0] - 1, i] != 0 and labelled_array[labelled_array.shape[0] - 1, i] != value:
|
|
93
|
+
labelled_array[labelled_array == labelled_array[labelled_array.shape[0] - 1, i]] = value
|
|
94
|
+
|
|
95
|
+
return labelled_array
|
|
96
|
+
|
|
97
|
+
|
|
56
98
|
def _validate_inputs(array, x_sizes, y_sizes):
|
|
57
99
|
"""Validate that array, x_sizes, y_sizes have matching shapes and no bad nans."""
|
|
58
100
|
if array.shape != x_sizes.shape or array.shape != y_sizes.shape:
|
|
@@ -64,47 +106,59 @@ def _validate_inputs(array, x_sizes, y_sizes):
|
|
|
64
106
|
raise ValueError('x or y sizes are nan in locations where array is not')
|
|
65
107
|
|
|
66
108
|
|
|
109
|
+
def _validate_labelled(labelled_array):
|
|
110
|
+
"""Raise TypeError if the array looks binary instead of labelled."""
|
|
111
|
+
if labelled_array.dtype == bool:
|
|
112
|
+
raise TypeError(
|
|
113
|
+
'labelled_array is boolean. '
|
|
114
|
+
'Pass a labelled array from label_structures() instead.'
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
67
118
|
# =============================================================================
|
|
68
119
|
# Area: pure numpy bincount — O(n)
|
|
69
120
|
# =============================================================================
|
|
70
121
|
|
|
71
122
|
def get_structure_areas(
|
|
72
|
-
|
|
123
|
+
labelled_array: NDArray,
|
|
124
|
+
nan_mask: NDArray,
|
|
125
|
+
n_labels: int,
|
|
73
126
|
x_sizes: NDArray,
|
|
74
127
|
y_sizes: NDArray,
|
|
75
|
-
structure: NDArray = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]),
|
|
76
128
|
) -> NDArray:
|
|
77
129
|
"""
|
|
78
|
-
Calculate areas of structures
|
|
79
|
-
|
|
80
|
-
Assumes toroidal (periodic) boundary conditions. For non-periodic domains,
|
|
81
|
-
pad edges with 0 or np.nan before calling.
|
|
130
|
+
Calculate areas of labelled structures.
|
|
82
131
|
|
|
83
132
|
Parameters
|
|
84
133
|
----------
|
|
85
|
-
|
|
86
|
-
|
|
134
|
+
labelled_array : np.ndarray
|
|
135
|
+
Labelled array from :func:`label_structures`.
|
|
136
|
+
nan_mask : np.ndarray
|
|
137
|
+
Boolean NaN mask from :func:`label_structures`.
|
|
138
|
+
n_labels : int
|
|
139
|
+
Number of labels from :func:`label_structures`.
|
|
87
140
|
x_sizes : np.ndarray
|
|
88
|
-
|
|
141
|
+
Pixel sizes in horizontal direction, same shape as labelled_array.
|
|
89
142
|
y_sizes : np.ndarray
|
|
90
|
-
|
|
91
|
-
structure : np.ndarray, default=np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]])
|
|
92
|
-
Defines connectivity.
|
|
143
|
+
Pixel sizes in vertical direction, same shape as labelled_array.
|
|
93
144
|
|
|
94
145
|
Returns
|
|
95
146
|
-------
|
|
96
147
|
areas : np.ndarray
|
|
97
|
-
1-D array,
|
|
148
|
+
1-D array of shape ``(n_labels,)`` where ``areas[i]`` is the area of
|
|
149
|
+
label ``i + 1``. Guarantees index alignment with other
|
|
150
|
+
``get_structure_*`` functions called on the same labelled array.
|
|
98
151
|
"""
|
|
99
|
-
|
|
100
|
-
labelled_array, nan_mask, n_labels = _label_array(array, structure, 'both')
|
|
101
|
-
if labelled_array is None:
|
|
102
|
-
return np.array([], dtype=np.float32)
|
|
152
|
+
_validate_labelled(labelled_array)
|
|
103
153
|
return _compute_areas(labelled_array, x_sizes, y_sizes, n_labels)
|
|
104
154
|
|
|
105
155
|
|
|
106
156
|
def _compute_areas(labelled_array, x_sizes, y_sizes, n_labels):
|
|
107
|
-
"""Compute per-label areas via np.bincount.
|
|
157
|
+
"""Compute per-label areas via np.bincount.
|
|
158
|
+
|
|
159
|
+
Returns array of shape (n_labels,) — index i is label i+1.
|
|
160
|
+
Merged labels from periodic wrapping may have area 0.
|
|
161
|
+
"""
|
|
108
162
|
pixel_areas = (x_sizes * y_sizes).ravel()
|
|
109
163
|
labels_flat = labelled_array.ravel()
|
|
110
164
|
mask = labels_flat > 0
|
|
@@ -113,10 +167,7 @@ def _compute_areas(labelled_array, x_sizes, y_sizes, n_labels):
|
|
|
113
167
|
weights=pixel_areas[mask],
|
|
114
168
|
minlength=n_labels + 1,
|
|
115
169
|
)
|
|
116
|
-
|
|
117
|
-
areas = areas[1:].astype(np.float32)
|
|
118
|
-
# Filter out labels with zero area (can happen with periodic wrapping merging labels)
|
|
119
|
-
return areas[areas > 0]
|
|
170
|
+
return areas[1:].astype(np.float32)
|
|
120
171
|
|
|
121
172
|
|
|
122
173
|
# =============================================================================
|
|
@@ -124,41 +175,39 @@ def _compute_areas(labelled_array, x_sizes, y_sizes, n_labels):
|
|
|
124
175
|
# =============================================================================
|
|
125
176
|
|
|
126
177
|
def get_structure_perimeters(
|
|
127
|
-
|
|
178
|
+
labelled_array: NDArray,
|
|
179
|
+
nan_mask: NDArray,
|
|
180
|
+
n_labels: int,
|
|
128
181
|
x_sizes: NDArray,
|
|
129
182
|
y_sizes: NDArray,
|
|
130
|
-
structure: NDArray = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]),
|
|
131
183
|
) -> NDArray:
|
|
132
184
|
"""
|
|
133
|
-
Calculate perimeters of structures
|
|
185
|
+
Calculate perimeters of labelled structures.
|
|
134
186
|
|
|
135
|
-
|
|
136
|
-
structure and nan is not counted. For non-periodic domains, pad edges
|
|
137
|
-
with 0 or np.nan before calling.
|
|
187
|
+
Perimeter between a structure and NaN is not counted.
|
|
138
188
|
|
|
139
189
|
Parameters
|
|
140
190
|
----------
|
|
141
|
-
|
|
142
|
-
|
|
191
|
+
labelled_array : np.ndarray
|
|
192
|
+
Labelled array from :func:`label_structures`.
|
|
193
|
+
nan_mask : np.ndarray
|
|
194
|
+
Boolean NaN mask from :func:`label_structures`.
|
|
195
|
+
n_labels : int
|
|
196
|
+
Number of labels from :func:`label_structures`.
|
|
143
197
|
x_sizes : np.ndarray
|
|
144
|
-
|
|
198
|
+
Pixel sizes in horizontal direction, same shape as labelled_array.
|
|
145
199
|
y_sizes : np.ndarray
|
|
146
|
-
|
|
147
|
-
structure : np.ndarray, default=np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]])
|
|
148
|
-
Defines connectivity.
|
|
200
|
+
Pixel sizes in vertical direction, same shape as labelled_array.
|
|
149
201
|
|
|
150
202
|
Returns
|
|
151
203
|
-------
|
|
152
204
|
perimeters : np.ndarray
|
|
153
|
-
1-D array
|
|
205
|
+
1-D array of shape ``(n_labels,)`` where ``perimeters[i]`` is the
|
|
206
|
+
perimeter of label ``i + 1``. Guarantees index alignment with other
|
|
207
|
+
``get_structure_*`` functions called on the same labelled array.
|
|
154
208
|
"""
|
|
155
|
-
|
|
156
|
-
labelled_array, nan_mask,
|
|
157
|
-
if labelled_array is None:
|
|
158
|
-
return np.array([], dtype=np.float32)
|
|
159
|
-
# Pass nan_mask so perimeter along nan edges is not counted
|
|
160
|
-
perimeters = _compute_perimeters(labelled_array, nan_mask, x_sizes, y_sizes, n_labels)
|
|
161
|
-
return perimeters[perimeters > 0]
|
|
209
|
+
_validate_labelled(labelled_array)
|
|
210
|
+
return _compute_perimeters(labelled_array, nan_mask, x_sizes, y_sizes, n_labels)
|
|
162
211
|
|
|
163
212
|
|
|
164
213
|
@njit(parallel=True)
|
|
@@ -225,34 +274,36 @@ def _compute_perimeters(labelled_array, nan_mask, x_sizes, y_sizes, n_labels):
|
|
|
225
274
|
# =============================================================================
|
|
226
275
|
|
|
227
276
|
def get_structure_height_width(
|
|
228
|
-
|
|
277
|
+
labelled_array: NDArray,
|
|
278
|
+
nan_mask: NDArray,
|
|
279
|
+
n_labels: int,
|
|
229
280
|
x_sizes: NDArray,
|
|
230
281
|
y_sizes: NDArray,
|
|
231
|
-
structure: NDArray = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]),
|
|
232
282
|
) -> tuple[NDArray, NDArray]:
|
|
233
283
|
"""
|
|
234
|
-
Calculate heights and widths of structures
|
|
235
|
-
|
|
236
|
-
Assumes toroidal (periodic) boundary conditions. For non-periodic domains,
|
|
237
|
-
pad edges with 0 or np.nan before calling.
|
|
284
|
+
Calculate heights and widths of labelled structures.
|
|
238
285
|
|
|
239
286
|
Parameters
|
|
240
287
|
----------
|
|
241
|
-
|
|
242
|
-
|
|
288
|
+
labelled_array : np.ndarray
|
|
289
|
+
Labelled array from :func:`label_structures`.
|
|
290
|
+
nan_mask : np.ndarray
|
|
291
|
+
Boolean NaN mask from :func:`label_structures`.
|
|
292
|
+
n_labels : int
|
|
293
|
+
Number of labels from :func:`label_structures`.
|
|
243
294
|
x_sizes : np.ndarray
|
|
244
|
-
|
|
295
|
+
Pixel sizes in horizontal direction, same shape as labelled_array.
|
|
245
296
|
y_sizes : np.ndarray
|
|
246
|
-
|
|
247
|
-
structure : np.ndarray, default=np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]])
|
|
248
|
-
Defines connectivity.
|
|
297
|
+
Pixel sizes in vertical direction, same shape as labelled_array.
|
|
249
298
|
|
|
250
299
|
Returns
|
|
251
300
|
-------
|
|
252
301
|
heights : np.ndarray
|
|
253
|
-
1-D array
|
|
302
|
+
1-D array of shape ``(n_labels,)`` where ``heights[i]`` is the height
|
|
303
|
+
of label ``i + 1``.
|
|
254
304
|
widths : np.ndarray
|
|
255
|
-
1-D array
|
|
305
|
+
1-D array of shape ``(n_labels,)`` where ``widths[i]`` is the width
|
|
306
|
+
of label ``i + 1``.
|
|
256
307
|
|
|
257
308
|
Notes
|
|
258
309
|
-----
|
|
@@ -260,23 +311,29 @@ def get_structure_height_width(
|
|
|
260
311
|
pixel widths of the pixels in the column and in the object. Similarly, the height
|
|
261
312
|
will be the sum of the average pixel heights of the pixels in the row and in the object.
|
|
262
313
|
"""
|
|
263
|
-
|
|
264
|
-
labelled_array, nan_mask, n_labels = _label_array(array, structure, 'both')
|
|
265
|
-
if labelled_array is None:
|
|
266
|
-
return np.array([], dtype=np.float32), np.array([], dtype=np.float32)
|
|
314
|
+
_validate_labelled(labelled_array)
|
|
267
315
|
|
|
268
|
-
|
|
269
|
-
# labelled_array has 0 where nans were (not nan), so separation is safe.
|
|
270
|
-
separated = _get_separated_structure_indices(labelled_array)
|
|
316
|
+
separated, label_ids = _get_separated_structure_indices(labelled_array)
|
|
271
317
|
if len(separated) == 0:
|
|
272
|
-
return np.
|
|
318
|
+
return np.zeros(n_labels, dtype=np.float32), np.zeros(n_labels, dtype=np.float32)
|
|
319
|
+
|
|
320
|
+
h_sparse, w_sparse = _compute_height_width(labelled_array, List(separated), x_sizes, y_sizes)
|
|
273
321
|
|
|
274
|
-
|
|
275
|
-
|
|
322
|
+
# Map sparse results back to label-indexed arrays
|
|
323
|
+
heights = np.zeros(n_labels, dtype=np.float32)
|
|
324
|
+
widths = np.zeros(n_labels, dtype=np.float32)
|
|
325
|
+
for k, lab in enumerate(label_ids):
|
|
326
|
+
heights[lab - 1] = h_sparse[k]
|
|
327
|
+
widths[lab - 1] = w_sparse[k]
|
|
328
|
+
return heights, widths
|
|
276
329
|
|
|
277
330
|
|
|
278
331
|
def _get_separated_structure_indices(labelled_array):
|
|
279
|
-
"""Get list of 2D index arrays, one per structure label.
|
|
332
|
+
"""Get list of 2D index arrays, one per structure label.
|
|
333
|
+
|
|
334
|
+
Returns (separated, label_ids) where label_ids[k] is the label value
|
|
335
|
+
for separated[k].
|
|
336
|
+
"""
|
|
280
337
|
values = np.sort(labelled_array.flatten())
|
|
281
338
|
original_locations = np.argsort(labelled_array.flatten())
|
|
282
339
|
indices_2d = np.array(np.unravel_index(original_locations, labelled_array.shape)).T
|
|
@@ -284,9 +341,14 @@ def _get_separated_structure_indices(labelled_array):
|
|
|
284
341
|
split_here = np.roll(values, shift=-1) - values
|
|
285
342
|
split_here[-1] = 0
|
|
286
343
|
|
|
287
|
-
|
|
344
|
+
split_points = np.where(split_here != 0)[0] + 1
|
|
345
|
+
separated = np.split(indices_2d, split_points)
|
|
288
346
|
separated = separated[1:] # Remove label 0
|
|
289
|
-
|
|
347
|
+
|
|
348
|
+
# Extract the label id for each group
|
|
349
|
+
unique_labels = np.unique(values)
|
|
350
|
+
label_ids = unique_labels[unique_labels > 0].astype(int)
|
|
351
|
+
return separated, label_ids
|
|
290
352
|
|
|
291
353
|
|
|
292
354
|
@njit(parallel=True)
|
|
@@ -330,9 +392,7 @@ def _compute_height_width(labelled_array, separated_structure_indices, x_sizes,
|
|
|
330
392
|
h[iteration] = height
|
|
331
393
|
w[iteration] = width
|
|
332
394
|
|
|
333
|
-
|
|
334
|
-
valid = (h > 0) | (w > 0)
|
|
335
|
-
return h[valid], w[valid]
|
|
395
|
+
return h, w
|
|
336
396
|
|
|
337
397
|
|
|
338
398
|
# =============================================================================
|
|
@@ -394,66 +454,21 @@ def get_structure_props(
|
|
|
394
454
|
get_structure_height_width.
|
|
395
455
|
"""
|
|
396
456
|
_validate_inputs(array, x_sizes, y_sizes)
|
|
397
|
-
labelled_array, nan_mask, n_labels =
|
|
457
|
+
labelled_array, nan_mask, n_labels = label_structures(array, structure, wrap='both')
|
|
398
458
|
if labelled_array is None:
|
|
399
459
|
if print_none:
|
|
400
460
|
print('No structures found')
|
|
401
461
|
return np.array([]), np.array([]), np.array([]), np.array([])
|
|
402
462
|
|
|
403
|
-
a =
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
p = p_all[p_all > 0]
|
|
407
|
-
|
|
408
|
-
separated = _get_separated_structure_indices(labelled_array)
|
|
409
|
-
if len(separated) == 0:
|
|
410
|
-
return np.array([]), np.array([]), np.array([]), np.array([])
|
|
411
|
-
h, w = _compute_height_width(labelled_array, List(separated), x_sizes, y_sizes)
|
|
412
|
-
|
|
413
|
-
return p, a, np.array(h, dtype=np.float32), np.array(w, dtype=np.float32)
|
|
463
|
+
a = get_structure_areas(labelled_array, nan_mask, n_labels, x_sizes, y_sizes)
|
|
464
|
+
p = get_structure_perimeters(labelled_array, nan_mask, n_labels, x_sizes, y_sizes)
|
|
465
|
+
h, w = get_structure_height_width(labelled_array, nan_mask, n_labels, x_sizes, y_sizes)
|
|
414
466
|
|
|
467
|
+
# Filter out labels with zero area (from periodic wrapping merging labels)
|
|
468
|
+
valid = a > 0
|
|
469
|
+
return p[valid], a[valid], h[valid], w[valid]
|
|
415
470
|
|
|
416
|
-
def label_periodic_boundaries(labelled_array: NDArray, wrap: str) -> NDArray:
|
|
417
|
-
"""
|
|
418
|
-
Make labelled structures that span a periodic boundary have the same label.
|
|
419
|
-
|
|
420
|
-
Parameters
|
|
421
|
-
----------
|
|
422
|
-
labelled_array : np.ndarray
|
|
423
|
-
A 2D array where each unique non-zero element represents a distinct label.
|
|
424
|
-
Should be the output of scipy.ndimage.label().
|
|
425
|
-
wrap : str
|
|
426
|
-
Determines how the boundaries of the array should be wrapped.
|
|
427
|
-
'sides': Sets labels on the right boundary to match those on the left.
|
|
428
|
-
'both': Also sets labels on the top boundary to match those on the bottom.
|
|
429
|
-
|
|
430
|
-
Returns
|
|
431
|
-
-------
|
|
432
|
-
np.ndarray
|
|
433
|
-
The input array with periodic boundaries labelled according to the wrap parameter.
|
|
434
|
-
|
|
435
|
-
Raises
|
|
436
|
-
------
|
|
437
|
-
ValueError
|
|
438
|
-
If wrap is neither 'sides' nor 'both'.
|
|
439
|
-
"""
|
|
440
|
-
if wrap == 'sides' or wrap == 'both':
|
|
441
|
-
# set those on right to the same i.d. as those on left
|
|
442
|
-
for j, value in enumerate(labelled_array[:, 0]):
|
|
443
|
-
if value != 0:
|
|
444
|
-
if labelled_array[j, labelled_array.shape[1] - 1] != 0 and labelled_array[j, labelled_array.shape[1] - 1] != value:
|
|
445
|
-
labelled_array[labelled_array == labelled_array[j, labelled_array.shape[1] - 1]] = value
|
|
446
471
|
|
|
447
|
-
if wrap == 'both':
|
|
448
|
-
# set those on top to the same i.d. as those on bottom
|
|
449
|
-
for i, value in enumerate(labelled_array[0, :]):
|
|
450
|
-
if value != 0:
|
|
451
|
-
if labelled_array[labelled_array.shape[0] - 1, i] != 0 and labelled_array[labelled_array.shape[0] - 1, i] != value:
|
|
452
|
-
labelled_array[labelled_array == labelled_array[labelled_array.shape[0] - 1, i]] = value
|
|
453
|
-
|
|
454
|
-
if wrap not in ['sides', 'both']:
|
|
455
|
-
raise ValueError(f'wrap = {wrap} not supported')
|
|
456
|
-
return labelled_array
|
|
457
472
|
|
|
458
473
|
|
|
459
474
|
def get_every_boundary_perimeter(
|
|
@@ -499,8 +514,13 @@ def get_every_boundary_perimeter(
|
|
|
499
514
|
if counter > 100:
|
|
500
515
|
raise ValueError('Hole layer limit reached: 100 layers')
|
|
501
516
|
all_holes_filled = remove_structure_holes(array)
|
|
502
|
-
|
|
503
|
-
|
|
517
|
+
encased = encase_in_value(all_holes_filled)
|
|
518
|
+
enc_xs = encase_in_value(x_sizes)
|
|
519
|
+
enc_ys = encase_in_value(y_sizes)
|
|
520
|
+
lab, nm, nl = label_structures(encased, wrap='both')
|
|
521
|
+
if lab is not None:
|
|
522
|
+
p = get_structure_perimeters(lab, nm, nl, enc_xs, enc_ys)
|
|
523
|
+
perimeters.extend(p[p > 0])
|
|
504
524
|
|
|
505
525
|
# remove one layer
|
|
506
526
|
new_array = all_holes_filled - array
|
|
@@ -625,7 +645,7 @@ def remove_structure_holes(
|
|
|
625
645
|
# invert and label
|
|
626
646
|
labelled, _ = label((1 - filled))
|
|
627
647
|
if periodic is not False:
|
|
628
|
-
labelled =
|
|
648
|
+
labelled = _merge_periodic_labels(labelled, periodic)
|
|
629
649
|
# largest structure will be the background or the cloudy areas.
|
|
630
650
|
unique_values, unique_counts = np.unique(labelled.flatten(), return_counts=True)
|
|
631
651
|
# Make sure we don't identify the cloudy areas as the background.
|
|
@@ -8,6 +8,7 @@ import numpy as np
|
|
|
8
8
|
from numpy.typing import NDArray
|
|
9
9
|
from warnings import warn
|
|
10
10
|
from ._object_analysis import (
|
|
11
|
+
label_structures,
|
|
11
12
|
remove_structures_touching_border_nan,
|
|
12
13
|
get_structure_areas,
|
|
13
14
|
get_structure_perimeters,
|
|
@@ -385,13 +386,20 @@ def array_size_distribution(
|
|
|
385
386
|
np.full((1, y_sizes.shape[1]), np.nan)], axis=0)
|
|
386
387
|
# wrap='both': fully periodic, no padding needed
|
|
387
388
|
|
|
388
|
-
|
|
389
|
-
|
|
389
|
+
lab, nm, nl = label_structures(array, structure, wrap='both')
|
|
390
|
+
if lab is None:
|
|
391
|
+
to_bin = np.array([], dtype=np.float32)
|
|
392
|
+
elif variable == 'area':
|
|
393
|
+
a = get_structure_areas(lab, nm, nl, x_sizes, y_sizes)
|
|
394
|
+
to_bin = a[a > 0]
|
|
390
395
|
elif variable == 'perimeter':
|
|
391
|
-
|
|
396
|
+
p = get_structure_perimeters(lab, nm, nl, x_sizes, y_sizes)
|
|
397
|
+
to_bin = p[p > 0]
|
|
392
398
|
elif variable in ('height', 'width'):
|
|
393
|
-
h, w = get_structure_height_width(
|
|
394
|
-
|
|
399
|
+
h, w = get_structure_height_width(lab, nm, nl, x_sizes, y_sizes)
|
|
400
|
+
a = get_structure_areas(lab, nm, nl, x_sizes, y_sizes)
|
|
401
|
+
valid = a > 0
|
|
402
|
+
to_bin = h[valid] if variable == 'height' else w[valid]
|
|
395
403
|
elif variable == 'nested perimeter':
|
|
396
404
|
to_bin = get_every_boundary_perimeter(array, x_sizes, y_sizes, False)
|
|
397
405
|
else:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: objscale
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Object-based analysis functions for fractal dimensions and size distributions
|
|
5
5
|
Author-email: Thomas DeWitt <thomas.dewitt@utah.edu>
|
|
6
6
|
License: MIT
|
|
@@ -162,10 +162,11 @@ ind_dim, ind_error = objscale.individual_fractal_dimension(arrays)
|
|
|
162
162
|
|
|
163
163
|
### Object Analysis
|
|
164
164
|
|
|
165
|
-
- `
|
|
166
|
-
- `
|
|
167
|
-
- `
|
|
168
|
-
- `
|
|
165
|
+
- `label_structures` - Label connected components (wraps scipy.ndimage.label with NaN handling and periodic boundaries)
|
|
166
|
+
- `get_structure_areas` - Calculate areas of labelled structures (O(n), fast)
|
|
167
|
+
- `get_structure_perimeters` - Calculate perimeters of labelled structures (O(n), fast)
|
|
168
|
+
- `get_structure_height_width` - Calculate height and width of labelled structures
|
|
169
|
+
- `get_structure_props` - Calculate perimeter, area, width, height from a binary array (convenience wrapper)
|
|
169
170
|
- `get_every_boundary_perimeter` - Perimeters of every boundary including nested holes
|
|
170
171
|
- `total_perimeter` - Total perimeter of all objects
|
|
171
172
|
- `total_number` - Count number of structures
|
|
@@ -173,7 +174,6 @@ ind_dim, ind_error = objscale.individual_fractal_dimension(arrays)
|
|
|
173
174
|
- `remove_structures_touching_border_nan` - Remove border-touching structures
|
|
174
175
|
- `remove_structure_holes` - Fill holes in structures
|
|
175
176
|
- `label_size` - Label each structure with its size value
|
|
176
|
-
- `label_periodic_boundaries` - Merge labels across periodic boundaries
|
|
177
177
|
- `clear_border_adjacent` - Clear structures touching array edges
|
|
178
178
|
|
|
179
179
|
### Utilities
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|