dkist-processing-math 2.2.2__py3-none-any.whl → 3.0.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.
- dkist_processing_math/arithmetic.py +19 -0
- dkist_processing_math/tests/test_arithmetic.py +54 -1
- dkist_processing_math/tests/test_transform.py +193 -289
- dkist_processing_math/transform/__init__.py +4 -14
- dkist_processing_math/transform/affine.py +146 -221
- {dkist_processing_math-2.2.2.dist-info → dkist_processing_math-3.0.0.dist-info}/METADATA +1 -1
- {dkist_processing_math-2.2.2.dist-info → dkist_processing_math-3.0.0.dist-info}/RECORD +9 -9
- {dkist_processing_math-2.2.2.dist-info → dkist_processing_math-3.0.0.dist-info}/WHEEL +1 -1
- {dkist_processing_math-2.2.2.dist-info → dkist_processing_math-3.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Arithmetic."""
|
|
2
2
|
|
|
3
|
+
from datetime import datetime
|
|
3
4
|
from typing import Generator
|
|
4
5
|
from typing import Iterable
|
|
5
6
|
from typing import Union
|
|
@@ -124,3 +125,21 @@ def divide_fits_access_by_array(
|
|
|
124
125
|
for obj in access_objs:
|
|
125
126
|
obj.data = next(divide_arrays_by_array(obj.data, array_to_divide_by))
|
|
126
127
|
yield obj
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def closest_datetime(
|
|
131
|
+
target_datetime: str | datetime, datetimes: list[str | datetime] | str | datetime
|
|
132
|
+
) -> str | datetime:
|
|
133
|
+
"""Find the closest datetime to a target datetime. Return type is of the same type as the datetimes list."""
|
|
134
|
+
if isinstance(target_datetime, str):
|
|
135
|
+
target_datetime = datetime.fromisoformat(target_datetime)
|
|
136
|
+
if isinstance(datetimes, str) or isinstance(datetimes, datetime):
|
|
137
|
+
datetimes = [datetimes]
|
|
138
|
+
converted_datetimes = [
|
|
139
|
+
datetime.fromisoformat(d) if isinstance(d, str) else d for d in datetimes
|
|
140
|
+
]
|
|
141
|
+
closest_index, _ = min(
|
|
142
|
+
enumerate(converted_datetimes),
|
|
143
|
+
key=lambda dt: abs(dt[1] - target_datetime),
|
|
144
|
+
)
|
|
145
|
+
return datetimes[closest_index]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from datetime import datetime
|
|
1
2
|
from typing import Generator
|
|
2
3
|
|
|
3
4
|
import numpy as np
|
|
@@ -5,6 +6,7 @@ import pytest
|
|
|
5
6
|
from astropy.io import fits
|
|
6
7
|
from dkist_processing_common.models.fits_access import FitsAccessBase
|
|
7
8
|
|
|
9
|
+
from dkist_processing_math.arithmetic import closest_datetime
|
|
8
10
|
from dkist_processing_math.arithmetic import divide_arrays_by_array
|
|
9
11
|
from dkist_processing_math.arithmetic import divide_fits_access_by_array
|
|
10
12
|
from dkist_processing_math.arithmetic import subtract_array_from_arrays
|
|
@@ -18,7 +20,7 @@ def multiple_test_fits_access(request, numpy_arrays):
|
|
|
18
20
|
for i, array in enumerate(numpy_arrays):
|
|
19
21
|
hdu = fits.PrimaryHDU(data=array)
|
|
20
22
|
hdu.header["TEST"] = i
|
|
21
|
-
access_list.append(FitsAccessBase(hdu=
|
|
23
|
+
access_list.append(FitsAccessBase(header=hdu.header, data=array))
|
|
22
24
|
if request.param == "multiple":
|
|
23
25
|
return access_list
|
|
24
26
|
return access_list[0]
|
|
@@ -126,3 +128,54 @@ def test_divide_arrays_by_array_wrong_shape(numpy_arrays_wrong_shape, numpy_arra
|
|
|
126
128
|
"""
|
|
127
129
|
with pytest.raises(ValueError):
|
|
128
130
|
list(divide_arrays_by_array(numpy_arrays_wrong_shape, numpy_array))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@pytest.mark.parametrize(
|
|
134
|
+
"datetimes, expected_result",
|
|
135
|
+
[
|
|
136
|
+
pytest.param(
|
|
137
|
+
["2025-12-31T00:00:00", "2026-01-07T00:00:00"],
|
|
138
|
+
"2025-12-31T00:00:00",
|
|
139
|
+
id="multiple solar gain ops",
|
|
140
|
+
),
|
|
141
|
+
pytest.param("2025-12-31T00:00:00", "2025-12-31T00:00:00", id="single solar gain op"),
|
|
142
|
+
],
|
|
143
|
+
)
|
|
144
|
+
def test_closest_datetime_with_strings(datetimes, expected_result):
|
|
145
|
+
"""
|
|
146
|
+
Given: a target datetime and a list of string datetimes
|
|
147
|
+
When: running the closest_datetime method
|
|
148
|
+
Then: the closest datetime from the list of datetimes is returned
|
|
149
|
+
"""
|
|
150
|
+
result = closest_datetime(
|
|
151
|
+
target_datetime="2026-01-01T00:00:00",
|
|
152
|
+
datetimes=datetimes,
|
|
153
|
+
)
|
|
154
|
+
assert result == expected_result
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@pytest.mark.parametrize(
|
|
158
|
+
"datetimes, expected_result",
|
|
159
|
+
[
|
|
160
|
+
pytest.param(
|
|
161
|
+
[
|
|
162
|
+
datetime(2025, 12, 31),
|
|
163
|
+
datetime(2026, 1, 7),
|
|
164
|
+
],
|
|
165
|
+
datetime(2025, 12, 31),
|
|
166
|
+
id="multiple solar gain ops",
|
|
167
|
+
),
|
|
168
|
+
pytest.param(datetime(2025, 12, 31), datetime(2025, 12, 31), id="single solar gain op"),
|
|
169
|
+
],
|
|
170
|
+
)
|
|
171
|
+
def test_closest_datetime_with_datetimes(datetimes, expected_result):
|
|
172
|
+
"""
|
|
173
|
+
Given: a target datetime and a list of datetimes
|
|
174
|
+
When: running the closest_datetime method
|
|
175
|
+
Then: the closest datetime from the list of datetimes is returned
|
|
176
|
+
"""
|
|
177
|
+
result = closest_datetime(
|
|
178
|
+
target_datetime=datetime(2026, 1, 1),
|
|
179
|
+
datetimes=datetimes,
|
|
180
|
+
)
|
|
181
|
+
assert result == expected_result
|
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
import pytest
|
|
3
|
-
from scipy import ndimage as spnd
|
|
4
3
|
from skimage import transform as transform
|
|
5
4
|
|
|
6
|
-
from dkist_processing_math.transform import
|
|
5
|
+
from dkist_processing_math.transform import affine_inverse_transform_matrix
|
|
6
|
+
from dkist_processing_math.transform import apply_affine_transform
|
|
7
7
|
from dkist_processing_math.transform import bin_arrays
|
|
8
8
|
from dkist_processing_math.transform import do_hough
|
|
9
9
|
from dkist_processing_math.transform import make_binary
|
|
10
|
-
from dkist_processing_math.transform import rotate_arrays_about_point
|
|
11
|
-
from dkist_processing_math.transform import rotation_matrix
|
|
12
|
-
from dkist_processing_math.transform import scale_matrix
|
|
13
|
-
from dkist_processing_math.transform import shear_matrix
|
|
14
|
-
from dkist_processing_math.transform import translate_arrays
|
|
15
|
-
from dkist_processing_math.transform import translation_matrix
|
|
16
10
|
from dkist_processing_math.transform.binning import resize_arrays
|
|
17
11
|
from dkist_processing_math.transform.binning import resize_arrays_local_mean
|
|
18
12
|
|
|
@@ -149,328 +143,238 @@ def test_resize_arrays_local_mean_single_array(numpy_array, resized_array):
|
|
|
149
143
|
np.testing.assert_allclose(result, resized_array)
|
|
150
144
|
|
|
151
145
|
|
|
152
|
-
|
|
153
|
-
""
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
""
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
146
|
+
@pytest.mark.parametrize(
|
|
147
|
+
"array_shape, shift, angle, order, expected_matrix",
|
|
148
|
+
[
|
|
149
|
+
pytest.param(
|
|
150
|
+
(100, 100),
|
|
151
|
+
np.array([0.0, 0.0]),
|
|
152
|
+
0.0,
|
|
153
|
+
"shift_then_rotate",
|
|
154
|
+
np.eye(3),
|
|
155
|
+
id="identity",
|
|
156
|
+
),
|
|
157
|
+
pytest.param(
|
|
158
|
+
(100, 200),
|
|
159
|
+
np.array([0.0, 0.0]),
|
|
160
|
+
np.pi,
|
|
161
|
+
"shift_then_rotate",
|
|
162
|
+
np.array([[-1.0, 0.0, 99.0], [0.0, -1.0, 199.0], [0.0, 0.0, 1.0]]),
|
|
163
|
+
id="180_rotation_about_center",
|
|
164
|
+
),
|
|
165
|
+
pytest.param(
|
|
166
|
+
(100, 100),
|
|
167
|
+
np.array([5.0, -3.0]),
|
|
168
|
+
0.0,
|
|
169
|
+
"shift_then_rotate",
|
|
170
|
+
np.array([[1.0, 0.0, -5.0], [0.0, 1.0, 3.0], [0.0, 0.0, 1.0]]),
|
|
171
|
+
id="pure_translation",
|
|
172
|
+
),
|
|
173
|
+
pytest.param(
|
|
174
|
+
(100, 100),
|
|
175
|
+
np.array([0.0, 0.0]),
|
|
176
|
+
np.pi / 2,
|
|
177
|
+
"shift_then_rotate",
|
|
178
|
+
np.array([[0.0, 1.0, 0.0], [-1.0, 0.0, 99.0], [0.0, 0.0, 1.0]]),
|
|
179
|
+
id="90_deg_rotation",
|
|
180
|
+
),
|
|
181
|
+
],
|
|
182
|
+
)
|
|
183
|
+
def test_affine_inverse_transform_matrix_hand_derived(
|
|
184
|
+
array_shape, shift, angle, order, expected_matrix
|
|
185
|
+
):
|
|
186
|
+
"""
|
|
187
|
+
Given: 2D array shape / shift / angle / order combinations with easy-to-derive-by-hand expected matrices
|
|
188
|
+
When: building the homogeneous inverse transform matrix
|
|
189
|
+
Then: the result matches the hand-derived expectation
|
|
190
|
+
"""
|
|
191
|
+
result = affine_inverse_transform_matrix(array_shape, shift, angle, order=order)
|
|
192
|
+
np.testing.assert_allclose(result, expected_matrix, atol=1e-10)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_affine_inverse_transform_matrix_rejects_invalid_order():
|
|
196
|
+
"""
|
|
197
|
+
Given: an `order` value that isn't one of the two recognized options
|
|
198
|
+
When: building the homogeneous inverse transform matrix
|
|
199
|
+
Then: a ValueError is raised, rather than silently falling back to some default behavior
|
|
200
|
+
"""
|
|
201
|
+
with pytest.raises(ValueError, match="order must be"):
|
|
202
|
+
affine_inverse_transform_matrix(
|
|
203
|
+
(10, 10), np.array([0.0, 0.0]), 0.0, order="not_a_real_order"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_affine_inverse_transform_matrix_requires_2d():
|
|
208
|
+
"""
|
|
209
|
+
Given: an array_shape with more than 2 dimensions
|
|
210
|
+
When: building the homogeneous inverse transform matrix
|
|
211
|
+
Then: a ValueError is raised
|
|
212
|
+
"""
|
|
213
|
+
with pytest.raises(ValueError, match="array_shape must be 2D"):
|
|
214
|
+
affine_inverse_transform_matrix((10, 10, 10), np.zeros(3), 0.3, order="shift_then_rotate")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@pytest.mark.parametrize(
|
|
218
|
+
"shift",
|
|
219
|
+
[
|
|
220
|
+
pytest.param(5.0, id="scalar_shift"),
|
|
221
|
+
pytest.param(np.array([5.0]), id="length_one_shift"),
|
|
222
|
+
pytest.param(np.array([1.0, 2.0, 3.0]), id="wrong_length_shift"),
|
|
223
|
+
],
|
|
224
|
+
)
|
|
225
|
+
def test_affine_inverse_transform_matrix_rejects_mismatched_shift(shift):
|
|
226
|
+
"""
|
|
227
|
+
Given: a `shift` whose shape doesn't match `array_shape`'s dimensionality -- including scalar
|
|
228
|
+
and length-1 shifts, which would otherwise silently broadcast rather than raising
|
|
229
|
+
When: building the homogeneous inverse transform matrix for a 2D array_shape
|
|
230
|
+
Then: a ValueError is raised
|
|
231
|
+
"""
|
|
232
|
+
with pytest.raises(ValueError, match="shift must have shape"):
|
|
233
|
+
affine_inverse_transform_matrix((10, 10), shift, 0.0, order="shift_then_rotate")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_apply_affine_transform_identity():
|
|
237
|
+
"""
|
|
238
|
+
Given: a zero shift and zero rotation
|
|
239
|
+
When: applying the correction
|
|
240
|
+
Then: the output array is numerically identical to the input
|
|
241
|
+
"""
|
|
242
|
+
array = np.arange(100.0).reshape(10, 10)
|
|
243
|
+
result = next(
|
|
244
|
+
apply_affine_transform(array, np.array([0.0, 0.0]), 0.0, order="shift_then_rotate")
|
|
245
|
+
)
|
|
246
|
+
np.testing.assert_allclose(result, array, atol=1e-10)
|
|
240
247
|
|
|
241
248
|
|
|
242
|
-
def
|
|
249
|
+
def test_apply_affine_transform_single_array_wrapped():
|
|
243
250
|
"""
|
|
244
|
-
Given: a
|
|
245
|
-
When: applying the
|
|
246
|
-
Then:
|
|
251
|
+
Given: a single ndarray (not wrapped in a list)
|
|
252
|
+
When: applying the correction
|
|
253
|
+
Then: it is accepted and the generator yields exactly one result
|
|
247
254
|
"""
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
255
|
+
array = np.zeros((5, 5))
|
|
256
|
+
results = list(
|
|
257
|
+
apply_affine_transform(array, np.array([0.0, 0.0]), 0.0, order="shift_then_rotate")
|
|
258
|
+
)
|
|
259
|
+
assert len(results) == 1
|
|
260
|
+
assert results[0].shape == (5, 5)
|
|
253
261
|
|
|
254
262
|
|
|
255
|
-
def
|
|
263
|
+
def test_apply_affine_transform_nonfinite_values_replaced():
|
|
256
264
|
"""
|
|
257
|
-
Given:
|
|
258
|
-
When: applying the
|
|
259
|
-
Then:
|
|
265
|
+
Given: an input array containing NaN and ±inf
|
|
266
|
+
When: applying the correction
|
|
267
|
+
Then: the output is fully finite (NaN/inf do not survive interpolation)
|
|
260
268
|
"""
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
np.
|
|
269
|
+
array = np.ones((10, 10))
|
|
270
|
+
array[0, 0] = np.nan
|
|
271
|
+
array[0, 1] = np.inf
|
|
272
|
+
array[0, 2] = -np.inf
|
|
273
|
+
result = next(
|
|
274
|
+
apply_affine_transform(array, np.array([0.0, 0.0]), 0.0, order="shift_then_rotate")
|
|
275
|
+
)
|
|
276
|
+
assert np.isfinite(result).all()
|
|
264
277
|
|
|
265
278
|
|
|
266
|
-
def
|
|
267
|
-
"""
|
|
268
|
-
Given: a numpy_array
|
|
269
|
-
When: applying the affine transform and specifying no parameters
|
|
270
|
-
Then: an error is raised
|
|
279
|
+
def test_apply_affine_transform_hand_derived_shift():
|
|
271
280
|
"""
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
281
|
+
Given: a distinctive single-pixel array and a pure integer shift (no rotation)
|
|
282
|
+
When: applying the correction with shift_then_rotate
|
|
283
|
+
Then: the bright pixel lands exactly where hand-derived arithmetic says it should
|
|
275
284
|
|
|
276
|
-
|
|
285
|
+
With shift=[2, 3] and no rotation, a pixel at (row=1, col=1) moves to (row=3, col=4).
|
|
286
|
+
This directly tests that the sign convention is correct end-to-end -- that a positive
|
|
287
|
+
shift moves content in the positive direction -- with no ambiguity from interpolation.
|
|
277
288
|
"""
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
"""
|
|
282
|
-
with pytest.raises(ValueError):
|
|
283
|
-
affine_transform_arrays(numpy_array, matrix=np.ones((2, 2)), shear=0.1)
|
|
289
|
+
array = np.zeros((10, 10))
|
|
290
|
+
array[1, 1] = 1.0
|
|
291
|
+
shift = np.array([2.0, 3.0])
|
|
284
292
|
|
|
293
|
+
result = next(apply_affine_transform(array, shift, 0.0, order="shift_then_rotate"))
|
|
285
294
|
|
|
286
|
-
|
|
287
|
-
"""
|
|
288
|
-
Given: a numpy_array
|
|
289
|
-
When: applying the affine transform and specifying both a full matrix and one or more transform parameters
|
|
290
|
-
Then: an error is raised
|
|
291
|
-
"""
|
|
292
|
-
with pytest.raises(ValueError):
|
|
293
|
-
affine_transform_arrays(numpy_array, matrix=np.ones((2, 2)))
|
|
295
|
+
assert result[3, 4] == pytest.approx(1.0)
|
|
294
296
|
|
|
295
297
|
|
|
296
|
-
def
|
|
298
|
+
def test_apply_affine_transform_yields_one_per_input():
|
|
297
299
|
"""
|
|
298
|
-
Given:
|
|
299
|
-
When: applying the
|
|
300
|
-
Then:
|
|
300
|
+
Given: a list of N arrays
|
|
301
|
+
When: applying the correction
|
|
302
|
+
Then: the generator yields exactly N arrays, each with the correct shape
|
|
301
303
|
"""
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
def test_affine_transform_arrays_single_array(numpy_array):
|
|
309
|
-
"""
|
|
310
|
-
Given: a single numpy array
|
|
311
|
-
When: applying the affine transform to an iterable of arrays
|
|
312
|
-
Then: an Iterator containing the transformed array is returned
|
|
313
|
-
"""
|
|
314
|
-
results = affine_transform_arrays(numpy_array, translation=(2, 3))
|
|
315
|
-
result = next(results)
|
|
316
|
-
desired_output = spnd.shift(numpy_array, (2, 3))
|
|
317
|
-
np.testing.assert_allclose(result, desired_output)
|
|
304
|
+
arrays = [np.ones((8, 8)) * i for i in range(3)]
|
|
305
|
+
results = list(
|
|
306
|
+
apply_affine_transform(arrays, np.array([0.0, 0.0]), 0.0, order="shift_then_rotate")
|
|
307
|
+
)
|
|
308
|
+
assert len(results) == 3
|
|
309
|
+
assert all(r.shape == (8, 8) for r in results)
|
|
318
310
|
|
|
319
311
|
|
|
320
|
-
def
|
|
312
|
+
def test_apply_affine_transform_hand_derived_rotation():
|
|
321
313
|
"""
|
|
322
|
-
Given:
|
|
323
|
-
When:
|
|
324
|
-
Then:
|
|
325
|
-
"""
|
|
326
|
-
angle = -45
|
|
327
|
-
p = np.array([5, 2])
|
|
328
|
-
results = rotate_arrays_about_point(numpy_arrays, angle=np.radians(angle), point=p)
|
|
329
|
-
desired_outputs = [transform.rotate(array, angle, center=p) for array in numpy_arrays]
|
|
330
|
-
for result, desired_output in zip(results, desired_outputs):
|
|
331
|
-
np.testing.assert_allclose(result, desired_output)
|
|
332
|
-
|
|
314
|
+
Given: a distinctive single-pixel array and a 90° CCW rotation (no shift)
|
|
315
|
+
When: applying the transform with shift_then_rotate
|
|
316
|
+
Then: the bright pixel lands exactly where hand-derived arithmetic says it should
|
|
333
317
|
|
|
334
|
-
|
|
318
|
+
With angle=π/2 and no shift, a pixel at (row=1, col=7) in a 10×10 array (center=(4.5, 4.5))
|
|
319
|
+
rotates CCW to (row=2, col=1). Derivation: the pull-back maps output (r', c') to input
|
|
320
|
+
(c', 9−r'), so output[2, 1] samples input[1, 7].
|
|
335
321
|
"""
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
Then: an Iterator containing the rotated array is returned
|
|
339
|
-
"""
|
|
340
|
-
angle = -45
|
|
341
|
-
p = np.array([5, 2])
|
|
342
|
-
results = rotate_arrays_about_point(numpy_array, angle=np.radians(angle), point=p)
|
|
343
|
-
result = next(results)
|
|
344
|
-
desired_output = transform.rotate(numpy_array, angle, center=p)
|
|
345
|
-
np.testing.assert_allclose(result, desired_output)
|
|
346
|
-
|
|
322
|
+
array = np.zeros((10, 10))
|
|
323
|
+
array[1, 7] = 1.0
|
|
347
324
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
When: translating an iterable of arrays by a vector
|
|
352
|
-
Then: an Iterator containing the translated arrays is returned
|
|
353
|
-
"""
|
|
354
|
-
results = translate_arrays(numpy_arrays, translation=(5, 2))
|
|
355
|
-
desired_outputs = [spnd.shift(array, (5, 2)) for array in numpy_arrays]
|
|
356
|
-
for result, desired_output in zip(results, desired_outputs):
|
|
357
|
-
np.testing.assert_allclose(result, desired_output)
|
|
325
|
+
result = next(
|
|
326
|
+
apply_affine_transform(array, np.array([0.0, 0.0]), np.pi / 2, order="shift_then_rotate")
|
|
327
|
+
)
|
|
358
328
|
|
|
359
|
-
|
|
360
|
-
def test_translate_arrays_single_array(numpy_array):
|
|
361
|
-
"""
|
|
362
|
-
Given: a single numpy array
|
|
363
|
-
When: translating an iterable of arrays by a vector
|
|
364
|
-
Then: an Iterator containing the translated array is returned
|
|
365
|
-
"""
|
|
366
|
-
results = translate_arrays(numpy_array, translation=(5, 2))
|
|
367
|
-
result = next(results)
|
|
368
|
-
desired_output = spnd.shift(numpy_array, (5, 2))
|
|
369
|
-
np.testing.assert_allclose(result, desired_output)
|
|
329
|
+
assert result[2, 1] == pytest.approx(1.0)
|
|
370
330
|
|
|
371
331
|
|
|
372
|
-
def
|
|
373
|
-
"""
|
|
374
|
-
Given: a single numpy array
|
|
375
|
-
When: creating a scale matrix operator
|
|
376
|
-
Then: a correct scale matrix is produced
|
|
332
|
+
def test_apply_affine_transform_hand_derived_shift_and_rotate():
|
|
377
333
|
"""
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
np.testing.assert_allclose(next(result), next(desired_output))
|
|
334
|
+
Given: a distinctive single-pixel array, a shift of [1, 0], and a 90° CCW rotation
|
|
335
|
+
When: applying shift_then_rotate
|
|
336
|
+
Then: the bright pixel lands exactly where hand-derived arithmetic says it should
|
|
382
337
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
Given: a single numpy array
|
|
387
|
-
When: creating a rotation matrix operator
|
|
388
|
-
Then: a correct rotation matrix is produced
|
|
338
|
+
Shift [1, 0] moves (1, 7) → (2, 7); 90° CCW about center (4.5, 4.5) then moves
|
|
339
|
+
(2, 7) → (2, 2). The pull-back for output (2, 2): after un-centering, applying CW
|
|
340
|
+
rotation, re-centering, and un-shifting gives input (1, 7).
|
|
389
341
|
"""
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
desired_output = affine_transform_arrays(numpy_array, rotation=angle)
|
|
393
|
-
result = affine_transform_arrays(numpy_array, matrix=rotation_mat)
|
|
394
|
-
np.testing.assert_allclose(next(result), next(desired_output))
|
|
342
|
+
array = np.zeros((10, 10))
|
|
343
|
+
array[1, 7] = 1.0
|
|
395
344
|
|
|
345
|
+
result = next(
|
|
346
|
+
apply_affine_transform(array, np.array([1.0, 0.0]), np.pi / 2, order="shift_then_rotate")
|
|
347
|
+
)
|
|
396
348
|
|
|
397
|
-
|
|
398
|
-
"""
|
|
399
|
-
Given: a single numpy array
|
|
400
|
-
When: creating a shear matrix operator
|
|
401
|
-
Then: a correct shear matrix is produced
|
|
402
|
-
"""
|
|
403
|
-
shear = (0.1, 0.2)
|
|
404
|
-
shear_mat = shear_matrix(shear)
|
|
405
|
-
desired_shear_mat = np.array([[1, 0.1, 0], [0.2, 1, 0], [0, 0, 1]])
|
|
406
|
-
np.testing.assert_allclose(shear_mat, desired_shear_mat)
|
|
407
|
-
desired_output = affine_transform_arrays(numpy_array, shear=shear)
|
|
408
|
-
result = affine_transform_arrays(numpy_array, matrix=shear_mat)
|
|
409
|
-
np.testing.assert_allclose(next(result), next(desired_output))
|
|
349
|
+
assert result[2, 2] == pytest.approx(1.0)
|
|
410
350
|
|
|
411
351
|
|
|
412
|
-
def
|
|
352
|
+
def test_affine_inverse_transform_matrix_matches_numpy_inverse():
|
|
413
353
|
"""
|
|
414
|
-
Given: a
|
|
415
|
-
When:
|
|
416
|
-
Then:
|
|
417
|
-
"""
|
|
418
|
-
shear = 0.1
|
|
419
|
-
shear_mat = shear_matrix(shear)
|
|
420
|
-
desired_shear_mat = np.array([[1, 0.1, 0], [0.1, 1, 0], [0, 0, 1]])
|
|
421
|
-
np.testing.assert_allclose(shear_mat, desired_shear_mat)
|
|
422
|
-
desired_output = affine_transform_arrays(numpy_array, shear=shear)
|
|
423
|
-
result = affine_transform_arrays(numpy_array, matrix=shear_mat)
|
|
424
|
-
np.testing.assert_allclose(next(result), next(desired_output))
|
|
425
|
-
|
|
354
|
+
Given: a known shift and rotation angle
|
|
355
|
+
When: computing the inverse transform matrix
|
|
356
|
+
Then: the result matches np.linalg.inv of the hand-composed forward matrix
|
|
426
357
|
|
|
427
|
-
|
|
358
|
+
This verifies the matrix algebra is correct end-to-end, independently of any
|
|
359
|
+
hand-derived expected values.
|
|
428
360
|
"""
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
"""
|
|
433
|
-
trans = (2.3, 5.7)
|
|
434
|
-
trans_mat = translation_matrix(trans)
|
|
435
|
-
desired_output = affine_transform_arrays(numpy_array, translation=trans)
|
|
436
|
-
result = affine_transform_arrays(numpy_array, matrix=trans_mat)
|
|
437
|
-
np.testing.assert_allclose(next(result), next(desired_output))
|
|
361
|
+
shape = (11, 21)
|
|
362
|
+
shift = np.array([3.0, -5.0])
|
|
363
|
+
angle = np.pi / 4
|
|
438
364
|
|
|
365
|
+
cx, cy = (shape[0] - 1) / 2.0, (shape[1] - 1) / 2.0 # 5.0, 10.0
|
|
366
|
+
cos_a, sin_a = np.cos(angle), np.sin(angle)
|
|
439
367
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
trans_mat = translation_matrix(trans)
|
|
448
|
-
desired_output = affine_transform_arrays(numpy_array, translation=trans)
|
|
449
|
-
result = affine_transform_arrays(numpy_array, matrix=trans_mat)
|
|
450
|
-
np.testing.assert_allclose(next(result), next(desired_output))
|
|
368
|
+
# Hand-composed forward matrix for shift_then_rotate:
|
|
369
|
+
# T(center) @ R_fwd @ T(-center) @ T(shift)
|
|
370
|
+
R_fwd = np.array([[cos_a, -sin_a, 0.0], [sin_a, cos_a, 0.0], [0.0, 0.0, 1.0]])
|
|
371
|
+
T_cen = np.array([[1.0, 0.0, cx], [0.0, 1.0, cy], [0.0, 0.0, 1.0]])
|
|
372
|
+
T_neg_cen = np.array([[1.0, 0.0, -cx], [0.0, 1.0, -cy], [0.0, 0.0, 1.0]])
|
|
373
|
+
T_s = np.array([[1.0, 0.0, shift[0]], [0.0, 1.0, shift[1]], [0.0, 0.0, 1.0]])
|
|
374
|
+
forward = T_cen @ R_fwd @ T_neg_cen @ T_s
|
|
451
375
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
"""
|
|
455
|
-
Given: a single numpy array
|
|
456
|
-
When: creating a composition of transform matrices
|
|
457
|
-
Then: a correct transform results from the composition
|
|
458
|
-
"""
|
|
459
|
-
trans = (2.3, 5.7)
|
|
460
|
-
trans_mat = translation_matrix(trans)
|
|
461
|
-
shear = (0.1, 0.2)
|
|
462
|
-
shear_mat = shear_matrix(shear)
|
|
463
|
-
angle = np.radians(30)
|
|
464
|
-
rotation_mat = rotation_matrix(angle)
|
|
465
|
-
scale = (0.2, 0.4)
|
|
466
|
-
scale_mat = scale_matrix(scale)
|
|
467
|
-
# The order in which the transforms are applied reads right to left:
|
|
468
|
-
composition = trans_mat @ rotation_mat @ scale_mat @ shear_mat
|
|
469
|
-
result = affine_transform_arrays(numpy_array, matrix=composition)
|
|
470
|
-
desired_output = affine_transform_arrays(
|
|
471
|
-
numpy_array, scale=scale, shear=shear, rotation=angle, translation=trans
|
|
472
|
-
)
|
|
473
|
-
np.testing.assert_allclose(next(result), next(desired_output))
|
|
376
|
+
result = affine_inverse_transform_matrix(shape, shift, angle, order="shift_then_rotate")
|
|
377
|
+
np.testing.assert_allclose(result, np.linalg.inv(forward), atol=1e-10)
|
|
474
378
|
|
|
475
379
|
|
|
476
380
|
def test_make_binary_image():
|
|
@@ -1,24 +1,14 @@
|
|
|
1
1
|
"""init."""
|
|
2
2
|
|
|
3
|
-
from .affine import
|
|
4
|
-
from .affine import
|
|
5
|
-
from .affine import rotation_matrix
|
|
6
|
-
from .affine import scale_matrix
|
|
7
|
-
from .affine import shear_matrix
|
|
8
|
-
from .affine import translate_arrays
|
|
9
|
-
from .affine import translation_matrix
|
|
3
|
+
from .affine import affine_inverse_transform_matrix
|
|
4
|
+
from .affine import apply_affine_transform
|
|
10
5
|
from .binary import make_binary
|
|
11
6
|
from .binning import bin_arrays
|
|
12
7
|
from .hough import do_hough
|
|
13
8
|
|
|
14
9
|
__all__ = [
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"rotate_arrays_about_point",
|
|
18
|
-
"rotation_matrix",
|
|
19
|
-
"translation_matrix",
|
|
20
|
-
"scale_matrix",
|
|
21
|
-
"shear_matrix",
|
|
10
|
+
"affine_inverse_transform_matrix",
|
|
11
|
+
"apply_affine_transform",
|
|
22
12
|
"make_binary",
|
|
23
13
|
"bin_arrays",
|
|
24
14
|
"do_hough",
|
|
@@ -1,261 +1,186 @@
|
|
|
1
1
|
"""Affine transforms."""
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from
|
|
5
|
-
from typing import
|
|
6
|
-
from typing import Tuple
|
|
7
|
-
from typing import Union
|
|
3
|
+
from collections.abc import Generator
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from typing import Literal
|
|
8
6
|
|
|
9
7
|
import numpy as np
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def affine_transform_arrays(
|
|
14
|
-
arrays: Union[Iterable[np.ndarray], np.ndarray],
|
|
15
|
-
matrix: np.ndarray = None,
|
|
16
|
-
scale: Union[Tuple[float, float], np.ndarray, float] = None,
|
|
17
|
-
translation: Union[Tuple[float, float], np.ndarray, float] = None,
|
|
18
|
-
rotation: float = None,
|
|
19
|
-
shear: Union[Tuple[float, float], np.ndarray, float] = None,
|
|
20
|
-
**kwargs,
|
|
21
|
-
) -> Iterator[np.ndarray]:
|
|
22
|
-
"""
|
|
23
|
-
Transform an iterable of input arrays using a generalized affine transform operator.
|
|
24
|
-
|
|
25
|
-
A transform matrix may be specified or separate parameters for scale, translation,
|
|
26
|
-
rotation and shear may be specified instead. This method abstracts the use of the
|
|
27
|
-
scikit-image affine transform, which may easily be replaced with one from scipy or
|
|
28
|
-
another origin. If transform parameters are specified, they are applied in this order:
|
|
29
|
-
shear, scale, rotation, translation
|
|
8
|
+
import scipy.ndimage as spnd
|
|
30
9
|
|
|
31
|
-
Parameters
|
|
32
|
-
----------
|
|
33
|
-
arrays
|
|
34
|
-
The array(s) to be transformed
|
|
35
|
-
matrix
|
|
36
|
-
The transformation matrix to be used for the transform. If specified, none of
|
|
37
|
-
[translation, rotation, or shear] may be used. Optional. If matrix is not specified,
|
|
38
|
-
at least one of the following must be used: scale, translation, rotation, shear
|
|
39
|
-
scale
|
|
40
|
-
The scale factor to be applied in the transform: (Sx, Sy). If a scalar is used, the same
|
|
41
|
-
value is applied to each axis.
|
|
42
|
-
translation
|
|
43
|
-
The translation to be applied in the transform: (Tx, Ty) in pixel units.
|
|
44
|
-
rotation
|
|
45
|
-
The rotation angle, in radians, to be applied to the transformation.
|
|
46
|
-
A positive angle is counter-clockwise in a right handed coordinate system
|
|
47
|
-
shear
|
|
48
|
-
The shear factor to be applied in the transform: (Shx, Shy). If a scalar is used, the same
|
|
49
|
-
value is applied to each axis.
|
|
50
|
-
**kwargs
|
|
51
|
-
Optional arguments to be passed on to skimage.transform.warp() if desired. See
|
|
52
|
-
https://scikit-image.org/docs/stable/api/skimage.transform.html#skimage.transform.warp
|
|
53
|
-
for more details.
|
|
54
|
-
|
|
55
|
-
Returns
|
|
56
|
-
-------
|
|
57
|
-
The transformed array(s)
|
|
58
10
|
|
|
11
|
+
def affine_inverse_transform_matrix(
|
|
12
|
+
array_shape: tuple[int, ...],
|
|
13
|
+
shift: np.ndarray,
|
|
14
|
+
angle: float,
|
|
15
|
+
order: Literal["rotate_then_shift", "shift_then_rotate"],
|
|
16
|
+
) -> np.ndarray:
|
|
59
17
|
"""
|
|
60
|
-
|
|
61
|
-
any_param_set = any(param is not None for param in [scale, translation, rotation, shear])
|
|
62
|
-
|
|
63
|
-
# We do our own pre-checks here so that we're not totally tied to use scikit-image in the future
|
|
64
|
-
#
|
|
65
|
-
if not any_param_set and matrix is None:
|
|
66
|
-
raise ValueError(
|
|
67
|
-
"You must specify matrix or at least one of scale, rotate, shear, or shift."
|
|
68
|
-
)
|
|
18
|
+
Build the homogeneous-coordinates inverse transform matrix for a center-preserving rotation and shift.
|
|
69
19
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
elif matrix is not None:
|
|
76
|
-
if matrix.shape != (3, 3):
|
|
77
|
-
raise ValueError(f"Invalid shape of transformation matrix: {matrix.shape}")
|
|
78
|
-
tform = skit.AffineTransform(matrix=matrix)
|
|
79
|
-
|
|
80
|
-
elif any_param_set:
|
|
81
|
-
# Construct the matrix from parameters
|
|
82
|
-
if scale is None:
|
|
83
|
-
scale = (1.0, 1.0)
|
|
84
|
-
elif np.isscalar(scale):
|
|
85
|
-
scale = (scale, scale)
|
|
86
|
-
|
|
87
|
-
if translation is None:
|
|
88
|
-
translation = (0.0, 0.0)
|
|
89
|
-
elif np.isscalar(translation):
|
|
90
|
-
translation = (translation, translation)
|
|
91
|
-
|
|
92
|
-
if rotation is None:
|
|
93
|
-
rotation = 0.0
|
|
94
|
-
|
|
95
|
-
if shear is None:
|
|
96
|
-
shear = (0.0, 0.0)
|
|
97
|
-
|
|
98
|
-
translation = translation[::-1] # Because skimage and numpy have different axes ordering
|
|
99
|
-
shear_mat = shear_matrix(shear)
|
|
100
|
-
temp_tform = skit.AffineTransform(scale=scale, translation=translation, rotation=rotation)
|
|
101
|
-
tform_mat = temp_tform.params @ shear_mat
|
|
102
|
-
tform = skit.AffineTransform(matrix=tform_mat)
|
|
103
|
-
|
|
104
|
-
arrays = [arrays] if isinstance(arrays, np.ndarray) else arrays
|
|
105
|
-
partial_warp = partial(skit.warp, inverse_map=tform.inverse, **kwargs)
|
|
106
|
-
return map(partial_warp, arrays)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def rotate_arrays_about_point(
|
|
110
|
-
arrays: Union[Iterable[np.ndarray], np.ndarray],
|
|
111
|
-
angle: float = 0.0,
|
|
112
|
-
point: Union[Tuple[float, float], np.ndarray] = None,
|
|
113
|
-
**kwargs,
|
|
114
|
-
) -> Iterator[np.ndarray]:
|
|
115
|
-
"""
|
|
116
|
-
Rotate an iterable of arrays about a specified point.
|
|
20
|
+
This is the matrix form needed by `scipy.ndimage.affine_transform`'s `matrix` kwarg
|
|
21
|
+
to apply a rotation about the array's center and a shift in a single pass. A positive
|
|
22
|
+
angle rotates content counter-clockwise (the conventional mathematical direction).
|
|
23
|
+
The `order` argument controls which forward operation is being modeled (see below).
|
|
117
24
|
|
|
118
25
|
Parameters
|
|
119
26
|
----------
|
|
120
|
-
|
|
121
|
-
The array
|
|
27
|
+
array_shape
|
|
28
|
+
The shape of the 2D array the transform will be applied to.
|
|
122
29
|
|
|
123
|
-
|
|
124
|
-
The
|
|
125
|
-
|
|
126
|
-
|
|
30
|
+
shift
|
|
31
|
+
The shift to apply, in pixels, with length `ndim` (matching `array_shape`). A positive
|
|
32
|
+
`shift` moves the array's content in the positive direction along each axis (e.g.
|
|
33
|
+
``shift=[0, 1]`` moves content one pixel toward higher column indices).
|
|
127
34
|
|
|
128
|
-
|
|
129
|
-
The
|
|
130
|
-
meaning the center of the array will be used. Note that the origin pixel is centered at
|
|
131
|
-
(0, 0) and has extent: [-0.5: 0.5, -0.5: 0.5]
|
|
35
|
+
angle
|
|
36
|
+
The rotation angle, in radians.
|
|
132
37
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
for more details.
|
|
38
|
+
order
|
|
39
|
+
Whether the modeled forward operation shifts first and then rotates
|
|
40
|
+
(``"shift_then_rotate"``) or rotates first and then shifts (``"rotate_then_shift"``).
|
|
137
41
|
|
|
138
42
|
Returns
|
|
139
43
|
-------
|
|
140
|
-
|
|
44
|
+
A ``(3, 3)`` homogeneous transform matrix encoding the rotation and shift together.
|
|
141
45
|
|
|
142
46
|
"""
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
47
|
+
ndim = len(array_shape)
|
|
48
|
+
|
|
49
|
+
shift = np.asarray(shift, dtype=np.float64)
|
|
50
|
+
if shift.shape != (ndim,):
|
|
51
|
+
raise ValueError(f"shift must have shape ({ndim},) to match array_shape, got {shift.shape}")
|
|
52
|
+
|
|
53
|
+
if order not in ("rotate_then_shift", "shift_then_rotate"):
|
|
54
|
+
raise ValueError(f"order must be 'rotate_then_shift' or 'shift_then_rotate', got {order!r}")
|
|
55
|
+
|
|
56
|
+
if ndim != 2:
|
|
57
|
+
raise ValueError(f"array_shape must be 2D; got {ndim = }.")
|
|
58
|
+
|
|
59
|
+
center = 0.5 * (np.asarray(array_shape, dtype=np.float64) - 1.0)
|
|
60
|
+
|
|
61
|
+
# This function returns the pull-back (inverse) of the requested forward transform, because
|
|
62
|
+
# `scipy.ndimage.affine_transform` maps each *output* pixel coordinate back to the *input*
|
|
63
|
+
# coordinate to sample. The inverse of T(v) is T(-v); the inverse of rotation R is R(-angle).
|
|
64
|
+
# Each matrix below uses its inverse form directly, so the negations are visible at the
|
|
65
|
+
# construction site rather than applied to the variables earlier in the function.
|
|
66
|
+
R = _rotation_matrix(-angle) # R_fwd^-1
|
|
67
|
+
T_uncenter = _translation_matrix(center) # T_fwd(-center)^-1 = T(+center)
|
|
68
|
+
T_center = _translation_matrix(-center) # T_fwd(+center)^-1 = T(-center)
|
|
69
|
+
T_shift = _translation_matrix(-shift) # T_fwd(+shift)^-1 = T(-shift)
|
|
70
|
+
|
|
71
|
+
if order == "shift_then_rotate":
|
|
72
|
+
# Inverse of: T(+center) @ R_fwd @ T(-center) @ T(+shift)
|
|
73
|
+
return T_shift @ T_uncenter @ R @ T_center
|
|
74
|
+
else: # order == "rotate_then_shift"
|
|
75
|
+
# Inverse of: T(+shift) @ T(+center) @ R_fwd @ T(-center)
|
|
76
|
+
return T_uncenter @ R @ T_center @ T_shift
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def apply_affine_transform(
|
|
80
|
+
arrays: Iterable[np.ndarray] | np.ndarray,
|
|
81
|
+
shift: np.ndarray,
|
|
82
|
+
angle: float,
|
|
83
|
+
order: Literal["rotate_then_shift", "shift_then_rotate"],
|
|
84
|
+
interp_order: int = 1,
|
|
85
|
+
mode: str = "nearest",
|
|
86
|
+
cval: float = np.nan,
|
|
87
|
+
) -> Generator[np.ndarray, None, None]:
|
|
174
88
|
"""
|
|
175
|
-
|
|
176
|
-
return affine_transform_arrays(arrays, translation=translation, **kwargs)
|
|
89
|
+
Apply a center-preserving rotation and shift to each input array.
|
|
177
90
|
|
|
91
|
+
Builds a single homogeneous-coordinates inverse transform matrix (via
|
|
92
|
+
`affine_inverse_transform_matrix`) and applies it with one
|
|
93
|
+
`scipy.ndimage.affine_transform` call per array, resolving both the rotation and shift in a
|
|
94
|
+
single interpolation pass. Non-finite values (NaN, ±inf) are replaced before interpolation so
|
|
95
|
+
that they don't spread during resampling.
|
|
178
96
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
Create an Affine Transform matrix for a scale operation.
|
|
97
|
+
The transform matrix is recomputed only when the array shape changes, so passing a sequence of
|
|
98
|
+
same-shaped arrays (the typical case) incurs only one matrix build.
|
|
182
99
|
|
|
183
100
|
Parameters
|
|
184
101
|
----------
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
is applied to both axes.
|
|
188
|
-
|
|
189
|
-
Returns
|
|
190
|
-
-------
|
|
191
|
-
A 3x3 homogeneous transform matrix implementing the scaling
|
|
192
|
-
|
|
193
|
-
"""
|
|
194
|
-
return skit.AffineTransform(scale=scale_factors).params
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def rotation_matrix(angle: float = 0.0) -> np.ndarray:
|
|
198
|
-
"""
|
|
199
|
-
Create an Affine Transform matrix for a rotation operation.
|
|
102
|
+
arrays
|
|
103
|
+
One or more arrays to correct. A single `np.ndarray` is wrapped automatically.
|
|
200
104
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
skimage.warp() method to achieve rotation about an arbitrary point
|
|
105
|
+
shift
|
|
106
|
+
The shift to apply, in pixels. See `affine_inverse_transform_matrix` for convention.
|
|
204
107
|
|
|
205
|
-
Parameters
|
|
206
|
-
----------
|
|
207
108
|
angle
|
|
208
|
-
The angle
|
|
209
|
-
in a right-handed coordinate system and clockwise rotation in a left-handed
|
|
210
|
-
system
|
|
109
|
+
The rotation angle, in radians. See `affine_inverse_transform_matrix` for details.
|
|
211
110
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
111
|
+
order
|
|
112
|
+
Whether the modeled forward operation shifts first then rotates (``"shift_then_rotate"``)
|
|
113
|
+
or rotates first then shifts (``"rotate_then_shift"``).
|
|
215
114
|
|
|
216
|
-
|
|
217
|
-
|
|
115
|
+
interp_order
|
|
116
|
+
Spline interpolation order for `scipy.ndimage.affine_transform`. Must be 0–5.
|
|
117
|
+
0 = nearest-neighbor, 1 = bilinear (default), …, 5 = quintic.
|
|
218
118
|
|
|
119
|
+
mode
|
|
120
|
+
How points outside the array boundary are handled. Passed directly to
|
|
121
|
+
`scipy.ndimage.affine_transform`. Default ``"nearest"``.
|
|
219
122
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
Create an Affine Transform matrix for a shear operation.
|
|
123
|
+
cval
|
|
124
|
+
Fill value used when ``mode="constant"``. Default `np.nan`.
|
|
223
125
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
Returns
|
|
230
|
-
-------
|
|
231
|
-
A 3x3 homogeneous transform matrix implementing the shear
|
|
126
|
+
Yields
|
|
127
|
+
------
|
|
128
|
+
np.ndarray
|
|
129
|
+
Corrected arrays, one per input, each with the same shape as its input.
|
|
232
130
|
|
|
233
131
|
"""
|
|
234
|
-
if isinstance(
|
|
235
|
-
shear_factors = shear_factors, shear_factors
|
|
236
|
-
shear_mat = np.eye(3, 3)
|
|
237
|
-
shear_mat[0, 1] = shear_factors[0]
|
|
238
|
-
shear_mat[1, 0] = shear_factors[1]
|
|
239
|
-
return shear_mat
|
|
132
|
+
arrays = [arrays] if isinstance(arrays, np.ndarray) else arrays
|
|
240
133
|
|
|
134
|
+
previous_shape = None
|
|
135
|
+
matrix = None
|
|
136
|
+
|
|
137
|
+
for array in arrays:
|
|
138
|
+
array = np.asarray(array, dtype=np.float64)
|
|
139
|
+
|
|
140
|
+
# Replace non-finite values before interpolation; affine_transform spreads NaN/inf.
|
|
141
|
+
finite_mask = np.isfinite(array)
|
|
142
|
+
if not finite_mask.all():
|
|
143
|
+
finite_values = array[finite_mask]
|
|
144
|
+
if finite_values.size == 0:
|
|
145
|
+
# Entire array is non-finite -- fill with a safe scalar.
|
|
146
|
+
array.fill(cval if np.isfinite(cval) else 0.0)
|
|
147
|
+
else:
|
|
148
|
+
pos_inf = np.isposinf(array)
|
|
149
|
+
if pos_inf.any():
|
|
150
|
+
array[pos_inf] = finite_values.max()
|
|
151
|
+
neg_inf = np.isneginf(array)
|
|
152
|
+
if neg_inf.any():
|
|
153
|
+
array[neg_inf] = finite_values.min()
|
|
154
|
+
nan_mask = np.isnan(array)
|
|
155
|
+
if nan_mask.any():
|
|
156
|
+
array[nan_mask] = np.nanmedian(array)
|
|
157
|
+
|
|
158
|
+
if array.shape != previous_shape:
|
|
159
|
+
matrix = affine_inverse_transform_matrix(array.shape, shift, angle, order=order)
|
|
160
|
+
previous_shape = array.shape
|
|
161
|
+
|
|
162
|
+
yield spnd.affine_transform(
|
|
163
|
+
array,
|
|
164
|
+
matrix=matrix,
|
|
165
|
+
order=interp_order,
|
|
166
|
+
mode=mode,
|
|
167
|
+
cval=cval,
|
|
168
|
+
prefilter=(interp_order > 1),
|
|
169
|
+
)
|
|
241
170
|
|
|
242
|
-
def translation_matrix(offsets: Union[Tuple[float, float], float] = (1.0, 1.0)) -> np.ndarray:
|
|
243
|
-
"""
|
|
244
|
-
Create an Affine Transform matrix for a translation operation.
|
|
245
171
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
172
|
+
def _rotation_matrix(angle: float) -> np.ndarray:
|
|
173
|
+
"""Build a (3, 3) homogeneous rotation matrix for the given angle in radians."""
|
|
174
|
+
cosine_angle, sine_angle = np.cos(angle), np.sin(angle)
|
|
175
|
+
return np.array(
|
|
176
|
+
[[cosine_angle, -sine_angle, 0.0], [sine_angle, cosine_angle, 0.0], [0.0, 0.0, 1.0]],
|
|
177
|
+
dtype=np.float64,
|
|
178
|
+
)
|
|
250
179
|
|
|
251
|
-
Returns
|
|
252
|
-
-------
|
|
253
|
-
A 3x3 homogeneous transform matrix implementing the translation
|
|
254
180
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
return trans_mat
|
|
181
|
+
def _translation_matrix(shift: np.ndarray) -> np.ndarray:
|
|
182
|
+
"""Build an (ndim+1, ndim+1) homogeneous pure-translation matrix."""
|
|
183
|
+
ndim = len(shift)
|
|
184
|
+
m = np.eye(ndim + 1, dtype=np.float64)
|
|
185
|
+
m[:ndim, ndim] = shift
|
|
186
|
+
return m
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
changelog/.gitempty,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
dkist_processing_math/__init__.py,sha256=z2HXBhrQyX-yBiLTWnPUgVevMA7QI_3ZdJKn3ev1F8I,252
|
|
3
|
-
dkist_processing_math/arithmetic.py,sha256
|
|
3
|
+
dkist_processing_math/arithmetic.py,sha256=-kLO64IT2RxyykH1jlthvC0pegkhyJpIj9oa_9rdAdY,4457
|
|
4
4
|
dkist_processing_math/feature.py,sha256=xdhPkkoOX9aO50BeMjU_2vKwAOG1AUBA3TvfHzZuP6A,2004
|
|
5
5
|
dkist_processing_math/linear_algebra.py,sha256=yCA6ZuNORFnAyXywj73QMmGCpv9ZC2FSoMEPc8wToQY,4180
|
|
6
6
|
dkist_processing_math/statistics.py,sha256=KkZTy8wdslPSQ5qP4z_We60Vizaxqna7rmMwxkoj36U,3178
|
|
7
7
|
dkist_processing_math/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
dkist_processing_math/tests/conftest.py,sha256=TV0o-R2lU1MvZleme0w-akcgO5wOTlFNKhjSlmDwkqg,1593
|
|
9
|
-
dkist_processing_math/tests/test_arithmetic.py,sha256=
|
|
9
|
+
dkist_processing_math/tests/test_arithmetic.py,sha256=IzNm6u1ER5w_zlasOgTXlXSZjmpXnXpUmRb20XPx4DM,7320
|
|
10
10
|
dkist_processing_math/tests/test_feature.py,sha256=ZB8r_IUrAyTAlKbxEXwr8Q2zS7KPmlJhaU1l0guK0Xc,2311
|
|
11
11
|
dkist_processing_math/tests/test_linear_algebra.py,sha256=k-XqIIBPsenNMKqo2nQnDkkN-6T1nvVT8BrJnupAIFU,6495
|
|
12
12
|
dkist_processing_math/tests/test_statistics.py,sha256=N0S8C9Bwhd1VgCAkm6RqIZF_Ulwrcet9wqfbndw8KZs,2998
|
|
13
|
-
dkist_processing_math/tests/test_transform.py,sha256
|
|
14
|
-
dkist_processing_math/transform/__init__.py,sha256=
|
|
15
|
-
dkist_processing_math/transform/affine.py,sha256=
|
|
13
|
+
dkist_processing_math/tests/test_transform.py,sha256=-y-ZZK8GXxWbnBNdA5GDD83IWnoBWFF5YQAZPfxDp8k,15862
|
|
14
|
+
dkist_processing_math/transform/__init__.py,sha256=D7g9xVIvSHF92QKgsNCsYA3hrCys01uN3gkKcOdnyTo,337
|
|
15
|
+
dkist_processing_math/transform/affine.py,sha256=e6VX_34ns3E5UZql1o8KnaiJHO-Lhn5R6xEBooP38Zg,7129
|
|
16
16
|
dkist_processing_math/transform/binary.py,sha256=a_wYYORIvfD8AmQor3t5fMO8LwoFondyX-5awGQeUZk,1207
|
|
17
17
|
dkist_processing_math/transform/binning.py,sha256=--CUP0nYxrWXbou48UJg8liSQJkwxBlDsKRXZTvhRCU,5173
|
|
18
18
|
dkist_processing_math/transform/hough.py,sha256=CUGkoFNLSTeCOF_0L-hT7BtPomB-oAdc0VVb_UfKGiY,1371
|
|
@@ -24,7 +24,7 @@ docs/landing_page.rst,sha256=JCyR9SRhYS6zeEUlonIzko1kQ0tAFDIfi7pCoQanZeA,251
|
|
|
24
24
|
docs/make.bat,sha256=mBAhtURwhQ7yc95pqwJzlhqBSvRknr1aqZ5s8NKvdKs,4513
|
|
25
25
|
docs/requirements.txt,sha256=Kbl_X4c7RQZw035YTeNB63We6I7pvXFU4T0Uflp2yDY,29
|
|
26
26
|
licenses/LICENSE.rst,sha256=piZaQplkzOMmH1NXg6QIdo9wwo9pPCoHkvm2-DmH76E,1462
|
|
27
|
-
dkist_processing_math-
|
|
28
|
-
dkist_processing_math-
|
|
29
|
-
dkist_processing_math-
|
|
30
|
-
dkist_processing_math-
|
|
27
|
+
dkist_processing_math-3.0.0.dist-info/METADATA,sha256=UT0lWh4pkz94qHbdpGz9tdHT6mUGYsReTO5p1SGj-pg,4467
|
|
28
|
+
dkist_processing_math-3.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
29
|
+
dkist_processing_math-3.0.0.dist-info/top_level.txt,sha256=7NeFrOlR4xhZ3LMT4jN99LQugukjr6l9u3w_s6b-uO4,46
|
|
30
|
+
dkist_processing_math-3.0.0.dist-info/RECORD,,
|
{dkist_processing_math-2.2.2.dist-info → dkist_processing_math-3.0.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|