From 78840539386240e331765e24e0af102332309e28 Mon Sep 17 00:00:00 2001 From: Przemyslaw Aszkowski Date: Mon, 18 Sep 2023 22:51:52 +0200 Subject: [PATCH 01/16] Meaningful error messages --- src/deepness/processing/models/detector.py | 6 +++--- src/deepness/processing/models/regressor.py | 2 +- src/deepness/processing/models/segmentor.py | 2 +- src/deepness/processing/models/superresolution.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/deepness/processing/models/detector.py b/src/deepness/processing/models/detector.py index c8ae923..776bb79 100644 --- a/src/deepness/processing/models/detector.py +++ b/src/deepness/processing/models/detector.py @@ -133,7 +133,7 @@ def get_number_of_output_channels(self): return self.outputs_layers[0].shape[shape_index] - 4 return self.outputs_layers[0].shape[shape_index] - 4 - 1 # shape - 4 bboxes - 1 conf else: - return NotImplementedError + raise NotImplementedError("Model with multiple output layer is not supported! Use only one output layer.") def preprocessing(self, image: np.ndarray): """Preprocess image before inference @@ -191,7 +191,7 @@ def postprocessing(self, model_output): elif self.model_type == DetectorType.YOLO_ULTRALYTICS: boxes, conf, classes = self._postprocessing_YOLO_ULTRALYTICS(model_output) else: - raise NotImplementedError + raise NotImplementedError(f"Model type not implemented! ('{self.model_type}')") detections = [] @@ -405,4 +405,4 @@ def check_loaded_model_outputs(self): ) else: - raise NotImplementedError + raise NotImplementedError("Model with multiple output layer is not supported! Use only one output layer.") diff --git a/src/deepness/processing/models/regressor.py b/src/deepness/processing/models/regressor.py index e536fd5..4352246 100644 --- a/src/deepness/processing/models/regressor.py +++ b/src/deepness/processing/models/regressor.py @@ -108,4 +108,4 @@ def check_loaded_model_outputs(self): raise Exception(f'Regression model can handle only square outputs masks. Has: {shape}') else: - raise NotImplementedError + raise NotImplementedError("Model with multiple output layer is not supported! Use only one output layer.") diff --git a/src/deepness/processing/models/segmentor.py b/src/deepness/processing/models/segmentor.py index 4a81255..f1dbf27 100644 --- a/src/deepness/processing/models/segmentor.py +++ b/src/deepness/processing/models/segmentor.py @@ -110,4 +110,4 @@ def check_loaded_model_outputs(self): raise Exception(f'Segmentation model can handle only square outputs masks. Has: {shape}') else: - raise NotImplementedError + raise NotImplementedError("Model with multiple output layer is not supported! Use only one output layer.") diff --git a/src/deepness/processing/models/superresolution.py b/src/deepness/processing/models/superresolution.py index 87a4fe0..766dd88 100644 --- a/src/deepness/processing/models/superresolution.py +++ b/src/deepness/processing/models/superresolution.py @@ -120,4 +120,4 @@ def check_loaded_model_outputs(self): raise Exception(f'Regression model can handle only square outputs masks. Has: {shape}') else: - raise NotImplementedError + raise NotImplementedError("Model with multiple output layer is not supported! Use only one output layer.") From 030006562505fabdeac0b3e364eaa3a23baa8aff Mon Sep 17 00:00:00 2001 From: Przemyslaw Aszkowski Date: Mon, 18 Sep 2023 23:12:14 +0200 Subject: [PATCH 02/16] Error messages improved even more --- src/deepness/common/channels_mapping.py | 8 ++++---- src/deepness/deepness.py | 2 ++ src/deepness/deepness_dockwidget.py | 2 ++ src/deepness/processing/map_processor/map_processor.py | 2 +- src/deepness/processing/models/model_base.py | 8 ++++---- src/deepness/processing/models/regressor.py | 4 ++-- src/deepness/processing/models/segmentor.py | 4 ++-- src/deepness/processing/models/superresolution.py | 6 +++--- 8 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/deepness/common/channels_mapping.py b/src/deepness/common/channels_mapping.py index 82b54a6..5cf5e73 100644 --- a/src/deepness/common/channels_mapping.py +++ b/src/deepness/common/channels_mapping.py @@ -18,10 +18,10 @@ def __init__(self, name): self.name = name def get_band_number(self): - raise NotImplementedError + raise NotImplementedError('Base class not implemented!') def get_byte_number(self): - raise NotImplementedError + raise NotImplementedError('Base class not implemented!') class ImageChannelStandaloneBand(ImageChannel): @@ -43,7 +43,7 @@ def get_band_number(self): return self.band_number def get_byte_number(self): - raise NotImplementedError + raise NotImplementedError('Something went wrong if we are here!') class ImageChannelCompositeByte(ImageChannel): @@ -62,7 +62,7 @@ def __str__(self): return txt def get_band_number(self): - raise NotImplementedError + raise NotImplementedError('Something went wrong if we are here!') def get_byte_number(self): return self.byte_number diff --git a/src/deepness/deepness.py b/src/deepness/deepness.py index 821d667..4c3b46f 100644 --- a/src/deepness/deepness.py +++ b/src/deepness/deepness.py @@ -5,6 +5,7 @@ Skeleton of this file was generate with the QGis plugin to create plugin skeleton - QGIS PluginBuilder """ +import logging import traceback from qgis.PyQt.QtCore import QCoreApplication, Qt @@ -292,6 +293,7 @@ def _map_processor_finished(self, result: MapProcessingResult): msg = f'Error! Processing error: "{result.message}"!' self.iface.messageBar().pushMessage(PLUGIN_NAME, msg, level=Qgis.Critical, duration=14) if result.exception is not None: + logging.error(msg) trace = '\n'.join(traceback.format_tb(result.exception.__traceback__)[-1:]) msg = f'{msg}\n\n\n' \ f'Details: ' \ diff --git a/src/deepness/deepness_dockwidget.py b/src/deepness/deepness_dockwidget.py index 1353c72..9185e75 100644 --- a/src/deepness/deepness_dockwidget.py +++ b/src/deepness/deepness_dockwidget.py @@ -508,6 +508,7 @@ def _run_inference(self): except OperationFailedException as e: msg = str(e) self.iface.messageBar().pushMessage(PLUGIN_NAME, msg, level=Qgis.Warning, duration=7) + logging.exception(msg) QMessageBox.critical(self, "Error!", msg) return @@ -531,6 +532,7 @@ def _run_training_data_export(self): except OperationFailedException as e: msg = str(e) self.iface.messageBar().pushMessage(PLUGIN_NAME, msg, level=Qgis.Warning) + logging.exception(msg) QMessageBox.critical(self, "Error!", msg) return diff --git a/src/deepness/processing/map_processor/map_processor.py b/src/deepness/processing/map_processor/map_processor.py index 20588b4..4a17aff 100644 --- a/src/deepness/processing/map_processor/map_processor.py +++ b/src/deepness/processing/map_processor/map_processor.py @@ -129,7 +129,7 @@ def run(self): return True def _run(self) -> MapProcessingResult: - return NotImplementedError + raise NotImplementedError('Base class not implemented!') def finished(self, result: bool): if not result: diff --git a/src/deepness/processing/models/model_base.py b/src/deepness/processing/models/model_base.py index 35a9b46..a244e6d 100644 --- a/src/deepness/processing/models/model_base.py +++ b/src/deepness/processing/models/model_base.py @@ -341,7 +341,7 @@ def preprocessing(self, img: np.ndarray) -> np.ndarray: np.ndarray Preprocessed image """ - return NotImplementedError + raise NotImplementedError('Base class not implemented!') def postprocessing(self, outs: List) -> np.ndarray: """ Abstract method for postprocessing @@ -356,7 +356,7 @@ def postprocessing(self, outs: List) -> np.ndarray: np.ndarray Postprocessed output """ - return NotImplementedError + raise NotImplementedError('Base class not implemented!') def get_number_of_output_channels(self) -> int: """ Abstract method for getting number of classes in the output layer @@ -365,10 +365,10 @@ def get_number_of_output_channels(self) -> int: ------- int Number of channels in the output layer""" - return NotImplementedError + raise NotImplementedError('Base class not implemented!') def check_loaded_model_outputs(self): """ Abstract method for checking if the model outputs are valid """ - return NotImplementedError + raise NotImplementedError('Base class not implemented!') diff --git a/src/deepness/processing/models/regressor.py b/src/deepness/processing/models/regressor.py index 4352246..5a870d9 100644 --- a/src/deepness/processing/models/regressor.py +++ b/src/deepness/processing/models/regressor.py @@ -72,7 +72,7 @@ def get_number_of_output_channels(self) -> int: if len(self.outputs_layers) == 1: return self.outputs_layers[0].shape[-3] else: - return NotImplementedError + raise NotImplementedError("Model with multiple output layers is not supported! Use only one output layer.") @classmethod def get_class_display_name(cls) -> str: @@ -108,4 +108,4 @@ def check_loaded_model_outputs(self): raise Exception(f'Regression model can handle only square outputs masks. Has: {shape}') else: - raise NotImplementedError("Model with multiple output layer is not supported! Use only one output layer.") + raise NotImplementedError("Model with multiple output layers is not supported! Use only one output layer.") diff --git a/src/deepness/processing/models/segmentor.py b/src/deepness/processing/models/segmentor.py index f1dbf27..e66f2a1 100644 --- a/src/deepness/processing/models/segmentor.py +++ b/src/deepness/processing/models/segmentor.py @@ -74,7 +74,7 @@ def get_number_of_output_channels(self): if len(self.outputs_layers) == 1: return self.outputs_layers[0].shape[-3] else: - return NotImplementedError + raise NotImplementedError("Model with multiple output layers is not supported! Use only one output layer.") @classmethod def get_class_display_name(cls): @@ -110,4 +110,4 @@ def check_loaded_model_outputs(self): raise Exception(f'Segmentation model can handle only square outputs masks. Has: {shape}') else: - raise NotImplementedError("Model with multiple output layer is not supported! Use only one output layer.") + raise NotImplementedError("Model with multiple output layers is not supported! Use only one output layer.") diff --git a/src/deepness/processing/models/superresolution.py b/src/deepness/processing/models/superresolution.py index 766dd88..d0724c6 100644 --- a/src/deepness/processing/models/superresolution.py +++ b/src/deepness/processing/models/superresolution.py @@ -71,7 +71,7 @@ def get_number_of_output_channels(self) -> int: if len(self.outputs_layers) == 1: return self.outputs_layers[0].shape[-3] else: - return NotImplementedError + raise NotImplementedError("Model with multiple output layers is not supported! Use only one output layer.") @classmethod def get_class_display_name(cls) -> str: @@ -95,7 +95,7 @@ def get_output_shape(self) -> List[int]: if len(self.outputs_layers) == 1: return self.outputs_layers[0].shape else: - return NotImplementedError + raise NotImplementedError("Model with multiple output layers is not supported! Use only one output layer.") def check_loaded_model_outputs(self): """ Check if the model has correct output layers @@ -120,4 +120,4 @@ def check_loaded_model_outputs(self): raise Exception(f'Regression model can handle only square outputs masks. Has: {shape}') else: - raise NotImplementedError("Model with multiple output layer is not supported! Use only one output layer.") + raise NotImplementedError("Model with multiple output layers is not supported! Use only one output layer.") From 3bd6606d722ba885f2295149ab597ce27f3cf42d Mon Sep 17 00:00:00 2001 From: Przemyslaw Aszkowski Date: Mon, 18 Sep 2023 23:17:45 +0200 Subject: [PATCH 03/16] readthedocs build fix? And version update --- .readthedocs.yaml | 1 - src/deepness/metadata.txt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index c22646d..eb3ffca 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -19,4 +19,3 @@ sphinx: python: install: - requirements: requirements_development.txt - system_packages: true diff --git a/src/deepness/metadata.txt b/src/deepness/metadata.txt index f63a83d..2a33568 100644 --- a/src/deepness/metadata.txt +++ b/src/deepness/metadata.txt @@ -6,7 +6,7 @@ name=Deepness: Deep Neural Remote Sensing qgisMinimumVersion=3.22 description=Inference of deep neural network models (ONNX) for segmentation, detection and regression -version=0.5.3 +version=0.5.4 author=PUT Vision email=przemyslaw.aszkowski@gmail.com From 31a9ac21601e36298a3e98743a6bd63c06c33de0 Mon Sep 17 00:00:00 2001 From: Bartosz Date: Fri, 22 Sep 2023 11:15:07 +0200 Subject: [PATCH 04/16] Handle seg models with smaller output mask --- src/deepness/processing/tile_params.py | 33 ++++++--- ...mentation_model_different_output_size.onnx | Bin 0 -> 20548 bytes ...ssor_segmentation_different_output_size.py | 66 ++++++++++++++++++ test/test_utils.py | 14 ++-- 4 files changed, 99 insertions(+), 14 deletions(-) create mode 100644 test/data/dummy_model/dummy_segmentation_model_different_output_size.onnx create mode 100644 test/test_map_processor_segmentation_different_output_size.py diff --git a/src/deepness/processing/tile_params.py b/src/deepness/processing/tile_params.py index 9251b84..04fecd1 100644 --- a/src/deepness/processing/tile_params.py +++ b/src/deepness/processing/tile_params.py @@ -11,7 +11,6 @@ from deepness.common.lazy_package_loader import LazyPackageLoader from deepness.common.processing_parameters.map_processing_parameters import MapProcessingParameters - cv2 = LazyPackageLoader('cv2') @@ -87,12 +86,14 @@ def get_slice_on_full_image_for_entire_tile(self) -> Tuple[slice, slice]: roi_slice = np.s_[y_min:y_max + 1, x_min:x_max + 1] return roi_slice - def get_slice_on_full_image_for_copying(self): + def get_slice_on_full_image_for_copying(self, tile_offset: int = 0): """ As we are doing processing with overlap, we are not going to copy the entire tile result to final image, but only the part that is not overlapping with the neighbouring tiles. Edge tiles have special handling too. + :param tile_offset: how many pixels to cut from the tile result (to remove the padding) + :return Slice to be used on the full image """ half_overlap = (self.params.tile_size_px - self.stride_px) // 2 @@ -101,7 +102,7 @@ def get_slice_on_full_image_for_copying(self): x_min = self.start_pixel_x + half_overlap x_max = self.start_pixel_x + self.params.tile_size_px - half_overlap - 1 y_min = self.start_pixel_y + half_overlap - y_max = self.start_pixel_y + self.params.tile_size_px - half_overlap - 1 + y_max = self.start_pixel_y + self.params.tile_size_px - half_overlap - 1 # edge tiles handling if self.x_bin_number == 0: @@ -113,20 +114,25 @@ def get_slice_on_full_image_for_copying(self): if self.y_bin_number == self.y_bins_number-1: y_max += half_overlap + x_min += tile_offset + x_max -= tile_offset + y_min += tile_offset + y_max -= tile_offset + roi_slice = np.s_[y_min:y_max + 1, x_min:x_max + 1] return roi_slice - def get_slice_on_tile_image_for_copying(self, roi_slice_on_full_image=None): + def get_slice_on_tile_image_for_copying(self, roi_slice_on_full_image=None, tile_offset: int = 0): """ Similar to _get_slice_on_full_image_for_copying, but ROI is a slice on the tile """ if not roi_slice_on_full_image: - roi_slice_on_full_image = self.get_slice_on_full_image_for_copying() + roi_slice_on_full_image = self.get_slice_on_full_image_for_copying(tile_offset=tile_offset) r = roi_slice_on_full_image roi_slice_on_tile = np.s_[ - r[0].start - self.start_pixel_y:r[0].stop - self.start_pixel_y, - r[1].start - self.start_pixel_x:r[1].stop - self.start_pixel_x + r[0].start - self.start_pixel_y - tile_offset:r[0].stop - self.start_pixel_y - tile_offset, + r[1].start - self.start_pixel_x - tile_offset:r[1].stop - self.start_pixel_x - tile_offset ] return roi_slice_on_tile @@ -147,8 +153,17 @@ def is_tile_within_mask(self, mask_img: Optional[np.ndarray]): return coverage_percentage > 0 # TODO - for training we can use tiles with higher coverage only def set_mask_on_full_img(self, full_result_img, tile_result): - roi_slice_on_full_image = self.get_slice_on_full_image_for_copying() - roi_slice_on_tile_image = self.get_slice_on_tile_image_for_copying(roi_slice_on_full_image) + if tile_result.shape[0] != self.params.tile_size_px or tile_result.shape[1] != self.params.tile_size_px: + tile_offset = (self.params.tile_size_px - tile_result.shape[0])//2 + + assert tile_offset % 2 == 0, "Model output shape is not even, cannot calculate offset" + assert tile_offset >= 0, "Model output shape is bigger than tile size, cannot calculate offset" + else: + tile_offset = 0 + + roi_slice_on_full_image = self.get_slice_on_full_image_for_copying(tile_offset=tile_offset) + roi_slice_on_tile_image = self.get_slice_on_tile_image_for_copying(roi_slice_on_full_image, tile_offset=tile_offset) + full_result_img[roi_slice_on_full_image] = tile_result[roi_slice_on_tile_image] def get_entire_tile_from_full_img(self, full_result_img) -> np.ndarray: diff --git a/test/data/dummy_model/dummy_segmentation_model_different_output_size.onnx b/test/data/dummy_model/dummy_segmentation_model_different_output_size.onnx new file mode 100644 index 0000000000000000000000000000000000000000..8e268cbf3345da66fa0a2dc55cd3a94ff57b1b81 GIT binary patch literal 20548 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 Date: Fri, 22 Sep 2023 11:36:53 +0200 Subject: [PATCH 05/16] Change asserts to Exceptions --- src/deepness/processing/tile_params.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/deepness/processing/tile_params.py b/src/deepness/processing/tile_params.py index 04fecd1..0cf1ce6 100644 --- a/src/deepness/processing/tile_params.py +++ b/src/deepness/processing/tile_params.py @@ -156,8 +156,11 @@ def set_mask_on_full_img(self, full_result_img, tile_result): if tile_result.shape[0] != self.params.tile_size_px or tile_result.shape[1] != self.params.tile_size_px: tile_offset = (self.params.tile_size_px - tile_result.shape[0])//2 - assert tile_offset % 2 == 0, "Model output shape is not even, cannot calculate offset" - assert tile_offset >= 0, "Model output shape is bigger than tile size, cannot calculate offset" + if tile_offset % 2 != 0: + raise Exception("Model output shape is not even, cannot calculate offset") + + if tile_offset < 0: + raise Exception("Model output shape is bigger than tile size, cannot calculate offset") else: tile_offset = 0 From e9c82d82a124b28bf972621bf0d0401f1161d94a Mon Sep 17 00:00:00 2001 From: Bartosz Date: Fri, 22 Sep 2023 14:10:09 +0200 Subject: [PATCH 06/16] Add pixel overlap option --- src/deepness/common/processing_overlap.py | 37 +++++ .../map_processing_parameters.py | 7 +- src/deepness/deepness_dockwidget.py | 27 +++- src/deepness/deepness_dockwidget.ui | 140 +++++++++++++----- ...ap_processor_detection_yolo_ultralytics.py | 3 +- ...ual_test_map_processor_detection_yolov6.py | 3 +- test/test_deepness_dockwidget.py | 2 +- ...st_map_processor_detection_oils_example.py | 22 +-- ..._map_processor_detection_planes_example.py | 15 +- test/test_map_processor_empty_detection.py | 16 +- test/test_map_processor_regression.py | 21 ++- test/test_map_processor_segmentation.py | 9 +- ...ssor_segmentation_different_output_size.py | 3 +- ...rocessor_segmentation_landcover_example.py | 15 +- test/test_map_processor_superresolution.py | 31 ++-- ...test_map_processor_training_data_export.py | 17 +-- 16 files changed, 238 insertions(+), 130 deletions(-) create mode 100644 src/deepness/common/processing_overlap.py diff --git a/src/deepness/common/processing_overlap.py b/src/deepness/common/processing_overlap.py new file mode 100644 index 0000000..a7c50f6 --- /dev/null +++ b/src/deepness/common/processing_overlap.py @@ -0,0 +1,37 @@ +import enum +from typing import Dict, List + + +class ProcessingOverlapOptions(enum.Enum): + OVERLAP_IN_PIXELS = 'Overlap in pixels' + OVERLAP_IN_PERCENT = 'Overlap in percent' + + +class ProcessingOverlap: + """ Represents overlap between tiles during processing + """ + def __init__(self, selected_option: ProcessingOverlapOptions, percentage: float = None, overlap_px: int = None): + self.selected_option = selected_option + + if selected_option == ProcessingOverlapOptions.OVERLAP_IN_PERCENT and percentage is None: + raise Exception(f"Percentage must be specified when using {ProcessingOverlapOptions.OVERLAP_IN_PERCENT}") + if selected_option == ProcessingOverlapOptions.OVERLAP_IN_PIXELS and overlap_px is None: + raise Exception(f"Overlap in pixels must be specified when using {ProcessingOverlapOptions.OVERLAP_IN_PIXELS}") + + if selected_option == ProcessingOverlapOptions.OVERLAP_IN_PERCENT: + self._percentage = percentage + elif selected_option == ProcessingOverlapOptions.OVERLAP_IN_PIXELS: + self._overlap_px = overlap_px + else: + raise Exception(f"Unknown option: {selected_option}") + + def get_overlap_px(self, tile_size_px: int) -> int: + """ Returns the overlap in pixels + + :param tile_size_px: Tile size in pixels + :return: Returns the overlap in pixels + """ + if self.selected_option == ProcessingOverlapOptions.OVERLAP_IN_PIXELS: + return self._overlap_px + else: + return int(tile_size_px * self._percentage / 100 * 2) // 2 # TODO: check if this is correct diff --git a/src/deepness/common/processing_parameters/map_processing_parameters.py b/src/deepness/common/processing_parameters/map_processing_parameters.py index 9caf031..7aedfd1 100644 --- a/src/deepness/common/processing_parameters/map_processing_parameters.py +++ b/src/deepness/common/processing_parameters/map_processing_parameters.py @@ -3,6 +3,7 @@ from typing import Optional from deepness.common.channels_mapping import ChannelsMapping +from deepness.common.processing_overlap import ProcessingOverlap class ProcessedAreaType(enum.Enum): @@ -40,7 +41,7 @@ class MapProcessingParameters: input_layer_id: str # raster layer to process mask_layer_id: Optional[str] # Processing of masked layer - if processed_area_type is FROM_POLYGONS - processing_overlap_percentage: float # aka stride - overlap of neighbouring tiles while processing (0-100) + processing_overlap: ProcessingOverlap # aka "stride" - how much to overlap tiles during processing input_channels_mapping: ChannelsMapping # describes mapping of image channels to model inputs @@ -54,9 +55,9 @@ def tile_size_m(self): @property def processing_overlap_px(self) -> int: """ - Always multiple of 2 + Always divide by 2, because overlap is on both sides of the tile """ - return int(self.tile_size_px * self.processing_overlap_percentage / 100 * 2) // 2 + return self.processing_overlap.get_overlap_px(self.tile_size_px) @property def resolution_m_per_px(self): diff --git a/src/deepness/deepness_dockwidget.py b/src/deepness/deepness_dockwidget.py index 9185e75..b6bb4b7 100644 --- a/src/deepness/deepness_dockwidget.py +++ b/src/deepness/deepness_dockwidget.py @@ -14,6 +14,7 @@ from deepness.common.config_entry_key import ConfigEntryKey from deepness.common.defines import IS_DEBUG, PLUGIN_NAME from deepness.common.errors import OperationFailedException +from deepness.common.processing_overlap import ProcessingOverlap, ProcessingOverlapOptions from deepness.common.processing_parameters.detection_parameters import DetectionParameters, DetectorType from deepness.common.processing_parameters.map_processing_parameters import (MapProcessingParameters, ModelOutputFormat, ProcessedAreaType) @@ -164,6 +165,7 @@ def _setup_misc_ui(self): self.mMapLayerComboBox_inputLayer.setFilters(QgsMapLayerProxyModel.RasterLayer) self.mMapLayerComboBox_areaMaskLayer.setFilters(QgsMapLayerProxyModel.VectorLayer) self._set_processed_area_mask_options() + self._set_processing_overlap_enabled() for model_definition in ModelDefinition.get_model_definitions(): self.comboBox_modelType.addItem(model_definition.model_type.value) @@ -201,6 +203,8 @@ def _create_connections(self): self.checkBox_pixelClassEnableThreshold.stateChanged.connect(self._set_probability_threshold_enabled) self.checkBox_removeSmallAreas.stateChanged.connect(self._set_remove_small_segment_enabled) self.comboBox_modelOutputFormat.currentIndexChanged.connect(self._model_output_format_changed) + self.radioButton_processingTileOverlapPercentage.toggled.connect(self._set_processing_overlap_enabled) + self.radioButton_processingTileOverlapPixels.toggled.connect(self._set_processing_overlap_enabled) def _model_type_changed(self): model_type = ModelType(self.comboBox_modelType.currentText()) @@ -235,6 +239,13 @@ def _model_output_format_changed(self): model_output_format = ModelOutputFormat(txt) class_number_selection_enabled = bool(model_output_format == ModelOutputFormat.ONLY_SINGLE_CLASS_AS_LAYER) self.comboBox_outputFormatClassNumber.setEnabled(class_number_selection_enabled) + + def _set_processing_overlap_enabled(self): + overlap_percentage_enabled = self.radioButton_processingTileOverlapPercentage.isChecked() + self.spinBox_processingTileOverlapPercentage.setEnabled(overlap_percentage_enabled) + + overlap_pixels_enabled = self.radioButton_processingTileOverlapPixels.isChecked() + self.spinBox_processingTileOverlapPixels.setEnabled(overlap_pixels_enabled) def _set_probability_threshold_enabled(self): self.doubleSpinBox_probabilityThreshold.setEnabled(self.checkBox_pixelClassEnableThreshold.isChecked()) @@ -400,6 +411,20 @@ def _get_input_layer_id(self): else: return '' + def _get_overlap_parameter(self): + if self.radioButton_processingTileOverlapPercentage.isChecked(): + return ProcessingOverlap( + selected_option=ProcessingOverlapOptions.OVERLAP_IN_PERCENT, + percentage=self.spinBox_processingTileOverlapPercentage.value(), + ) + elif self.radioButton_processingTileOverlapPixels.isChecked(): + return ProcessingOverlap( + selected_option=ProcessingOverlapOptions.OVERLAP_IN_PIXELS, + overlap_px=self.spinBox_processingTileOverlapPixels.value(), + ) + else: + raise Exception('Something goes wrong. No overlap parameter selected!') + def _get_pixel_classification_threshold(self): if not self.checkBox_pixelClassEnableThreshold.isChecked(): return 0 @@ -491,7 +516,7 @@ def _get_map_processing_parameters(self) -> MapProcessingParameters: processed_area_type=processed_area_type, mask_layer_id=self.get_mask_layer_id(), input_layer_id=self._get_input_layer_id(), - processing_overlap_percentage=self.spinBox_processingTileOverlapPercentage.value(), + processing_overlap=self._get_overlap_parameter(), input_channels_mapping=self._input_channels_mapping_widget.get_channels_mapping(), model_output_format=ModelOutputFormat(self.comboBox_modelOutputFormat.currentText()), model_output_format__single_class_number=self.comboBox_outputFormatClassNumber.currentIndex(), diff --git a/src/deepness/deepness_dockwidget.ui b/src/deepness/deepness_dockwidget.ui index 20bc878..6748e35 100644 --- a/src/deepness/deepness_dockwidget.ui +++ b/src/deepness/deepness_dockwidget.ui @@ -26,7 +26,7 @@ 0 0 452 - 1503 + 1487 @@ -44,7 +44,6 @@ 9 - 75 true @@ -238,7 +237,6 @@ - 75 true @@ -265,13 +263,6 @@ Processing parameters - - - - Tile size [px]: - - - @@ -291,26 +282,100 @@ - - + + + + Tiles overlap: + + + + + + + 0 + 0 + + + + + 80 + 0 + + + + <html><head/><body><p>Defines how much tiles should overlap on their neighbours during processing.</p><p>Especially required for model which introduce distortions on the edges of images, so that it can be removed in postprocessing.</p></body></html> + + + + + + 15 + + + + + + + + 0 + 0 + + + + + 80 + 0 + + + + 9999999 + + + + + + + [px] + + + + + + + [%] + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + - Resolution [cm/px]: + Tile size [px]: - - - - true - - - <html><head/><body><p>Size of the images passed to the model.</p><p>Usually needs to be the same as the one used during training.</p></body></html> - - - 99999 - - - 512 + + + + Resolution [cm/px]: @@ -322,23 +387,19 @@ for some models - - - - Tiles overlap [%]: + + + + true - - - - - <html><head/><body><p>Defines how much tiles should overlap on their neighbours during processing.</p><p>Especially required for model which introduce distortions on the edges of images, so that it can be removed in postprocessing.</p></body></html> + <html><head/><body><p>Size of the images passed to the model.</p><p>Usually needs to be the same as the one used during training.</p></body></html> - - + + 99999 - 15 + 512 @@ -371,7 +432,6 @@ for some models - 75 true @@ -567,7 +627,6 @@ for some models - 75 true @@ -661,7 +720,6 @@ for some models - 75 true diff --git a/test/manual_test_map_processor_detection_yolo_ultralytics.py b/test/manual_test_map_processor_detection_yolo_ultralytics.py index 4465615..983c5f8 100644 --- a/test/manual_test_map_processor_detection_yolo_ultralytics.py +++ b/test/manual_test_map_processor_detection_yolo_ultralytics.py @@ -3,6 +3,7 @@ from test.test_utils import create_default_input_channels_mapping_for_rgb_bands, create_rlayer_from_file, init_qgis from unittest.mock import MagicMock +from deepness.common.processing_overlap import ProcessingOverlap, ProcessingOverlapOptions from deepness.common.processing_parameters.detection_parameters import DetectionParameters, DetectorType from deepness.common.processing_parameters.map_processing_parameters import ModelOutputFormat, ProcessedAreaType from deepness.processing.map_processor.map_processor_detection import MapProcessorDetection @@ -32,7 +33,7 @@ def test_map_processor_detection_yolo_ultralytics(): mask_layer_id=None, input_layer_id=rlayer.id(), input_channels_mapping=INPUT_CHANNELS_MAPPING, - processing_overlap_percentage=60, + processing_overlap=ProcessingOverlap(ProcessingOverlapOptions.OVERLAP_IN_PERCENT, percentage=60), model=model_wrapper, confidence=0.5, iou_threshold=0.4, diff --git a/test/manual_test_map_processor_detection_yolov6.py b/test/manual_test_map_processor_detection_yolov6.py index 4b391d6..218f6db 100644 --- a/test/manual_test_map_processor_detection_yolov6.py +++ b/test/manual_test_map_processor_detection_yolov6.py @@ -3,6 +3,7 @@ from test.test_utils import create_default_input_channels_mapping_for_rgb_bands, create_rlayer_from_file, init_qgis from unittest.mock import MagicMock +from deepness.common.processing_overlap import ProcessingOverlap, ProcessingOverlapOptions from deepness.common.processing_parameters.detection_parameters import DetectionParameters, DetectorType from deepness.common.processing_parameters.map_processing_parameters import ModelOutputFormat, ProcessedAreaType from deepness.processing.map_processor.map_processor_detection import MapProcessorDetection @@ -32,7 +33,7 @@ def test_map_processor_detection_yolov6(): mask_layer_id=None, input_layer_id=rlayer.id(), input_channels_mapping=INPUT_CHANNELS_MAPPING, - processing_overlap_percentage=0, + processing_overlap=ProcessingOverlap(ProcessingOverlapOptions.OVERLAP_IN_PERCENT, percentage=0), model=model_wrapper, confidence=0.9, iou_threshold=0.4, diff --git a/test/test_deepness_dockwidget.py b/test/test_deepness_dockwidget.py index cbbb9d6..fa88f78 100644 --- a/test/test_deepness_dockwidget.py +++ b/test/test_deepness_dockwidget.py @@ -92,7 +92,7 @@ def test_get_inference_parameters(): assert params.processed_area_type == ProcessedAreaType.VISIBLE_PART assert params.tile_size_px == 512 # should be read from model input # assert params.input_layer_id == rlayer.id() - assert params.processing_overlap_percentage == 44 + assert params.processing_overlap.get_overlap_px(params.tile_size_px) == int(0.44*params.tile_size_px) assert params.input_channels_mapping.get_number_of_model_inputs() == 3 assert params.input_channels_mapping.get_number_of_image_channels() == 4 assert params.input_channels_mapping.get_image_channel_index_for_model_input(2) == 2 diff --git a/test/test_map_processor_detection_oils_example.py b/test/test_map_processor_detection_oils_example.py index 8a8c933..9378a4a 100644 --- a/test/test_map_processor_detection_oils_example.py +++ b/test/test_map_processor_detection_oils_example.py @@ -1,18 +1,16 @@ +import os +from pathlib import Path +from test.test_utils import create_default_input_channels_mapping_for_rgb_bands, create_rlayer_from_file, init_qgis from unittest.mock import MagicMock -import numpy as np + import matplotlib.pyplot as plt +import numpy as np +from deepness.common.processing_overlap import ProcessingOverlap, ProcessingOverlapOptions from deepness.common.processing_parameters.detection_parameters import DetectionParameters -from deepness.common.processing_parameters.map_processing_parameters import ProcessedAreaType, \ - ModelOutputFormat +from deepness.common.processing_parameters.map_processing_parameters import ModelOutputFormat, ProcessedAreaType from deepness.processing.map_processor.map_processor_detection import MapProcessorDetection from deepness.processing.models.detector import Detector -from test.test_utils import init_qgis, create_rlayer_from_file, \ - create_default_input_channels_mapping_for_rgb_bands - -import os - -from pathlib import Path HOME_DIR = Path(__file__).resolve().parents[1] EXAMPLE_DATA_DIR = os.path.join(HOME_DIR, 'examples', 'yolov5_oils_detection_bing_map') @@ -36,7 +34,7 @@ def test_map_processor_detection_oil_example(): mask_layer_id=None, input_layer_id=rlayer.id(), input_channels_mapping=INPUT_CHANNELS_MAPPING, - processing_overlap_percentage=40, + processing_overlap=ProcessingOverlap(ProcessingOverlapOptions.OVERLAP_IN_PERCENT, percentage=40), model=model_wrapper, confidence=0.5, iou_threshold=0.1, @@ -54,6 +52,7 @@ def test_map_processor_detection_oil_example(): map_processor.run() + def test_map_processor_detection_oil_example_with_remove_small(): qgs = init_qgis() rlayer = create_rlayer_from_file(RASTER_FILE_PATH) @@ -67,7 +66,7 @@ def test_map_processor_detection_oil_example_with_remove_small(): mask_layer_id=None, input_layer_id=rlayer.id(), input_channels_mapping=INPUT_CHANNELS_MAPPING, - processing_overlap_percentage=40, + processing_overlap=ProcessingOverlap(ProcessingOverlapOptions.OVERLAP_IN_PERCENT, percentage=40), model=model_wrapper, confidence=0.5, iou_threshold=0.3, @@ -85,6 +84,7 @@ def test_map_processor_detection_oil_example_with_remove_small(): map_processor.run() + if __name__ == '__main__': test_map_processor_detection_oil_example() test_map_processor_detection_oil_example_with_remove_small() diff --git a/test/test_map_processor_detection_planes_example.py b/test/test_map_processor_detection_planes_example.py index 1492dd4..8311936 100644 --- a/test/test_map_processor_detection_planes_example.py +++ b/test/test_map_processor_detection_planes_example.py @@ -1,16 +1,13 @@ +import os +from pathlib import Path +from test.test_utils import create_default_input_channels_mapping_for_rgb_bands, create_rlayer_from_file, init_qgis from unittest.mock import MagicMock +from deepness.common.processing_overlap import ProcessingOverlap, ProcessingOverlapOptions from deepness.common.processing_parameters.detection_parameters import DetectionParameters -from deepness.common.processing_parameters.map_processing_parameters import ProcessedAreaType, \ - ModelOutputFormat +from deepness.common.processing_parameters.map_processing_parameters import ModelOutputFormat, ProcessedAreaType from deepness.processing.map_processor.map_processor_detection import MapProcessorDetection from deepness.processing.models.detector import Detector -from test.test_utils import init_qgis, create_rlayer_from_file, \ - create_default_input_channels_mapping_for_rgb_bands - -import os - -from pathlib import Path HOME_DIR = Path(__file__).resolve().parents[1] EXAMPLE_DATA_DIR = os.path.join(HOME_DIR, 'examples', 'yolov7_planes_detection_google_earth') @@ -34,7 +31,7 @@ def test_map_processor_detection_planes_example(): mask_layer_id=None, input_layer_id=rlayer.id(), input_channels_mapping=INPUT_CHANNELS_MAPPING, - processing_overlap_percentage=60, + processing_overlap=ProcessingOverlap(ProcessingOverlapOptions.OVERLAP_IN_PERCENT, percentage=60), model=model_wrapper, confidence=0.5, iou_threshold=0.4, diff --git a/test/test_map_processor_empty_detection.py b/test/test_map_processor_empty_detection.py index d610d45..247d1c8 100644 --- a/test/test_map_processor_empty_detection.py +++ b/test/test_map_processor_empty_detection.py @@ -1,16 +1,14 @@ +import os +from pathlib import Path +from test.test_utils import (create_default_input_channels_mapping_for_rgb_bands, create_rlayer_from_file, + get_dummy_fotomap_small_path, init_qgis) from unittest.mock import MagicMock +from deepness.common.processing_overlap import ProcessingOverlap, ProcessingOverlapOptions from deepness.common.processing_parameters.detection_parameters import DetectionParameters -from deepness.common.processing_parameters.map_processing_parameters import ProcessedAreaType, \ - ModelOutputFormat +from deepness.common.processing_parameters.map_processing_parameters import ModelOutputFormat, ProcessedAreaType from deepness.processing.map_processor.map_processor_detection import MapProcessorDetection from deepness.processing.models.detector import Detector -from test.test_utils import init_qgis, create_rlayer_from_file, \ - create_default_input_channels_mapping_for_rgb_bands, get_dummy_fotomap_small_path - -import os - -from pathlib import Path HOME_DIR = Path(__file__).resolve().parents[1] EXAMPLE_DATA_DIR = os.path.join(HOME_DIR, 'examples', 'yolov7_planes_detection_google_earth') @@ -34,7 +32,7 @@ def test_map_processor_empty_detection(): mask_layer_id=None, input_layer_id=rlayer.id(), input_channels_mapping=INPUT_CHANNELS_MAPPING, - processing_overlap_percentage=0, + processing_overlap=ProcessingOverlap(ProcessingOverlapOptions.OVERLAP_IN_PERCENT, percentage=0), model=model_wrapper, confidence=0.99, iou_threshold=0.99, diff --git a/test/test_map_processor_regression.py b/test/test_map_processor_regression.py index bf47cf1..b1bc48e 100644 --- a/test/test_map_processor_regression.py +++ b/test/test_map_processor_regression.py @@ -1,22 +1,19 @@ +from test.test_utils import (create_default_input_channels_mapping_for_rgba_bands, create_rlayer_from_file, + create_vlayer_from_file, get_dummy_fotomap_area_path, get_dummy_fotomap_small_path, + get_dummy_regression_model_path, get_dummy_segmentation_model_path, init_qgis) from unittest.mock import MagicMock +import numpy as np from qgis.core import QgsCoordinateReferenceSystem, QgsRectangle +from deepness.common.processing_overlap import ProcessingOverlap, ProcessingOverlapOptions +from deepness.common.processing_parameters.map_processing_parameters import ModelOutputFormat, ProcessedAreaType from deepness.common.processing_parameters.regression_parameters import RegressionParameters from deepness.common.processing_parameters.segmentation_parameters import SegmentationParameters -from deepness.common.processing_parameters.map_processing_parameters import ProcessedAreaType, \ - ModelOutputFormat from deepness.processing.map_processor.map_processor_regression import MapProcessorRegression from deepness.processing.map_processor.map_processor_segmentation import MapProcessorSegmentation from deepness.processing.models.regressor import Regressor from deepness.processing.models.segmentor import Segmentor -from test.test_utils import init_qgis, create_rlayer_from_file, \ - create_vlayer_from_file, get_dummy_fotomap_area_path, get_dummy_fotomap_small_path, \ - get_dummy_segmentation_model_path, \ - create_default_input_channels_mapping_for_rgba_bands, get_dummy_regression_model_path - -import numpy as np - RASTER_FILE_PATH = get_dummy_fotomap_small_path() @@ -45,7 +42,7 @@ def test_dummy_model_processing__entire_file(): input_layer_id=rlayer.id(), input_channels_mapping=INPUT_CHANNELS_MAPPING, output_scaling=1.0, - processing_overlap_percentage=20, + processing_overlap=ProcessingOverlap(ProcessingOverlapOptions.OVERLAP_IN_PERCENT, percentage=20), model_output_format=ModelOutputFormat.ALL_CLASSES_AS_SEPARATE_LAYERS, model_output_format__single_class_number=-1, model=model, @@ -89,7 +86,7 @@ def test_dummy_model_processing__entire_file(): # input_layer_id=rlayer.id(), # input_channels_mapping=INPUT_CHANNELS_MAPPING, # postprocessing_dilate_erode_size=5, -# processing_overlap_percentage=20, +# processing_overlap=20, # pixel_classification__probability_threshold=0.5, # model_output_format=ModelOutputFormat.ONLY_SINGLE_CLASS_AS_LAYER, # model_output_format__single_class_number=1, @@ -123,7 +120,7 @@ def test_dummy_model_processing__entire_file(): # input_layer_id=rlayer.id(), # input_channels_mapping=INPUT_CHANNELS_MAPPING, # postprocessing_dilate_erode_size=5, -# processing_overlap_percentage=20, +# processing_overlap=20, # pixel_classification__probability_threshold=0.5, # model_output_format=ModelOutputFormat.CLASSES_AS_SEPARATE_LAYERS_WITHOUT_ZERO_CLASS, # model_output_format__single_class_number=-1, diff --git a/test/test_map_processor_segmentation.py b/test/test_map_processor_segmentation.py index 2cbe326..22aa486 100644 --- a/test/test_map_processor_segmentation.py +++ b/test/test_map_processor_segmentation.py @@ -7,6 +7,7 @@ import numpy as np from qgis.core import QgsCoordinateReferenceSystem, QgsRectangle +from deepness.common.processing_overlap import ProcessingOverlap, ProcessingOverlapOptions from deepness.common.processing_parameters.map_processing_parameters import ModelOutputFormat, ProcessedAreaType from deepness.common.processing_parameters.segmentation_parameters import SegmentationParameters from deepness.processing.map_processor.map_processor_segmentation import MapProcessorSegmentation @@ -41,7 +42,7 @@ def test_dummy_model_processing__entire_file(): input_layer_id=rlayer.id(), input_channels_mapping=INPUT_CHANNELS_MAPPING, postprocessing_dilate_erode_size=5, - processing_overlap_percentage=20, + processing_overlap=ProcessingOverlap(ProcessingOverlapOptions.OVERLAP_IN_PERCENT, percentage=20), pixel_classification__probability_threshold=0.5, model_output_format=ModelOutputFormat.ALL_CLASSES_AS_SEPARATE_LAYERS, model_output_format__single_class_number=-1, @@ -86,7 +87,7 @@ def test_generic_processing_test__specified_extent_from_vlayer(): input_layer_id=rlayer.id(), input_channels_mapping=INPUT_CHANNELS_MAPPING, postprocessing_dilate_erode_size=5, - processing_overlap_percentage=20, + processing_overlap=ProcessingOverlap(ProcessingOverlapOptions.OVERLAP_IN_PERCENT, percentage=20), pixel_classification__probability_threshold=0.5, model_output_format=ModelOutputFormat.ONLY_SINGLE_CLASS_AS_LAYER, model_output_format__single_class_number=1, @@ -130,7 +131,7 @@ def test_generic_processing_test__specified_extent_from_vlayer_crs3857(): input_layer_id=rlayer.id(), input_channels_mapping=INPUT_CHANNELS_MAPPING, postprocessing_dilate_erode_size=5, - processing_overlap_percentage=20, + processing_overlap=ProcessingOverlap(ProcessingOverlapOptions.OVERLAP_IN_PERCENT, percentage=20), pixel_classification__probability_threshold=0.5, model_output_format=ModelOutputFormat.ONLY_SINGLE_CLASS_AS_LAYER, model_output_format__single_class_number=1, @@ -175,7 +176,7 @@ def test_generic_processing_test__specified_extent_from_active_map_extent(): input_layer_id=rlayer.id(), input_channels_mapping=INPUT_CHANNELS_MAPPING, postprocessing_dilate_erode_size=5, - processing_overlap_percentage=20, + processing_overlap=ProcessingOverlap(ProcessingOverlapOptions.OVERLAP_IN_PERCENT, percentage=20), pixel_classification__probability_threshold=0.5, model_output_format=ModelOutputFormat.CLASSES_AS_SEPARATE_LAYERS_WITHOUT_ZERO_CLASS, model_output_format__single_class_number=-1, diff --git a/test/test_map_processor_segmentation_different_output_size.py b/test/test_map_processor_segmentation_different_output_size.py index 1ed3c0b..d71ef2f 100644 --- a/test/test_map_processor_segmentation_different_output_size.py +++ b/test/test_map_processor_segmentation_different_output_size.py @@ -8,6 +8,7 @@ import numpy as np from qgis.core import QgsCoordinateReferenceSystem, QgsRectangle +from deepness.common.processing_overlap import ProcessingOverlap, ProcessingOverlapOptions from deepness.common.processing_parameters.map_processing_parameters import ModelOutputFormat, ProcessedAreaType from deepness.common.processing_parameters.segmentation_parameters import SegmentationParameters from deepness.processing.map_processor.map_processor_segmentation import MapProcessorSegmentation @@ -42,7 +43,7 @@ def test_dummy_model_processing_when_different_output_size(): input_layer_id=rlayer.id(), input_channels_mapping=INPUT_CHANNELS_MAPPING, postprocessing_dilate_erode_size=5, - processing_overlap_percentage=20, + processing_overlap=ProcessingOverlap(ProcessingOverlapOptions.OVERLAP_IN_PERCENT, percentage=20), pixel_classification__probability_threshold=0.5, model_output_format=ModelOutputFormat.ALL_CLASSES_AS_SEPARATE_LAYERS, model_output_format__single_class_number=-1, diff --git a/test/test_map_processor_segmentation_landcover_example.py b/test/test_map_processor_segmentation_landcover_example.py index 34f192d..f6c9924 100644 --- a/test/test_map_processor_segmentation_landcover_example.py +++ b/test/test_map_processor_segmentation_landcover_example.py @@ -1,16 +1,13 @@ +import os +from pathlib import Path +from test.test_utils import create_default_input_channels_mapping_for_rgb_bands, create_rlayer_from_file, init_qgis from unittest.mock import MagicMock +from deepness.common.processing_overlap import ProcessingOverlap, ProcessingOverlapOptions +from deepness.common.processing_parameters.map_processing_parameters import ModelOutputFormat, ProcessedAreaType from deepness.common.processing_parameters.segmentation_parameters import SegmentationParameters -from deepness.common.processing_parameters.map_processing_parameters import ProcessedAreaType, \ - ModelOutputFormat from deepness.processing.map_processor.map_processor_segmentation import MapProcessorSegmentation from deepness.processing.models.segmentor import Segmentor -from test.test_utils import init_qgis, create_rlayer_from_file, \ - create_default_input_channels_mapping_for_rgb_bands - -import os - -from pathlib import Path HOME_DIR = Path(__file__).resolve().parents[1] EXAMPLE_DATA_DIR = os.path.join(HOME_DIR, 'examples', 'deeplabv3_segmentation_landcover') @@ -35,7 +32,7 @@ def test_map_processor_segmentation_landcover_example(): input_layer_id=rlayer.id(), input_channels_mapping=INPUT_CHANNELS_MAPPING, postprocessing_dilate_erode_size=5, - processing_overlap_percentage=20, + processing_overlap=ProcessingOverlap(ProcessingOverlapOptions.OVERLAP_IN_PERCENT, percentage=20), pixel_classification__probability_threshold=0.5, model_output_format=ModelOutputFormat.ALL_CLASSES_AS_SEPARATE_LAYERS, model_output_format__single_class_number=-1, diff --git a/test/test_map_processor_superresolution.py b/test/test_map_processor_superresolution.py index fc0c1f1..fa5f524 100644 --- a/test/test_map_processor_superresolution.py +++ b/test/test_map_processor_superresolution.py @@ -1,22 +1,19 @@ +from test.test_utils import (create_default_input_channels_mapping_for_rgba_bands, create_rlayer_from_file, + create_vlayer_from_file, get_dummy_fotomap_area_path, get_dummy_fotomap_small_path, + get_dummy_segmentation_model_path, get_dummy_superresolution_model_path, init_qgis) from unittest.mock import MagicMock +import numpy as np from qgis.core import QgsCoordinateReferenceSystem, QgsRectangle -from deepness.common.processing_parameters.superresolution_parameters import SuperresolutionParameters +from deepness.common.processing_overlap import ProcessingOverlap, ProcessingOverlapOptions +from deepness.common.processing_parameters.map_processing_parameters import ModelOutputFormat, ProcessedAreaType from deepness.common.processing_parameters.segmentation_parameters import SegmentationParameters -from deepness.common.processing_parameters.map_processing_parameters import ProcessedAreaType, \ - ModelOutputFormat -from deepness.processing.map_processor.map_processor_superresolution import MapProcessorSuperresolution +from deepness.common.processing_parameters.superresolution_parameters import SuperresolutionParameters from deepness.processing.map_processor.map_processor_segmentation import MapProcessorSegmentation -from deepness.processing.models.superresolution import Superresolution +from deepness.processing.map_processor.map_processor_superresolution import MapProcessorSuperresolution from deepness.processing.models.segmentor import Segmentor -from test.test_utils import init_qgis, create_rlayer_from_file, \ - create_vlayer_from_file, get_dummy_fotomap_area_path, get_dummy_fotomap_small_path, \ - get_dummy_segmentation_model_path, \ - create_default_input_channels_mapping_for_rgba_bands, get_dummy_superresolution_model_path - -import numpy as np - +from deepness.processing.models.superresolution import Superresolution RASTER_FILE_PATH = get_dummy_fotomap_small_path() @@ -27,8 +24,8 @@ INPUT_CHANNELS_MAPPING = create_default_input_channels_mapping_for_rgba_bands() PROCESSED_EXTENT_1 = QgsRectangle( # big part of the fotomap - 638840.370, 5802593.197, - 638857.695, 5802601.792) + 638840.370, 5802593.197, + 638857.695, 5802601.792) def test_dummy_model_processing__entire_file(): @@ -46,7 +43,7 @@ def test_dummy_model_processing__entire_file(): input_channels_mapping=INPUT_CHANNELS_MAPPING, output_scaling=1.0, scale_factor=2.0, - processing_overlap_percentage=0, + processing_overlap=ProcessingOverlap(ProcessingOverlapOptions.OVERLAP_IN_PERCENT, percentage=0), model_output_format=ModelOutputFormat.ALL_CLASSES_AS_SEPARATE_LAYERS, model_output_format__single_class_number=-1, model=model, @@ -63,12 +60,10 @@ def test_dummy_model_processing__entire_file(): result_img = map_processor.get_result_imgs() result_img = result_img # take only the first band - assert result_img.shape == (int(560*2),int( 828*2),3) # 2x upscaled + assert result_img.shape == (int(560*2), int(828*2), 3) # 2x upscaled # TODO - add detailed check for pixel values once we have output channels mapping with thresholding - - if __name__ == '__main__': test_dummy_model_processing__entire_file() # test_generic_processing_test__specified_extent_from_vlayer() diff --git a/test/test_map_processor_training_data_export.py b/test/test_map_processor_training_data_export.py index 41fb12d..7f4e1cc 100644 --- a/test/test_map_processor_training_data_export.py +++ b/test/test_map_processor_training_data_export.py @@ -1,13 +1,12 @@ +from test.test_utils import (create_default_input_channels_mapping_for_rgba_bands, create_rlayer_from_file, + create_vlayer_from_file, get_dummy_fotomap_area_path, get_dummy_fotomap_small_path, + init_qgis) from unittest.mock import MagicMock -from deepness.common.processing_parameters.map_processing_parameters import ProcessedAreaType, \ - ModelOutputFormat -from deepness.common.processing_parameters.training_data_export_parameters import \ - TrainingDataExportParameters +from deepness.common.processing_overlap import ProcessingOverlap, ProcessingOverlapOptions +from deepness.common.processing_parameters.map_processing_parameters import ModelOutputFormat, ProcessedAreaType +from deepness.common.processing_parameters.training_data_export_parameters import TrainingDataExportParameters from deepness.processing.map_processor.map_processor_training_data_export import MapProcessorTrainingDataExport -from test.test_utils import init_qgis, create_rlayer_from_file, \ - create_vlayer_from_file, get_dummy_fotomap_area_path, get_dummy_fotomap_small_path, \ - create_default_input_channels_mapping_for_rgba_bands RASTER_FILE_PATH = get_dummy_fotomap_small_path() @@ -28,7 +27,7 @@ def test_export_dummy_fotomap(): mask_layer_id=None, input_layer_id=rlayer.id(), input_channels_mapping=create_default_input_channels_mapping_for_rgba_bands(), - processing_overlap_percentage=20, + processing_overlap=ProcessingOverlap(ProcessingOverlapOptions.OVERLAP_IN_PERCENT, percentage=20), model_output_format=ModelOutputFormat.ALL_CLASSES_AS_SEPARATE_LAYERS, model_output_format__single_class_number=-1, ) @@ -76,7 +75,7 @@ def test_export_dummy_fotomap(): # mask_layer_id=None, # input_layer_id=rlayer.id(), # input_channels_mapping=create_default_input_channels_mapping_for_google_satellite_bands(), -# processing_overlap_percentage=20, +# processing_overlap=20, # ) # # processed_extent = QgsRectangle( From f47b5fb9654ad4893dc35fe9d86bd464de7b0ff3 Mon Sep 17 00:00:00 2001 From: Bartosz Ptak Date: Fri, 22 Sep 2023 14:19:35 +0200 Subject: [PATCH 07/16] Update map_processing_parameters.py --- .../common/processing_parameters/map_processing_parameters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/deepness/common/processing_parameters/map_processing_parameters.py b/src/deepness/common/processing_parameters/map_processing_parameters.py index 7aedfd1..75d80f3 100644 --- a/src/deepness/common/processing_parameters/map_processing_parameters.py +++ b/src/deepness/common/processing_parameters/map_processing_parameters.py @@ -55,7 +55,7 @@ def tile_size_m(self): @property def processing_overlap_px(self) -> int: """ - Always divide by 2, because overlap is on both sides of the tile + Always divisible by 2, because overlap is on both sides of the tile """ return self.processing_overlap.get_overlap_px(self.tile_size_px) From d8a62f546cb235da15e85829f74eda83024f7b2c Mon Sep 17 00:00:00 2001 From: Bartosz Date: Fri, 22 Sep 2023 14:33:40 +0200 Subject: [PATCH 08/16] Add pixel overlap test --- test/test_map_processor_segmentation.py | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/test_map_processor_segmentation.py b/test/test_map_processor_segmentation.py index 22aa486..dcb96e6 100644 --- a/test/test_map_processor_segmentation.py +++ b/test/test_map_processor_segmentation.py @@ -62,6 +62,38 @@ def test_dummy_model_processing__entire_file(): assert result_img.shape == (561, 829) # TODO - add detailed check for pixel values once we have output channels mapping with thresholding +def test_dummy_model_processing__entire_file_overlap_in_pixels(): + qgs = init_qgis() + + rlayer = create_rlayer_from_file(RASTER_FILE_PATH) + model = Segmentor(MODEL_FILE_PATH) + + params = SegmentationParameters( + resolution_cm_per_px=3, + tile_size_px=model.get_input_size_in_pixels()[0], # same x and y dimensions, so take x + processed_area_type=ProcessedAreaType.ENTIRE_LAYER, + mask_layer_id=None, + input_layer_id=rlayer.id(), + input_channels_mapping=INPUT_CHANNELS_MAPPING, + postprocessing_dilate_erode_size=5, + processing_overlap=ProcessingOverlap(ProcessingOverlapOptions.OVERLAP_IN_PIXELS, overlap_px=int(model.get_input_size_in_pixels()[0] * 0.2)), + pixel_classification__probability_threshold=0.5, + model_output_format=ModelOutputFormat.ALL_CLASSES_AS_SEPARATE_LAYERS, + model_output_format__single_class_number=-1, + model=model, + ) + + map_processor = MapProcessorSegmentation( + rlayer=rlayer, + vlayer_mask=None, + map_canvas=MagicMock(), + params=params, + ) + + map_processor.run() + result_img = map_processor.get_result_img() + + assert result_img.shape == (561, 829) def model_process_mock(x): x = x[:, :, 0:2] From 92e68bfabff0882c309d90913dc26734663a4902 Mon Sep 17 00:00:00 2001 From: Bartosz Date: Tue, 26 Sep 2023 10:44:00 +0200 Subject: [PATCH 09/16] Introduce YOLO_Ultralytics_segmentation --- .../detection_parameters.py | 3 +- src/deepness/processing/models/detector.py | 2 + ...map_processor_instance_yolo_ultralytics.py | 60 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 test/manual_test_map_processor_instance_yolo_ultralytics.py diff --git a/src/deepness/common/processing_parameters/detection_parameters.py b/src/deepness/common/processing_parameters/detection_parameters.py index 995a19b..f9c21ea 100644 --- a/src/deepness/common/processing_parameters/detection_parameters.py +++ b/src/deepness/common/processing_parameters/detection_parameters.py @@ -21,6 +21,7 @@ class DetectorType(enum.Enum): YOLO_v5_v7_DEFAULT = 'YOLO_v5_or_v7_default' YOLO_v6 = 'YOLO_v6' YOLO_ULTRALYTICS = 'YOLO_Ultralytics' + YOLO_ULTRALYTICS_SEGMENTATION = 'YOLO_Ultralytics_segmentation' def get_parameters(self): if self == DetectorType.YOLO_v5_v7_DEFAULT: @@ -29,7 +30,7 @@ def get_parameters(self): return DetectorTypeParameters( ignore_objectness_probability=True, ) - elif self == DetectorType.YOLO_ULTRALYTICS: + elif self == DetectorType.YOLO_ULTRALYTICS or self == DetectorType.YOLO_ULTRALYTICS_SEGMENTATION: return DetectorTypeParameters( has_inverted_output_shape=True, skipped_objectness_probability=True, diff --git a/src/deepness/processing/models/detector.py b/src/deepness/processing/models/detector.py index 776bb79..e36f6d7 100644 --- a/src/deepness/processing/models/detector.py +++ b/src/deepness/processing/models/detector.py @@ -30,6 +30,8 @@ class of the detected object """float: confidence of the detection""" clss: int """int: class of the detected object""" + mask: np.ndarray | None = None + """np.ndarray: mask of the detected object""" def convert_to_global(self, offset_x: int, offset_y: int): """Apply (x,y) offset to bounding box coordinates diff --git a/test/manual_test_map_processor_instance_yolo_ultralytics.py b/test/manual_test_map_processor_instance_yolo_ultralytics.py new file mode 100644 index 0000000..b5e0aa5 --- /dev/null +++ b/test/manual_test_map_processor_instance_yolo_ultralytics.py @@ -0,0 +1,60 @@ +import os +from pathlib import Path +from test.test_utils import create_default_input_channels_mapping_for_rgb_bands, create_rlayer_from_file, init_qgis +from unittest.mock import MagicMock + +from deepness.common.processing_overlap import ProcessingOverlap, ProcessingOverlapOptions +from deepness.common.processing_parameters.detection_parameters import DetectionParameters, DetectorType +from deepness.common.processing_parameters.map_processing_parameters import ModelOutputFormat, ProcessedAreaType +from deepness.processing.map_processor.map_processor_detection import MapProcessorDetection +from deepness.processing.models.detector import Detector + +# Files and model from github issue: https://github.com/PUTvision/qgis-plugin-deepness/discussions/101 + +HOME_DIR = Path(__file__).resolve().parents[1] +EXAMPLE_DATA_DIR = os.path.join(HOME_DIR, 'examples', 'manually_downloaded') + +MODEL_FILE_PATH = os.path.join(EXAMPLE_DATA_DIR, 'earthquake_segmentor_yolo_v8_21_09_23.onnx') +RASTER_FILE_PATH = os.path.join(EXAMPLE_DATA_DIR, 'tile_img_1_9_png.rf.058cef38273ca9aa63e86ea761841315.jpg') + +INPUT_CHANNELS_MAPPING = create_default_input_channels_mapping_for_rgb_bands() + + +def test_map_processor_detection_yolo_ultralytics(): + qgs = init_qgis() + + rlayer = create_rlayer_from_file(RASTER_FILE_PATH) + model_wrapper = Detector(MODEL_FILE_PATH) + + params = DetectionParameters( + resolution_cm_per_px=400, + tile_size_px=model_wrapper.get_input_size_in_pixels()[0], # same x and y dimensions, so take x + processed_area_type=ProcessedAreaType.ENTIRE_LAYER, + mask_layer_id=None, + input_layer_id=rlayer.id(), + input_channels_mapping=INPUT_CHANNELS_MAPPING, + processing_overlap=ProcessingOverlap(ProcessingOverlapOptions.OVERLAP_IN_PERCENT, percentage=60), + model=model_wrapper, + confidence=0.5, + iou_threshold=0.4, + remove_overlapping_detections=False, + model_output_format=ModelOutputFormat.ALL_CLASSES_AS_SEPARATE_LAYERS, + model_output_format__single_class_number=-1, + detector_type=DetectorType.YOLO_ULTRALYTICS_SEGMENTATION, + ) + + map_processor = MapProcessorDetection( + rlayer=rlayer, + vlayer_mask=None, + map_canvas=MagicMock(), + params=params, + ) + + map_processor.run() + + assert len(map_processor.get_all_detections()) == 17 + + +if __name__ == '__main__': + test_map_processor_detection_strange_format() + print('Done') From 88e59f221b54bd6d594f4eb2093669f28891efb1 Mon Sep 17 00:00:00 2001 From: Bartosz Date: Tue, 26 Sep 2023 12:12:39 +0200 Subject: [PATCH 10/16] Fix overlap --- src/deepness/processing/tile_params.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/deepness/processing/tile_params.py b/src/deepness/processing/tile_params.py index 0cf1ce6..8e06029 100644 --- a/src/deepness/processing/tile_params.py +++ b/src/deepness/processing/tile_params.py @@ -96,13 +96,13 @@ def get_slice_on_full_image_for_copying(self, tile_offset: int = 0): :return Slice to be used on the full image """ - half_overlap = (self.params.tile_size_px - self.stride_px) // 2 + half_overlap = max((self.params.tile_size_px - self.stride_px) // 2 - 2*tile_offset, 0) # 'core' part of the tile (not overlapping with other tiles), for sure copied for each tile x_min = self.start_pixel_x + half_overlap x_max = self.start_pixel_x + self.params.tile_size_px - half_overlap - 1 y_min = self.start_pixel_y + half_overlap - y_max = self.start_pixel_y + self.params.tile_size_px - half_overlap - 1 + y_max = self.start_pixel_y + self.params.tile_size_px - half_overlap - 1 # edge tiles handling if self.x_bin_number == 0: From 5b28a94c0f27e0d649f70c2e9a7f9a1fdb5fa76f Mon Sep 17 00:00:00 2001 From: Bartosz Date: Tue, 26 Sep 2023 14:48:45 +0200 Subject: [PATCH 11/16] WIP ULTRALYTICS YOLO SEGMENT support --- .../map_processor/map_processor_detection.py | 55 +++++++---- src/deepness/processing/models/detector.py | 98 +++++++++++++++++-- ...map_processor_instance_yolo_ultralytics.py | 4 +- 3 files changed, 127 insertions(+), 30 deletions(-) diff --git a/src/deepness/processing/map_processor/map_processor_detection.py b/src/deepness/processing/map_processor/map_processor_detection.py index 3bfdbe6..074009b 100644 --- a/src/deepness/processing/map_processor/map_processor_detection.py +++ b/src/deepness/processing/map_processor/map_processor_detection.py @@ -2,16 +2,16 @@ from typing import List +import cv2 import numpy as np -from qgis.core import QgsVectorLayer, QgsProject, QgsGeometry, QgsFeature +from qgis.core import QgsFeature, QgsGeometry, QgsProject, QgsVectorLayer from deepness.common.processing_parameters.detection_parameters import DetectionParameters, DetectorType from deepness.processing import processing_utils -from deepness.processing.map_processor.map_processing_result import MapProcessingResultCanceled, \ - MapProcessingResultSuccess, MapProcessingResult +from deepness.processing.map_processor.map_processing_result import (MapProcessingResult, MapProcessingResultCanceled, + MapProcessingResultSuccess) from deepness.processing.map_processor.map_processor_with_model import MapProcessorWithModel -from deepness.processing.models.detector import Detection -from deepness.processing.models.detector import Detector +from deepness.processing.models.detector import Detection, Detector from deepness.processing.tile_params import TileParams @@ -119,19 +119,38 @@ def _create_vlayer_for_output_bounding_boxes(self, bounding_boxes: List[Detectio features = [] for det in filtered_bounding_boxes: - bbox_corners_pixels = det.bbox.get_4_corners() - bbox_corners_crs = processing_utils.transform_points_list_xy_to_target_crs( - points=bbox_corners_pixels, - extent=self.extended_extent, - rlayer_units_per_pixel=self.rlayer_units_per_pixel, - ) - feature = QgsFeature() - polygon_xy_vec_vec = [ - bbox_corners_crs - ] - geometry = QgsGeometry.fromPolygonXY(polygon_xy_vec_vec) - feature.setGeometry(geometry) - features.append(feature) + if det.mask is None: + bbox_corners_pixels = det.bbox.get_4_corners() + bbox_corners_crs = processing_utils.transform_points_list_xy_to_target_crs( + points=bbox_corners_pixels, + extent=self.extended_extent, + rlayer_units_per_pixel=self.rlayer_units_per_pixel, + ) + feature = QgsFeature() + polygon_xy_vec_vec = [ + bbox_corners_crs + ] + geometry = QgsGeometry.fromPolygonXY(polygon_xy_vec_vec) + feature.setGeometry(geometry) + features.append(feature) + else: + contours, hierarchy = cv2.findContours(det.mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + contours = processing_utils.transform_contours_yx_pixels_to_target_crs( + contours=contours, + extent=self.base_extent, + rlayer_units_per_pixel=self.rlayer_units_per_pixel) + features = [] + + if len(contours): + processing_utils.convert_cv_contours_to_features( + features=features, + cv_contours=contours, + hierarchy=hierarchy[0], + is_hole=False, + current_holes=[], + current_contour_index=0) + else: + pass # just nothing, we already have an empty list of features vlayer = QgsVectorLayer("multipolygon", self.model.get_channel_name(channel_id), "memory") vlayer.setCrs(self.rlayer.crs()) diff --git a/src/deepness/processing/models/detector.py b/src/deepness/processing/models/detector.py index e36f6d7..93dcaad 100644 --- a/src/deepness/processing/models/detector.py +++ b/src/deepness/processing/models/detector.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from typing import List +import cv2 import numpy as np from deepness.common.processing_parameters.detection_parameters import DetectorType @@ -134,9 +135,11 @@ def get_number_of_output_channels(self): if model_type_params.skipped_objectness_probability: return self.outputs_layers[0].shape[shape_index] - 4 return self.outputs_layers[0].shape[shape_index] - 4 - 1 # shape - 4 bboxes - 1 conf + elif len(self.outputs_layers) == 2 and self.model_type == DetectorType.YOLO_ULTRALYTICS_SEGMENTATION: + return self.outputs_layers[0].shape[shape_index] - 4 - self.outputs_layers[1].shape[1] else: raise NotImplementedError("Model with multiple output layer is not supported! Use only one output layer.") - + def preprocessing(self, image: np.ndarray): """Preprocess image before inference @@ -184,20 +187,22 @@ def postprocessing(self, model_output): "Model type is not set for model. Use self.set_model_type_param" ) - model_output = model_output[0][0] - if self.model_type == DetectorType.YOLO_v5_v7_DEFAULT: - boxes, conf, classes = self._postprocessing_YOLO_v5_v7_DEFAULT(model_output) + boxes, conf, classes = self._postprocessing_YOLO_v5_v7_DEFAULT(model_output[0][0]) elif self.model_type == DetectorType.YOLO_v6: - boxes, conf, classes = self._postprocessing_YOLO_v6(model_output) + boxes, conf, classes = self._postprocessing_YOLO_v6(model_output[0][0]) elif self.model_type == DetectorType.YOLO_ULTRALYTICS: - boxes, conf, classes = self._postprocessing_YOLO_ULTRALYTICS(model_output) + boxes, conf, classes = self._postprocessing_YOLO_ULTRALYTICS(model_output[0][0]) + elif self.model_type == DetectorType.YOLO_ULTRALYTICS_SEGMENTATION: + boxes, conf, classes, masks = self._postprocessing_YOLO_ULTRALYTICS_SEGMENTATION(model_output) else: raise NotImplementedError(f"Model type not implemented! ('{self.model_type}')") detections = [] + + masks = masks if masks is not None else [None] * len(boxes) - for b, c, cl in zip(boxes, conf, classes): + for b, c, cl, m in zip(boxes, conf, classes, masks): det = Detection( bbox=BoundingBox( x_min=b[0], @@ -205,7 +210,8 @@ def postprocessing(self, model_output): y_min=b[1], y_max=b[3]), conf=c, - clss=cl + clss=cl, + mask=m, ) detections.append(det) @@ -288,7 +294,78 @@ def _postprocessing_YOLO_ULTRALYTICS(self, model_output): return boxes, conf, classes + def _postprocessing_YOLO_ULTRALYTICS_SEGMENTATION(self, model_output): + detections = model_output[0][0] + protos = model_output[1][0] + + detections = np.transpose(detections, (1, 0)) + + number_of_class = self.get_number_of_output_channels() + mask_start_index = 4 + number_of_class + + outputs_filtered = np.array( + list(filter(lambda x: np.max(x[4:4+number_of_class]) >= self.confidence, detections)) + ) + + if len(outputs_filtered.shape) < 2: + return [], [], [], [] + + probabilities = np.max(outputs_filtered[:, 4:4+number_of_class], axis=1) + outputs_x1y1x2y2 = self.xywh2xyxy(outputs_filtered) + + pick_indxs = self.non_max_suppression_fast( + outputs_x1y1x2y2, + probs=probabilities, + iou_threshold=self.iou_threshold) + + outputs_nms = outputs_x1y1x2y2[pick_indxs] + + boxes = np.array(outputs_nms[:, :4], dtype=int) + conf = np.max(outputs_nms[:, 4:4+number_of_class], axis=1) + classes = np.argmax(outputs_nms[:, 4:4+number_of_class], axis=1) + masks_in = np.array(outputs_nms[:, mask_start_index:], dtype=float) + + masks = self.process_mask(protos, masks_in, boxes) + + return boxes, conf, classes, masks + + # based on https://github.com/ultralytics/ultralytics/blob/main/ultralytics/utils/ops.py#L638C1-L638C67 + def process_mask(self, protos, masks_in, bboxes): + c, mh, mw = protos.shape # CHW + ih, iw = self.input_shape[2:] + + masks = self.sigmoid(np.matmul(masks_in, protos.astype(float).reshape(c, -1))).reshape(-1, mh, mw) + + downsampled_bboxes = bboxes.copy().astype(float) + downsampled_bboxes[:, 0] *= mw / iw + downsampled_bboxes[:, 2] *= mw / iw + downsampled_bboxes[:, 3] *= mh / ih + downsampled_bboxes[:, 1] *= mh / ih + + masks = self.crop_mask(masks, downsampled_bboxes) + scaled_masks = np.zeros((len(masks), ih, iw)) + + for i in range(len(masks)): + scaled_masks[i] = cv2.resize(masks[i], (iw, ih), interpolation=cv2.INTER_LINEAR) + + masks = np.uint8(scaled_masks >= 0.5) + + return masks + + @staticmethod + def sigmoid(x): + return 1 / (1 + np.exp(-x)) + + @staticmethod + # based on https://github.com/ultralytics/ultralytics/blob/main/ultralytics/utils/ops.py#L598C1-L614C65 + def crop_mask(masks, boxes): + n, h, w = masks.shape + x1, y1, x2, y2 = np.split(boxes[:, :, None], 4, axis=1) # x1 shape(n,1,1) + r = np.arange(w, dtype=x1.dtype)[None, None, :] # rows shape(1,1,w) + c = np.arange(h, dtype=x1.dtype)[None, :, None] # cols shape(1,h,1) + + return masks * ((r >= x1) * (r < x2) * (c >= y1) * (c < y2)) @staticmethod def xywh2xyxy(x: np.ndarray) -> np.ndarray: @@ -388,11 +465,12 @@ def non_max_suppression_fast(boxes: np.ndarray, probs: np.ndarray, iou_threshold def check_loaded_model_outputs(self): """Check if model outputs are valid. Valid model are: - - has 1 output layer + - has 1 or 2 outputs layer - output layer shape length is 3 - batch size is 1 """ - if len(self.outputs_layers) == 1: + + if len(self.outputs_layers) == 1 or len(self.outputs_layers) == 2: shape = self.outputs_layers[0].shape if len(shape) != 3: diff --git a/test/manual_test_map_processor_instance_yolo_ultralytics.py b/test/manual_test_map_processor_instance_yolo_ultralytics.py index b5e0aa5..e1fcd08 100644 --- a/test/manual_test_map_processor_instance_yolo_ultralytics.py +++ b/test/manual_test_map_processor_instance_yolo_ultralytics.py @@ -52,9 +52,9 @@ def test_map_processor_detection_yolo_ultralytics(): map_processor.run() - assert len(map_processor.get_all_detections()) == 17 + assert len(map_processor.get_all_detections()) == 2 if __name__ == '__main__': - test_map_processor_detection_strange_format() + test_map_processor_detection_yolo_ultralytics() print('Done') From 2343abfc68830b2b4315d846016a102f8198fbd4 Mon Sep 17 00:00:00 2001 From: Bartosz Date: Tue, 26 Sep 2023 15:00:10 +0200 Subject: [PATCH 12/16] Fix bug --- src/deepness/processing/map_processor/map_processor_detection.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/deepness/processing/map_processor/map_processor_detection.py b/src/deepness/processing/map_processor/map_processor_detection.py index 074009b..f742451 100644 --- a/src/deepness/processing/map_processor/map_processor_detection.py +++ b/src/deepness/processing/map_processor/map_processor_detection.py @@ -139,7 +139,6 @@ def _create_vlayer_for_output_bounding_boxes(self, bounding_boxes: List[Detectio contours=contours, extent=self.base_extent, rlayer_units_per_pixel=self.rlayer_units_per_pixel) - features = [] if len(contours): processing_utils.convert_cv_contours_to_features( From 79d996e70912802471ddc1e76ca70766a12e4096 Mon Sep 17 00:00:00 2001 From: Bartosz Date: Tue, 26 Sep 2023 15:18:52 +0200 Subject: [PATCH 13/16] Fix broken test --- src/deepness/processing/models/detector.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/deepness/processing/models/detector.py b/src/deepness/processing/models/detector.py index 93dcaad..ce3f36c 100644 --- a/src/deepness/processing/models/detector.py +++ b/src/deepness/processing/models/detector.py @@ -187,6 +187,8 @@ def postprocessing(self, model_output): "Model type is not set for model. Use self.set_model_type_param" ) + masks = None + if self.model_type == DetectorType.YOLO_v5_v7_DEFAULT: boxes, conf, classes = self._postprocessing_YOLO_v5_v7_DEFAULT(model_output[0][0]) elif self.model_type == DetectorType.YOLO_v6: From 0441b229c0884605107948214f44c76fcf68e6a3 Mon Sep 17 00:00:00 2001 From: Bartosz Date: Tue, 26 Sep 2023 15:35:19 +0200 Subject: [PATCH 14/16] Make code compatible with python 3.8 --- src/deepness/processing/models/detector.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/deepness/processing/models/detector.py b/src/deepness/processing/models/detector.py index ce3f36c..fe9b18c 100644 --- a/src/deepness/processing/models/detector.py +++ b/src/deepness/processing/models/detector.py @@ -1,7 +1,7 @@ """ Module including the class for the object detection task and related functions """ from dataclasses import dataclass -from typing import List +from typing import List, Optional import cv2 import numpy as np @@ -31,7 +31,7 @@ class of the detected object """float: confidence of the detection""" clss: int """int: class of the detected object""" - mask: np.ndarray | None = None + mask: Optional[np.ndarray] = None """np.ndarray: mask of the detected object""" def convert_to_global(self, offset_x: int, offset_y: int): From b5a93aa23878c13a6ddefca31c6f2be30eae0e69 Mon Sep 17 00:00:00 2001 From: Bartosz Date: Tue, 26 Sep 2023 22:18:59 +0200 Subject: [PATCH 15/16] Fix mask drawing function --- .../map_processor/map_processor_detection.py | 47 ++++++++++++------- src/deepness/processing/models/detector.py | 6 ++- src/deepness/processing/processing_utils.py | 14 ++---- 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/deepness/processing/map_processor/map_processor_detection.py b/src/deepness/processing/map_processor/map_processor_detection.py index f742451..6353750 100644 --- a/src/deepness/processing/map_processor/map_processor_detection.py +++ b/src/deepness/processing/map_processor/map_processor_detection.py @@ -1,5 +1,6 @@ """ This file implements map processing for detection model """ +from itertools import count from typing import List import cv2 @@ -134,23 +135,35 @@ def _create_vlayer_for_output_bounding_boxes(self, bounding_boxes: List[Detectio feature.setGeometry(geometry) features.append(feature) else: - contours, hierarchy = cv2.findContours(det.mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - contours = processing_utils.transform_contours_yx_pixels_to_target_crs( - contours=contours, - extent=self.base_extent, - rlayer_units_per_pixel=self.rlayer_units_per_pixel) - - if len(contours): - processing_utils.convert_cv_contours_to_features( - features=features, - cv_contours=contours, - hierarchy=hierarchy[0], - is_hole=False, - current_holes=[], - current_contour_index=0) - else: - pass # just nothing, we already have an empty list of features - + contours, _ = cv2.findContours(det.mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + contours = sorted(contours, key=cv2.contourArea, reverse=True) + + x_offset, y_offset = det.mask_offsets + + if len(contours) > 0: + countur = contours[0] + + corners = [] + for point in countur: + corners.append(int(point[0][0]) + x_offset) + corners.append(int(point[0][1]) + y_offset) + + mask_corners_pixels = cv2.convexHull(np.array(corners).reshape((-1, 2))).squeeze() + + mask_corners_crs = processing_utils.transform_points_list_xy_to_target_crs( + points=mask_corners_pixels, + extent=self.extended_extent, + rlayer_units_per_pixel=self.rlayer_units_per_pixel, + ) + + feature = QgsFeature() + polygon_xy_vec_vec = [ + mask_corners_crs + ] + geometry = QgsGeometry.fromPolygonXY(polygon_xy_vec_vec) + feature.setGeometry(geometry) + features.append(feature) + vlayer = QgsVectorLayer("multipolygon", self.model.get_channel_name(channel_id), "memory") vlayer.setCrs(self.rlayer.crs()) prov = vlayer.dataProvider() diff --git a/src/deepness/processing/models/detector.py b/src/deepness/processing/models/detector.py index fe9b18c..a09313a 100644 --- a/src/deepness/processing/models/detector.py +++ b/src/deepness/processing/models/detector.py @@ -1,7 +1,7 @@ """ Module including the class for the object detection task and related functions """ from dataclasses import dataclass -from typing import List, Optional +from typing import List, Optional, Tuple import cv2 import numpy as np @@ -33,6 +33,7 @@ class of the detected object """int: class of the detected object""" mask: Optional[np.ndarray] = None """np.ndarray: mask of the detected object""" + mask_offsets: Optional[Tuple[int, int]] = None def convert_to_global(self, offset_x: int, offset_y: int): """Apply (x,y) offset to bounding box coordinates @@ -45,6 +46,9 @@ def convert_to_global(self, offset_x: int, offset_y: int): _description_ """ self.bbox.apply_offset(offset_x=offset_x, offset_y=offset_y) + + if self.mask is not None: + self.mask_offsets = (offset_x, offset_y) def get_bbox_xyxy(self) -> np.ndarray: """Convert stored bounding box into x1y1x2y2 format diff --git a/src/deepness/processing/processing_utils.py b/src/deepness/processing/processing_utils.py index 9a0d26e..6d750cf 100644 --- a/src/deepness/processing/processing_utils.py +++ b/src/deepness/processing/processing_utils.py @@ -4,22 +4,17 @@ import logging from dataclasses import dataclass -from typing import Optional, List, Tuple +from typing import List, Optional, Tuple import numpy as np -from qgis.core import Qgis -from qgis.core import QgsFeature, QgsGeometry, QgsPointXY -from qgis.core import QgsRasterLayer, QgsCoordinateTransform -from qgis.core import QgsRectangle -from qgis.core import QgsUnitTypes -from qgis.core import QgsWkbTypes +from qgis.core import (Qgis, QgsCoordinateTransform, QgsFeature, QgsGeometry, QgsPointXY, QgsRasterLayer, QgsRectangle, + QgsUnitTypes, QgsWkbTypes) from deepness.common.defines import IS_DEBUG from deepness.common.lazy_package_loader import LazyPackageLoader from deepness.common.processing_parameters.map_processing_parameters import MapProcessingParameters from deepness.common.processing_parameters.segmentation_parameters import SegmentationParameters - cv2 = LazyPackageLoader('cv2') @@ -391,9 +386,6 @@ def get_4_corners(self) -> List[Tuple]: (self.x_max, self.y_min), ] - roi_slice = np.s_[self.y_min:self.y_max + 1, self.x_min:self.x_max + 1] - return roi_slice - def transform_polygon_with_rings_epsg_to_extended_xy_pixels( polygons: List[List[QgsPointXY]], From b6a7cf003ef24080f077dce6361541edccb5db88 Mon Sep 17 00:00:00 2001 From: Bartosz Date: Wed, 27 Sep 2023 14:00:02 +0200 Subject: [PATCH 16/16] Add UAVVaste model --- .../source/creators/creators_description_classes.rst | 10 +++++++++- docs/source/main/model_zoo/MODEL_ZOO.md | 1 + .../map_processor/map_processor_detection.py | 12 ++++++------ 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/source/creators/creators_description_classes.rst b/docs/source/creators/creators_description_classes.rst index 440e5a5..e4cdfd8 100644 --- a/docs/source/creators/creators_description_classes.rst +++ b/docs/source/creators/creators_description_classes.rst @@ -43,7 +43,7 @@ Detection models allow to solve problem of objects detection, that is finding an Example application is detection of oil and water tanks on satellite images. The detection model output is list of bounding boxes, with assigned class and confidence value. This information is not really standardized between different model architectures. -Currently plugin supports :code:`YOLOv5` and :code:`YOLOv7` output types. +Currently plugin supports :code:`YOLOv5`, :code:`YOLOv7` and :code:`ULTRALYTICS` output types. For each object class, a separate vector layer can be created, with information saved as rectangle polygons (so the output can be potentially easily exported to a text). @@ -65,6 +65,14 @@ Usually, only one output map (class) is used, as the model usually tries to solv Output report contains statistics for each class, that is average, min, max and standard deviation of values. +===================== +SuperResolution Model +===================== +SuperResolution models allow to solve problem of increasing the resolution of the image. The model takes a low resolution image as input and outputs a high resolution image. + +Example application is increasing the resolution of satellite images. + +The superresolution model output is also an image, with same dimension as the input tile, but with higher resolution (GDS). ================ Extending Models diff --git a/docs/source/main/model_zoo/MODEL_ZOO.md b/docs/source/main/model_zoo/MODEL_ZOO.md index d2342e2..7500e21 100644 --- a/docs/source/main/model_zoo/MODEL_ZOO.md +++ b/docs/source/main/model_zoo/MODEL_ZOO.md @@ -28,6 +28,7 @@ The [Model ZOO](https://chmura.put.poznan.pl/s/2pJk4izRurzQwu3) is a collection | [Airbus Planes Detection](https://chmura.put.poznan.pl/s/bBIJ5FDPgyQvJ49) | 256 | 70 | YOLOv7 tiny model for object detection on satellite images. Based on the [Airbus Aircraft Detection dataset](https://www.kaggle.com/datasets/airbusgeo/airbus-aircrafts-sample-dataset). | [Image](https://chmura.put.poznan.pl/s/VfLmcWhvWf0UJfI) | | [Airbus Oil Storage Detection](https://chmura.put.poznan.pl/s/gMundpKsYUC7sNb) | 512 | 150 | YOLOv5-m model for object detection on satellite images. Based on the [Airbus Oil Storage Detection dataset](https://www.kaggle.com/datasets/airbusgeo/airbus-oil-storage-detection-dataset). | [Image](https://chmura.put.poznan.pl/s/T3pwaKlbFDBB2C3) | | [Aerial Cars Detection](https://chmura.put.poznan.pl/s/vgOeUN4H4tGsrGm) | 640 | 10 | YOLOv7-m model for cars detection on aerial images. Based on the [ITCVD](https://arxiv.org/pdf/1801.07339.pdf). | [Image](https://chmura.put.poznan.pl/s/cPzw1mkXlprSUIJ) | +| [UAVVaste Instance Segmentation](https://chmura.put.poznan.pl/s/v99rDlSPbyNpOCH) | 640 | 0.5 | YOLOv8-L Instance Segmentation model for litter detection on high-quality UAV images. Based on the [UAVVaste dataset](https://github.com/PUTvision/UAVVaste). | [Image](https://chmura.put.poznan.pl/s/KFQTlS2qtVnaG0q) | ## Super Resolution Models | Model | Input size | CM/PX | Scale Factor |Description | Example image | diff --git a/src/deepness/processing/map_processor/map_processor_detection.py b/src/deepness/processing/map_processor/map_processor_detection.py index 6353750..340e26e 100644 --- a/src/deepness/processing/map_processor/map_processor_detection.py +++ b/src/deepness/processing/map_processor/map_processor_detection.py @@ -137,9 +137,9 @@ def _create_vlayer_for_output_bounding_boxes(self, bounding_boxes: List[Detectio else: contours, _ = cv2.findContours(det.mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) contours = sorted(contours, key=cv2.contourArea, reverse=True) - + x_offset, y_offset = det.mask_offsets - + if len(contours) > 0: countur = contours[0] @@ -147,15 +147,15 @@ def _create_vlayer_for_output_bounding_boxes(self, bounding_boxes: List[Detectio for point in countur: corners.append(int(point[0][0]) + x_offset) corners.append(int(point[0][1]) + y_offset) - + mask_corners_pixels = cv2.convexHull(np.array(corners).reshape((-1, 2))).squeeze() - + mask_corners_crs = processing_utils.transform_points_list_xy_to_target_crs( points=mask_corners_pixels, extent=self.extended_extent, rlayer_units_per_pixel=self.rlayer_units_per_pixel, ) - + feature = QgsFeature() polygon_xy_vec_vec = [ mask_corners_crs @@ -163,7 +163,7 @@ def _create_vlayer_for_output_bounding_boxes(self, bounding_boxes: List[Detectio geometry = QgsGeometry.fromPolygonXY(polygon_xy_vec_vec) feature.setGeometry(geometry) features.append(feature) - + vlayer = QgsVectorLayer("multipolygon", self.model.get_channel_name(channel_id), "memory") vlayer.setCrs(self.rlayer.crs()) prov = vlayer.dataProvider()