threads.nifti_utils_threads

  1import json
  2import nibabel as nib
  3import numpy as np
  4
  5from PyQt6.QtCore import QThread, pyqtSignal, QCoreApplication
  6from logger import get_logger
  7
  8
  9log = get_logger()
 10
 11class SaveNiftiThread(QThread):
 12    """
 13    Background thread for saving a NIfTI image and its associated metadata
 14    to disk in BIDS-like format.
 15
 16    This class prevents the UI from freezing during file write operations
 17    by running the saving process in a separate thread.
 18
 19    Signals:
 20        success (str, str): Emitted when both the NIfTI and JSON files are successfully saved.
 21                            Provides the paths to the saved files.
 22        error (str): Emitted when an error occurs during saving.
 23
 24    Args:
 25        data (np.ndarray): The voxel data to be saved as a NIfTI image.
 26        affine (np.ndarray): The affine transformation matrix for spatial orientation.
 27        path (str): Output file path for the NIfTI image.
 28        json_path (str): Output file path for the JSON metadata file.
 29        relative_path (str): Relative path reference (used for the "Sources" field in JSON).
 30        radius (float): Region growing radius parameter.
 31        difference (float): Intensity difference threshold parameter.
 32    """
 33
 34    success = pyqtSignal(str, str)
 35    """**Signal(str, str):** Emitted when an operation completes successfully.  
 36    Parameters:  
 37    - `str`: Name or identifier of the completed task.  
 38    - `str`: Success message or additional details.  
 39    """
 40
 41    error = pyqtSignal(str)
 42    """**Signal(str):** Emitted when an error occurs during an operation.  
 43    Parameters:  
 44    - `str`: Description of the error or failure reason.  
 45    """
 46
 47    def __init__(self, data, affine, path, json_path, relative_path,source_dict):
 48        super().__init__()
 49        self.data = data
 50        self.affine = affine
 51        self.path = path
 52        self.relative_path = relative_path
 53        self.source_dict = source_dict
 54        self.json_path = json_path
 55
 56    def run(self):
 57        """
 58        Executes the file-saving process in a background thread.
 59
 60        The method saves the NIfTI image using `nibabel`, then creates and writes
 61        a companion JSON file describing the ROI (Region of Interest) with
 62        user-specified parameters.
 63
 64        Emits:
 65            - success(path, json_path): On successful save.
 66            - error(message): If an exception occurs.
 67        """
 68        try:
 69            log.debug(f"Save nifti in {self.path}")
 70            # Save the NIfTI file
 71            nib.save(nib.Nifti1Image(self.data.astype(np.uint8), self.affine), self.path)
 72
 73            log.debug("Preparing json")
 74            json_dict = {
 75                "Type": "ROI",
 76                "Sources": [f"bids:{self.relative_path}"],
 77                "Description": "ROI mask",
 78                "Origin": self.source_dict
 79            }
 80
 81            log.debug("Writing json")
 82            # Write the JSON metadata
 83            with open(self.json_path, "w") as json_file:
 84                json.dump(json_dict, json_file, indent=4)
 85            log.debug("Send success")
 86            # Notify the main thread of success
 87            self.success.emit(self.path, self.json_path)
 88
 89        except Exception as e:
 90            # Emit error message if something goes wrong
 91            self.error.emit(str(e))
 92
 93
 94class ImageLoadThread(QThread):
 95    """
 96    Thread for loading and normalizing large NIfTI files without blocking the UI.
 97
 98    This class supports both 3D and 4D NIfTI volumes and includes
 99    robust normalization based on percentile scaling similar to matplotlib's
100    default image scaling behavior.
101
102    Signals:
103        finished (object, object, object, bool, bool): Emitted when loading completes successfully.
104            Contains (img_data, dims, affine, is_4d, is_overlay).
105        error (str): Emitted if an error occurs during file loading.
106        progress (int): Emits loading progress updates (0–100).
107
108    Args:
109        file_path (str): Path to the NIfTI file to load.
110        is_overlay (bool): Flag indicating whether the file is an overlay image.
111    """
112
113    finished = pyqtSignal(object, object, object, bool, bool)
114    """**Signal(object, object, object, bool, bool):**  
115    Emitted when the file is successifully loaded.  
116
117    Parameters:  
118    - `object`: img_data.  
119    - `object`: dims.
120    - `object`: affine. 
121    - `bool`: is_4d. 
122    - `bool`: is_overlay.  
123    """
124
125    error = pyqtSignal(str)
126    """**Signal(str):**  
127    Emitted when an error occurs during the deep learning execution.  
128
129    Parameters:  
130    - `str`: Description or message detailing the error.  
131    """
132
133    progress = pyqtSignal(int)
134    """**Signal(int):**  
135    Emitted to report progress updates during execution.  
136
137    Parameters:  
138    - `int`: Current progress percentage (0–100).  
139    """
140
141    def __init__(self, file_path, is_overlay):
142        super().__init__()
143        self.file_path = file_path
144        self.is_overlay = is_overlay
145
146    def run(self):
147        """
148        Loads and normalizes a NIfTI image in a background thread.
149
150        Steps:
151            1. Load image using memory mapping to minimize RAM usage.
152            2. Verify the file is a valid NIfTI image.
153            3. Canonicalize to RAS+ orientation.
154            4. Normalize data using percentile scaling.
155            5. Emit the finished signal with image data and metadata.
156        """
157        try:
158            self.progress.emit(10)
159
160            # Load image (memory-mapped to avoid heavy RAM use)
161            img = nib.load(self.file_path, mmap="c")
162            self.progress.emit(30)
163
164            if not isinstance(img, (nib.Nifti1Image, nib.Nifti2Image)):
165                raise ValueError(QCoreApplication.translate("Threads", "Not a valid NIfTI file"))
166
167            # Convert to canonical orientation (RAS+)
168            canonical_img = nib.as_closest_canonical(img)
169            self.progress.emit(50)
170
171            dims = canonical_img.header.get_data_shape()
172            is_4d = len(dims) == 4
173            affine = canonical_img.affine
174
175            self.progress.emit(70)
176            log.debug("Load voxel data")
177            # Load voxel data
178            img_data = np.asanyarray(canonical_img.dataobj, dtype=np.float32)
179            self.progress.emit(80)
180
181            log.debug("Normalize image intensities")
182            # Normalize image intensities
183            img_data = self.normalize_data_matplotlib_style(img_data)
184
185            self.progress.emit(100)
186            log.debug("Emit finished signal with image data and metadata.")
187            # Emit the results to the main thread
188            self.finished.emit(img_data, dims, affine, is_4d, self.is_overlay)
189
190        except Exception as e:
191            # Report any errors encountered
192            self.error.emit(str(e))
193
194    def normalize_data_matplotlib_style(self, data):
195        """
196        Normalize NIfTI data using robust percentile scaling (0.5th–99.5th percentiles).
197
198        This approach is similar to how matplotlib normalizes image intensities,
199        providing consistent visual scaling even for datasets with outliers.
200
201        Args:
202            data (np.ndarray): The 3D or 4D voxel intensity data.
203
204        Returns:
205            np.ndarray: Normalized float32 data with intensity values scaled to [0, 1].
206        """
207
208        if data.size == 0:
209            return data
210
211        def normalize_volume(volume):
212            """Normalize a single 3D volume to [0, 1] using percentile-based contrast stretching."""
213            valid_data = volume[np.isfinite(volume)]
214            if valid_data.size == 0:
215                return np.zeros_like(volume, dtype=np.float32)
216
217            vmin, vmax = np.percentile(valid_data, [0.1, 99.9])
218            if vmax <= vmin:
219                vmax = vmin + 1.0
220
221            return np.clip((volume - vmin) / (vmax - vmin), 0, 1).astype(np.float32)
222
223        # Handle both 3D and 4D volumes
224        if data.ndim == 4:
225            normalized = np.empty_like(data, dtype=np.float32)
226            for i in range(data.shape[3]):
227                normalized[..., i] = normalize_volume(data[..., i])
228        else:
229            normalized = normalize_volume(data)
230
231        return normalized
log = <Logger GliAAns-UI (WARNING)>
class SaveNiftiThread(PyQt6.QtCore.QThread):
12class SaveNiftiThread(QThread):
13    """
14    Background thread for saving a NIfTI image and its associated metadata
15    to disk in BIDS-like format.
16
17    This class prevents the UI from freezing during file write operations
18    by running the saving process in a separate thread.
19
20    Signals:
21        success (str, str): Emitted when both the NIfTI and JSON files are successfully saved.
22                            Provides the paths to the saved files.
23        error (str): Emitted when an error occurs during saving.
24
25    Args:
26        data (np.ndarray): The voxel data to be saved as a NIfTI image.
27        affine (np.ndarray): The affine transformation matrix for spatial orientation.
28        path (str): Output file path for the NIfTI image.
29        json_path (str): Output file path for the JSON metadata file.
30        relative_path (str): Relative path reference (used for the "Sources" field in JSON).
31        radius (float): Region growing radius parameter.
32        difference (float): Intensity difference threshold parameter.
33    """
34
35    success = pyqtSignal(str, str)
36    """**Signal(str, str):** Emitted when an operation completes successfully.  
37    Parameters:  
38    - `str`: Name or identifier of the completed task.  
39    - `str`: Success message or additional details.  
40    """
41
42    error = pyqtSignal(str)
43    """**Signal(str):** Emitted when an error occurs during an operation.  
44    Parameters:  
45    - `str`: Description of the error or failure reason.  
46    """
47
48    def __init__(self, data, affine, path, json_path, relative_path,source_dict):
49        super().__init__()
50        self.data = data
51        self.affine = affine
52        self.path = path
53        self.relative_path = relative_path
54        self.source_dict = source_dict
55        self.json_path = json_path
56
57    def run(self):
58        """
59        Executes the file-saving process in a background thread.
60
61        The method saves the NIfTI image using `nibabel`, then creates and writes
62        a companion JSON file describing the ROI (Region of Interest) with
63        user-specified parameters.
64
65        Emits:
66            - success(path, json_path): On successful save.
67            - error(message): If an exception occurs.
68        """
69        try:
70            log.debug(f"Save nifti in {self.path}")
71            # Save the NIfTI file
72            nib.save(nib.Nifti1Image(self.data.astype(np.uint8), self.affine), self.path)
73
74            log.debug("Preparing json")
75            json_dict = {
76                "Type": "ROI",
77                "Sources": [f"bids:{self.relative_path}"],
78                "Description": "ROI mask",
79                "Origin": self.source_dict
80            }
81
82            log.debug("Writing json")
83            # Write the JSON metadata
84            with open(self.json_path, "w") as json_file:
85                json.dump(json_dict, json_file, indent=4)
86            log.debug("Send success")
87            # Notify the main thread of success
88            self.success.emit(self.path, self.json_path)
89
90        except Exception as e:
91            # Emit error message if something goes wrong
92            self.error.emit(str(e))

