def test_in_bounds_zero_crop_area(self) -> None: """ Test that crop is not ``in_bounds`` when it has zero area (undefined). """ # noinspection PyArgumentList bb = AxisAlignedBoundingBox([1, 2], [1, 2]) assert not crop_in_bounds(bb, 4, 6)
def test_in_bounds_completely_outside(self) -> None: """ Test that being completely outside the given bounds causes ``in_bounds`` to return False. """ bb = AxisAlignedBoundingBox([100, 100], [102, 102]) assert not crop_in_bounds(bb, 4, 6) bb = AxisAlignedBoundingBox([-100, -100], [-98, -98]) assert not crop_in_bounds(bb, 4, 6) bb = AxisAlignedBoundingBox([-100, 100], [-98, 102]) assert not crop_in_bounds(bb, 4, 6) bb = AxisAlignedBoundingBox([100, -100], [102, -98]) assert not crop_in_bounds(bb, 4, 6)
def _load_as_matrix( self, data_element: DataElement, pixel_crop: Optional[AxisAlignedBoundingBox] = None) \ -> numpy.ndarray: """ Internal method to be implemented that attempts loading an image from the given data element and returning it as an image matrix. Pre-conditions: - ``pixel_crop`` has a non-zero volume and is composed of integer types. :param smqtk.representation.DataElement data_element: DataElement to load image data from. :param None|smqtk.representation.AxisAlignedBoundingBox pixel_crop: Optional pixel crop region to load from the given data. If this is provided it must represent a valid sub-region within the loaded image, otherwise a RuntimeError is raised. :raises RuntimeError: A crop region was specified but did not specify a valid sub-region of the image. :return: Numpy ndarray of the image data. Specific return format is implementation dependant. :rtype: numpy.ndarray """ # We may have to add a mode where we use write_temp and load from that # if loading large images straight from bytes-in-memory is a problem # and that approach actually alleviates anything. # Catch and raise alternate IOError exception for readability. try: #: :type: PIL.Image.Image img = PIL.Image.open(BytesIO(data_element.get_bytes())) except IOError as ex: ex_str = str(ex) if 'cannot identify image file' in ex_str: raise IOError("Failed to identify image from bytes provided " "by {}".format(data_element)) else: # pass through other exceptions raise if pixel_crop: if not crop_in_bounds(pixel_crop, *img.size): raise RuntimeError("Crop provided not within input image. " "Image shape: {}, crop: {}".format( img.size, pixel_crop)) img = img.crop(pixel_crop.min_vertex.tolist() + pixel_crop.max_vertex.tolist()) # If the loaded image is not already the optionally provided # explicit mode, convert it. if self._explicit_mode and img.mode != self._explicit_mode: img = img.convert(mode=self._explicit_mode) # noinspection PyTypeChecker return numpy.asarray(img)
def test_in_bounds_inside_edges(self) -> None: """ Test that a crop is "in bounds" when contacting the 4 edges of the given rectangular bounds. +--+ | | ## | => (4, 6) image, (2,2) crop ## | | | +--+ +##+ |##| | | => (4, 6) image, (2,2) crop | | | | +--+ +--+ | | | ## => (4, 6) image, (2,2) crop | ## | | +--+ +--+ | | | | => (4, 6) image, (2,2) crop | | |##| +##+ """ # noinspection PyArgumentList bb = AxisAlignedBoundingBox([0, 2], [2, 4]) assert crop_in_bounds(bb, 4, 6) bb = AxisAlignedBoundingBox([1, 0], [3, 2]) assert crop_in_bounds(bb, 4, 6) bb = AxisAlignedBoundingBox([2, 2], [4, 4]) assert crop_in_bounds(bb, 4, 6) bb = AxisAlignedBoundingBox([1, 4], [3, 6]) assert crop_in_bounds(bb, 4, 6)
def test_in_bounds_crossing_edges(self) -> None: """ Test that ``in_bounds`` returns False when crop crossed the 4 edges. +--+ | | ### | => (4, 6) image, (3,2) crop ### | | | +--+ +--+ | | | ### => (4, 6) image, (3,2) crop | ### | | +--+ ## +##+ |##| | | => (4, 6) image, (2,3) crop | | | | +--+ +--+ | | | | => (4, 6) image, (2,3) crop | | |##| +##+ ## """ bb = AxisAlignedBoundingBox([-1, 2], [2, 4]) assert not crop_in_bounds(bb, 4, 6) bb = AxisAlignedBoundingBox([2, 2], [5, 4]) assert not crop_in_bounds(bb, 4, 6) bb = AxisAlignedBoundingBox([1, -1], [3, 2]) assert not crop_in_bounds(bb, 4, 6) bb = AxisAlignedBoundingBox([1, 4], [3, 7]) assert not crop_in_bounds(bb, 4, 6)
def test_in_bounds_inside(self) -> None: """ Test that ``in_bounds`` passes when crop inside given rectangle bounds. +--+ | | |##| => (4, 6) image, (2,2) crop |##| | | +--+ """ bb = AxisAlignedBoundingBox([1, 2], [3, 4]) assert crop_in_bounds(bb, 4, 8)
def _load_as_matrix( self, data_element: DataElement, pixel_crop: AxisAlignedBoundingBox = None) -> np.ndarray: """ Internal method to be implemented that attempts loading an image from the given data element and returning it as an image matrix. Pre-conditions: - ``pixel_crop`` has a non-zero volume and is composed of integer types. :param smqtk.representation.DataElement data_element: DataElement to load image data from. :param None|smqtk.representation.AxisAlignedBoundingBox pixel_crop: Optional pixel crop region to load from the given data. If this is provided it must represent a valid sub-region within the loaded image, otherwise a RuntimeError is raised. :raises RuntimeError: A crop region was specified but did not specify a valid sub-region of the image. :return: Numpy ndarray of the image data. Specific return format is implementation dependant. :rtype: np.ndarray """ if data_element.is_empty(): raise ValueError( "GdalImageReader cannot load 0-sized data (no bytes in {}).". format(data_element)) load_cm = self.LOAD_METHOD_CONTEXTMANAGERS[self._load_method] with load_cm(data_element) as gdal_ds: # type: gdal.Dataset img_width = gdal_ds.RasterXSize img_height = gdal_ds.RasterYSize # GDAL wants [x, y, width, height] as the first 4 positional # arguments to ``ReadAsArray``. xywh = [0, 0, img_width, img_height] if pixel_crop: if not crop_in_bounds(pixel_crop, img_width, img_height): raise RuntimeError("Crop provided not within input image. " "Image shape: {}, crop: {}".format( (img_width, img_height), pixel_crop)) # This is testing faster than ``np.concatenate``. xywh = \ pixel_crop.min_vertex.tolist() + pixel_crop.deltas.tolist() # Select specific channels if they are present in this dataset, or # just get all of them if self._channel_order is not None: assert self._channel_order_gci is not None, ( "When a channel-order is set, the GCI equivalent should " "also be set.") # Map raster bands from CI value to band index. # - GDAL uses 1-based indexing. band_ci_to_idx = { gdal_ds.GetRasterBand(b_i).GetColorInterpretation(): b_i for b_i in range(1, gdal_ds.RasterCount + 1) } gci_diff = (set( self._channel_order_gci).difference(band_ci_to_idx)) if gci_diff: raise RuntimeError( "Data element did not provide channels required to " "satisfy requested channel order {}. " "Data had bands: {} (missing {}).".format( map_gci_list_to_names(self._channel_order_gci), map_gci_list_to_names(band_ci_to_idx), map_gci_list_to_names(gci_diff))) # Initialize a matrix to read band image data into # TODO: Handle when there are no bands? band_dtype = gdal_array.GDALTypeCodeToNumericTypeCode( gdal_ds.GetRasterBand(1).DataType) if len(self._channel_order_gci) > 1: img_mat = np.ndarray( [xywh[3], xywh[2], len(self._channel_order_gci)], dtype=band_dtype) for i, gci in enumerate(self._channel_order_gci): #: :type: gdal.Band b = gdal_ds.GetRasterBand(band_ci_to_idx[gci]) b.ReadAsArray(*xywh, buf_obj=img_mat[:, :, i]) else: img_mat = np.ndarray([xywh[3], xywh[2]], dtype=band_dtype) gci = self._channel_order_gci[0] b = gdal_ds.GetRasterBand(band_ci_to_idx[gci]) b.ReadAsArray(*xywh, buf_obj=img_mat) else: img_mat = gdal_ds.ReadAsArray(*xywh) if img_mat.ndim > 2: # Transpose into [height, width, channel] format. img_mat = img_mat.transpose(1, 2, 0) return img_mat