diff --git a/requirements.txt b/requirements.txt
index a641a9f..b946c67 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,6 +11,7 @@ pandas==2.2.3
 pyfiglet==1.0.2
 pyflakes==3.2.0
 pyjokes==0.6.0
+PyOpenGL==3.1.7
 pyqtdarktheme==2.1.0
 pyqtgraph==0.13.7
 pyserial==3.5
diff --git a/setup.py b/setup.py
index 5871e7c..a5cb910 100644
--- a/setup.py
+++ b/setup.py
@@ -5,7 +5,7 @@
 
 setuptools.setup(
     name='microEye',
-    version='2.2.2',
+    version='2.2.2.post1',
     author='Mohammad Nour Alsamsam',
     author_email='nour.alsamsam@gmail.com',
     description='A python toolkit for fluorescence microscopy \
@@ -47,6 +47,7 @@
         'pyfiglet==1.0.2',
         'pyflakes==3.2.0',
         'pyjokes==0.6.0',
+        'PyOpenGL==3.1.7',
         'pyqtdarktheme==2.1.0',
         'pyqtgraph==0.13.7',
         'pyserial==3.5',
diff --git a/src/microEye/__init__.py b/src/microEye/__init__.py
index 9e86106..ff92ca0 100644
--- a/src/microEye/__init__.py
+++ b/src/microEye/__init__.py
@@ -38,7 +38,7 @@ def getArgs():
 
 ARGS = getArgs()
 
-VERSION = '2.2.2'
+VERSION = '2.2.2.post1'
 
 from microEye.qt import *  # noqa: I001, E402
 import microEye.analysis.fitting.pyfit3Dcspline as pyfit3Dcspline  # noqa: E402
diff --git a/src/microEye/analysis/cmosMaps.py b/src/microEye/analysis/cmosMaps.py
index a3be4e0..abd33c7 100644
--- a/src/microEye/analysis/cmosMaps.py
+++ b/src/microEye/analysis/cmosMaps.py
@@ -74,7 +74,7 @@ def InitLayout(self):
 
         self.exp_spin = QtWidgets.QDoubleSpinBox()
         self.exp_spin.setMinimum(0)
-        self.exp_spin.setMaximum(1e4)
+        self.exp_spin.setMaximum(10000)
         self.exp_spin.setDecimals(5)
         self.exp_spin.setValue(self.expTime)
 
@@ -94,10 +94,10 @@ def InitLayout(self):
         self.Y.setMinimum(0)
         self.W.setMinimum(0)
         self.H.setMinimum(0)
-        self.X.setMaximum(1e4)
-        self.Y.setMaximum(1e4)
-        self.W.setMaximum(1e4)
-        self.H.setMaximum(1e4)
+        self.X.setMaximum(10000)
+        self.Y.setMaximum(10000)
+        self.W.setMaximum(10000)
+        self.H.setMaximum(10000)
 
         self.main_layout.addRow(QtWidgets.QLabel('ROI X:'), self.X)
         self.main_layout.addRow(QtWidgets.QLabel('ROI Y:'), self.Y)
diff --git a/src/microEye/analysis/fitting/nena.py b/src/microEye/analysis/fitting/nena.py
index 2198655..b2631d0 100644
--- a/src/microEye/analysis/fitting/nena.py
+++ b/src/microEye/analysis/fitting/nena.py
@@ -220,7 +220,7 @@ def __init__(
 
         self.bins = QtWidgets.QSpinBox()
         self.bins.setMinimum(5)
-        self.bins.setMaximum(1e4)
+        self.bins.setMaximum(10000)
         self.bins.setValue(200)
 
         self.fitlay.addRow(
diff --git a/src/microEye/analysis/fitting/psf/extract.py b/src/microEye/analysis/fitting/psf/extract.py
index e15d1ba..ca5d4d2 100644
--- a/src/microEye/analysis/fitting/psf/extract.py
+++ b/src/microEye/analysis/fitting/psf/extract.py
@@ -7,22 +7,33 @@
 import h5py
 import numba as nb
 import numpy as np
+import pyqtgraph as pg
+from scipy.optimize import curve_fit
+from scipy.signal import find_peaks, peak_prominences
 from tqdm import tqdm
 
 from microEye.analysis.fitting.results import PARAMETER_HEADERS
 from microEye.utils.uImage import TiffSeqHandler, ZarrImageSequence, uImage
 
 
+def gaussian(x, a, x0, sigma, offset):
+    return a * np.exp(-((x - x0) ** 2) / (2 * sigma**2)) + offset
+
+
 class PSFdata:
     def __init__(
         self, data_dict: dict[str, Union[int, np.ndarray, list, float]]
     ) -> None:
         self._data = data_dict
+        self._last_field = 0
 
     def get_field_psf(self, grid_size: int = 3):
         '''
         Get PSF field for the given grid size.
         '''
+        if self._last_field == grid_size:
+            return
+
         width, height = self.dim
         width *= self.upsample
         height *= self.upsample
@@ -46,6 +57,8 @@ def get_grid_cell_index(x_start, y_start):
                     zslice['field'][idx].append(zslice['rois'][i].copy())
                 zslice['field_idx'][i] = idx
 
+        self._last_field = grid_size
+
     @property
     def zslices(self) -> list[dict[str, Union[int, np.ndarray, list, float]]]:
         return self._data.get('zslices')
@@ -179,6 +192,501 @@ def __next__(self):
         else:
             raise StopIteration
 
+    def get_z_slice(
+        self,
+        z_index: int,
+        type: str = 'mean',
+        roi_index: int = 0,
+        grid_size: int = 1,
+        normalize: bool = False,
+    ) -> tuple[Optional[np.ndarray], Optional[np.ndarray]]:
+        """
+        Get the XY slice for the given z-index.
+
+        Parameters:
+        -----------
+        z_index : int
+            The z-index of the slice
+        type : str, optional
+            The type of data to return, by default 'mean'
+            Options: 'mean', 'std', 'median', 'roi'
+        roi_index : int, optional
+            The index of the ROI to return, by default 0
+        grid_size : int, optional
+            The grid size for field PSF, by default 1
+        normalize : bool, optional
+            Whether to normalize the data, by default False
+
+        Returns:
+        --------
+        tuple[np.ndarray, np.ndarray]
+            The data array and grid overlay
+        """
+        # check if Z index is in bounds
+        if z_index >= len(self.zslices):
+            raise IndexError(f'Z index {z_index} out of bounds')
+
+        # get the z-slice
+        zslice = self.zslices[z_index]
+
+        # check if roi index is in bounds
+        if roi_index >= zslice['count']:
+            raise IndexError(
+                f'ROI index {roi_index} out of bounds for z-slice {z_index}'
+            )
+
+        # limit grid_size from 1 to 5
+        grid_size = max(1, min(grid_size, 5))
+
+        # get the ROI
+        if type in ['mean', 'median', 'std', 'roi']:
+            if grid_size == 1:
+                if type == 'roi':
+                    data = zslice['rois'][roi_index]
+                    return None if np.isnan(data).any() else data, None
+                return zslice[type], None
+            else:
+                self.get_field_psf(grid_size)
+                colors = [
+                    pg.intColor(i, grid_size**2).getRgb() for i in range(grid_size**2)
+                ]
+
+                roi_size = self.roi_size
+                size = roi_size * grid_size
+
+                # Create empty arrays for visualization
+                psf_image = np.zeros((size, size))
+                # RGBA for grid overlay
+                grid_overlay = np.zeros((size, size, 4))
+
+                for field_idx, field_rois in enumerate(zslice['field']):
+                    if not field_rois:
+                        continue
+
+                    field_rois = np.array(field_rois)
+
+                    if type == 'mean':
+                        stat = np.nanmean(field_rois, axis=0)
+                    elif type == 'median':
+                        stat = np.nanmedian(field_rois, axis=0)
+                    elif type == 'std':
+                        stat = np.nanstd(field_rois, axis=0)
+                    else:  # Single ROI
+                        stat = (
+                            field_rois[roi_index]
+                            if roi_index < len(field_rois)
+                            else None
+                        )
+
+                    if stat is not None:
+                        y = field_idx // grid_size
+                        x = field_idx % grid_size
+                        psf_image[
+                            y * roi_size : (y + 1) * roi_size,
+                            x * roi_size : (x + 1) * roi_size,
+                        ] = 2**16 * stat / np.sum(stat) if normalize else stat
+
+                        # Add colored overlay for grid visualization
+                        color = colors[field_idx]
+                        grid_overlay[
+                            y * roi_size : (y + 1) * roi_size,
+                            x * roi_size : (x + 1) * roi_size,
+                        ] = [color[0], color[1], color[2], 128]
+                return psf_image, grid_overlay
+        else:
+            raise ValueError(f'Invalid type {type}')
+
+    def get_longitudinal_slice(
+        self,
+        index: int,
+        type: str = 'mean',
+        grid_size: int = 1,
+        sagittal: bool = True,
+        normalize: bool = False,
+    ) -> tuple[Optional[np.ndarray], Optional[np.ndarray]]:
+        """
+        Get the longitudinal slice for the given index.
+
+        Parameters
+        ----------
+        index : int
+            The index of the slice
+        type : str, optional
+            The type of data to return, by default 'mean'
+            Options: 'mean', 'median', 'std'
+        grid_size : int, optional
+            The grid size for field PSF, by default 1
+        sagittal : bool, optional
+            Whether to get the sagittal slice, or coronal, by default True
+        normalize : bool, optional
+            Whether to normalize the data, by default False
+
+        Returns
+        -------
+        tuple[np.ndarray, np.ndarray]
+            The data array and grid overlay
+        """
+        # check if index is in bounds
+        if index >= self.roi_size:
+            raise IndexError(f'Y index {index} out of bounds')
+
+        # limit grid_size from 1 to 5
+        grid_size = np.clip(grid_size, 1, 5)
+
+        roi_height, roi_width = self.roi_size, len(self.zslices)
+
+        if type not in ['mean', 'median', 'std']:
+            raise ValueError(f'Invalid type {type}')
+
+        if grid_size == 1:
+            data = np.array(
+                [
+                    zslice[type][index] if sagittal else zslice[type][:, index]
+                    for zslice in self.zslices
+                ]
+            ).T
+            return data, None
+
+        self.get_field_psf(grid_size)
+        colors = [pg.intColor(i, grid_size**2).getRgb() for i in range(grid_size**2)]
+
+        height, width = roi_height * grid_size, roi_width * grid_size
+
+        psf_image = np.zeros((height, width))
+        grid_overlay = np.zeros((height, width, 4), dtype=np.uint8)
+
+        stat_func = {'mean': np.nanmean, 'median': np.nanmedian, 'std': np.nanstd}[type]
+
+        grid_painted = []
+        for i, zslice in enumerate(self.zslices):
+            for field_idx, field_rois in enumerate(zslice['field']):
+                if not field_rois:
+                    continue
+
+                field_rois = (
+                    np.array(field_rois)[:, index, :]
+                    if sagittal
+                    else np.array(field_rois)[..., index]
+                )
+                stat = stat_func(field_rois, axis=0)
+
+                if stat is not None:
+                    y, x = divmod(field_idx, grid_size)
+                    psf_image[
+                        y * roi_height : (y + 1) * roi_height, x * roi_width + i
+                    ] = stat
+
+                    if field_idx not in grid_painted:
+                        color = colors[field_idx]
+                        grid_overlay[
+                            y * roi_height : (y + 1) * roi_height,
+                            x * roi_width : (x + 1) * roi_width,
+                        ] = [*color[:3], 128]
+                        grid_painted.append(field_idx)
+
+        return psf_image, grid_overlay
+
+    def get_x_slice(
+        self, x_index: int, type: str = 'mean', grid_size: int = 1
+    ) -> tuple[Optional[np.ndarray], Optional[np.ndarray]]:
+        """
+        Get the YZ slice for the given x-index.
+
+        Parameters
+        ----------
+        x_index : int
+            The x-index of the slice
+        type : str, optional
+            The type of data to return, by default 'mean'
+            Options: 'mean', 'median', 'std'
+        grid_size : int, optional
+            The grid size for field PSF, by default 1
+
+        Returns
+        -------
+        tuple[np.ndarray, np.ndarray]
+            The data array and grid overlay
+        """
+        return self.get_longitudinal_slice(
+            x_index, type=type, grid_size=grid_size, sagittal=False
+        )
+
+    def get_y_slice(
+        self, y_index: int, type: str = 'mean', grid_size: int = 1
+    ) -> tuple[Optional[np.ndarray], Optional[np.ndarray]]:
+        """
+        Get the XZ slice for the given y-index.
+
+        Parameters
+        ----------
+        y_index : int
+            The y-index of the slice
+        type : str, optional
+            The type of data to return, by default 'mean'
+            Options: 'mean', 'median', 'std'
+        grid_size : int, optional
+            The grid size for field PSF, by default 1
+
+        Returns
+        -------
+        tuple[np.ndarray, np.ndarray]
+            The data array and grid overlay
+        """
+        return self.get_longitudinal_slice(
+            y_index, type=type, grid_size=grid_size, sagittal=True
+        )
+
+    def get_volume(self, type: str = 'mean'):
+        """
+        Get the 3D volume for the given type.
+
+        Parameters
+        ----------
+        type : str, optional
+            The type of data to return, by default 'mean'
+
+            Options:
+                'mean' - Mean intensity
+
+                'median' - Median intensity
+
+                'std' - Standard deviation of intensity
+
+        Returns
+        -------
+        np.ndarray
+            The 3D volume data
+        """
+        if type not in ['mean', 'median', 'std']:
+            raise ValueError(f'Invalid type {type}')
+
+        volume_data = np.zeros((len(self), self.roi_size, self.roi_size))
+
+        for i, z_slice in enumerate(self.zslices):
+            volume_data[i] = (
+                z_slice[type]
+                if z_slice[type] is not None
+                else np.zeros((self.roi_size, self.roi_size))
+            )
+
+        return volume_data
+
+    def get_ratio(self) -> float:
+        '''
+        Get the ratio of Z step to lateral pixel size.
+
+        Returns
+        -------
+        float
+            The ratio of Z step to lateral pixel size
+        '''
+        return self.z_step * self.upsample / self.pixel_size
+
+    def get_intensity_stats(self):
+        '''
+        Get the intensity statistics for all z-slices.
+
+        Returns
+        -------
+        _indices, _mean, _median, _std: tuple[np.ndarray, list, list, list]
+            The indices, mean, median, and standard deviation of the intensity values
+        '''
+        _indices = (np.arange(len(self)) - self.zero_plane) * self.z_step
+        _mean = []
+        _median = []
+        _std = []
+
+        for z_slice in self.zslices:
+            if z_slice['rois'] is not None:
+                valid_rois = z_slice['rois'][
+                    ~np.isnan(z_slice['rois']).any(axis=(1, 2))
+                ]
+                if len(valid_rois) > 0:
+                    _mean.append(np.mean(valid_rois))
+                    _median.append(np.median(valid_rois))
+                    _std.append(np.std(valid_rois))
+                else:
+                    _mean.append(np.nan)
+                    _median.append(np.nan)
+                    _std.append(np.nan)
+            else:
+                _mean.append(np.nan)
+                _median.append(np.nan)
+                _std.append(np.nan)
+
+        return _indices, _mean, _median, _std
+
+    def get_stats(self, selected_stat: str):
+        """
+        Get the statistic data for all z-slices.
+
+        Parameters
+        ----------
+        selected_stat : str
+            The selected statistic to return
+            Options:
+                'Counts', 'Sigma', 'Sigma (sum)', 'Sigma (diff)', 'Sigma (abs(diff))',
+                'Intensity', 'Background'
+
+        Returns
+        -------
+        z_indices, param_stat: tuple[np.ndarray, np.ndarray]
+            The z-indices and the statistic data for all z-slices
+        """
+        z_indices = (np.arange(len(self)) - self.zero_plane) * self.z_step
+        header = PARAMETER_HEADERS[self.fitting_method]
+
+        param_stat = []
+        if selected_stat == 'Sigma':
+            if 'sigmax' in header:
+                param_stat.append([])
+            if 'sigmay' in header:
+                param_stat.append([])
+
+        for z_slice in self.zslices:
+            if z_slice['rois'] is not None:
+                valid_rois = z_slice['rois'][
+                    ~np.isnan(z_slice['rois']).any(axis=(1, 2))
+                ]
+                if selected_stat == 'Counts':
+                    param_stat.append(len(valid_rois))
+                elif len(valid_rois) > 0:
+                    if 'Sigma' in selected_stat:
+                        sigma_x = sigma_y = 0
+                        if 'sigmax' in header:
+                            sigma_x = np.mean(
+                                z_slice['params'][:, header.index('sigmax')]
+                            )
+                        if 'sigmay' in header:
+                            sigma_y = np.mean(
+                                z_slice['params'][:, header.index('sigmay')]
+                            )
+                        if selected_stat == 'Sigma':
+                            param_stat[0].append(sigma_x)
+                            param_stat[1].append(sigma_y)
+                        elif selected_stat == 'Sigma (sum)':
+                            param_stat.append(sigma_x + sigma_y)
+                        elif selected_stat == 'Sigma (diff)':
+                            param_stat.append(sigma_x - sigma_y)
+                        elif selected_stat == 'Sigma (abs(diff))':
+                            param_stat.append(abs(sigma_x - sigma_y))
+                        else:
+                            param_stat.append(np.nan)
+                    elif selected_stat == 'Intensity':
+                        param_stat.append(
+                            np.mean(z_slice['params'][:, header.index('intensity')])
+                        )
+                    elif selected_stat == 'Background':
+                        param_stat.append(
+                            np.mean(z_slice['params'][:, header.index('background')])
+                        )
+                else:
+                    if selected_stat == 'Sigma':
+                        if 'sigmax' in header:
+                            param_stat[0].append(np.nan)
+                        if 'sigmay' in header:
+                            param_stat[1].append(np.nan)
+                    else:
+                        param_stat.append(np.nan)
+            else:
+                param_stat.append(np.nan)
+
+        return z_indices, np.array(param_stat)
+
+    def adjust_zero_plane(
+        self, selected_stat: str, method: str, region: tuple[int, int]
+    ):
+        """
+        Adjust the zero plane based on the selected statistic and method.
+
+        Parameters
+        ----------
+        selected_stat : str
+            The selected statistic to use for zero plane adjustment
+
+            Options:
+
+                'Counts', 'Sigma', 'Sigma (sum)', 'Sigma (diff)', 'Sigma (abs(diff))',
+                'Intensity', 'Background'
+        method : str
+            The method to use for zero plane adjustment
+
+            Options: 'Peak', 'Valley', 'Gaussian Fit', 'Gaussian Fit (Inverted)',
+            'Manual'
+        region : tuple[int, int]
+            The region to use for zero plane adjustment
+
+        Returns
+        -------
+        int
+            The new zero plane
+        """
+        if selected_stat == 'Sigma':
+            selected_stat = 'Sigma (sum)'
+
+        start, end = map(
+            int,
+            [v / self.z_step + self.zero_plane for v in region],
+        )
+
+        # Store the old zero plane for potential restoration
+        self.old_zero_plane = self.zero_plane
+
+        _, stat_data = self.get_stats(selected_stat)
+
+        # Slice the data according to the specified range
+        x = np.arange(start, end)
+        y = stat_data[start:end]
+
+        if method == 'Peak':
+            # Find peaks
+            peaks, _ = find_peaks(y)
+            if len(peaks) > 0:
+                # Get peak prominences
+                prominences = peak_prominences(y, peaks)[0]
+                # Select the most prominent peak
+                max_peak = peaks[np.argmax(prominences)]
+                new_zero_plane = start + max_peak
+            else:
+                new_zero_plane = start + np.argmax(y)
+        elif method == 'Valley':
+            # Invert the data to find valleys as peaks
+            inverted_y = -y
+            valleys, _ = find_peaks(inverted_y)
+            if len(valleys) > 0:
+                # Get valley prominences
+                prominences = peak_prominences(inverted_y, valleys)[0]
+                # Select the most prominent valley
+                max_valley = valleys[np.argmax(prominences)]
+                new_zero_plane = start + max_valley
+            else:
+                new_zero_plane = start + np.argmin(y)
+        elif 'Gaussian Fit' in method:
+            if 'Inverted' in method:
+                y = -y
+            try:
+                # Initial guess for Gaussian parameters
+                a_init = np.max(y) - np.min(y)
+                x0_init = x[np.argmax(y)]
+                sigma_init = (end - start) / 4
+                offset_init = np.min(y)
+
+                # Perform Gaussian fit
+                popt, _ = curve_fit(
+                    gaussian, x, y, p0=[a_init, x0_init, sigma_init, offset_init]
+                )
+                new_zero_plane = int(popt[1])  # x0 is the center of the Gaussian
+            except Exception:
+                # Fallback to original plane if fitting fails
+                new_zero_plane = self.zero_plane
+        else:
+            return self.zero_plane  # Keep current zero plane for 'Manual' method
+
+        # Update the zero plane
+        self.zero_plane = new_zero_plane
+
+        return self.zero_plane
+
     def get_consistent_rois(self) -> 'PSFdata':
         '''
         Creates a new PSFdata object with consistent ROIs across all z-slices.
diff --git a/src/microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUmleFit_LM.py b/src/microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUmleFit_LM.py
index 3d7af20..130bfe2 100644
--- a/src/microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUmleFit_LM.py
+++ b/src/microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUmleFit_LM.py
@@ -844,7 +844,7 @@ def kernel_MLEFit_LM_sigmaxy(d_data, PSFSigma, sz, iterations, d_varim=None):
 
     oldTheta[:] = newTheta[:]
 
-    for _ in range(iterations):
+    for kk in range(iterations):  # noqa: B007
         # Compute Jacobian and Hessian
         newErr = 0
         jacobian.fill(0)
diff --git a/src/microEye/analysis/fitting/tardis.py b/src/microEye/analysis/fitting/tardis.py
index f67cbf8..1201a72 100644
--- a/src/microEye/analysis/fitting/tardis.py
+++ b/src/microEye/analysis/fitting/tardis.py
@@ -65,7 +65,7 @@ def __init__(
 
         self.bins = QtWidgets.QSpinBox()
         self.bins.setMinimum(5)
-        self.bins.setMaximum(1e4)
+        self.bins.setMaximum(10000)
         self.bins.setValue(1200)
 
         self.tardis_lay.addRow(
@@ -74,7 +74,7 @@ def __init__(
 
         self.maxDist = QtWidgets.QSpinBox()
         self.maxDist.setMinimum(5)
-        self.maxDist.setMaximum(1e4)
+        self.maxDist.setMaximum(10000)
         self.maxDist.setValue(1200)
 
         self.tardis_lay.addRow(
diff --git a/src/microEye/analysis/viewer/images.py b/src/microEye/analysis/viewer/images.py
index 2e5ddf9..7e7d7af 100644
--- a/src/microEye/analysis/viewer/images.py
+++ b/src/microEye/analysis/viewer/images.py
@@ -757,7 +757,7 @@ def extraction_protocol(self, filename: str, **kwargs):
             params,
             crlbs,
             loglike,
-            method,
+            method.value,
             self.get_param(Parameters.PIXEL_SIZE).value(),
             self.get_param(Parameters.PSF_ZSTEP).value(),
             self.get_param(Parameters.ROI_SIZE).value(),
diff --git a/src/microEye/analysis/viewer/psf.py b/src/microEye/analysis/viewer/psf.py
index 0f4d114..867e7d4 100644
--- a/src/microEye/analysis/viewer/psf.py
+++ b/src/microEye/analysis/viewer/psf.py
@@ -5,8 +5,6 @@
 import pyqtgraph as pg
 import pyqtgraph.opengl as gl
 from pyqtgraph import functions as fn
-from scipy.optimize import curve_fit
-from scipy.signal import find_peaks, peak_prominences
 
 from microEye.analysis.fitting.psf import PSFdata
 from microEye.analysis.fitting.results import PARAMETER_HEADERS, FittingMethod
@@ -46,6 +44,9 @@ def __init__(self, psf_data: Union['PSFdata', str] = None):
         self.view_tabs = QtWidgets.QTabWidget()
         self.main_layout.addWidget(self.view_tabs)
 
+        # Default view mode
+        self.view_mode = 'XY'
+
         # Setup different view tabs
         self.setup_merged_view_tab()
         self.setup_3d_view_tab()
@@ -64,15 +65,15 @@ def setup_merged_view_tab(self):
         visual_layout = QtWidgets.QVBoxLayout()
 
         self.image_widget = pg.GraphicsLayoutWidget()
-        self.view_box = self.image_widget.addViewBox(row=0, col=0)
+        self.view_box: pg.ViewBox = self.image_widget.addViewBox(row=0, col=0)
         self.view_box.setAspectLocked(True)
         self.view_box.setAutoVisible(True)
         self.view_box.enableAutoRange()
         self.view_box.invertY(True)
 
         # Create two image items: one for PSF data and one for grid overlay
-        self.psf_image_item = pg.ImageItem()
-        self.grid_overlay_item = pg.ImageItem()
+        self.psf_image_item: pg.ImageItem = pg.ImageItem(axisOrder='row-major')
+        self.grid_overlay_item: pg.ImageItem = pg.ImageItem(axisOrder='row-major')
         self.view_box.addItem(self.psf_image_item)
         self.view_box.addItem(self.grid_overlay_item)
 
@@ -86,27 +87,28 @@ def setup_merged_view_tab(self):
 
         visual_layout.addWidget(self.image_widget)
 
-        # Z-slice selection
-        z_slice_layout = QtWidgets.QHBoxLayout()
-        self.z_slice_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
-        self.z_slice_spinbox = QtWidgets.QSpinBox()
-        self.z_slice_slider.setMinimum(0)
-        self.z_slice_spinbox.setMinimum(0)
-        self.z_slice_spinbox.setMinimumWidth(75)
+        # Modify Z-slice selection to be more generic
+        slice_layout = QtWidgets.QHBoxLayout()
+        self.slice_label = QtWidgets.QLabel('Z-Slice:')
+        self.slice_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
+        self.slice_spinbox = QtWidgets.QSpinBox()
+        self.slice_slider.setMinimum(0)
+        self.slice_spinbox.setMinimum(0)
+        self.slice_spinbox.setMinimumWidth(75)
 
-        max_z = len(self.psf_data) - 1
-        self.z_slice_slider.setMaximum(max_z)
-        self.z_slice_spinbox.setMaximum(max_z)
+        max_slice = len(self.psf_data) - 1
+        self.slice_slider.setMaximum(max_slice)
+        self.slice_spinbox.setMaximum(max_slice)
 
-        self.z_slice_slider.valueChanged.connect(self.z_slice_spinbox.setValue)
-        self.z_slice_spinbox.valueChanged.connect(self.z_slice_slider.setValue)
-        self.z_slice_slider.valueChanged.connect(self.update_merged_view)
+        self.slice_slider.valueChanged.connect(self.slice_spinbox.setValue)
+        self.slice_spinbox.valueChanged.connect(self.slice_slider.setValue)
+        self.slice_slider.valueChanged.connect(self.update_merged_view)
 
-        z_slice_layout.addWidget(QtWidgets.QLabel('Z-Slice:'))
-        z_slice_layout.addWidget(self.z_slice_slider)
-        z_slice_layout.addWidget(self.z_slice_spinbox)
+        slice_layout.addWidget(self.slice_label)
+        slice_layout.addWidget(self.slice_slider)
+        slice_layout.addWidget(self.slice_spinbox)
 
-        visual_layout.addLayout(z_slice_layout)
+        visual_layout.addLayout(slice_layout)
 
         merged_layout.addLayout(visual_layout, stretch=2)
 
@@ -128,7 +130,7 @@ def setup_merged_view_tab(self):
         grid_layout.addRow('Grid Size:', self.grid_size)
 
         # Grid overlay opacity
-        self.grid_opacity = QtWidgets.QSlider(QtCore.Qt.Horizontal)
+        self.grid_opacity = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
         self.grid_opacity.setMinimum(0)
         self.grid_opacity.setMaximum(100)
         self.grid_opacity.setValue(50)
@@ -142,6 +144,13 @@ def setup_merged_view_tab(self):
         display_group = QtWidgets.QGroupBox('Display Options')
         display_layout = QtWidgets.QVBoxLayout()
 
+        # Add view mode selector
+        display_layout.addWidget(QtWidgets.QLabel('View Mode:'))
+        self.view_mode_combo = QtWidgets.QComboBox()
+        self.view_mode_combo.addItems(['XY', 'XZ', 'YZ'])
+        self.view_mode_combo.currentTextChanged.connect(self.change_view_mode)
+        display_layout.addWidget(self.view_mode_combo)
+
         self.display_mean = QtWidgets.QRadioButton('Mean')
         self.display_median = QtWidgets.QRadioButton('Median')
         self.display_std = QtWidgets.QRadioButton('Standard Deviation')
@@ -159,7 +168,7 @@ def setup_merged_view_tab(self):
 
         # Add Single ROI selection
         single_roi_layout = QtWidgets.QHBoxLayout()
-        self.single_roi_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
+        self.single_roi_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
         self.single_roi_spinbox = QtWidgets.QSpinBox()
 
         self.single_roi_slider.setMinimum(0)
@@ -199,7 +208,11 @@ def setup_merged_view_tab(self):
         self.auto_level_checkbox = QtWidgets.QCheckBox('Auto Level')
         self.auto_level_checkbox.setChecked(True)
         self.auto_level_checkbox.stateChanged.connect(self.update_merged_view)
+        self.normalization_checkbox = QtWidgets.QCheckBox('Normalize (Field)')
+        self.normalization_checkbox.setChecked(True)
+        self.normalization_checkbox.stateChanged.connect(self.update_merged_view)
         auto_level_layout.addWidget(self.auto_level_checkbox)
+        auto_level_layout.addWidget(self.normalization_checkbox)
         auto_level_group.setLayout(auto_level_layout)
         controls_layout.addWidget(auto_level_group)
 
@@ -216,9 +229,7 @@ def setup_merged_view_tab(self):
         self.refresh_btn = QtWidgets.QPushButton('Refresh View')
         self.refresh_btn.clicked.connect(self.update_merged_view)
         self.zero_btn = QtWidgets.QPushButton(f'Zero Plane {self.psf_data.zero_plane}')
-        self.zero_btn.clicked.connect(
-            lambda: self.z_slice_spinbox.setValue(self.psf_data.zero_plane)
-        )
+        self.zero_btn.clicked.connect(self.update_slice_controls)
         controls_layout.addWidget(self.refresh_btn)
         controls_layout.addWidget(self.zero_btn)
 
@@ -262,6 +273,7 @@ def setup_statistics_tab(self):
                 'Sigma',
                 'Sigma (sum)',
                 'Sigma (diff)',
+                'Sigma (abs(diff))',
                 'Intensity',
                 'Background',
             ]
@@ -374,7 +386,22 @@ def setup_3d_view_tab(self):
         type_group = QtWidgets.QGroupBox('Display Type')
         type_layout = QtWidgets.QVBoxLayout()
         self.view_3d_type = QtWidgets.QComboBox()
-        self.view_3d_type.addItems(['Mean', 'Median', 'Standard Deviation'])
+        display_types = [
+            {
+                'name': 'Mean',
+                'value': 'mean',
+            },
+            {
+                'name': 'Median',
+                'value': 'median',
+            },
+            {
+                'name': 'Standard Deviation',
+                'value': 'std',
+            },
+        ]
+        for t in display_types:
+            self.view_3d_type.addItem(t['name'], t['value'])
         self.view_3d_type.currentTextChanged.connect(self.update_3d_view)
         type_layout.addWidget(self.view_3d_type)
         type_group.setLayout(type_layout)
@@ -383,7 +410,7 @@ def setup_3d_view_tab(self):
         # Opacity control
         opacity_group = QtWidgets.QGroupBox('Opacity')
         opacity_layout = QtWidgets.QVBoxLayout()
-        self.opacity_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
+        self.opacity_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
         self.opacity_slider.setMinimum(0)
         self.opacity_slider.setMaximum(100)
         self.opacity_slider.setValue(10)
@@ -418,92 +445,41 @@ def set_3d_view(self, args: tuple[int]):
         self.gl_widget.opts['distance'] = 180
         self.gl_widget.update()
 
+    def get_display_type(self) -> str:
+        '''Get the display type for the merged view.'''
+        if self.display_mean.isChecked():
+            return 'mean'
+        elif self.display_median.isChecked():
+            return 'median'
+        elif self.display_std.isChecked():
+            return 'std'
+        else:
+            return 'roi'
+
     def update_merged_view(self):
-        '''Update the merged slice/field visualization based on current settings.'''
         if self.psf_data is None:
             return
 
-        z_index = self.z_slice_slider.value()
+        slice_index = self.slice_slider.value()
         grid_size = self.grid_size.value()
+        # get display type (mean, median ... etc)
+        display_type = self.get_display_type()
+        norm = self.normalization_checkbox.isChecked()
 
-        if z_index >= len(self.psf_data):
-            return
+        if self.view_mode == 'XY':
+            roi_index = self.single_roi_slider.value()
 
-        # Get field PSF data if needed
-        if grid_size > 1:
-            self.psf_data.get_field_psf(grid_size)
-
-        current_slice = self.psf_data.zslices[z_index]
-        roi_size = self.psf_data.roi_size
-        size = roi_size * grid_size
-
-        # Update single ROI slider/spinbox maximum
-        max_rois = (current_slice['count'] - 1) if current_slice['count'] > 0 else 0
-        self.single_roi_slider.setMaximum(max_rois)
-        self.single_roi_spinbox.setMaximum(max_rois)
-
-        # Create empty arrays for visualization
-        psf_image = np.zeros((size, size))
-        grid_overlay = np.zeros((size, size, 4))  # RGBA for grid overlay
-
-        if grid_size == 1:
-            # Handle single grid case (original slice view)
-            if self.display_mean.isChecked():
-                display_data = current_slice['mean']
-            elif self.display_median.isChecked():
-                display_data = current_slice['median']
-            elif self.display_std.isChecked():
-                display_data = current_slice['std']
-            else:  # Single ROI
-                if current_slice['rois'] is not None and current_slice['count'] > 0:
-                    roi_index = self.single_roi_slider.value()
-                    if roi_index < len(current_slice['rois']):
-                        display_data = current_slice['rois'][roi_index]
-                    else:
-                        display_data = None
-                else:
-                    display_data = None
-
-            if display_data is not None and not np.isnan(display_data).all():
-                psf_image = display_data
-        else:
-            # Handle multiple grid case (field view)
-            colors = [
-                pg.intColor(i, grid_size**2).getRgb() for i in range(grid_size**2)
-            ]
-
-            for field_idx, field_rois in enumerate(current_slice['field']):
-                if not field_rois:
-                    continue
-
-                field_rois = np.array(field_rois)
-
-                if self.display_mean.isChecked():
-                    stat = np.nanmean(field_rois, axis=0)
-                elif self.display_median.isChecked():
-                    stat = np.nanmedian(field_rois, axis=0)
-                elif self.display_std.isChecked():
-                    stat = np.nanstd(field_rois, axis=0)
-                else:  # Single ROI
-                    roi_index = self.single_roi_slider.value()
-                    stat = (
-                        field_rois[roi_index] if roi_index < len(field_rois) else None
-                    )
-
-                if stat is not None:
-                    y = field_idx // grid_size
-                    x = field_idx % grid_size
-                    psf_image[
-                        y * roi_size : (y + 1) * roi_size,
-                        x * roi_size : (x + 1) * roi_size,
-                    ] = stat
-
-                    # Add colored overlay for grid visualization
-                    color = colors[field_idx]
-                    grid_overlay[
-                        y * roi_size : (y + 1) * roi_size,
-                        x * roi_size : (x + 1) * roi_size,
-                    ] = [color[0], color[1], color[2], 128]
+            psf_image, grid_overlay = self.psf_data.get_z_slice(
+                slice_index, display_type, roi_index, grid_size, norm
+            )
+        elif self.view_mode == 'XZ':
+            psf_image, grid_overlay = self.psf_data.get_y_slice(
+                slice_index, display_type, grid_size
+            )
+        else:  # YZ
+            psf_image, grid_overlay = self.psf_data.get_x_slice(
+                slice_index, display_type, grid_size
+            )
 
         # Update the visualization
         if self.auto_level_checkbox.isChecked():
@@ -511,12 +487,20 @@ def update_merged_view(self):
         else:
             self.psf_image_item.setImage(psf_image, autoLevels=False)
 
-        self.grid_overlay_item.setImage(grid_overlay)
-        self.update_grid_opacity()
+        if grid_overlay is not None:
+            self.grid_overlay_item.setImage(grid_overlay)
+            self.update_grid_opacity()
+        else:
+            self.grid_overlay_item.clear()
 
         # Update metadata
         self.update_metadata()
 
+    def change_view_mode(self, mode):
+        self.view_mode = mode
+        self.update_slice_controls()
+        self.update_merged_view()
+
     def update_metadata(self):
         '''Update the metadata display in the PSF view tab.'''
         if self.psf_data is None:
@@ -566,30 +550,8 @@ def update_3d_view(self):
             zero = self.psf_data.zero_plane
 
             # Prepare volumetric data based on selected type
-            view_type = self.view_3d_type.currentText()
-            volume_data = np.zeros(
-                (len(self.psf_data), self.psf_data.roi_size, self.psf_data.roi_size)
-            )
-
-            for i, z_slice in enumerate(self.psf_data.zslices):
-                if view_type == 'Mean':
-                    volume_data[i] = (
-                        z_slice['mean']
-                        if z_slice['mean'] is not None
-                        else np.zeros(self.psf_data.dim)
-                    )
-                elif view_type == 'Median':
-                    volume_data[i] = (
-                        z_slice['median']
-                        if z_slice['median'] is not None
-                        else np.zeros(self.psf_data.dim)
-                    )
-                elif view_type == 'Standard Deviation':
-                    volume_data[i] = (
-                        z_slice['std']
-                        if z_slice['std'] is not None
-                        else np.zeros(self.psf_data.dim)
-                    )
+            view_type = self.view_3d_type.currentData()
+            volume_data = self.psf_data.get_volume(view_type)
 
             # Normalize data
             if np.all(volume_data == 0):
@@ -615,12 +577,20 @@ def update_3d_view(self):
             opacity = self.opacity_slider.value() / 100.0
             d2[..., 3] = (normalized_data * 255 * opacity).astype(np.ubyte)
 
+            # Set scaling ratios for X and Y axes
+            x_ratio = y_ratio = 1  # X and Y are the reference, Z scaled
+            # z_ratio
+            z_ratio = self.psf_data.get_ratio()
+
             # Create and add volume item
             v = gl.GLVolumeItem(d2)
+
+            v.scale(z_ratio, y_ratio, x_ratio)  # Apply scaling in (z, y, x) order
+
             v.translate(
-                -zero,
-                -volume_data.shape[1] // 2,
-                -volume_data.shape[2] // 2,
+                -zero * z_ratio,
+                -volume_data.shape[1] // 2 * y_ratio,
+                -volume_data.shape[2] // 2 * x_ratio,
             )
             self.gl_widget.addItem(v)
 
@@ -658,141 +628,24 @@ def update_range_roi(self):
 
     def restore_zero_plane(self):
         if hasattr(self, 'old_zero_plane'):
-            self.psf_data.zero_plane = self.old_zero_plane
+            self.psf_data.zero_plane = self.psf_data.old_zero_plane
             self.update_all_views()
 
-    def gaussian(self, x, a, x0, sigma, offset):
-        return a * np.exp(-((x - x0) ** 2) / (2 * sigma**2)) + offset
-
     def adjust_zero_plane(self):
         if self.psf_data is None:
             return
 
         method = self.zero_plane_method.currentText()
-        start, end = map(
-            int,
-            [
-                v / self.psf_data.z_step + self.psf_data.zero_plane
-                for v in self.range_roi.getRegion()
-            ],
-        )
-
-        # Store the old zero plane for potential restoration
-        self.old_zero_plane = self.psf_data.zero_plane
-
-        # Get the current statistic data
         selected_stat = self.stat_combo.currentText()
-        stat_data = self.get_statistic_data(selected_stat)
-
-        # Slice the data according to the specified range
-        x = np.arange(start, end)
-        y = stat_data[start:end]
-
-        if method == 'Peak':
-            # Find peaks
-            peaks, _ = find_peaks(y)
-            if len(peaks) > 0:
-                # Get peak prominences
-                prominences = peak_prominences(y, peaks)[0]
-                # Select the most prominent peak
-                max_peak = peaks[np.argmax(prominences)]
-                new_zero_plane = start + max_peak
-            else:
-                new_zero_plane = start + np.argmax(y)
-        elif method == 'Valley':
-            # Invert the data to find valleys as peaks
-            inverted_y = -y
-            valleys, _ = find_peaks(inverted_y)
-            if len(valleys) > 0:
-                # Get valley prominences
-                prominences = peak_prominences(inverted_y, valleys)[0]
-                # Select the most prominent valley
-                max_valley = valleys[np.argmax(prominences)]
-                new_zero_plane = start + max_valley
-            else:
-                new_zero_plane = start + np.argmin(y)
-        elif 'Gaussian Fit' in method:
-            if 'Inverted' in method:
-                y = -y
-            try:
-                # Initial guess for Gaussian parameters
-                a_init = np.max(y) - np.min(y)
-                x0_init = x[np.argmax(y)]
-                sigma_init = (end - start) / 4
-                offset_init = np.min(y)
-
-                # Perform Gaussian fit
-                popt, _ = curve_fit(
-                    self.gaussian, x, y, p0=[a_init, x0_init, sigma_init, offset_init]
-                )
-                new_zero_plane = int(popt[1])  # x0 is the center of the Gaussian
-            except Exception:
-                # Fallback to original plane if fitting fails
-                new_zero_plane = self.psf_data.zero_plane
-        else:
-            return  # Keep current zero plane for 'Manual' method
 
-        # Update the zero plane
-        self.psf_data.zero_plane = new_zero_plane
+        self.psf_data.adjust_zero_plane(
+            selected_stat, method, self.range_roi.getRegion()
+        )
 
         # Update the plots
         self.update_roi_statistics()
         self.update_intensity_statistics()
 
-    def get_statistic_data(self, selected_stat):
-        stat_data = []
-        header = PARAMETER_HEADERS[self.psf_data.fitting_method]
-
-        for z_slice in self.psf_data:
-            if z_slice['rois'] is not None and len(z_slice['rois']) > 0:
-                valid_rois = z_slice['rois'][
-                    ~np.isnan(z_slice['rois']).any(axis=(1, 2))
-                ]
-                if len(valid_rois) > 0:
-                    if selected_stat == 'Counts':
-                        stat_data.append(len(valid_rois))
-                    elif selected_stat == 'Sigma':
-                        if 'sigmax' in header and 'sigmay' in header:
-                            sigma_x = np.mean(
-                                z_slice['params'][:, header.index('sigmax')]
-                            )
-                            sigma_y = np.mean(
-                                z_slice['params'][:, header.index('sigmay')]
-                            )
-                            stat_data.append(np.mean([sigma_x, sigma_y]))
-                    elif selected_stat == 'Sigma (sum)':
-                        if 'sigmax' in header and 'sigmay' in header:
-                            sigma_x = np.sum(
-                                z_slice['params'][:, header.index('sigmax')]
-                            )
-                            sigma_y = np.sum(
-                                z_slice['params'][:, header.index('sigmay')]
-                            )
-                            stat_data.append(np.sum([sigma_x, sigma_y]))
-                    elif selected_stat == 'Sigma (diff)':
-                        if 'sigmax' in header and 'sigmay' in header:
-                            sigma_x = np.mean(
-                                z_slice['params'][:, header.index('sigmax')]
-                            )
-                            sigma_y = np.mean(
-                                z_slice['params'][:, header.index('sigmay')]
-                            )
-                            stat_data.append(abs(sigma_x - sigma_y))
-                    elif selected_stat == 'Intensity':
-                        stat_data.append(
-                            np.mean(z_slice['params'][:, header.index('intensity')])
-                        )
-                    elif selected_stat == 'Background':
-                        stat_data.append(
-                            np.mean(z_slice['params'][:, header.index('background')])
-                        )
-                else:
-                    stat_data.append(np.nan)
-            else:
-                stat_data.append(np.nan)
-
-        return np.array(stat_data)
-
     def update_roi_statistics(self):
         '''Update ROI statistics plot.'''
         if self.psf_data is None:
@@ -801,87 +654,18 @@ def update_roi_statistics(self):
         self.roi_plot.clear()
 
         selected_stat = self.stat_combo.currentText()
-        z_indices = (
-            np.arange(len(self.psf_data)) - self.psf_data.zero_plane
-        ) * self.psf_data.z_step
-        header = PARAMETER_HEADERS[self.psf_data.fitting_method]
-
-        mean_param = []
-        if selected_stat == 'Sigma':
-            if 'sigmax' in header:
-                mean_param.append([])
-            if 'sigmay' in header:
-                mean_param.append([])
-
-        for z_slice in self.psf_data:
-            if z_slice['rois'] is not None:
-                valid_rois = z_slice['rois'][
-                    ~np.isnan(z_slice['rois']).any(axis=(1, 2))
-                ]
-                if selected_stat == 'Counts':
-                    mean_param.append(len(valid_rois))
-                elif len(valid_rois) > 0:
-                    if selected_stat == 'Sigma':
-                        if 'sigmax' in header:
-                            mean_param[0].append(
-                                np.mean(z_slice['params'][:, header.index('sigmax')])
-                                * self.psf_data.pixel_size
-                            )
-                        if 'sigmay' in header:
-                            mean_param[1].append(
-                                np.mean(z_slice['params'][:, header.index('sigmay')])
-                                * self.psf_data.pixel_size
-                            )
-                    elif selected_stat == 'Sigma (sum)':
-                        sum_sigma = 0
-                        if 'sigmax' in header:
-                            sum_sigma += (
-                                np.mean(z_slice['params'][:, header.index('sigmax')])
-                                * self.psf_data.pixel_size
-                            )
-                        if 'sigmay' in header:
-                            sum_sigma += (
-                                np.mean(z_slice['params'][:, header.index('sigmay')])
-                                * self.psf_data.pixel_size
-                            )
-                        mean_param.append(sum_sigma)
-                    elif selected_stat == 'Sigma (diff)':
-                        sigma_x = (
-                            np.mean(z_slice['params'][:, header.index('sigmax')])
-                            * self.psf_data.pixel_size
-                        )
-                        sigma_y = (
-                            np.mean(z_slice['params'][:, header.index('sigmay')])
-                            * self.psf_data.pixel_size
-                        )
-                        mean_param.append(abs(sigma_x - sigma_y))
-                    elif selected_stat == 'Intensity':
-                        mean_param.append(
-                            np.mean(z_slice['params'][:, header.index('intensity')])
-                        )
-                    elif selected_stat == 'Background':
-                        mean_param.append(
-                            np.mean(z_slice['params'][:, header.index('background')])
-                        )
-                else:
-                    if selected_stat == 'Sigma':
-                        if 'sigmax' in header:
-                            mean_param[0].append(np.nan)
-                        if 'sigmay' in header:
-                            mean_param[1].append(np.nan)
-                    else:
-                        mean_param.append(np.nan)
-            else:
-                mean_param.append(np.nan)
+        z_indices, param_stat = self.psf_data.get_stats(selected_stat)
 
         # Plot selected statistic
         if selected_stat == 'Sigma':
-            if 'sigmax' in header:
-                self.roi_plot.plot(z_indices, mean_param[0], pen='b', name='Sigma X')
-            if 'sigmay' in header:
-                self.roi_plot.plot(z_indices, mean_param[1], pen='r', name='Sigma Y')
+            if 1 <= len(param_stat) <= 2:
+                self.roi_plot.plot(z_indices, param_stat[0], pen='b', name='Sigma X')
+                if len(param_stat) == 2:
+                    self.roi_plot.plot(
+                        z_indices, param_stat[1], pen='r', name='Sigma Y'
+                    )
         else:
-            self.roi_plot.plot(z_indices, mean_param, pen='b', name=selected_stat)
+            self.roi_plot.plot(z_indices, param_stat, pen='b', name=selected_stat)
 
         # Update zero plane line
         zero_line_roi = pg.InfiniteLine(pos=0, angle=90, pen='w', movable=False)
@@ -911,35 +695,12 @@ def update_intensity_statistics(self):
 
         self.intensity_plot.clear()
 
-        z_indices = (
-            np.arange(len(self.psf_data)) - self.psf_data.zero_plane
-        ) * self.psf_data.z_step
-        mean_intensities = []
-        median_intensities = []
-        std_intensities = []
-
-        for z_slice in self.psf_data:
-            if z_slice['rois'] is not None:
-                valid_rois = z_slice['rois'][
-                    ~np.isnan(z_slice['rois']).any(axis=(1, 2))
-                ]
-                if len(valid_rois) > 0:
-                    mean_intensities.append(np.mean(valid_rois))
-                    median_intensities.append(np.median(valid_rois))
-                    std_intensities.append(np.std(valid_rois))
-                else:
-                    mean_intensities.append(np.nan)
-                    median_intensities.append(np.nan)
-                    std_intensities.append(np.nan)
-            else:
-                mean_intensities.append(np.nan)
-                median_intensities.append(np.nan)
-                std_intensities.append(np.nan)
+        _indices, _mean, _median, _std = self.psf_data.get_intensity_stats()
 
         # Plot intensity statistics
-        self.intensity_plot.plot(z_indices, mean_intensities, pen='r', name='Mean')
-        self.intensity_plot.plot(z_indices, median_intensities, pen='g', name='Median')
-        self.intensity_plot.plot(z_indices, std_intensities, pen='y', name='Std Dev')
+        self.intensity_plot.plot(_indices, _mean, pen='r', name='Mean')
+        self.intensity_plot.plot(_indices, _median, pen='g', name='Median')
+        self.intensity_plot.plot(_indices, _std, pen='y', name='Std Dev')
 
         # Update zero plane line
         zero_line_intensity = pg.InfiniteLine(pos=0, angle=90, pen='w', movable=False)
@@ -962,6 +723,34 @@ def update_all_views(self, extra=True):
             self.update_roi_statistics()
             self.update_intensity_statistics()
 
+    def update_slice_controls(self):
+        if self.view_mode == 'XY':
+            self.slice_label.setText('Z-Slice:')
+            max_slice = len(self.psf_data) - 1
+
+            # set viewbox aspect ratio
+            ratio = 1
+            zero = self.psf_data.zero_plane
+        else:
+            if self.view_mode == 'XZ':
+                self.slice_label.setText('Y-Slice:')
+            else:  # YZ
+                self.slice_label.setText('X-Slice:')
+
+            max_slice = self.psf_data.roi_size - 1
+
+            # set viewbox aspect ratio
+            ratio = self.psf_data.get_ratio()
+            zero = max_slice // 2
+
+        # set viewbox aspect ratio
+        self.view_box.setAspectLocked(True, ratio)
+
+        self.slice_slider.setMaximum(max_slice)
+        self.slice_spinbox.setMaximum(max_slice)
+        self.zero_btn.setText(f'Zero Plane {zero}')
+        self.slice_slider.setValue(zero)
+
     def set_psf_data(self, psf_data: 'PSFdata'):
         '''Set new PSF data and update all views.
 
@@ -972,10 +761,7 @@ def set_psf_data(self, psf_data: 'PSFdata'):
         '''
         self.psf_data = psf_data
 
-        # Update Z-slice slider and spinbox ranges
-        max_z = len(psf_data) - 1
-        self.z_slice_slider.setMaximum(max_z)
-        self.z_slice_spinbox.setMaximum(max_z)
-
+        # Update slice controls
+        self.update_slice_controls()
         # Update all views
         self.update_all_views()
diff --git a/src/microEye/hardware/cams/micam.py b/src/microEye/hardware/cams/micam.py
index 88cd78e..83797d1 100644
--- a/src/microEye/hardware/cams/micam.py
+++ b/src/microEye/hardware/cams/micam.py
@@ -24,10 +24,18 @@ def __init__(self, Cam_ID=0) -> None:
     def height(self):
         return self._height
 
+    @height.setter
+    def height(self, value):
+        self._height = value
+
     @property
     def width(self):
         return self._width
 
+    @width.setter
+    def width(self, value):
+        self._width = value
+
     def get_temperature(self):
         return self.temperature
 
diff --git a/src/microEye/hardware/misc/reglo.py b/src/microEye/hardware/misc/reglo.py
index 07a0932..6a7f43a 100644
--- a/src/microEye/hardware/misc/reglo.py
+++ b/src/microEye/hardware/misc/reglo.py
@@ -594,13 +594,13 @@ def LayoutInit(self):
         self.config_Layout.addLayout(pause_time_layout)
 
         self.cycle_label = QtWidgets.QLabel('Number of Cycles: 1')
-        self.cycle_slider = QtWidgets.QSlider(Qt.Horizontal)
+        self.cycle_slider = QtWidgets.QSlider(Qt.Orientation.Horizontal)
         self.cycle_slider.setMinimum(2)
         self.cycle_slider.setMaximum(50)
         self.cycle_slider.valueChanged.connect(self.cycles_changed)
 
         self.inc_label = QtWidgets.QLabel('Speed Increment: 1')
-        self.inc_slider = QtWidgets.QSlider(Qt.Horizontal)
+        self.inc_slider = QtWidgets.QSlider(Qt.Orientation.Horizontal)
         self.inc_slider.setMinimum(1)
         self.inc_slider.setMaximum(50)
         self.inc_slider.valueChanged.connect(self.inc_changed)
diff --git a/src/microEye/hardware/port_config.py b/src/microEye/hardware/port_config.py
index 99732ce..f4b7805 100644
--- a/src/microEye/hardware/port_config.py
+++ b/src/microEye/hardware/port_config.py
@@ -30,7 +30,7 @@ def __init__(self, parent=None, title='Serial Port Config.', **kwargs):
 
         # dialog buttons
         buttonBox = QtWidgets.QDialogButtonBox()
-        buttonBox.setOrientation(Qt.Horizontal)
+        buttonBox.setOrientation(Qt.Orientation.Horizontal)
         buttonBox.setStandardButtons(
             QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok
         )