Background thread for saving a NIfTI image and its associated metadata to disk in BIDS-like format.

This class prevents the UI from freezing during file write operations by running the saving process in a separate thread.

Signals: success (str, str): Emitted when both the NIfTI and JSON files are successfully saved. Provides the paths to the saved files. error (str): Emitted when an error occurs during saving.

Args: data (np.ndarray): The voxel data to be saved as a NIfTI image. affine (np.ndarray): The affine transformation matrix for spatial orientation. path (str): Output file path for the NIfTI image. json_path (str): Output file path for the JSON metadata file. relative_path (str): Relative path reference (used for the "Sources" field in JSON). radius (float): Region growing radius parameter. difference (float): Intensity difference threshold parameter.

SaveNiftiThread(data, affine, path, json_path, relative_path, source_dict)
48    def __init__(self, data, affine, path, json_path, relative_path,source_dict):
49        super().__init__()
50        self.data = data
51        self.affine = affine
52        self.path = path
53        self.relative_path = relative_path
54        self.source_dict = source_dict
55        self.json_path = json_path
def success(unknown):

Signal(str, str): Emitted when an operation completes successfully.
Parameters:

  • str: Name or identifier of the completed task.
  • str: Success message or additional details.
def error(unknown):

Signal(str): Emitted when an error occurs during an operation.
Parameters:

  • str: Description of the error or failure reason.
