Skip to content

Commit 92818cd

Browse files
authored
Merge pull request #42 from scipp/normalization
Normalization step in the YMIR image workflow.
2 parents df1d69b + 58a36e0 commit 92818cd

File tree

6 files changed

+766
-24
lines changed

6 files changed

+766
-24
lines changed

docs/api-reference/index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,8 @@
2626
:toctree: ../generated/modules
2727
:template: module-template.rst
2828
:recursive:
29+
30+
normalize
31+
io
32+
workflow
2933
```

docs/user-guide/histogram_mode_detector.ipynb

Lines changed: 115 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,77 @@
66
"source": [
77
"# Histogram Mode Detector\n",
88
"\n",
9+
"## TL;DR - Get normalized sample images averaged along the rotation angle."
10+
]
11+
},
12+
{
13+
"cell_type": "code",
14+
"execution_count": null,
15+
"metadata": {},
16+
"outputs": [],
17+
"source": [
18+
"from ess.imaging.data import get_ymir_images_path\n",
19+
"from ess.imaging.io import FilePath\n",
20+
"from ess.imaging.normalize import NormalizedSampleImages\n",
21+
"from ess.imaging.workflow import (\n",
22+
" YmirImageNormalizationWorkflow,\n",
23+
" ImageDetectorName,\n",
24+
" RotationMotionSensorName,\n",
25+
")\n",
26+
"\n",
27+
"wf = YmirImageNormalizationWorkflow()\n",
28+
"wf[FilePath] = (\n",
29+
" get_ymir_images_path()\n",
30+
") # Replace with the path to your images in nexus file.\n",
31+
"wf[ImageDetectorName] = 'orca'\n",
32+
"wf[RotationMotionSensorName] = 'motion_cabinet_2'\n",
33+
"normalized = wf.compute(NormalizedSampleImages)\n",
34+
"normalized"
35+
]
36+
},
37+
{
38+
"cell_type": "code",
39+
"execution_count": null,
40+
"metadata": {},
41+
"outputs": [],
42+
"source": [
43+
"normalized['time', 0].plot(\n",
44+
" title=f'Normalized Sample Image at Rotation Angle of {normalized.coords[\"rotation_angle\"].values[0]:.3f}'\n",
45+
")"
46+
]
47+
},
48+
{
49+
"cell_type": "markdown",
50+
"metadata": {},
51+
"source": [
52+
"You can use a slicer to browse images along the rotation angle.\n",
53+
"\n",
54+
"```python\n",
55+
"%matplotlib widget\n",
56+
"from plopp import slicer\n",
57+
"\n",
58+
"slicer(normalized)\n",
59+
"```\n",
60+
"\n",
61+
"If you want to bin the images by linear steps of angles,\n",
62+
"you can use ``sc.groupby`` like below.\n",
63+
"\n",
64+
"```python\n",
65+
"%matplotlib widget\n",
66+
"import scipp as sc\n",
67+
"from plopp import slicer\n",
68+
"\n",
69+
"slicer(normalized.groupby(\n",
70+
" 'rotation_angle',\n",
71+
" bins=sc.linspace(dim='rotation_angle', start=-0.5, stop=4.5, num=5, unit='deg'),\n",
72+
").mean('rotation_angle'))\n",
73+
"```"
74+
]
75+
},
76+
{
77+
"cell_type": "markdown",
78+
"metadata": {},
79+
"source": [
980
"## IO\n",
1081
"\n",
1182
"I/O providers can load white beam images and slice them according to the image key.\n",
@@ -14,7 +85,9 @@
1485
"\n",
1586
"Instead, we slice the data according to the ``image_key`` and separate image stacks earlier due to performance limitation of 3-dimensional data binning.\n",
1687
"\n",
17-
"> For example, for 400 images of 2_048 x 2_048 pixels couldn't be done in the regular laptop."
88+
"> For example, for 400 images of 2_048 x 2_048 pixels couldn't be done in the regular laptop.\n",
89+
"\n",
90+
"### Load the data as a stack of images"
1891
]
1992
},
2093
{
@@ -26,19 +99,19 @@
2699
"from ess.imaging.data import get_ymir_images_path\n",
27100
"from ess.imaging.io import (\n",
28101
" FilePath,\n",
29-
" SampleImageStacks,\n",
102+
" SampleImageStacksWithLogs,\n",
30103
" RawSampleImageStacks,\n",
31104
" OpenBeamImageStacks,\n",
32105
" DarkCurrentImageStacks,\n",
33106
" AllImageStacks,\n",
34107
")\n",
35108
"from ess.imaging.workflow import (\n",
36-
" YmirWorkflow,\n",
109+
" YmirImageNormalizationWorkflow,\n",
37110
" ImageDetectorName,\n",
38111
" RotationMotionSensorName,\n",
39112
")\n",
40113
"\n",
41-
"wf = YmirWorkflow()\n",
114+
"wf = YmirImageNormalizationWorkflow()\n",
42115
"wf[FilePath] = get_ymir_images_path()\n",
43116
"wf[ImageDetectorName] = 'orca'\n",
44117
"wf[RotationMotionSensorName] = 'motion_cabinet_2'"
@@ -55,7 +128,7 @@
55128
" RawSampleImageStacks,\n",
56129
" OpenBeamImageStacks,\n",
57130
" DarkCurrentImageStacks,\n",
58-
" SampleImageStacks,\n",
131+
" SampleImageStacksWithLogs,\n",
59132
")\n",
60133
"wf.visualize(image_stack_types)"
61134
]
@@ -67,14 +140,14 @@
67140
"outputs": [],
68141
"source": [
69142
"results = wf.compute(image_stack_types)\n",
70-
"results[SampleImageStacks]"
143+
"results[SampleImageStacksWithLogs]"
71144
]
72145
},
73146
{
74147
"cell_type": "markdown",
75148
"metadata": {},
76149
"source": [
77-
"## Save Individual Frame"
150+
"### Save Individual Frame"
78151
]
79152
},
80153
{
@@ -117,7 +190,7 @@
117190
"cell_type": "markdown",
118191
"metadata": {},
119192
"source": [
120-
"## Save All Frames in One File"
193+
"### Save All Frames in One File"
121194
]
122195
},
123196
{
@@ -167,11 +240,43 @@
167240
"source": [
168241
"!rm -rf images"
169242
]
243+
},
244+
{
245+
"cell_type": "markdown",
246+
"metadata": {},
247+
"source": [
248+
"## Normalization\n",
249+
"\n",
250+
"See :attr:ess.imaging.normalize.normalize_sample_images for more details."
251+
]
252+
},
253+
{
254+
"cell_type": "code",
255+
"execution_count": null,
256+
"metadata": {},
257+
"outputs": [],
258+
"source": [
259+
"from ess.imaging.normalize import NormalizedSampleImages\n",
260+
"\n",
261+
"wf[OpenBeamImageStacks] = results[OpenBeamImageStacks]\n",
262+
"wf[DarkCurrentImageStacks] = results[DarkCurrentImageStacks]\n",
263+
"wf[SampleImageStacksWithLogs] = results[SampleImageStacksWithLogs]\n",
264+
"wf.visualize(NormalizedSampleImages)"
265+
]
266+
},
267+
{
268+
"cell_type": "code",
269+
"execution_count": null,
270+
"metadata": {},
271+
"outputs": [],
272+
"source": [
273+
"wf.compute(NormalizedSampleImages)"
274+
]
170275
}
171276
],
172277
"metadata": {
173278
"kernelspec": {
174-
"display_name": "img-dev-310",
279+
"display_name": "Python 3 (ipykernel)",
175280
"language": "python",
176281
"name": "python3"
177282
},
@@ -189,5 +294,5 @@
189294
}
190295
},
191296
"nbformat": 4,
192-
"nbformat_minor": 2
297+
"nbformat_minor": 4
193298
}

