diff --git a/COPYING b/COPYING
index 7cd7b82385b43a76797fb2995e55da74a5b153eb..b242b98048e8f58273f4b4458f534129eededd92 100644
--- a/COPYING
+++ b/COPYING
@@ -1,4 +1,5 @@
 Copyright 2014-2016 Jim Rybarski, 2017 Ruben Verweij
+Copyright 2020-2021 Lorenzo Zolfanelli, ESPCI Paris - PSL
 
 nd2reader is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
diff --git a/nd2reader/parser.py b/nd2reader/parser.py
index 8534e7996fad76eec9c1e85efe55caefb0d3d4bf..bebbcaa0a731d243b253f9566177f50c0140b57e 100644
--- a/nd2reader/parser.py
+++ b/nd2reader/parser.py
@@ -78,6 +78,38 @@ class Parser(object):
         else:
             return Frame(image, frame_no=frame_number, metadata=self._get_frame_metadata())
 
+    def get_slice_by_attributes(self, xywh, frame_number, field_of_view, channel, z_level, height, width):
+        """Gets a rectangular slice of an image based on its attributes alone
+
+        Args:
+            xywh: tuples containing (x, y, w, h) values of the
+                  rectangular region to load
+            frame_number: the frame number
+            field_of_view: the field of view
+            channel_name: the color channel name
+            z_level: the z level
+            height: the height of the image
+            width: the width of the image
+
+        Returns:
+            Frame: the requested image
+
+        """
+        frame_number = 0 if frame_number is None else frame_number
+        field_of_view = 0 if field_of_view is None else field_of_view
+        channel = 0 if channel is None else channel
+        z_level = 0 if z_level is None else z_level
+
+        image_group_number = self._calculate_image_group_number(frame_number, field_of_view, z_level)
+        try:
+            timestamp, raw_image_data = self._get_raw_slice_data(
+                xywh, image_group_number, channel, height, width
+            )
+        except (TypeError):
+            return Frame([], frame_no=frame_number, metadata=self._get_frame_metadata())
+        else:
+            return Frame(raw_image_data, frame_no=frame_number, metadata=self._get_frame_metadata())
+
     def get_image_by_attributes(self, frame_number, field_of_view, channel, z_level, height, width):
         """Gets an image based on its attributes alone
 
@@ -246,6 +278,79 @@ class Parser(object):
         """
         return {channel: n for n, channel in enumerate(self.metadata["channels"])}
 
+    def _get_raw_slice_data(self, xywh, image_group_number, channel, height, width):
+        """Reads the raw bytes and the timestamp of a rectangular slice
+        of an image.
+
+        Args:
+            xywh: tuples containing (x, y, w, h) values of the
+                  rectangular region to load
+            image_group_number: the image group number (see _calculate_image_group_number)
+            channel: the position (int) of the channel to load
+            height: the height of the image
+            width: the width of the image
+
+        Returns:
+
+        """
+        size_c = len(self.metadata["channels"])
+
+        x0, y0, w, h = xywh
+        chunk_location = self._label_map.get_image_data_location(image_group_number)
+        fh = self._fh
+
+        if chunk_location is None or fh is None:
+            return None
+        fh.seek(chunk_location)
+        # The chunk metadata is always 16 bytes long
+        chunk_metadata = fh.read(16)
+        header, relative_offset, data_length = struct.unpack("IIQ", chunk_metadata)
+        if header != 0xabeceda:
+            raise ValueError("The ND2 file seems to be corrupted.")
+        # We start at the location of the chunk metadata, skip over the metadata, and then proceed to the
+        # start of the actual data field, which is at some arbitrary place after the metadata.
+        fh.seek(chunk_location + 16 + relative_offset)
+
+        # Read timestamp (8 bytes)
+        timestamp = struct.unpack("d", fh.read(8))[0]
+
+        # Stitched Images: evaluate number of bytes to strip
+        # (with stitched images sometimes after each row we have a regular number of extra bytes)
+        n_unwanted_bytes = (data_length-8) % (height*width)
+        assert 0 == n_unwanted_bytes % height
+        rowskip = n_unwanted_bytes // height
+
+        # Read ROI: row-by-row
+        image_start_pos = chunk_location + 16 + relative_offset + 8
+
+        line_bytemask = np.zeros(size_c, dtype=np.bool)
+        line_bytemask[channel] = True
+        line_bytemask = np.tile(line_bytemask.repeat(2),w)
+
+        def get_line(y):
+            fh.seek(image_start_pos + size_c*2*((width)*y+x0) + y*rowskip)
+            return np.frombuffer(fh.read(size_c*2*w), np.byte)[line_bytemask]
+
+        data = [get_line(y) for y in range(y0, y0+h)]
+        data = bytes().join(data)
+
+        image_group_data = array.array("H", data)
+        true_channels_no = int(len(image_group_data) / (h * w))
+
+        image_data = np.reshape(image_group_data, (h, w, true_channels_no))
+
+        missing_channels = ~np.any(image_data, axis=(0, 1))
+        image_data[..., missing_channels] = np.full(
+            (h, w, missing_channels.sum()), np.nan)
+
+        if np.any(missing_channels):
+            warnings.warn(
+                "ND2 file contains gap frames which are represented by "
+                + "np.nan-filled arrays; to convert to zeros use e.g. "
+                + "np.nan_to_num(array)")
+        return timestamp, image_data[...,0]
+
+
     def _get_raw_image_data(self, image_group_number, channel_offset, height, width):
         """Reads the raw bytes and the timestamp of an image.
 
diff --git a/nd2reader/raw_metadata.py b/nd2reader/raw_metadata.py
index 11522196f4026a56605956251bab60d12994f423..3c25d576cfcd9d9c393e3ab2886c9a86ccf918e2 100644
--- a/nd2reader/raw_metadata.py
+++ b/nd2reader/raw_metadata.py
@@ -45,9 +45,13 @@ class RawMetadata(object):
             "frames": self._parse_frames(),
             "z_levels": self._parse_z_levels(),
             "z_coordinates": parse_if_not_none(self.z_data, self._parse_z_coordinates),
+            "x_coordinates": parse_if_not_none(self.x_data, self._parse_x_coordinates),
+            "y_coordinates": parse_if_not_none(self.y_data, self._parse_y_coordinates),
             "total_images_per_channel": frames_per_channel,
             "channels": self._parse_channels(),
-            "pixel_microns": parse_if_not_none(self.image_calibration, self._parse_calibration)
+            "pixel_microns": parse_if_not_none(self.image_calibration, self._parse_calibration),
+            "camera_stage_angle": parse_if_not_none(self.image_metadata_sequence, self._parse_camera_angle),
+            "camera_stage_matrix": parse_if_not_none(self.image_metadata_sequence, self._parse_camera_matrix)
         }
 
         self._set_default_if_not_empty('fields_of_view')
@@ -195,6 +199,62 @@ class RawMetadata(object):
         """
         return self.z_data.tolist()
 