data
affine
path
relative_path
source_dict
json_path
def run(self):
57    def run(self):
58        """
59        Executes the file-saving process in a background thread.
60
61        The method saves the NIfTI image using `nibabel`, then creates and writes
62        a companion JSON file describing the ROI (Region of Interest) with
63        user-specified parameters.
64
65        Emits:
66            - success(path, json_path): On successful save.
67            - error(message): If an exception occurs.
68        """
69        try:
70            log.debug(f"Save nifti in {self.path}")
71            # Save the NIfTI file
72            nib.save(nib.Nifti1Image(self.data.astype(np.uint8), self.affine), self.path)
73
74            log.debug("Preparing json")
75            json_dict = {
76                "Type": "ROI",
77                "Sources": [f"bids:{self.relative_path}"],
78                "Description": "ROI mask",
79                "Origin": self.source_dict
80            }
81
82            log.debug("Writing json")
83            # Write the JSON metadata
84            with open(self.json_path, "w") as json_file:
85                json.dump(json_dict, json_file, indent=4)
86            log.debug("Send success")
87            # Notify the main thread of success
88            self.success.emit(self.path, self.json_path)
89
90        except Exception as e:
91            # Emit error message if something goes wrong
92            self.error.emit(str(e))

Executes the file-saving process in a background thread.

