diff --git a/hexrdgui/hexrd_config.py b/hexrdgui/hexrd_config.py index 13ed285da..ef12145a2 100644 --- a/hexrdgui/hexrd_config.py +++ b/hexrdgui/hexrd_config.py @@ -1429,10 +1429,15 @@ def recursive_key_check(d: dict[str, Any], c: dict[str, Any]) -> None: 'reset_exclusions': False, } + if self.instrument_has_roi: + names = self.detector_group_names + else: + names = self.detector_names + data = [] - for det in self.detector_names: + for name in names: data.append( - {'file': f'{det}.npz', 'args': {'path': 'imageseries'}, 'panel': det} + {'file': f'{name}.npz', 'args': {'path': 'imageseries'}, 'panel': name} ) image_series = {'format': 'frame-cache', 'data': data} diff --git a/hexrdgui/indexing/fit_grains_results_dialog.py b/hexrdgui/indexing/fit_grains_results_dialog.py index 1dc5a90d0..9f8ab75c0 100644 --- a/hexrdgui/indexing/fit_grains_results_dialog.py +++ b/hexrdgui/indexing/fit_grains_results_dialog.py @@ -21,6 +21,7 @@ if TYPE_CHECKING: from matplotlib.backend_bases import PickEvent from matplotlib.colors import Colormap + from hexrd.core.imageseries.imageseriesabc import ImageSeriesABC from hexrd.material import Material from hexrd.matrixutil import vecMVToSymm @@ -726,20 +727,29 @@ def full_path(file_name: str) -> str: # Convenience function to generate full path via pathlib return str(Path(selected_directory) / file_name) + has_roi = HexrdConfig().instrument_has_roi + HexrdConfig().working_dir = selected_directory HexrdConfig().save_indexing_config(full_path('workflow.yml')) HexrdConfig().save_materials_hdf5(full_path('materials.h5')) HexrdConfig().save_instrument_config( full_path('instrument.hexrd'), - # Remove ROIs, since we are saving the imageseries without them - remove_rois=True, + # Keep ROIs when groups exist so exported workflow can be + # re-used with original monolithic frame-cache files + remove_rois=not has_roi, ) + if has_roi: + self._save_group_images(selected_directory) + else: + self._save_detector_images(selected_directory) + + def _save_detector_images(self, selected_directory: str) -> None: ims_dict = HexrdConfig().unagg_images assert ims_dict is not None for det in HexrdConfig().detector_names: - path = full_path(f'{det}.npz') + path = str(Path(selected_directory) / f'{det}.npz') kwargs: dict[str, Any] = { 'ims': ims_dict.get(det), 'name': det, @@ -750,6 +760,27 @@ def full_path(file_name: str) -> str: } HexrdConfig().save_imageseries(**kwargs) + def _save_group_images(self, selected_directory: str) -> None: + ims_dict = HexrdConfig().unagg_images + assert ims_dict is not None + for group in HexrdConfig().detector_group_names: + # Get first subpanel in group to access its underlying image + first_det = HexrdConfig().detectors_in_group(group)[0] + subpanel_ims = ims_dict[first_det] + + # Get the monolithic image (unwrap rectangle op) + monolithic_ims = _get_monolithic_ims(subpanel_ims) + + path = str(Path(selected_directory) / f'{group}.npz') + HexrdConfig().save_imageseries( + ims=monolithic_ims, + name=group, + write_file=path, + selected_format='frame-cache', + cache_file=path, + threshold=0, + ) + def on_export_workflow_clicked(self) -> None: selected_directory = QFileDialog.getExistingDirectory( self.ui, 'Select Directory', HexrdConfig().working_dir @@ -760,11 +791,16 @@ def on_export_workflow_clicked(self) -> None: return # Warn the user if any files will be over-written + if HexrdConfig().instrument_has_roi: + image_names = HexrdConfig().detector_group_names + else: + image_names = HexrdConfig().detector_names + write_files = [ 'workflow.yml', 'materials.h5', 'instrument.hexrd', - ] + [f'{det}.npz' for det in HexrdConfig().detector_names] + ] + [f'{name}.npz' for name in image_names] overwrite_files = [] for f in write_files: @@ -784,6 +820,54 @@ def on_export_workflow_clicked(self) -> None: self.async_runner.run(self._save_workflow_files, selected_directory) +def _get_monolithic_ims( + subpanel_ims: ImageSeriesABC, +) -> ImageSeriesABC: + """Get monolithic image from a subpanel's image series. + + Recursively unwraps the image series chain, collecting all + non-rectangle operations and frame_list selections along the way. + The result is a single image series rooted at the base adapter + with all non-rectangle processing preserved. + + The chain may contain arbitrary nesting of: + - ``ImageSeries`` / ``OmegaImageSeries`` (use ``_adapter``) + - ``ProcessedImageSeries`` (use ``_imser``, hold ``_oplist``) + """ + from hexrd.core.imageseries.process import ProcessedImageSeries + + # Walk the full chain, collecting non-rectangle ops and frame_lists. + non_rect_ops: list = [] + frame_list: list[int] | None = None + ims: ImageSeriesABC = subpanel_ims + + while True: + if isinstance(ims, ProcessedImageSeries): + # Collect ops (excluding rectangle) and frame_list + non_rect_ops.extend( + op for op in ims._oplist if op[0] != 'rectangle' + ) + if frame_list is None and ims._hasframelist: + frame_list = list(ims._frames) + ims = ims._imser + elif hasattr(ims, '_adapter'): + ims = ims._adapter + else: + # Reached the base adapter — stop. + break + + # ims is now the root image series (e.g. FrameCacheImageSeriesAdapter + # wrapped in ImageSeries). Rebuild with only the collected ops. + if not non_rect_ops and frame_list is None: + return ims + + kwargs: dict = {} + if frame_list is not None: + kwargs['frame_list'] = frame_list + + return ProcessedImageSeries(ims, non_rect_ops, **kwargs) + + if __name__ == '__main__': from PySide6.QtCore import QCoreApplication from PySide6.QtWidgets import QApplication