diff --git a/.vscode/settings.json b/.vscode/settings.json index 1231491..d0cc6f7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,7 +8,7 @@ "python.analysis.autoImportUserSymbols": true, "[python]": { "editor.codeActionsOnSave": { - "source.organizeImports": true + "source.organizeImports": "explicit" } }, "isort.args": [ @@ -41,4 +41,12 @@ "python.testing.pytestArgs": [ "." ], + "autopep8.args": [ + "--max-line-length", + "9999", + "--experimental", + ], + "flake8.args": [ + "--max-line-length=9999" + ], } diff --git a/docs/source/creators/creators_description_classes.rst b/docs/source/creators/creators_description_classes.rst index e4cdfd8..c9e84ad 100644 --- a/docs/source/creators/creators_description_classes.rst +++ b/docs/source/creators/creators_description_classes.rst @@ -14,7 +14,7 @@ Once the processing of ortophoto is finished, a report with model-specific infor Common rules for models and processing: * Model needs to be in ONNX format, which contains both the network architecture and weights. * All model classes process the data in chunks called 'tiles', that is a small part of the entire ortophoto - tiles size and overlap is configurable. - * Every model should have one input of size :code:`[BATCH_SIZE, CHANNELS, SIZE_PX, SIZE_PX]`. :code:`BATCH_SIZE` can be 1. + * Every model should have one input of size :code:`[BATCH_SIZE, CHANNELS, SIZE_PX, SIZE_PX]`. :code:`BATCH_SIZE` can be 1 or dynamic. * Size of processed tiles (in pixels) is model defined, but needs to be equal in x and y axes, so that the tiles can be square. * If the processed tile needs to be padded (e.g. on otophoto borders) it will be padded with 0 values. * Input image data - only uint8_t value for each pixel channel is supported @@ -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`, :code:`YOLOv7` and :code:`ULTRALYTICS` output types. +Currently plugin supports :code:`YOLOv5`, :code:`YOLOv7` and :code:`ULTRALYTICS` output types. Detection model also supports the instance segmentation output type from :code:`ULTRALYTICS`. 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). diff --git a/docs/source/creators/creators_example_onnx_model.rst b/docs/source/creators/creators_example_onnx_model.rst index 95f94d9..10df3b2 100644 --- a/docs/source/creators/creators_example_onnx_model.rst +++ b/docs/source/creators/creators_example_onnx_model.rst @@ -31,7 +31,7 @@ Steps based on `EXPORTING A MODEL FROM PYTORCH TO ONNX AND RUNNING IT USING ONNX x = torch.rand(1, INP_CHANNEL, INP_HEIGHT, INP_WIDTH) # eg. torch.rand([1, 3, 256, 256]) _ = model(x) -* Step 3. Call export function +* Step 3a. Call export function with static batch_size=1: .. code-block:: @@ -44,6 +44,20 @@ Steps based on `EXPORTING A MODEL FROM PYTORCH TO ONNX AND RUNNING IT USING ONNX output_names=['output'], do_constant_folding=False) +* Step 3b. Call export function with dynamic batch_size: + + .. code-block:: + + torch.onnx.export(model, + x, # model input + 'model.onnx', # where to save the model + export_params=True, + opset_version=15, + input_names=['input'], + output_names=['output'], + dynamic_axes={'input': {0: 'batch_size'}, # variable lenght axes + 'output': {0: 'batch_size'}}) + ================ Tensorflow/Keras ================ @@ -63,3 +77,9 @@ Steps based on the `tensorflow-onnx `_ .. code-block:: python -m tf2onnx.convert --saved-model YOUR_MODEL_CHECKPOINT_PATH --output model.onnx --opset 15 + +=============================================== +Update ONNX model to support dynamic batch size +=============================================== + +To convert model to support dynamic batch size, you need to update :code:`model.onnx` file. You can do it manually using `this ` script. Please note that the script is not perfect and may not work for all models. diff --git a/docs/source/images/ui_processing_params.webp b/docs/source/images/ui_processing_params.webp index 8ce52c6..c3b8bf9 100644 Binary files a/docs/source/images/ui_processing_params.webp and b/docs/source/images/ui_processing_params.webp differ diff --git a/docs/source/main/main_ui_explanation.rst b/docs/source/main/main_ui_explanation.rst index 3812652..d733aa9 100644 --- a/docs/source/main/main_ui_explanation.rst +++ b/docs/source/main/main_ui_explanation.rst @@ -56,11 +56,15 @@ Processing parameters .. image:: ../images/ui_processing_params.webp +These options may be a fixed value for some models. + **Resolution** - Size of the images passed to the model in pixels. Usually needs to be the same as the one used during training. **Tile size** - Defines the processing resolution (in px/cm) of the Input layer Determines the resolution of images fed into the model, allowing to scale of the input images. Should be similar to the resolution used to train the model. -**Tiles overlap** - Defines how much tiles should overlap with their neighbors during processing. Especially required for a model which introduces distortions on the edges of images, so that they can be removed in postprocessing. +**Batch size** - Number of images passed to the model at once. + +**Tiles overlap** - Defines how much tiles should overlap with their neighbors during processing. Especially required for a model which introduces distortions on the edges of images, so that they can be removed in postprocessing. Can be defined in percent of tile size or in pixels. .. image:: ../images/ui_segment_params.webp diff --git a/examples/deeplabv3_segmentation_landcover/deeplabv3_landcover_4c_batched.onnx b/examples/deeplabv3_segmentation_landcover/deeplabv3_landcover_4c_batched.onnx new file mode 100644 index 0000000..eb775dc Binary files /dev/null and b/examples/deeplabv3_segmentation_landcover/deeplabv3_landcover_4c_batched.onnx differ diff --git a/src/deepness/common/config_entry_key.py b/src/deepness/common/config_entry_key.py index 3975392..89dc171 100644 --- a/src/deepness/common/config_entry_key.py +++ b/src/deepness/common/config_entry_key.py @@ -20,6 +20,8 @@ class ConfigEntryKey(enum.Enum): PROCESSED_AREA_TYPE = enum.auto(), '' # string of ProcessedAreaType, e.g. "ProcessedAreaType.VISIBLE_PART.value" MODEL_TYPE = enum.auto(), '' # string of ModelType enum, e.g. "ModelType.SEGMENTATION.value" PREPROCESSING_RESOLUTION = enum.auto(), 3.0 + MODEL_BATCH_SIZE = enum.auto(), 1 + PROCESS_LOCAL_CACHE = enum.auto(), False PREPROCESSING_TILES_OVERLAP = enum.auto(), 15 SEGMENTATION_PROBABILITY_THRESHOLD_ENABLED = enum.auto(), True diff --git a/src/deepness/common/processing_parameters/map_processing_parameters.py b/src/deepness/common/processing_parameters/map_processing_parameters.py index 75d80f3..8f2cf23 100644 --- a/src/deepness/common/processing_parameters/map_processing_parameters.py +++ b/src/deepness/common/processing_parameters/map_processing_parameters.py @@ -37,6 +37,8 @@ class MapProcessingParameters: resolution_cm_per_px: float # image resolution to used during processing processed_area_type: ProcessedAreaType # whether to perform operation on the entire field or part tile_size_px: int # Tile size for processing (model input size) + batch_size: int # Batch size for processing + local_cache: bool # Whether to use local cache for tiles (on disk, /tmp directory) input_layer_id: str # raster layer to process mask_layer_id: Optional[str] # Processing of masked layer - if processed_area_type is FROM_POLYGONS diff --git a/src/deepness/common/temp_files_handler.py b/src/deepness/common/temp_files_handler.py new file mode 100644 index 0000000..3962e23 --- /dev/null +++ b/src/deepness/common/temp_files_handler.py @@ -0,0 +1,19 @@ +import os.path as path +import shutil +from tempfile import mkdtemp + + +class TempFilesHandler: + def __init__(self) -> None: + self._temp_dir = mkdtemp() + + print(f'Created temp dir: {self._temp_dir} for processing') + + def get_results_img_path(self): + return path.join(self._temp_dir, 'results.dat') + + def get_area_mask_img_path(self): + return path.join(self._temp_dir, 'area_mask.dat') + + def __del__(self): + shutil.rmtree(self._temp_dir) diff --git a/src/deepness/deepness_dockwidget.py b/src/deepness/deepness_dockwidget.py index 430853f..3044d9c 100644 --- a/src/deepness/deepness_dockwidget.py +++ b/src/deepness/deepness_dockwidget.py @@ -92,8 +92,9 @@ def _load_ui_from_config(self): # needs to be loaded after the model is set up self.comboBox_outputFormatClassNumber.setCurrentIndex(ConfigEntryKey.MODEL_OUTPUT_FORMAT_CLASS_NUMBER.get()) - self.doubleSpinBox_resolution_cm_px.setValue(ConfigEntryKey.PREPROCESSING_RESOLUTION.get()) + self.spinBox_batchSize.setValue(ConfigEntryKey.MODEL_BATCH_SIZE.get()) + self.checkBox_local_cache.setChecked(ConfigEntryKey.PROCESS_LOCAL_CACHE.get()) self.spinBox_processingTileOverlapPercentage.setValue(ConfigEntryKey.PREPROCESSING_TILES_OVERLAP.get()) self.doubleSpinBox_probabilityThreshold.setValue( @@ -129,6 +130,8 @@ def _save_ui_to_config(self): ConfigEntryKey.MODEL_OUTPUT_FORMAT_CLASS_NUMBER.set(self.comboBox_outputFormatClassNumber.currentIndex()) ConfigEntryKey.PREPROCESSING_RESOLUTION.set(self.doubleSpinBox_resolution_cm_px.value()) + ConfigEntryKey.MODEL_BATCH_SIZE.set(self.spinBox_batchSize.value()) + ConfigEntryKey.PROCESS_LOCAL_CACHE.set(self.checkBox_local_cache.isChecked()) ConfigEntryKey.PREPROCESSING_TILES_OVERLAP.set(self.spinBox_processingTileOverlapPercentage.value()) ConfigEntryKey.SEGMENTATION_PROBABILITY_THRESHOLD_ENABLED.set( @@ -225,10 +228,10 @@ def _model_type_changed(self): else: raise Exception(f"Unsupported model type ({model_type})!") - self.mGroupBox_segmentationParameters.setEnabled(segmentation_enabled) - self.mGroupBox_detectionParameters.setEnabled(detection_enabled) - self.mGroupBox_regressionParameters.setEnabled(regression_enabled) - self.mGroupBox_superresolutionParameters.setEnabled(superresolution_enabled) + self.mGroupBox_segmentationParameters.setVisible(segmentation_enabled) + self.mGroupBox_detectionParameters.setVisible(detection_enabled) + self.mGroupBox_regressionParameters.setVisible(regression_enabled) + self.mGroupBox_superresolutionParameters.setVisible(superresolution_enabled) # Disable output format options for super-resolution models. self.mGroupBox_6.setEnabled(not superresolution_enabled) @@ -272,6 +275,13 @@ def _load_default_model_parameters(self): value = self._model.get_metadata_resolution() if value is not None: self.doubleSpinBox_resolution_cm_px.setValue(value) + + value = self._model.get_model_batch_size() + if value is not None: + self.spinBox_batchSize.setValue(value) + self.spinBox_batchSize.setEnabled(False) + else: + self.spinBox_batchSize.setEnabled(True) value = self._model.get_metadata_tile_size() if value is not None: @@ -355,10 +365,18 @@ def _load_model_and_display_info(self, abort_if_no_file_path: bool = False): input_0_shape = self._model.get_input_shape() txt += f'Input shape: {input_0_shape} = [BATCH_SIZE * CHANNELS * SIZE * SIZE]' input_size_px = input_0_shape[-1] + batch_size = self._model.get_model_batch_size() # TODO idk how variable input will be handled self.spinBox_tileSize_px.setValue(input_size_px) self.spinBox_tileSize_px.setEnabled(False) + + if batch_size is not None: + self.spinBox_batchSize.setValue(batch_size) + self.spinBox_batchSize.setEnabled(False) + else: + self.spinBox_batchSize.setEnabled(True) + self._input_channels_mapping_widget.set_model(self._model) # super resolution @@ -375,6 +393,7 @@ def _load_model_and_display_info(self, abort_if_no_file_path: bool = False): "Model may be not usable." logging.exception(txt) self.spinBox_tileSize_px.setEnabled(True) + self.spinBox_batchSize.setEnabled(True) length_limit = 300 exception_msg = (str(e)[:length_limit] + '..') if len(str(e)) > length_limit else str(e) msg = txt + f'\n\nException: {exception_msg}' @@ -517,6 +536,8 @@ def _get_map_processing_parameters(self) -> MapProcessingParameters: params = MapProcessingParameters( resolution_cm_per_px=self.doubleSpinBox_resolution_cm_px.value(), tile_size_px=self.spinBox_tileSize_px.value(), + batch_size=self.spinBox_batchSize.value(), + local_cache=self.checkBox_local_cache.isChecked(), processed_area_type=processed_area_type, mask_layer_id=self.get_mask_layer_id(), input_layer_id=self._get_input_layer_id(), diff --git a/src/deepness/deepness_dockwidget.ui b/src/deepness/deepness_dockwidget.ui index 6748e35..0e4aaa1 100644 --- a/src/deepness/deepness_dockwidget.ui +++ b/src/deepness/deepness_dockwidget.ui @@ -26,7 +26,7 @@ 0 0 452 - 1487 + 1621 @@ -44,6 +44,7 @@ 9 + 75 true @@ -237,6 +238,7 @@ + 75 true @@ -263,31 +265,32 @@ Processing parameters - - - - <html><head/><body><p>Defines the processing resolution of the &quot;Input layer&quot;.</p><p><br/></p><p>Determines the resolution of images fed into the model, allowing to scale the input images.</p><p>Should be similar as the resolution used to train the model.</p></body></html> - - - 2 - - - 0.000000000000000 - - - 999999.000000000000000 - - - 3.000000000000000 - - - - + Tiles overlap: + + + + [px] + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + @@ -332,13 +335,6 @@ - - - - [px] - - - @@ -349,57 +345,102 @@ - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + + + + 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 + + + + + + + <html><head/><body><p>Defines the processing resolution of the &quot;Input layer&quot;.</p><p><br/></p><p>Determines the resolution of images fed into the model, allowing to scale the input images.</p><p>Should be similar as the resolution used to train the model.</p></body></html> + + + 2 + + + 0.000000000000000 + + + 999999.000000000000000 + + + 3.000000000000000 + + + + Tile size [px]: - + Resolution [cm/px]: - + + + + 75 + false + true + + - May be a fixed value -for some models + NOTE: These options may be a fixed value for some models + + + true - - - - true + + + + Batch size: + + + + - <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> + <html><head/><body><p>The size of the data batch in the model.</p><p>The size depends on the computing resources, in particular the available RAM / GPU memory.</p></body></html> + + + 1 - 99999 + 9999999 - - 512 + + + + + + <html><head/><body><p>If True, local memory caching is performed - this is helpful when large area maps are processed, but is probably slower than processing in RAM.</p><p><br/></p></body></html> + + + Process using local cache @@ -418,35 +459,22 @@ for some models Segmentation parameters - - + + - <html><head/><body><p>Postprocessing option, to remove small areas (small clusters of pixels) belonging to each class, smoothing the predictions.</p><p>The actual size (in meters) of the smoothing can be calculated as &quot;Resolution&quot; * &quot;value of this parameter&quot;.<br/>Works as application of dilate and erode operation (twice, in reverse order).<br/>Similar effect to median filter.</p></body></html> - - - 9 + <html><head/><body><p>Minimum required probability for the class to be considered as belonging to this class.</p></body></html> - - - - - - - true - + + 2 - - NOTE: Applicable only if a segmentation model is used + + 1.000000000000000 - - - - - - Apply class probability threshold: + + 0.050000000000000 - - true + + 0.500000000000000 @@ -466,33 +494,47 @@ for some models - - + + - Remove small segment - areas (dilate/erode size) [px]: + Apply class probability threshold: true - - + + - <html><head/><body><p>Minimum required probability for the class to be considered as belonging to this class.</p></body></html> + <html><head/><body><p>Postprocessing option, to remove small areas (small clusters of pixels) belonging to each class, smoothing the predictions.</p><p>The actual size (in meters) of the smoothing can be calculated as &quot;Resolution&quot; * &quot;value of this parameter&quot;.<br/>Works as application of dilate and erode operation (twice, in reverse order).<br/>Similar effect to median filter.</p></body></html> - - 2 + + 9 - - 1.000000000000000 + + + + + + + 75 + true + - - 0.050000000000000 + + NOTE: Applicable only if a segmentation model is used - - 0.500000000000000 + + + + + + Remove small segment + areas (dilate/erode size) [px]: + + + true @@ -627,6 +669,7 @@ for some models + 75 true @@ -720,6 +763,7 @@ for some models + 75 true diff --git a/src/deepness/processing/map_processor/map_processor.py b/src/deepness/processing/map_processor/map_processor.py index 4a17aff..8382ce0 100644 --- a/src/deepness/processing/map_processor/map_processor.py +++ b/src/deepness/processing/map_processor/map_processor.py @@ -1,22 +1,19 @@ """ This file implements core map processing logic """ import logging -from typing import Optional, Tuple +from typing import List, Optional, Tuple import numpy as np -from qgis.PyQt.QtCore import pyqtSignal -from qgis.core import QgsRasterLayer -from qgis.core import QgsTask -from qgis.core import QgsVectorLayer +from qgis.core import QgsRasterLayer, QgsTask, QgsVectorLayer from qgis.gui import QgsMapCanvas +from qgis.PyQt.QtCore import pyqtSignal 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, \ - ProcessedAreaType -from deepness.processing import processing_utils, extent_utils -from deepness.processing.map_processor.map_processing_result import MapProcessingResult, \ - MapProcessingResultFailed +from deepness.common.processing_parameters.map_processing_parameters import MapProcessingParameters, ProcessedAreaType +from deepness.common.temp_files_handler import TempFilesHandler +from deepness.processing import extent_utils, processing_utils +from deepness.processing.map_processor.map_processing_result import MapProcessingResult, MapProcessingResultFailed from deepness.processing.tile_params import TileParams cv2 = LazyPackageLoader('cv2') @@ -33,8 +30,10 @@ class MapProcessor(QgsTask): Work is done within QgsTask, for seamless integration with QGis GUI and logic. """ - finished_signal = pyqtSignal(MapProcessingResult) # error message if finished with error, empty string otherwise - show_img_signal = pyqtSignal(object, str) # request to show an image. Params: (image, window_name) + # error message if finished with error, empty string otherwise + finished_signal = pyqtSignal(MapProcessingResult) + # request to show an image. Params: (image, window_name) + show_img_signal = pyqtSignal(object, str) def __init__(self, rlayer: QgsRasterLayer, @@ -65,6 +64,8 @@ def __init__(self, self.rlayer_units_per_pixel = processing_utils.convert_meters_to_rlayer_units( self.rlayer, self.params.resolution_m_per_px) # number of rlayer units for one tile pixel + self.file_handler = TempFilesHandler() if self.params.local_cache else None + # extent in which the actual required area is contained, without additional extensions, rounded to rlayer grid self.base_extent = extent_utils.calculate_base_processing_extent_in_rlayer_crs( map_canvas=map_canvas, @@ -93,10 +94,8 @@ def __init__(self, # Number of tiles in x and y dimensions which will be used during processing # As we are using "extended_extent" this should divide without any rest - self.x_bins_number = round((self.img_size_x_pixels - self.params.tile_size_px) - / self.stride_px) + 1 - self.y_bins_number = round((self.img_size_y_pixels - self.params.tile_size_px) - / self.stride_px) + 1 + self.x_bins_number = round((self.img_size_x_pixels - self.params.tile_size_px) / self.stride_px) + 1 + self.y_bins_number = round((self.img_size_y_pixels - self.params.tile_size_px) / self.stride_px) + 1 # Mask determining area to process (within extended_extent coordinates) self.area_mask_img = processing_utils.create_area_mask_image( @@ -104,7 +103,8 @@ def __init__(self, rlayer=self.rlayer, extended_extent=self.extended_extent, rlayer_units_per_pixel=self.rlayer_units_per_pixel, - image_shape_yx=[self.img_size_y_pixels, self.img_size_x_pixels]) # type: Optional[np.ndarray] + image_shape_yx=(self.img_size_y_pixels, self.img_size_x_pixels), + files_handler=self.file_handler) # type: Optional[np.ndarray] def _assert_qgis_doesnt_need_reload(self): """ If the plugin is somehow invalid, it cannot compare the enums correctly @@ -158,6 +158,18 @@ def limit_extended_extent_image_to_base_extent_with_mask(self, full_img): result_img = full_img[b.y_min:b.y_max+1, b.x_min:b.x_max+1] return result_img + def _get_array_or_mmapped_array(self, final_shape_px): + if self.file_handler is not None: + full_result_img = np.memmap( + self.file_handler.get_results_img_path(), + dtype=np.uint8, + mode='w+', + shape=final_shape_px) + else: + full_result_img = np.zeros(final_shape_px, np.uint8) + + return full_result_img + def tiles_generator(self) -> Tuple[np.ndarray, TileParams]: """ Iterate over all tiles, as a Python generator function @@ -182,4 +194,24 @@ def tiles_generator(self) -> Tuple[np.ndarray, TileParams]: tile_img = processing_utils.get_tile_image( rlayer=self.rlayer, extent=tile_params.extent, params=self.params) + yield tile_img, tile_params + + def tiles_generator_batched(self) -> Tuple[np.ndarray, List[TileParams]]: + """ + Iterate over all tiles, as a Python generator function, but return them in batches + """ + + tile_img_batch, tile_params_batch = [], [] + + for tile_img, tile_params in self.tiles_generator(): + tile_img_batch.append(tile_img) + tile_params_batch.append(tile_params) + + if len(tile_img_batch) >= self.params.batch_size: + yield np.array(tile_img_batch), tile_params_batch + tile_img_batch, tile_params_batch = [], [] + + if len(tile_img_batch) > 0: + yield np.array(tile_img_batch), tile_params_batch + tile_img_batch, tile_params_batch = [], [] diff --git a/src/deepness/processing/map_processor/map_processor_detection.py b/src/deepness/processing/map_processor/map_processor_detection.py index 340e26e..211971e 100644 --- a/src/deepness/processing/map_processor/map_processor_detection.py +++ b/src/deepness/processing/map_processor/map_processor_detection.py @@ -43,12 +43,12 @@ def get_all_detections(self) -> List[Detection]: def _run(self) -> MapProcessingResult: all_bounding_boxes = [] # type: List[Detection] - for tile_img, tile_params in self.tiles_generator(): + for tile_img_batched, tile_params_batched in self.tiles_generator_batched(): if self.isCanceled(): return MapProcessingResultCanceled() - bounding_boxes_in_tile = self._process_tile(tile_img, tile_params) - all_bounding_boxes += bounding_boxes_in_tile + bounding_boxes_in_tile_batched = self._process_tile(tile_img_batched, tile_params_batched) + all_bounding_boxes += [d for det in bounding_boxes_in_tile_batched for d in det] if len(all_bounding_boxes) > 0: all_bounding_boxes_suppressed = self.apply_non_maximum_suppression(all_bounding_boxes) @@ -219,7 +219,10 @@ def convert_bounding_boxes_to_absolute_positions(bounding_boxes_relative: List[D for det in bounding_boxes_relative: det.convert_to_global(offset_x=tile_params.start_pixel_x, offset_y=tile_params.start_pixel_y) - def _process_tile(self, tile_img: np.ndarray, tile_params: TileParams) -> np.ndarray: - bounding_boxes: List[Detection] = self.model.process(tile_img) - self.convert_bounding_boxes_to_absolute_positions(bounding_boxes, tile_params) - return bounding_boxes + def _process_tile(self, tile_img: np.ndarray, tile_params_batched: List[TileParams]) -> np.ndarray: + bounding_boxes_batched: List[Detection] = self.model.process(tile_img) + + for bounding_boxes, tile_params in zip(bounding_boxes_batched, tile_params_batched): + self.convert_bounding_boxes_to_absolute_positions(bounding_boxes, tile_params) + + return bounding_boxes_batched diff --git a/src/deepness/processing/map_processor/map_processor_regression.py b/src/deepness/processing/map_processor/map_processor_regression.py index 511e429..8f65930 100644 --- a/src/deepness/processing/map_processor/map_processor_regression.py +++ b/src/deepness/processing/map_processor/map_processor_regression.py @@ -1,20 +1,17 @@ """ This file implements map processing for regression model """ -import uuid -from typing import List import os import uuid from typing import List import numpy as np from osgeo import gdal, osr -from qgis.core import QgsProject -from qgis.core import QgsRasterLayer +from qgis.core import QgsProject, QgsRasterLayer from deepness.common.misc import TMP_DIR_PATH from deepness.common.processing_parameters.regression_parameters import RegressionParameters -from deepness.processing.map_processor.map_processing_result import MapProcessingResult, \ - MapProcessingResultCanceled, MapProcessingResultSuccess +from deepness.processing.map_processor.map_processing_result import (MapProcessingResult, MapProcessingResultCanceled, + MapProcessingResultSuccess) from deepness.processing.map_processor.map_processor_with_model import MapProcessorWithModel @@ -39,20 +36,22 @@ def get_result_imgs(self): def _run(self) -> MapProcessingResult: number_of_output_channels = len(self._get_indexes_of_model_output_channels_to_create()) - final_shape_px = (self.img_size_y_pixels, self.img_size_x_pixels) + final_shape_px = (number_of_output_channels, self.img_size_y_pixels, self.img_size_x_pixels) # NOTE: consider whether we can use float16/uint16 as datatype - full_result_imgs = [np.zeros(final_shape_px, np.float32) for i in range(number_of_output_channels)] + full_result_imgs = self._get_array_or_mmapped_array(final_shape_px) - for tile_img, tile_params in self.tiles_generator(): + for tile_img_batched, tile_params_batched in self.tiles_generator_batched(): if self.isCanceled(): return MapProcessingResultCanceled() - tile_results = self._process_tile(tile_img) - for i in range(number_of_output_channels): - tile_params.set_mask_on_full_img( - tile_result=tile_results[i], - full_result_img=full_result_imgs[i]) + tile_results_batched = self._process_tile(tile_img_batched) + + for tile_results, tile_params in zip(tile_results_batched, tile_params_batched): + for i in range(number_of_output_channels): + tile_params.set_mask_on_full_img( + tile_result=tile_results[i], + full_result_img=full_result_imgs[i]) # plt.figure(); plt.imshow(full_result_img); plt.show(block=False); plt.pause(0.001) full_result_imgs = self.limit_extended_extent_images_to_base_extent_with_mask(full_imgs=full_result_imgs) diff --git a/src/deepness/processing/map_processor/map_processor_segmentation.py b/src/deepness/processing/map_processor/map_processor_segmentation.py index 1803f50..abbef65 100644 --- a/src/deepness/processing/map_processor/map_processor_segmentation.py +++ b/src/deepness/processing/map_processor/map_processor_segmentation.py @@ -1,14 +1,13 @@ """ This file implements map processing for segmentation model """ import numpy as np -from qgis.core import QgsProject -from qgis.core import QgsVectorLayer +from qgis.core import QgsProject, QgsVectorLayer from deepness.common.lazy_package_loader import LazyPackageLoader from deepness.common.processing_parameters.segmentation_parameters import SegmentationParameters from deepness.processing import processing_utils -from deepness.processing.map_processor.map_processing_result import MapProcessingResult, \ - MapProcessingResultCanceled, MapProcessingResultSuccess +from deepness.processing.map_processor.map_processing_result import (MapProcessingResult, MapProcessingResultCanceled, + MapProcessingResultSuccess) from deepness.processing.map_processor.map_processor_with_model import MapProcessorWithModel cv2 = LazyPackageLoader('cv2') @@ -39,17 +38,20 @@ def get_result_img(self): def _run(self) -> MapProcessingResult: final_shape_px = (self.img_size_y_pixels, self.img_size_x_pixels) - full_result_img = np.zeros(final_shape_px, np.uint8) - for tile_img, tile_params in self.tiles_generator(): + + full_result_img = self._get_array_or_mmapped_array(final_shape_px) + + for tile_img_batched, tile_params_batched in self.tiles_generator_batched(): if self.isCanceled(): return MapProcessingResultCanceled() # See note in the class description why are we adding/subtracting 1 here - tile_result = self._process_tile(tile_img) + 1 + tile_result_batched = self._process_tile(tile_img_batched) + 1 - tile_params.set_mask_on_full_img( - tile_result=tile_result, - full_result_img=full_result_img) + for tile_result, tile_params in zip(tile_result_batched, tile_params_batched): + tile_params.set_mask_on_full_img( + tile_result=tile_result, + full_result_img=full_result_img) blur_size = int(self.segmentation_parameters.postprocessing_dilate_erode_size // 2) * 2 + 1 # needs to be odd full_result_img = cv2.medianBlur(full_result_img, blur_size) @@ -131,13 +133,15 @@ def _create_vlayer_from_mask_for_base_extent(self, mask_img): QgsProject.instance().addMapLayer(vlayer, False) group.addLayer(vlayer) - def _process_tile(self, tile_img: np.ndarray) -> np.ndarray: + def _process_tile(self, tile_img_batched: np.ndarray) -> np.ndarray: # TODO - create proper mapping for output channels - result = self.model.process(tile_img) - + result = self.model.process(tile_img_batched) + result[result < self.segmentation_parameters.pixel_classification__probability_threshold] = 0.0 - if (result.shape[0] == 1): - result = (result != 0).astype(int)[0] + + if (result.shape[1] == 1): + result = (result != 0).astype(int)[:, 0] else: - result = np.argmax(result, axis=0) + result = np.argmax(result, axis=1) + return result diff --git a/src/deepness/processing/map_processor/map_processor_superresolution.py b/src/deepness/processing/map_processor/map_processor_superresolution.py index 3dbde49..3e27450 100644 --- a/src/deepness/processing/map_processor/map_processor_superresolution.py +++ b/src/deepness/processing/map_processor/map_processor_superresolution.py @@ -1,20 +1,17 @@ """ This file implements map processing for Super Resolution model """ -import uuid -from typing import List import os import uuid from typing import List import numpy as np from osgeo import gdal, osr -from qgis.core import QgsProject -from qgis.core import QgsRasterLayer +from qgis.core import QgsProject, QgsRasterLayer from deepness.common.misc import TMP_DIR_PATH from deepness.common.processing_parameters.superresolution_parameters import SuperresolutionParameters -from deepness.processing.map_processor.map_processing_result import MapProcessingResult, \ - MapProcessingResultCanceled, MapProcessingResultSuccess +from deepness.processing.map_processor.map_processing_result import (MapProcessingResult, MapProcessingResultCanceled, + MapProcessingResultSuccess) from deepness.processing.map_processor.map_processor_with_model import MapProcessorWithModel @@ -42,16 +39,18 @@ def _run(self) -> MapProcessingResult: final_shape_px = (int(self.img_size_y_pixels*self.superresolution_parameters.scale_factor), int(self.img_size_x_pixels*self.superresolution_parameters.scale_factor), number_of_output_channels) # NOTE: consider whether we can use float16/uint16 as datatype - full_result_imgs = np.zeros(final_shape_px, np.float32) + full_result_imgs = self._get_array_or_mmapped_array(final_shape_px) - for tile_img, tile_params in self.tiles_generator(): + for tile_img_batched, tile_params_batched in self.tiles_generator_batched(): if self.isCanceled(): return MapProcessingResultCanceled() - tile_results = self._process_tile(tile_img) - full_result_imgs[int(tile_params.start_pixel_y*self.superresolution_parameters.scale_factor):int((tile_params.start_pixel_y+tile_params.stride_px)*self.superresolution_parameters.scale_factor), - int(tile_params.start_pixel_x*self.superresolution_parameters.scale_factor):int((tile_params.start_pixel_x+tile_params.stride_px)*self.superresolution_parameters.scale_factor), - :] = tile_results.transpose(1, 2, 0) # transpose to chanels last + tile_results_batched = self._process_tile(tile_img_batched) + + for tile_results, tile_params in zip(tile_results_batched, tile_params_batched): + full_result_imgs[int(tile_params.start_pixel_y*self.superresolution_parameters.scale_factor):int((tile_params.start_pixel_y+tile_params.stride_px)*self.superresolution_parameters.scale_factor), + int(tile_params.start_pixel_x*self.superresolution_parameters.scale_factor):int((tile_params.start_pixel_x+tile_params.stride_px)*self.superresolution_parameters.scale_factor), + :] = tile_results.transpose(1, 2, 0) # transpose to chanels last # plt.figure(); plt.imshow(full_result_img); plt.show(block=False); plt.pause(0.001) full_result_imgs = self.limit_extended_extent_image_to_base_extent_with_mask(full_img=full_result_imgs) diff --git a/src/deepness/processing/map_processor/map_processor_training_data_export.py b/src/deepness/processing/map_processor/map_processor_training_data_export.py index 3f07d49..a6b29c0 100644 --- a/src/deepness/processing/map_processor/map_processor_training_data_export.py +++ b/src/deepness/processing/map_processor/map_processor_training_data_export.py @@ -6,11 +6,10 @@ from qgis.core import QgsProject from deepness.common.lazy_package_loader import LazyPackageLoader -from deepness.common.processing_parameters.training_data_export_parameters import \ - TrainingDataExportParameters +from deepness.common.processing_parameters.training_data_export_parameters import TrainingDataExportParameters from deepness.processing import processing_utils -from deepness.processing.map_processor.map_processing_result import MapProcessingResultSuccess, \ - MapProcessingResultCanceled +from deepness.processing.map_processor.map_processing_result import (MapProcessingResultCanceled, + MapProcessingResultSuccess) from deepness.processing.map_processor.map_processor import MapProcessor from deepness.processing.tile_params import TileParams @@ -48,10 +47,11 @@ def _run(self): vlayer_mask=vlayer_segmentation, extended_extent=self.extended_extent, rlayer_units_per_pixel=self.rlayer_units_per_pixel, - image_shape_yx=[self.img_size_y_pixels, self.img_size_x_pixels]) + image_shape_yx=(self.img_size_y_pixels, self.img_size_x_pixels), + files_handler=self.file_handler) number_of_written_tiles = 0 - for tile_img, tile_params in self.tiles_generator(): + for tile_img, tile_params in self.tiles_generator_batched(): if self.isCanceled(): return MapProcessingResultCanceled() diff --git a/src/deepness/processing/models/detector.py b/src/deepness/processing/models/detector.py index a09313a..39e02f2 100644 --- a/src/deepness/processing/models/detector.py +++ b/src/deepness/processing/models/detector.py @@ -143,28 +143,6 @@ def get_number_of_output_channels(self): 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 - - Parameters - ---------- - image : np.ndarray - Image to preprocess in RGB format - - Returns - ------- - np.ndarray - Preprocessed image - """ - img = image[:, :, : self.input_shape[-3]] - - input_data = img / 255.0 - input_data = np.transpose(input_data, (2, 0, 1)) - input_batch = np.expand_dims(input_data, 0) - input_batch = input_batch.astype(np.float32) - - return input_batch def postprocessing(self, model_output): """Postprocess model output @@ -179,7 +157,7 @@ def postprocessing(self, model_output): Returns ------- list - List of detections + Batch of lists of detections """ if self.confidence is None or self.iou_threshold is None: return Exception( @@ -190,38 +168,41 @@ def postprocessing(self, model_output): return Exception( "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: - 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[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, m in zip(boxes, conf, classes, masks): - det = Detection( - bbox=BoundingBox( - x_min=b[0], - x_max=b[2], - y_min=b[1], - y_max=b[3]), - conf=c, - clss=cl, - mask=m, - ) - detections.append(det) + batch_detection = [] + for i in range(len(model_output)): + masks = None + detections = [] + + if self.model_type == DetectorType.YOLO_v5_v7_DEFAULT: + boxes, conf, classes = self._postprocessing_YOLO_v5_v7_DEFAULT(model_output[0][i]) + elif self.model_type == DetectorType.YOLO_v6: + boxes, conf, classes = self._postprocessing_YOLO_v6(model_output[0][i]) + elif self.model_type == DetectorType.YOLO_ULTRALYTICS: + boxes, conf, classes = self._postprocessing_YOLO_ULTRALYTICS(model_output[0][i]) + elif self.model_type == DetectorType.YOLO_ULTRALYTICS_SEGMENTATION: + boxes, conf, classes, masks = self._postprocessing_YOLO_ULTRALYTICS_SEGMENTATION(model_output[0][i], model_output[1][i]) + else: + raise NotImplementedError(f"Model type not implemented! ('{self.model_type}')") + + masks = masks if masks is not None else [None] * len(boxes) + + for b, c, cl, m in zip(boxes, conf, classes, masks): + det = Detection( + bbox=BoundingBox( + x_min=b[0], + x_max=b[2], + y_min=b[1], + y_max=b[3]), + conf=c, + clss=cl, + mask=m, + ) + detections.append(det) + + batch_detection.append(detections) - return detections + return batch_detection def _postprocessing_YOLO_v5_v7_DEFAULT(self, model_output): outputs_filtered = np.array( @@ -300,10 +281,7 @@ 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] - + def _postprocessing_YOLO_ULTRALYTICS_SEGMENTATION(self, detections, protos): detections = np.transpose(detections, (1, 0)) number_of_class = self.get_number_of_output_channels() @@ -485,10 +463,5 @@ def check_loaded_model_outputs(self): f"Actually has: {shape}" ) - if shape[0] != 1: - raise Exception( - f"Detection model can handle only 1-Batch outputs. Has {shape}" - ) - else: raise NotImplementedError("Model with multiple output layer is not supported! Use only one output layer.") diff --git a/src/deepness/processing/models/model_base.py b/src/deepness/processing/models/model_base.py index a244e6d..627d5ef 100644 --- a/src/deepness/processing/models/model_base.py +++ b/src/deepness/processing/models/model_base.py @@ -71,6 +71,21 @@ def get_input_shape(self) -> tuple: """ return self.input_shape + def get_model_batch_size(self) -> Optional[int]: + """ Get batch size of the model + + Returns + ------- + Optional[int] | None + Batch size or None if not found (dynamic batch size) + """ + bs = self.input_shape[0] + + if isinstance(bs, str): + return None + else: + return bs + def get_input_size_in_pixels(self) -> int: """ Get number of input pixels in x and y direction (the same value) @@ -308,7 +323,7 @@ def get_number_of_channels(self) -> int: """ return self.input_shape[-3] - def process(self, img): + def process(self, tiles_batched: np.ndarray): """ Process a single tile image Parameters @@ -321,27 +336,33 @@ def process(self, img): np.ndarray Single prediction """ - input_batch = self.preprocessing(img) + input_batch = self.preprocessing(tiles_batched) model_output = self.sess.run( output_names=None, input_feed={self.input_name: input_batch}) res = self.postprocessing(model_output) return res - def preprocessing(self, img: np.ndarray) -> np.ndarray: - """ Abstract method for preprocessing + def preprocessing(self, tiles_batched: np.ndarray) -> np.ndarray: + """ Preprocess the batch of images for the model (resize, normalization, etc) Parameters ---------- - img : np.ndarray - Image to process ([TILE_SIZE x TILE_SIZE x channels], type uint8, values 0 to 255, RGB order) + image : np.ndarray + Batch of images to preprocess (N,H,W,C), RGB, 0-255 Returns ------- np.ndarray - Preprocessed image + Preprocessed batch of image (N,C,H,W), RGB, 0-1 """ - raise NotImplementedError('Base class not implemented!') + tiles_batched = tiles_batched[:, :, :, :self.input_shape[-3]] + + tiles_batched = tiles_batched.astype('float32') + tiles_batched /= 255 + tiles_batched = tiles_batched.transpose(0, 3, 1, 2) + + return tiles_batched def postprocessing(self, outs: List) -> np.ndarray: """ Abstract method for postprocessing diff --git a/src/deepness/processing/models/regressor.py b/src/deepness/processing/models/regressor.py index 5a870d9..e991a4b 100644 --- a/src/deepness/processing/models/regressor.py +++ b/src/deepness/processing/models/regressor.py @@ -23,28 +23,6 @@ def __init__(self, model_file_path: str): """ super(Regressor, self).__init__(model_file_path) - def preprocessing(self, image: np.ndarray) -> np.ndarray: - """ Preprocess the image for the model (resize, normalization, etc) - - Parameters - ---------- - image : np.ndarray - Image to preprocess (H,W,C), RGB, 0-255 - - Returns - ------- - np.ndarray - Preprocessed image (1,C,H,W), RGB, 0-1 - """ - img = image[:, :, :self.input_shape[-3]] - - input_batch = img.astype('float32') - input_batch /= 255 - input_batch = input_batch.transpose(2, 0, 1) - input_batch = np.expand_dims(input_batch, axis=0) - - return input_batch - def postprocessing(self, model_output: List) -> np.ndarray: """ Postprocess the model output. @@ -56,10 +34,10 @@ def postprocessing(self, model_output: List) -> np.ndarray: Returns ------- np.ndarray - Postprocessed mask (H,W,C), 0-1 (one output channel) + Postprocessed batch of masks (N,H,W,C), 0-1 (one output channel) """ - return model_output[0][0] + return model_output[0] def get_number_of_output_channels(self) -> int: """ Returns number of channels in the output layer @@ -101,9 +79,6 @@ def check_loaded_model_outputs(self): raise Exception(f'Regression model output should have 4 dimensions: (Batch_size, Channels, H, W). \n' f'Actually has: {shape}') - if shape[0] != 1: - raise Exception(f'Regression model can handle only 1-Batch outputs. Has {shape}') - if shape[2] != shape[3]: raise Exception(f'Regression model can handle only square outputs masks. Has: {shape}') diff --git a/src/deepness/processing/models/segmentor.py b/src/deepness/processing/models/segmentor.py index 1d567a5..9ce8252 100644 --- a/src/deepness/processing/models/segmentor.py +++ b/src/deepness/processing/models/segmentor.py @@ -23,28 +23,6 @@ def __init__(self, model_file_path: str): """ super(Segmentor, self).__init__(model_file_path) - def preprocessing(self, image: np.ndarray): - """ Preprocess the image for the model - - Parameters - ---------- - image : np.ndarray - Image to preprocess (H,W,C), RGB, 0-255 - - Returns - ------- - np.ndarray - Preprocessed image (1,C,H,W), RGB, 0-1 - """ - img = image[:, :, :self.input_shape[-3]] - - input_batch = img.astype('float32') - input_batch /= 255 - input_batch = input_batch.transpose(2, 0, 1) - input_batch = np.expand_dims(input_batch, axis=0) - - return input_batch - def postprocessing(self, model_output: List) -> np.ndarray: """ Postprocess the model output. Function returns the mask with the probability of the presence of the class in the image. @@ -57,9 +35,9 @@ def postprocessing(self, model_output: List) -> np.ndarray: Returns ------- np.ndarray - Postprocessed mask (H,W,C), 0-1 + Batch of postprocessed masks (N,H,W,C), 0-1 """ - labels = np.clip(model_output[0][0], 0, 1) + labels = np.clip(model_output[0], 0, 1) return labels @@ -109,9 +87,6 @@ def check_loaded_model_outputs(self): if len(shape) != 4: raise Exception(f'Segmentation model output should have 4 dimensions: (B,C,H,W). Has {shape}') - if shape[0] != 1: - raise Exception(f'Segmentation model can handle only 1-Batch outputs. Has {shape}') - if shape[2] != shape[3]: raise Exception(f'Segmentation model can handle only square outputs masks. Has: {shape}') diff --git a/src/deepness/processing/models/superresolution.py b/src/deepness/processing/models/superresolution.py index d0724c6..777dcdd 100644 --- a/src/deepness/processing/models/superresolution.py +++ b/src/deepness/processing/models/superresolution.py @@ -1,6 +1,7 @@ """ Module including Super Resolution model definition """ from typing import List + import numpy as np from deepness.processing.models.model_base import ModelBase @@ -22,28 +23,6 @@ def __init__(self, model_file_path: str): """ super(Superresolution, self).__init__(model_file_path) - def preprocessing(self, image: np.ndarray) -> np.ndarray: - """ Preprocess the image for the model (resize, normalization, etc) - - Parameters - ---------- - image : np.ndarray - Image to preprocess (H,W,C), RGB, 0-255 - - Returns - ------- - np.ndarray - Preprocessed image (1,C,H,W), RGB, 0-1 - """ - img = image[:, :, :self.input_shape[-3]] - - input_batch = img.astype('float32') - input_batch /= 255 - input_batch = input_batch.transpose(2, 0, 1) - input_batch = np.expand_dims(input_batch, axis=0) - - return input_batch - def postprocessing(self, model_output: List) -> np.ndarray: """ Postprocess the model output. @@ -58,7 +37,7 @@ def postprocessing(self, model_output: List) -> np.ndarray: Postprocessed mask (H,W,C), 0-1 (one output channel) """ - return model_output[0][0] + return model_output[0] def get_number_of_output_channels(self) -> int: """ Returns number of channels in the output layer @@ -113,9 +92,6 @@ def check_loaded_model_outputs(self): raise Exception(f'Regression model output should have 4 dimensions: (Batch_size, Channels, H, W). \n' f'Actually has: {shape}') - if shape[0] != 1: - raise Exception(f'Regression model can handle only 1-Batch outputs. Has {shape}') - if shape[2] != shape[3]: raise Exception(f'Regression model can handle only square outputs masks. Has: {shape}') diff --git a/src/deepness/processing/processing_utils.py b/src/deepness/processing/processing_utils.py index d6a8f42..1488795 100644 --- a/src/deepness/processing/processing_utils.py +++ b/src/deepness/processing/processing_utils.py @@ -14,6 +14,7 @@ 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 +from deepness.common.temp_files_handler import TempFilesHandler cv2 = LazyPackageLoader('cv2') @@ -47,7 +48,6 @@ def get_numpy_data_type_for_qgis_type(data_type_qgis: Qgis.DataType): raise Exception("Invalid input layer data type!") - def get_tile_image( rlayer: QgsRasterLayer, extent: QgsRectangle, @@ -70,7 +70,8 @@ def get_tile_image( """ expected_meters_per_pixel = params.resolution_cm_per_px / 100 - expected_units_per_pixel = convert_meters_to_rlayer_units(rlayer, expected_meters_per_pixel) + expected_units_per_pixel = convert_meters_to_rlayer_units( + rlayer, expected_meters_per_pixel) expected_units_per_pixel_2d = expected_units_per_pixel, expected_units_per_pixel # to get all pixels - use the 'rlayer.rasterUnitsPerPixelX()' instead of 'expected_units_per_pixel_2d' image_size = round((extent.width()) / expected_units_per_pixel_2d[0]), \ @@ -86,8 +87,10 @@ def get_tile_image( raise Exception("Somehow invalid rlayer!") data_provider.enableProviderResampling(True) original_resampling_method = data_provider.zoomedInResamplingMethod() - data_provider.setZoomedInResamplingMethod(data_provider.ResamplingMethod.Bilinear) - data_provider.setZoomedOutResamplingMethod(data_provider.ResamplingMethod.Bilinear) + data_provider.setZoomedInResamplingMethod( + data_provider.ResamplingMethod.Bilinear) + data_provider.setZoomedOutResamplingMethod( + data_provider.ResamplingMethod.Bilinear) def get_raster_block(band_number_): raster_block = rlayer.dataProvider().block( @@ -106,9 +109,11 @@ def get_raster_block(band_number_): if input_channels_mapping.are_all_inputs_standalone_bands(): band_count = rlayer.bandCount() for i in range(number_of_model_inputs): - image_channel = input_channels_mapping.get_image_channel_for_model_input(i) + image_channel = input_channels_mapping.get_image_channel_for_model_input( + i) band_number = image_channel.get_band_number() - assert band_number <= band_count # we cannot obtain a higher band than the maximum in the image + # we cannot obtain a higher band than the maximum in the image + assert band_number <= band_count rb = get_raster_block(band_number) raw_data = rb.data() bytes_array = bytes(raw_data) @@ -123,21 +128,26 @@ def get_raster_block(band_number_): bytes_array = bytes(raw_data) dt = rb.dataType() number_of_image_channels = input_channels_mapping.get_number_of_image_channels() - assert number_of_image_channels == 4 # otherwise we did something wrong earlier... + # otherwise we did something wrong earlier... + assert number_of_image_channels == 4 if dt != Qgis.DataType.ARGB32: raise Exception("Invalid input layer data type!") a = np.frombuffer(bytes_array, dtype=np.uint8) b = a.reshape((image_size[1], image_size[0], number_of_image_channels)) for i in range(number_of_model_inputs): - image_channel = input_channels_mapping.get_image_channel_for_model_input(i) + image_channel = input_channels_mapping.get_image_channel_for_model_input( + i) byte_number = image_channel.get_byte_number() - assert byte_number < number_of_image_channels # we cannot get more bytes than there are - tile_data.append(b[:, :, byte_number:byte_number+1]) # last index to keep dimension + # we cannot get more bytes than there are + assert byte_number < number_of_image_channels + # last index to keep dimension + tile_data.append(b[:, :, byte_number:byte_number+1]) else: raise Exception("Unsupported image channels composition!") - data_provider.setZoomedInResamplingMethod(original_resampling_method) # restore old resampling method + data_provider.setZoomedInResamplingMethod( + original_resampling_method) # restore old resampling method img = np.concatenate(tile_data, axis=2) return img @@ -412,7 +422,8 @@ def transform_polygon_with_rings_epsg_to_extended_xy_pixels( for point_epsg in polygon: x_epsg, y_epsg = point_epsg x = round((x_epsg - x_min_epsg) / rlayer_units_per_pixel) - y = y_max_pixel - round((y_epsg - y_min_epsg) / rlayer_units_per_pixel) + y = y_max_pixel - \ + round((y_epsg - y_min_epsg) / rlayer_units_per_pixel) # NOTE: here we can get pixels +-1 values, because we operate on already rounded bounding boxes xy_pixel_contour.append((x, y)) @@ -428,7 +439,8 @@ def create_area_mask_image(vlayer_mask, rlayer: QgsRasterLayer, extended_extent: QgsRectangle, rlayer_units_per_pixel: float, - image_shape_yx) -> Optional[np.ndarray]: + image_shape_yx: Tuple[int, int], + files_handler: Optional[TempFilesHandler] = None) -> Optional[np.ndarray]: """ Mask determining area to process (within extended_extent coordinates) None if no mask layer provided. @@ -436,7 +448,15 @@ def create_area_mask_image(vlayer_mask, if vlayer_mask is None: return None - img = np.zeros(shape=image_shape_yx, dtype=np.uint8) + + if files_handler is None: + img = np.zeros(shape=image_shape_yx, dtype=np.uint8) + else: + img = np.memmap(files_handler.get_area_mask_img_path(), + dtype=np.uint8, + mode='w+', + shape=image_shape_yx) + features = vlayer_mask.getFeatures() if vlayer_mask.crs() != rlayer.crs(): diff --git a/test/data/dummy_model/dummy_regression_model_batched.onnx b/test/data/dummy_model/dummy_regression_model_batched.onnx new file mode 100644 index 0000000..e620622 Binary files /dev/null and b/test/data/dummy_model/dummy_regression_model_batched.onnx differ diff --git a/test/manual_test_map_processor_detection_yolo_ultralytics.py b/test/manual_test_map_processor_detection_yolo_ultralytics.py index 983c5f8..fda16ac 100644 --- a/test/manual_test_map_processor_detection_yolo_ultralytics.py +++ b/test/manual_test_map_processor_detection_yolo_ultralytics.py @@ -29,6 +29,8 @@ def test_map_processor_detection_yolo_ultralytics(): params = DetectionParameters( resolution_cm_per_px=3, tile_size_px=model_wrapper.get_input_size_in_pixels()[0], # same x and y dimensions, so take x + batch_size=1, + local_cache=False, processed_area_type=ProcessedAreaType.ENTIRE_LAYER, mask_layer_id=None, input_layer_id=rlayer.id(), diff --git a/test/manual_test_map_processor_detection_yolov6.py b/test/manual_test_map_processor_detection_yolov6.py index 218f6db..1b43048 100644 --- a/test/manual_test_map_processor_detection_yolov6.py +++ b/test/manual_test_map_processor_detection_yolov6.py @@ -29,6 +29,8 @@ def test_map_processor_detection_yolov6(): params = DetectionParameters( resolution_cm_per_px=2, tile_size_px=model_wrapper.get_input_size_in_pixels()[0], # same x and y dimensions, so take x + batch_size=1, + local_cache=False, processed_area_type=ProcessedAreaType.ENTIRE_LAYER, mask_layer_id=None, input_layer_id=rlayer.id(), diff --git a/test/manual_test_map_processor_instance_yolo_ultralytics.py b/test/manual_test_map_processor_instance_yolo_ultralytics.py index e1fcd08..0985bb1 100644 --- a/test/manual_test_map_processor_instance_yolo_ultralytics.py +++ b/test/manual_test_map_processor_instance_yolo_ultralytics.py @@ -29,6 +29,8 @@ def test_map_processor_detection_yolo_ultralytics(): 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 + batch_size=1, + local_cache=False, processed_area_type=ProcessedAreaType.ENTIRE_LAYER, mask_layer_id=None, input_layer_id=rlayer.id(), diff --git a/test/test_map_processor_detection_oils_example.py b/test/test_map_processor_detection_oils_example.py index 9378a4a..1e5b0d7 100644 --- a/test/test_map_processor_detection_oils_example.py +++ b/test/test_map_processor_detection_oils_example.py @@ -30,6 +30,8 @@ def test_map_processor_detection_oil_example(): params = DetectionParameters( resolution_cm_per_px=150, tile_size_px=model_wrapper.get_input_size_in_pixels()[0], # same x and y dimensions, so take x + batch_size=1, + local_cache=False, processed_area_type=ProcessedAreaType.ENTIRE_LAYER, mask_layer_id=None, input_layer_id=rlayer.id(), @@ -52,6 +54,38 @@ def test_map_processor_detection_oil_example(): map_processor.run() +def test_map_processor_detection_oil_example_using_cache(): + qgs = init_qgis() + rlayer = create_rlayer_from_file(RASTER_FILE_PATH) + + model_wrapper = Detector(MODEL_FILE_PATH) + + params = DetectionParameters( + resolution_cm_per_px=150, + tile_size_px=model_wrapper.get_input_size_in_pixels()[0], # same x and y dimensions, so take x + batch_size=1, + local_cache=True, + 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=40), + model=model_wrapper, + confidence=0.5, + iou_threshold=0.1, + remove_overlapping_detections=False, + model_output_format=ModelOutputFormat.ALL_CLASSES_AS_SEPARATE_LAYERS, + model_output_format__single_class_number=-1, + ) + + map_processor = MapProcessorDetection( + rlayer=rlayer, + vlayer_mask=None, + map_canvas=MagicMock(), + params=params, + ) + + map_processor.run() def test_map_processor_detection_oil_example_with_remove_small(): qgs = init_qgis() @@ -62,6 +96,8 @@ def test_map_processor_detection_oil_example_with_remove_small(): params = DetectionParameters( resolution_cm_per_px=150, tile_size_px=model_wrapper.get_input_size_in_pixels()[0], # same x and y dimensions, so take x + batch_size=1, + local_cache=False, processed_area_type=ProcessedAreaType.ENTIRE_LAYER, mask_layer_id=None, input_layer_id=rlayer.id(), diff --git a/test/test_map_processor_detection_planes_example.py b/test/test_map_processor_detection_planes_example.py index 8311936..00cd14c 100644 --- a/test/test_map_processor_detection_planes_example.py +++ b/test/test_map_processor_detection_planes_example.py @@ -27,6 +27,8 @@ def test_map_processor_detection_planes_example(): params = DetectionParameters( resolution_cm_per_px=70, tile_size_px=model_wrapper.get_input_size_in_pixels()[0], # same x and y dimensions, so take x + batch_size=1, + local_cache=False, processed_area_type=ProcessedAreaType.ENTIRE_LAYER, mask_layer_id=None, input_layer_id=rlayer.id(), diff --git a/test/test_map_processor_empty_detection.py b/test/test_map_processor_empty_detection.py index 247d1c8..6704888 100644 --- a/test/test_map_processor_empty_detection.py +++ b/test/test_map_processor_empty_detection.py @@ -28,6 +28,8 @@ def test_map_processor_empty_detection(): params = DetectionParameters( resolution_cm_per_px=100, tile_size_px=model_wrapper.get_input_size_in_pixels()[0], # same x and y dimensions, so take x + batch_size=1, + local_cache=False, processed_area_type=ProcessedAreaType.ENTIRE_LAYER, mask_layer_id=None, input_layer_id=rlayer.id(), diff --git a/test/test_map_processor_regression.py b/test/test_map_processor_regression.py index b1bc48e..85d27f6 100644 --- a/test/test_map_processor_regression.py +++ b/test/test_map_processor_regression.py @@ -1,6 +1,7 @@ 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) + get_dummy_regression_model_path, get_dummy_regression_model_path_batched, + get_dummy_segmentation_model_path, init_qgis) from unittest.mock import MagicMock import numpy as np @@ -20,6 +21,7 @@ VLAYER_MASK_FILE_PATH = get_dummy_fotomap_area_path() MODEL_FILE_PATH = get_dummy_regression_model_path() +MODEL_FILE_PATH_BATCHED = get_dummy_regression_model_path_batched() INPUT_CHANNELS_MAPPING = create_default_input_channels_mapping_for_rgba_bands() @@ -37,6 +39,8 @@ def test_dummy_model_processing__entire_file(): params = RegressionParameters( resolution_cm_per_px=3, tile_size_px=model.get_input_size_in_pixels()[0], # same x and y dimensions, so take x + batch_size=1, + local_cache=False, processed_area_type=ProcessedAreaType.ENTIRE_LAYER, mask_layer_id=None, input_layer_id=rlayer.id(), @@ -62,92 +66,77 @@ 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_batched(): + qgs = init_qgis() + + rlayer = create_rlayer_from_file(RASTER_FILE_PATH) + model = Regressor(MODEL_FILE_PATH_BATCHED) + + params = RegressionParameters( + resolution_cm_per_px=3, + tile_size_px=model.get_input_size_in_pixels()[0], # same x and y dimensions, so take x + batch_size=2, + local_cache=False, + processed_area_type=ProcessedAreaType.ENTIRE_LAYER, + mask_layer_id=None, + input_layer_id=rlayer.id(), + input_channels_mapping=INPUT_CHANNELS_MAPPING, + output_scaling=1.0, + 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, + ) + + map_processor = MapProcessorRegression( + rlayer=rlayer, + vlayer_mask=None, + map_canvas=MagicMock(), + params=params, + ) + + map_processor.run() + result_imgs = map_processor.get_result_imgs() + result_img = result_imgs[0] + + assert result_img.shape == (561, 829) -# def model_process_mock(x): -# x = x[:, :, 0:2] -# return np.transpose(x, (2, 0, 1)) -# -# -# def test_generic_processing_test__specified_extent_from_vlayer(): -# qgs = init_qgis() -# -# rlayer = create_rlayer_from_file(RASTER_FILE_PATH) -# vlayer_mask = create_vlayer_from_file(VLAYER_MASK_FILE_PATH) -# model = MagicMock() -# model.process = model_process_mock -# model.get_number_of_channels = lambda: 2 -# model.get_number_of_output_channels = lambda: 2 -# -# params = SegmentationParameters( -# resolution_cm_per_px=3, -# tile_size_px=512, -# processed_area_type=ProcessedAreaType.FROM_POLYGONS, -# mask_layer_id=vlayer_mask.id(), -# input_layer_id=rlayer.id(), -# input_channels_mapping=INPUT_CHANNELS_MAPPING, -# postprocessing_dilate_erode_size=5, -# 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, -# model=model, -# ) -# map_processor = MapProcessorSegmentation( -# rlayer=rlayer, -# vlayer_mask=vlayer_mask, -# map_canvas=MagicMock(), -# params=params, -# ) -# -# # just run - we will check the results in a more detailed test -# map_processor.run() -# -# -# def test_generic_processing_test__specified_extent_from_active_map_extent(): -# qgs = init_qgis() -# -# rlayer = create_rlayer_from_file(RASTER_FILE_PATH) -# model = MagicMock() -# model.process = model_process_mock -# model.get_number_of_channels = lambda: 2 -# model.get_number_of_output_channels = lambda: 2 -# -# params = SegmentationParameters( -# resolution_cm_per_px=3, -# tile_size_px=512, -# processed_area_type=ProcessedAreaType.VISIBLE_PART, -# mask_layer_id=None, -# input_layer_id=rlayer.id(), -# input_channels_mapping=INPUT_CHANNELS_MAPPING, -# postprocessing_dilate_erode_size=5, -# 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, -# model=model, -# ) -# processed_extent = PROCESSED_EXTENT_1 -# -# # we want to use a fake extent, which is the Visible Part of the map, -# # so we need to mock its function calls -# params.processed_area_type = ProcessedAreaType.VISIBLE_PART -# map_canvas = MagicMock() -# map_canvas.extent = lambda: processed_extent -# map_canvas.mapSettings().destinationCrs = lambda: QgsCoordinateReferenceSystem("EPSG:32633") -# -# map_processor = MapProcessorSegmentation( -# rlayer=rlayer, -# vlayer_mask=None, -# map_canvas=map_canvas, -# params=params, -# ) -# -# # just run - we will check the results in a more detailed test -# map_processor.run() +def test_dummy_model_processing__entire_file_with_cache(): + qgs = init_qgis() + + rlayer = create_rlayer_from_file(RASTER_FILE_PATH) + model = Regressor(MODEL_FILE_PATH) + + params = RegressionParameters( + resolution_cm_per_px=3, + tile_size_px=model.get_input_size_in_pixels()[0], # same x and y dimensions, so take x + batch_size=1, + local_cache=True, + processed_area_type=ProcessedAreaType.ENTIRE_LAYER, + mask_layer_id=None, + input_layer_id=rlayer.id(), + input_channels_mapping=INPUT_CHANNELS_MAPPING, + output_scaling=1.0, + 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, + ) + map_processor = MapProcessorRegression( + rlayer=rlayer, + vlayer_mask=None, + map_canvas=MagicMock(), + params=params, + ) + + map_processor.run() + result_imgs = map_processor.get_result_imgs() + result_img = result_imgs[0] + + assert result_img.shape == (561, 829) if __name__ == '__main__': test_dummy_model_processing__entire_file() - # test_generic_processing_test__specified_extent_from_vlayer() - # test_generic_processing_test__specified_extent_from_active_map_extent() - print('Done') + test_dummy_model_processing__entire_file_batched() + test_dummy_model_processing__entire_file_with_cache() diff --git a/test/test_map_processor_segmentation.py b/test/test_map_processor_segmentation.py index dcb96e6..e6b3062 100644 --- a/test/test_map_processor_segmentation.py +++ b/test/test_map_processor_segmentation.py @@ -37,6 +37,8 @@ def test_dummy_model_processing__entire_file(): 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 + batch_size=1, + local_cache=False, processed_area_type=ProcessedAreaType.ENTIRE_LAYER, mask_layer_id=None, input_layer_id=rlayer.id(), @@ -71,6 +73,8 @@ def test_dummy_model_processing__entire_file_overlap_in_pixels(): 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 + batch_size=1, + local_cache=False, processed_area_type=ProcessedAreaType.ENTIRE_LAYER, mask_layer_id=None, input_layer_id=rlayer.id(), @@ -96,8 +100,8 @@ def test_dummy_model_processing__entire_file_overlap_in_pixels(): assert result_img.shape == (561, 829) def model_process_mock(x): - x = x[:, :, 0:2] - return np.transpose(x, (2, 0, 1)) + x = x[:, :, :, 0:2] + return np.transpose(x, (0, 3, 1, 2)) def test_generic_processing_test__specified_extent_from_vlayer(): @@ -114,6 +118,8 @@ def test_generic_processing_test__specified_extent_from_vlayer(): params = SegmentationParameters( resolution_cm_per_px=3, tile_size_px=512, + batch_size=1, + local_cache=False, processed_area_type=ProcessedAreaType.FROM_POLYGONS, mask_layer_id=vlayer_mask.id(), input_layer_id=rlayer.id(), @@ -143,6 +149,50 @@ def test_generic_processing_test__specified_extent_from_vlayer(): # and counts of different values np.testing.assert_allclose(np.unique(result_img, return_counts=True)[1], np.array([166903, 45270, 171919]), atol=3) +def test_generic_processing_test__specified_extent_from_vlayer_using_cache(): + qgs = init_qgis() + + rlayer = create_rlayer_from_file(RASTER_FILE_PATH) + vlayer_mask = create_vlayer_from_file(VLAYER_MASK_FILE_PATH) + model = MagicMock() + model.process = model_process_mock + model.get_number_of_channels = lambda: 2 + model.get_number_of_output_channels = lambda: 2 + model.get_channel_name = lambda x: str(x) + + params = SegmentationParameters( + resolution_cm_per_px=3, + tile_size_px=512, + batch_size=1, + local_cache=True, + processed_area_type=ProcessedAreaType.FROM_POLYGONS, + mask_layer_id=vlayer_mask.id(), + input_layer_id=rlayer.id(), + input_channels_mapping=INPUT_CHANNELS_MAPPING, + postprocessing_dilate_erode_size=5, + 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, + model=model, + ) + map_processor = MapProcessorSegmentation( + rlayer=rlayer, + vlayer_mask=vlayer_mask, + map_canvas=MagicMock(), + params=params, + ) + + # just run - we will check the results in a more detailed test + map_processor.run() + result_img = map_processor.get_result_img() + assert result_img.shape == (524, 733) + + # just check a few pixels + assert all(result_img.ravel()[[365, 41234, 59876, 234353, 111222, 134534, 223423, 65463, 156451]] == + np.asarray([0, 2, 2, 2, 2, 0, 0, 2, 0])) + # and counts of different values + np.testing.assert_allclose(np.unique(result_img, return_counts=True)[1], np.array([166903, 45270, 171919]), atol=3) def test_generic_processing_test__specified_extent_from_vlayer_crs3857(): qgs = init_qgis() @@ -158,6 +208,8 @@ def test_generic_processing_test__specified_extent_from_vlayer_crs3857(): params = SegmentationParameters( resolution_cm_per_px=3, tile_size_px=512, + batch_size=1, + local_cache=False, processed_area_type=ProcessedAreaType.FROM_POLYGONS, mask_layer_id=vlayer_mask.id(), input_layer_id=rlayer.id(), @@ -203,6 +255,8 @@ def test_generic_processing_test__specified_extent_from_active_map_extent(): params = SegmentationParameters( resolution_cm_per_px=3, tile_size_px=512, + batch_size=1, + local_cache=False, processed_area_type=ProcessedAreaType.VISIBLE_PART, mask_layer_id=None, input_layer_id=rlayer.id(), diff --git a/test/test_map_processor_segmentation_different_output_size.py b/test/test_map_processor_segmentation_different_output_size.py index d71ef2f..b75757a 100644 --- a/test/test_map_processor_segmentation_different_output_size.py +++ b/test/test_map_processor_segmentation_different_output_size.py @@ -38,6 +38,8 @@ def test_dummy_model_processing_when_different_output_size(): 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 + batch_size=1, + local_cache=False, processed_area_type=ProcessedAreaType.ENTIRE_LAYER, mask_layer_id=None, input_layer_id=rlayer.id(), diff --git a/test/test_map_processor_segmentation_landcover_example.py b/test/test_map_processor_segmentation_landcover_example.py index f6c9924..697c148 100644 --- a/test/test_map_processor_segmentation_landcover_example.py +++ b/test/test_map_processor_segmentation_landcover_example.py @@ -1,5 +1,7 @@ import os from pathlib import Path +import numpy as np + from test.test_utils import create_default_input_channels_mapping_for_rgb_bands, create_rlayer_from_file, init_qgis from unittest.mock import MagicMock @@ -27,6 +29,8 @@ def test_map_processor_segmentation_landcover_example(): params = SegmentationParameters( resolution_cm_per_px=100, tile_size_px=model.get_input_size_in_pixels()[0], # same x and y dimensions, so take x + batch_size=1, + local_cache=False, processed_area_type=ProcessedAreaType.ENTIRE_LAYER, mask_layer_id=None, input_layer_id=rlayer.id(), @@ -48,8 +52,29 @@ def test_map_processor_segmentation_landcover_example(): map_processor.run() result_img = map_processor.get_result_img() - + assert result_img.shape == (2351, 2068) + + assert result_img[1000, 1000] == 1 + assert result_img[2000, 2000] == 3 + assert result_img[150:300, 150:300].sum() == 41478 + + unique, counts = np.unique(result_img, return_counts=True) + + counts = dict(zip(unique, counts)) + + gt_counts = { + 1: 3294546, + 2: 71169, + 3: 1054899, + 4: 365915, + 5: 75339, + } + + assert set(counts.keys()) == set(gt_counts.keys()) + + for k, v in gt_counts.items(): + assert counts[k] == v if __name__ == '__main__': diff --git a/test/test_map_processor_segmentation_landcover_example_batched.py b/test/test_map_processor_segmentation_landcover_example_batched.py new file mode 100644 index 0000000..ac38bc0 --- /dev/null +++ b/test/test_map_processor_segmentation_landcover_example_batched.py @@ -0,0 +1,82 @@ +import os +from pathlib import Path +import numpy as np + +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.processing.map_processor.map_processor_segmentation import MapProcessorSegmentation +from deepness.processing.models.segmentor import Segmentor + +HOME_DIR = Path(__file__).resolve().parents[1] +EXAMPLE_DATA_DIR = os.path.join(HOME_DIR, 'examples', 'deeplabv3_segmentation_landcover') + +MODEL_FILE_PATH = os.path.join(EXAMPLE_DATA_DIR, 'deeplabv3_landcover_4c_batched.onnx') +RASTER_FILE_PATH = os.path.join(EXAMPLE_DATA_DIR, 'N-33-60-D-c-4-2.tif') + +INPUT_CHANNELS_MAPPING = create_default_input_channels_mapping_for_rgb_bands() + + +def test_map_processor_segmentation_landcover_example(): + qgs = init_qgis() + + rlayer = create_rlayer_from_file(RASTER_FILE_PATH) + model = Segmentor(MODEL_FILE_PATH) + + params = SegmentationParameters( + resolution_cm_per_px=100, + tile_size_px=model.get_input_size_in_pixels()[0], # same x and y dimensions, so take x + batch_size=2, + local_cache=False, + 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_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, + 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 == (2351, 2068) + + assert result_img[1000, 1000] == 1 + assert result_img[2000, 2000] == 3 + assert result_img[150:300, 150:300].sum() == 41478 + + unique, counts = np.unique(result_img, return_counts=True) + + counts = dict(zip(unique, counts)) + + gt_counts = { + 1: 3294546, + 2: 71169, + 3: 1054899, + 4: 365915, + 5: 75339, + } + + assert set(counts.keys()) == set(gt_counts.keys()) + + for k, v in gt_counts.items(): + assert counts[k] == v + + +if __name__ == '__main__': + test_map_processor_segmentation_landcover_example() + print('Done') diff --git a/test/test_map_processor_superresolution.py b/test/test_map_processor_superresolution.py index fa5f524..b54792e 100644 --- a/test/test_map_processor_superresolution.py +++ b/test/test_map_processor_superresolution.py @@ -37,6 +37,8 @@ def test_dummy_model_processing__entire_file(): params = SuperresolutionParameters( resolution_cm_per_px=3, tile_size_px=model.get_input_size_in_pixels()[0], # same x and y dimensions, so take x + batch_size=1, + local_cache=False, processed_area_type=ProcessedAreaType.ENTIRE_LAYER, mask_layer_id=None, input_layer_id=rlayer.id(), @@ -63,6 +65,77 @@ def test_dummy_model_processing__entire_file(): 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 +def test_dummy_model_processing__entire_file_cached(): + qgs = init_qgis() + + rlayer = create_rlayer_from_file(RASTER_FILE_PATH) + model = Superresolution(MODEL_FILE_PATH) + + params = SuperresolutionParameters( + resolution_cm_per_px=3, + tile_size_px=model.get_input_size_in_pixels()[0], # same x and y dimensions, so take x + batch_size=1, + local_cache=True, + processed_area_type=ProcessedAreaType.ENTIRE_LAYER, + mask_layer_id=None, + input_layer_id=rlayer.id(), + input_channels_mapping=INPUT_CHANNELS_MAPPING, + output_scaling=1.0, + scale_factor=2.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, + ) + + map_processor = MapProcessorSuperresolution( + rlayer=rlayer, + vlayer_mask=None, + map_canvas=MagicMock(), + params=params, + ) + + map_processor.run() + 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 + +def test_dummy_model_processing__entire_file_batched(): + qgs = init_qgis() + + rlayer = create_rlayer_from_file(RASTER_FILE_PATH) + model = Superresolution(MODEL_FILE_PATH) + + params = SuperresolutionParameters( + resolution_cm_per_px=3, + tile_size_px=model.get_input_size_in_pixels()[0], # same x and y dimensions, so take x + batch_size=1, + local_cache=True, + processed_area_type=ProcessedAreaType.ENTIRE_LAYER, + mask_layer_id=None, + input_layer_id=rlayer.id(), + input_channels_mapping=INPUT_CHANNELS_MAPPING, + output_scaling=1.0, + scale_factor=2.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, + ) + + map_processor = MapProcessorSuperresolution( + rlayer=rlayer, + vlayer_mask=None, + map_canvas=MagicMock(), + params=params, + ) + + map_processor.run() + 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 if __name__ == '__main__': test_dummy_model_processing__entire_file() diff --git a/test/test_map_processor_training_data_export.py b/test/test_map_processor_training_data_export.py index 7f4e1cc..2deeb85 100644 --- a/test/test_map_processor_training_data_export.py +++ b/test/test_map_processor_training_data_export.py @@ -20,6 +20,8 @@ def test_export_dummy_fotomap(): params = TrainingDataExportParameters( export_image_tiles=True, resolution_cm_per_px=3, + batch_size=1, + local_cache=False, segmentation_mask_layer_id=vlayer.id(), output_directory_path='/tmp/qgis_test', tile_size_px=512, # same x and y dimensions, so take x diff --git a/test/test_utils.py b/test/test_utils.py index dd6e3e5..44f9d96 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -31,6 +31,13 @@ def get_dummy_regression_model_path(): """ return os.path.join(TEST_DATA_DIR, 'dummy_model', 'dummy_regression_model.onnx') +def get_dummy_regression_model_path_batched(): + """ + Get path of a dummy onnx model. See details in README in model directory. + Model used for unit tests processing purposes + """ + return os.path.join(TEST_DATA_DIR, 'dummy_model', 'dummy_regression_model_batched.onnx') + def get_dummy_superresolution_model_path(): """ Get path of a dummy onnx model. See details in README in model directory.