dkist-processing-cryonirsp 1.3.4__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.
Potentially problematic release.
This version of dkist-processing-cryonirsp might be problematic. Click here for more details.
- changelog/.gitempty +0 -0
- dkist_processing_cryonirsp/__init__.py +11 -0
- dkist_processing_cryonirsp/config.py +12 -0
- dkist_processing_cryonirsp/models/__init__.py +1 -0
- dkist_processing_cryonirsp/models/constants.py +248 -0
- dkist_processing_cryonirsp/models/exposure_conditions.py +26 -0
- dkist_processing_cryonirsp/models/parameters.py +296 -0
- dkist_processing_cryonirsp/models/tags.py +168 -0
- dkist_processing_cryonirsp/models/task_name.py +14 -0
- dkist_processing_cryonirsp/parsers/__init__.py +1 -0
- dkist_processing_cryonirsp/parsers/cryonirsp_l0_fits_access.py +111 -0
- dkist_processing_cryonirsp/parsers/cryonirsp_l1_fits_access.py +30 -0
- dkist_processing_cryonirsp/parsers/exposure_conditions.py +163 -0
- dkist_processing_cryonirsp/parsers/map_repeats.py +40 -0
- dkist_processing_cryonirsp/parsers/measurements.py +55 -0
- dkist_processing_cryonirsp/parsers/modstates.py +31 -0
- dkist_processing_cryonirsp/parsers/optical_density_filters.py +40 -0
- dkist_processing_cryonirsp/parsers/polarimetric_check.py +120 -0
- dkist_processing_cryonirsp/parsers/scan_step.py +412 -0
- dkist_processing_cryonirsp/parsers/time.py +80 -0
- dkist_processing_cryonirsp/parsers/wavelength.py +26 -0
- dkist_processing_cryonirsp/tasks/__init__.py +19 -0
- dkist_processing_cryonirsp/tasks/assemble_movie.py +202 -0
- dkist_processing_cryonirsp/tasks/bad_pixel_map.py +96 -0
- dkist_processing_cryonirsp/tasks/beam_boundaries_base.py +279 -0
- dkist_processing_cryonirsp/tasks/ci_beam_boundaries.py +55 -0
- dkist_processing_cryonirsp/tasks/ci_science.py +169 -0
- dkist_processing_cryonirsp/tasks/cryonirsp_base.py +67 -0
- dkist_processing_cryonirsp/tasks/dark.py +98 -0
- dkist_processing_cryonirsp/tasks/gain.py +251 -0
- dkist_processing_cryonirsp/tasks/instrument_polarization.py +447 -0
- dkist_processing_cryonirsp/tasks/l1_output_data.py +44 -0
- dkist_processing_cryonirsp/tasks/linearity_correction.py +582 -0
- dkist_processing_cryonirsp/tasks/make_movie_frames.py +302 -0
- dkist_processing_cryonirsp/tasks/mixin/__init__.py +1 -0
- dkist_processing_cryonirsp/tasks/mixin/beam_access.py +52 -0
- dkist_processing_cryonirsp/tasks/mixin/corrections.py +177 -0
- dkist_processing_cryonirsp/tasks/mixin/intermediate_frame.py +193 -0
- dkist_processing_cryonirsp/tasks/mixin/linearized_frame.py +309 -0
- dkist_processing_cryonirsp/tasks/mixin/shift_measurements.py +297 -0
- dkist_processing_cryonirsp/tasks/parse.py +281 -0
- dkist_processing_cryonirsp/tasks/quality_metrics.py +271 -0
- dkist_processing_cryonirsp/tasks/science_base.py +511 -0
- dkist_processing_cryonirsp/tasks/sp_beam_boundaries.py +270 -0
- dkist_processing_cryonirsp/tasks/sp_dispersion_axis_correction.py +484 -0
- dkist_processing_cryonirsp/tasks/sp_geometric.py +585 -0
- dkist_processing_cryonirsp/tasks/sp_science.py +299 -0
- dkist_processing_cryonirsp/tasks/sp_solar_gain.py +475 -0
- dkist_processing_cryonirsp/tasks/trial_output_data.py +61 -0
- dkist_processing_cryonirsp/tasks/write_l1.py +1033 -0
- dkist_processing_cryonirsp/tests/__init__.py +1 -0
- dkist_processing_cryonirsp/tests/conftest.py +456 -0
- dkist_processing_cryonirsp/tests/header_models.py +592 -0
- dkist_processing_cryonirsp/tests/local_trial_workflows/__init__.py +0 -0
- dkist_processing_cryonirsp/tests/local_trial_workflows/l0_cals_only.py +541 -0
- dkist_processing_cryonirsp/tests/local_trial_workflows/l0_to_l1.py +615 -0
- dkist_processing_cryonirsp/tests/local_trial_workflows/linearize_only.py +96 -0
- dkist_processing_cryonirsp/tests/local_trial_workflows/local_trial_helpers.py +592 -0
- dkist_processing_cryonirsp/tests/test_assemble_movie.py +144 -0
- dkist_processing_cryonirsp/tests/test_assemble_qualilty.py +517 -0
- dkist_processing_cryonirsp/tests/test_bad_pixel_maps.py +115 -0
- dkist_processing_cryonirsp/tests/test_ci_beam_boundaries.py +106 -0
- dkist_processing_cryonirsp/tests/test_ci_science.py +355 -0
- dkist_processing_cryonirsp/tests/test_corrections.py +126 -0
- dkist_processing_cryonirsp/tests/test_cryo_base.py +202 -0
- dkist_processing_cryonirsp/tests/test_cryo_constants.py +76 -0
- dkist_processing_cryonirsp/tests/test_dark.py +287 -0
- dkist_processing_cryonirsp/tests/test_gain.py +278 -0
- dkist_processing_cryonirsp/tests/test_instrument_polarization.py +531 -0
- dkist_processing_cryonirsp/tests/test_linearity_correction.py +245 -0
- dkist_processing_cryonirsp/tests/test_make_movie_frames.py +111 -0
- dkist_processing_cryonirsp/tests/test_parameters.py +266 -0
- dkist_processing_cryonirsp/tests/test_parse.py +1439 -0
- dkist_processing_cryonirsp/tests/test_quality.py +203 -0
- dkist_processing_cryonirsp/tests/test_sp_beam_boundaries.py +112 -0
- dkist_processing_cryonirsp/tests/test_sp_dispersion_axis_correction.py +155 -0
- dkist_processing_cryonirsp/tests/test_sp_geometric.py +319 -0
- dkist_processing_cryonirsp/tests/test_sp_make_movie_frames.py +121 -0
- dkist_processing_cryonirsp/tests/test_sp_science.py +483 -0
- dkist_processing_cryonirsp/tests/test_sp_solar.py +198 -0
- dkist_processing_cryonirsp/tests/test_trial_create_quality_report.py +79 -0
- dkist_processing_cryonirsp/tests/test_trial_output_data.py +251 -0
- dkist_processing_cryonirsp/tests/test_workflows.py +9 -0
- dkist_processing_cryonirsp/tests/test_write_l1.py +436 -0
- dkist_processing_cryonirsp/workflows/__init__.py +2 -0
- dkist_processing_cryonirsp/workflows/ci_l0_processing.py +77 -0
- dkist_processing_cryonirsp/workflows/sp_l0_processing.py +84 -0
- dkist_processing_cryonirsp/workflows/trial_workflows.py +190 -0
- dkist_processing_cryonirsp-1.3.4.dist-info/METADATA +194 -0
- dkist_processing_cryonirsp-1.3.4.dist-info/RECORD +111 -0
- dkist_processing_cryonirsp-1.3.4.dist-info/WHEEL +5 -0
- dkist_processing_cryonirsp-1.3.4.dist-info/top_level.txt +4 -0
- docs/Makefile +134 -0
- docs/bad_pixel_calibration.rst +47 -0
- docs/beam_angle_calculation.rst +53 -0
- docs/beam_boundary_computation.rst +88 -0
- docs/changelog.rst +7 -0
- docs/ci_science_calibration.rst +33 -0
- docs/conf.py +52 -0
- docs/index.rst +21 -0
- docs/l0_to_l1_cryonirsp_ci-full-trial.rst +10 -0
- docs/l0_to_l1_cryonirsp_ci.rst +10 -0
- docs/l0_to_l1_cryonirsp_sp-full-trial.rst +10 -0
- docs/l0_to_l1_cryonirsp_sp.rst +10 -0
- docs/linearization.rst +43 -0
- docs/make.bat +170 -0
- docs/requirements.txt +1 -0
- docs/requirements_table.rst +8 -0
- docs/scientific_changelog.rst +10 -0
- docs/sp_science_calibration.rst +59 -0
- licenses/LICENSE.rst +11 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""CryoNIRSP-specific assemble movie task subclass."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from dkist_processing_common.codecs.fits import fits_array_decoder
|
|
4
|
+
from dkist_processing_common.tasks import AssembleMovie
|
|
5
|
+
from dkist_service_configuration.logging import logger
|
|
6
|
+
from PIL import ImageDraw
|
|
7
|
+
from PIL.ImageFont import FreeTypeFont
|
|
8
|
+
|
|
9
|
+
from dkist_processing_cryonirsp.models.constants import CryonirspConstants
|
|
10
|
+
from dkist_processing_cryonirsp.models.tags import CryonirspTag
|
|
11
|
+
from dkist_processing_cryonirsp.parsers.cryonirsp_l0_fits_access import CryonirspL0FitsAccess
|
|
12
|
+
from dkist_processing_cryonirsp.parsers.cryonirsp_l1_fits_access import CryonirspL1FitsAccess
|
|
13
|
+
|
|
14
|
+
__all__ = ["AssembleCryonirspMovie", "SPAssembleCryonirspMovie"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AssembleCryonirspMovieBase(AssembleMovie):
|
|
18
|
+
"""
|
|
19
|
+
Assemble all CryoNIRSP movie frames (tagged with CryonirspTag.movie_frame()) into an mp4 movie file.
|
|
20
|
+
|
|
21
|
+
Subclassed from the AssembleMovie task in dkist_processing_common to add CryoNIRSP specific text overlays.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
recipe_run_id : int
|
|
27
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
28
|
+
workflow_name : str
|
|
29
|
+
name of the workflow to which this instance of the task belongs
|
|
30
|
+
workflow_version : str
|
|
31
|
+
version of the workflow to which this instance of the task belongs
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
MPL_COLOR_MAP = "gray"
|
|
35
|
+
|
|
36
|
+
def compute_frame_shape(self) -> tuple[int, int]:
|
|
37
|
+
"""Dynamically set the dimensions of the movie based on L1 file shape."""
|
|
38
|
+
movie_frame_arrays = self.read(
|
|
39
|
+
tags=[CryonirspTag.movie_frame()], decoder=fits_array_decoder
|
|
40
|
+
)
|
|
41
|
+
random_frame = next(movie_frame_arrays)
|
|
42
|
+
raw_L1_shape = random_frame.shape
|
|
43
|
+
flipped_shape = raw_L1_shape[::-1]
|
|
44
|
+
|
|
45
|
+
standard_HD_num_pix = 1920 * 1080
|
|
46
|
+
frame_num_pix = np.prod(flipped_shape)
|
|
47
|
+
scale_factor = (
|
|
48
|
+
np.sqrt(standard_HD_num_pix / frame_num_pix)
|
|
49
|
+
if frame_num_pix > standard_HD_num_pix
|
|
50
|
+
else 1.0
|
|
51
|
+
)
|
|
52
|
+
scaled_shape = tuple(int(i * scale_factor) for i in flipped_shape)
|
|
53
|
+
|
|
54
|
+
return scaled_shape
|
|
55
|
+
|
|
56
|
+
def pre_run(self) -> None:
|
|
57
|
+
"""Set the movie frame shape prior to running."""
|
|
58
|
+
super().pre_run()
|
|
59
|
+
frame_shape = self.compute_frame_shape()
|
|
60
|
+
logger.info(f"Setting movie shape to {frame_shape}")
|
|
61
|
+
self.MOVIE_FRAME_SHAPE = frame_shape
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def constants_model_class(self):
|
|
65
|
+
"""Get CryoNIRSP constants."""
|
|
66
|
+
return CryonirspConstants
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def fits_parsing_class(self):
|
|
70
|
+
"""Cryonirsp specific subclass of L1FitsAccess to use for reading images."""
|
|
71
|
+
return CryonirspL1FitsAccess
|
|
72
|
+
|
|
73
|
+
def write_overlay(self, draw: ImageDraw, fits_obj: CryonirspL0FitsAccess) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Mark each image with its instrument, observed wavelength, and observation time.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
draw
|
|
80
|
+
A PIL.ImageDraw object
|
|
81
|
+
|
|
82
|
+
fits_obj
|
|
83
|
+
A single movie "image", i.e., a single array tagged with CryonirspTag.movie_frame
|
|
84
|
+
"""
|
|
85
|
+
self.write_line(
|
|
86
|
+
draw=draw,
|
|
87
|
+
text=f"INSTRUMENT: {self.constants.instrument}",
|
|
88
|
+
line=3,
|
|
89
|
+
column="right",
|
|
90
|
+
font=self.font_36,
|
|
91
|
+
)
|
|
92
|
+
self.write_line(
|
|
93
|
+
draw=draw,
|
|
94
|
+
text=f"WAVELENGTH: {fits_obj.wavelength} nm",
|
|
95
|
+
line=2,
|
|
96
|
+
column="right",
|
|
97
|
+
font=self.font_36,
|
|
98
|
+
)
|
|
99
|
+
self.write_line(
|
|
100
|
+
draw=draw,
|
|
101
|
+
text=f"OBS TIME: {fits_obj.time_obs}",
|
|
102
|
+
line=1,
|
|
103
|
+
column="right",
|
|
104
|
+
font=self.font_36,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if self.constants.correct_for_polarization:
|
|
108
|
+
self.write_line(draw=draw, text="Stokes-I", line=4, column="right", font=self.font_36)
|
|
109
|
+
|
|
110
|
+
def get_middle_line(self, draw: ImageDraw, text: str, font: FreeTypeFont) -> int:
|
|
111
|
+
"""
|
|
112
|
+
Get the line number for the middle of the frame.
|
|
113
|
+
|
|
114
|
+
We need to compute this in real time because the frame size is dynamically based on the L1 file shape.
|
|
115
|
+
"""
|
|
116
|
+
_, _, _, text_height = draw.textbbox(xy=(0, 0), text=text, font=font)
|
|
117
|
+
# See `write_line` in `dkist-processing-common` for why this is the expression.
|
|
118
|
+
line = (self.MOVIE_FRAME_SHAPE[1] // 2) / (self.TEXT_MARGIN_PX + text_height)
|
|
119
|
+
return line
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# See note below on `SPAssembleCryonirspMovie`
|
|
123
|
+
class AssembleCryonirspMovie(AssembleCryonirspMovieBase):
|
|
124
|
+
"""
|
|
125
|
+
Assemble all CryoNIRSP CI movie frames (tagged with CryonirspTag.movie_frame()) into an mp4 movie file.
|
|
126
|
+
|
|
127
|
+
Subclassed from the AssembleMovie task in dkist_processing_common to add CryoNIRSP specific text overlays.
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
Parameters
|
|
131
|
+
----------
|
|
132
|
+
recipe_run_id : int
|
|
133
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
134
|
+
workflow_name : str
|
|
135
|
+
name of the workflow to which this instance of the task belongs
|
|
136
|
+
workflow_version : str
|
|
137
|
+
version of the workflow to which this instance of the task belongs
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def num_images(self) -> int:
|
|
144
|
+
"""Total number of images in final movie.
|
|
145
|
+
|
|
146
|
+
Overloaded from `dkist-processing-common` because DSPS repeat does not correspond to map scan in CryoNIRSP
|
|
147
|
+
"""
|
|
148
|
+
return self.constants.num_map_scans * self.constants.num_scan_steps
|
|
149
|
+
|
|
150
|
+
def tags_for_image_n(self, n: int) -> list[str]:
|
|
151
|
+
"""Return tags that grab the n'th movie image.
|
|
152
|
+
|
|
153
|
+
Overloaded from `dkist-processing-common` because DSPS repeat does not correspond to map scan in CryoNIRSP
|
|
154
|
+
"""
|
|
155
|
+
map_scan_num = n // self.constants.num_scan_steps + 1
|
|
156
|
+
scan_step = n % self.constants.num_scan_steps + 1
|
|
157
|
+
|
|
158
|
+
tags = [
|
|
159
|
+
CryonirspTag.map_scan(map_scan_num),
|
|
160
|
+
CryonirspTag.scan_step(scan_step),
|
|
161
|
+
]
|
|
162
|
+
logger.info(f"AssembleMovie.tags_for_image_n: {tags =}")
|
|
163
|
+
return tags
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# NOTE:
|
|
167
|
+
# This task isn't used right now because the SP movies need some better handling of the wavelength dimension.
|
|
168
|
+
# For the time being both arms use the `MakeCryonirspMovieFrames` task above.
|
|
169
|
+
# See PR #91 for more information.
|
|
170
|
+
class SPAssembleCryonirspMovie(AssembleCryonirspMovieBase):
|
|
171
|
+
"""
|
|
172
|
+
Assemble all CryoNIRSP SP movie frames (tagged with CryonirspTag.movie_frame()) into an mp4 movie file.
|
|
173
|
+
|
|
174
|
+
Subclassed from the AssembleMovie task in dkist_processing_common to add CryoNIRSP specific text overlays.
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
recipe_run_id : int
|
|
180
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
181
|
+
workflow_name : str
|
|
182
|
+
name of the workflow to which this instance of the task belongs
|
|
183
|
+
workflow_version : str
|
|
184
|
+
version of the workflow to which this instance of the task belongs
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def num_images(self) -> int:
|
|
191
|
+
"""Total number of images in final movie.
|
|
192
|
+
|
|
193
|
+
Overloaded from `dkist-processing-common` because DSPS repeat does not correspond to map scan in CryoNIRSP
|
|
194
|
+
"""
|
|
195
|
+
return self.constants.num_map_scans
|
|
196
|
+
|
|
197
|
+
def tags_for_image_n(self, n: int) -> list[str]:
|
|
198
|
+
"""Return tags that grab the n'th movie image.
|
|
199
|
+
|
|
200
|
+
Overloaded from `dkist-processing-common` because DSPS repeat does not correspond to map scan in CryoNIRSP
|
|
201
|
+
"""
|
|
202
|
+
return [CryonirspTag.map_scan(n + 1)]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Task class for bad pixel map computation."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
import scipy.ndimage as spnd
|
|
4
|
+
from dkist_processing_math.statistics import average_numpy_arrays
|
|
5
|
+
from dkist_service_configuration.logging import logger
|
|
6
|
+
|
|
7
|
+
from dkist_processing_cryonirsp.models.tags import CryonirspTag
|
|
8
|
+
from dkist_processing_cryonirsp.models.task_name import CryonirspTaskName
|
|
9
|
+
from dkist_processing_cryonirsp.tasks.cryonirsp_base import CryonirspTaskBase
|
|
10
|
+
|
|
11
|
+
__all__ = ["BadPixelMapCalibration"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BadPixelMapCalibration(CryonirspTaskBase):
|
|
15
|
+
"""
|
|
16
|
+
Task class for calculation of the bad pixel map for later use during calibration.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
recipe_run_id : int
|
|
21
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
22
|
+
workflow_name : str
|
|
23
|
+
name of the workflow to which this instance of the task belongs
|
|
24
|
+
workflow_version : str
|
|
25
|
+
version of the workflow to which this instance of the task belongs
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
record_provenance = True
|
|
30
|
+
|
|
31
|
+
def run(self):
|
|
32
|
+
"""
|
|
33
|
+
Compute the bad pixel map by analyzing a set of solar gain images.
|
|
34
|
+
|
|
35
|
+
Steps:
|
|
36
|
+
1. Compute the average gain image
|
|
37
|
+
2. Smooth the array with a median filter
|
|
38
|
+
3. Calculate the difference between the smoothed and input arrays
|
|
39
|
+
4. Threshold the difference array on both ends to determine good and bad pixels
|
|
40
|
+
5. Save the bad pixel map as a fits file
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
None
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
with self.apm_task_step(f"Compute average uncorrected solar gain image"):
|
|
48
|
+
average_solar_gain_array = self.compute_average_gain_array()
|
|
49
|
+
|
|
50
|
+
with self.apm_task_step(f"Compute the bad pixel map"):
|
|
51
|
+
with self.apm_processing_step("Smooth array with median filter"):
|
|
52
|
+
filter_size = self.parameters.bad_pixel_map_median_filter_size
|
|
53
|
+
filtered_array = spnd.median_filter(
|
|
54
|
+
average_solar_gain_array,
|
|
55
|
+
size=filter_size,
|
|
56
|
+
mode="constant",
|
|
57
|
+
cval=np.nanmedian(average_solar_gain_array),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
with self.apm_processing_step("Identify bad pixels"):
|
|
61
|
+
thresh = self.parameters.bad_pixel_map_threshold_factor
|
|
62
|
+
|
|
63
|
+
diff = filtered_array - average_solar_gain_array
|
|
64
|
+
bad_pixel_map = np.array((np.abs(diff) > thresh * diff.std()), dtype=int)
|
|
65
|
+
|
|
66
|
+
# Find and fix any residual zeros that slipped through the bad pixel map.
|
|
67
|
+
zeros = np.where(average_solar_gain_array == 0.0)
|
|
68
|
+
bad_pixel_map[zeros] = 1
|
|
69
|
+
|
|
70
|
+
with self.apm_writing_step("Writing bad pixel map"):
|
|
71
|
+
self.intermediate_frame_write_arrays(
|
|
72
|
+
bad_pixel_map, task=CryonirspTaskName.bad_pixel_map.value
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def compute_average_gain_array(self) -> np.ndarray:
|
|
76
|
+
"""
|
|
77
|
+
Compute an average of uncorrected solar gain arrays.
|
|
78
|
+
|
|
79
|
+
We are computing the overall illumination pattern for one (CI) or both (SP) beams
|
|
80
|
+
simultaneously, so no dark correction is required and no beam splitting is used at
|
|
81
|
+
this point. Solar gain images are used because of their higher flux levels and they
|
|
82
|
+
more accurately represent the illuminated beam seen in solar images.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
The average gain array
|
|
87
|
+
"""
|
|
88
|
+
lin_corr_gain_arrays = self.linearized_frame_full_array_generator(
|
|
89
|
+
tags=[
|
|
90
|
+
CryonirspTag.task_solar_gain(),
|
|
91
|
+
CryonirspTag.linearized(),
|
|
92
|
+
CryonirspTag.frame(),
|
|
93
|
+
]
|
|
94
|
+
)
|
|
95
|
+
averaged_gain_data = average_numpy_arrays(lin_corr_gain_arrays)
|
|
96
|
+
return averaged_gain_data
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""CryoNIRSP compute beam boundary task."""
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from dkist_processing_math.statistics import average_numpy_arrays
|
|
7
|
+
from dkist_service_configuration.logging import logger
|
|
8
|
+
from largestinteriorrectangle import lir
|
|
9
|
+
from skimage import filters
|
|
10
|
+
from skimage.exposure import rescale_intensity
|
|
11
|
+
from skimage.morphology import disk
|
|
12
|
+
from skimage.util import img_as_ubyte
|
|
13
|
+
|
|
14
|
+
from dkist_processing_cryonirsp.models.tags import CryonirspTag
|
|
15
|
+
from dkist_processing_cryonirsp.tasks.cryonirsp_base import CryonirspTaskBase
|
|
16
|
+
|
|
17
|
+
__all__ = ["BeamBoundariesCalibrationBase", "BeamBoundary"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class BeamBoundary:
|
|
22
|
+
"""Simple dataclass to hold boundary information for the illuminated portion of the beam array."""
|
|
23
|
+
|
|
24
|
+
y_min: int
|
|
25
|
+
y_max: int
|
|
26
|
+
x_min: int
|
|
27
|
+
x_max: int
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def y_slice(self):
|
|
31
|
+
"""Return a slice object representing the illumination along the y-axis (numpy 0 axis)."""
|
|
32
|
+
return slice(self.y_min, self.y_max)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def x_slice(self):
|
|
36
|
+
"""Return a slice object representing the illumination along the x-axis (numpy 1 axis)."""
|
|
37
|
+
return slice(self.x_min, self.x_max)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def beam_boundaries(self):
|
|
41
|
+
"""Return a tuple containing the beam boundaries."""
|
|
42
|
+
return self.y_min, self.y_max, self.x_min, self.x_max
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def beam_boundaries_array(self):
|
|
46
|
+
"""Return a tuple containing the beam boundaries."""
|
|
47
|
+
return np.array(self.beam_boundaries)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class BeamBoundariesCalibrationBase(CryonirspTaskBase):
|
|
51
|
+
"""
|
|
52
|
+
Task class for calculation of the beam boundaries for later use during calibration.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
recipe_run_id : int
|
|
57
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
58
|
+
workflow_name : str
|
|
59
|
+
name of the workflow to which this instance of the task belongs
|
|
60
|
+
workflow_version : str
|
|
61
|
+
version of the workflow to which this instance of the task belongs
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
record_provenance = True
|
|
66
|
+
|
|
67
|
+
def run(self):
|
|
68
|
+
"""
|
|
69
|
+
Compute the beam boundaries by analyzing a set of solar gain images.
|
|
70
|
+
|
|
71
|
+
Steps:
|
|
72
|
+
1. Compute the average gain image
|
|
73
|
+
2. Correct any bad pixels
|
|
74
|
+
3. Smooth the image using a median filter
|
|
75
|
+
4. Split the beam along the horizontal axis (SP only)
|
|
76
|
+
5. Use a bimodal threshold filter to segment the image into illuminated and non-illuminated regions
|
|
77
|
+
6. Compute the boundaries of the illuminated region
|
|
78
|
+
7. Extract the illuminated portion of the beam images
|
|
79
|
+
8. Find the horizontal shift between the two images
|
|
80
|
+
9. Identify the boundaries of the overlap
|
|
81
|
+
10. Save the boundaries as a fits file (json?)
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
None
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
# Step 1:
|
|
89
|
+
with self.apm_processing_step(f"Compute average solar gain image"):
|
|
90
|
+
average_solar_gain_array = self.compute_average_gain_array()
|
|
91
|
+
|
|
92
|
+
# Step 2:
|
|
93
|
+
with self.apm_task_step(f"Retrieve bad pixel map"):
|
|
94
|
+
bad_pixel_map = self.intermediate_frame_load_full_bad_pixel_map()
|
|
95
|
+
corrected_solar_gain_array = self.corrections_correct_bad_pixels(
|
|
96
|
+
average_solar_gain_array, bad_pixel_map
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Step 3
|
|
100
|
+
with self.apm_processing_step(f"Smooth the array to get good segmentation"):
|
|
101
|
+
smoothed_solar_gain_array = self.smooth_gain_array(corrected_solar_gain_array)
|
|
102
|
+
|
|
103
|
+
# Step 4
|
|
104
|
+
with self.apm_processing_step(f"Split the beam horizontally"):
|
|
105
|
+
split_beams = self.split_beams(smoothed_solar_gain_array)
|
|
106
|
+
|
|
107
|
+
# Step 5
|
|
108
|
+
with self.apm_processing_step(
|
|
109
|
+
f"Segment the beams into illuminated and non-illuminated pixels"
|
|
110
|
+
):
|
|
111
|
+
segmented_beams = self.segment_arrays(split_beams)
|
|
112
|
+
|
|
113
|
+
# Step 6:
|
|
114
|
+
with self.apm_processing_step(
|
|
115
|
+
f"Compute the inscribed rectangular extents of the illuminated portions of the sensor"
|
|
116
|
+
):
|
|
117
|
+
illuminated_boundaries = self.compute_boundaries_of_beam_illumination_regions(
|
|
118
|
+
segmented_beams
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Steps 7 - 9:
|
|
122
|
+
with self.apm_processing_step(f"Compute the boundaries of the illuminated beams"):
|
|
123
|
+
split_beams_float = [split_beam.astype(float) for split_beam in split_beams]
|
|
124
|
+
boundaries = self.compute_final_beam_boundaries(
|
|
125
|
+
split_beams_float, illuminated_boundaries
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Step 10:
|
|
129
|
+
with self.apm_writing_step("Writing beam boundaries"):
|
|
130
|
+
for beam, bounds in enumerate(boundaries, start=1):
|
|
131
|
+
self.intermediate_frame_write_arrays(
|
|
132
|
+
bounds.beam_boundaries_array,
|
|
133
|
+
task_tag=CryonirspTag.task_beam_boundaries(),
|
|
134
|
+
beam=beam,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def compute_average_gain_array(self) -> np.ndarray:
|
|
138
|
+
"""
|
|
139
|
+
Compute an average of uncorrected solar gain arrays.
|
|
140
|
+
|
|
141
|
+
We are computing the overall illumination pattern for both beams simultaneously,
|
|
142
|
+
so no dark correction is required and no beam splitting is used at this point.
|
|
143
|
+
Solar gain images are used because they have larger flux than the lamp gain images
|
|
144
|
+
and the lamp gain images do not have the same illumination pattern as the solar
|
|
145
|
+
gain images.
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
The average gain array
|
|
150
|
+
"""
|
|
151
|
+
lin_corr_gain_arrays = self.linearized_frame_full_array_generator(
|
|
152
|
+
tags=[
|
|
153
|
+
CryonirspTag.task_solar_gain(),
|
|
154
|
+
CryonirspTag.linearized(),
|
|
155
|
+
CryonirspTag.frame(),
|
|
156
|
+
]
|
|
157
|
+
)
|
|
158
|
+
averaged_gain_data = average_numpy_arrays(lin_corr_gain_arrays)
|
|
159
|
+
return averaged_gain_data
|
|
160
|
+
|
|
161
|
+
def smooth_gain_array(self, array: np.ndarray) -> np.ndarray:
|
|
162
|
+
"""
|
|
163
|
+
Smooth the input array with morphological filtering using a disk shape.
|
|
164
|
+
|
|
165
|
+
The array is smoothed to help eliminate artifacts in the image segmentation step.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
array
|
|
170
|
+
The input array to be smoothed
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
The smoothed output array
|
|
175
|
+
"""
|
|
176
|
+
# skimage.filters requires ubyte arrays and float->ubyte conversion only works with float in range [-1, 1]
|
|
177
|
+
norm_gain = img_as_ubyte(rescale_intensity(array, out_range=(0, 1.0)))
|
|
178
|
+
|
|
179
|
+
disk_size = self.parameters.beam_boundaries_smoothing_disk_size
|
|
180
|
+
norm_gain = filters.rank.median(norm_gain, disk(disk_size))
|
|
181
|
+
return norm_gain
|
|
182
|
+
|
|
183
|
+
@abstractmethod
|
|
184
|
+
def split_beams(self, input_array: np.ndarray) -> list[np.ndarray]:
|
|
185
|
+
"""
|
|
186
|
+
Split the beams along the horizontal axis.
|
|
187
|
+
|
|
188
|
+
We use an abstract method so that the processing sequence is the same for both SP and CI
|
|
189
|
+
although the processing steps may be different.
|
|
190
|
+
|
|
191
|
+
Parameters
|
|
192
|
+
----------
|
|
193
|
+
input_array
|
|
194
|
+
The array to be split
|
|
195
|
+
|
|
196
|
+
Returns
|
|
197
|
+
-------
|
|
198
|
+
[array, ...]
|
|
199
|
+
A list of arrays after the split
|
|
200
|
+
"""
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def segment_arrays(arrays: list[np.ndarray]) -> list[np.ndarray]:
|
|
205
|
+
"""
|
|
206
|
+
Segment the arrays into illuminated (True) and non-illuminated (False) regions.
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
arrays
|
|
211
|
+
The arrays to be segmented
|
|
212
|
+
|
|
213
|
+
Returns
|
|
214
|
+
-------
|
|
215
|
+
[array, ...]
|
|
216
|
+
The boolean segmented output arrays
|
|
217
|
+
"""
|
|
218
|
+
segmented_arrays = []
|
|
219
|
+
for beam_num, beam_array in enumerate(arrays, start=1):
|
|
220
|
+
thresh = filters.threshold_minimum(beam_array)
|
|
221
|
+
logger.info(f"Segmentation threshold for beam {beam_num} = {thresh}")
|
|
222
|
+
segmented_arrays.append((beam_array > thresh).astype(bool))
|
|
223
|
+
return segmented_arrays
|
|
224
|
+
|
|
225
|
+
def compute_boundaries_of_beam_illumination_regions(
|
|
226
|
+
self, arrays: list[np.ndarray]
|
|
227
|
+
) -> list[BeamBoundary]:
|
|
228
|
+
"""
|
|
229
|
+
Compute the rectangular boundaries describing the illuminated regions of the beam images.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
arrays
|
|
234
|
+
A list of segmented boolean arrays over which the boundaries are to be computed
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
[BeamBoundary, ...]
|
|
239
|
+
A list of the BeamBoundary objects representing the illuminated region(s) of the beam image(s)
|
|
240
|
+
"""
|
|
241
|
+
boundaries = []
|
|
242
|
+
for beam, array in enumerate(arrays, start=1):
|
|
243
|
+
# Find the largest interior rectangle that inscribes the illuminated region for this beam
|
|
244
|
+
x_min, y_min, x_range, y_range = lir(array)
|
|
245
|
+
# Compute the new image bounds, the maximums are exclusive and can be used in slices
|
|
246
|
+
y_max = y_min + y_range
|
|
247
|
+
x_max = x_min + x_range
|
|
248
|
+
|
|
249
|
+
# Make sure all pixels are 1s
|
|
250
|
+
if not np.all(array[y_min:y_max, x_min:x_max]):
|
|
251
|
+
raise RuntimeError(
|
|
252
|
+
f"Unable to compute illuminated image boundaries for beam {beam}"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
boundaries.append(BeamBoundary(y_min, y_max, x_min, x_max))
|
|
256
|
+
|
|
257
|
+
return boundaries
|
|
258
|
+
|
|
259
|
+
@abstractmethod
|
|
260
|
+
def compute_final_beam_boundaries(
|
|
261
|
+
self,
|
|
262
|
+
smoothed_solar_gain_arrays: list[np.ndarray],
|
|
263
|
+
illuminated_boundaries: list[BeamBoundary],
|
|
264
|
+
) -> list[BeamBoundary]:
|
|
265
|
+
"""
|
|
266
|
+
Compute the final beam boundaries to be used when accessing individual beam images from the input dual-beam arrays.
|
|
267
|
+
|
|
268
|
+
Parameters
|
|
269
|
+
----------
|
|
270
|
+
smoothed_solar_gain_arrays
|
|
271
|
+
Smoothed solar gain arrays to be used for coarsely aligning the individual beam images.
|
|
272
|
+
illuminated_boundaries
|
|
273
|
+
A list of BeamBoundary objects representing the illuminated regions of each beam image.
|
|
274
|
+
|
|
275
|
+
Returns
|
|
276
|
+
-------
|
|
277
|
+
A list of BeamBoundary objects describing the final beam boundaries for each beam.
|
|
278
|
+
"""
|
|
279
|
+
pass
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Cryonirsp CI beam boundaries task."""
|
|
2
|
+
import largestinteriorrectangle as lir
|
|
3
|
+
import numpy as np
|
|
4
|
+
from dkist_service_configuration.logging import logger
|
|
5
|
+
|
|
6
|
+
from dkist_processing_cryonirsp.tasks.beam_boundaries_base import BeamBoundariesCalibrationBase
|
|
7
|
+
from dkist_processing_cryonirsp.tasks.beam_boundaries_base import BeamBoundary
|
|
8
|
+
|
|
9
|
+
__all__ = ["CIBeamBoundariesCalibration"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CIBeamBoundariesCalibration(BeamBoundariesCalibrationBase):
|
|
13
|
+
"""Task class for calculation of the CI beam boundaries for later use during calibration."""
|
|
14
|
+
|
|
15
|
+
def split_beams(self, array: np.ndarray) -> list[np.ndarray]:
|
|
16
|
+
"""
|
|
17
|
+
Return the input array, as there is no beam split for CI.
|
|
18
|
+
|
|
19
|
+
This is just a pass-through method to facilitate using the same processing
|
|
20
|
+
steps in the base run method.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
array
|
|
25
|
+
The input array
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
[array]
|
|
30
|
+
The input array embedded in a list
|
|
31
|
+
"""
|
|
32
|
+
return [array]
|
|
33
|
+
|
|
34
|
+
def compute_final_beam_boundaries(
|
|
35
|
+
self, smoothed_solar_gain_array: np.ndarray, illuminated_boundaries: list[BeamBoundary]
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Compute the final beam boundaries to be used when accessing CI beam images from the input dual-beam arrays.
|
|
39
|
+
|
|
40
|
+
Note: For CI, the illuminated beam boundaries ARE the final beam boundaries.
|
|
41
|
+
This method is simply a pass-through that allows both SP and CI to use the processing sequence.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
smoothed_solar_gain_array
|
|
46
|
+
The smoothed solar gain array for a single CI beam.
|
|
47
|
+
illuminated_boundaries
|
|
48
|
+
A list with a single BeamBoundary object for the illuminated CI beam.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
[BeamBoundary]
|
|
53
|
+
The CI BeamBoundary object embedded in a list.
|
|
54
|
+
"""
|
|
55
|
+
return illuminated_boundaries
|