src/ess/imaging/io.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,18 @@
2222
RotationMotionSensorName,
2323
)
2424

25+
FileLock = NewType("FileLock", bool)
26+
"""File lock mode for reading nexus file."""
27+
DEFAULT_FILE_LOCK = FileLock(True)
28+
2529
HistogramModeDetector = NewType("HistogramModeDetector", sc.DataGroup)
2630
"""Histogram mode detector data group."""
2731
HistogramModeDetectorData = NewType("HistogramModeDetectorData", sc.DataArray)
2832
"""Histogram mode detector data."""
2933
ImageKeyCoord = NewType("ImageKeyCoord", sc.Variable)
3034
"""Image key coordinate."""
31-
SampleImageStacks = NewType("SampleImageStacks", sc.DataArray)
32-
"""Image stacks separated by ImageKey values via timestamp."""
35+
SampleImageStacksWithLogs = NewType("SampleImageStacksWithLogs", sc.DataArray)
36+
"""Raw image stacks separated by ImageKey values via timestamp."""
3337
RotationAngleCoord = NewType("RotationAngleCoord", sc.Variable)
3438
"""Rotation angle coordinate."""
3539

@@ -79,10 +83,20 @@ def load_nexus_histogram_mode_detector(
7983
file_path: FilePath,
8084
image_detector_name: ImageDetectorName,
8185
histogram_mode_detectors_path: HistogramModeDetectorsPath = DEFAULT_HISTOGRAM_PATH,
86+
locking: FileLock = DEFAULT_FILE_LOCK,
8287
) -> HistogramModeDetector:
83-
with snx.File(file_path, mode="r") as f:
84-
img_path = f"{histogram_mode_detectors_path}/{image_detector_name}"
85-
dg: sc.DataGroup = f[img_path][()]
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
86100