+    def _parse_x_coordinates(self):
+        """The coordinate in micron for all x frames.
+
+        Returns:
+            list: the x coordinates in micron
+        """
+        return self.x_data.tolist()
+
+    def _parse_y_coordinates(self):
+        """The coordinate in micron for all y frames.
+
+        Returns:
+            list: the y coordinates in micron
+        """
+        return self.y_data.tolist()
+
+    def _parse_camera_angle(self):
+        if self.image_metadata_sequence is None:
+            return []
+
+        try:
+            metadata = self.image_metadata_sequence[six.b('SLxPictureMetadata')]
+        except KeyError:
+            return []
+
+        try:
+            return metadata[b'dAngle']
+        except KeyError:
+            return None
+
+    def _parse_camera_matrix(self):
+        if self.image_metadata_sequence is None:
+            return []
+
+        try:
+            metadata = self.image_metadata_sequence[six.b('SLxPictureMetadata')][b'sPicturePlanes']
+        except KeyError:
+            return []
+
+        validity = self._get_channel_validity_list(metadata)
+
+        channels = []
+        for valid, (label, chan) in zip(validity, sorted(metadata[b'sSampleSetting'].items())):
+            if not valid:
+                continue
+            if chan[b'matCameraToStage'] is not None:
+                mat_data = chan[b'matCameraToStage'][b'Data']
+                mat_rows = chan[b'matCameraToStage'][b'Rows']
+                mat_columns = chan[b'matCameraToStage'][b'Columns']
+                mat = np.frombuffer(mat_data, dtype=np.float64).reshape([mat_rows, mat_columns])
+                channels.append(mat)
+            else:
+                channels.append(None)
+        return channels
+
+
     def _parse_dimension_text(self):
         """While there are metadata values that represent a lot of what we want to capture, they seem to be unreliable.
         Sometimes certain elements don't exist, or change their data type randomly. However, the human-readable text
diff --git a/nd2reader/reader.py b/nd2reader/reader.py
index 4e14f42e3cd37f39f59ffb9dd5624c1a32287c82..295bded08427ac578f4bd0197fac6c52bd892464 100644
--- a/nd2reader/reader.py
+++ b/nd2reader/reader.py
@@ -69,6 +69,21 @@ class ND2Reader(FramesSequenceND):
         except KeyError:
             return 0
 
+    def get_roi(self, roi, c=0, t=0, z=0, x=0, y=0, v=0):
+        height = self.metadata['height']
+        width = self.metadata['width']
+        ylim = roi[0].indices(height)
+        xlim = roi[1].indices(width)
+
+        y = ylim[0]
+        x = xlim[0]
+        w = xlim[1]-xlim[0]
+        h = ylim[1]-ylim[0]
+
+        return self._parser.get_slice_by_attributes(
+            (x, y, w, h), t, v, c, z, height, width
+        )
+
     def get_frame_2D(self, c=0, t=0, z=0, x=0, y=0, v=0):
         """Gets a given frame using the parser
         Args:
diff --git a/setup.py b/setup.py
index c5dde1c857064f2449080fe28ca756d603fda5f6..5b6ebdd4cca15cf3810e29f26b6ffda85e834c59 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,5 @@
 from setuptools import setup
-
-VERSION = '3.3.0'
+#from nd2reader import __version__ as VERSION
 
 if __name__ == '__main__':
     setup(
@@ -10,15 +9,15 @@ if __name__ == '__main__':
             'numpy>=1.14',
             'six>=1.4',
             'xmltodict>=0.9.2',
-            'pims>=0.3.0'
+            'PIMS>=0.5.0'
         ],
         python_requires=">=3.6",
-        version=VERSION,
+        version="3.2.3-zolfa-dev0",
         description='A tool for reading ND2 files produced by NIS Elements',
         author='Ruben Verweij',
         author_email='ruben@lighthacking.nl',
         url='https://github.com/rbnvrw/nd2reader',
-        download_url='https://github.com/rbnvrw/nd2reader/tarball/%s' % VERSION,
+        download_url='https://github.com/rbnvrw/nd2reader/tarball/%s' % "3.2.3-zolfa-dev0",
         keywords=['nd2', 'nikon', 'microscopy', 'NIS Elements'],
         classifiers=['Development Status :: 5 - Production/Stable',
                      'Intended Audience :: Science/Research',