essimaging 24.9.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.
- ess/imaging/__init__.py +12 -0
- ess/imaging/data.py +44 -0
- ess/imaging/io.py +360 -0
- ess/imaging/normalize.py +262 -0
- ess/imaging/py.typed +0 -0
- ess/imaging/types.py +24 -0
- ess/imaging/workflow.py +121 -0
- essimaging-24.9.0.dist-info/LICENSE +29 -0
- essimaging-24.9.0.dist-info/METADATA +80 -0
- essimaging-24.9.0.dist-info/RECORD +12 -0
- essimaging-24.9.0.dist-info/WHEEL +5 -0
- essimaging-24.9.0.dist-info/top_level.txt +1 -0
ess/imaging/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
+
# Copyright (c) 2024 Scipp contributors (https://github.com/scipp)
|
|
3
|
+
# ruff: noqa: E402, F401
|
|
4
|
+
|
|
5
|
+
import importlib.metadata
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
__version__ = importlib.metadata.version("essimaging")
|
|
9
|
+
except importlib.metadata.PackageNotFoundError:
|
|
10
|
+
__version__ = "0.0.0"
|
|
11
|
+
|
|
12
|
+
del importlib
|
ess/imaging/data.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
+
# Copyright (c) 2024 Scipp contributors (https://github.com/scipp)
|
|
3
|
+
import pathlib
|
|
4
|
+
|
|
5
|
+
import pooch
|
|
6
|
+
|
|
7
|
+
_version = '0'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _make_pooch():
|
|
11
|
+
return pooch.create(
|
|
12
|
+
path=pooch.os_cache('essimaging'),
|
|
13
|
+
env='BEAMLIME_DATA_DIR',
|
|
14
|
+
retry_if_failed=3,
|
|
15
|
+
base_url='https://public.esss.dk/groups/scipp/ess/imaging/',
|
|
16
|
+
version=_version,
|
|
17
|
+
registry={
|
|
18
|
+
'small_ymir_images.hdf': 'md5:cf83695d5da29e686c10a31b402b8bdb',
|
|
19
|
+
'README.md': 'md5:0f375972d4008de6060b065ac13ba17f',
|
|
20
|
+
},
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_pooch = _make_pooch()
|
|
25
|
+
_pooch.fetch('README.md')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_path(name: str) -> pathlib.Path:
|
|
29
|
+
"""
|
|
30
|
+
Return the path to a data file bundled with ess.imaging test helpers.
|
|
31
|
+
|
|
32
|
+
This function only works with example data and cannot handle
|
|
33
|
+
paths to custom files.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
return pathlib.Path(_pooch.fetch(name))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_ymir_images_path() -> pathlib.Path:
|
|
40
|
+
"""
|
|
41
|
+
Return the path to the small YMIR images HDF5 file.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
return get_path('small_ymir_images.hdf')
|
ess/imaging/io.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
+
# Copyright (c) 2024 Scipp contributors (https://github.com/scipp)
|
|
3
|
+
import warnings
|
|
4
|
+
from collections.abc import Callable, Generator, Iterable
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from itertools import pairwise
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import NewType
|
|
9
|
+
|
|
10
|
+
import scipp as sc
|
|
11
|
+
import scippnexus as snx
|
|
12
|
+
from tifffile import imwrite
|
|
13
|
+
|
|
14
|
+
from ess.reduce.nexus.types import FilePath
|
|
15
|
+
|
|
16
|
+
from .types import (
|
|
17
|
+
DEFAULT_HISTOGRAM_PATH,
|
|
18
|
+
HistogramModeDetectorsPath,
|
|
19
|
+
ImageDetectorName,
|
|
20
|
+
ImageKeyLogs,
|
|
21
|
+
RotationLogs,
|
|
22
|
+
RotationMotionSensorName,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
FileLock = NewType("FileLock", bool)
|
|
26
|
+
"""File lock mode for reading nexus file."""
|
|
27
|
+
DEFAULT_FILE_LOCK = FileLock(True)
|
|
28
|
+
|
|
29
|
+
HistogramModeDetector = NewType("HistogramModeDetector", sc.DataGroup)
|
|
30
|
+
"""Histogram mode detector data group."""
|
|
31
|
+
HistogramModeDetectorData = NewType("HistogramModeDetectorData", sc.DataArray)
|
|
32
|
+
"""Histogram mode detector data."""
|
|
33
|
+
ImageKeyCoord = NewType("ImageKeyCoord", sc.Variable)
|
|
34
|
+
"""Image key coordinate."""
|
|
35
|
+
SampleImageStacksWithLogs = NewType("SampleImageStacksWithLogs", sc.DataArray)
|
|
36
|
+
"""Raw image stacks separated by ImageKey values via timestamp."""
|
|
37
|
+
RotationAngleCoord = NewType("RotationAngleCoord", sc.Variable)
|
|
38
|
+
"""Rotation angle coordinate."""
|
|
39
|
+
|
|
40
|
+
RawSampleImageStacks = NewType("RawSampleImageStacks", sc.DataArray)
|
|
41
|
+
"""Sample image stacks."""
|
|
42
|
+
OpenBeamImageStacks = NewType("OpenBeamImageStacks", sc.DataArray)
|
|
43
|
+
"""Open beam image stacks."""
|
|
44
|
+
DarkCurrentImageStacks = NewType("DarkCurrentImageStacks", sc.DataArray)
|
|
45
|
+
"""Dark current image stacks."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
IMAGE_KEY_COORD_NAME = "image_key"
|
|
49
|
+
"""Image key coordinate name."""
|
|
50
|
+
TIME_COORD_NAME = "time"
|
|
51
|
+
"""Time coordinate name."""
|
|
52
|
+
ROTATION_ANGLE_COORD_NAME = "rotation_angle"
|
|
53
|
+
"""Rotation angle coordinate name."""
|
|
54
|
+
DIM1_COORD_NAME = "dim_1"
|
|
55
|
+
"""Dimension 1 coordinate name."""
|
|
56
|
+
DIM2_COORD_NAME = "dim_2"
|
|
57
|
+
"""Dimension 2 coordinate name."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ImageKey(Enum):
|
|
61
|
+
"""Image key values."""
|
|
62
|
+
|
|
63
|
+
SAMPLE = 0
|
|
64
|
+
OPEN_BEAM = 1
|
|
65
|
+
DARK_CURRENT = 2
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def as_index(cls, key: "ImageKey", target_da: sc.DataArray | None) -> sc.Variable:
|
|
69
|
+
if target_da is None:
|
|
70
|
+
return sc.scalar(cls(key).value, unit=None)
|
|
71
|
+
elif IMAGE_KEY_COORD_NAME in target_da.coords:
|
|
72
|
+
return sc.scalar(
|
|
73
|
+
cls(key).value,
|
|
74
|
+
unit=target_da.coords[IMAGE_KEY_COORD_NAME].unit,
|
|
75
|
+
dtype=target_da.coords[IMAGE_KEY_COORD_NAME].dtype,
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
return sc.scalar(cls(key).value, unit=target_da.unit, dtype=target_da.dtype)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load_nexus_histogram_mode_detector(
|
|
82
|
+
*,
|
|
83
|
+
file_path: FilePath,
|
|
84
|
+
image_detector_name: ImageDetectorName,
|
|
85
|
+
histogram_mode_detectors_path: HistogramModeDetectorsPath = DEFAULT_HISTOGRAM_PATH,
|
|
86
|
+
locking: FileLock = DEFAULT_FILE_LOCK,
|
|
87
|
+
) -> HistogramModeDetector:
|
|
88
|
+
try:
|
|
89
|
+
with snx.File(file_path, mode="r", locking=locking) as f:
|
|
90
|
+
img_path = f"{histogram_mode_detectors_path}/{image_detector_name}"
|
|
91
|
+
dg: sc.DataGroup = f[img_path][()]
|
|
92
|
+
except PermissionError as e:
|
|
93
|
+
raise PermissionError(
|
|
94
|
+
f"Permission denied to read the nexus file [{file_path}]. "
|
|
95
|
+
"Please check the permission of the file or the directory. "
|
|
96
|
+
"Consider using the `file_lock` parameter to avoid file locking "
|
|
97
|
+
"if the file system is mounted on a network file system. "
|
|
98
|
+
"and it is safe to read the file without locking."
|
|
99
|
+
) from e
|
|
100
|
+
|
|
101
|
+
# Manually assign unit to the histogram detector mode data
|
|
102
|
+
img: sc.DataArray = dg['data']
|
|
103
|
+
if (original_unit := img.unit) != 'counts':
|
|
104
|
+
img.unit = 'counts'
|
|
105
|
+
warnings.warn(
|
|
106
|
+
f"The unit of the histogram detector data is [{original_unit}]. "
|
|
107
|
+
f"It is expected to be [{img.unit}]. "
|
|
108
|
+
f"The loader manually assigned the unit to be [{img.unit}].",
|
|
109
|
+
stacklevel=0,
|
|
110
|
+
)
|
|
111
|
+
return HistogramModeDetector(dg)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
MinDim1 = NewType("MinDim1", sc.Variable | None)
|
|
115
|
+
"""Minimum value of the first dimension."""
|
|
116
|
+
MaxDim1 = NewType("MaxDim1", sc.Variable | None)
|
|
117
|
+
"""Maximum value of the first dimension."""
|
|
118
|
+
MinDim2 = NewType("MinDim2", sc.Variable | None)
|
|
119
|
+
"""Minimum value of the second dimension."""
|
|
120
|
+
MaxDim2 = NewType("MaxDim2", sc.Variable | None)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _make_coord_if_needed(da: sc.DataArray, dim: str) -> None:
|
|
124
|
+
if dim not in da.coords.keys():
|
|
125
|
+
da.coords[dim] = sc.arange(dim=dim, start=0, stop=da.sizes[dim])
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def separate_detector_images(
|
|
129
|
+
dg: HistogramModeDetector,
|
|
130
|
+
min_dim_1: MinDim1,
|
|
131
|
+
max_dim_1: MaxDim1,
|
|
132
|
+
min_dim_2: MinDim2,
|
|
133
|
+
max_dim_2: MaxDim2,
|
|
134
|
+
) -> HistogramModeDetectorData:
|
|
135
|
+
da: sc.DataArray = sc.sort(dg['data'], 'time')
|
|
136
|
+
# Assign position coordinates to the detector data
|
|
137
|
+
_make_coord_if_needed(da, DIM1_COORD_NAME)
|
|
138
|
+
_make_coord_if_needed(da, DIM2_COORD_NAME)
|
|
139
|
+
# Crop the detector data by the given coordinates
|
|
140
|
+
da = da[DIM1_COORD_NAME, min_dim_1:max_dim_1][DIM2_COORD_NAME, min_dim_2:max_dim_2]
|
|
141
|
+
return HistogramModeDetectorData(da)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def separate_image_key_logs(*, dg: HistogramModeDetector) -> ImageKeyLogs:
|
|
145
|
+
return ImageKeyLogs(sc.sort(dg['image_key']['value'], key='time'))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def load_nexus_rotation_logs(
|
|
149
|
+
file_path: FilePath,
|
|
150
|
+
motion_sensor_name: RotationMotionSensorName,
|
|
151
|
+
locking: FileLock = DEFAULT_FILE_LOCK,
|
|
152
|
+
) -> RotationLogs:
|
|
153
|
+
log_path = f"entry/instrument/{motion_sensor_name}/rotation_stage_readback"
|
|
154
|
+
with snx.File(file_path, mode="r", locking=locking) as f:
|
|
155
|
+
return RotationLogs(f[log_path][()]['value'])
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def derive_log_coord_by_range(da: sc.DataArray, log: sc.DataArray) -> sc.Variable:
|
|
159
|
+
"""Sort the logs by time and decide which log entry corresponds to each time bin.
|
|
160
|
+
|
|
161
|
+
It assumes a log value is valid until the next log entry.
|
|
162
|
+
"""
|
|
163
|
+
log = sc.sort(log, TIME_COORD_NAME)
|
|
164
|
+
indices = [*log.coords[TIME_COORD_NAME], None]
|
|
165
|
+
return sc.concat(
|
|
166
|
+
[
|
|
167
|
+
sc.broadcast(
|
|
168
|
+
log.data[TIME_COORD_NAME, i_time],
|
|
169
|
+
dims=[TIME_COORD_NAME],
|
|
170
|
+
shape=(da[TIME_COORD_NAME, start:end].sizes[TIME_COORD_NAME],),
|
|
171
|
+
)
|
|
172
|
+
for i_time, (start, end) in enumerate(pairwise(indices))
|
|
173
|
+
],
|
|
174
|
+
TIME_COORD_NAME,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _slice_da_by_keys(
|
|
179
|
+
da: sc.DataArray, image_keys: ImageKeyLogs, image_key: ImageKey
|
|
180
|
+
) -> Generator[sc.DataArray, None, None]:
|
|
181
|
+
matching_value = image_key.as_index(image_key, image_keys)
|
|
182
|
+
time_coord = image_keys.coords[TIME_COORD_NAME]
|
|
183
|
+
time_intervals = image_keys.sizes[TIME_COORD_NAME]
|
|
184
|
+
for i_time, (cur_time, image_key) in enumerate(
|
|
185
|
+
zip(time_coord, image_keys.data, strict=True)
|
|
186
|
+
):
|
|
187
|
+
if image_key == matching_value:
|
|
188
|
+
if i_time == time_intervals - 1:
|
|
189
|
+
yield da[TIME_COORD_NAME, cur_time:]
|
|
190
|
+
else:
|
|
191
|
+
next_time = time_coord[i_time + 1]
|
|
192
|
+
yield da[TIME_COORD_NAME, cur_time:next_time]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _retrieve_image_stacks_by_key(
|
|
196
|
+
da: HistogramModeDetectorData, image_keys: ImageKeyLogs, image_key: ImageKey
|
|
197
|
+
) -> sc.DataArray:
|
|
198
|
+
images = [
|
|
199
|
+
sliced_da
|
|
200
|
+
for sliced_da in _slice_da_by_keys(da, image_keys, image_key)
|
|
201
|
+
if da.sizes[TIME_COORD_NAME] > 0
|
|
202
|
+
]
|
|
203
|
+
if len(images) == 0:
|
|
204
|
+
raise ValueError(f"No images found for {image_key}.")
|
|
205
|
+
elif len(images) == 1:
|
|
206
|
+
return images[0]
|
|
207
|
+
return sc.concat(images, 'time')
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
AllImageStacks = NewType("AllImageStacks", dict[ImageKey, sc.DataArray])
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def separate_image_by_keys(
|
|
214
|
+
da: HistogramModeDetectorData,
|
|
215
|
+
image_keys: ImageKeyLogs,
|
|
216
|
+
) -> AllImageStacks:
|
|
217
|
+
return AllImageStacks(
|
|
218
|
+
{key: _retrieve_image_stacks_by_key(da, image_keys, key) for key in ImageKey}
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def retrieve_open_beam_images(
|
|
223
|
+
da: HistogramModeDetectorData, image_keys: ImageKeyLogs
|
|
224
|
+
) -> OpenBeamImageStacks:
|
|
225
|
+
return OpenBeamImageStacks(
|
|
226
|
+
_retrieve_image_stacks_by_key(da, image_keys, ImageKey.OPEN_BEAM)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def retrieve_dark_current_images(
|
|
231
|
+
da: HistogramModeDetectorData, image_keys: ImageKeyLogs
|
|
232
|
+
) -> DarkCurrentImageStacks:
|
|
233
|
+
return DarkCurrentImageStacks(
|
|
234
|
+
_retrieve_image_stacks_by_key(da, image_keys, ImageKey.DARK_CURRENT)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def retrieve_sample_images(
|
|
239
|
+
da: HistogramModeDetectorData, image_keys: ImageKeyLogs
|
|
240
|
+
) -> RawSampleImageStacks:
|
|
241
|
+
return RawSampleImageStacks(
|
|
242
|
+
_retrieve_image_stacks_by_key(da, image_keys, ImageKey.SAMPLE)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def apply_logs_as_coords(
|
|
247
|
+
samples: RawSampleImageStacks, rotation_angles: RotationLogs
|
|
248
|
+
) -> SampleImageStacksWithLogs:
|
|
249
|
+
# Make sure the data has the same range as the rotation angle coordinate
|
|
250
|
+
min_log_time = rotation_angles.coords[TIME_COORD_NAME].min(TIME_COORD_NAME)
|
|
251
|
+
sliced = samples[TIME_COORD_NAME, min_log_time:].copy(deep=False)
|
|
252
|
+
if sliced.sizes != samples.sizes:
|
|
253
|
+
warnings.warn(
|
|
254
|
+
"The sample data has been sliced to match the rotation angle coordinate.",
|
|
255
|
+
stacklevel=0,
|
|
256
|
+
)
|
|
257
|
+
rotation_angle_coord = derive_log_coord_by_range(samples, rotation_angles)
|
|
258
|
+
sliced.coords['rotation_angle'] = rotation_angle_coord
|
|
259
|
+
return SampleImageStacksWithLogs(sliced)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
DEFAULT_IMAGE_NAME_PREFIX_MAP = {
|
|
263
|
+
ImageKey.SAMPLE: "sample",
|
|
264
|
+
ImageKey.DARK_CURRENT: "dc",
|
|
265
|
+
ImageKey.OPEN_BEAM: "ob",
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def dummy_progress_wrapper(core_iterator: Iterable) -> Iterable:
|
|
270
|
+
yield from core_iterator
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _save_merged_images(
|
|
274
|
+
*, image_stacks: SampleImageStacksWithLogs, image_prefix: str, output_dir: Path
|
|
275
|
+
) -> None:
|
|
276
|
+
image_path = output_dir / Path(
|
|
277
|
+
f"{image_prefix}_0000_{image_stacks.sizes['time']:04d}.tiff"
|
|
278
|
+
)
|
|
279
|
+
imwrite(image_path, image_stacks.values)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _save_individual_images(
|
|
283
|
+
*,
|
|
284
|
+
image_stacks: SampleImageStacksWithLogs,
|
|
285
|
+
image_prefix: str,
|
|
286
|
+
output_dir: Path,
|
|
287
|
+
progress_wrapper: Callable[[Iterable], Iterable] = dummy_progress_wrapper,
|
|
288
|
+
) -> None:
|
|
289
|
+
for i_image in progress_wrapper(range(image_stacks.sizes['time'])):
|
|
290
|
+
cur_image = image_stacks['time', i_image]
|
|
291
|
+
image_path = output_dir / Path(f"{image_prefix}_{i_image:04d}.tiff")
|
|
292
|
+
imwrite(image_path, cur_image.values)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _validate_output_dir(output_dir: str | Path) -> None:
|
|
296
|
+
output_dir = Path(output_dir)
|
|
297
|
+
if not output_dir.exists():
|
|
298
|
+
# make sure the output directory exists
|
|
299
|
+
output_dir.mkdir(parents=True, exist_ok=False)
|
|
300
|
+
elif not output_dir.is_dir():
|
|
301
|
+
raise ValueError(f"Output directory {output_dir} is not a directory.")
|
|
302
|
+
elif next(output_dir.iterdir(), None) is not None:
|
|
303
|
+
raise RuntimeError(f"Output directory {output_dir} is not empty.")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def export_image_stacks_as_tiff(
|
|
307
|
+
*,
|
|
308
|
+
output_dir: str | Path,
|
|
309
|
+
image_stacks: AllImageStacks,
|
|
310
|
+
merge_image_by_key: bool,
|
|
311
|
+
overwrite: bool,
|
|
312
|
+
progress_wrapper: Callable[[Iterable], Iterable] = dummy_progress_wrapper,
|
|
313
|
+
image_prefix_map: dict[ImageKey, str] = DEFAULT_IMAGE_NAME_PREFIX_MAP,
|
|
314
|
+
) -> None:
|
|
315
|
+
"""Save images into disk.
|
|
316
|
+
|
|
317
|
+
Parameters
|
|
318
|
+
----------
|
|
319
|
+
output_dir:
|
|
320
|
+
Output directory to save images.
|
|
321
|
+
|
|
322
|
+
image_stacks:
|
|
323
|
+
Image stacks to save.
|
|
324
|
+
|
|
325
|
+
merge_image_by_key:
|
|
326
|
+
Flag to merge images into one file.
|
|
327
|
+
|
|
328
|
+
overwrite:
|
|
329
|
+
Flag to overwrite existing files.
|
|
330
|
+
If True, it will clear the output directory before saving images.
|
|
331
|
+
|
|
332
|
+
image_prefix_map:
|
|
333
|
+
Map of image name prefixes to their corresponding image key.
|
|
334
|
+
|
|
335
|
+
"""
|
|
336
|
+
# Remove existing files if overwrite is True
|
|
337
|
+
if (
|
|
338
|
+
overwrite
|
|
339
|
+
and (output_path := Path(output_dir)).exists()
|
|
340
|
+
and output_path.is_dir()
|
|
341
|
+
):
|
|
342
|
+
for file in output_path.iterdir():
|
|
343
|
+
file.unlink()
|
|
344
|
+
|
|
345
|
+
_validate_output_dir(output_path)
|
|
346
|
+
|
|
347
|
+
for image_key, cur_images in progress_wrapper(image_stacks.items()):
|
|
348
|
+
if merge_image_by_key:
|
|
349
|
+
_save_merged_images(
|
|
350
|
+
image_stacks=SampleImageStacksWithLogs(cur_images),
|
|
351
|
+
image_prefix=image_prefix_map[image_key],
|
|
352
|
+
output_dir=output_path,
|
|
353
|
+
)
|
|
354
|
+
else:
|
|
355
|
+
_save_individual_images(
|
|
356
|
+
image_stacks=SampleImageStacksWithLogs(cur_images),
|
|
357
|
+
image_prefix=image_prefix_map[image_key],
|
|
358
|
+
output_dir=output_path,
|
|
359
|
+
progress_wrapper=progress_wrapper,
|
|
360
|
+
)
|
ess/imaging/normalize.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
+
# Copyright (c) 2024 Scipp contributors (https://github.com/scipp)
|
|
3
|
+
|
|
4
|
+
import warnings
|
|
5
|
+
from typing import NewType
|
|
6
|
+
|
|
7
|
+
import scipp as sc
|
|
8
|
+
|
|
9
|
+
from .io import (
|
|
10
|
+
TIME_COORD_NAME,
|
|
11
|
+
DarkCurrentImageStacks,
|
|
12
|
+
OpenBeamImageStacks,
|
|
13
|
+
SampleImageStacksWithLogs,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
AverageBackgroundPixelCounts = NewType("AverageBackgroundPixelCounts", sc.Variable)
|
|
17
|
+
"""mean(background)."""
|
|
18
|
+
AverageSamplePixelCounts = NewType("AverageSamplePixelCounts", sc.Variable)
|
|
19
|
+
"""mean(sample)."""
|
|
20
|
+
ScaleFactor = NewType("ScaleFactor", sc.Variable)
|
|
21
|
+
"""AverageBackgroundPixelCounts / AverageSamplePixelCounts."""
|
|
22
|
+
|
|
23
|
+
OpenBeamImage = NewType("OpenBeamImage", sc.DataArray)
|
|
24
|
+
"""Open beam image. mean(OpenBeam)"""
|
|
25
|
+
DarkCurrentImage = NewType("DarkCurrentImage", sc.DataArray)
|
|
26
|
+
"""Dark current image. mean(DarkCurrentImages)"""
|
|
27
|
+
CleansedOpenBeamImage = NewType("CleansedOpenBeamImage", sc.DataArray)
|
|
28
|
+
"""OpenBeam - DarkCrrent"""
|
|
29
|
+
CleansedSampleImages = NewType("CleansedSampleImages", sc.DataArray)
|
|
30
|
+
"""SampleImageStack - DarkCurrent"""
|
|
31
|
+
SampleImageStacks = NewType("SampleImageStacks", sc.DataArray)
|
|
32
|
+
"""Sample image stack ready to be used for normalization."""
|
|
33
|
+
BackgroundImage = NewType("BackgroundImage", sc.DataArray)
|
|
34
|
+
"""Background image ready to be used for normalization."""
|
|
35
|
+
NormalizedSampleImages = NewType("NormalizedSampleImages", sc.DataArray)
|
|
36
|
+
"""Normalized sample image stack. SampleImages / Background * ScaleFactor"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
BackgroundPixelThreshold = NewType("BackgroundPixelThreshold", sc.Variable)
|
|
40
|
+
"""Threshold of the background pixel values."""
|
|
41
|
+
SamplePixelThreshold = NewType("SamplePixelThreshold", sc.Variable)
|
|
42
|
+
"""Threshold of the sample pixel values."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _warn_constant_exposure_time(target: str) -> None:
|
|
46
|
+
warning_message = f"Computing {target.strip()} assuming constant exposure time."
|
|
47
|
+
warnings.warn(warning_message, stacklevel=1)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _mean_all_dims(data: sc.Variable) -> sc.Variable:
|
|
51
|
+
"""Calculate the mean of all dimensions one by one to avoid overflow."""
|
|
52
|
+
if data.shape == (): # scalar
|
|
53
|
+
return data
|
|
54
|
+
return _mean_all_dims(data.mean(dim=data.dims[0]))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def average_open_beam_images(open_beam: OpenBeamImageStacks) -> OpenBeamImage:
|
|
58
|
+
"""Average the open beam image stack.
|
|
59
|
+
|
|
60
|
+
.. math::
|
|
61
|
+
|
|
62
|
+
OpenBeam = mean(OpenBeam, dim=\\text{'time'})
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
_warn_constant_exposure_time("average open beam image")
|
|
66
|
+
return OpenBeamImage(sc.mean(open_beam, dim=TIME_COORD_NAME))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def average_dark_current_images(
|
|
70
|
+
dark_current: DarkCurrentImageStacks,
|
|
71
|
+
) -> DarkCurrentImage:
|
|
72
|
+
"""Average the dark current image stack.
|
|
73
|
+
|
|
74
|
+
.. math::
|
|
75
|
+
|
|
76
|
+
DarkCurrent = mean(DarkCurrent, dim=\\text{'time'})
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
_warn_constant_exposure_time("average dark current image")
|
|
80
|
+
return DarkCurrentImage(sc.mean(dark_current, dim=TIME_COORD_NAME))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def cleanse_open_beam_image(
|
|
84
|
+
open_beam: OpenBeamImage, dark_current: DarkCurrentImage
|
|
85
|
+
) -> CleansedOpenBeamImage:
|
|
86
|
+
"""Calculate the background image stack.
|
|
87
|
+
|
|
88
|
+
.. math::
|
|
89
|
+
|
|
90
|
+
Background = OpenBeam - DarkCurrent
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
open_beam:
|
|
95
|
+
Open beam image.
|
|
96
|
+
|
|
97
|
+
dark_current:
|
|
98
|
+
Dark current image.
|
|
99
|
+
|
|
100
|
+
"""
|
|
101
|
+
return CleansedOpenBeamImage(open_beam - dark_current)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def cleanse_sample_images(
|
|
105
|
+
sample_images: SampleImageStacksWithLogs, dark_current: DarkCurrentImage
|
|
106
|
+
) -> CleansedSampleImages:
|
|
107
|
+
"""Cleanse the sample image stack.
|
|
108
|
+
|
|
109
|
+
We subtract the averaged dark current image from the sample image stack.
|
|
110
|
+
|
|
111
|
+
.. math::
|
|
112
|
+
|
|
113
|
+
CleansedSample_{i} = Sample_{i} - mean(DarkCurrent, dim=\\text{'time'})
|
|
114
|
+
|
|
115
|
+
\\text{where } i \\text{ is an index of an image.}
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
------
|
|
119
|
+
sample_images:
|
|
120
|
+
Sample image stack.
|
|
121
|
+
|
|
122
|
+
dark_current:
|
|
123
|
+
Dark current image.
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
return CleansedSampleImages(sample_images - dark_current)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def average_background_pixel_counts(
|
|
130
|
+
background: BackgroundImage,
|
|
131
|
+
) -> AverageBackgroundPixelCounts:
|
|
132
|
+
"""Calculate the average background pixel counts."""
|
|
133
|
+
_warn_constant_exposure_time("average background pixel counts")
|
|
134
|
+
return AverageBackgroundPixelCounts(background.data.mean())
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def average_sample_pixel_counts(
|
|
138
|
+
sample_images: SampleImageStacks,
|
|
139
|
+
) -> AverageSamplePixelCounts:
|
|
140
|
+
"""Calculate the average sample pixel counts.
|
|
141
|
+
|
|
142
|
+
Notes
|
|
143
|
+
-----
|
|
144
|
+
For performance reason, we tried calculating
|
|
145
|
+
the mean of sample images and dark current images
|
|
146
|
+
first and subtract them afterwards,
|
|
147
|
+
instead of using the subtracted image stack directly.
|
|
148
|
+
It was to utilize that the integer operation is faster than
|
|
149
|
+
the floating point operation.
|
|
150
|
+
|
|
151
|
+
However, we are ceiling negative values to zero
|
|
152
|
+
after cleansing the sample images with dark current images.
|
|
153
|
+
|
|
154
|
+
Therefore we need to calculate the mean of the cleansed sample images
|
|
155
|
+
to avoid negative values in the average calculation.
|
|
156
|
+
|
|
157
|
+
We don't calculate ``mean(cleansed_sample_images)`` at once
|
|
158
|
+
since it is a large array and it may cause memory issues.
|
|
159
|
+
|
|
160
|
+
There was an example of 361 images of 2048x2048 pixels with 32-bit integer data
|
|
161
|
+
exceeded the limit of the maximum integer so the average calculation failed
|
|
162
|
+
and returned negative values.
|
|
163
|
+
"""
|
|
164
|
+
_warn_constant_exposure_time("average sample pixel counts")
|
|
165
|
+
return AverageSamplePixelCounts(_mean_all_dims(sample_images.data))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def calculate_scale_factor(
|
|
169
|
+
average_bg: AverageBackgroundPixelCounts, average_sample: AverageSamplePixelCounts
|
|
170
|
+
) -> ScaleFactor:
|
|
171
|
+
"""Calculate the scale factor from average background and sample pixel counts.
|
|
172
|
+
|
|
173
|
+
.. math::
|
|
174
|
+
|
|
175
|
+
ScaleFactor = AverageBackgroundPixelCounts / AverageSamplePixelCounts
|
|
176
|
+
|
|
177
|
+
"""
|
|
178
|
+
return ScaleFactor(average_bg / average_sample)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def apply_threshold_to_sample_images(
|
|
182
|
+
samples: CleansedSampleImages, sample_threshold: SamplePixelThreshold
|
|
183
|
+
) -> SampleImageStacks:
|
|
184
|
+
"""Apply the threshold to the sample image stack.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
samples:
|
|
189
|
+
Sample image stack.
|
|
190
|
+
|
|
191
|
+
sample_threshold:
|
|
192
|
+
Threshold for the sample pixel values.
|
|
193
|
+
Any pixel values less than ``sample_threshold``
|
|
194
|
+
are replaced with ``sample_threshold``.
|
|
195
|
+
|
|
196
|
+
"""
|
|
197
|
+
samples = CleansedSampleImages(samples.copy(deep=False))
|
|
198
|
+
samples.data = sc.where(
|
|
199
|
+
samples.data < sample_threshold, sample_threshold, samples.data
|
|
200
|
+
)
|
|
201
|
+
return SampleImageStacks(samples)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def apply_threshold_to_background_image(
|
|
205
|
+
background: CleansedOpenBeamImage, background_threshold: BackgroundPixelThreshold
|
|
206
|
+
) -> BackgroundImage:
|
|
207
|
+
"""Apply the threshold to the background image.
|
|
208
|
+
|
|
209
|
+
Parameters
|
|
210
|
+
----------
|
|
211
|
+
background:
|
|
212
|
+
Background image.
|
|
213
|
+
|
|
214
|
+
background_threshold:
|
|
215
|
+
Threshold for the background pixel values.
|
|
216
|
+
Any pixel values less than ``background_threshold``
|
|
217
|
+
are replaced with ``background_threshold``.
|
|
218
|
+
|
|
219
|
+
"""
|
|
220
|
+
background = CleansedOpenBeamImage(background.copy(deep=False))
|
|
221
|
+
background.data = sc.where(
|
|
222
|
+
background.data < background_threshold, background_threshold, background.data
|
|
223
|
+
)
|
|
224
|
+
return BackgroundImage(background)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def normalize_sample_images(
|
|
228
|
+
*, samples: SampleImageStacks, background: BackgroundImage, factor: ScaleFactor
|
|
229
|
+
) -> NormalizedSampleImages:
|
|
230
|
+
"""Normalize the sample image stack.
|
|
231
|
+
|
|
232
|
+
.. math::
|
|
233
|
+
|
|
234
|
+
NormalizedImages = SampleImages / Background * ScaleFactor
|
|
235
|
+
|
|
236
|
+
Parameters
|
|
237
|
+
----------
|
|
238
|
+
|
|
239
|
+
samples:
|
|
240
|
+
Sample image stack to be normalized.
|
|
241
|
+
|
|
242
|
+
background:
|
|
243
|
+
Background image to be used for normalization.
|
|
244
|
+
|
|
245
|
+
factor:
|
|
246
|
+
Scale factor for the normalization.
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
Raises
|
|
250
|
+
------
|
|
251
|
+
ValueError:
|
|
252
|
+
If the scale factor is negative.
|
|
253
|
+
It is for the safety of the calculation on short data type.
|
|
254
|
+
Depending on how you calculate the scale factor,
|
|
255
|
+
the operation might fail and return negative values.
|
|
256
|
+
|
|
257
|
+
"""
|
|
258
|
+
if factor < 0:
|
|
259
|
+
raise ValueError(f"Scale factor must be positive, but got {factor}.")
|
|
260
|
+
_warn_constant_exposure_time("normalized sample image stack")
|
|
261
|
+
# For performance reason, background / factor is calculated first.
|
|
262
|
+
return NormalizedSampleImages(samples / (background / factor))
|
ess/imaging/py.typed
ADDED
|
File without changes
|
ess/imaging/types.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
+
# Copyright (c) 2024 Scipp contributors (https://github.com/scipp)
|
|
3
|
+
from typing import NewType
|
|
4
|
+
|
|
5
|
+
import scipp as sc
|
|
6
|
+
|
|
7
|
+
ImageDetectorName = NewType('ImageDetectorName', str)
|
|
8
|
+
"""Histogram mode detector name."""
|
|
9
|
+
|
|
10
|
+
ImageKeyLogs = NewType('ImageKeyLogs', sc.DataArray)
|
|
11
|
+
"""Image key logs."""
|
|
12
|
+
|
|
13
|
+
RotationMotionSensorName = NewType('RotationMotionSensorName', str)
|
|
14
|
+
"""Rotation sensor name."""
|
|
15
|
+
|
|
16
|
+
RotationLogs = NewType('RotationLogs', sc.DataArray)
|
|
17
|
+
"""Rotation logs data."""
|
|
18
|
+
|
|
19
|
+
HistogramModeDetectorsPath = NewType('HistogramModeDetectorsPath', str)
|
|
20
|
+
"""Path to the histogram mode detectors in a nexus file."""
|
|
21
|
+
|
|
22
|
+
DEFAULT_HISTOGRAM_PATH = HistogramModeDetectorsPath(
|
|
23
|
+
"/entry/instrument/histogram_mode_detectors"
|
|
24
|
+
)
|
ess/imaging/workflow.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
+
# Copyright (c) 2024 Scipp contributors (https://github.com/scipp)
|
|
3
|
+
import sciline as sl
|
|
4
|
+
import scipp as sc
|
|
5
|
+
|
|
6
|
+
from .io import (
|
|
7
|
+
DEFAULT_FILE_LOCK,
|
|
8
|
+
FileLock,
|
|
9
|
+
MaxDim1,
|
|
10
|
+
MaxDim2,
|
|
11
|
+
MinDim1,
|
|
12
|
+
MinDim2,
|
|
13
|
+
apply_logs_as_coords,
|
|
14
|
+
load_nexus_histogram_mode_detector,
|
|
15
|
+
load_nexus_rotation_logs,
|
|
16
|
+
retrieve_dark_current_images,
|
|
17
|
+
retrieve_open_beam_images,
|
|
18
|
+
retrieve_sample_images,
|
|
19
|
+
separate_detector_images,
|
|
20
|
+
separate_image_by_keys,
|
|
21
|
+
separate_image_key_logs,
|
|
22
|
+
)
|
|
23
|
+
from .normalize import (
|
|
24
|
+
BackgroundPixelThreshold,
|
|
25
|
+
SamplePixelThreshold,
|
|
26
|
+
apply_threshold_to_background_image,
|
|
27
|
+
apply_threshold_to_sample_images,
|
|
28
|
+
average_background_pixel_counts,
|
|
29
|
+
average_dark_current_images,
|
|
30
|
+
average_open_beam_images,
|
|
31
|
+
average_sample_pixel_counts,
|
|
32
|
+
calculate_scale_factor,
|
|
33
|
+
cleanse_open_beam_image,
|
|
34
|
+
cleanse_sample_images,
|
|
35
|
+
normalize_sample_images,
|
|
36
|
+
)
|
|
37
|
+
from .types import (
|
|
38
|
+
DEFAULT_HISTOGRAM_PATH,
|
|
39
|
+
HistogramModeDetectorsPath,
|
|
40
|
+
ImageDetectorName,
|
|
41
|
+
RotationMotionSensorName,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
_IO_PROVIDERS = (
|
|
45
|
+
apply_logs_as_coords,
|
|
46
|
+
load_nexus_histogram_mode_detector,
|
|
47
|
+
load_nexus_rotation_logs,
|
|
48
|
+
retrieve_dark_current_images,
|
|
49
|
+
retrieve_open_beam_images,
|
|
50
|
+
retrieve_sample_images,
|
|
51
|
+
separate_detector_images,
|
|
52
|
+
separate_image_by_keys,
|
|
53
|
+
separate_image_key_logs,
|
|
54
|
+
)
|
|
55
|
+
_NORMALIZATION_PROVIDERS = (
|
|
56
|
+
apply_threshold_to_background_image,
|
|
57
|
+
apply_threshold_to_sample_images,
|
|
58
|
+
average_background_pixel_counts,
|
|
59
|
+
average_dark_current_images,
|
|
60
|
+
average_open_beam_images,
|
|
61
|
+
average_sample_pixel_counts,
|
|
62
|
+
calculate_scale_factor,
|
|
63
|
+
cleanse_open_beam_image,
|
|
64
|
+
cleanse_sample_images,
|
|
65
|
+
normalize_sample_images,
|
|
66
|
+
)
|
|
67
|
+
_DEFAULT_BACKGROUND_THRESHOLD = BackgroundPixelThreshold(sc.scalar(1.0, unit="counts"))
|
|
68
|
+
_DEFAULT_SAMPLE_THRESHOLD = SamplePixelThreshold(sc.scalar(0.0, unit="counts"))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def YmirImageNormalizationWorkflow() -> sl.Pipeline:
|
|
72
|
+
"""
|
|
73
|
+
Ymir histogram mode imaging normalization workflow.
|
|
74
|
+
|
|
75
|
+
Default Normalization Formula
|
|
76
|
+
-----------------------------
|
|
77
|
+
|
|
78
|
+
.. math::
|
|
79
|
+
|
|
80
|
+
NormalizedSample_{i} = SampleImageStacks_{i} / BackgroundImage * ScaleFactor
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
.. math::
|
|
84
|
+
|
|
85
|
+
ScaleFactor = AverageBackgroundPixelCounts / AverageSamplePixelCounts
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
.. math::
|
|
89
|
+
|
|
90
|
+
SampleImageStacks_{i} = Sample_{i} - mean(DarkCurrent, dim=\\text{'time'})
|
|
91
|
+
|
|
92
|
+
\\text{where } i \\text{ is an index of an image.}
|
|
93
|
+
|
|
94
|
+
\\text{Pixel values less than sample_threshold}
|
|
95
|
+
|
|
96
|
+
\\text{ are replaced with sample_threshold}.
|
|
97
|
+
|
|
98
|
+
.. math::
|
|
99
|
+
|
|
100
|
+
BackgroundImage = mean(OpenBeam, dim=\\text{'time'})
|
|
101
|
+
- mean(DarkCurrent, dim=\\text{'time'})
|
|
102
|
+
|
|
103
|
+
\\text{Pixel values less than } \\text{background_threshold}
|
|
104
|
+
|
|
105
|
+
\\text{ are replaced with } \\text{background_threshold}.
|
|
106
|
+
"""
|
|
107
|
+
return sl.Pipeline(
|
|
108
|
+
(*_IO_PROVIDERS, *_NORMALIZATION_PROVIDERS),
|
|
109
|
+
params={
|
|
110
|
+
MinDim1: MinDim1(None),
|
|
111
|
+
MaxDim1: MaxDim1(None),
|
|
112
|
+
MinDim2: MinDim2(None),
|
|
113
|
+
MaxDim2: MaxDim2(None),
|
|
114
|
+
HistogramModeDetectorsPath: DEFAULT_HISTOGRAM_PATH,
|
|
115
|
+
ImageDetectorName: ImageDetectorName('orca'),
|
|
116
|
+
RotationMotionSensorName: RotationMotionSensorName('motion_cabinet_2'),
|
|
117
|
+
BackgroundPixelThreshold: _DEFAULT_BACKGROUND_THRESHOLD,
|
|
118
|
+
SamplePixelThreshold: _DEFAULT_SAMPLE_THRESHOLD,
|
|
119
|
+
FileLock: DEFAULT_FILE_LOCK,
|
|
120
|
+
},
|
|
121
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Scipp contributors (https://github.com/scipp)
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: essimaging
|
|
3
|
+
Version: 24.9.0
|
|
4
|
+
Summary: Imaging data reduction for the European Spallation Source
|
|
5
|
+
Author: Scipp contributors
|
|
6
|
+
License: BSD 3-Clause License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024, Scipp contributors (https://github.com/scipp)
|
|
9
|
+
All rights reserved.
|
|
10
|
+
|
|
11
|
+
Redistribution and use in source and binary forms, with or without
|
|
12
|
+
modification, are permitted provided that the following conditions are met:
|
|
13
|
+
|
|
14
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
15
|
+
list of conditions and the following disclaimer.
|
|
16
|
+
|
|
17
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
18
|
+
this list of conditions and the following disclaimer in the documentation
|
|
19
|
+
and/or other materials provided with the distribution.
|
|
20
|
+
|
|
21
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
22
|
+
contributors may be used to endorse or promote products derived from
|
|
23
|
+
this software without specific prior written permission.
|
|
24
|
+
|
|
25
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
26
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
27
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
28
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
29
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
30
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
31
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
32
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
33
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
34
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
35
|
+
|
|
36
|
+
Project-URL: Bug Tracker, https://github.com/scipp/essimaging/issues
|
|
37
|
+
Project-URL: Documentation, https://scipp.github.io/essimaging
|
|
38
|
+
Project-URL: Source, https://github.com/scipp/essimaging
|
|
39
|
+
Classifier: Intended Audience :: Science/Research
|
|
40
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
41
|
+
Classifier: Natural Language :: English
|
|
42
|
+
Classifier: Operating System :: OS Independent
|
|
43
|
+
Classifier: Programming Language :: Python :: 3
|
|
44
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
45
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
46
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
47
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
48
|
+
Classifier: Topic :: Scientific/Engineering
|
|
49
|
+
Classifier: Typing :: Typed
|
|
50
|
+
Requires-Python: >=3.10
|
|
51
|
+
Description-Content-Type: text/markdown
|
|
52
|
+
License-File: LICENSE
|
|
53
|
+
Requires-Dist: dask
|
|
54
|
+
Requires-Dist: graphviz
|
|
55
|
+
Requires-Dist: plopp[all]
|
|
56
|
+
Requires-Dist: sciline >=23.9.1
|
|
57
|
+
Requires-Dist: scipp >=23.8.0
|
|
58
|
+
Requires-Dist: scippnexus >=23.11.1
|
|
59
|
+
Requires-Dist: essreduce
|
|
60
|
+
Requires-Dist: tifffile
|
|
61
|
+
Provides-Extra: test
|
|
62
|
+
Requires-Dist: pytest ; extra == 'test'
|
|
63
|
+
Requires-Dist: pooch ; extra == 'test'
|
|
64
|
+
|
|
65
|
+
[](CODE_OF_CONDUCT.md)
|
|
66
|
+
[](https://pypi.python.org/pypi/essimaging)
|
|
67
|
+
[](https://anaconda.org/scipp/essimaging)
|
|
68
|
+
[](LICENSE)
|
|
69
|
+
|
|
70
|
+
# ESSimaging
|
|
71
|
+
|
|
72
|
+
## About
|
|
73
|
+
|
|
74
|
+
Imaging data reduction for the European Spallation Source
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
python -m pip install essimaging
|
|
80
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
ess/imaging/__init__.py,sha256=x8w4NyzTnfZ3IDYuwfo_TftgHFUgh2maZBR0Ej82_8w,313
|
|
2
|
+
ess/imaging/data.py,sha256=OxKAm-NBSGOKWQ6AmHnnxMXBd3TR7WNyYqpERO_rZ9U,1060
|
|
3
|
+
ess/imaging/io.py,sha256=hZku7nr0KYmxSnXxONGGvUZgrGQAe31zP9NYh0DiRa0,12203
|
|
4
|
+
ess/imaging/normalize.py,sha256=uCI_PxoLN_M4wMxz0dYar-TosS4dzWvB0_jvGRcjxek,8084
|
|
5
|
+
ess/imaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
ess/imaging/types.py,sha256=HI3o6wcqRgszf5x7nx13xtjKqhyWNdf9C4qsIpu2Pvo,736
|
|
7
|
+
ess/imaging/workflow.py,sha256=ImrA_jH-NmBGF6JpjtN2ASqVcwUE8ieZF_ktw5_et0k,3455
|
|
8
|
+
essimaging-24.9.0.dist-info/LICENSE,sha256=nVEiume4Qj6jMYfSRjHTM2jtJ4FGu0g-5Sdh7osfEYw,1553
|
|
9
|
+
essimaging-24.9.0.dist-info/METADATA,sha256=1swB3bGlyob0_zrXzLJtZpX7RrTzlmv2dwa-0BuSYRU,3637
|
|
10
|
+
essimaging-24.9.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
11
|
+
essimaging-24.9.0.dist-info/top_level.txt,sha256=0JxTCgMKPLKtp14wb1-RKisQPQWX7i96innZNvHBr-s,4
|
|
12
|
+
essimaging-24.9.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ess
|