87101
# Manually assign unit to the histogram detector mode data
88102
img: sc.DataArray = dg['data']
@@ -134,9 +148,10 @@ def separate_image_key_logs(*, dg: HistogramModeDetector) -> ImageKeyLogs:
134148
def load_nexus_rotation_logs(
135149
file_path: FilePath,
136150
motion_sensor_name: RotationMotionSensorName,
151+
locking: FileLock = DEFAULT_FILE_LOCK,
137152
) -> RotationLogs:
138153
log_path = f"entry/instrument/{motion_sensor_name}/rotation_stage_readback"
139-
with snx.File(file_path, mode="r") as f:
154+
with snx.File(file_path, mode="r", locking=locking) as f:
140155
return RotationLogs(f[log_path][()]['value'])
141156

142157

@@ -230,7 +245,7 @@ def retrieve_sample_images(
230245

231246
def apply_logs_as_coords(
232247
samples: RawSampleImageStacks, rotation_angles: RotationLogs
233-
) -> SampleImageStacks:
248+
) -> SampleImageStacksWithLogs:
234249
# Make sure the data has the same range as the rotation angle coordinate
235250
min_log_time = rotation_angles.coords[TIME_COORD_NAME].min(TIME_COORD_NAME)
236251
sliced = samples[TIME_COORD_NAME, min_log_time:].copy(deep=False)
@@ -241,7 +256,7 @@ def apply_logs_as_coords(
241256
)
242257
rotation_angle_coord = derive_log_coord_by_range(samples, rotation_angles)
243258
sliced.coords['rotation_angle'] = rotation_angle_coord
244-
return SampleImageStacks(sliced)
259+
return SampleImageStacksWithLogs(sliced)
245260

246261

247262
DEFAULT_IMAGE_NAME_PREFIX_MAP = {
@@ -256,7 +271,7 @@ def dummy_progress_wrapper(core_iterator: Iterable) -> Iterable:
256271

257272

258273
def _save_merged_images(
259-
*, image_stacks: SampleImageStacks, image_prefix: str, output_dir: Path
274+
*, image_stacks: SampleImageStacksWithLogs, image_prefix: str, output_dir: Path
260275
) -> None:
261276
image_path = output_dir / Path(
262277
f"{image_prefix}_0000_{image_stacks.sizes['time']:04d}.tiff"
@@ -266,7 +281,7 @@ def _save_merged_images(
266281

267282
def _save_individual_images(
268283
*,
269-
image_stacks: SampleImageStacks,
284+
image_stacks: SampleImageStacksWithLogs,
270285
image_prefix: str,
271286
output_dir: Path,
272287
progress_wrapper: Callable[[Iterable], Iterable] = dummy_progress_wrapper,
@@ -332,13 +347,13 @@ def export_image_stacks_as_tiff(
332347
for image_key, cur_images in progress_wrapper(image_stacks.items()):
333348
if merge_image_by_key:
334349
_save_merged_images(
335-
image_stacks=SampleImageStacks(cur_images),
350+
image_stacks=SampleImageStacksWithLogs(cur_images),
336351
image_prefix=image_prefix_map[image_key],
337352
output_dir=output_path,
338353
)
339354
else:
340355
_save_individual_images(
341-
image_stacks=SampleImageStacks(cur_images),
356+
image_stacks=SampleImageStacksWithLogs(cur_images),
342357
image_prefix=image_prefix_map[image_key],
343358
output_dir=output_path,
344359
progress_wrapper=progress_wrapper,

0 commit comments

Comments
 (0)