The method saves the NIfTI image using nibabel, then creates and writes a companion JSON file describing the ROI (Region of Interest) with user-specified parameters.

Emits: - success(path, json_path): On successful save. - error(message): If an exception occurs.

class ImageLoadThread(PyQt6.QtCore.QThread):
 95class ImageLoadThread(QThread):
 96    """
 97    Thread for loading and normalizing large NIfTI files without blocking the UI.
 98
 99    This class supports both 3D and 4D NIfTI volumes and includes
100    robust normalization based on percentile scaling similar to matplotlib's
101    default image scaling behavior.
102
103    Signals:
104        finished (object, object, object, bool, bool): Emitted when loading completes successfully.
105            Contains (img_data, dims, affine, is_4d, is_overlay).
106        error (str): Emitted if an error occurs during file loading.
107        progress (int): Emits loading progress updates (0–100).
108
109    Args:
110        file_path (str): Path to the NIfTI file to load.
111        is_overlay (bool): Flag indicating whether the file is an overlay image.
112    """
113
114    finished = pyqtSignal(object, object, object, bool, bool)
115    """**Signal(object, object, object, bool, bool):**  
116    Emitted when the file is successifully loaded.  
117
118    Parameters:  
119    - `object`: img_data.  
120    - `object`: dims.
121    - `object`: affine. 
122    - `bool`: is_4d. 
123    - `bool`: is_overlay.  
124    """
125
126    error = pyqtSignal(str)
127    """**Signal(str):**  
128    Emitted when an error occurs during the deep learning execution.  
129
130    Parameters:  
131    - `str`: Description or message detailing the error.  
132    """
133
134    progress = pyqtSignal(int)
135    """**Signal(int):**  
136    Emitted to report progress updates during execution.  
137
138    Parameters:  
139    - `int`: Current progress percentage (0–100).  
140    """
141
142    def __init__(self, file_path, is_overlay):
143        super().__init__()
144        self.file_path = file_path
145        self.is_overlay = is_overlay
146
147    def run(self):
148        """
149        Loads and normalizes a NIfTI image in a background thread.
150
151        Steps:
152            1. Load image using memory mapping to minimize RAM usage.
153            2. Verify the file is a valid NIfTI image.
154            3. Canonicalize to RAS+ orientation.
155            4. Normalize data using percentile scaling.
156            5. Emit the finished signal with image data and metadata.
157        """
158        try:
159            self.progress.emit(10)
160
161            # Load image (memory-mapped to avoid heavy RAM use)
162            img = nib.load(self.file_path, mmap="c")
163            self.progress.emit(30)
164
165            if not isinstance(img, (nib.Nifti1Image, nib.Nifti2Image)):
166                raise ValueError(QCoreApplication.translate("Threads", "Not a valid NIfTI file"))
167
168            # Convert to canonical orientation (RAS+)
169            canonical_img = nib.as_closest_canonical(img)
170            self.progress.emit(50)
171
172            dims = canonical_img.header.get_data_shape()
173            is_4d = len(dims) == 4
174            affine = canonical_img.affine
175
176            self.progress.emit(70)
177            log.debug("Load voxel data")
178            # Load voxel data
179            img_data = np.asanyarray(canonical_img.dataobj, dtype=np.float32)
180            self.progress.emit(80)
181
182            log.debug("Normalize image intensities")
183            # Normalize image intensities
184            img_data = self.normalize_data_matplotlib_style(img_data)
185
186            self.progress.emit(100)
187            log.debug("Emit finished signal with image data and metadata.")
188            # Emit the results to the main thread
189            self.finished.emit(img_data, dims, affine, is_4d, self.is_overlay)
190
191        except Exception as e:
192            # Report any errors encountered
193            self.error.emit(str(e))
194
195    def normalize_data_matplotlib_style(self, data):
196        """
197        Normalize NIfTI data using robust percentile scaling (0.5th–99.5th percentiles).
198
199        This approach is similar to how matplotlib normalizes image intensities,
200        providing consistent visual scaling even for datasets with outliers.
201
202        Args:
203            data (np.ndarray): The 3D or 4D voxel intensity data.
204
205        Returns:
206            np.ndarray: Normalized float32 data with intensity values scaled to [0, 1].
207        """
208
209        if data.size == 0:
210            return data
211
212        def normalize_volume(volume):
213            """Normalize a single 3D volume to [0, 1] using percentile-based contrast stretching."""
214            valid_data = volume[np.isfinite(volume)]
215            if valid_data.size == 0:
216                return np.zeros_like(volume, dtype=np.float32)
217
218            vmin, vmax = np.percentile(valid_data, [0.1, 99.9])
219            if vmax <= vmin:
220                vmax = vmin + 1.0
221
222            return np.clip((volume - vmin) / (vmax - vmin), 0, 1).astype(np.float32)
223
224        # Handle both 3D and 4D volumes
225        if data.ndim == 4:
226            normalized = np.empty_like(data, dtype=np.float32)
227            for i in range(data.shape[3]):
228                normalized[..., i] = normalize_volume(data[..., i])
229        else:
230            normalized = normalize_volume(data)
231
232        return normalized

