diff --git a/zipdeepness/deepness/README.md b/zipdeepness/deepness/README.md deleted file mode 100644 index e8d22bce930e79342f58cd0cdef5305e8be62454..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Deepness: Deep Neural Remote Sensing - -Plugin for QGIS to perform map/image segmentation, regression and object detection with (ONNX) neural network models. - -Please visit the documentation webpage for details: https://qgis-plugin-deepness.readthedocs.io/ - -Or the repository: https://github.com/PUTvision/qgis-plugin-deepness diff --git a/zipdeepness/deepness/__init__.py b/zipdeepness/deepness/__init__.py deleted file mode 100644 index f39b0a2ef08e2c60b2c0ef47be3d85a38bbbc57c..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Main plugin module - entry point for the plugin.""" -import os - -# increase limit of pixels (2^30), before importing cv2. -# We are doing it here to make sure it will be done before importing cv2 for the first time -os.environ["OPENCV_IO_MAX_IMAGE_PIXELS"] = pow(2, 40).__str__() - - -# noinspection PyPep8Naming -def classFactory(iface): # pylint: disable=invalid-name - """Load Deepness class from file Deepness. - :param iface: A QGIS interface instance. - :type iface: QgsInterface - """ - from deepness.dialogs.packages_installer import packages_installer_dialog - packages_installer_dialog.check_required_packages_and_install_if_necessary(iface=iface) - - from deepness.deepness import Deepness - return Deepness(iface) \ No newline at end of file diff --git a/zipdeepness/deepness/common/__init__.py b/zipdeepness/deepness/common/__init__.py deleted file mode 100644 index c4b1577d62082b1c69aa61c20449064def038db9..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/common/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -""" Submodule that contains the common functions for the deepness plugin. -""" diff --git a/zipdeepness/deepness/common/channels_mapping.py b/zipdeepness/deepness/common/channels_mapping.py deleted file mode 100644 index 5cf5e7374a564c7cbf6208b7fe40c57e39f3086d..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/common/channels_mapping.py +++ /dev/null @@ -1,262 +0,0 @@ -""" -Raster layer (ortophoto) which is being processed consist of channels (usually Red, Green, Blue). -The neural model expects input channels with some model-defined meaning. -Channel mappings in this file define how the ortophoto channels translate to model inputs (e.g first model input is Red, second Green). -""" - -import copy -from typing import Dict, List - - -class ImageChannel: - """ - Defines an image channel - how is it being stored in the data source. - See note at top of this file for details. - """ - - def __init__(self, name): - self.name = name - - def get_band_number(self): - raise NotImplementedError('Base class not implemented!') - - def get_byte_number(self): - raise NotImplementedError('Base class not implemented!') - - -class ImageChannelStandaloneBand(ImageChannel): - """ - Defines an image channel, where each image channel is a separate band in the data source. - See note at top of this file for details. - """ - - def __init__(self, band_number: int, name: str): - super().__init__(name) - self.band_number = band_number # index within bands (counted from one) - - def __str__(self): - txt = f'ImageChannelStandaloneBand(name={self.name}, ' \ - f'band_number={self.band_number})' - return txt - - def get_band_number(self): - return self.band_number - - def get_byte_number(self): - raise NotImplementedError('Something went wrong if we are here!') - - -class ImageChannelCompositeByte(ImageChannel): - """ - Defines an image channel, where each image channel is a smaller part of a bigger value (e.g. one byte within uint32 for each pixel). - See note at top of this file for details. - """ - - def __init__(self, byte_number: int, name: str): - super().__init__(name) - self.byte_number = byte_number # position in composite byte (byte number in ARGB32, counted from zero) - - def __str__(self): - txt = f'ImageChannelCompositeByte(name={self.name}, ' \ - f'byte_number={self.byte_number})' - return txt - - def get_band_number(self): - raise NotImplementedError('Something went wrong if we are here!') - - def get_byte_number(self): - return self.byte_number - - -class ChannelsMapping: - """ - Defines mapping of model input channels to input image channels (bands). - See note at top of this file for details. - """ - - INVALID_INPUT_CHANNEL = -1 - - def __init__(self): - self._number_of_model_inputs = 0 - self._number_of_model_output_channels = 0 - self._image_channels = [] # type: List[ImageChannel] # what channels are available from input image - - # maps model channels to input image channels - # model_channel_number: image_channel_index (index in self._image_channels) - self._mapping = {} # type: Dict[int, int] - - def __str__(self): - txt = f'ChannelsMapping(' \ - f'number_of_model_inputs={self._number_of_model_inputs}, ' \ - f'image_channels = {self._image_channels}, ' \ - f'mapping {self._mapping})' - return txt - - def __eq__(self, other): - if self._number_of_model_inputs != other._number_of_model_inputs: - return False - return True - - def get_as_default_mapping(self): - """ - Get the same channels mapping as we have right now, but without the mapping itself - (so just a definition of inputs and outputs) - - Returns - ------- - ChannelsMapping - """ - default_channels_mapping = copy.deepcopy(self) - default_channels_mapping._mapping = {} - return default_channels_mapping - - def are_all_inputs_standalone_bands(self): - """ - Checks whether all image_channels are standalone bands (ImageChannelStandaloneBand) - """ - for image_channel in self._image_channels: - if not isinstance(image_channel, ImageChannelStandaloneBand): - return False - return True - - def are_all_inputs_composite_byte(self): - """ - Checks whether all image_channels are composite byte (ImageChannelCompositeByte) - """ - for image_channel in self._image_channels: - if not isinstance(image_channel, ImageChannelCompositeByte): - return False - return True - - def set_number_of_model_inputs(self, number_of_model_inputs: int): - """ Set how many input channels does the model has - Parameters - ---------- - number_of_model_inputs : int - """ - self._number_of_model_inputs = number_of_model_inputs - - def set_number_of_model_output_channels(self, number_of_output_channels: int): - """ Set how many output channels does the model has - - Parameters - ---------- - number_of_output_channels : int - """ - self._number_of_model_output_channels = number_of_output_channels - - def set_number_of_model_inputs_same_as_image_channels(self): - """ Set the number of model input channels to be the same as number of image channels - """ - self._number_of_model_inputs = len(self._image_channels) - - def get_number_of_model_inputs(self) -> int: - """ Get number of model input channels - - Returns - ------- - int - """ - return self._number_of_model_inputs - - def get_number_of_model_output_channels(self) -> int: - """ Get number of model output channels - - Returns - ------- - int - """ - return self._number_of_model_output_channels - - def get_number_of_image_channels(self) -> int: - """ Get number of image input channels - - Returns - ------- - int - """ - return len(self._image_channels) - - def set_image_channels(self, image_channels: List[ImageChannel]): - """ Set what are the image channels - - Parameters - ---------- - image_channels : List[ImageChannel] - Image channels to set - """ - self._image_channels = image_channels - if not self.are_all_inputs_standalone_bands() and not self.are_all_inputs_composite_byte(): - raise Exception("Unsupported image channels composition!") - - def get_image_channels(self) -> List[ImageChannel]: - """ Get the current image channels definition - - Returns - ------- - List[ImageChannel] - """ - return self._image_channels - - def get_image_channel_index_for_model_input(self, model_input_number) -> int: - """ - Similar to 'get_image_channel_for_model_input', but return an index in array of inputs, - instead of ImageChannel - """ - if len(self._image_channels) == 0: - raise Exception("No image channels!") - - image_channel_index = self._mapping.get(model_input_number, model_input_number) - image_channel_index = min(image_channel_index, len(self._image_channels) - 1) - return image_channel_index - - def get_image_channel_for_model_input(self, model_input_number: int) -> ImageChannel: - """ - Get ImageChannel which should be used for the specified model input - - Parameters - ---------- - model_input_number : int - Model input number, counted from 0 - - Returns - ------- - ImageChannel - """ - image_channel_index = self.get_image_channel_index_for_model_input(model_input_number) - return self._image_channels[image_channel_index] - - def set_image_channel_for_model_input(self, model_input_number: int, image_channel_index: int) -> ImageChannel: - """ - Set image_channel_index which should be used for this model input - """ - if image_channel_index >= len(self._image_channels): - raise Exception("Invalid image channel index!") - # image_channel = self._image_channels[image_channel_index] - self._mapping[model_input_number] = image_channel_index - - def get_mapping_as_list(self) -> List[int]: - """ Get the mapping of model input channels to image channels, but as a list (e.g. to store it in QGis configuration) - - Returns - ------- - List[int] - """ - mapping_list = [] - for i in range(self._number_of_model_inputs): - if i in self._mapping: - mapping_list.append(self._mapping[i]) - else: - mapping_list.append(-1) - return mapping_list - - def load_mapping_from_list(self, mapping_list: List[int]): - """ - Load self._mapping from a plain list of channels (which is saved in config) - """ - for i in range(min(self._number_of_model_inputs), len(mapping_list)): - proposed_channel = mapping_list[i] - if proposed_channel == -1 or proposed_channel >= self._number_of_model_inputs: - continue - - self._mapping[i] = proposed_channel diff --git a/zipdeepness/deepness/common/config_entry_key.py b/zipdeepness/deepness/common/config_entry_key.py deleted file mode 100644 index 546aa3112a19be3cfa4ec9cb945a11b79699f411..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/common/config_entry_key.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -This file contains utilities to write and read configuration parameters to the QGis Project configuration -""" - -import enum - -from qgis.core import QgsProject - -from deepness.common.defines import PLUGIN_NAME - - -class ConfigEntryKey(enum.Enum): - """ - Entries to be stored in Project Configuration. - Second element of enum value (in tuple) is the default value for this field - """ - - MODEL_FILE_PATH = enum.auto(), '' # Path to the model file - INPUT_LAYER_ID = enum.auto(), '' - 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 - SEGMENTATION_PROBABILITY_THRESHOLD_VALUE = enum.auto(), 0.5 - SEGMENTATION_REMOVE_SMALL_SEGMENT_ENABLED = enum.auto(), True - SEGMENTATION_REMOVE_SMALL_SEGMENT_SIZE = enum.auto(), 9 - - REGRESSION_OUTPUT_SCALING = enum.auto(), 1.0 - - DETECTION_CONFIDENCE = enum.auto(), 0.5 - DETECTION_IOU = enum.auto(), 0.5 - DETECTOR_TYPE = enum.auto(), 'YOLO_v5_v7_DEFAULT' - - DATA_EXPORT_DIR = enum.auto(), '' - DATA_EXPORT_TILES_ENABLED = enum.auto(), True - DATA_EXPORT_SEGMENTATION_MASK_ENABLED = enum.auto(), False - DATA_EXPORT_SEGMENTATION_MASK_ID = enum.auto(), '' - - INPUT_CHANNELS_MAPPING__ADVANCED_MODE = enum.auto, False - INPUT_CHANNELS_MAPPING__MAPPING_LIST_STR = enum.auto, [] - - def get(self): - """ - Get the value store in config (or a default one) for the specified field - """ - read_function = None - - # check the default value to determine the entry type - default_value = self.value[1] # second element in the 'value' tuple - if isinstance(default_value, int): - read_function = QgsProject.instance().readNumEntry - elif isinstance(default_value, float): - read_function = QgsProject.instance().readDoubleEntry - elif isinstance(default_value, bool): - read_function = QgsProject.instance().readBoolEntry - elif isinstance(default_value, str): - read_function = QgsProject.instance().readEntry - elif isinstance(default_value, str): - read_function = QgsProject.instance().readListEntry - else: - raise Exception("Unsupported entry type!") - - value, _ = read_function(PLUGIN_NAME, self.name, default_value) - return value - - def set(self, value): - """ Set the value store in config, for the specified field - - Parameters - ---------- - value : - Value to set in the configuration - """ - write_function = None - - # check the default value to determine the entry type - default_value = self.value[1] # second element in the 'value' tuple - if isinstance(default_value, int): - write_function = QgsProject.instance().writeEntry - elif isinstance(default_value, float): - write_function = QgsProject.instance().writeEntryDouble - elif isinstance(default_value, bool): - write_function = QgsProject.instance().writeEntryBool - elif isinstance(default_value, str): - write_function = QgsProject.instance().writeEntry - elif isinstance(default_value, list): - write_function = QgsProject.instance().writeEntry - else: - raise Exception("Unsupported entry type!") - - write_function(PLUGIN_NAME, self.name, value) diff --git a/zipdeepness/deepness/common/defines.py b/zipdeepness/deepness/common/defines.py deleted file mode 100644 index c78854ddcbafaf232be4c1e19060785801f5cbfd..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/common/defines.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -This file contain common definitions used in the project -""" - -import os - -PLUGIN_NAME = 'Deepness' -LOG_TAB_NAME = PLUGIN_NAME - - -# enable some debugging options (e.g. printing exceptions) - set in terminal before running qgis -IS_DEBUG = os.getenv("IS_DEBUG", 'False').lower() in ('true', '1', 't') diff --git a/zipdeepness/deepness/common/errors.py b/zipdeepness/deepness/common/errors.py deleted file mode 100644 index e56b11747e0e43266fd72a15ad8c1149105a63a3..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/common/errors.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -This file contains common exceptions used in the project -""" - - -class OperationFailedException(Exception): - """ - Base class for a failed operation, in order to have a good error message to show for the user - """ - pass diff --git a/zipdeepness/deepness/common/lazy_package_loader.py b/zipdeepness/deepness/common/lazy_package_loader.py deleted file mode 100644 index 51365ae32c044dcf0bdb8e471d19eabc6f7520d4..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/common/lazy_package_loader.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -This file contains utility to lazy import packages -""" - -import importlib - - -class LazyPackageLoader: - """ Allows to wrap python package into a lazy version, so that the package will be loaded once it is actually used - - Usage: - cv2 = LazyPackageLoader('cv2') # This will not import cv2 yet - ... - cv2.waitKey(3) # here will be the actual import - """ - - def __init__(self, package_name): - self._package_name = package_name - self._package = None - - def __getattr__(self, name): - if self._package is None: - self._package = importlib.import_module(self._package_name) - return getattr(self._package, name) diff --git a/zipdeepness/deepness/common/misc.py b/zipdeepness/deepness/common/misc.py deleted file mode 100644 index e6078449c4f0c41ebbeaa4068fefc28a6f891850..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/common/misc.py +++ /dev/null @@ -1,9 +0,0 @@ -""" -This file contains miscellaneous stuff used in the project -""" - -import os -import tempfile - -_TMP_DIR = tempfile.TemporaryDirectory() -TMP_DIR_PATH = os.path.join(_TMP_DIR.name, 'qgis') diff --git a/zipdeepness/deepness/common/processing_overlap.py b/zipdeepness/deepness/common/processing_overlap.py deleted file mode 100644 index a7c50f6403801c48e5f4589c8811a200677ceb28..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/common/processing_overlap.py +++ /dev/null @@ -1,37 +0,0 @@ -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/zipdeepness/deepness/common/processing_parameters/__init__.py b/zipdeepness/deepness/common/processing_parameters/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/zipdeepness/deepness/common/processing_parameters/detection_parameters.py b/zipdeepness/deepness/common/processing_parameters/detection_parameters.py deleted file mode 100644 index c96232c607fe30bf291da238449f01122642e907..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/common/processing_parameters/detection_parameters.py +++ /dev/null @@ -1,70 +0,0 @@ -import enum -from dataclasses import dataclass - -from deepness.common.processing_parameters.map_processing_parameters import MapProcessingParameters -from deepness.processing.models.model_base import ModelBase - - -@dataclass -class DetectorTypeParameters: - """ - Defines some model-specific parameters for each model 'type' (e.g. default YOLOv7 has different model output shape than the one trained with Ultralytics' YOLO) - """ - has_inverted_output_shape: bool = False # whether the output shape of the model is inverted (and we need to apply np.transpose(model_output, (1, 0))) - skipped_objectness_probability: bool = False # whether the model output has has no 'objectness' probability, and only probability for each class - ignore_objectness_probability: bool = False # if the model output has the 'objectness' probability, we can still ignore it (it is needeed sometimes, when the probability was always 1...). The behavior should be the same as with `skipped_objectness_probability` (of course one model output needs be skipped) - - -class DetectorType(enum.Enum): - """ Type of the detector model """ - - YOLO_v5_v7_DEFAULT = 'YOLO_v5_or_v7_default' - YOLO_v6 = 'YOLO_v6' - YOLO_v9 = 'YOLO_v9' - YOLO_ULTRALYTICS = 'YOLO_Ultralytics' - YOLO_ULTRALYTICS_SEGMENTATION = 'YOLO_Ultralytics_segmentation' - YOLO_ULTRALYTICS_OBB = 'YOLO_Ultralytics_obb' - - def get_parameters(self): - if self == DetectorType.YOLO_v5_v7_DEFAULT: - return DetectorTypeParameters() # all default - elif self == DetectorType.YOLO_v6: - return DetectorTypeParameters( - ignore_objectness_probability=True, - ) - elif self == DetectorType.YOLO_v9: - return DetectorTypeParameters( - has_inverted_output_shape=True, - skipped_objectness_probability=True, - ) - elif self == DetectorType.YOLO_ULTRALYTICS or self == DetectorType.YOLO_ULTRALYTICS_SEGMENTATION or self == DetectorType.YOLO_ULTRALYTICS_OBB: - return DetectorTypeParameters( - has_inverted_output_shape=True, - skipped_objectness_probability=True, - ) - else: - raise ValueError(f'Unknown detector type: {self}') - - def get_formatted_description(self): - txt = '' - txt += ' ' * 10 + f'Inverted output shape: {self.get_parameters().has_inverted_output_shape}\n' - txt += ' ' * 10 + f'Skipped objectness : {self.get_parameters().skipped_objectness_probability}\n' - txt += ' ' * 10 + f'Ignore objectness: {self.get_parameters().ignore_objectness_probability}\n' - return txt - - def get_all_display_values(): - return [x.value for x in DetectorType] - - -@dataclass -class DetectionParameters(MapProcessingParameters): - """ - Parameters for Inference of detection model (including pre/post-processing) obtained from UI. - """ - - model: ModelBase # wrapper of the loaded model - - confidence: float - iou_threshold: float - - detector_type: DetectorType = DetectorType.YOLO_v5_v7_DEFAULT # parameters specific for each model type diff --git a/zipdeepness/deepness/common/processing_parameters/map_processing_parameters.py b/zipdeepness/deepness/common/processing_parameters/map_processing_parameters.py deleted file mode 100644 index 755cf7459f8fd1c317e0576e1a65768f7ccf7437..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/common/processing_parameters/map_processing_parameters.py +++ /dev/null @@ -1,56 +0,0 @@ -import enum -from dataclasses import dataclass -from typing import Optional - -from deepness.common.channels_mapping import ChannelsMapping -from deepness.common.processing_overlap import ProcessingOverlap - - -class ProcessedAreaType(enum.Enum): - VISIBLE_PART = 'Visible part' - ENTIRE_LAYER = 'Entire layer' - FROM_POLYGONS = 'From polygons' - - @classmethod - def get_all_names(cls): - return [e.value for e in cls] - -@dataclass -class MapProcessingParameters: - """ - Common parameters for map processing obtained from UI. - - TODO: Add default values here, to later set them in UI at startup - """ - - 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 - - processing_overlap: ProcessingOverlap # aka "stride" - how much to overlap tiles during processing - - input_channels_mapping: ChannelsMapping # describes mapping of image channels to model inputs - - @property - def tile_size_m(self): - return self.tile_size_px * self.resolution_cm_per_px / 100 - - @property - def processing_overlap_px(self) -> int: - """ - Always divisible by 2, because overlap is on both sides of the tile - """ - return self.processing_overlap.get_overlap_px(self.tile_size_px) - - @property - def resolution_m_per_px(self): - return self.resolution_cm_per_px / 100 - - @property - def processing_stride_px(self): - return self.tile_size_px - self.processing_overlap_px diff --git a/zipdeepness/deepness/common/processing_parameters/recognition_parameters.py b/zipdeepness/deepness/common/processing_parameters/recognition_parameters.py deleted file mode 100644 index e350b6855a7d3e4c8b960c9aa609d354d7a144d4..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/common/processing_parameters/recognition_parameters.py +++ /dev/null @@ -1,17 +0,0 @@ -import enum -from dataclasses import dataclass -from typing import Optional - -from deepness.common.processing_parameters.map_processing_parameters import \ - MapProcessingParameters -from deepness.processing.models.model_base import ModelBase - - -@dataclass -class RecognitionParameters(MapProcessingParameters): - """ - Parameters for Inference of Recognition model (including pre/post-processing) obtained from UI. - """ - - query_image_path: str # path to query image - model: ModelBase # wrapper of the loaded model diff --git a/zipdeepness/deepness/common/processing_parameters/regression_parameters.py b/zipdeepness/deepness/common/processing_parameters/regression_parameters.py deleted file mode 100644 index 51e9eacc92ee34982869f7cb37e6b5b973498be4..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/common/processing_parameters/regression_parameters.py +++ /dev/null @@ -1,14 +0,0 @@ -from dataclasses import dataclass - -from deepness.common.processing_parameters.map_processing_parameters import MapProcessingParameters -from deepness.processing.models.model_base import ModelBase - - -@dataclass -class RegressionParameters(MapProcessingParameters): - """ - Parameters for Inference of Regression model (including pre/post-processing) obtained from UI. - """ - - output_scaling: float # scaling factor for the model output (keep 1 if maximum model output value is 1) - model: ModelBase # wrapper of the loaded model diff --git a/zipdeepness/deepness/common/processing_parameters/segmentation_parameters.py b/zipdeepness/deepness/common/processing_parameters/segmentation_parameters.py deleted file mode 100644 index bc69510d485011a9f44f6c2fb06a0c1c6a3cc459..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/common/processing_parameters/segmentation_parameters.py +++ /dev/null @@ -1,16 +0,0 @@ -from dataclasses import dataclass - -from deepness.common.processing_parameters.map_processing_parameters import MapProcessingParameters -from deepness.processing.models.model_base import ModelBase - - -@dataclass -class SegmentationParameters(MapProcessingParameters): - """ - Parameters for Inference of Segmentation model (including pre/post-processing) obtained from UI. - """ - - postprocessing_dilate_erode_size: int # dilate/erode operation size, once we have a single class map. 0 if inactive. Implementation may use median filer instead of erode/dilate - model: ModelBase # wrapper of the loaded model - - pixel_classification__probability_threshold: float # Minimum required class probability for pixel. 0 if disabled diff --git a/zipdeepness/deepness/common/processing_parameters/standardization_parameters.py b/zipdeepness/deepness/common/processing_parameters/standardization_parameters.py deleted file mode 100644 index 615ca3ed919fb752ae73b5273e31770e8491abb1..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/common/processing_parameters/standardization_parameters.py +++ /dev/null @@ -1,11 +0,0 @@ -import numpy as np - - -class StandardizationParameters: - def __init__(self, channels_number: int): - self.mean = np.array([0.0 for _ in range(channels_number)], dtype=np.float32) - self.std = np.array([1.0 for _ in range(channels_number)], dtype=np.float32) - - def set_mean_std(self, mean: np.array, std: np.array): - self.mean = np.array(mean, dtype=np.float32) - self.std = np.array(std, dtype=np.float32) diff --git a/zipdeepness/deepness/common/processing_parameters/superresolution_parameters.py b/zipdeepness/deepness/common/processing_parameters/superresolution_parameters.py deleted file mode 100644 index 036b7c631b8feb0ba108185de17e519427b3d669..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/common/processing_parameters/superresolution_parameters.py +++ /dev/null @@ -1,18 +0,0 @@ -import enum -from dataclasses import dataclass -from typing import Optional - -from deepness.common.channels_mapping import ChannelsMapping -from deepness.common.processing_parameters.map_processing_parameters import MapProcessingParameters -from deepness.processing.models.model_base import ModelBase - - -@dataclass -class SuperresolutionParameters(MapProcessingParameters): - """ - Parameters for Inference of Super Resolution model (including pre/post-processing) obtained from UI. - """ - - output_scaling: float # scaling factor for the model output (keep 1 if maximum model output value is 1) - model: ModelBase # wrapper of the loaded model - scale_factor: int # scale factor for the model output size diff --git a/zipdeepness/deepness/common/processing_parameters/training_data_export_parameters.py b/zipdeepness/deepness/common/processing_parameters/training_data_export_parameters.py deleted file mode 100644 index b80f7ac6f1bce87cd1edd523bcfe68a13e18bf1b..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/common/processing_parameters/training_data_export_parameters.py +++ /dev/null @@ -1,15 +0,0 @@ -from dataclasses import dataclass -from typing import Optional - -from deepness.common.processing_parameters.map_processing_parameters import MapProcessingParameters - - -@dataclass -class TrainingDataExportParameters(MapProcessingParameters): - """ - Parameters for Exporting Data obtained from UI. - """ - - export_image_tiles: bool # whether to export input image tiles - segmentation_mask_layer_id: Optional[str] # id for mask, to be exported as separate tiles - output_directory_path: str # path where the output files will be saved diff --git a/zipdeepness/deepness/common/temp_files_handler.py b/zipdeepness/deepness/common/temp_files_handler.py deleted file mode 100644 index 3962e23e5a0712c45b10df7d6e163704ca0004fb..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/common/temp_files_handler.py +++ /dev/null @@ -1,19 +0,0 @@ -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/zipdeepness/deepness/deepness.py b/zipdeepness/deepness/deepness.py deleted file mode 100644 index 9349af480eeae41c149bc1457ec2c5936ab24820..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/deepness.py +++ /dev/null @@ -1,317 +0,0 @@ -"""Main plugin file - entry point for the plugin. - -Links the UI and the processing. - -Skeleton of this file was generate with the QGis plugin to create plugin skeleton - QGIS PluginBuilder -""" - -import logging -import traceback - -from qgis.core import Qgis, QgsApplication, QgsProject, QgsVectorLayer -from qgis.gui import QgisInterface -from qgis.PyQt.QtCore import QCoreApplication, Qt -from qgis.PyQt.QtGui import QIcon -from qgis.PyQt.QtWidgets import QAction, QMessageBox - -from deepness.common.defines import IS_DEBUG, PLUGIN_NAME -from deepness.common.lazy_package_loader import LazyPackageLoader -from deepness.common.processing_parameters.map_processing_parameters import MapProcessingParameters, ProcessedAreaType -from deepness.common.processing_parameters.training_data_export_parameters import TrainingDataExportParameters -from deepness.deepness_dockwidget import DeepnessDockWidget -from deepness.dialogs.resizable_message_box import ResizableMessageBox -from deepness.images.get_image_path import get_icon_path -from deepness.processing.map_processor.map_processing_result import (MapProcessingResult, MapProcessingResultCanceled, - MapProcessingResultFailed, - MapProcessingResultSuccess) -from deepness.processing.map_processor.map_processor_training_data_export import MapProcessorTrainingDataExport - -cv2 = LazyPackageLoader('cv2') - - -class Deepness: - """ QGIS Plugin Implementation - main class of the plugin. - Creates the UI classes and processing models and links them together. - """ - - def __init__(self, iface: QgisInterface): - """ - :param iface: An interface instance that will be passed to this class - which provides the hook by which you can manipulate the QGIS - application at run time. - :type iface: QgsInterface - """ - self.iface = iface - - # Declare instance attributes - self.actions = [] - self.menu = self.tr(u'&Deepness') - - self.toolbar = self.iface.addToolBar(u'Deepness') - self.toolbar.setObjectName(u'Deepness') - - self.pluginIsActive = False - self.dockwidget = None - self._map_processor = None - - # noinspection PyMethodMayBeStatic - def tr(self, message): - """Get the translation for a string using Qt translation API. - - We implement this ourselves since we do not inherit QObject. - - :param message: String for translation. - :type message: str, QString - - :returns: Translated version of message. - :rtype: QString - """ - # noinspection PyTypeChecker,PyArgumentList,PyCallByClass - return QCoreApplication.translate('Deepness', message) - - def add_action( - self, - icon_path, - text, - callback, - enabled_flag=True, - add_to_menu=True, - add_to_toolbar=True, - status_tip=None, - whats_this=None, - parent=None): - """Add a toolbar icon to the toolbar. - - :param icon_path: Path to the icon for this action. Can be a resource - path (e.g. ':/plugins/foo/bar.png') or a normal file system path. - :type icon_path: str - - :param text: Text that should be shown in menu items for this action. - :type text: str - - :param callback: Function to be called when the action is triggered. - :type callback: function - - :param enabled_flag: A flag indicating if the action should be enabled - by default. Defaults to True. - :type enabled_flag: bool - - :param add_to_menu: Flag indicating whether the action should also - be added to the menu. Defaults to True. - :type add_to_menu: bool - - :param add_to_toolbar: Flag indicating whether the action should also - be added to the toolbar. Defaults to True. - :type add_to_toolbar: bool - - :param status_tip: Optional text to show in a popup when mouse pointer - hovers over the action. - :type status_tip: str - - :param parent: Parent widget for the new action. Defaults None. - :type parent: QWidget - - :param whats_this: Optional text to show in the status bar when the - mouse pointer hovers over the action. - - :returns: The action that was created. Note that the action is also - added to self.actions list. - :rtype: QAction - """ - - icon = QIcon(icon_path) - action = QAction(icon, text, parent) - action.triggered.connect(callback) - action.setEnabled(enabled_flag) - - if status_tip is not None: - action.setStatusTip(status_tip) - - if whats_this is not None: - action.setWhatsThis(whats_this) - - if add_to_toolbar: - self.toolbar.addAction(action) - - if add_to_menu: - self.iface.addPluginToMenu( - self.menu, - action) - - self.actions.append(action) - - return action - - def initGui(self): - """Create the menu entries and toolbar icons inside the QGIS GUI.""" - - icon_path = get_icon_path() - self.add_action( - icon_path, - text=self.tr(u'Deepness'), - callback=self.run, - parent=self.iface.mainWindow()) - - if IS_DEBUG: - self.run() - - def onClosePlugin(self): - """Cleanup necessary items here when plugin dockwidget is closed""" - - # disconnects - self.dockwidget.closingPlugin.disconnect(self.onClosePlugin) - - # remove this statement if dockwidget is to remain - # for reuse if plugin is reopened - # Commented next statement since it causes QGIS crashe - # when closing the docked window: - # self.dockwidget = None - - self.pluginIsActive = False - - def unload(self): - """Removes the plugin menu item and icon from QGIS GUI.""" - - for action in self.actions: - self.iface.removePluginMenu( - self.tr(u'&Deepness'), - action) - self.iface.removeToolBarIcon(action) - # remove the toolbar - del self.toolbar - - def _layers_changed(self, _): - pass - - def run(self): - """Run method that loads and starts the plugin""" - - if not self.pluginIsActive: - self.pluginIsActive = True - - # dockwidget may not exist if: - # first run of plugin - # removed on close (see self.onClosePlugin method) - if self.dockwidget is None: - # Create the dockwidget (after translation) and keep reference - self.dockwidget = DeepnessDockWidget(self.iface) - self._layers_changed(None) - QgsProject.instance().layersAdded.connect(self._layers_changed) - QgsProject.instance().layersRemoved.connect(self._layers_changed) - - # connect to provide cleanup on closing of dockwidget - self.dockwidget.closingPlugin.connect(self.onClosePlugin) - self.dockwidget.run_model_inference_signal.connect(self._run_model_inference) - self.dockwidget.run_training_data_export_signal.connect(self._run_training_data_export) - - self.iface.addDockWidget(Qt.RightDockWidgetArea, self.dockwidget) - self.dockwidget.show() - - def _are_map_processing_parameters_are_correct(self, params: MapProcessingParameters): - if self._map_processor and self._map_processor.is_busy(): - msg = "Error! Processing already in progress! Please wait or cancel previous task." - self.iface.messageBar().pushMessage(PLUGIN_NAME, msg, level=Qgis.Critical, duration=7) - return False - - rlayer = QgsProject.instance().mapLayers()[params.input_layer_id] - if rlayer is None: - msg = "Error! Please select the layer to process first!" - self.iface.messageBar().pushMessage(PLUGIN_NAME, msg, level=Qgis.Critical, duration=7) - return False - - if isinstance(rlayer, QgsVectorLayer): - msg = "Error! Please select a raster layer (vector layer selected)" - self.iface.messageBar().pushMessage(PLUGIN_NAME, msg, level=Qgis.Critical, duration=7) - return False - - return True - - def _display_processing_started_info(self): - msg = "Processing in progress... Cool! It's tea time!" - self.iface.messageBar().pushMessage(PLUGIN_NAME, msg, level=Qgis.Info, duration=2) - - def _run_training_data_export(self, training_data_export_parameters: TrainingDataExportParameters): - if not self._are_map_processing_parameters_are_correct(training_data_export_parameters): - return - - vlayer = None - - rlayer = QgsProject.instance().mapLayers()[training_data_export_parameters.input_layer_id] - if training_data_export_parameters.processed_area_type == ProcessedAreaType.FROM_POLYGONS: - vlayer = QgsProject.instance().mapLayers()[training_data_export_parameters.mask_layer_id] - - self._map_processor = MapProcessorTrainingDataExport( - rlayer=rlayer, - vlayer_mask=vlayer, # layer with masks - map_canvas=self.iface.mapCanvas(), - params=training_data_export_parameters) - self._map_processor.finished_signal.connect(self._map_processor_finished) - self._map_processor.show_img_signal.connect(self._show_img) - QgsApplication.taskManager().addTask(self._map_processor) - self._display_processing_started_info() - - def _run_model_inference(self, params: MapProcessingParameters): - from deepness.processing.models.model_types import ModelDefinition # import here to avoid pulling external dependencies to early - - if not self._are_map_processing_parameters_are_correct(params): - return - - vlayer = None - - rlayer = QgsProject.instance().mapLayers()[params.input_layer_id] - if params.processed_area_type == ProcessedAreaType.FROM_POLYGONS: - vlayer = QgsProject.instance().mapLayers()[params.mask_layer_id] - - model_definition = ModelDefinition.get_definition_for_params(params) - map_processor_class = model_definition.map_processor_class - - self._map_processor = map_processor_class( - rlayer=rlayer, - vlayer_mask=vlayer, - map_canvas=self.iface.mapCanvas(), - params=params) - self._map_processor.finished_signal.connect(self._map_processor_finished) - self._map_processor.show_img_signal.connect(self._show_img) - QgsApplication.taskManager().addTask(self._map_processor) - self._display_processing_started_info() - - @staticmethod - def _show_img(img_rgb, window_name: str): - """ Helper function to show an image while developing and debugging the plugin """ - # We are importing it here, because it is debug tool, - # and we don't want to have it in the main scope from the project startup - img_bgr = img_rgb[..., ::-1] - cv2.namedWindow(window_name, cv2.WINDOW_NORMAL) - cv2.resizeWindow(window_name, 800, 800) - cv2.imshow(window_name, img_bgr) - cv2.waitKey(1) - - def _map_processor_finished(self, result: MapProcessingResult): - """ Slot for finished processing of the ortophoto """ - if isinstance(result, MapProcessingResultFailed): - 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: ' \ - f'{str(result.exception.__class__.__name__)} - {result.exception}\n' \ - f'Last Traceback: \n' \ - f'{trace}' - QMessageBox.critical(self.dockwidget, "Unhandled exception", msg) - elif isinstance(result, MapProcessingResultCanceled): - msg = f'Info! Processing canceled by user!' - self.iface.messageBar().pushMessage(PLUGIN_NAME, msg, level=Qgis.Info, duration=7) - elif isinstance(result, MapProcessingResultSuccess): - msg = 'Processing finished!' - self.iface.messageBar().pushMessage(PLUGIN_NAME, msg, level=Qgis.Success, duration=3) - message_to_show = result.message - - msgBox = ResizableMessageBox(self.dockwidget) - msgBox.setWindowTitle("Processing Result") - msgBox.setText(message_to_show) - msgBox.setStyleSheet("QLabel{min-width:800 px; font-size: 24px;} QPushButton{ width:250px; font-size: 18px; }") - msgBox.exec() - - self._map_processor = None diff --git a/zipdeepness/deepness/deepness_dockwidget.py b/zipdeepness/deepness/deepness_dockwidget.py deleted file mode 100644 index 24d1409e670d0bebb3b661e6d0b9572cbc6e3d25..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/deepness_dockwidget.py +++ /dev/null @@ -1,603 +0,0 @@ -""" -This file contain the main widget of the plugin -""" - -import logging -import os -from typing import Optional - -from qgis.core import Qgis, QgsMapLayerProxyModel, QgsProject -from qgis.PyQt import QtWidgets, uic -from qgis.PyQt.QtCore import pyqtSignal -from qgis.PyQt.QtWidgets import QComboBox, QFileDialog, QMessageBox - -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.lazy_package_loader import LazyPackageLoader -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, ProcessedAreaType -from deepness.common.processing_parameters.recognition_parameters import RecognitionParameters -from deepness.common.processing_parameters.regression_parameters import RegressionParameters -from deepness.common.processing_parameters.segmentation_parameters import SegmentationParameters -from deepness.common.processing_parameters.superresolution_parameters import SuperresolutionParameters -from deepness.common.processing_parameters.training_data_export_parameters import TrainingDataExportParameters -from deepness.processing.models.model_base import ModelBase -from deepness.widgets.input_channels_mapping.input_channels_mapping_widget import InputChannelsMappingWidget -from deepness.widgets.training_data_export_widget.training_data_export_widget import TrainingDataExportWidget - -FORM_CLASS, _ = uic.loadUiType(os.path.join( - os.path.dirname(__file__), 'deepness_dockwidget.ui')) - - -class DeepnessDockWidget(QtWidgets.QDockWidget, FORM_CLASS): - """ - Main widget of the plugin. - 'Dock' means it is a 'dcoked' widget, embedded in the QGis application window. - - The UI design is defined in the `deepness_dockwidget.ui` fiel - recommended to be open in QtDesigner. - Note: Default values for ui edits are based on 'ConfigEntryKey' default value, not taken from the UI form. - """ - - closingPlugin = pyqtSignal() - run_model_inference_signal = pyqtSignal(MapProcessingParameters) # run Segmentation or Detection - run_training_data_export_signal = pyqtSignal(TrainingDataExportParameters) - - def __init__(self, iface, parent=None): - super(DeepnessDockWidget, self).__init__(parent) - self.iface = iface - self._model = None # type: Optional[ModelBase] - self.setupUi(self) - - self._input_channels_mapping_widget = InputChannelsMappingWidget(self) # mapping of model and input ortophoto channels - self._training_data_export_widget = TrainingDataExportWidget(self) # widget with UI for data export tool - - self._create_connections() - self._setup_misc_ui() - self._load_ui_from_config() - - def _show_debug_warning(self): - """ Show label with warning if we are running debug mode """ - self.label_debugModeWarning.setVisible(IS_DEBUG) - - def _load_ui_from_config(self): - """ Load the UI values from the project configuration - """ - layers = QgsProject.instance().mapLayers() - - try: - input_layer_id = ConfigEntryKey.INPUT_LAYER_ID.get() - if input_layer_id and input_layer_id in layers: - self.mMapLayerComboBox_inputLayer.setLayer(layers[input_layer_id]) - - processed_area_type_txt = ConfigEntryKey.PROCESSED_AREA_TYPE.get() - self.comboBox_processedAreaSelection.setCurrentText(processed_area_type_txt) - - model_type_txt = ConfigEntryKey.MODEL_TYPE.get() - self.comboBox_modelType.setCurrentText(model_type_txt) - - self._input_channels_mapping_widget.load_ui_from_config() - self._training_data_export_widget.load_ui_from_config() - - # NOTE: load the model after setting the model_type above - model_file_path = ConfigEntryKey.MODEL_FILE_PATH.get() - if model_file_path: - self.lineEdit_modelPath.setText(model_file_path) - self._load_model_and_display_info(abort_if_no_file_path=True) # to prepare other ui components - - # needs to be loaded after the model is set up - 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( - ConfigEntryKey.SEGMENTATION_PROBABILITY_THRESHOLD_VALUE.get()) - self.checkBox_pixelClassEnableThreshold.setChecked( - ConfigEntryKey.SEGMENTATION_PROBABILITY_THRESHOLD_ENABLED.get()) - self._set_probability_threshold_enabled() - self.spinBox_dilateErodeSize.setValue( - ConfigEntryKey.SEGMENTATION_REMOVE_SMALL_SEGMENT_SIZE.get()) - self.checkBox_removeSmallAreas.setChecked( - ConfigEntryKey.SEGMENTATION_REMOVE_SMALL_SEGMENT_ENABLED.get()) - self._set_remove_small_segment_enabled() - - self.doubleSpinBox_regressionScaling.setValue(ConfigEntryKey.REGRESSION_OUTPUT_SCALING.get()) - - self.doubleSpinBox_confidence.setValue(ConfigEntryKey.DETECTION_CONFIDENCE.get()) - self.doubleSpinBox_iouScore.setValue(ConfigEntryKey.DETECTION_IOU.get()) - self.comboBox_detectorType.setCurrentText(ConfigEntryKey.DETECTOR_TYPE.get()) - except Exception: - logging.exception("Failed to load the ui state from config!") - - def _save_ui_to_config(self): - """ Save value from the UI forms to the project config - """ - ConfigEntryKey.MODEL_FILE_PATH.set(self.lineEdit_modelPath.text()) - ConfigEntryKey.INPUT_LAYER_ID.set(self._get_input_layer_id()) - ConfigEntryKey.MODEL_TYPE.set(self.comboBox_modelType.currentText()) - ConfigEntryKey.PROCESSED_AREA_TYPE.set(self.comboBox_processedAreaSelection.currentText()) - - 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( - self.checkBox_pixelClassEnableThreshold.isChecked()) - ConfigEntryKey.SEGMENTATION_PROBABILITY_THRESHOLD_VALUE.set(self.doubleSpinBox_probabilityThreshold.value()) - ConfigEntryKey.SEGMENTATION_REMOVE_SMALL_SEGMENT_ENABLED.set( - self.checkBox_removeSmallAreas.isChecked()) - ConfigEntryKey.SEGMENTATION_REMOVE_SMALL_SEGMENT_SIZE.set(self.spinBox_dilateErodeSize.value()) - - ConfigEntryKey.REGRESSION_OUTPUT_SCALING.set(self.doubleSpinBox_regressionScaling.value()) - - ConfigEntryKey.DETECTION_CONFIDENCE.set(self.doubleSpinBox_confidence.value()) - ConfigEntryKey.DETECTION_IOU.set(self.doubleSpinBox_iouScore.value()) - ConfigEntryKey.DETECTOR_TYPE.set(self.comboBox_detectorType.currentText()) - - self._input_channels_mapping_widget.save_ui_to_config() - self._training_data_export_widget.save_ui_to_config() - - def _rlayer_updated(self): - self._input_channels_mapping_widget.set_rlayer(self._get_input_layer()) - - def _setup_misc_ui(self): - """ Setup some misceleounous ui forms - """ - from deepness.processing.models.model_types import \ - ModelDefinition # import here to avoid pulling external dependencies to early - - self._show_debug_warning() - combobox = self.comboBox_processedAreaSelection - for name in ProcessedAreaType.get_all_names(): - combobox.addItem(name) - - self.verticalLayout_inputChannelsMapping.addWidget(self._input_channels_mapping_widget) - self.verticalLayout_trainingDataExport.addWidget(self._training_data_export_widget) - - self.mMapLayerComboBox_inputLayer.setFilters(QgsMapLayerProxyModel.RasterLayer) - self.mMapLayerComboBox_areaMaskLayer.setFilters(QgsMapLayerProxyModel.VectorLayer) - - self.mGroupBox_8.setCollapsed(True) # collapse the group by default - 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) - - for detector_type in DetectorType.get_all_display_values(): - self.comboBox_detectorType.addItem(detector_type) - self._detector_type_changed() - - self._rlayer_updated() # to force refresh the dependant ui elements - - def _set_processed_area_mask_options(self): - show_mask_combobox = (self.get_selected_processed_area_type() == ProcessedAreaType.FROM_POLYGONS) - self.mMapLayerComboBox_areaMaskLayer.setVisible(show_mask_combobox) - self.label_areaMaskLayer.setVisible(show_mask_combobox) - - def get_selected_processed_area_type(self) -> ProcessedAreaType: - combobox = self.comboBox_processedAreaSelection # type: QComboBox - txt = combobox.currentText() - return ProcessedAreaType(txt) - - def _create_connections(self): - self.pushButton_runInference.clicked.connect(self._run_inference) - self.pushButton_runTrainingDataExport.clicked.connect(self._run_training_data_export) - self.pushButton_browseQueryImagePath.clicked.connect(self._browse_query_image_path) - self.pushButton_browseModelPath.clicked.connect(self._browse_model_path) - self.comboBox_processedAreaSelection.currentIndexChanged.connect(self._set_processed_area_mask_options) - self.comboBox_modelType.currentIndexChanged.connect(self._model_type_changed) - self.comboBox_detectorType.currentIndexChanged.connect(self._detector_type_changed) - self.pushButton_reloadModel.clicked.connect(self._load_model_and_display_info) - self.pushButton_loadDefaultModelParameters.clicked.connect(self._load_default_model_parameters) - self.mMapLayerComboBox_inputLayer.layerChanged.connect(self._rlayer_updated) - self.checkBox_pixelClassEnableThreshold.stateChanged.connect(self._set_probability_threshold_enabled) - self.checkBox_removeSmallAreas.stateChanged.connect(self._set_remove_small_segment_enabled) - 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): - from deepness.processing.models.model_types import \ - ModelType # import here to avoid pulling external dependencies to early - - model_type = ModelType(self.comboBox_modelType.currentText()) - - segmentation_enabled = False - detection_enabled = False - regression_enabled = False - superresolution_enabled = False - recognition_enabled = False - - if model_type == ModelType.SEGMENTATION: - segmentation_enabled = True - elif model_type == ModelType.DETECTION: - detection_enabled = True - elif model_type == ModelType.REGRESSION: - regression_enabled = True - elif model_type == ModelType.SUPERRESOLUTION: - superresolution_enabled = True - elif model_type == ModelType.RECOGNITION: - recognition_enabled = True - else: - raise Exception(f"Unsupported model type ({model_type})!") - - self.mGroupBox_segmentationParameters.setVisible(segmentation_enabled) - self.mGroupBox_detectionParameters.setVisible(detection_enabled) - self.mGroupBox_regressionParameters.setVisible(regression_enabled) - self.mGroupBox_superresolutionParameters.setVisible(superresolution_enabled) - self.mGroupBox_recognitionParameters.setVisible(recognition_enabled) - - def _detector_type_changed(self): - detector_type = DetectorType(self.comboBox_detectorType.currentText()) - self.label_detectorTypeDescription.setText(detector_type.get_formatted_description()) - - 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()) - - def _set_remove_small_segment_enabled(self): - self.spinBox_dilateErodeSize.setEnabled(self.checkBox_removeSmallAreas.isChecked()) - - def _browse_model_path(self): - file_path, _ = QFileDialog.getOpenFileName( - self, - 'Select Model ONNX file...', - os.path.expanduser('~'), - 'All files (*.*);; ONNX files (*.onnx)') - if file_path: - self.lineEdit_modelPath.setText(file_path) - self._load_model_and_display_info() - - def _browse_query_image_path(self): - file_path, _ = QFileDialog.getOpenFileName( - self, - "Select image file...", - os.path.expanduser("~"), - "All files (*.*)", - ) - if file_path: - self.lineEdit_recognitionPath.setText(file_path) - - def _load_default_model_parameters(self): - """ - Load the default parameters from model metadata - """ - 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: - self.spinBox_tileSize_px.setValue(value) - - value = self._model.get_metadata_tiles_overlap() - if value is not None: - self.spinBox_processingTileOverlapPercentage.setValue(value) - - value = self._model.get_metadata_model_type() - if value is not None: - print(f'{value =}') - self.comboBox_modelType.setCurrentText(value) - - value = self._model.get_detector_type() - if value is not None: - self.comboBox_detectorType.setCurrentText(value) - - value = self._model.get_metadata_segmentation_threshold() - if value is not None: - self.checkBox_pixelClassEnableThreshold.setChecked(bool(value != 0)) - self.doubleSpinBox_probabilityThreshold.setValue(value) - - value = self._model.get_metadata_segmentation_small_segment() - if value is not None: - self.checkBox_removeSmallAreas.setChecked(bool(value != 0)) - self.spinBox_dilateErodeSize.setValue(value) - - value = self._model.get_metadata_regression_output_scaling() - if value is not None: - self.doubleSpinBox_regressionScaling.setValue(value) - - value = self._model.get_metadata_detection_confidence() - if value is not None: - self.doubleSpinBox_confidence.setValue(value) - - value = self._model.get_metadata_detection_iou_threshold() - if value is not None: - self.doubleSpinBox_iouScore.setValue(value) - - def _load_model_with_type_from_metadata(self, model_class_from_ui, file_path): - """ - If model has model_type in metadata - use this type to create proper model class. - Otherwise model_class_from_ui will be used - """ - from deepness.processing.models.model_types import ( # import here to avoid pulling external dependencies to early - ModelDefinition, ModelType) - - model_class = model_class_from_ui - - model_type_str_from_metadata = ModelBase.get_model_type_from_metadata(file_path) - if model_type_str_from_metadata is not None: - model_type = ModelType(model_type_str_from_metadata) - model_class = ModelDefinition.get_definition_for_type(model_type).model_class - self.comboBox_modelType.setCurrentText(model_type.value) - - print(f'{model_type_str_from_metadata = }, {model_class = }') - - model = model_class(file_path) - return model - - def _load_model_and_display_info(self, abort_if_no_file_path: bool = False): - """ - Tries to load the model and display its message. - """ - import deepness.processing.models.detector as detector_module # import here to avoid pulling external dependencies to early - from deepness.processing.models.model_types import \ - ModelType # import here to avoid pulling external dependencies to early - - file_path = self.lineEdit_modelPath.text() - - if not file_path and abort_if_no_file_path: - return - - txt = '' - - try: - model_definition = self.get_selected_model_class_definition() - model_class = model_definition.model_class - self._model = self._load_model_with_type_from_metadata( - model_class_from_ui=model_class, - file_path=file_path) - self._model.check_loaded_model_outputs() - input_0_shape = self._model.get_input_shape() - txt += 'Legend: [BATCH_SIZE, CHANNELS, HEIGHT, WIDTH]\n' - txt += 'Inputs:\n' - txt += f'\t- Input: {input_0_shape}\n' - input_size_px = input_0_shape[-1] - batch_size = self._model.get_model_batch_size() - - txt += 'Outputs:\n' - - for i, output_shape in enumerate(self._model.get_output_shapes()): - txt += f'\t- Output {i}: {output_shape}\n' - - # 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 - if model_class == ModelType.SUPERRESOLUTION: - output_0_shape = self._model.get_output_shape() - scale_factor = output_0_shape[-1] / input_size_px - self.doubleSpinBox_superresolutionScaleFactor.setValue(int(scale_factor)) - # Disable output format options for super-resolution models - except Exception as e: - if IS_DEBUG: - raise e - txt = "Error! Failed to load the model!\n" \ - "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}' - QMessageBox.critical(self, "Error!", msg) - - self.label_modelInfo.setText(txt) - - if isinstance(self._model, detector_module.Detector): - detector_type = DetectorType(self.comboBox_detectorType.currentText()) - self._model.set_model_type_param(detector_type) - - def get_mask_layer_id(self): - if not self.get_selected_processed_area_type() == ProcessedAreaType.FROM_POLYGONS: - return None - - mask_layer_id = self.mMapLayerComboBox_areaMaskLayer.currentLayer().id() - return mask_layer_id - - def _get_input_layer(self): - return self.mMapLayerComboBox_inputLayer.currentLayer() - - def _get_input_layer_id(self): - layer = self._get_input_layer() - if layer: - return layer.id() - 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 - return self.doubleSpinBox_probabilityThreshold.value() - - def get_selected_model_class_definition(self): # -> ModelDefinition: # we cannot import it here yet - """ - Get the currently selected model class (in UI) - """ - from deepness.processing.models.model_types import ( # import here to avoid pulling external dependencies to early - ModelDefinition, ModelType) - - model_type_txt = self.comboBox_modelType.currentText() - model_type = ModelType(model_type_txt) - model_definition = ModelDefinition.get_definition_for_type(model_type) - return model_definition - - def get_inference_parameters(self) -> MapProcessingParameters: - """ Get the parameters for the model interface. - The returned type is derived from `MapProcessingParameters` class, depending on the selected model type. - """ - from deepness.processing.models.model_types import \ - ModelType # import here to avoid pulling external dependencies to early - - map_processing_parameters = self._get_map_processing_parameters() - - if self._model is None: - raise OperationFailedException("Please select and load a model first!") - - model_type = self.get_selected_model_class_definition().model_type - if model_type == ModelType.SEGMENTATION: - params = self.get_segmentation_parameters(map_processing_parameters) - elif model_type == ModelType.REGRESSION: - params = self.get_regression_parameters(map_processing_parameters) - elif model_type == ModelType.SUPERRESOLUTION: - params = self.get_superresolution_parameters(map_processing_parameters) - elif model_type == ModelType.RECOGNITION: - params = self.get_recognition_parameters(map_processing_parameters) - elif model_type == ModelType.DETECTION: - params = self.get_detection_parameters(map_processing_parameters) - - else: - raise Exception(f"Unknown model type '{model_type}'!") - - return params - - def get_segmentation_parameters(self, map_processing_parameters: MapProcessingParameters) -> SegmentationParameters: - postprocessing_dilate_erode_size = self.spinBox_dilateErodeSize.value() \ - if self.checkBox_removeSmallAreas.isChecked() else 0 - - params = SegmentationParameters( - **map_processing_parameters.__dict__, - postprocessing_dilate_erode_size=postprocessing_dilate_erode_size, - pixel_classification__probability_threshold=self._get_pixel_classification_threshold(), - model=self._model, - ) - return params - - def get_regression_parameters(self, map_processing_parameters: MapProcessingParameters) -> RegressionParameters: - params = RegressionParameters( - **map_processing_parameters.__dict__, - output_scaling=self.doubleSpinBox_regressionScaling.value(), - model=self._model, - ) - return params - - def get_superresolution_parameters(self, map_processing_parameters: MapProcessingParameters) -> SuperresolutionParameters: - params = SuperresolutionParameters( - **map_processing_parameters.__dict__, - model=self._model, - scale_factor=self.doubleSpinBox_superresolutionScaleFactor.value(), - output_scaling=self.doubleSpinBox_superresolutionScaling.value(), - ) - return params - - def get_recognition_parameters(self, map_processing_parameters: MapProcessingParameters) -> RecognitionParameters: - params = RecognitionParameters( - **map_processing_parameters.__dict__, - model=self._model, - query_image_path=self.lineEdit_recognitionPath.text(), - ) - return params - - def get_detection_parameters(self, map_processing_parameters: MapProcessingParameters) -> DetectionParameters: - - params = DetectionParameters( - **map_processing_parameters.__dict__, - confidence=self.doubleSpinBox_confidence.value(), - iou_threshold=self.doubleSpinBox_iouScore.value(), - model=self._model, - detector_type=DetectorType(self.comboBox_detectorType.currentText()), - ) - - return params - - def _get_map_processing_parameters(self) -> MapProcessingParameters: - """ - Get common parameters for inference and exporting - """ - processed_area_type = self.get_selected_processed_area_type() - 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(), - processing_overlap=self._get_overlap_parameter(), - input_channels_mapping=self._input_channels_mapping_widget.get_channels_mapping(), - ) - return params - - def _run_inference(self): - # check_required_packages_and_install_if_necessary() - try: - params = self.get_inference_parameters() - - if not params.input_layer_id: - raise OperationFailedException("Please select an input layer first!") - 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 - - self._save_ui_to_config() - self.run_model_inference_signal.emit(params) - - def _run_training_data_export(self): - # check_required_packages_and_install_if_necessary() - try: - map_processing_parameters = self._get_map_processing_parameters() - training_data_export_parameters = self._training_data_export_widget.get_training_data_export_parameters( - map_processing_parameters) - - if not map_processing_parameters.input_layer_id: - raise OperationFailedException("Please select an input layer first!") - - # Overwrite common parameter - we don't want channels mapping as for the model, - # but just to take all channels - training_data_export_parameters.input_channels_mapping = \ - self._input_channels_mapping_widget.get_channels_mapping_for_training_data_export() - 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 - - self._save_ui_to_config() - self.run_training_data_export_signal.emit(training_data_export_parameters) - - def closeEvent(self, event): - self.closingPlugin.emit() - event.accept() diff --git a/zipdeepness/deepness/deepness_dockwidget.ui b/zipdeepness/deepness/deepness_dockwidget.ui deleted file mode 100644 index 78c6447444eff0437ada7243227bb0df556909a4..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/deepness_dockwidget.ui +++ /dev/null @@ -1,893 +0,0 @@ - - - DeepnessDockWidgetBase - - - - 0 - 0 - 486 - 1368 - - - - Deepness - - - - - - - true - - - - - 0 - -268 - 452 - 1564 - - - - - 0 - - - 0 - - - 0 - - - - - - 9 - true - - - - color:rgb(198, 70, 0) - - - WARNING: Running plugin in DEBUG mode -(because env variable IS_DEBUG=true) - - - - - - - - 0 - 0 - - - - Input data: - - - - - - <html><head/><body><p><span style=" font-weight:600;">Layer which will be processed.</span></p><p>Most probably this is your ortophoto or map source (like satellite image from google earth).<br/>Needs to be a raster layer.</p></body></html> - - - - - - - <html><head/><body><p>Defines what part of the &quot;Input layer&quot; should be processed.</p><p><br/> - &quot;<span style=" font-style:italic;">Visible Part</span>&quot; allows to process the part currently visible on the map canvas.<br/> - &quot;<span style=" font-style:italic;">Entire Layer</span>&quot; allows to process the entire ortophoto file<br/> - &quot;<span style=" font-style:italic;">From Polygons</span>&quot; allows to select a polygon describing the area to be processed (e.g. if the processed field is a polygon, and we don't want to process outside of it)</p></body></html> - - - - - - - Input layer: - - - - - - - Processed area mask: - - - - - - - Area mask layer: - - - - - - - <html><head/><body><p>Defines the layer which is being used as a mask for the processing of &quot;Input layer&quot;. <br/>Only pixels within this mask layer will be processed.</p><p>Needs to be a vector layer.</p></body></html> - - - - - - - - - - - 0 - 0 - - - - ONNX Model - - - - - - true - - - Path to the model file - - - - - - - Model file path: - - - - - - - true - - - Browse... - - - - - - - Model type: - - - - - - - <html><head/><body><p>Type of the model (model class) which you want to use.<br/>You should obtain this information along with the model file.</p><p>Please refer to the plugin documentation for more details.</p></body></html> - - - - - - - Model info: - - - - - - - - 7 - - - - color: rgb(135, 135, 133); - - - Model not loaded! Please select its path and click "Load Model" button above first! - - - true - - - - - - - - - Reload the model given in the line edit above - - - Reload Model - - - - - - - <html><head/><body><p><span style=" font-weight:600;">Load default model parameters.</span></p><p>ONNX Models can have metadata, which can be parsed and used to set default value for fields in UI, w.g. for tile_size or confidence threshold</p></body></html> - - - Load default parameters - - - - - - - - - - - - - 0 - 0 - - - - Input channels mapping - - - - - - - - - - true - - - - NOTE: This configuration is depending on the input layer and model type. Please make sure to select the "Input layer" and load the model first! - - - true - - - - - - - - - - - 0 - 0 - - - - Processing parameters - - - - - - Tiles overlap: - - - - - - [px] - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 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 - - - - - - - [%] - - - true - - - - - - - - - - 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]: - - - - - - - - false - true - - - - NOTE: These options may be a fixed value for some models - - - true - - - - - - - Batch size: - - - - - - - <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 - - - 9999999 - - - - - - - <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 - - - - - - - - - - - 0 - 0 - - - - Segmentation parameters - - - - - - <html><head/><body><p>Minimum required probability for the class to be considered as belonging to this class.</p></body></html> - - - 2 - - - 1.000000000000000 - - - 0.050000000000000 - - - 0.500000000000000 - - - - - - - false - - - - - - Argmax (most probable class only) - - - true - - - - - - - Apply class probability threshold: - - - true - - - - - - - <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 - - - - - - - - true - - - - NOTE: Applicable only if a segmentation model is used - - - - - - - Remove small segment - areas (dilate/erode size) [px]: - - - true - - - - - - - - - - - 0 - 0 - - - - Superresolution Parameters - - - - - - Upscaling Factor - - - - - - - 2 - - - - - - - 100000000000.000000000000000 - - - 255.000000000000000 - - - - - - - Output scaling - - - - - - - - - - - 0 - 0 - - - - Regression parameters - - - - - - <html><head/><body><p>Scaling factor for model output values.</p><p>Each pixel value will be multiplied by this factor.</p></body></html> - - - 3 - - - 9999.000000000000000 - - - 1.000000000000000 - - - - - - - Output scaling -(keep 1.00 if max output value is 1): - - - - - - - - - - - 0 - 0 - - - - Recognition parameters - - - - - - - true - - - - NOTE: Applicable only if a recognition model is used - - - - - - - Image to localize path: - - - - - - - - - - Browse - - - - - - - - - - Detection parameters - - - - - - Confidence: - - - - - - - Detector type: - - - - - - - - - - IoU threshold: - - - - - - - <html><head/><body><p>Minimal confidence of the potential detection, to consider it as a detection.</p></body></html> - - - 2 - - - 1.000000000000000 - - - 0.050000000000000 - - - - - - - - true - - - - NOTE: Applicable only if a detection model is used - - - - - - - - 9 - - - - Here goes a longer description of detector type... - - - - - - - <html><head/><body><p>Parameter used in Non Maximum Suppression in post processing.</p><p>Defines the threshold of overlap between to neighbouring detections, to consider them as the same object.</p></body></html> - - - 2 - - - 1.000000000000000 - - - 0.050000000000000 - - - - - - - - - - - 0 - 0 - - - - Training data export - - - false - - - - - - Note: This group allows to export the data for the training process, with similar data as during inference. - - - true - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - <html><head/><body><p>Run the export of the data</p></body></html> - - - Export training data - - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - - - <html><head/><body><p>Run the inference, for the selected above paraeters.</p></body></html> - - - Run - - - - - - - - - - - QgsCollapsibleGroupBox - QGroupBox -
qgscollapsiblegroupbox.h
- 1 -
- - QgsDoubleSpinBox - QDoubleSpinBox -
qgsdoublespinbox.h
-
- - QgsMapLayerComboBox - QComboBox -
qgsmaplayercombobox.h
-
- - QgsSpinBox - QSpinBox -
qgsspinbox.h
-
-
- - -
diff --git a/zipdeepness/deepness/dialogs/packages_installer/packages_installer_dialog.py b/zipdeepness/deepness/dialogs/packages_installer/packages_installer_dialog.py deleted file mode 100644 index 1afdbc8023a73e772ed038e2668e559898d3c17c..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/dialogs/packages_installer/packages_installer_dialog.py +++ /dev/null @@ -1,359 +0,0 @@ -""" -This QGIS plugin requires some Python packages to be installed and available. -This tool allows to install them in a local directory, if they are not installed yet. -""" - -import importlib -import logging -import os -import subprocess -import sys -import traceback -import urllib -from dataclasses import dataclass -from pathlib import Path -from threading import Thread -from typing import List - -from qgis.PyQt import QtCore, uic -from qgis.PyQt.QtCore import pyqtSignal -from qgis.PyQt.QtGui import QCloseEvent -from qgis.PyQt.QtWidgets import QDialog, QMessageBox, QTextBrowser - -from deepness.common.defines import PLUGIN_NAME - -PYTHON_VERSION = sys.version_info -SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -PLUGIN_ROOT_DIR = os.path.realpath(os.path.abspath(os.path.join(SCRIPT_DIR, '..', '..'))) -PACKAGES_INSTALL_DIR = os.path.join(PLUGIN_ROOT_DIR, f'python{PYTHON_VERSION.major}.{PYTHON_VERSION.minor}') - - -FORM_CLASS, _ = uic.loadUiType(os.path.join( - os.path.dirname(__file__), 'packages_installer_dialog.ui')) - -_ERROR_COLOR = '#ff0000' - - -@dataclass -class PackageToInstall: - name: str - version: str - import_name: str # name while importing package - - def __str__(self): - return f'{self.name}{self.version}' - - -REQUIREMENTS_PATH = os.path.join(PLUGIN_ROOT_DIR, 'python_requirements/requirements.txt') - -with open(REQUIREMENTS_PATH, 'r') as f: - raw_txt = f.read() - -libraries_versions = {} - -for line in raw_txt.split('\n'): - if line.startswith('#') or not line.strip(): - continue - - line = line.split(';')[0] - - if '==' in line: - lib, version = line.split('==') - libraries_versions[lib] = '==' + version - elif '>=' in line: - lib, version = line.split('>=') - libraries_versions[lib] = '>=' + version - elif '<=' in line: - lib, version = line.split('<=') - libraries_versions[lib] = '<=' + version - else: - libraries_versions[line] = '' - - -packages_to_install = [ - PackageToInstall(name='opencv-python-headless', version=libraries_versions['opencv-python-headless'], import_name='cv2'), -] - -if sys.platform == "linux" or sys.platform == "linux2": - packages_to_install += [ - PackageToInstall(name='onnxruntime-gpu', version=libraries_versions['onnxruntime-gpu'], import_name='onnxruntime'), - ] - PYTHON_EXECUTABLE_PATH = sys.executable -elif sys.platform == "darwin": # MacOS - packages_to_install += [ - PackageToInstall(name='onnxruntime', version=libraries_versions['onnxruntime-gpu'], import_name='onnxruntime'), - ] - PYTHON_EXECUTABLE_PATH = str(Path(sys.prefix) / 'bin' / 'python3') # sys.executable yields QGIS in macOS -elif sys.platform == "win32": - packages_to_install += [ - PackageToInstall(name='onnxruntime', version=libraries_versions['onnxruntime-gpu'], import_name='onnxruntime'), - ] - PYTHON_EXECUTABLE_PATH = 'python' # sys.executable yields QGis.exe in Windows -else: - raise Exception("Unsupported operating system!") - - -class PackagesInstallerDialog(QDialog, FORM_CLASS): - """ - Dialog witch controls the installation process of packages. - UI design defined in the `packages_installer_dialog.ui` file. - """ - - signal_log_line = pyqtSignal(str) # we need to use signal because we cannot edit GUI from another thread - - INSTALLATION_IN_PROGRESS = False # to make sure we will not start the installation twice - - def __init__(self, iface, parent=None): - super(PackagesInstallerDialog, self).__init__(parent) - self.setupUi(self) - self.iface = iface - self.tb = self.textBrowser_log # type: QTextBrowser - self._create_connections() - self._setup_message() - self.aborted = False - self.thread = None - - def move_to_top(self): - """ Move the window to the top. - Although if installed from plugin manager, the plugin manager will move itself to the top anyway. - """ - self.setWindowState((self.windowState() & ~QtCore.Qt.WindowMinimized) | QtCore.Qt.WindowActive) - - if sys.platform == "linux" or sys.platform == "linux2": - pass - elif sys.platform == "darwin": # MacOS - self.raise_() # FIXME: this does not really work, the window is still behind the plugin manager - elif sys.platform == "win32": - self.activateWindow() - else: - raise Exception("Unsupported operating system!") - - def _create_connections(self): - self.pushButton_close.clicked.connect(self.close) - self.pushButton_install_packages.clicked.connect(self._run_packages_installation) - self.signal_log_line.connect(self._log_line) - - def _log_line(self, txt): - txt = txt \ - .replace(' ', '  ') \ - .replace('\n', '
') - self.tb.append(txt) - - def log(self, txt): - self.signal_log_line.emit(txt) - - def _setup_message(self) -> None: - - self.log(f'