Thread for loading and normalizing large NIfTI files without blocking the UI.

This class supports both 3D and 4D NIfTI volumes and includes robust normalization based on percentile scaling similar to matplotlib's default image scaling behavior.

Signals: finished (object, object, object, bool, bool): Emitted when loading completes successfully. Contains (img_data, dims, affine, is_4d, is_overlay). error (str): Emitted if an error occurs during file loading. progress (int): Emits loading progress updates (0–100).

Args: file_path (str): Path to the NIfTI file to load. is_overlay (bool): Flag indicating whether the file is an overlay image.

ImageLoadThread(file_path, is_overlay)
142    def __init__(self, file_path, is_overlay):
143        super().__init__()
144        self.file_path = file_path
145        self.is_overlay = is_overlay
def finished(unknown):

Signal(object, object, object, bool, bool):
Emitted when the file is successifully loaded.

Parameters:

  • object: img_data.
  • object: dims.
  • object: affine.
  • bool: is_4d.
  • bool: is_overlay.
def error(unknown):

Signal(str):
Emitted when an error occurs during the deep learning execution.

Parameters:

  • str: Description or message detailing the error.
def progress(unknown):

Signal(int):
Emitted to report progress updates during execution.

Parameters:

  • int: Current progress percentage (0–100).
file_path
is_overlay
def run(self):
147    def run(self):
148        """
149        Loads and normalizes a NIfTI image in a background thread.
150
151        Steps:
152            1. Load image using memory mapping to minimize RAM usage.
153            2. Verify the file is a valid NIfTI image.
154            3. Canonicalize to RAS+ orientation.
155            4. Normalize data using percentile scaling.
156            5. Emit the finished signal with image data and metadata.
157        """
158        try:
159            self.progress.emit(10)
160
161            # Load image (memory-mapped to avoid heavy RAM use)
162            img = nib.load(self.file_path, mmap="c")
163            self.progress.emit(30)
164
165            if not isinstance(img, (nib.Nifti1Image, nib.Nifti2Image)):
166                raise ValueError(QCoreApplication.translate("Threads", "Not a valid NIfTI file"))
167
168            # Convert to canonical orientation (RAS+)
169            canonical_img = nib.as_closest_canonical(img)
170            self.progress.emit(50)
171
172            dims = canonical_img.header.get_data_shape()
173            is_4d = len(dims) == 4
174            affine = canonical_img.affine
175
176            self.progress.emit(70)
177            log.debug("Load voxel data")
178            # Load voxel data
179            img_data = np.asanyarray(canonical_img.dataobj, dtype=np.float32)
180            self.progress.emit(80)
181
182            log.debug("Normalize image intensities")
183            # Normalize image intensities
184            img_data = self.normalize_data_matplotlib_style(img_data)
185
186            self.progress.emit(100)
187            log.debug("Emit finished signal with image data and metadata.")
188            # Emit the results to the main thread
189            self.finished.emit(img_data, dims, affine, is_4d, self.is_overlay)
190
191        except Exception as e:
192            # Report any errors encountered
193            self.error.emit(str(e))