' - f'Plugin {PLUGIN_NAME} - Packages installer

\n' - f'\n' - f'This plugin requires the following Python packages to be installed:') - - for package in packages_to_install: - self.log(f'\t- {package.name}{package.version}') - - self.log('\n\n' - f'If this packages are not installed in the global environment ' - f'(or environment in which QGIS is started) ' - f'you can install these packages in the local directory (which is included to the Python path).\n\n' - f'This Dialog does it for you! (Though you can still install these packages manually instead).\n' - f'Please click "Install packages" button below to install them automatically, ' - f'or "Test and Close" if you installed them manually...\n') - - def _run_packages_installation(self): - if self.INSTALLATION_IN_PROGRESS: - self.log(f'Error! Installation already in progress, cannot start again!') - return - self.aborted = False - self.INSTALLATION_IN_PROGRESS = True - self.thread = Thread(target=self._install_packages) - self.thread.start() - - def _install_packages(self) -> None: - self.log('\n\n') - self.log('=' * 60) - self.log(f'

Attempting to install required packages...

') - os.makedirs(PACKAGES_INSTALL_DIR, exist_ok=True) - - self._install_pip_if_necessary() - - self.log(f'

Attempting to install required packages...

\n') - try: - self._pip_install_packages(packages_to_install) - except Exception as e: - msg = (f'\n ' - f'Packages installation failed with exception: {e}!\n' - f'Please try to install the packages again. ' - f'\nCheck if there is no error related to system packages, ' - f'which may be required to be installed by your system package manager, e.g. "apt". ' - f'Copy errors from the stack above and google for possible solutions. ' - f'Please report these as an issue on the plugin repository tracker!') - self.log(msg) - - # finally, validate the installation, if there was no error so far... - self.log('\n\n Installation of required packages finished. Validating installation...') - self._check_packages_installation_and_log() - self.INSTALLATION_IN_PROGRESS = False - - def reject(self) -> None: - self.close() - - def closeEvent(self, event: QCloseEvent): - self.aborted = True - if self._check_packages_installation_and_log(): - event.accept() - return - - res = QMessageBox.question(self.iface.mainWindow(), - f'{PLUGIN_NAME} - skip installation?', - 'Are you sure you want to abort the installation of the required python packages? ' - 'The plugin may not function correctly without them!', - QMessageBox.No, QMessageBox.Yes) - log_msg = 'User requested to close the dialog, but the packages are not installed correctly!\n' - if res == QMessageBox.Yes: - log_msg += 'And the user confirmed to close the dialog, knowing the risk!' - event.accept() - else: - log_msg += 'The user reconsidered their decision, and will try to install the packages again!' - event.ignore() - log_msg += '\n' - self.log(log_msg) - - def _install_pip_if_necessary(self): - """ - Install pip if not present. - It happens e.g. in flatpak applications. - - TODO - investigate whether we can also install pip in local directory - """ - - self.log(f'

Making sure pip is installed...

') - if check_pip_installed(): - self.log(f'Pip is installed, skipping installation...\n') - return - - install_pip_command = [PYTHON_EXECUTABLE_PATH, '-m', 'ensurepip'] - self.log(f'Running command to install pip: \n $ {" ".join(install_pip_command)} ') - with subprocess.Popen(install_pip_command, - stdout=subprocess.PIPE, - universal_newlines=True, - stderr=subprocess.STDOUT, - env={'SETUPTOOLS_USE_DISTUTILS': 'stdlib'}) as process: - try: - self._do_process_output_logging(process) - except InterruptedError as e: - self.log(str(e)) - return False - - if process.returncode != 0: - msg = (f'' - f'pip installation failed! Consider installing it manually.' - f'') - self.log(msg) - self.log('\n') - - def _pip_install_packages(self, packages: List[PackageToInstall]) -> None: - cmd = [PYTHON_EXECUTABLE_PATH, '-m', 'pip', 'install', '-U', f'--target={PACKAGES_INSTALL_DIR}'] - cmd_string = ' '.join(cmd) - - for pck in packages: - cmd.append(f"{pck}") - cmd_string += f"{pck}" - - self.log(f'Running command: \n $ {cmd_string} ') - with subprocess.Popen(cmd, - stdout=subprocess.PIPE, - universal_newlines=True, - stderr=subprocess.STDOUT) as process: - self._do_process_output_logging(process) - - if process.returncode != 0: - raise RuntimeError('Installation with pip failed') - - msg = (f'\n' - f'Packages installed correctly!' - f'\n\n') - self.log(msg) - - def _do_process_output_logging(self, process: subprocess.Popen) -> None: - """ - :param process: instance of 'subprocess.Popen' - """ - for stdout_line in iter(process.stdout.readline, ""): - if stdout_line.isspace(): - continue - txt = f'{stdout_line.rstrip(os.linesep)}' - self.log(txt) - if self.aborted: - raise InterruptedError('Installation aborted by user') - - def _check_packages_installation_and_log(self) -> bool: - packages_ok = are_packages_importable() - self.pushButton_install_packages.setEnabled(not packages_ok) - - if packages_ok: - msg1 = f'All required packages are importable! You can close this window now!' - self.log(msg1) - return True - - try: - import_packages() - raise Exception("Unexpected successful import of packages?!? It failed a moment ago, we shouldn't be here!") - except Exception: - msg_base = 'Python packages required by the plugin could not be loaded due to the following error:' - logging.exception(msg_base) - tb = traceback.format_exc() - msg1 = (f'' - f'{msg_base} \n ' - f'{tb}\n\n' - f'Please try installing the packages again.' - f'') - self.log(msg1) - - return False - - -dialog = None - - -def import_package(package: PackageToInstall): - importlib.import_module(package.import_name) - - -def import_packages(): - for package in packages_to_install: - import_package(package) - - -def are_packages_importable() -> bool: - try: - import_packages() - except Exception: - logging.exception(f'Python packages required by the plugin could not be loaded due to the following error:') - return False - - return True - - -def check_pip_installed() -> bool: - try: - subprocess.check_output([PYTHON_EXECUTABLE_PATH, '-m', 'pip', '--version']) - return True - except subprocess.CalledProcessError: - return False - - -def check_required_packages_and_install_if_necessary(iface): - os.makedirs(PACKAGES_INSTALL_DIR, exist_ok=True) - if PACKAGES_INSTALL_DIR not in sys.path: - sys.path.append(PACKAGES_INSTALL_DIR) # TODO: check for a less intrusive way to do this - - if are_packages_importable(): - # if packages are importable we are fine, nothing more to do then - return - - global dialog - dialog = PackagesInstallerDialog(iface) - dialog.setWindowModality(QtCore.Qt.WindowModal) - dialog.show() - dialog.move_to_top() diff --git a/zipdeepness/deepness/dialogs/packages_installer/packages_installer_dialog.ui b/zipdeepness/deepness/dialogs/packages_installer/packages_installer_dialog.ui deleted file mode 100644 index 38e6b301ad4ac2b6151bb5783a2d2a37ce1e4450..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/dialogs/packages_installer/packages_installer_dialog.ui +++ /dev/null @@ -1,65 +0,0 @@ - - - PackagesInstallerDialog - - - - 0 - 0 - 693 - 494 - - - - Deepness - Packages Installer Dialog - - - - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 75 - true - - - - Install packages - - - - - - - Test and Close - - - - - - - - - - diff --git a/zipdeepness/deepness/dialogs/resizable_message_box.py b/zipdeepness/deepness/dialogs/resizable_message_box.py deleted file mode 100644 index b2a67948bfbb3df5cba712f6f87d737cfb9a5807..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/dialogs/resizable_message_box.py +++ /dev/null @@ -1,20 +0,0 @@ -from qgis.PyQt.QtWidgets import QMessageBox, QTextEdit - - -class ResizableMessageBox(QMessageBox): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.setSizeGripEnabled(True) - - def event(self, event): - if event.type() in (event.LayoutRequest, event.Resize): - if event.type() == event.Resize: - res = super().event(event) - else: - res = False - details = self.findChild(QTextEdit) - if details: - details.setMaximumSize(16777215, 16777215) - self.setMaximumSize(16777215, 16777215) - return res - return super().event(event) diff --git a/zipdeepness/deepness/images/get_image_path.py b/zipdeepness/deepness/images/get_image_path.py deleted file mode 100644 index 03b5ebfd4ab7d4d50f46e4d7e30c3dc0c34cdff2..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/images/get_image_path.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -This file contains image related functionalities -""" - -import os - -SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) - - -def get_icon_path() -> str: - """ Get path to the file with the main plugin icon - - Returns - ------- - str - Path to the icon - """ - return get_image_path('icon.png') - - -def get_image_path(image_name) -> str: - """ Get path to an image resource, accessing it just by the name of the file (provided it is in the common directory) - - Returns - ------- - str - file path - """ - return os.path.join(SCRIPT_DIR, image_name) diff --git a/zipdeepness/deepness/images/icon.png b/zipdeepness/deepness/images/icon.png deleted file mode 100644 index 49a6d9ac8befcc34fa4de6cc05b3e1c3be439537..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/images/icon.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1dffd56a93df4d230e3b5b6521e3ddce178423d5c5d5692240f2f1d27bd9d070 -size 167299 diff --git a/zipdeepness/deepness/landcover_model.onnx b/zipdeepness/deepness/landcover_model.onnx deleted file mode 100644 index 4d36437a93d66989e0d4bf774b213a72ab637cba..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/landcover_model.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:12ae15a9bcc5e28f675e9c829cacbf2ab81776382f92e28645b2f91de3491d93 -size 12336500 diff --git a/zipdeepness/deepness/metadata.txt b/zipdeepness/deepness/metadata.txt deleted file mode 100644 index 32b8bd13b2b70470cdb1b55eaf39f9b25b76ded0..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/metadata.txt +++ /dev/null @@ -1,56 +0,0 @@ -# This file contains metadata for your plugin. - -# This file should be included when you package your plugin.# Mandatory items: - -[general] -name=Deepness: Deep Neural Remote Sensing -qgisMinimumVersion=3.22 -description=Inference of deep neural network models (ONNX) for segmentation, detection and regression -version=0.7.0 -author=PUT Vision -email=przemyslaw.aszkowski@gmail.com - -about= - Deepness plugin allows to easily perform segmentation, detection and regression on raster ortophotos with custom ONNX Neural Network models, bringing the power of deep learning to casual users. - Features highlights: - - processing any raster layer (custom ortophoto from file or layers from online providers, e.g Google Satellite) - - limiting processing range to predefined area (visible part or area defined by vector layer polygons) - - common types of models are supported: segmentation, regression, detection - - integration with layers (both for input data and model output layers). Once an output layer is created, it can be saved as a file manually - - model ZOO under development (planes detection on Bing Aerial, Corn field damage, Oil Storage tanks detection, cars detection, ...) - - training data Export Tool - exporting raster and mask as small tiles - - parametrization of the processing for advanced users (spatial resolution, overlap, postprocessing) - Plugin requires external python packages to be installed. After the first plugin startup, a Dialog will show, to assist in this process. Please visit plugin the documentation for details. - -tracker=https://github.com/PUTvision/qgis-plugin-deepness/issues -repository=https://github.com/PUTvision/qgis-plugin-deepness -# End of mandatory metadata - -# Recommended items: - -hasProcessingProvider=no -# Uncomment the following line and add your changelog: -# changelog= - -# Tags are comma separated with spaces allowed -tags=segmentation,detection,classification,machine learning,onnx,neural network,deep learning,regression,deepness,analysis,remote sensing,supervised classification - -homepage=https://qgis-plugin-deepness.readthedocs.io/ -category=Plugins -icon=images/icon.png -# experimental flag -experimental=False - -# deprecated flag (applies to the whole plugin, not just a single version) -deprecated=False - -# Since QGIS 3.8, a comma separated list of plugins to be installed -# (or upgraded) can be specified. -# Check the documentation for more information. -# plugin_dependencies= - -Category of the plugin: Raster, Vector, Database or Web -# category= - -# If the plugin can run on QGIS Server. -server=False diff --git a/zipdeepness/deepness/processing/__init__.py b/zipdeepness/deepness/processing/__init__.py deleted file mode 100644 index 74fb9ed9de376dccabda69d2632c4942cb464310..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -""" Main submodule for image processing and deep learning things. -""" diff --git a/zipdeepness/deepness/processing/extent_utils.py b/zipdeepness/deepness/processing/extent_utils.py deleted file mode 100644 index 9e3a8548df5c220e5c10adf293dc466c305d7ca8..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/extent_utils.py +++ /dev/null @@ -1,219 +0,0 @@ -""" -This file contains utilities related to Extent processing -""" - -import logging - -from qgis.core import QgsCoordinateTransform, QgsRasterLayer, QgsRectangle, QgsVectorLayer -from qgis.gui import QgsMapCanvas - -from deepness.common.errors import OperationFailedException -from deepness.common.processing_parameters.map_processing_parameters import MapProcessingParameters, ProcessedAreaType -from deepness.processing.processing_utils import BoundingBox, convert_meters_to_rlayer_units - - -def round_extent_to_rlayer_grid(extent: QgsRectangle, rlayer: QgsRasterLayer) -> QgsRectangle: - """ - Round to rlayer "grid" for pixels. - Grid starts at rlayer_extent.xMinimum & yMinimum - with resolution of rlayer_units_per_pixel - - :param extent: Extent to round, needs to be in rlayer CRS units - :param rlayer: layer detemining the grid - """ - # For some ortophotos grid spacing is close to (1.0, 1.0), while it shouldn't be. - # Seems like it is some bug or special "feature" that I do not understand. - # In that case, just return the extent as it is - grid_spacing = rlayer.rasterUnitsPerPixelX(), rlayer.rasterUnitsPerPixelY() - if abs(grid_spacing[0] - 1.0) < 0.0001 and abs(grid_spacing[1] - 1.0) < 0.0001: - logging.warning('Grid spacing is close to 1.0, which is suspicious, returning extent as it is. It shouldn not be a problem for most cases.') - return extent - - grid_start = rlayer.extent().xMinimum(), rlayer.extent().yMinimum() - - x_min = grid_start[0] + int((extent.xMinimum() - grid_start[0]) / grid_spacing[0]) * grid_spacing[0] - x_max = grid_start[0] + int((extent.xMaximum() - grid_start[0]) / grid_spacing[0]) * grid_spacing[0] - y_min = grid_start[1] + int((extent.yMinimum() - grid_start[1]) / grid_spacing[1]) * grid_spacing[1] - y_max = grid_start[1] + int((extent.yMaximum() - grid_start[1]) / grid_spacing[1]) * grid_spacing[1] - - new_extent = QgsRectangle(x_min, y_min, x_max, y_max) - return new_extent - - -def calculate_extended_processing_extent(base_extent: QgsRectangle, - params: MapProcessingParameters, - rlayer: QgsVectorLayer, - rlayer_units_per_pixel: float) -> QgsRectangle: - """Calculate the "extended" processing extent, which is the full processing area, rounded to the tile size and overlap - - Parameters - ---------- - base_extent : QgsRectangle - Base extent of the processed ortophoto, which is not rounded to the tile size - params : MapProcessingParameters - Processing parameters - rlayer : QgsVectorLayer - mask layer - rlayer_units_per_pixel : float - how many rlayer CRS units are in 1 pixel - - Returns - ------- - QgsRectangle - The "extended" processing extent - """ - - # first try to add pixels at every border - same as half-overlap for other tiles - additional_pixels = params.processing_overlap_px // 2 - additional_pixels_in_units = additional_pixels * rlayer_units_per_pixel - - tmp_extent = QgsRectangle( - base_extent.xMinimum() - additional_pixels_in_units, - base_extent.yMinimum() - additional_pixels_in_units, - base_extent.xMaximum() + additional_pixels_in_units, - base_extent.yMaximum() + additional_pixels_in_units, - ) - - rlayer_extent_infinite = rlayer.extent().isEmpty() # empty extent for infinite layers - if not rlayer_extent_infinite: - tmp_extent = tmp_extent.intersect(rlayer.extent()) - - # then add borders to have the extent be equal to N * stride + tile_size, where N is a natural number - tile_size_px = params.tile_size_px - stride_px = params.processing_stride_px # stride in pixels - - current_x_pixels = round(tmp_extent.width() / rlayer_units_per_pixel) - if current_x_pixels <= tile_size_px: - missing_pixels_x = tile_size_px - current_x_pixels # just one tile - else: - pixels_in_last_stride_x = (current_x_pixels - tile_size_px) % stride_px - missing_pixels_x = (stride_px - pixels_in_last_stride_x) % stride_px - - current_y_pixels = round(tmp_extent.height() / rlayer_units_per_pixel) - if current_y_pixels <= tile_size_px: - missing_pixels_y = tile_size_px - current_y_pixels # just one tile - else: - pixels_in_last_stride_y = (current_y_pixels - tile_size_px) % stride_px - missing_pixels_y = (stride_px - pixels_in_last_stride_y) % stride_px - - missing_pixels_x_in_units = missing_pixels_x * rlayer_units_per_pixel - missing_pixels_y_in_units = missing_pixels_y * rlayer_units_per_pixel - tmp_extent.setXMaximum(tmp_extent.xMaximum() + missing_pixels_x_in_units) - tmp_extent.setYMaximum(tmp_extent.yMaximum() + missing_pixels_y_in_units) - - extended_extent = tmp_extent - return extended_extent - - -def is_extent_infinite_or_too_big(rlayer: QgsRasterLayer) -> bool: - """Check whether layer covers whole earth (infinite extent) or or is too big for processing""" - rlayer_extent = rlayer.extent() - - # empty extent happens for infinite layers - if rlayer_extent.isEmpty(): - return True - - rlayer_area_m2 = rlayer_extent.area() * (1 / convert_meters_to_rlayer_units(rlayer, 1)) ** 2 - - if rlayer_area_m2 > (1606006962349394 // 10): # so 1/3 of the earth (this magic value is from bing aerial map area) - return True - - return False - - -def calculate_base_processing_extent_in_rlayer_crs(map_canvas: QgsMapCanvas, - rlayer: QgsRasterLayer, - vlayer_mask: QgsVectorLayer, - params: MapProcessingParameters) -> QgsRectangle: - """ Determine the Base Extent of processing (Extent (rectangle) in which the actual required area is contained) - - Parameters - ---------- - map_canvas : QgsMapCanvas - currently visible map in the UI - rlayer : QgsRasterLayer - ortophotomap which is being processed - vlayer_mask : QgsVectorLayer - mask layer containing the processed area - params : MapProcessingParameters - Processing parameters - - Returns - ------- - QgsRectangle - Base Extent of processing - """ - - rlayer_extent = rlayer.extent() - processed_area_type = params.processed_area_type - rlayer_extent_infinite = is_extent_infinite_or_too_big(rlayer) - - if processed_area_type == ProcessedAreaType.ENTIRE_LAYER: - expected_extent = rlayer_extent - if rlayer_extent_infinite: - msg = "Cannot process entire layer - layer extent is not defined or too big. " \ - "Make sure you are not processing 'Entire layer' which covers entire earth surface!!" - raise OperationFailedException(msg) - elif processed_area_type == ProcessedAreaType.FROM_POLYGONS: - expected_extent_in_vlayer_crs = vlayer_mask.extent() - if vlayer_mask.crs() == rlayer.crs(): - expected_extent = expected_extent_in_vlayer_crs - else: - t = QgsCoordinateTransform() - t.setSourceCrs(vlayer_mask.crs()) - t.setDestinationCrs(rlayer.crs()) - expected_extent = t.transform(expected_extent_in_vlayer_crs) - elif processed_area_type == ProcessedAreaType.VISIBLE_PART: - # transform visible extent from mapCanvas CRS to layer CRS - active_extent_in_canvas_crs = map_canvas.extent() - canvas_crs = map_canvas.mapSettings().destinationCrs() - t = QgsCoordinateTransform() - t.setSourceCrs(canvas_crs) - t.setDestinationCrs(rlayer.crs()) - expected_extent = t.transform(active_extent_in_canvas_crs) - else: - raise Exception("Invalid processed are type!") - - expected_extent = round_extent_to_rlayer_grid(extent=expected_extent, rlayer=rlayer) - - if rlayer_extent_infinite: - base_extent = expected_extent - else: - base_extent = expected_extent.intersect(rlayer_extent) - - return base_extent - - -def calculate_base_extent_bbox_in_full_image(image_size_y: int, - base_extent: QgsRectangle, - extended_extent: QgsRectangle, - rlayer_units_per_pixel) -> BoundingBox: - """Calculate how the base extent fits in extended_extent in terms of pixel position - - Parameters - ---------- - image_size_y : int - Size of the image in y axis in pixels - base_extent : QgsRectangle - Base Extent of processing - extended_extent : QgsRectangle - Extended extent of processing - rlayer_units_per_pixel : _type_ - Number of layer units per a single image pixel - - Returns - ------- - BoundingBox - Bounding box describing position of base extent in the extended extent - """ - base_extent = base_extent - extended_extent = extended_extent - - # should round without a rest anyway, as extends are aligned to rlayer grid - base_extent_bbox_in_full_image = BoundingBox( - x_min=round((base_extent.xMinimum() - extended_extent.xMinimum()) / rlayer_units_per_pixel), - y_min=image_size_y - 1 - round((base_extent.yMaximum() - extended_extent.yMinimum()) / rlayer_units_per_pixel - 1), - x_max=round((base_extent.xMaximum() - extended_extent.xMinimum()) / rlayer_units_per_pixel) - 1, - y_max=image_size_y - 1 - round((base_extent.yMinimum() - extended_extent.yMinimum()) / rlayer_units_per_pixel), - ) - return base_extent_bbox_in_full_image diff --git a/zipdeepness/deepness/processing/map_processor/__init__.py b/zipdeepness/deepness/processing/map_processor/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/zipdeepness/deepness/processing/map_processor/map_processing_result.py b/zipdeepness/deepness/processing/map_processor/map_processing_result.py deleted file mode 100644 index f5c2e85c68f70a5164ea0ddc70f1a1a3ac158a33..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/map_processor/map_processing_result.py +++ /dev/null @@ -1,47 +0,0 @@ -""" This file defines possible outcomes of map processing -""" - - -from typing import Callable, Optional - - -class MapProcessingResult: - """ - Base class for signaling finished processing result - """ - - def __init__(self, message: str, gui_delegate: Optional[Callable] = None): - """ - :param message: message to be shown to the user - :param gui_delegate: function to be called in GUI thread, as it is not safe to call GUI functions from other threads - """ - self.message = message - self.gui_delegate = gui_delegate - - -class MapProcessingResultSuccess(MapProcessingResult): - """ - Processing result on success - """ - - def __init__(self, message: str = '', gui_delegate: Optional[Callable] = None): - super().__init__(message=message, gui_delegate=gui_delegate) - - -class MapProcessingResultFailed(MapProcessingResult): - """ - Processing result on error - """ - - def __init__(self, error_message: str, exception=None): - super().__init__(error_message) - self.exception = exception - - -class MapProcessingResultCanceled(MapProcessingResult): - """ - Processing when processing was aborted - """ - - def __init__(self): - super().__init__(message='') diff --git a/zipdeepness/deepness/processing/map_processor/map_processor.py b/zipdeepness/deepness/processing/map_processor/map_processor.py deleted file mode 100644 index 903b307c42219e8846d2ed10c7daf5ede4537868..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/map_processor/map_processor.py +++ /dev/null @@ -1,237 +0,0 @@ -""" This file implements core map processing logic """ - -import logging -from typing import List, Optional, Tuple - -import numpy as np -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.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') - - -class MapProcessor(QgsTask): - """ - Base class for processing the ortophoto with parameters received from the UI. - - Actual processing is done in specialized child classes. Here we have the "core" functionality, - like iterating over single tiles. - - Objects of this class are created and managed by the 'Deepness'. - Work is done within QgsTask, for seamless integration with QGis GUI and logic. - """ - - # 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, - vlayer_mask: Optional[QgsVectorLayer], - map_canvas: QgsMapCanvas, - params: MapProcessingParameters): - """ init - Parameters - ---------- - rlayer : QgsRasterLayer - Raster layer which is being processed - vlayer_mask : Optional[QgsVectorLayer] - Vector layer with outline of area which should be processed (within rlayer) - map_canvas : QgsMapCanvas - active map canvas (in the GUI), required if processing visible map area - params : MapProcessingParameters - see MapProcessingParameters - """ - QgsTask.__init__(self, self.__class__.__name__) - self._processing_finished = False - self.rlayer = rlayer - self.vlayer_mask = vlayer_mask - self.params = params - self._assert_qgis_doesnt_need_reload() - self._processing_result = MapProcessingResultFailed('Failed to get processing result!') - - self.stride_px = self.params.processing_stride_px # stride in pixels - 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, - rlayer=self.rlayer, - vlayer_mask=self.vlayer_mask, - params=self.params) - - # extent which should be used during model inference, as it includes extra margins to have full tiles, - # rounded to rlayer grid - self.extended_extent = extent_utils.calculate_extended_processing_extent( - base_extent=self.base_extent, - rlayer=self.rlayer, - params=self.params, - rlayer_units_per_pixel=self.rlayer_units_per_pixel) - - # processed rlayer dimensions (for extended_extent) - self.img_size_x_pixels = round(self.extended_extent.width() / self.rlayer_units_per_pixel) # how many columns (x) - self.img_size_y_pixels = round(self.extended_extent.height() / self.rlayer_units_per_pixel) # how many rows (y) - - # Coordinate of base image within extended image (images for base_extent and extended_extent) - self.base_extent_bbox_in_full_image = extent_utils.calculate_base_extent_bbox_in_full_image( - image_size_y=self.img_size_y_pixels, - base_extent=self.base_extent, - extended_extent=self.extended_extent, - rlayer_units_per_pixel=self.rlayer_units_per_pixel) - - # 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 - - # Mask determining area to process (within extended_extent coordinates) - self.area_mask_img = processing_utils.create_area_mask_image( - vlayer_mask=self.vlayer_mask, - 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), - files_handler=self.file_handler) # type: Optional[np.ndarray] - - self._result_img = None - - def set_results_img(self, img): - if self._result_img is not None: - raise Exception("Result image already created!") - - self._result_img = img - - def get_result_img(self): - if self._result_img is None: - raise Exception("Result image not yet created!") - - return self._result_img - - def _assert_qgis_doesnt_need_reload(self): - """ If the plugin is somehow invalid, it cannot compare the enums correctly - I suppose it could be fixed somehow, but no need to investigate it now, - it affects only the development - """ - - if self.params.processed_area_type.__class__ != ProcessedAreaType: - raise Exception("Disable plugin, restart QGis and enable plugin again!") - - def run(self): - try: - self._processing_result = self._run() - except Exception as e: - logging.exception("Error occurred in MapProcessor:") - msg = "Unhandled exception occurred. See Python Console for details" - self._processing_result = MapProcessingResultFailed(msg, exception=e) - if IS_DEBUG: - raise e - - self._processing_finished = True - return True - - def _run(self) -> MapProcessingResult: - raise NotImplementedError('Base class not implemented!') - - def finished(self, result: bool): - if result: - gui_delegate = self._processing_result.gui_delegate - if gui_delegate is not None: - gui_delegate() - else: - self._processing_result = MapProcessingResultFailed("Unhandled processing error!") - self.finished_signal.emit(self._processing_result) - - @staticmethod - def is_busy(): - return True - - def _show_image(self, img, window_name='img'): - self.show_img_signal.emit(img, window_name) - - def limit_extended_extent_image_to_base_extent_with_mask(self, full_img): - """ - Limit an image which is for extended_extent to the base_extent image. - If a limiting polygon was used for processing, it will be also applied. - :param full_img: - :return: - """ - # TODO look for some inplace operation to save memory - # cv2.copyTo(src=full_img, mask=area_mask_img, dst=full_img) # this doesn't work due to implementation details - - for i in range(full_img.shape[0]): - full_img[i] = cv2.copyTo(src=full_img[i], mask=self.area_mask_img) - - b = self.base_extent_bbox_in_full_image - 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 - """ - total_tiles = self.x_bins_number * self.y_bins_number - - for y_bin_number in range(self.y_bins_number): - for x_bin_number in range(self.x_bins_number): - tile_no = y_bin_number * self.x_bins_number + x_bin_number - progress = tile_no / total_tiles * 100 - self.setProgress(progress) - print(f" Processing tile {tile_no} / {total_tiles} [{progress:.2f}%]") - tile_params = TileParams( - x_bin_number=x_bin_number, y_bin_number=y_bin_number, - x_bins_number=self.x_bins_number, y_bins_number=self.y_bins_number, - params=self.params, - processing_extent=self.extended_extent, - rlayer_units_per_pixel=self.rlayer_units_per_pixel) - - if not tile_params.is_tile_within_mask(self.area_mask_img): - continue # tile outside of mask - to be skipped - - 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/zipdeepness/deepness/processing/map_processor/map_processor_detection.py b/zipdeepness/deepness/processing/map_processor/map_processor_detection.py deleted file mode 100644 index 8b5658b4249eb21970cf3302dec0f7d7a1bdd7a7..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/map_processor/map_processor_detection.py +++ /dev/null @@ -1,288 +0,0 @@ -""" This file implements map processing for detection model """ -from typing import List - -import cv2 -import numpy as np -from qgis.core import QgsFeature, QgsGeometry, QgsProject, QgsVectorLayer -from qgis.PyQt.QtCore import QVariant -from qgis.core import QgsFields, QgsField - -from deepness.common.processing_parameters.detection_parameters import DetectionParameters -from deepness.processing import processing_utils -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.map_processor.utils.ckdtree import cKDTree -from deepness.processing.models.detector import Detection, Detector -from deepness.processing.tile_params import TileParams -from deepness.processing.models.detector import DetectorType - - -class MapProcessorDetection(MapProcessorWithModel): - """ - MapProcessor specialized for detecting objects (where there is a finite list of detected objects - of different classes, which area (bounding boxes) may overlap) - """ - - def __init__(self, - params: DetectionParameters, - **kwargs): - super().__init__( - params=params, - model=params.model, - **kwargs) - self.detection_parameters = params - self.model = params.model # type: Detector - self.model.set_inference_params( - confidence=params.confidence, - iou_threshold=params.iou_threshold - ) - self.model.set_model_type_param(model_type=params.detector_type) - self._all_detections = None - - def get_all_detections(self) -> List[Detection]: - return self._all_detections - - def _run(self) -> MapProcessingResult: - all_bounding_boxes = [] # type: List[Detection] - for tile_img_batched, tile_params_batched in self.tiles_generator_batched(): - if self.isCanceled(): - return MapProcessingResultCanceled() - - 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] - - with_rot = self.detection_parameters.detector_type == DetectorType.YOLO_ULTRALYTICS_OBB - - if len(all_bounding_boxes) > 0: - all_bounding_boxes_nms = self.remove_overlaping_detections(all_bounding_boxes, iou_threshold=self.detection_parameters.iou_threshold, with_rot=with_rot) - all_bounding_boxes_restricted = self.limit_bounding_boxes_to_processed_area(all_bounding_boxes_nms) - else: - all_bounding_boxes_restricted = [] - - gui_delegate = self._create_vlayer_for_output_bounding_boxes(all_bounding_boxes_restricted) - - result_message = self._create_result_message(all_bounding_boxes_restricted) - self._all_detections = all_bounding_boxes_restricted - return MapProcessingResultSuccess( - message=result_message, - gui_delegate=gui_delegate, - ) - - def limit_bounding_boxes_to_processed_area(self, bounding_boxes: List[Detection]) -> List[Detection]: - """ - Limit all bounding boxes to the constrained area that we process. - E.g. if we are detecting peoples in a circle, we don't want to count peoples in the entire rectangle - - :return: - """ - bounding_boxes_restricted = [] - for det in bounding_boxes: - # if bounding box is not in the area_mask_img (at least in some percentage) - remove it - - if self.area_mask_img is not None: - det_slice = det.bbox.get_slice() - area_subimg = self.area_mask_img[det_slice] - pixels_in_area = np.count_nonzero(area_subimg) - else: - det_bounding_box = det.bbox - pixels_in_area = self.base_extent_bbox_in_full_image.calculate_overlap_in_pixels(det_bounding_box) - total_pixels = det.bbox.get_area() - coverage = pixels_in_area / total_pixels - if coverage > 0.5: # some arbitrary value, 50% seems reasonable - bounding_boxes_restricted.append(det) - - return bounding_boxes_restricted - - def _create_result_message(self, bounding_boxes: List[Detection]) -> str: - # hack, allways one output - model_outputs = self._get_indexes_of_model_output_channels_to_create() - channels = range(model_outputs[0]) - - counts_mapping = {} - total_counts = 0 - for channel_id in channels: - filtered_bounding_boxes = [det for det in bounding_boxes if det.clss == channel_id] - counts = len(filtered_bounding_boxes) - counts_mapping[channel_id] = counts - total_counts += counts - - txt = f'Detection done for {len(channels)} model output classes, with the following statistics:\n' - for channel_id in channels: - counts = counts_mapping[channel_id] - - if total_counts: - counts_percentage = counts / total_counts * 100 - else: - counts_percentage = 0 - - txt += f' - {self.model.get_channel_name(0, channel_id)}: counts = {counts} ({counts_percentage:.2f} %)\n' - - return txt - - def _create_vlayer_for_output_bounding_boxes(self, bounding_boxes: List[Detection]): - vlayers = [] - - # hack, allways one output - model_outputs = self._get_indexes_of_model_output_channels_to_create() - channels = range(model_outputs[0]) - - for channel_id in channels: - filtered_bounding_boxes = [det for det in bounding_boxes if det.clss == channel_id] - print(f'Detections for class {channel_id}: {len(filtered_bounding_boxes)}') - - vlayer = QgsVectorLayer("multipolygon", self.model.get_channel_name(0, channel_id), "memory") - vlayer.setCrs(self.rlayer.crs()) - prov = vlayer.dataProvider() - prov.addAttributes([QgsField("confidence", QVariant.Double)]) - vlayer.updateFields() - - features = [] - for det in filtered_bounding_boxes: - feature = QgsFeature() - 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() #move outside of the if block - polygon_xy_vec_vec = [ - bbox_corners_crs - ] - geometry = QgsGeometry.fromPolygonXY(polygon_xy_vec_vec) - #feature.setGeometry(geometry) - #features.append(feature) - 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] - - 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) - feature.setGeometry(geometry) - feature.setAttributes([float(det.conf)]) - features.append(feature) - - #vlayer = QgsVectorLayer("multipolygon", self.model.get_channel_name(0, channel_id), "memory") - #vlayer.setCrs(self.rlayer.crs()) - #prov = vlayer.dataProvider() - - color = vlayer.renderer().symbol().color() - OUTPUT_VLAYER_COLOR_TRANSPARENCY = 80 - color.setAlpha(OUTPUT_VLAYER_COLOR_TRANSPARENCY) - vlayer.renderer().symbol().setColor(color) - # TODO - add also outline for the layer (thicker black border) - - prov.addFeatures(features) - vlayer.updateExtents() - - vlayers.append(vlayer) - - # accessing GUI from non-GUI thread is not safe, so we need to delegate it to the GUI thread - def add_to_gui(): - group = QgsProject.instance().layerTreeRoot().insertGroup(0, 'model_output') - for vlayer in vlayers: - QgsProject.instance().addMapLayer(vlayer, False) - group.addLayer(vlayer) - - return add_to_gui - - @staticmethod - def remove_overlaping_detections(bounding_boxes: List[Detection], iou_threshold: float, with_rot: bool = False) -> List[Detection]: - bboxes = [] - probs = [] - for det in bounding_boxes: - if with_rot: - bboxes.append(det.get_bbox_xyxy_rot()) - else: - bboxes.append(det.get_bbox_xyxy()) - probs.append(det.conf) - - bboxes = np.array(bboxes) - probs = np.array(probs) - - pick_ids = Detector.non_max_suppression_fast(boxes=bboxes, probs=probs, iou_threshold=iou_threshold, with_rot=with_rot) - - filtered_bounding_boxes = [x for i, x in enumerate(bounding_boxes) if i in pick_ids] - filtered_bounding_boxes = sorted(filtered_bounding_boxes, reverse=True) - - pick_ids_kde = MapProcessorDetection.non_max_kdtree(filtered_bounding_boxes, iou_threshold) - - filtered_bounding_boxes = [x for i, x in enumerate(filtered_bounding_boxes) if i in pick_ids_kde] - - return filtered_bounding_boxes - - @staticmethod - def non_max_kdtree(bounding_boxes: List[Detection], iou_threshold: float) -> List[int]: - """ Remove overlapping bounding boxes using kdtree - - :param bounding_boxes: List of bounding boxes in (xyxy format) - :param iou_threshold: Threshold for intersection over union - :return: Pick ids to keep - """ - - centers = np.array([det.get_bbox_center() for det in bounding_boxes]) - - kdtree = cKDTree(centers) - pick_ids = set() - removed_ids = set() - - for i, bbox in enumerate(bounding_boxes): - if i in removed_ids: - continue - - indices = kdtree.query(bbox.get_bbox_center(), k=min(10, len(bounding_boxes))) - - for j in indices: - if j in removed_ids: - continue - - if i == j: - continue - - iou = bbox.bbox.calculate_intersection_over_smaler_area(bounding_boxes[j].bbox) - - if iou > iou_threshold: - removed_ids.add(j) - - pick_ids.add(i) - - return pick_ids - - @staticmethod - def convert_bounding_boxes_to_absolute_positions(bounding_boxes_relative: List[Detection], - tile_params: TileParams): - 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_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/zipdeepness/deepness/processing/map_processor/map_processor_recognition.py b/zipdeepness/deepness/processing/map_processor/map_processor_recognition.py deleted file mode 100644 index 340a568edda5c1680a4d719a75b40cecc2fda15f..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/map_processor/map_processor_recognition.py +++ /dev/null @@ -1,221 +0,0 @@ -""" This file implements map processing for Recognition model """ - -import os -import uuid -from typing import List - -import numpy as np -from numpy.linalg import norm -from osgeo import gdal, osr -from qgis.core import QgsProject, QgsRasterLayer - -from deepness.common.defines import IS_DEBUG -from deepness.common.lazy_package_loader import LazyPackageLoader -from deepness.common.misc import TMP_DIR_PATH -from deepness.common.processing_parameters.recognition_parameters import RecognitionParameters -from deepness.processing.map_processor.map_processing_result import (MapProcessingResult, MapProcessingResultCanceled, - MapProcessingResultFailed, - MapProcessingResultSuccess) -from deepness.processing.map_processor.map_processor_with_model import MapProcessorWithModel - -cv2 = LazyPackageLoader('cv2') - - -class MapProcessorRecognition(MapProcessorWithModel): - """ - MapProcessor specialized for Recognition model - """ - - def __init__(self, params: RecognitionParameters, **kwargs): - super().__init__(params=params, model=params.model, **kwargs) - self.recognition_parameters = params - self.model = params.model - - def _run(self) -> MapProcessingResult: - try: - query_img = cv2.imread(self.recognition_parameters.query_image_path) - assert query_img is not None, f"Error occurred while reading query image: {self.recognition_parameters.query_image_path}" - except Exception as e: - return MapProcessingResultFailed(f"Error occurred while reading query image: {e}") - - # some hardcoded code for recognition model - query_img = cv2.cvtColor(query_img, cv2.COLOR_BGR2RGB) - query_img_resized = cv2.resize(query_img, self.model.get_input_shape()[2:4][::-1]) - query_img_batched = np.array([query_img_resized]) - - query_img_emb = self.model.process(query_img_batched)[0][0] - - final_shape_px = ( - self.img_size_y_pixels, - self.img_size_x_pixels, - ) - - stride = self.stride_px - full_result_img = np.zeros(final_shape_px, np.float32) - mask = np.zeros_like(full_result_img, dtype=np.int16) - highest = 0 - for tile_img_batched, tile_params_batched in self.tiles_generator_batched(): - if self.isCanceled(): - return MapProcessingResultCanceled() - - tile_result_batched = self._process_tile(tile_img_batched)[0] - - for tile_result, tile_params in zip(tile_result_batched, tile_params_batched): - cossim = np.dot(query_img_emb, tile_result)/(norm(query_img_emb)*norm(tile_result)) - - x_bin = tile_params.x_bin_number - y_bin = tile_params.y_bin_number - size = self.params.tile_size_px - - if cossim > highest: - highest = cossim - x_high = x_bin - y_high = y_bin - - full_result_img[y_bin*stride:y_bin*stride+size, x_bin*stride:x_bin*stride + size] += cossim - mask[y_bin*stride:y_bin*stride+size, x_bin*stride:x_bin*stride + size] += 1 - - full_result_img = full_result_img/mask - self.set_results_img(full_result_img) - - gui_delegate = self._create_rlayers_from_images_for_base_extent(self.get_result_img(), x_high, y_high, size, stride) - result_message = self._create_result_message(self.get_result_img(), x_high*self.params.tile_size_px, y_high*self.params.tile_size_px) - return MapProcessingResultSuccess( - message=result_message, - gui_delegate=gui_delegate, - ) - - def _create_result_message(self, result_img: List[np.ndarray], x_high, y_high) -> str: - txt = f"Recognition ended, best result found at {x_high}, {y_high}, {result_img.shape}" - return txt - - def limit_extended_extent_image_to_base_extent_with_mask(self, full_img): - """ - Limit an image which is for extended_extent to the base_extent image. - If a limiting polygon was used for processing, it will be also applied. - :param full_img: - :return: - """ - # TODO look for some inplace operation to save memory - # cv2.copyTo(src=full_img, mask=area_mask_img, dst=full_img) # this doesn't work due to implementation details - # full_img = cv2.copyTo(src=full_img, mask=self.area_mask_img) - - b = self.base_extent_bbox_in_full_image - result_img = full_img[ - int(b.y_min * self.recognition_parameters.scale_factor): int( - b.y_max * self.recognition_parameters.scale_factor - ), - int(b.x_min * self.recognition_parameters.scale_factor): int( - b.x_max * self.recognition_parameters.scale_factor - ), - :, - ] - return result_img - - def load_rlayer_from_file(self, file_path): - """ - Create raster layer from tif file - """ - file_name = os.path.basename(file_path) - base_file_name = file_name.split("___")[ - 0 - ] # we remove the random_id string we created a moment ago - rlayer = QgsRasterLayer(file_path, base_file_name) - if rlayer.width() == 0: - raise Exception( - "0 width - rlayer not loaded properly. Probably invalid file path?" - ) - rlayer.setCrs(self.rlayer.crs()) - return rlayer - - def _create_rlayers_from_images_for_base_extent( - self, result_img: np.ndarray, - x_high, - y_high, - size, - stride - ): - y = y_high * stride - x = x_high * stride - - result_img[y, x:x+size-1] = 1 - result_img[y+size-1, x:x+size-1] = 1 - result_img[y:y+size-1, x] = 1 - result_img[y:y+size-1, x+size-1] = 1 - - # TODO: We are creating a new file for each layer. - # Maybe can we pass ownership of this file to QGis? - # Or maybe even create vlayer directly from array, without a file? - - random_id = str(uuid.uuid4()).replace("-", "") - file_path = os.path.join(TMP_DIR_PATH, f"{random_id}.tif") - self.save_result_img_as_tif(file_path=file_path, img=np.expand_dims(result_img, axis=2)) - - rlayer = self.load_rlayer_from_file(file_path) - OUTPUT_RLAYER_OPACITY = 0.5 - rlayer.renderer().setOpacity(OUTPUT_RLAYER_OPACITY) - - # accessing GUI from non-GUI thread is not safe, so we need to delegate it to the GUI thread - def add_to_gui(): - group = ( - QgsProject.instance() - .layerTreeRoot() - .insertGroup(0, "Cosine similarity score") - ) - QgsProject.instance().addMapLayer(rlayer, False) - group.addLayer(rlayer) - - return add_to_gui - - def save_result_img_as_tif(self, file_path: str, img: np.ndarray): - """ - As we cannot pass easily an numpy array to be displayed as raster layer, we create temporary geotif files, - which will be loaded as layer later on - - Partially based on example from: - https://gis.stackexchange.com/questions/82031/gdal-python-set-projection-of-a-raster-not-working - """ - os.makedirs(os.path.dirname(file_path), exist_ok=True) - - extent = self.base_extent - crs = self.rlayer.crs() - - geo_transform = [ - extent.xMinimum(), - self.rlayer_units_per_pixel, - 0, - extent.yMaximum(), - 0, - -self.rlayer_units_per_pixel, - ] - - driver = gdal.GetDriverByName("GTiff") - n_lines = img.shape[0] - n_cols = img.shape[1] - n_chanels = img.shape[2] - # data_type = gdal.GDT_Byte - data_type = gdal.GDT_Float32 - grid_data = driver.Create( - "grid_data", n_cols, n_lines, n_chanels, data_type - ) # , options) - # loop over chanels - for i in range(1, img.shape[2] + 1): - grid_data.GetRasterBand(i).WriteArray(img[:, :, i - 1]) - - # crs().srsid() - maybe we can use the ID directly - but how? - # srs.ImportFromEPSG() - srs = osr.SpatialReference() - srs.SetFromUserInput(crs.authid()) - - grid_data.SetProjection(srs.ExportToWkt()) - grid_data.SetGeoTransform(geo_transform) - driver.CreateCopy(file_path, grid_data, 0) - - def _process_tile(self, tile_img: np.ndarray) -> np.ndarray: - result = self.model.process(tile_img) - - # NOTE - currently we are saving result as float32, so we are losing some accuraccy. - # result = np.clip(result, 0, 255) # old version with uint8_t - not used anymore - # result = result.astype(np.float32) - - return result diff --git a/zipdeepness/deepness/processing/map_processor/map_processor_regression.py b/zipdeepness/deepness/processing/map_processor/map_processor_regression.py deleted file mode 100644 index 15c3c8dc9e9f9789f2ab1415cdaa66c57b744c2b..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/map_processor/map_processor_regression.py +++ /dev/null @@ -1,174 +0,0 @@ -""" This file implements map processing for regression model """ - -import os -import uuid -from typing import List - -import numpy as np -from osgeo import gdal, osr -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_processor_with_model import MapProcessorWithModel - - -class MapProcessorRegression(MapProcessorWithModel): - """ - MapProcessor specialized for Regression model (where each pixel has a value representing some feature intensity) - """ - - def __init__(self, - params: RegressionParameters, - **kwargs): - super().__init__( - params=params, - model=params.model, - **kwargs) - self.regression_parameters = params - self.model = params.model - - def _run(self) -> MapProcessingResult: - number_of_output_channels = len(self._get_indexes_of_model_output_channels_to_create()) - 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 = 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() - - tile_results_batched = self._process_tile(tile_img_batched) - - for tile_results, tile_params in zip(tile_results_batched, tile_params_batched): - tile_params.set_mask_on_full_img( - tile_result=tile_results, - full_result_img=full_result_imgs) - - # 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) - self.set_results_img(full_result_imgs) - - gui_delegate = self._create_rlayers_from_images_for_base_extent(self.get_result_img()) - result_message = self._create_result_message(self.get_result_img()) - return MapProcessingResultSuccess( - message=result_message, - gui_delegate=gui_delegate, - ) - - def _create_result_message(self, result_imgs: List[np.ndarray]) -> str: - txt = f'Regression done, with the following statistics:\n' - for output_id, _ in enumerate(self._get_indexes_of_model_output_channels_to_create()): - result_img = result_imgs[output_id] - - average_value = np.mean(result_img) - std = np.std(result_img) - - txt += f' - {self.model.get_channel_name(output_id, 0)}: average_value = {average_value:.2f} (std = {std:.2f}, ' \ - f'min={np.min(result_img)}, max={np.max(result_img)})\n' - - return txt - - def limit_extended_extent_images_to_base_extent_with_mask(self, full_imgs: List[np.ndarray]): - """ - Same as 'limit_extended_extent_image_to_base_extent_with_mask' but for a list of images. - See `limit_extended_extent_image_to_base_extent_with_mask` for details. - :param full_imgs: - :return: - """ - return self.limit_extended_extent_image_to_base_extent_with_mask(full_img=full_imgs) - - def load_rlayer_from_file(self, file_path): - """ - Create raster layer from tif file - """ - file_name = os.path.basename(file_path) - base_file_name = file_name.split('___')[0] # we remove the random_id string we created a moment ago - rlayer = QgsRasterLayer(file_path, base_file_name) - if rlayer.width() == 0: - raise Exception("0 width - rlayer not loaded properly. Probably invalid file path?") - rlayer.setCrs(self.rlayer.crs()) - return rlayer - - def _create_rlayers_from_images_for_base_extent(self, result_imgs: List[np.ndarray]): - # TODO: We are creating a new file for each layer. - # Maybe can we pass ownership of this file to QGis? - # Or maybe even create vlayer directly from array, without a file? - rlayers = [] - - for output_id, _ in enumerate(self._get_indexes_of_model_output_channels_to_create()): - - random_id = str(uuid.uuid4()).replace('-', '') - file_path = os.path.join(TMP_DIR_PATH, f'{self.model.get_channel_name(output_id, 0)}__{random_id}.tif') - self.save_result_img_as_tif(file_path=file_path, img=result_imgs[output_id]) - - rlayer = self.load_rlayer_from_file(file_path) - OUTPUT_RLAYER_OPACITY = 0.5 - rlayer.renderer().setOpacity(OUTPUT_RLAYER_OPACITY) - rlayers.append(rlayer) - - def add_to_gui(): - group = QgsProject.instance().layerTreeRoot().insertGroup(0, 'model_output') - for rlayer in rlayers: - QgsProject.instance().addMapLayer(rlayer, False) - group.addLayer(rlayer) - - return add_to_gui - - def save_result_img_as_tif(self, file_path: str, img: np.ndarray): - """ - As we cannot pass easily an numpy array to be displayed as raster layer, we create temporary geotif files, - which will be loaded as layer later on - - Partially based on example from: - https://gis.stackexchange.com/questions/82031/gdal-python-set-projection-of-a-raster-not-working - """ - os.makedirs(os.path.dirname(file_path), exist_ok=True) - - extent = self.base_extent - crs = self.rlayer.crs() - - geo_transform = [extent.xMinimum(), self.rlayer_units_per_pixel, 0, - extent.yMaximum(), 0, -self.rlayer_units_per_pixel] - - driver = gdal.GetDriverByName('GTiff') - n_lines = img.shape[0] - n_cols = img.shape[1] - # data_type = gdal.GDT_Byte - data_type = gdal.GDT_Float32 - grid_data = driver.Create('grid_data', n_cols, n_lines, 1, data_type) # , options) - grid_data.GetRasterBand(1).WriteArray(img) - - # crs().srsid() - maybe we can use the ID directly - but how? - # srs.ImportFromEPSG() - srs = osr.SpatialReference() - srs.SetFromUserInput(crs.authid()) - - grid_data.SetProjection(srs.ExportToWkt()) - grid_data.SetGeoTransform(geo_transform) - driver.CreateCopy(file_path, grid_data, 0) - print(f'***** {file_path = }') - - def _process_tile(self, tile_img: np.ndarray) -> np.ndarray: - many_result = self.model.process(tile_img) - many_outputs = [] - - for result in many_result: - result[np.isnan(result)] = 0 - result *= self.regression_parameters.output_scaling - - # NOTE - currently we are saving result as float32, so we are losing some accuraccy. - # result = np.clip(result, 0, 255) # old version with uint8_t - not used anymore - result = result.astype(np.float32) - - if len(result.shape) == 3: - result = np.expand_dims(result, axis=1) - - many_outputs.append(result[:, 0]) - - many_outputs = np.array(many_outputs).transpose((1, 0, 2, 3)) - - return many_outputs diff --git a/zipdeepness/deepness/processing/map_processor/map_processor_segmentation.py b/zipdeepness/deepness/processing/map_processor/map_processor_segmentation.py deleted file mode 100644 index 75f98910862afc05292b9da0b609b65d8e293893..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/map_processor/map_processor_segmentation.py +++ /dev/null @@ -1,386 +0,0 @@ -""" This file implements map processing for segmentation model """ - -from typing import Callable - -import numpy as np -from qgis.core import QgsProject, QgsVectorLayer, QgsCoordinateTransform, QgsCoordinateReferenceSystem, QgsField, QgsFeature, QgsGeometry, QgsMessageLog -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_processor_with_model import MapProcessorWithModel -from deepness.processing.tile_params import TileParams -import geopandas as gpd -import traceback -import pandas as pd -from rasterio.features import shapes -from affine import Affine -from shapely.geometry import shape - -cv2 = LazyPackageLoader('cv2') - - -class MapProcessorSegmentation(MapProcessorWithModel): - """ - MapProcessor specialized for Segmentation model (where each pixel is assigned to one class). - """ - - def __init__(self, - params: SegmentationParameters, - **kwargs): - super().__init__( - params=params, - model=params.model, - **kwargs) - self.segmentation_parameters = params - self.model = params.model - self.new_class_names = { - "0": "background", - "2": "zone-verte", - "3": "eau", - "4": "route", - "5": "non-residentiel", - "6": "Villa", - "7": "traditionnel", - "8": "appartement", - "9": "autre" - } - - - def tile_mask_to_gdf_latlon(self,tile: TileParams, mask: np.ndarray, raster_layer_crs=None): - - - # remove channel if exists - if mask.ndim == 3: - mask = mask[0] - - H, W = mask.shape - - mask_binary = mask != 0 - - x_min = tile.extent.xMinimum() - y_max = tile.extent.yMaximum() - pixel_size = tile.rlayer_units_per_pixel - transform = Affine(pixel_size, 0, x_min, 0, -pixel_size, y_max) - - - results = ( - {"properties": {"class_id": int(v)}, "geometry": s} - for s, v in shapes(mask.astype(np.int16), mask=mask_binary, transform=transform) - ) - geoms = [] - for r in results: - class_id = r["properties"]["class_id"] - class_name = self.new_class_names.get(str(class_id), "unknown") - geom = shape(r["geometry"]) - geoms.append({"geometry": geom, "class_id": class_id, "class_name": class_name}) - if geoms: - gdf = gpd.GeoDataFrame(geoms, geometry="geometry", crs=raster_layer_crs.authid() if raster_layer_crs else "EPSG:3857") - else: - # empty GeoDataFrame with the right columns - gdf = gpd.GeoDataFrame(columns=["geometry", "class_id", "class_name"], geometry="geometry", crs=raster_layer_crs.authid() if raster_layer_crs else "EPSG:3857") - -# Transform to lat/lon if needed - if not gdf.empty: - gdf = gdf.to_crs("EPSG:4326") - - gdf = gpd.GeoDataFrame(geoms, geometry="geometry", crs=raster_layer_crs.authid() if raster_layer_crs else "EPSG:3857") - - # Transform to lat/lon if needed - gdf = gdf.to_crs("EPSG:4326") - - # # compute CRS coordinates for each pixel - # xs = x_min + np.arange(W) * pixel_size - # ys = y_max - np.arange(H) * pixel_size # Y decreases downward - - # xs_grid, ys_grid = np.meshgrid(xs, ys) - - # # flatten - # xs_flat = xs_grid.flatten() - # ys_flat = ys_grid.flatten() - # classes_flat = mask.flatten() - - # # create points - # points = [Point(x, y) for x, y in zip(xs_flat, ys_flat)] - - # gdf = gpd.GeoDataFrame({'class': classes_flat}, geometry=points) - - # # set CRS - # if raster_layer_crs is None: - # raster_layer_crs = QgsCoordinateReferenceSystem("EPSG:3857") # fallback - # gdf.set_crs(raster_layer_crs.authid(), inplace=True) - - # # transform to lat/lon (EPSG:4326) - # transformer = QgsCoordinateTransform(raster_layer_crs, QgsCoordinateReferenceSystem("EPSG:4326"), QgsProject.instance()) - # gdf['geometry'] = gdf['geometry'].apply(lambda pt: Point(*transformer.transform(pt.x, pt.y))) - # # transformed_coords = [transformer.transform(pt.x, pt.y) for pt in gdf.geometry] - # # gdf['geometry'] = [Point(x, y) for x, y in transformed_coords] - - - return gdf - - - def _run(self) -> MapProcessingResult: - final_shape_px = (len(self._get_indexes_of_model_output_channels_to_create()), self.img_size_y_pixels, self.img_size_x_pixels) - - full_result_img = self._get_array_or_mmapped_array(final_shape_px) - gdf_list = [] - raster_layer = QgsProject.instance().mapLayer(self.params.input_layer_id) - raster_layer_crs = raster_layer.crs() if raster_layer else QgsCoordinateReferenceSystem("EPSG:3857") - - for tile_img_batched, tile_params_batched in self.tiles_generator_batched(): - if self.isCanceled(): - return MapProcessingResultCanceled() - - tile_result_batched = self._process_tile(tile_img_batched) - - 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) - try: - gdf_tile = self.tile_mask_to_gdf_latlon( - tile=tile_params, - mask=tile_result, - raster_layer_crs=raster_layer_crs, - ) - gdf_list.append(gdf_tile) - QgsMessageLog.logMessage(f"Tile {tile_params.x_bin_number},{tile_params.y_bin_number}: got {len(gdf_tile)} points", "Segmentation", 0) - except Exception as e: - QgsMessageLog.logMessage(f"Tile {tile_params.x_bin_number},{tile_params.y_bin_number} failed: {e}", "Segmentation", 2) - QgsMessageLog.logMessage(traceback.format_exc(), "Segmentation", 2) - - blur_size = int(self.segmentation_parameters.postprocessing_dilate_erode_size // 2) * 2 + 1 # needs to be odd - - for i in range(full_result_img.shape[0]): - full_result_img[i] = cv2.medianBlur(full_result_img[i], blur_size) - - full_result_img = self.limit_extended_extent_image_to_base_extent_with_mask(full_img=full_result_img) - - self.set_results_img(full_result_img) - if gdf_list: - final_gdf = gpd.GeoDataFrame(pd.concat(gdf_list, ignore_index=True), crs=gdf_list[0].crs) - csv_file = r"C:\Users\carin\Documents\segmen.csv" - final_gdf.to_csv(csv_file, index=False) - else: - final_gdf = None - print("No GeoDataFrame generated.") - - - gui_delegate = self._create_vlayer_from_mask_for_base_extent(self.get_result_img()) - - result_message = self._create_result_message(self.get_result_img()) - return MapProcessingResultSuccess( - message=result_message, - gui_delegate=gui_delegate, - ) - - def _check_output_layer_is_sigmoid_and_has_more_than_one_name(self, output_id: int) -> bool: - if self.model.outputs_names is None or self.model.outputs_are_sigmoid is None: - return False - - return len(self.model.outputs_names[output_id]) > 1 and self.model.outputs_are_sigmoid[output_id] - - def _create_result_message(self, result_img: np.ndarray) -> str: - - txt = f'Segmentation done, with the following statistics:\n' - - for output_id, layer_sizes in enumerate(self._get_indexes_of_model_output_channels_to_create()): - - txt += f'Channels for output {output_id}:\n' - - unique, counts = np.unique(result_img[output_id], return_counts=True) - counts_map = {} - for i in range(len(unique)): - counts_map[unique[i]] = counts[i] - - # # we cannot simply take image dimensions, because we may have irregular processing area from polygon - number_of_pixels_in_processing_area = np.sum([counts_map[k] for k in counts_map.keys()]) - total_area = number_of_pixels_in_processing_area * self.params.resolution_m_per_px**2 - - for channel_id in range(layer_sizes): - pixels_count = counts_map.get(channel_id + 1, 0) # we add 1 to avoid 0 values, find the MADD1 code for explanation - area = pixels_count * self.params.resolution_m_per_px**2 - - if total_area > 0 and not np.isnan(total_area) and not np.isinf(total_area): - area_percentage = area / total_area * 100 - else: - area_percentage = 0.0 - # TODO - - txt += f'\t- {self.model.get_channel_name(output_id, channel_id)}: area = {area:.2f} m^2 ({area_percentage:.2f} %)\n' - - return txt - - def _create_vlayer_from_mask_for_base_extent(self, mask_img, vector_gdf: gpd.GeoDataFrame = None) -> Callable: - """ create vector layer with polygons from the mask image - :return: function to be called in GUI thread - """ - vlayers = [] - - for output_id, layer_sizes in enumerate(self._get_indexes_of_model_output_channels_to_create()): - output_vlayers = [] - for channel_id in range(layer_sizes): - local_mask_img = np.uint8(mask_img[output_id] == (channel_id + 1)) # we add 1 to avoid 0 values, find the MADD1 code for explanation - - contours, hierarchy = cv2.findContours(local_mask_img, 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 - - layer_name = self.model.get_channel_name(output_id, channel_id) - vlayer = QgsVectorLayer("multipolygon", layer_name, "memory") - vlayer.setCrs(self.rlayer.crs()) - prov = vlayer.dataProvider() - - color = vlayer.renderer().symbol().color() - OUTPUT_VLAYER_COLOR_TRANSPARENCY = 80 - color.setAlpha(OUTPUT_VLAYER_COLOR_TRANSPARENCY) - vlayer.renderer().symbol().setColor(color) - # TODO - add also outline for the layer (thicker black border) - - prov.addFeatures(features) - vlayer.updateExtents() - - output_vlayers.append(vlayer) - - vlayers.append(output_vlayers) - - # accessing GUI from non-GUI thread is not safe, so we need to delegate it to the GUI thread - def add_to_gui(): - group = QgsProject.instance().layerTreeRoot().insertGroup(0, 'model_output') - - if len(vlayers) == 1: - for vlayer in vlayers[0]: - QgsProject.instance().addMapLayer(vlayer, False) - group.addLayer(vlayer) - else: - for i, output_vlayers in enumerate(vlayers): - output_group = group.insertGroup(0, f'output_{i}') - for vlayer in output_vlayers: - QgsProject.instance().addMapLayer(vlayer, False) - output_group.addLayer(vlayer) - - return add_to_gui - - def _process_tile(self, tile_img_batched: np.ndarray) -> np.ndarray: - - res = self.model.process(tile_img_batched) - - # Thresholding optional (apply only on non-background) - threshold = self.segmentation_parameters.pixel_classification__probability_threshold - # onnx_classes = np.argmax(build, axis=1)[0].astype(np.int8) - # confidences = np.max(build, axis=1)[0] - - # onnx_classes[confidences < threshold] = 0 - # BUILDING_ID = 1 - - # final_mask = onnx_classes.copy() - - # final_mask[onnx_classes == BUILDING_ID] = type_build[onnx_classes == BUILDING_ID] - - # final_mask = np.expand_dims(final_mask, axis=(0, 1)) - final_mask = self.process_dual_batch(res, threshold) - - return final_mask - - def process_dual_batch(self, result, threshold=0.0): - - N = result[0].shape[0] - threshold = self.segmentation_parameters.pixel_classification__probability_threshold - - # Run the dual model - res = result[0] # (N, num_classes, H, W) - segmentation_maps = result[1] # list of N segmentation maps (H, W) - - # Prepare output batch - final_batch_masks = np.zeros((N, 1, 512, 512), dtype=np.int8) - individual_masks = [] - - new_class = {0: 0, 1: 5, 2: 6, 3: 7, 4: 8} - BUILDING_ID = 1 - - for i in range(N): - seg_map = segmentation_maps[i] # (H, W) - - # Map old classes to building classes - build_mask = np.zeros_like(seg_map, dtype=np.int8) - for k, v in new_class.items(): - build_mask[seg_map == k] = v - - # ONNX class predictions and confidences - onnx_classes = np.argmax(res[i], axis=0).astype(np.int8) # (H, W) - confidences = np.max(res[i], axis=0) # (H, W) - - # Threshold low-confidence predictions - onnx_classes[confidences < threshold] = 0 - - # Merge building predictions with type mask - final_mask = onnx_classes.copy() - final_mask[onnx_classes == BUILDING_ID] = build_mask[onnx_classes == BUILDING_ID] - - # Save results - - final_batch_masks[i, 0] = final_mask - - return final_batch_masks - - - # def _process_tile(self, tile_img_batched: np.ndarray) -> np.ndarray: - # many_result = self.model.process(tile_img_batched) - # many_outputs = [] - - # for result in many_result: - # result[result < self.segmentation_parameters.pixel_classification__probability_threshold] = 0.0 - - # if len(result.shape) == 3: - # result = np.expand_dims(result, axis=1) - - # if (result.shape[1] == 1): - # result = (result != 0).astype(int) + 1 # we add 1 to avoid 0 values, find the MADD1 code for explanation - # else: - # shape = result.shape - # result = np.argmax(result, axis=1).reshape(shape[0], 1, shape[2], shape[3]) + 1 # we add 1 to avoid 0 values, find the MADD1 code for explanation - - # assert len(result.shape) == 4 - # assert result.shape[1] == 1 - - # many_outputs.append(result[:, 0]) - - # many_outputs = np.array(many_outputs).transpose((1, 0, 2, 3)) - - # return many_outputs - - # def _process_tile(self, tile_img_batched: np.ndarray) -> np.ndarray: - - # merged_probs = self.model.process(tile_img_batched) - - # # Thresholding optional (apply only on non-background) - # threshold = self.segmentation_parameters.pixel_classification__probability_threshold - # merged_probs[:, 1:, :, :][merged_probs[:, 1:, :, :] < threshold] = 0.0 - - # # Argmax over channels to get single-channel mask - # single_channel_mask = np.argmax(merged_probs, axis=1).astype(np.uint8) # shape: (1, H, W) - - # # Remove 'other' class pixels - # single_channel_mask[single_channel_mask == 1] = 0 - # single_channel_mask = np.expand_dims(single_channel_mask, axis=1) - - # return single_channel_mask - - - diff --git a/zipdeepness/deepness/processing/map_processor/map_processor_superresolution.py b/zipdeepness/deepness/processing/map_processor/map_processor_superresolution.py deleted file mode 100644 index b1d0d3d5c77260589cc20ffb91d325449dbf050c..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/map_processor/map_processor_superresolution.py +++ /dev/null @@ -1,175 +0,0 @@ -""" This file implements map processing for Super Resolution model """ - -import os -import uuid -from typing import List - -import numpy as np -from osgeo import gdal, osr -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_processor_with_model import MapProcessorWithModel - - -class MapProcessorSuperresolution(MapProcessorWithModel): - """ - MapProcessor specialized for Super Resolution model (whic is is used to upscale the input image to a higher resolution) - """ - - def __init__(self, - params: SuperresolutionParameters, - **kwargs): - super().__init__( - params=params, - model=params.model, - **kwargs) - self.superresolution_parameters = params - self.model = params.model - - def _run(self) -> MapProcessingResult: - number_of_output_channels = self.model.get_number_of_output_channels() - - # always one output - number_of_output_channels = number_of_output_channels[0] - - 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 = 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() - - 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) - self.set_results_img(full_result_imgs) - - gui_delegate = self._create_rlayers_from_images_for_base_extent(self.get_result_img()) - result_message = self._create_result_message(self.get_result_img()) - return MapProcessingResultSuccess( - message=result_message, - gui_delegate=gui_delegate, - ) - - def _create_result_message(self, result_img: List[np.ndarray]) -> str: - channels = self._get_indexes_of_model_output_channels_to_create() - txt = f'Super-resolution done \n' - - if len(channels) > 0: - total_area = result_img.shape[0] * result_img.shape[1] * (self.params.resolution_m_per_px / self.superresolution_parameters.scale_factor)**2 - txt += f'Total are is {total_area:.2f} m^2' - return txt - - def limit_extended_extent_image_to_base_extent_with_mask(self, full_img): - """ - Limit an image which is for extended_extent to the base_extent image. - If a limiting polygon was used for processing, it will be also applied. - :param full_img: - :return: - """ - # TODO look for some inplace operation to save memory - # cv2.copyTo(src=full_img, mask=area_mask_img, dst=full_img) # this doesn't work due to implementation details - # full_img = cv2.copyTo(src=full_img, mask=self.area_mask_img) - - b = self.base_extent_bbox_in_full_image - result_img = full_img[int(b.y_min*self.superresolution_parameters.scale_factor):int(b.y_max*self.superresolution_parameters.scale_factor), - int(b.x_min*self.superresolution_parameters.scale_factor):int(b.x_max*self.superresolution_parameters.scale_factor), - :] - return result_img - - def load_rlayer_from_file(self, file_path): - """ - Create raster layer from tif file - """ - file_name = os.path.basename(file_path) - base_file_name = file_name.split('___')[0] # we remove the random_id string we created a moment ago - rlayer = QgsRasterLayer(file_path, base_file_name) - if rlayer.width() == 0: - raise Exception("0 width - rlayer not loaded properly. Probably invalid file path?") - rlayer.setCrs(self.rlayer.crs()) - return rlayer - - def _create_rlayers_from_images_for_base_extent(self, result_imgs: List[np.ndarray]): - # TODO: We are creating a new file for each layer. - # Maybe can we pass ownership of this file to QGis? - # Or maybe even create vlayer directly from array, without a file? - rlayers = [] - - for i, channel_id in enumerate(['Super Resolution']): - result_img = result_imgs - random_id = str(uuid.uuid4()).replace('-', '') - file_path = os.path.join(TMP_DIR_PATH, f'{channel_id}___{random_id}.tif') - self.save_result_img_as_tif(file_path=file_path, img=result_img) - - rlayer = self.load_rlayer_from_file(file_path) - OUTPUT_RLAYER_OPACITY = 0.5 - rlayer.renderer().setOpacity(OUTPUT_RLAYER_OPACITY) - rlayers.append(rlayer) - - def add_to_gui(): - group = QgsProject.instance().layerTreeRoot().insertGroup(0, 'Super Resolution Results') - for rlayer in rlayers: - QgsProject.instance().addMapLayer(rlayer, False) - group.addLayer(rlayer) - - return add_to_gui - - def save_result_img_as_tif(self, file_path: str, img: np.ndarray): - """ - As we cannot pass easily an numpy array to be displayed as raster layer, we create temporary geotif files, - which will be loaded as layer later on - - Partially based on example from: - https://gis.stackexchange.com/questions/82031/gdal-python-set-projection-of-a-raster-not-working - """ - os.makedirs(os.path.dirname(file_path), exist_ok=True) - - extent = self.base_extent - crs = self.rlayer.crs() - - geo_transform = [extent.xMinimum(), self.rlayer_units_per_pixel/self.superresolution_parameters.scale_factor, 0, - extent.yMaximum(), 0, -self.rlayer_units_per_pixel/self.superresolution_parameters.scale_factor] - - driver = gdal.GetDriverByName('GTiff') - n_lines = img.shape[0] - n_cols = img.shape[1] - n_chanels = img.shape[2] - # data_type = gdal.GDT_Byte - data_type = gdal.GDT_Float32 - grid_data = driver.Create('grid_data', n_cols, n_lines, n_chanels, data_type) # , options) - # loop over chanels - for i in range(1, img.shape[2]+1): - grid_data.GetRasterBand(i).WriteArray(img[:, :, i-1]) - - # crs().srsid() - maybe we can use the ID directly - but how? - # srs.ImportFromEPSG() - srs = osr.SpatialReference() - srs.SetFromUserInput(crs.authid()) - - grid_data.SetProjection(srs.ExportToWkt()) - grid_data.SetGeoTransform(geo_transform) - driver.CreateCopy(file_path, grid_data, 0) - print(f'***** {file_path = }') - - def _process_tile(self, tile_img: np.ndarray) -> np.ndarray: - result = self.model.process(tile_img) - result[np.isnan(result)] = 0 - result *= self.superresolution_parameters.output_scaling - - # NOTE - currently we are saving result as float32, so we are losing some accuraccy. - # result = np.clip(result, 0, 255) # old version with uint8_t - not used anymore - result = result.astype(np.float32) - - return result diff --git a/zipdeepness/deepness/processing/map_processor/map_processor_training_data_export.py b/zipdeepness/deepness/processing/map_processor/map_processor_training_data_export.py deleted file mode 100644 index a4996ee3834fc29da3c74bd617930f63e9b7bb79..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/map_processor/map_processor_training_data_export.py +++ /dev/null @@ -1,95 +0,0 @@ -""" This file implements map processing for the Training Data Export Tool """ - -import datetime -import os - -import numpy as np -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.processing import processing_utils -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 - -cv2 = LazyPackageLoader('cv2') - - -class MapProcessorTrainingDataExport(MapProcessor): - """ - Map Processor specialized in exporting training data, not doing any prediction with model. - Exports tiles for the ortophoto and a mask layer. - """ - - def __init__(self, - params: TrainingDataExportParameters, - **kwargs): - super().__init__( - params=params, - **kwargs) - self.params = params - self.output_dir_path = self._create_output_dir() - - def _create_output_dir(self) -> str: - datetime_string = datetime.datetime.now().strftime("%d%m%Y_%H%M%S") - full_path = os.path.join(self.params.output_directory_path, datetime_string) - os.makedirs(full_path, exist_ok=True) - return full_path - - def _run(self): - export_segmentation_mask = self.params.segmentation_mask_layer_id is not None - if export_segmentation_mask: - vlayer_segmentation = QgsProject.instance().mapLayers()[self.params.segmentation_mask_layer_id] - vlayer_segmentation.setCrs(self.rlayer.crs()) - segmentation_mask_full = processing_utils.create_area_mask_image( - rlayer=self.rlayer, - 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), - files_handler=self.file_handler) - - segmentation_mask_full = segmentation_mask_full[np.newaxis, ...] - - number_of_written_tiles = 0 - for tile_img, tile_params in self.tiles_generator(): - if self.isCanceled(): - return MapProcessingResultCanceled() - - tile_params = tile_params # type: TileParams - - if self.params.export_image_tiles: - file_name = f'tile_img_{tile_params.x_bin_number}_{tile_params.y_bin_number}.png' - file_path = os.path.join(self.output_dir_path, file_name) - - if tile_img.dtype in [np.uint32, np.int32]: - print(f'Exporting image with data type {tile_img.dtype} is not supported. Trimming to uint16. Consider changing the data type in the source image.') - tile_img = tile_img.astype(np.uint16) - - if tile_img.shape[-1] == 4: - tile_img = cv2.cvtColor(tile_img, cv2.COLOR_RGBA2BGRA) - elif tile_img.shape[-1] == 3: - tile_img = cv2.cvtColor(tile_img, cv2.COLOR_RGB2BGR) - - cv2.imwrite(file_path, tile_img) - number_of_written_tiles += 1 - - if export_segmentation_mask: - segmentation_mask_for_tile = tile_params.get_entire_tile_from_full_img(segmentation_mask_full) - - file_name = f'tile_mask_{tile_params.x_bin_number}_{tile_params.y_bin_number}.png' - file_path = os.path.join(self.output_dir_path, file_name) - - cv2.imwrite(file_path, segmentation_mask_for_tile[0]) - - result_message = self._create_result_message(number_of_written_tiles) - return MapProcessingResultSuccess(result_message) - - def _create_result_message(self, number_of_written_tiles) -> str: - total_area = self.img_size_x_pixels * self.img_size_y_pixels * self.params.resolution_m_per_px**2 - return f'Exporting data finished!\n' \ - f'Exported {number_of_written_tiles} tiles.\n' \ - f'Total processed area: {total_area:.2f} m^2\n' \ - f'Directory: "{self.output_dir_path}"' diff --git a/zipdeepness/deepness/processing/map_processor/map_processor_with_model.py b/zipdeepness/deepness/processing/map_processor/map_processor_with_model.py deleted file mode 100644 index f56a319f5ae98df720c56955508317525fd0c503..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/map_processor/map_processor_with_model.py +++ /dev/null @@ -1,27 +0,0 @@ - -""" This file implements map processing functions common for all map processors using nural model """ - -from typing import List - -from deepness.processing.map_processor.map_processor import MapProcessor -from deepness.processing.models.model_base import ModelBase - - -class MapProcessorWithModel(MapProcessor): - """ - Common base class for MapProcessor with models - """ - - def __init__(self, - model: ModelBase, - **kwargs): - super().__init__( - **kwargs) - self.model = model - - def _get_indexes_of_model_output_channels_to_create(self) -> List[int]: - """ - Decide what model output channels/classes we want to use at presentation level - (e.g. for which channels create a layer with results) - """ - return self.model.get_number_of_output_channels() diff --git a/zipdeepness/deepness/processing/map_processor/utils/ckdtree.py b/zipdeepness/deepness/processing/map_processor/utils/ckdtree.py deleted file mode 100644 index 3577a21159b2a7c2b75080625661e4647ef1c210..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/map_processor/utils/ckdtree.py +++ /dev/null @@ -1,62 +0,0 @@ -import heapq - -import numpy as np - - -class cKDTree: - def __init__(self, data): - self.data = np.asarray(data) - self.tree = self._build_kdtree(np.arange(len(data))) - - def _build_kdtree(self, indices, depth=0): - if len(indices) == 0: - return None - axis = depth % self.data.shape[1] # alternate between x and y dimensions - - sorted_indices = indices[np.argsort(self.data[indices, axis])] - mid = len(sorted_indices) // 2 - node = { - 'index': sorted_indices[mid], - 'left': self._build_kdtree(sorted_indices[:mid], depth + 1), - 'right': self._build_kdtree(sorted_indices[mid + 1:], depth + 1) - } - return node - - def query(self, point: np.ndarray, k: int): - if type(point) is not np.ndarray: - point = np.array(point) - - return [index for _, index in self._query(point, k, self.tree)] - - def _query(self, point, k, node, depth=0, best_indices=None, best_distances=None): - if node is None: - return None - - axis = depth % self.data.shape[1] - - if point[axis] < self.data[node['index']][axis]: - next_node = node['left'] - other_node = node['right'] - else: - next_node = node['right'] - other_node = node['left'] - - if best_indices is None: - best_indices = [] - best_distances = [] - - current_distance = np.linalg.norm(self.data[node['index']] - point) - - if len(best_indices) < k: - heapq.heappush(best_indices, (-current_distance, node['index'])) - elif current_distance < -best_indices[0][0]: - heapq.heappop(best_indices) - heapq.heappush(best_indices, (-current_distance, node['index'])) - - if point[axis] < self.data[node['index']][axis] or len(best_indices) < k or abs(point[axis] - self.data[node['index']][axis]) < -best_indices[0][0]: - self._query(point, k, next_node, depth + 1, best_indices, best_distances) - - if point[axis] >= self.data[node['index']][axis] or len(best_indices) < k or abs(point[axis] - self.data[node['index']][axis]) < -best_indices[0][0]: - self._query(point, k, other_node, depth + 1, best_indices, best_distances) - - return best_indices diff --git a/zipdeepness/deepness/processing/models/__init__.py b/zipdeepness/deepness/processing/models/__init__.py deleted file mode 100644 index d080009a194567252f67421a85ea8ac478e3b030..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/models/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -""" Module including classes implemetations for the deep learning inference and related functions -""" \ No newline at end of file diff --git a/zipdeepness/deepness/processing/models/buildings_type_MA.onnx b/zipdeepness/deepness/processing/models/buildings_type_MA.onnx deleted file mode 100644 index d4c0d930a26ece468401dfd85f7350a4a26b92f0..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/models/buildings_type_MA.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4e7c06401e20f6791e272f5ec694d4ae285c79ed6ceb9213875972114869b90f -size 464134848 diff --git a/zipdeepness/deepness/processing/models/detector.py b/zipdeepness/deepness/processing/models/detector.py deleted file mode 100644 index 474802d5ca4a5b5ad070456ade469a35fbfb5eb3..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/models/detector.py +++ /dev/null @@ -1,709 +0,0 @@ -""" Module including the class for the object detection task and related functions -""" -from dataclasses import dataclass -from typing import List, Optional, Tuple -from qgis.core import Qgis, QgsGeometry, QgsRectangle, QgsPointXY - -import cv2 -import numpy as np - -from deepness.common.processing_parameters.detection_parameters import DetectorType -from deepness.processing.models.model_base import ModelBase -from deepness.processing.processing_utils import BoundingBox - - -@dataclass -class Detection: - """Class that represents single detection result in object detection model - - Parameters - ---------- - bbox : BoundingBox - bounding box describing the detection rectangle - conf : float - confidence of the detection - clss : int - class of the detected object - """ - - bbox: BoundingBox - """BoundingBox: bounding box describing the detection rectangle""" - conf: float - """float: confidence of the detection""" - clss: int - """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 - """Tuple[int, int]: offsets of the mask""" - - def convert_to_global(self, offset_x: int, offset_y: int): - """Apply (x,y) offset to bounding box coordinates - - Parameters - ---------- - offset_x : int - _description_ - 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 - - Returns - ------- - np.ndarray - Array in (x1, y1, x2, y2) format - """ - return self.bbox.get_xyxy() - - def get_bbox_xyxy_rot(self) -> np.ndarray: - """Convert stored bounding box into x1y1x2y2r format - - Returns - ------- - np.ndarray - Array in (x1, y1, x2, y2, r) format - """ - return self.bbox.get_xyxy_rot() - - def get_bbox_center(self) -> Tuple[int, int]: - """Get center of the bounding box - - Returns - ------- - Tuple[int, int] - Center of the bounding box - """ - return self.bbox.get_center() - - def __lt__(self, other): - return self.bbox.get_area() < other.bbox.get_area() - - -class Detector(ModelBase): - """Class implements object detection features - - Detector model is used for detection of objects in images. It is based on YOLOv5/YOLOv7 models style. - """ - - def __init__(self, model_file_path: str): - """Initialize object detection model - - Parameters - ---------- - model_file_path : str - Path to model file""" - super(Detector, self).__init__(model_file_path) - - self.confidence = None - """float: Confidence threshold""" - self.iou_threshold = None - """float: IoU threshold""" - self.model_type: Optional[DetectorType] = None - """DetectorType: Model type""" - - def set_inference_params(self, confidence: float, iou_threshold: float): - """Set inference parameters - - Parameters - ---------- - confidence : float - Confidence threshold - iou_threshold : float - IoU threshold - """ - self.confidence = confidence - self.iou_threshold = iou_threshold - - def set_model_type_param(self, model_type: DetectorType): - """Set model type parameters - - Parameters - ---------- - model_type : str - Model type - """ - self.model_type = model_type - - @classmethod - def get_class_display_name(cls): - """Get class display name - - Returns - ------- - str - Class display name""" - return cls.__name__ - - def get_number_of_output_channels(self): - """Get number of output channels - - Returns - ------- - int - Number of output channels - """ - class_names = self.get_outputs_channel_names()[0] - if class_names is not None: - return [len(class_names)] # If class names are specified, we expect to have exactly this number of channels as specidied - - model_type_params = self.model_type.get_parameters() - - shape_index = -2 if model_type_params.has_inverted_output_shape else -1 - - if len(self.outputs_layers) == 1: - # YOLO_ULTRALYTICS_OBB - if self.model_type == DetectorType.YOLO_ULTRALYTICS_OBB: - return [self.outputs_layers[0].shape[shape_index] - 4 - 1] - - elif 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 - - # YOLO_ULTRALYTICS_SEGMENTATION - 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 postprocessing(self, model_output): - """Postprocess model output - - NOTE: Maybe refactor this, as it has many added layers of checks which can be simplified. - - Parameters - ---------- - model_output : list - Model output - - Returns - ------- - list - Batch of lists of detections - """ - if self.confidence is None or self.iou_threshold is None: - return Exception( - "Confidence or IOU threshold is not set for model. Use self.set_inference_params" - ) - - if self.model_type is None: - return Exception( - "Model type is not set for model. Use self.set_model_type_param" - ) - - batch_detection = [] - outputs_range = len(model_output) - - if self.model_type == DetectorType.YOLO_ULTRALYTICS_SEGMENTATION or self.model_type == DetectorType.YOLO_v9: - outputs_range = len(model_output[0]) - - for i in range(outputs_range): - masks = None - rots = 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_v9: - boxes, conf, classes = self._postprocessing_YOLO_v9(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]) - elif self.model_type == DetectorType.YOLO_ULTRALYTICS_OBB: - boxes, conf, classes, rots = self._postprocessing_YOLO_ULTRALYTICS_OBB(model_output[0][i]) - else: - raise NotImplementedError(f"Model type not implemented! ('{self.model_type}')") - - masks = masks if masks is not None else [None] * len(boxes) - rots = rots if rots is not None else [0.0] * len(boxes) - - for b, c, cl, m, r in zip(boxes, conf, classes, masks, rots): - det = Detection( - bbox=BoundingBox( - x_min=b[0], - x_max=b[2], - y_min=b[1], - y_max=b[3], - rot=r), - conf=c, - clss=cl, - mask=m, - ) - detections.append(det) - - batch_detection.append(detections) - - return batch_detection - - def _postprocessing_YOLO_v5_v7_DEFAULT(self, model_output): - outputs_filtered = np.array( - list(filter(lambda x: x[4] >= self.confidence, model_output)) - ) - - if len(outputs_filtered.shape) < 2: - return [], [], [] - - probabilities = outputs_filtered[:, 4] - - 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 = outputs_nms[:, 4] - classes = np.argmax(outputs_nms[:, 5:], axis=1) - - return boxes, conf, classes - - def _postprocessing_YOLO_v6(self, model_output): - outputs_filtered = np.array( - list(filter(lambda x: np.max(x[5:]) >= self.confidence, model_output)) - ) - - if len(outputs_filtered.shape) < 2: - return [], [], [] - - probabilities = outputs_filtered[:, 4] - - 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[:, 5:], axis=1) - classes = np.argmax(outputs_nms[:, 5:], axis=1) - - return boxes, conf, classes - - def _postprocessing_YOLO_v9(self, model_output): - model_output = np.transpose(model_output, (1, 0)) - - outputs_filtered = np.array( - list(filter(lambda x: np.max(x[4:]) >= self.confidence, model_output)) - ) - - if len(outputs_filtered.shape) < 2: - return [], [], [] - - probabilities = np.max(outputs_filtered[:, 4:], 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:], axis=1) - classes = np.argmax(outputs_nms[:, 4:], axis=1) - - return boxes, conf, classes - - def _postprocessing_YOLO_ULTRALYTICS(self, model_output): - model_output = np.transpose(model_output, (1, 0)) - - outputs_filtered = np.array( - list(filter(lambda x: np.max(x[4:]) >= self.confidence, model_output)) - ) - - if len(outputs_filtered.shape) < 2: - return [], [], [] - - probabilities = np.max(outputs_filtered[:, 4:], 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:], axis=1) - classes = np.argmax(outputs_nms[:, 4:], axis=1) - - return boxes, conf, classes - - def _postprocessing_YOLO_ULTRALYTICS_SEGMENTATION(self, detections, protos): - detections = np.transpose(detections, (1, 0)) - - number_of_class = self.get_number_of_output_channels()[0] - 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 - - def _postprocessing_YOLO_ULTRALYTICS_OBB(self, model_output): - model_output = np.transpose(model_output, (1, 0)) - - outputs_filtered = np.array( - list(filter(lambda x: np.max(x[4:-1]) >= self.confidence, model_output)) - ) - - if len(outputs_filtered.shape) < 2: - return [], [], [], [] - - probabilities = np.max(outputs_filtered[:, 4:-1], axis=1) - rotations = outputs_filtered[:, -1] - - outputs_x1y1x2y2_rot = self.xywhr2xyxyr(outputs_filtered, rotations) - - pick_indxs = self.non_max_suppression_fast( - outputs_x1y1x2y2_rot, - probs=probabilities, - iou_threshold=self.iou_threshold, - with_rot=True) - - outputs_nms = outputs_x1y1x2y2_rot[pick_indxs] - - boxes = np.array(outputs_nms[:, :4], dtype=int) - conf = np.max(outputs_nms[:, 4:-1], axis=1) - classes = np.argmax(outputs_nms[:, 4:-1], axis=1) - rots = outputs_nms[:, -1] - - return boxes, conf, classes, rots - - # 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: - """Convert bounding box from (x,y,w,h) to (x1,y1,x2,y2) format - - Parameters - ---------- - x : np.ndarray - Bounding box in (x,y,w,h) format with classes' probabilities - - Returns - ------- - np.ndarray - Bounding box in (x1,y1,x2,y2) format - """ - y = np.copy(x) - y[:, 0] = x[:, 0] - x[:, 2] / 2 # top left x - y[:, 1] = x[:, 1] - x[:, 3] / 2 # top left y - y[:, 2] = x[:, 0] + x[:, 2] / 2 # bottom right x - y[:, 3] = x[:, 1] + x[:, 3] / 2 # bottom right y - return y - - @staticmethod - def xywhr2xyxyr(bbox: np.ndarray, rot: np.ndarray) -> np.ndarray: - """Convert bounding box from (x,y,w,h,r) to (x1,y1,x2,y2,r) format, keeping rotated boxes in range [0, pi/2] - - Parameters - ---------- - bbox : np.ndarray - Bounding box in (x,y,w,h) format with classes' probabilities and rotations - - Returns - ------- - np.ndarray - Bounding box in (x1,y1,x2,y2,r) format, keep classes' probabilities - """ - x, y, w, h = bbox[:, 0], bbox[:, 1], bbox[:, 2], bbox[:, 3] - - w_ = np.where(w > h, w, h) - h_ = np.where(w > h, h, w) - r_ = np.where(w > h, rot, rot + np.pi / 2) % np.pi - - new_bbox_xywh = np.stack([x, y, w_, h_], axis=1) - new_bbox_xyxy = Detector.xywh2xyxy(new_bbox_xywh) - - return np.concatenate([new_bbox_xyxy, bbox[:, 4:-1], r_[:, None]], axis=1) - - - @staticmethod - def non_max_suppression_fast(boxes: np.ndarray, probs: np.ndarray, iou_threshold: float, with_rot: bool = False) -> List: - """Apply non-maximum suppression to bounding boxes - - Based on: - https://github.com/amusi/Non-Maximum-Suppression/blob/master/nms.py - - Parameters - ---------- - boxes : np.ndarray - Bounding boxes in (x1,y1,x2,y2) format or (x1,y1,x2,y2,r) format if with_rot is True - probs : np.ndarray - Confidence scores - iou_threshold : float - IoU threshold - with_rot: bool - If True, use rotated IoU - - Returns - ------- - List - List of indexes of bounding boxes to keep - """ - # If no bounding boxes, return empty list - if len(boxes) == 0: - return [] - - # Bounding boxes - boxes = np.array(boxes) - - # coordinates of bounding boxes - start_x = boxes[:, 0] - start_y = boxes[:, 1] - end_x = boxes[:, 2] - end_y = boxes[:, 3] - - if with_rot: - # Rotations of bounding boxes - rotations = boxes[:, 4] - - # Confidence scores of bounding boxes - score = np.array(probs) - - # Picked bounding boxes - picked_boxes = [] - - # Compute areas of bounding boxes - areas = (end_x - start_x + 1) * (end_y - start_y + 1) - - # Sort by confidence score of bounding boxes - order = np.argsort(score) - - # Iterate bounding boxes - while order.size > 0: - # The index of largest confidence score - index = order[-1] - - # Pick the bounding box with largest confidence score - picked_boxes.append(index) - - if not with_rot: - ratio = Detector.compute_iou(index, order, start_x, start_y, end_x, end_y, areas) - else: - ratio = Detector.compute_rotated_iou(index, order, start_x, start_y, end_x, end_y, rotations, areas) - - left = np.where(ratio < iou_threshold) - order = order[left] - - return picked_boxes - - @staticmethod - def compute_iou(index: int, order: np.ndarray, start_x: np.ndarray, start_y: np.ndarray, end_x: np.ndarray, end_y: np.ndarray, areas: np.ndarray) -> np.ndarray: - """Compute IoU for bounding boxes - - Parameters - ---------- - index : int - Index of the bounding box - order : np.ndarray - Order of bounding boxes - start_x : np.ndarray - Start x coordinate of bounding boxes - start_y : np.ndarray - Start y coordinate of bounding boxes - end_x : np.ndarray - End x coordinate of bounding boxes - end_y : np.ndarray - End y coordinate of bounding boxes - areas : np.ndarray - Areas of bounding boxes - - Returns - ------- - np.ndarray - IoU values - """ - - # Compute ordinates of intersection-over-union(IOU) - x1 = np.maximum(start_x[index], start_x[order[:-1]]) - x2 = np.minimum(end_x[index], end_x[order[:-1]]) - y1 = np.maximum(start_y[index], start_y[order[:-1]]) - y2 = np.minimum(end_y[index], end_y[order[:-1]]) - - # Compute areas of intersection-over-union - w = np.maximum(0.0, x2 - x1 + 1) - h = np.maximum(0.0, y2 - y1 + 1) - intersection = w * h - - # Compute the ratio between intersection and union - return intersection / (areas[index] + areas[order[:-1]] - intersection) - - - @staticmethod - def compute_rotated_iou(index: int, order: np.ndarray, start_x: np.ndarray, start_y: np.ndarray, end_x: np.ndarray, end_y: np.ndarray, rotations: np.ndarray, areas: np.ndarray) -> np.ndarray: - """Compute IoU for rotated bounding boxes - - Parameters - ---------- - index : int - Index of the bounding box - order : np.ndarray - Order of bounding boxes - start_x : np.ndarray - Start x coordinate of bounding boxes - start_y : np.ndarray - Start y coordinate of bounding boxes - end_x : np.ndarray - End x coordinate of bounding boxes - end_y : np.ndarray - End y coordinate of bounding boxes - rotations : np.ndarray - Rotations of bounding boxes (in radians, around the center) - areas : np.ndarray - Areas of bounding boxes - - Returns - ------- - np.ndarray - IoU values - """ - - def create_rotated_geom(x1, y1, x2, y2, rotation): - """Helper function to create a rotated QgsGeometry rectangle""" - # Define the corners of the box before rotation - center_x = (x1 + x2) / 2 - center_y = (y1 + y2) / 2 - - # Create a rectangle using QgsRectangle - rect = QgsRectangle(QgsPointXY(x1, y1), QgsPointXY(x2, y2)) - - # Convert to QgsGeometry - geom = QgsGeometry.fromRect(rect) - - # Rotate the geometry around its center - result = geom.rotate(np.degrees(rotation), QgsPointXY(center_x, center_y)) - - if result == Qgis.GeometryOperationResult.Success: - return geom - else: - return QgsGeometry() - - # Create the rotated geometry for the current bounding box - geom1 = create_rotated_geom(start_x[index], start_y[index], end_x[index], end_y[index], rotations[index]) - - iou_values = [] - - # Iterate over the rest of the boxes in order and calculate IoU - for i in range(len(order) - 1): - # Create the rotated geometry for the other boxes in the order - geom2 = create_rotated_geom(start_x[order[i]], start_y[order[i]], end_x[order[i]], end_y[order[i]], rotations[order[i]]) - - # Compute the intersection geometry - intersection_geom = geom1.intersection(geom2) - - # Check if intersection is empty - if intersection_geom.isEmpty(): - intersection_area = 0.0 - else: - # Compute the intersection area - intersection_area = intersection_geom.area() - - # Compute the union area - union_area = areas[index] + areas[order[i]] - intersection_area - - # Compute IoU - iou = intersection_area / union_area if union_area > 0 else 0.0 - iou_values.append(iou) - - return np.array(iou_values) - - - def check_loaded_model_outputs(self): - """Check if model outputs are valid. - Valid model are: - - has 1 or 2 outputs layer - - output layer shape length is 3 - - batch size is 1 - """ - - if len(self.outputs_layers) == 1 or len(self.outputs_layers) == 2: - shape = self.outputs_layers[0].shape - - if len(shape) != 3: - raise Exception( - f"Detection model output should have 3 dimensions: (Batch_size, detections, values). " - f"Actually has: {shape}" - ) - - else: - raise NotImplementedError("Model with multiple output layer is not supported! Use only one output layer.") diff --git a/zipdeepness/deepness/processing/models/dual.py b/zipdeepness/deepness/processing/models/dual.py deleted file mode 100644 index dff77f85a0ea2ba8f62bd9a74fcb1f4f1ac5f4dd..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/models/dual.py +++ /dev/null @@ -1,168 +0,0 @@ -from deepness.processing.models.segmentor import Segmentor -import numpy as np -import cv2 -import onnxruntime as ort -import os -from typing import List - -class DualModel(Segmentor): - - def __init__(self, model_file_path: str): - super().__init__(model_file_path) - current_dir = os.path.dirname(os.path.abspath(__file__)) - # Both models are in the same folder - self.second_model_file_path = os.path.join(current_dir, "buildings_type_MA.onnx") - self.second_model = ort.InferenceSession(self.second_model_file_path, providers=["CUDAExecutionProvider", "CPUExecutionProvider"]) - - - - - def preprocess_tiles_ade20k(self,tiles_batched: np.ndarray) -> np.ndarray: - # ADE20K mean/std in 0-1 range - ADE_MEAN = np.array([123.675, 116.280, 103.530]) / 255.0 - ADE_STD = np.array([58.395, 57.120, 57.375]) / 255.0 - - tiles = tiles_batched.astype(np.float32) / 255.0 # 0-1 - - # Standardize per channel - tiles = (tiles - ADE_MEAN) / ADE_STD # broadcasting over (N,H,W,C) - - # Ensure 3D channels (if grayscale) - if tiles.ndim == 3: # (H,W,C) single image - tiles = np.expand_dims(tiles, axis=0) # add batch dim - - if tiles.shape[-1] == 1: # if only 1 channel - tiles = np.repeat(tiles, 3, axis=-1) - - # NHWC -> NCHW - tiles = np.transpose(tiles, (0, 3, 1, 2)) - - return tiles.astype(np.float32) - - - - - def stable_sigmoid(self, x): - out = np.empty_like(x, dtype=np.float32) - positive_mask = x >= 0 - negative_mask = ~positive_mask - - out[positive_mask] = 1 / (1 + np.exp(-x[positive_mask])) - exp_x = np.exp(x[negative_mask]) - out[negative_mask] = exp_x / (1 + exp_x) - return out - - - def stable_softmax(self, x, axis=-1): - max_x = np.max(x, axis=axis, keepdims=True) - exp_x = np.exp(x - max_x) - return exp_x / np.sum(exp_x, axis=axis, keepdims=True) - - - def post_process_semantic_segmentation_numpy(self, class_queries_logits, masks_queries_logits, target_sizes=None): - # Softmax over classes (remove null class) - masks_classes = self.stable_softmax(class_queries_logits, axis=-1)[..., :-1] - - # Sigmoid for masks - masks_probs = self.stable_sigmoid(masks_queries_logits) - - # Combine: torch.einsum("bqc,bqhw->bchw") - segmentation = np.einsum("bqc,bqhw->bchw", masks_classes, masks_probs) - - semantic_segmentation = [] - if target_sizes is not None: - if len(target_sizes) != class_queries_logits.shape[0]: - raise ValueError("target_sizes length must match batch size") - - for idx in range(len(target_sizes)): - out_h, out_w = target_sizes[idx] - logits_resized = np.zeros((segmentation.shape[1], out_h, out_w), dtype=np.float32) - for c in range(segmentation.shape[1]): - logits_resized[c] = cv2.resize( - segmentation[idx, c], - (out_w, out_h), - interpolation=cv2.INTER_LINEAR - ) - semantic_map = np.argmax(logits_resized, axis=0) - semantic_segmentation.append(semantic_map.astype(np.int32)) - else: - for idx in range(segmentation.shape[0]): - semantic_map = np.argmax(segmentation[idx], axis=0) - semantic_segmentation.append(semantic_map.astype(np.int32)) - - return semantic_segmentation - - def return_probs_mask2former(self, - class_queries_logits, - masks_queries_logits, - target_sizes=None): - - # Softmax over classes (remove null class) - masks_classes = self.stable_softmax(class_queries_logits, axis=-1)[..., :-1] - - # Sigmoid for masks - masks_probs = self.stable_sigmoid(masks_queries_logits) - - # Combine: einsum bqc,bqhw -> bchw - segmentation = np.einsum("bqc,bqhw->bchw", masks_classes, masks_probs) - - # Resize if target_sizes is given - if target_sizes is not None: - if len(target_sizes) != segmentation.shape[0]: - raise ValueError("target_sizes length must match batch size") - - segmentation_resized = np.zeros( - (segmentation.shape[0], segmentation.shape[1], target_sizes[0][0], target_sizes[0][1]), - dtype=np.float32 - ) - - for idx in range(segmentation.shape[0]): - out_h, out_w = target_sizes[idx] - for c in range(segmentation.shape[1]): - segmentation_resized[idx, c] = cv2.resize( - segmentation[idx, c], - (out_w, out_h), - interpolation=cv2.INTER_LINEAR - ) - segmentation = segmentation_resized - - - return segmentation # shape: (B, C, H, W) - - - - - - def process(self, tiles_batched: np.ndarray): - input_batch = self.preprocessing(tiles_batched) - input_building_batch = self.preprocess_tiles_ade20k(tiles_batched) - - model_output = self.sess.run( - output_names=None, - input_feed={self.input_name: input_batch}) - res = self.postprocessing(model_output) - logits_np, masks_np = self.second_model.run(["logits", "outputs_mask.35"], {"pixel_values": input_building_batch}) - target_sizes=[(512, 512)]*logits_np.shape[0] - predicted_seg = self.post_process_semantic_segmentation_numpy( - class_queries_logits=logits_np, - masks_queries_logits=masks_np, - target_sizes=target_sizes - ) - # new_class = {0:0, 1:5, 2:6, 3:7, 4:8} - # build_mask = np.zeros((512,512), dtype=np.int8) - # segmentation_map = predicted_seg[0] - # for k,v in new_class.items(): - # build_mask[segmentation_map == k] = v - - return res[0], predicted_seg - - - def get_number_of_output_channels(self) -> List[int]: - return [9] - - def get_output_shapes(self) -> List[tuple]: - return [("N", 9, 512, 512)] - - - - diff --git a/zipdeepness/deepness/processing/models/model_base.py b/zipdeepness/deepness/processing/models/model_base.py deleted file mode 100644 index 11dc214f8fd54746da5be72286a8f8e40843b9e9..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/models/model_base.py +++ /dev/null @@ -1,444 +0,0 @@ -""" Module including the base model interfaces and utilities""" -import ast -import json -from typing import List, Optional - -import numpy as np - -from deepness.common.lazy_package_loader import LazyPackageLoader -from deepness.common.processing_parameters.standardization_parameters import StandardizationParameters - -ort = LazyPackageLoader('onnxruntime') - - -class ModelBase: - """ - Wraps the ONNX model used during processing into a common interface - """ - - def __init__(self, model_file_path: str): - """ - - Parameters - ---------- - model_file_path : str - Path to the model file - """ - self.model_file_path = model_file_path - - options = ort.SessionOptions() - options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL - - providers = [ - 'CUDAExecutionProvider', - 'CPUExecutionProvider' - ] - - self.sess = ort.InferenceSession(self.model_file_path, options=options, providers=providers) - inputs = self.sess.get_inputs() - if len(inputs) > 1: - raise Exception("ONNX model: unsupported number of inputs") - input_0 = inputs[0] - - self.input_shape = input_0.shape - self.input_name = input_0.name - - self.outputs_layers = self.sess.get_outputs() - self.standardization_parameters: StandardizationParameters = self.get_metadata_standarization_parameters() - - self.outputs_names = self.get_outputs_channel_names() - - @classmethod - def get_model_type_from_metadata(cls, model_file_path: str) -> Optional[str]: - """ Get model type from metadata - - Parameters - ---------- - model_file_path : str - Path to the model file - - Returns - ------- - Optional[str] - Model type or None if not found - """ - model = cls(model_file_path) - return model.get_metadata_model_type() - - def get_input_shape(self) -> tuple: - """ Get shape of the input for the model - - Returns - ------- - tuple - Shape of the input (batch_size, channels, height, width) - """ - return self.input_shape - - def get_output_shapes(self) -> List[tuple]: - """ Get shapes of the outputs for the model - - Returns - ------- - List[tuple] - Shapes of the outputs (batch_size, channels, height, width) - """ - return [output.shape for output in self.outputs_layers] - - 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) - - Returns - ------- - int - Number of pixels in x and y direction - """ - return self.input_shape[-2:] - - def get_outputs_channel_names(self) -> Optional[List[List[str]]]: - """ Get class names from metadata - - Returns - ------- - List[List[str]] | None - List of class names for each model output or None if not found - """ - meta = self.sess.get_modelmeta() - - allowed_key_names = ['class_names', 'names'] # support both names for backward compatibility - for name in allowed_key_names: - if name not in meta.custom_metadata_map: - continue - - txt = meta.custom_metadata_map[name] - try: - class_names = json.loads(txt) # default format recommended in the documentation - classes encoded as json - except json.decoder.JSONDecodeError: - class_names = ast.literal_eval(txt) # keys are integers instead of strings - use ast - - if isinstance(class_names, dict): - class_names = [class_names] - - sorted_by_key = [sorted(cn.items(), key=lambda kv: int(kv[0])) for cn in class_names] - - all_names = [] - - for output_index in range(len(sorted_by_key)): - output_names = [] - class_counter = 0 - - for key, value in sorted_by_key[output_index]: - if int(key) != class_counter: - raise Exception("Class names in the model metadata are not consecutive (missing class label)") - class_counter += 1 - output_names.append(value) - all_names.append(output_names) - - return all_names - - return None - - def get_channel_name(self, layer_id: int, channel_id: int) -> str: - """ Get channel name by id if exists in model metadata - - Parameters - ---------- - channel_id : int - Channel id (means index in the output tensor) - - Returns - ------- - str - Channel name or empty string if not found - """ - - channel_id_str = str(channel_id) - default_return = f'channel_{channel_id_str}' - - if self.outputs_names is None: - return default_return - - if layer_id >= len(self.outputs_names): - raise Exception(f'Layer id {layer_id} is out of range of the model outputs') - - if channel_id >= len(self.outputs_names[layer_id]): - raise Exception(f'Channel id {channel_id} is out of range of the model outputs') - - return f'{self.outputs_names[layer_id][channel_id]}' - - def get_metadata_model_type(self) -> Optional[str]: - """ Get model type from metadata - - Returns - ------- - Optional[str] - Model type or None if not found - """ - meta = self.sess.get_modelmeta() - name = 'model_type' - if name in meta.custom_metadata_map: - value = json.loads(meta.custom_metadata_map[name]) - return str(value).capitalize() - return None - - def get_metadata_standarization_parameters(self) -> Optional[StandardizationParameters]: - """ Get standardization parameters from metadata if exists - - Returns - ------- - Optional[StandardizationParameters] - Standardization parameters or None if not found - """ - meta = self.sess.get_modelmeta() - name_mean = 'standardization_mean' - name_std = 'standardization_std' - - param = StandardizationParameters(channels_number=self.get_input_shape()[-3]) - - if name_mean in meta.custom_metadata_map and name_std in meta.custom_metadata_map: - mean = json.loads(meta.custom_metadata_map[name_mean]) - std = json.loads(meta.custom_metadata_map[name_std]) - - mean = [float(x) for x in mean] - std = [float(x) for x in std] - - param.set_mean_std(mean=mean, std=std) - - return param - - return param # default, no standardization - - def get_metadata_resolution(self) -> Optional[float]: - """ Get resolution from metadata if exists - - Returns - ------- - Optional[float] - Resolution or None if not found - """ - meta = self.sess.get_modelmeta() - name = 'resolution' - if name in meta.custom_metadata_map: - value = json.loads(meta.custom_metadata_map[name]) - return float(value) - return None - - def get_metadata_tile_size(self) -> Optional[int]: - """ Get tile size from metadata if exists - - Returns - ------- - Optional[int] - Tile size or None if not found - """ - meta = self.sess.get_modelmeta() - name = 'tile_size' - if name in meta.custom_metadata_map: - value = json.loads(meta.custom_metadata_map[name]) - return int(value) - return None - - def get_metadata_tiles_overlap(self) -> Optional[int]: - """ Get tiles overlap from metadata if exists - - Returns - ------- - Optional[int] - Tiles overlap or None if not found - """ - meta = self.sess.get_modelmeta() - name = 'tiles_overlap' - if name in meta.custom_metadata_map: - value = json.loads(meta.custom_metadata_map[name]) - return int(value) - return None - - def get_metadata_segmentation_threshold(self) -> Optional[float]: - """ Get segmentation threshold from metadata if exists - - Returns - ------- - Optional[float] - Segmentation threshold or None if not found - """ - meta = self.sess.get_modelmeta() - name = 'seg_thresh' - if name in meta.custom_metadata_map: - value = json.loads(meta.custom_metadata_map[name]) - return float(value) - return None - - def get_metadata_segmentation_small_segment(self) -> Optional[int]: - """ Get segmentation small segment from metadata if exists - - Returns - ------- - Optional[int] - Segmentation small segment or None if not found - """ - meta = self.sess.get_modelmeta() - name = 'seg_small_segment' - if name in meta.custom_metadata_map: - value = json.loads(meta.custom_metadata_map[name]) - return int(value) - return None - - def get_metadata_regression_output_scaling(self) -> Optional[float]: - """ Get regression output scaling from metadata if exists - - Returns - ------- - Optional[float] - Regression output scaling or None if not found - """ - meta = self.sess.get_modelmeta() - name = 'reg_output_scaling' - if name in meta.custom_metadata_map: - value = json.loads(meta.custom_metadata_map[name]) - return float(value) - return None - - def get_metadata_detection_confidence(self) -> Optional[float]: - """ Get detection confidence from metadata if exists - - Returns - ------- - Optional[float] - Detection confidence or None if not found - """ - meta = self.sess.get_modelmeta() - name = 'det_conf' - if name in meta.custom_metadata_map: - value = json.loads(meta.custom_metadata_map[name]) - return float(value) - return None - - def get_detector_type(self) -> Optional[str]: - """ Get detector type from metadata if exists - - Returns string value of DetectorType enum or None if not found - ------- - Optional[str] - Detector type or None if not found - """ - meta = self.sess.get_modelmeta() - name = 'det_type' - if name in meta.custom_metadata_map: - value = json.loads(meta.custom_metadata_map[name]) - return str(value) - return None - - def get_metadata_detection_iou_threshold(self) -> Optional[float]: - """ Get detection iou threshold from metadata if exists - - Returns - ------- - Optional[float] - Detection iou threshold or None if not found - """ - meta = self.sess.get_modelmeta() - name = 'det_iou_thresh' - if name in meta.custom_metadata_map: - value = json.loads(meta.custom_metadata_map[name]) - return float(value) - return None - - def get_number_of_channels(self) -> int: - """ Returns number of channels in the input layer - - Returns - ------- - int - Number of channels in the input layer - """ - return self.input_shape[-3] - - def process(self, tiles_batched: np.ndarray): - """ Process a single tile image - - Parameters - ---------- - img : np.ndarray - Image to process ([TILE_SIZE x TILE_SIZE x channels], type uint8, values 0 to 255) - - Returns - ------- - np.ndarray - Single prediction - """ - 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, tiles_batched: np.ndarray) -> np.ndarray: - """ Preprocess the batch of images for the model (resize, normalization, etc) - - Parameters - ---------- - image : np.ndarray - Batch of images to preprocess (N,H,W,C), RGB, 0-255 - - Returns - ------- - np.ndarray - Preprocessed batch of image (N,C,H,W), RGB, 0-1 - """ - - # imported here, to avoid isseue with uninstalled dependencies during the first plugin start - # in other places we use LazyPackageLoader, but here it is not so easy - import deepness.processing.models.preprocessing_utils as preprocessing_utils - - tiles_batched = preprocessing_utils.limit_channels_number(tiles_batched, limit=self.input_shape[-3]) - tiles_batched = preprocessing_utils.normalize_values_to_01(tiles_batched) - tiles_batched = preprocessing_utils.standardize_values(tiles_batched, params=self.standardization_parameters) - tiles_batched = preprocessing_utils.transpose_nhwc_to_nchw(tiles_batched) - - return tiles_batched - - def postprocessing(self, outs: List) -> np.ndarray: - """ Abstract method for postprocessing - - Parameters - ---------- - outs : List - Output from the model (depends on the model type) - - Returns - ------- - np.ndarray - Postprocessed output - """ - raise NotImplementedError('Base class not implemented!') - - def get_number_of_output_channels(self) -> List[int]: - """ Abstract method for getting number of classes in the output layer - - Returns - ------- - int - Number of channels in the output layer""" - raise NotImplementedError('Base class not implemented!') - - def check_loaded_model_outputs(self): - """ Abstract method for checking if the model outputs are valid - - """ - raise NotImplementedError('Base class not implemented!') diff --git a/zipdeepness/deepness/processing/models/model_types.py b/zipdeepness/deepness/processing/models/model_types.py deleted file mode 100644 index a0e2c7d6db23be65ad289462e740a94e00d564ee..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/models/model_types.py +++ /dev/null @@ -1,93 +0,0 @@ -import enum -from dataclasses import dataclass - -from deepness.common.processing_parameters.detection_parameters import DetectionParameters -from deepness.common.processing_parameters.map_processing_parameters import MapProcessingParameters -from deepness.common.processing_parameters.recognition_parameters import RecognitionParameters -from deepness.common.processing_parameters.regression_parameters import RegressionParameters -from deepness.common.processing_parameters.segmentation_parameters import SegmentationParameters -from deepness.common.processing_parameters.superresolution_parameters import SuperresolutionParameters -from deepness.processing.map_processor.map_processor_detection import MapProcessorDetection -from deepness.processing.map_processor.map_processor_recognition import MapProcessorRecognition -from deepness.processing.map_processor.map_processor_regression import MapProcessorRegression -from deepness.processing.map_processor.map_processor_segmentation import MapProcessorSegmentation -from deepness.processing.map_processor.map_processor_superresolution import MapProcessorSuperresolution -from deepness.processing.models.detector import Detector -from deepness.processing.models.recognition import Recognition -from deepness.processing.models.regressor import Regressor -from deepness.processing.models.segmentor import Segmentor -from deepness.processing.models.superresolution import Superresolution -from deepness.processing.models.dual import DualModel - - -class ModelType(enum.Enum): - SEGMENTATION = Segmentor.get_class_display_name() - REGRESSION = Regressor.get_class_display_name() - DETECTION = Detector.get_class_display_name() - SUPERRESOLUTION = Superresolution.get_class_display_name() - RECOGNITION = Recognition.get_class_display_name() - - -@dataclass -class ModelDefinition: - model_type: ModelType - model_class: type - parameters_class: type - map_processor_class: type - - @classmethod - def get_model_definitions(cls): - return [ - cls( - model_type=ModelType.SEGMENTATION, - model_class=DualModel, - parameters_class=SegmentationParameters, - map_processor_class=MapProcessorSegmentation, - ), - cls( - model_type=ModelType.REGRESSION, - model_class=Regressor, - parameters_class=RegressionParameters, - map_processor_class=MapProcessorRegression, - ), - cls( - model_type=ModelType.DETECTION, - model_class=Detector, - parameters_class=DetectionParameters, - map_processor_class=MapProcessorDetection, - ), # superresolution - cls( - model_type=ModelType.SUPERRESOLUTION, - model_class=Superresolution, - parameters_class=SuperresolutionParameters, - map_processor_class=MapProcessorSuperresolution, - ), # recognition - cls( - model_type=ModelType.RECOGNITION, - model_class=Recognition, - parameters_class=RecognitionParameters, - map_processor_class=MapProcessorRecognition, - ) - - ] - - @classmethod - def get_definition_for_type(cls, model_type: ModelType): - model_definitions = cls.get_model_definitions() - for model_definition in model_definitions: - if model_definition.model_type == model_type: - return model_definition - raise Exception(f"Unknown model type: '{model_type}'!") - - @classmethod - def get_definition_for_params(cls, params: MapProcessingParameters): - """ get model definition corresponding to the specified parameters """ - model_definitions = cls.get_model_definitions() - for model_definition in model_definitions: - if type(params) == model_definition.parameters_class: - return model_definition - - for model_definition in model_definitions: - if isinstance(params, model_definition.parameters_class): - return model_definition - raise Exception(f"Unknown model type for parameters: '{params}'!") diff --git a/zipdeepness/deepness/processing/models/preprocessing_utils.py b/zipdeepness/deepness/processing/models/preprocessing_utils.py deleted file mode 100644 index eff25fa163318356e2b1d95f8223c290cd4fd754..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/models/preprocessing_utils.py +++ /dev/null @@ -1,41 +0,0 @@ -import numpy as np - -from deepness.common.processing_parameters.standardization_parameters import StandardizationParameters - - -def limit_channels_number(tiles_batched: np.array, limit: int) -> np.array: - """ Limit the number of channels in the input image to the model - - :param tiles_batched: Batch of tiles - :param limit: Number of channels to keep - :return: Batch of tiles with limited number of channels - """ - return tiles_batched[:, :, :, :limit] - - -def normalize_values_to_01(tiles_batched: np.array) -> np.array: - """ Normalize the values of the input image to the model to the range [0, 1] - - :param tiles_batched: Batch of tiles - :return: Batch of tiles with values in the range [0, 1], in float32 - """ - return np.float32(tiles_batched * 1./255.) - - -def standardize_values(tiles_batched: np.array, params: StandardizationParameters) -> np.array: - """ Standardize the input image to the model - - :param tiles_batched: Batch of tiles - :param params: Parameters for standardization of type STANDARIZE_PARAMS - :return: Batch of tiles with standardized values - """ - return (tiles_batched - params.mean) / params.std - - -def transpose_nhwc_to_nchw(tiles_batched: np.array) -> np.array: - """ Transpose the input image from NHWC to NCHW - - :param tiles_batched: Batch of tiles in NHWC format - :return: Batch of tiles in NCHW format - """ - return np.transpose(tiles_batched, (0, 3, 1, 2)) diff --git a/zipdeepness/deepness/processing/models/recognition.py b/zipdeepness/deepness/processing/models/recognition.py deleted file mode 100644 index 12233e4b006597ec8fcd0775ee1c2f7ff0ff8573..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/models/recognition.py +++ /dev/null @@ -1,98 +0,0 @@ -""" Module including the class for the recognition of the images -""" -import logging -from typing import List - -import numpy as np - -from deepness.common.lazy_package_loader import LazyPackageLoader -from deepness.processing.models.model_base import ModelBase - -cv2 = LazyPackageLoader('cv2') - - -class Recognition(ModelBase): - """Class implements recognition model - - Recognition model is used to predict class confidence per pixel of the image. - """ - - def __init__(self, model_file_path: str): - """ - - Parameters - ---------- - model_file_path : str - Path to the model file - """ - super(Recognition, self).__init__(model_file_path) - - def postprocessing(self, model_output: List) -> np.ndarray: - """Postprocess the model output. - Function returns the array of embeddings - - Parameters - ---------- - model_output : List - Output embeddings from the (Recognition) model - - Returns - ------- - np.ndarray - Same as input - """ - # TODO - compute cosine similarity to self.query_img_emb - # cannot, won't work for query image - - return np.array(model_output) - - def get_number_of_output_channels(self): - """Returns model's number of class - - Returns - ------- - int - Number of channels in the output layer - """ - logging.warning(f"outputs_layers: {self.outputs_layers}") - logging.info(f"outputs_layers: {self.outputs_layers}") - - if len(self.outputs_layers) == 1: - return [self.outputs_layers[0].shape[1]] - else: - raise NotImplementedError( - "Model with multiple output layers is not supported! Use only one output layer." - ) - - @classmethod - def get_class_display_name(cls): - """Returns the name of the class to be displayed in the GUI - - Returns - ------- - str - Name of the class - """ - return cls.__name__ - - def check_loaded_model_outputs(self): - """Checks if the model outputs are valid - - Valid means that: - - the model has only one output - - the output is 2D (N,C) - - the batch size is 1 - - """ - if len(self.outputs_layers) == 1: - shape = self.outputs_layers[0].shape - - if len(shape) != 2: - raise Exception( - f"Recognition model output should have 4 dimensions: (B,C,H,W). Has {shape}" - ) - - else: - raise NotImplementedError( - "Model with multiple output layers is not supported! Use only one output layer." - ) diff --git a/zipdeepness/deepness/processing/models/regressor.py b/zipdeepness/deepness/processing/models/regressor.py deleted file mode 100644 index f995938d981bf2425d9db242ad25fa734e260bf7..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/models/regressor.py +++ /dev/null @@ -1,92 +0,0 @@ -""" Module including Regression model definition -""" -from typing import List - -import numpy as np - -from deepness.processing.models.model_base import ModelBase - - -class Regressor(ModelBase): - """ Class implements regression model. - - Regression model is used to predict metric per pixel of the image. - """ - - def __init__(self, model_file_path: str): - """ - - Parameters - ---------- - model_file_path : str - Path to the model file - """ - super(Regressor, self).__init__(model_file_path) - - def postprocessing(self, model_output: List) -> np.ndarray: - """ Postprocess the model output. - - Parameters - ---------- - model_output : List - Output from the (Regression) model - - Returns - ------- - np.ndarray - Output from the (Regression) model - """ - return model_output - - def get_number_of_output_channels(self) -> List[int]: - """ Returns number of channels in the output layer - - Returns - ------- - int - Number of channels in the output layer - """ - channels = [] - - for layer in self.outputs_layers: - if len(layer.shape) != 4 and len(layer.shape) != 3: - raise Exception(f'Output layer should have 3 or 4 dimensions: (Bs, H, W) or (Bs, Channels, H, W). Actually has: {layer.shape}') - - if len(layer.shape) == 3: - channels.append(1) - elif len(layer.shape) == 4: - channels.append(layer.shape[-3]) - - return channels - - @classmethod - def get_class_display_name(cls) -> str: - """ Returns display name of the model class - - Returns - ------- - str - Display name of the model class - """ - return cls.__name__ - - def check_loaded_model_outputs(self): - """ Check if the model has correct output layers - - Correct means that: - - there is at least one output layer - - batch size is 1 or parameter - - each output layer regresses only one channel - - output resolution is square - """ - for layer in self.outputs_layers: - if len(layer.shape) != 4 and len(layer.shape) != 3: - raise Exception(f'Output layer should have 3 or 4 dimensions: (Bs, H, W) or (Bs, Channels, H, W). Actually has: {layer.shape}') - - if len(layer.shape) == 4: - if layer.shape[2] != layer.shape[3]: - raise Exception(f'Regression model can handle only square outputs masks. Has: {layer.shape}') - - elif len(layer.shape) == 3: - if layer.shape[1] != layer.shape[2]: - raise Exception(f'Regression model can handle only square outputs masks. Has: {layer.shape}') diff --git a/zipdeepness/deepness/processing/models/segmentor.py b/zipdeepness/deepness/processing/models/segmentor.py deleted file mode 100644 index 6dabf26b2cc56d594eb1ed9615d45e6ce4882791..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/models/segmentor.py +++ /dev/null @@ -1,104 +0,0 @@ -""" Module including the class for the segmentation of the images -""" -from typing import List - -import numpy as np - -from deepness.processing.models.model_base import ModelBase - - -class Segmentor(ModelBase): - """Class implements segmentation model - - Segmentation model is used to predict class confidence per pixel of the image. - """ - - def __init__(self, model_file_path: str): - """ - - Parameters - ---------- - model_file_path : str - Path to the model file - """ - super(Segmentor, self).__init__(model_file_path) - - self.outputs_are_sigmoid = self.check_loaded_model_outputs() - - for idx in range(len(self.outputs_layers)): - if self.outputs_names is None: - continue - - if len(self.outputs_names[idx]) == 1 and self.outputs_are_sigmoid[idx]: - self.outputs_names[idx] = ['background', self.outputs_names[idx][0]] - - 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. - - Parameters - ---------- - model_output : List - Output from the (Segmentation) model - - Returns - ------- - np.ndarray - Output from the (Segmentation) model - """ - return model_output - - def get_number_of_output_channels(self) -> List[int]: - """ Returns model's number of class - - Returns - ------- - int - Number of channels in the output layer - """ - output_channels = [] - for layer in self.outputs_layers: - ls = layer.shape - - if len(ls) == 3: - output_channels.append(2) - elif len(ls) == 4: - chn = ls[-3] - if chn == 1: - output_channels.append(2) - else: - output_channels.append(chn) - - return output_channels - - @classmethod - def get_class_display_name(cls): - """ Returns the name of the class to be displayed in the GUI - - Returns - ------- - str - Name of the class - """ - return cls.__name__ - - def check_loaded_model_outputs(self) -> List[bool]: - """ Check if the model outputs are sigmoid (for segmentation) - - Parameters - ---------- - - Returns - ------- - List[bool] - List of booleans indicating if the model outputs are sigmoid - """ - outputs = [] - - for output in self.outputs_layers: - if len(output.shape) == 3: - outputs.append(True) - else: - outputs.append(output.shape[-3] == 1) - - return outputs diff --git a/zipdeepness/deepness/processing/models/superresolution.py b/zipdeepness/deepness/processing/models/superresolution.py deleted file mode 100644 index 1fdaed87e097df953df8b9f5def0c353679b6460..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/models/superresolution.py +++ /dev/null @@ -1,99 +0,0 @@ -""" Module including Super Resolution model definition -""" -from typing import List - -import numpy as np - -from deepness.processing.models.model_base import ModelBase - - -class Superresolution(ModelBase): - """ Class implements super resolution model. - - Super Resolution model is used improve the resolution of an image. - """ - - def __init__(self, model_file_path: str): - """ - - Parameters - ---------- - model_file_path : str - Path to the model file - """ - super(Superresolution, self).__init__(model_file_path) - - def postprocessing(self, model_output: List) -> np.ndarray: - """ Postprocess the model output. - - Parameters - ---------- - model_output : List - Output from the (Regression) model - - Returns - ------- - np.ndarray - Postprocessed mask (H,W,C), 0-1 (one output channel) - - """ - return model_output[0] - - def get_number_of_output_channels(self) -> List[int]: - """ Returns number of channels in the output layer - - Returns - ------- - int - Number of channels in the output layer - """ - if len(self.outputs_layers) == 1: - return [self.outputs_layers[0].shape[-3]] - else: - raise NotImplementedError("Model with multiple output layers is not supported! Use only one output layer.") - - @classmethod - def get_class_display_name(cls) -> str: - """ Returns display name of the model class - - Returns - ------- - str - Display name of the model class - """ - return cls.__name__ - - def get_output_shape(self) -> List[int]: - """ Returns shape of the output layer - - Returns - ------- - List[int] - Shape of the output layer - """ - if len(self.outputs_layers) == 1: - return self.outputs_layers[0].shape - else: - 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 - - Correct means that: - - there is only one output layer - - output layer has 1 channel - - batch size is 1 - - output resolution is square - """ - if len(self.outputs_layers) == 1: - shape = self.outputs_layers[0].shape - - if len(shape) != 4: - raise Exception(f'Regression model output should have 4 dimensions: (Batch_size, Channels, H, W). \n' - f'Actually has: {shape}') - - if shape[2] != shape[3]: - raise Exception(f'Regression model can handle only square outputs masks. Has: {shape}') - - else: - raise NotImplementedError("Model with multiple output layers is not supported! Use only one output layer.") diff --git a/zipdeepness/deepness/processing/processing_utils.py b/zipdeepness/deepness/processing/processing_utils.py deleted file mode 100644 index 25edbad477cbb4182a02c312b0c760ec16e9a3c8..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/processing_utils.py +++ /dev/null @@ -1,557 +0,0 @@ -""" -This file contains utilities related to processing of the ortophoto -""" - -import logging -from dataclasses import dataclass -from typing import List, Optional, Tuple - -import numpy as np -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 -from deepness.common.temp_files_handler import TempFilesHandler - -cv2 = LazyPackageLoader('cv2') - - -def convert_meters_to_rlayer_units(rlayer: QgsRasterLayer, distance_m: float) -> float: - """ How many map units are there in one meter. - :param rlayer: raster layer for which we want to convert meters to its units - :param distance_m: distance in meters - """ - if rlayer.crs().mapUnits() != QgsUnitTypes.DistanceUnit.DistanceMeters: - logging.warning(f"Map units are not meters but '{rlayer.crs().mapUnits()}'. It should be fine for most cases, but be aware.") - - # Now we support all units, but we need to convert them to meters - # to have a consistent unit for the distance - scaling_factor = QgsUnitTypes.fromUnitToUnitFactor(QgsUnitTypes.DistanceMeters, rlayer.crs().mapUnits()) - distance = distance_m * scaling_factor - assert distance != 0 - return distance - - -def get_numpy_data_type_for_qgis_type(data_type_qgis: Qgis.DataType): - """Conver QGIS data type to corresponding numpy data type - In [58]: Qgis.DataType? - implemented: Byte, UInt16, Int16, Float32, Float64 - """ - if data_type_qgis == Qgis.DataType.Byte: - return np.uint8 - if data_type_qgis == Qgis.DataType.UInt16: - return np.uint16 - if data_type_qgis == Qgis.DataType.UInt32: - return np.uint32 - if data_type_qgis == Qgis.DataType.Int16: - return np.int16 - if data_type_qgis == Qgis.DataType.Int32: - return np.int32 - if data_type_qgis == Qgis.DataType.Float32: - return np.float32 - if data_type_qgis == Qgis.DataType.Float64: - return np.float64 - raise Exception(f"Invalid input layer data type ({data_type_qgis})!") - - -def get_tile_image( - rlayer: QgsRasterLayer, - extent: QgsRectangle, - params: MapProcessingParameters) -> np.ndarray: - """_summary_ - - Parameters - ---------- - rlayer : QgsRasterLayer - raster layer from which the image will be extracted - extent : QgsRectangle - extent of the image to extract - params : MapProcessingParameters - map processing parameters - - Returns - ------- - np.ndarray - extracted image [SIZE x SIZE x CHANNELS]. Probably RGBA channels - """ - - 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_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]), \ - round((extent.height()) / expected_units_per_pixel_2d[1]) - - # sanity check, that we gave proper extent as parameter - assert image_size[0] == params.tile_size_px - assert image_size[1] == params.tile_size_px - - # enable resampling - data_provider = rlayer.dataProvider() - if data_provider is None: - 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) - - def get_raster_block(band_number_): - raster_block = rlayer.dataProvider().block( - band_number_, - extent, - image_size[0], image_size[1]) - block_height, block_width = raster_block.height(), raster_block.width() - if block_height == 0 or block_width == 0: - raise Exception("No data on layer within the expected extent!") - return raster_block - - input_channels_mapping = params.input_channels_mapping - number_of_model_inputs = input_channels_mapping.get_number_of_model_inputs() - tile_data = [] - - 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) - band_number = image_channel.get_band_number() - # 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) - data_type = rb.dataType() - data_type_numpy = get_numpy_data_type_for_qgis_type(data_type) - a = np.frombuffer(bytes_array, dtype=data_type_numpy) - b = a.reshape((image_size[1], image_size[0], 1)) - tile_data.append(b) - elif input_channels_mapping.are_all_inputs_composite_byte(): - rb = get_raster_block(1) # the data are always in band 1 - raw_data = rb.data() - bytes_array = bytes(raw_data) - dt = rb.dataType() - number_of_image_channels = input_channels_mapping.get_number_of_image_channels() - # 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) - byte_number = image_channel.get_byte_number() - # 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 - img = np.concatenate(tile_data, axis=2) - return img - - -def erode_dilate_image(img, segmentation_parameters: SegmentationParameters): - """Apply to dilate and erode to the input image""" - if segmentation_parameters.postprocessing_dilate_erode_size: - size = (segmentation_parameters.postprocessing_dilate_erode_size // 2) ** 2 + 1 - kernel = np.ones((size, size), np.uint8) - img = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel) - img = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel) - return img - - -def convert_cv_contours_to_features(features, - cv_contours, - hierarchy, - current_contour_index, - is_hole, - current_holes): - """ - Convert contour found with OpenCV to features accepted by QGis. - Called recursively. - """ - - if current_contour_index == -1: - return - - while True: - contour = cv_contours[current_contour_index] - if len(contour) >= 3: - first_child = hierarchy[current_contour_index][2] - internal_holes = [] - convert_cv_contours_to_features( - features=features, - cv_contours=cv_contours, - hierarchy=hierarchy, - current_contour_index=first_child, - is_hole=not is_hole, - current_holes=internal_holes) - - if is_hole: - current_holes.append(contour) - else: - feature = QgsFeature() - polygon_xy_vec_vec = [ - contour, - *internal_holes - ] - geometry = QgsGeometry.fromPolygonXY(polygon_xy_vec_vec) - feature.setGeometry(geometry) - - # polygon = shapely.geometry.Polygon(contour, holes=internal_holes) - features.append(feature) - - current_contour_index = hierarchy[current_contour_index][0] - if current_contour_index == -1: - break - - -def transform_points_list_xy_to_target_crs( - points: List[Tuple], - extent: QgsRectangle, - rlayer_units_per_pixel: float): - """ Transform points from xy coordinates to the target CRS system coordinates""" - x_left = extent.xMinimum() - y_upper = extent.yMaximum() - points_crs = [] - - for point_xy in points: - x_crs = point_xy[0] * rlayer_units_per_pixel + x_left - y_crs = -(point_xy[1] * rlayer_units_per_pixel - y_upper) - points_crs.append(QgsPointXY(x_crs, y_crs)) - return points_crs - - -def transform_contours_yx_pixels_to_target_crs( - contours, - extent: QgsRectangle, - rlayer_units_per_pixel: float): - """ Transform countours with points as yx pixels to the target CRS system coordinates""" - x_left = extent.xMinimum() - y_upper = extent.yMaximum() - - polygons_crs = [] - for polygon_3d in contours: - # https://stackoverflow.com/questions/33458362/opencv-findcontours-why-do-we-need-a- - # vectorvectorpoint-to-store-the-cont - polygon = polygon_3d.squeeze(axis=1) - - polygon_crs = [] - for i in range(len(polygon)): - yx_px = polygon[i] - x_crs = yx_px[0] * rlayer_units_per_pixel + x_left - y_crs = -(yx_px[1] * rlayer_units_per_pixel - y_upper) - polygon_crs.append(QgsPointXY(x_crs, y_crs)) - polygons_crs.append(polygon_crs) - return polygons_crs - - -@dataclass -class BoundingBox: - """ - Describes a bounding box rectangle. - Similar to cv2.Rect - """ - x_min: int - x_max: int - y_min: int - y_max: int - rot: float = 0.0 - - def get_shape(self) -> Tuple[int, int]: - """ Returns the shape of the bounding box as a tuple (height, width) - - Returns - ------- - tuple - (height, width) - """ - return [ - self.y_max - self.y_min + 1, - self.x_max - self.x_min + 1 - ] - - def get_xyxy(self) -> Tuple[int, int, int, int]: - """ Returns the bounding box as a tuple (x_min, y_min, x_max, y_max) - - Returns - ------- - Tuple[int, int, int, int] - (x_min, y_min, x_max, y_max) - """ - return [ - self.x_min, - self.y_min, - self.x_max, - self.y_max - ] - - def get_xyxy_rot(self) -> Tuple[int, int, int, int, float]: - """ Returns the bounding box as a tuple (x_min, y_min, x_max, y_max, rotation) - - Returns - ------- - Tuple[int, int, int, int, float] - (x_min, y_min, x_max, y_max, rotation) - """ - return [ - self.x_min, - self.y_min, - self.x_max, - self.y_max, - self.rot - ] - - def get_xywh(self) -> Tuple[int, int, int, int]: - """ Returns the bounding box as a tuple (x_min, y_min, width, height) - - Returns - ------- - Tuple[int, int, int, int] - (x_min, y_min, width, height) - """ - return [ - self.x_min, - self.y_min, - self.x_max - self.x_min, - self.y_max - self.y_min - ] - - def get_center(self) -> Tuple[int, int]: - """ Returns the center of the bounding box as a tuple (x, y) - - Returns - ------- - Tuple[int, int] - (x, y) - """ - return [ - (self.x_min + self.x_max) // 2, - (self.y_min + self.y_max) // 2 - ] - - def get_area(self) -> float: - """Calculate bounding box reactangle area - - Returns - ------- - float - Bounding box area - """ - shape = self.get_shape() - return shape[0] * shape[1] - - def calculate_overlap_in_pixels(self, other) -> float: - """Calculate overlap between two bounding boxes in pixels - - Parameters - ---------- - other : BoundingBox - Other bounding box - - Returns - ------- - float - Overlap in pixels - """ - dx = min(self.x_max, other.x_max) - max(self.x_min, other.x_min) - dy = min(self.y_max, other.y_max) - max(self.y_min, other.y_min) - if (dx >= 0) and (dy >= 0): - return dx * dy - return 0 - - def calculate_intersection_over_smaler_area(self, other) -> float: - """ Calculate intersection over smaler area (IoS) between two bounding boxes - - Parameters - ---------- - other : BoundingBox - Other bounding bo - - Returns - ------- - float - Value between 0 and 1 - """ - - Aarea = (self.x_max - self.x_min) * (self.y_max - self.y_min) - Barea = (other.x_max - other.x_min) * (other.y_max - other.y_min) - - xA = max(self.x_min, other.x_min) - yA = max(self.y_min, other.y_min) - xB = min(self.x_max, other.x_max) - yB = min(self.y_max, other.y_max) - - # compute the area of intersection rectangle - return max(0, xB - xA + 1) * max(0, yB - yA + 1) / min(Aarea, Barea) - - def get_slice(self) -> Tuple[slice, slice]: - """ Returns the bounding box as a tuple of slices (y_slice, x_slice) - - Returns - ------- - Tuple[slice, slice] - (y_slice, x_slice) - """ - roi_slice = np.s_[self.y_min:self.y_max + 1, self.x_min:self.x_max + 1] - return roi_slice - - def apply_offset(self, offset_x: int, offset_y: int): - """Apply (x,y) offset to keeping coordinates - - Parameters - ---------- - offset_x : int - x-axis offset in pixels - offset_y : int - y-axis offset in pixels - """ - self.x_min += offset_x - self.y_min += offset_y - self.x_max += offset_x - self.y_max += offset_y - - def get_4_corners(self) -> List[Tuple]: - """Get 4 points (corners) describing the detection rectangle, each point in (x, y) format - - Returns - ------- - List[Tuple] - List of 4 rectangle corners in (x, y) format - """ - if np.isclose(self.rot, 0.0): - return [ - (self.x_min, self.y_min), - (self.x_min, self.y_max), - (self.x_max, self.y_max), - (self.x_max, self.y_min), - ] - else: - x_center = (self.x_min + self.x_max) / 2 - y_center = (self.y_min + self.y_max) / 2 - - corners = np.array([ - [self.x_min, self.y_min], - [self.x_min, self.y_max], - [self.x_max, self.y_max], - [self.x_max, self.y_min], - ]) - - xys = x_center + np.cos(self.rot) * (corners[:, 0] - x_center) - np.sin(self.rot) * (corners[:, 1] - y_center) - yys = y_center + np.sin(self.rot) * (corners[:, 0] - x_center) + np.cos(self.rot) * (corners[:, 1] - y_center) - - return [(int(x), int(y)) for x, y in zip(xys, yys)] - - -def transform_polygon_with_rings_epsg_to_extended_xy_pixels( - polygons: List[List[QgsPointXY]], - extended_extent: QgsRectangle, - img_size_y_pixels: int, - rlayer_units_per_pixel: float) -> List[List[Tuple]]: - """ - Transform coordinates polygons to pixels contours (with cv2 format), in base_extent pixels system - :param polygons: List of tuples with two lists each (x and y points respoectively) - :param extended_extent: - :param img_size_y_pixels: - :param rlayer_units_per_pixel: - :return: 2D contours list - """ - xy_pixel_contours = [] - for polygon in polygons: - xy_pixel_contour = [] - - x_min_epsg = extended_extent.xMinimum() - y_min_epsg = extended_extent.yMinimum() - y_max_pixel = img_size_y_pixels - 1 # -1 to have the max pixel, not shape - 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) - # NOTE: here we can get pixels +-1 values, because we operate on already rounded bounding boxes - xy_pixel_contour.append((x, y)) - - # Values: - # extended_extent.height() / rlayer_units_per_pixel, extended_extent.width() / rlayer_units_per_pixel - # are not integers, because extents are aligned to grid, not pixels resolution - - xy_pixel_contours.append(np.asarray(xy_pixel_contour)) - return xy_pixel_contours - - -def create_area_mask_image(vlayer_mask, - rlayer: QgsRasterLayer, - extended_extent: QgsRectangle, - rlayer_units_per_pixel: float, - 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. - """ - - if vlayer_mask is None: - return None - - 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(): - xform = QgsCoordinateTransform() - xform.setSourceCrs(vlayer_mask.crs()) - xform.setDestinationCrs(rlayer.crs()) - - # see https://docs.qgis.org/3.22/en/docs/pyqgis_developer_cookbook/vector.html#iterating-over-vector-layer - for feature in features: - print("Feature ID: ", feature.id()) - geom = feature.geometry() - - if vlayer_mask.crs() != rlayer.crs(): - geom.transform(xform) - - geom_single_type = QgsWkbTypes.isSingleType(geom.wkbType()) - - if geom.type() == QgsWkbTypes.PointGeometry: - logging.warning("Point geometry not supported!") - elif geom.type() == QgsWkbTypes.LineGeometry: - logging.warning("Line geometry not supported!") - elif geom.type() == QgsWkbTypes.PolygonGeometry: - polygons = [] - if geom_single_type: - polygon = geom.asPolygon() # polygon with rings - polygons.append(polygon) - else: - polygons = geom.asMultiPolygon() - - for polygon_with_rings in polygons: - polygon_with_rings_xy = transform_polygon_with_rings_epsg_to_extended_xy_pixels( - polygons=polygon_with_rings, - extended_extent=extended_extent, - img_size_y_pixels=image_shape_yx[0], - rlayer_units_per_pixel=rlayer_units_per_pixel) - # first polygon is actual polygon - cv2.fillPoly(img, pts=polygon_with_rings_xy[:1], color=255) - if len(polygon_with_rings_xy) > 1: # further polygons are rings - cv2.fillPoly(img, pts=polygon_with_rings_xy[1:], color=0) - else: - print("Unknown or invalid geometry") - - return img diff --git a/zipdeepness/deepness/processing/tile_params.py b/zipdeepness/deepness/processing/tile_params.py deleted file mode 100644 index bea0b9e2337d7beae34a34efff0119bad81b6052..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/processing/tile_params.py +++ /dev/null @@ -1,176 +0,0 @@ -""" -This file contains utilities related to processing a tile. -Tile is a small part of the ortophoto, which is being processed by the model one by one. -""" - -from typing import Optional, Tuple - -import numpy as np -from qgis.core import QgsRectangle - -from deepness.common.lazy_package_loader import LazyPackageLoader -from deepness.common.processing_parameters.map_processing_parameters import MapProcessingParameters - -cv2 = LazyPackageLoader('cv2') - - -class TileParams: - """ Defines a single tile parameters - image that's being processed by model""" - - def __init__(self, - x_bin_number: int, - y_bin_number: int, - x_bins_number: int, - y_bins_number: int, - params: MapProcessingParameters, - rlayer_units_per_pixel, - processing_extent: QgsRectangle): - """ init - - Parameters - ---------- - x_bin_number : int - what is the tile number in a row, counting from left side - y_bin_number : int - what is the tile number in a column - x_bins_number : int - how many tiles are there in a row - y_bins_number : int - how many tiles are there in a column - params : MapProcessingParameters - processing parameters - rlayer_units_per_pixel : _type_ - How many rlayer crs units are in a pixel - processing_extent : QgsRectangle - """ - self.x_bin_number = x_bin_number - self.y_bin_number = y_bin_number - self.x_bins_number = x_bins_number - self.y_bins_number = y_bins_number - self.stride_px = params.processing_stride_px - self.start_pixel_x = x_bin_number * self.stride_px - self.start_pixel_y = y_bin_number * self.stride_px - self.params = params - self.rlayer_units_per_pixel = rlayer_units_per_pixel - - self.extent = self._calculate_extent(processing_extent) # type: QgsRectangle # tile extent in CRS cordinates - - def _calculate_extent(self, processing_extent): - tile_extent = QgsRectangle(processing_extent) # copy - x_min = processing_extent.xMinimum() + self.start_pixel_x * self.rlayer_units_per_pixel - y_max = processing_extent.yMaximum() - self.start_pixel_y * self.rlayer_units_per_pixel - tile_extent.setXMinimum(x_min) - # extent needs to be on the further edge (so including the corner pixel, hence we do not subtract 1) - tile_extent.setXMaximum(x_min + self.params.tile_size_px * self.rlayer_units_per_pixel) - tile_extent.setYMaximum(y_max) - y_min = y_max - self.params.tile_size_px * self.rlayer_units_per_pixel - tile_extent.setYMinimum(y_min) - return tile_extent - - def get_slice_on_full_image_for_entire_tile(self) -> Tuple[slice, slice]: - """ Obtain slice to get the entire tile from full final image, - including the overlapping parts. - - Returns - ------- - Tuple[slice, slice] - Slice to be used on the full image - """ - - # 'core' part of the tile (not overlapping with other tiles), for sure copied for each tile - x_min = self.start_pixel_x - x_max = self.start_pixel_x + self.params.tile_size_px - 1 - y_min = self.start_pixel_y - y_max = self.start_pixel_y + self.params.tile_size_px - 1 - - 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, 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 = 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 - - # edge tiles handling - if self.x_bin_number == 0: - x_min -= half_overlap - if self.y_bin_number == 0: - y_min -= half_overlap - if self.x_bin_number == self.x_bins_number-1: - x_max += half_overlap - 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, 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(tile_offset=tile_offset) - - r = roi_slice_on_full_image - roi_slice_on_tile = np.s_[ - :, - r[1].start - self.start_pixel_y - tile_offset:r[1].stop - self.start_pixel_y - tile_offset, - r[2].start - self.start_pixel_x - tile_offset:r[2].stop - self.start_pixel_x - tile_offset - ] - return roi_slice_on_tile - - def is_tile_within_mask(self, mask_img: Optional[np.ndarray]): - """ - To check if tile is within the mask image - """ - if mask_img is None: - return True # if we don't have a mask, we are going to process all tiles - - roi_slice = self.get_slice_on_full_image_for_copying() - mask_roi = mask_img[roi_slice[0]] - # check corners first - if mask_roi[0, 0] and mask_roi[1, -1] and mask_roi[-1, 0] and mask_roi[-1, -1]: - return True # all corners in mask, almost for sure a good tile - - coverage_percentage = cv2.countNonZero(mask_roi) / (mask_roi.shape[0] * mask_roi.shape[1]) * 100 - 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): - if tile_result.shape[1] != self.params.tile_size_px or tile_result.shape[2] != self.params.tile_size_px: - tile_offset = (self.params.tile_size_px - tile_result.shape[1])//2 - - 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 - - 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: - roi_slice_on_full_image = self.get_slice_on_full_image_for_entire_tile() - img = full_result_img[roi_slice_on_full_image] - return img diff --git a/zipdeepness/deepness/python_requirements/requirements.txt b/zipdeepness/deepness/python_requirements/requirements.txt deleted file mode 100644 index 3156cf5aa5dc05772f4868c1008f25b5fea709c1..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/python_requirements/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -## CORE ## -#numpy # available already from qgis repo -#pyqt5 # available already from qgis repo - - -# NOTE - for the time being - keep the same packages and versions in the `packages_installer_dialog.py` -numpy<2.0.0 -onnxruntime-gpu>=1.12.1,<=1.17.0 -opencv-python-headless>=4.5.5.64,<=4.9.0.80 -rasterio \ No newline at end of file diff --git a/zipdeepness/deepness/python_requirements/requirements_development.txt b/zipdeepness/deepness/python_requirements/requirements_development.txt deleted file mode 100644 index cb7da2e1bef6d85feffd344a2d1555bda3d6a4cc..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/python_requirements/requirements_development.txt +++ /dev/null @@ -1,21 +0,0 @@ -# Install main requirements --r requirements.txt - -# Requirements required only to test and debug code while developing -pytest==7.4.0 -pytest-cov==4.1.0 -flake8==6.0.0 -onnx==1.14.0 - -# docs -sphinx==6.1.3 -sphinxcontrib-youtube==1.2.0 -sphinx-autodoc-typehints==1.22 -sphinx-autopackagesummary==1.3 -sphinx-rtd-theme==0.5.1 -m2r2==0.3.3 -sphinx-rtd-size==0.2.0 - -# vscode -pylint==2.17.4 -autopep8==2.0.2 diff --git a/zipdeepness/deepness/resources.py b/zipdeepness/deepness/resources.py deleted file mode 100644 index f434ed642379ecd17d8b9a73cc1eeeebcf82bcec..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/resources.py +++ /dev/null @@ -1,107 +0,0 @@ -# -*- coding: utf-8 -*- - -# Resource object code -# -# Created by: The Resource Compiler for PyQt5 (Qt v5.15.3) -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore - -qt_resource_data = b"\ -\x00\x00\x02\x32\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x17\x00\x00\x00\x18\x08\x06\x00\x00\x00\x11\x7c\x66\x75\ -\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ -\x00\x00\x01\xe9\x49\x44\x41\x54\x48\x89\xa5\x95\xdf\x6a\x13\x51\ -\x10\xc6\x7f\x67\xb2\xa6\xa4\xa2\x5e\x14\x1a\x08\x8a\x4d\xad\x8b\ -\x15\xd4\x2b\x7d\x80\x7a\x21\x78\xe9\x75\x9f\xc1\x27\x28\xb4\x0f\ -\xe1\x23\xf4\xce\x27\x10\xb4\x0f\xa0\x05\x2f\x0a\x0d\x04\xd3\x82\ -\xa4\x42\x85\x52\x2c\x92\x54\x76\xcf\x1c\x2f\x36\xc5\xa4\xe7\x4f\ -\x9b\xf8\x41\x60\x99\x73\xe6\x9b\xef\x9b\x9d\xd9\x18\x2e\x43\x97\ -\x14\x63\x8c\x17\xbf\x80\xc3\x21\x87\x12\x3d\x1f\x83\x7f\xe9\xd4\ -\x9c\x26\x33\x0c\x06\xdb\xb6\xb3\x91\x2f\x1c\x2e\xa0\xe8\x15\x59\ -\x82\xb6\xb5\xfa\x2d\x29\xb6\x6d\x59\xbf\xb5\xee\xeb\xc8\x75\x44\ -\xa4\x4a\xd1\xef\x73\xb2\xb1\xc1\xd9\xf6\x36\xb6\x6d\x91\x40\xf1\ -\x14\x8e\xf4\x88\x96\xb4\xc6\xc8\x9d\xf3\x6f\x95\x25\xfb\xab\x42\ -\xcd\x4e\x47\x7e\x09\x91\xe4\x2c\xe3\xe3\xab\xff\x22\x4e\x90\x03\ -\x6f\xdf\xc1\x87\xd7\xe0\x4c\xc0\xd9\xf5\x60\x4c\xae\x0e\xc0\x11\ -\x9f\x3e\x3a\xb9\x22\xe5\x74\x4e\x1c\x4e\xb4\x2b\x68\x57\x58\x1b\ -\xee\x20\xb1\x21\x59\xed\x0a\xc5\x5c\x7a\x82\xc6\x48\xf9\xde\x1c\ -\xf2\xa8\x87\x71\x30\x61\xfb\x4e\xfe\x8b\x33\x6e\x5f\xdf\x81\x1b\ -\xcb\xff\x53\xb7\x3c\xdb\x17\x10\x01\x7c\x72\x80\x07\xcb\x3d\x0e\ -\xb2\xe5\x78\x01\x53\x56\x3d\x2c\x32\xe5\xc9\x5e\x09\xf5\x7a\x75\ -\x38\xb9\xd9\x41\x72\x80\x17\xf7\x3f\xf3\x65\xee\x79\xb8\x00\x45\ -\x01\x65\x09\x8d\x46\xe4\x42\x9a\x1c\xe0\xe5\xbd\x4f\xec\x34\xd6\ -\x52\xf9\x49\x18\xa5\x7a\x8b\x86\xf0\xb8\xa4\x1d\x5c\x41\x7e\xf1\ -\x30\x80\x41\x03\x82\x36\x9b\x2b\xc7\xfc\x94\xc5\xd9\xc9\x01\x14\ -\x34\xe6\x60\x3e\x1f\x30\x0c\xd7\x8e\x62\x62\xac\x36\x61\x33\x76\ -\x71\xd0\x9d\x8f\xef\x41\x04\x9e\xca\x94\x7a\x00\xc9\x35\xbd\xcd\ -\x7b\x8f\xe1\xc6\x39\x60\xa6\xfc\xa4\x02\x2d\xfb\x23\x7e\xd8\xc9\ -\xa1\x7e\x5e\x49\x33\xce\x27\xdf\x85\xdd\x14\x79\xbf\x77\x17\xe3\ -\x4d\xaf\x73\xbc\x7f\x03\x52\x4e\x44\x83\xfe\x66\x6a\x4d\xe7\xa1\ -\x43\xec\x44\x30\xd8\x96\x98\x7a\x05\x1d\x2d\x9d\xbf\x78\xc6\x7a\ -\x62\xa2\xea\x2c\x58\x09\x14\xb7\x60\xb3\x95\xe3\x13\x64\xf1\xdf\ -\xe0\x77\x72\xaf\x25\x51\xe5\x00\x5b\xb0\x15\x8a\xd7\xa0\xf6\xfb\ -\x5b\xf3\x26\x8c\xfe\x7b\xbf\x3e\x0d\x12\x03\xfc\x05\xb8\x94\xac\ -\x7e\xe6\x88\x91\xed\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\ -\x82\ -" - -qt_resource_name = b"\ -\x00\x07\ -\x07\x3b\xe0\xb3\ -\x00\x70\ -\x00\x6c\x00\x75\x00\x67\x00\x69\x00\x6e\x00\x73\ -\x00\x1b\ -\x02\xd8\xc9\xeb\ -\x00\x64\ -\x00\x65\x00\x65\x00\x70\x00\x5f\x00\x73\x00\x65\x00\x67\x00\x6d\x00\x65\x00\x6e\x00\x74\x00\x61\x00\x74\x00\x69\x00\x6f\x00\x6e\ -\x00\x5f\x00\x66\x00\x72\x00\x61\x00\x6d\x00\x65\x00\x77\x00\x6f\x00\x72\x00\x6b\ -\x00\x06\ -\x07\x03\x7d\xc3\ -\x00\x69\ -\x00\x6d\x00\x61\x00\x67\x00\x65\x00\x73\ -\x00\x08\ -\x0a\x61\x5a\xa7\ -\x00\x69\ -\x00\x63\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ -" - -qt_resource_struct_v1 = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ -\x00\x00\x00\x14\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\ -\x00\x00\x00\x50\x00\x02\x00\x00\x00\x01\x00\x00\x00\x04\ -\x00\x00\x00\x62\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -" - -qt_resource_struct_v2 = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x14\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x50\x00\x02\x00\x00\x00\x01\x00\x00\x00\x04\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x62\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x82\xb5\xcc\x01\x44\ -" - -qt_version = [int(v) for v in QtCore.qVersion().split('.')] -if qt_version < [5, 8, 0]: - rcc_version = 1 - qt_resource_struct = qt_resource_struct_v1 -else: - rcc_version = 2 - qt_resource_struct = qt_resource_struct_v2 - -def qInitResources(): - QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) - -def qCleanupResources(): - QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) - -qInitResources() diff --git a/zipdeepness/deepness/resources.qrc b/zipdeepness/deepness/resources.qrc deleted file mode 100644 index 5385347c5a77e41690a1a5361565f436d3be57bc..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/resources.qrc +++ /dev/null @@ -1,5 +0,0 @@ - - - images/icon.png - - diff --git a/zipdeepness/deepness/widgets/__init__.py b/zipdeepness/deepness/widgets/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/zipdeepness/deepness/widgets/input_channels_mapping/input_channels_mapping_widget.py b/zipdeepness/deepness/widgets/input_channels_mapping/input_channels_mapping_widget.py deleted file mode 100644 index e9633d7cbbd3d4628b09c6036957b0090daffcb9..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/widgets/input_channels_mapping/input_channels_mapping_widget.py +++ /dev/null @@ -1,177 +0,0 @@ -""" -This file contains a single widget, which is embedded in the main dockwiget - to select channels mapping -""" - - -import os -from typing import Optional, List - -from qgis.PyQt import QtWidgets, uic -from qgis.PyQt.QtWidgets import QComboBox -from qgis.PyQt.QtWidgets import QLabel -from qgis.core import Qgis, QgsRasterLayer - -from deepness.common.channels_mapping import ChannelsMapping, ImageChannelStandaloneBand, \ - ImageChannelCompositeByte -from deepness.common.channels_mapping import ImageChannel -from deepness.common.config_entry_key import ConfigEntryKey -from deepness.processing.models.model_base import ModelBase - -FORM_CLASS, _ = uic.loadUiType(os.path.join( - os.path.dirname(__file__), 'input_channels_mapping_widget.ui')) - - -class InputChannelsMappingWidget(QtWidgets.QWidget, FORM_CLASS): - """ - Widget responsible for mapping image channels to model input channels. - Allows to define the channels mapping (see `deepness.common.channels_mapping` for details). - - UI design defined in `input_channels_mapping_widget.ui` file. - """ - - def __init__(self, rlayer, parent=None): - super(InputChannelsMappingWidget, self).__init__(parent) - self.setupUi(self) - self._model_wrapper: Optional[ModelBase] = None - self._rlayer: QgsRasterLayer = rlayer - self._create_connections() - self._channels_mapping = ChannelsMapping() - - self._channels_mapping_labels: List[QLabel] = [] - self._channels_mapping_comboboxes: List[QComboBox] = [] - - self._selection_mode_changed() - - def load_ui_from_config(self): - is_advanced_mode = ConfigEntryKey.INPUT_CHANNELS_MAPPING__ADVANCED_MODE.get() - self.radioButton_advancedMapping.setChecked(is_advanced_mode) - - if is_advanced_mode: - mapping_list_str = ConfigEntryKey.INPUT_CHANNELS_MAPPING__MAPPING_LIST_STR.get() - mapping_list = [int(v) for v in mapping_list_str] - self._channels_mapping.load_mapping_from_list(mapping_list) - - def save_ui_to_config(self): - is_advanced_mode = self.radioButton_advancedMapping.isChecked() - ConfigEntryKey.INPUT_CHANNELS_MAPPING__ADVANCED_MODE.set(is_advanced_mode) - - if is_advanced_mode: - mapping_list = self._channels_mapping.get_mapping_as_list() - mapping_list_str = [str(v) for v in mapping_list] - ConfigEntryKey.INPUT_CHANNELS_MAPPING__MAPPING_LIST_STR.set(mapping_list_str) - - def get_channels_mapping(self) -> ChannelsMapping: - """ Get the channels mapping currently selected in the UI """ - if self.radioButton_defaultMapping.isChecked(): - return self._channels_mapping.get_as_default_mapping() - else: # advanced mapping - return self._channels_mapping - - def get_channels_mapping_for_training_data_export(self) -> ChannelsMapping: - """ Get the channels mapping to be used for the `training data export tool`. - It is not channels mapping exactly, because we do not have the model, but we can use it - """ - mapping = self._channels_mapping.get_as_default_mapping() - mapping.set_number_of_model_inputs_same_as_image_channels() - return mapping - - def _create_connections(self): - self.radioButton_defaultMapping.clicked.connect(self._selection_mode_changed) - self.radioButton_advancedMapping.clicked.connect(self._selection_mode_changed) - - def _selection_mode_changed(self): - is_advanced = self.radioButton_advancedMapping.isChecked() - self.widget_mapping.setVisible(is_advanced) - - def set_model(self, model_wrapper: ModelBase): - """ Set the model for which we are creating the mapping here """ - self._model_wrapper = model_wrapper - number_of_channels = self._model_wrapper.get_number_of_channels() - self.label_modelInputs.setText(f'{number_of_channels}') - self._channels_mapping.set_number_of_model_inputs(number_of_channels) - self.regenerate_mapping() - - def set_rlayer(self, rlayer: QgsRasterLayer): - """ Set the raster layer (ortophoto file) which is selected (for which we create the mapping here)""" - self._rlayer = rlayer - - if rlayer: - number_of_image_bands = rlayer.bandCount() - else: - number_of_image_bands = 0 - - image_channels = [] # type: List[ImageChannel] - - if number_of_image_bands == 1: - # if there is one band, then there is probably more "bands" hidden in a more complex data type (e.g. RGBA) - data_type = rlayer.dataProvider().dataType(1) - if data_type in [Qgis.DataType.Byte, Qgis.DataType.UInt16, Qgis.DataType.Int16, - Qgis.DataType.Float32]: - image_channel = ImageChannelStandaloneBand( - band_number=1, - name=rlayer.bandName(1)) - image_channels.append(image_channel) - elif data_type == Qgis.DataType.ARGB32: - # Alpha channel is at byte number 3, red is byte 2, ... - reversed order - band_names = [ - 'Alpha (band 4)', - 'Red (band 1)', - 'Green (band 2)', - 'Blue (band 3)', - ] - for i in [1, 2, 3, 0]: # We want order of model inputs as 'RGB' first and then 'A' - image_channel = ImageChannelCompositeByte( - byte_number=3 - i, # bytes are in reversed order - name=band_names[i]) - image_channels.append(image_channel) - else: - raise Exception("Invalid input layer data type!") - else: - for band_number in range(1, number_of_image_bands + 1): # counted from 1 - image_channel = ImageChannelStandaloneBand( - band_number=band_number, - name=rlayer.bandName(band_number)) - image_channels.append(image_channel) - - self.label_imageInputs.setText(f'{len(image_channels)}') - self._channels_mapping.set_image_channels(image_channels) - self.regenerate_mapping() - - def _combobox_index_changed(self, model_input_channel_number): - combobox = self._channels_mapping_comboboxes[model_input_channel_number] # type: QComboBox - image_channel_index = combobox.currentIndex() - # print(f'Combobox {model_input_channel_number} changed to {current_index}') - self._channels_mapping.set_image_channel_for_model_input( - model_input_number=model_input_channel_number, - image_channel_index=image_channel_index) - - def regenerate_mapping(self): - """ Regenerate the mapping after the model or ortophoto was changed or mapping was read from config""" - for combobox in self._channels_mapping_comboboxes: - self.gridLayout_mapping.removeWidget(combobox) - self._channels_mapping_comboboxes.clear() - for label in self._channels_mapping_labels: - self.gridLayout_mapping.removeWidget(label) - self._channels_mapping_labels.clear() - - for model_input_channel_number in range(self._channels_mapping.get_number_of_model_inputs()): - label = QLabel(self) - label.setText(f"Model input {model_input_channel_number}:") - combobox = QComboBox(self) - for image_channel in self._channels_mapping.get_image_channels(): - combobox.addItem(image_channel.name) - - if self._channels_mapping.get_number_of_image_channels() > 0: - # image channel witch is currently assigned to the current model channel - image_channel_index = self._channels_mapping.get_image_channel_index_for_model_input( - model_input_channel_number) - combobox.setCurrentIndex(image_channel_index) - - combobox.currentIndexChanged.connect( - lambda _, v=model_input_channel_number: self._combobox_index_changed(v)) - - self.gridLayout_mapping.addWidget(label, model_input_channel_number, 0) - self.gridLayout_mapping.addWidget(combobox, model_input_channel_number, 1, 1, 2) - - self._channels_mapping_comboboxes.append(combobox) - self._channels_mapping_labels.append(label) diff --git a/zipdeepness/deepness/widgets/input_channels_mapping/input_channels_mapping_widget.ui b/zipdeepness/deepness/widgets/input_channels_mapping/input_channels_mapping_widget.ui deleted file mode 100644 index c22efbe2bd93c5f5d3ae95ed9c7ade144e97f330..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/widgets/input_channels_mapping/input_channels_mapping_widget.ui +++ /dev/null @@ -1,100 +0,0 @@ - - - Form - - - - 0 - 0 - 399 - 378 - - - - Form - - - - - - - - Model inputs (channels): - - - - - - - Image inputs (bands): - - - - - - - ... - - - - - - - ... - - - - - - - - - Qt::Horizontal - - - - - - - Default (image channels passed -in sequence as input channels) - - - true - - - - - - - Advanced (manually select which input image -channel is assigned to each model input) - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - - - diff --git a/zipdeepness/deepness/widgets/training_data_export_widget/training_data_export_widget.py b/zipdeepness/deepness/widgets/training_data_export_widget/training_data_export_widget.py deleted file mode 100644 index 0854aa2a55453b25ce2c7a4fb48d0fc7a79cc7ea..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/widgets/training_data_export_widget/training_data_export_widget.py +++ /dev/null @@ -1,96 +0,0 @@ -""" -This file contains a single widget, which is embedded in the main dockwiget - to select the training data export parameters -""" - -import os - -from qgis.PyQt import QtWidgets, uic -from qgis.PyQt.QtWidgets import QFileDialog -from qgis.core import QgsMapLayerProxyModel -from qgis.core import QgsProject - -from deepness.common.config_entry_key import ConfigEntryKey -from deepness.common.processing_parameters.map_processing_parameters import MapProcessingParameters -from deepness.common.processing_parameters.training_data_export_parameters import \ - TrainingDataExportParameters - -FORM_CLASS, _ = uic.loadUiType(os.path.join( - os.path.dirname(__file__), 'training_data_export_widget.ui')) - - -class TrainingDataExportWidget(QtWidgets.QWidget, FORM_CLASS): - """ - Widget responsible for defining the parameters for the Trainign Data Export process (not doing the actual export). - - UI design defined in the `training_data_export_widget.ui` file. - """ - - def __init__(self, rlayer, parent=None): - super(TrainingDataExportWidget, self).__init__(parent) - self.setupUi(self) - self._create_connections() - self._setup_misc_ui() - - def load_ui_from_config(self): - layers = QgsProject.instance().mapLayers() - - self.lineEdit_outputDirPath.setText(ConfigEntryKey.DATA_EXPORT_DIR.get()) - self.checkBox_exportImageTiles.setChecked( - ConfigEntryKey.DATA_EXPORT_TILES_ENABLED.get()) - self.checkBox_exportMaskEnabled.setChecked( - ConfigEntryKey.DATA_EXPORT_SEGMENTATION_MASK_ENABLED.get()) - - segmentation_layer_id = ConfigEntryKey.DATA_EXPORT_SEGMENTATION_MASK_ID.get() - if segmentation_layer_id and segmentation_layer_id in layers: - self.mMapLayerComboBox_inputLayer.setLayer(layers[segmentation_layer_id]) - - def save_ui_to_config(self): - ConfigEntryKey.DATA_EXPORT_DIR.set(self.lineEdit_outputDirPath.text()) - ConfigEntryKey.DATA_EXPORT_TILES_ENABLED.set( - self.checkBox_exportImageTiles.isChecked()) - ConfigEntryKey.DATA_EXPORT_SEGMENTATION_MASK_ENABLED.set( - self.checkBox_exportMaskEnabled.isChecked()) - ConfigEntryKey.DATA_EXPORT_SEGMENTATION_MASK_ID.set(self.get_segmentation_mask_layer_id()) - - def _browse_output_directory(self): - current_directory = self.lineEdit_outputDirPath.text() - if not current_directory: - current_directory = os.path.expanduser('~') - new_directory = QFileDialog.getExistingDirectory( - self, - "Select Directory", - current_directory, - QFileDialog.ShowDirsOnly) - self.lineEdit_outputDirPath.setText(new_directory) - - def _enable_disable_mask_layer_selection(self): - is_enabled = self.checkBox_exportMaskEnabled.isChecked() - self.mMapLayerComboBox_maskLayer.setEnabled(is_enabled) - - def _create_connections(self): - self.checkBox_exportMaskEnabled.toggled.connect(self._enable_disable_mask_layer_selection) - self.pushButton_browseOutputDirectory.clicked.connect(self._browse_output_directory) - - def _setup_misc_ui(self): - self.mMapLayerComboBox_maskLayer.setFilters(QgsMapLayerProxyModel.VectorLayer) - self._enable_disable_mask_layer_selection() - - def get_segmentation_mask_layer_id(self): - if not self.checkBox_exportMaskEnabled.isChecked(): - return None - return self.mMapLayerComboBox_maskLayer.currentLayer().id() - - def get_training_data_export_parameters(self, map_processing_parameters: MapProcessingParameters): - """ Get the parameters from the UI for the data exporting process""" - if self.checkBox_exportMaskEnabled.isChecked(): - segmentation_mask_layer_id = self.mMapLayerComboBox_maskLayer.currentLayer().id() - else: - segmentation_mask_layer_id = None - - params = TrainingDataExportParameters( - **map_processing_parameters.__dict__, - export_image_tiles=self.checkBox_exportImageTiles.isChecked(), - segmentation_mask_layer_id=segmentation_mask_layer_id, - output_directory_path=self.lineEdit_outputDirPath.text(), - ) - return params diff --git a/zipdeepness/deepness/widgets/training_data_export_widget/training_data_export_widget.ui b/zipdeepness/deepness/widgets/training_data_export_widget/training_data_export_widget.ui deleted file mode 100644 index 9540d968c4003af591b9701aac707dba90ff2069..0000000000000000000000000000000000000000 --- a/zipdeepness/deepness/widgets/training_data_export_widget/training_data_export_widget.ui +++ /dev/null @@ -1,133 +0,0 @@ - - - Form - - - - 0 - 0 - 461 - 378 - - - - Form - - - - - - - - - - - Resolution [cm/px]: - - - - - - - Path to directory where the generated data will be saved. - - - - - - - Whether tiles from the "Input layer" (the main ortophoto) should be created. - - - Export image tiles: - - - true - - - - - - - Browse... - - - - - - - Input layer selected in -"Input Layer" section - - - - - - - Tile size [px]: - - - - - - - Tiles overlap [%]: - - - - - - - Output dir path: - - - - - - - <html><head/><body><p>Whether tiles from the segmentation mask should be created.</p><p>Can be used to create training images for the model, if you already have manually annotated the image.</p></body></html> - - - Export segmentation -mask for layer: - - - - - - - Selected in section -"Processing parameters" - - - - - - - Selected in section -"Processing parameters" - - - - - - - Selected in section -"Processing parameters" - - - - - - - - - - QgsMapLayerComboBox - QComboBox -
qgsmaplayercombobox.h
-
-
- - -