Loads and normalizes a NIfTI image in a background thread.

Steps: 1. Load image using memory mapping to minimize RAM usage. 2. Verify the file is a valid NIfTI image. 3. Canonicalize to RAS+ orientation. 4. Normalize data using percentile scaling. 5. Emit the finished signal with image data and metadata.

def normalize_data_matplotlib_style(self, data):
195    def normalize_data_matplotlib_style(self, data):
196        """
197        Normalize NIfTI data using robust percentile scaling (0.5th–99.5th percentiles).
198
199        This approach is similar to how matplotlib normalizes image intensities,
200        providing consistent visual scaling even for datasets with outliers.
201
202        Args:
203            data (np.ndarray): The 3D or 4D voxel intensity data.
204
205        Returns:
206            np.ndarray: Normalized float32 data with intensity values scaled to [0, 1].
207        """
208
209        if data.size == 0:
210            return data
211
212        def normalize_volume(volume):
213            """Normalize a single 3D volume to [0, 1] using percentile-based contrast stretching."""
214            valid_data = volume[np.isfinite(volume)]
215            if valid_data.size == 0:
216                return np.zeros_like(volume, dtype=np.float32)
217
218            vmin, vmax = np.percentile(valid_data, [0.1, 99.9])
219            if vmax <= vmin:
220                vmax = vmin + 1.0
221
222            return np.clip((volume - vmin) / (vmax - vmin), 0, 1).astype(np.float32)
223
224        # Handle both 3D and 4D volumes
225        if data.ndim == 4:
226            normalized = np.empty_like(data, dtype=np.float32)
227            for i in range(data.shape[3]):
228                normalized[..., i] = normalize_volume(data[..., i])
229        else:
230            normalized = normalize_volume(data)
231
232        return normalized

Normalize NIfTI data using robust percentile scaling (0.5th–99.5th percentiles).

This approach is similar to how matplotlib normalizes image intensities, providing consistent visual scaling even for datasets with outliers.

Args: data (np.ndarray): The 3D or 4D voxel intensity data.

Returns: np.ndarray: Normalized float32 data with intensity values scaled to [0, 1].