From 0d1fef4e0b985c5e39b5f356f463f7b0cfc359ee Mon Sep 17 00:00:00 2001 From: Alexander Dokuchaev Date: Mon, 4 Apr 2022 17:48:27 +0300 Subject: [PATCH 01/11] Add nncf configs and update mmseg --- .../compression_config.json | 62 +++++++++++++++++++ .../ocr-lite-hrnet-18-mod2/template.yaml | 2 +- .../ocr-lite-hrnet-18/compression_config.json | 2 +- .../compression_config.json | 62 +++++++++++++++++++ .../ocr-lite-hrnet-s-mod2/template.yaml | 2 +- .../compression_config.json | 62 +++++++++++++++++++ .../ocr-lite-hrnet-x-mod3/template.yaml | 2 +- external/mmsegmentation/submodule | 2 +- 8 files changed, 191 insertions(+), 5 deletions(-) create mode 100644 external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-18-mod2/compression_config.json create mode 100644 external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-s-mod2/compression_config.json create mode 100644 external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-x-mod3/compression_config.json diff --git a/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-18-mod2/compression_config.json b/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-18-mod2/compression_config.json new file mode 100644 index 00000000000..d95f41d6dbb --- /dev/null +++ b/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-18-mod2/compression_config.json @@ -0,0 +1,62 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mDice", + "input_info": { + "sample_size": [1, 3, 512, 512] + }, + "compression": [], + "log_dir": "." + }, + "params_config": { + "iters": 0, + "open_layers": [] + }, + "checkpoint_config": { + "interval": -1 + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 1e-4 + }, + "lr_config": { + "fixed": null, + "fixed_iters": 0, + "warmup": null, + "warmup_iters": 0, + "step": [20] + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 500 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 500 + } + }, + "ignored_scopes": [ + "{re}.*cross_resolution_weighting.*__mul__.*", + "{re}.*spatial_weighting.*__mul__.*" + ] + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 1.0, + "maximal_total_epochs": 40 + } + } + } + }, + "order_of_parts": [ + "nncf_quantization" + ] +} diff --git a/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-18-mod2/template.yaml b/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-18-mod2/template.yaml index a7a70d4df8f..5ee464a276f 100644 --- a/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-18-mod2/template.yaml +++ b/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-18-mod2/template.yaml @@ -14,7 +14,7 @@ framework: OTESegmentation v0.14.0 entrypoints: base: segmentation_tasks.apis.segmentation.OTESegmentationTrainingTask openvino: segmentation_tasks.apis.segmentation.OpenVINOSegmentationTask - # nncf: segmentation_tasks.apis.segmentation.OTESegmentationNNCFTask + nncf: segmentation_tasks.apis.segmentation.OTESegmentationNNCFTask # Capabilities. capabilities: diff --git a/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-18/compression_config.json b/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-18/compression_config.json index b0ab985f8de..d95f41d6dbb 100644 --- a/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-18/compression_config.json +++ b/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-18/compression_config.json @@ -56,7 +56,7 @@ } } }, - "order_of_parts": [ + "order_of_parts": [ "nncf_quantization" ] } diff --git a/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-s-mod2/compression_config.json b/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-s-mod2/compression_config.json new file mode 100644 index 00000000000..a1cb3639fa0 --- /dev/null +++ b/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-s-mod2/compression_config.json @@ -0,0 +1,62 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mDice", + "input_info": { + "sample_size": [1, 3, 512, 512] + }, + "compression": [], + "log_dir": "." + }, + "params_config": { + "iters": 0, + "open_layers": [] + }, + "checkpoint_config": { + "interval": -1 + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 1e-4 + }, + "lr_config": { + "fixed": null, + "fixed_iters": 0, + "warmup": null, + "warmup_iters": 0, + "step": [20] + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 5 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 5 + } + }, + "ignored_scopes": [ + "{re}.*cross_resolution_weighting.*__mul__.*", + "{re}.*spatial_weighting.*__mul__.*" + ] + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 1.0, + "maximal_total_epochs": 40 + } + } + } + }, + "order_of_parts": [ + "nncf_quantization" + ] +} diff --git a/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-s-mod2/template.yaml b/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-s-mod2/template.yaml index 9a3c18eb36a..d397be7841c 100644 --- a/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-s-mod2/template.yaml +++ b/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-s-mod2/template.yaml @@ -14,7 +14,7 @@ framework: OTESegmentation v0.14.0 entrypoints: base: segmentation_tasks.apis.segmentation.OTESegmentationTrainingTask openvino: segmentation_tasks.apis.segmentation.OpenVINOSegmentationTask - # nncf: segmentation_tasks.apis.segmentation.OTESegmentationNNCFTask + nncf: segmentation_tasks.apis.segmentation.OTESegmentationNNCFTask # Capabilities. capabilities: diff --git a/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-x-mod3/compression_config.json b/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-x-mod3/compression_config.json new file mode 100644 index 00000000000..d95f41d6dbb --- /dev/null +++ b/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-x-mod3/compression_config.json @@ -0,0 +1,62 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mDice", + "input_info": { + "sample_size": [1, 3, 512, 512] + }, + "compression": [], + "log_dir": "." + }, + "params_config": { + "iters": 0, + "open_layers": [] + }, + "checkpoint_config": { + "interval": -1 + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 1e-4 + }, + "lr_config": { + "fixed": null, + "fixed_iters": 0, + "warmup": null, + "warmup_iters": 0, + "step": [20] + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 500 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 500 + } + }, + "ignored_scopes": [ + "{re}.*cross_resolution_weighting.*__mul__.*", + "{re}.*spatial_weighting.*__mul__.*" + ] + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 1.0, + "maximal_total_epochs": 40 + } + } + } + }, + "order_of_parts": [ + "nncf_quantization" + ] +} diff --git a/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-x-mod3/template.yaml b/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-x-mod3/template.yaml index 11080e7b407..4a7106689e3 100644 --- a/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-x-mod3/template.yaml +++ b/external/mmsegmentation/configs/custom-sematic-segmentation/ocr-lite-hrnet-x-mod3/template.yaml @@ -14,7 +14,7 @@ framework: OTESegmentation v0.14.0 entrypoints: base: segmentation_tasks.apis.segmentation.OTESegmentationTrainingTask openvino: segmentation_tasks.apis.segmentation.OpenVINOSegmentationTask - # nncf: segmentation_tasks.apis.segmentation.OTESegmentationNNCFTask + nncf: segmentation_tasks.apis.segmentation.OTESegmentationNNCFTask # Capabilities. capabilities: diff --git a/external/mmsegmentation/submodule b/external/mmsegmentation/submodule index 571a0d38069..bf341d9e125 160000 --- a/external/mmsegmentation/submodule +++ b/external/mmsegmentation/submodule @@ -1 +1 @@ -Subproject commit 571a0d38069588787916c682d94888614352972b +Subproject commit bf341d9e125759a5ddab4ddfc4ec820c58cc9851 From 56f1b09f61a3fc94dc6fa7835d71793b270904a8 Mon Sep 17 00:00:00 2001 From: Alexander Dokuchaev Date: Mon, 4 Apr 2022 17:51:39 +0300 Subject: [PATCH 02/11] Update mmdet --- external/mmdetection/submodule | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/mmdetection/submodule b/external/mmdetection/submodule index b7afe852faf..4474a63b8b8 160000 --- a/external/mmdetection/submodule +++ b/external/mmdetection/submodule @@ -1 +1 @@ -Subproject commit b7afe852fafeab36c9fd9f126e8d3f48d44675ba +Subproject commit 4474a63b8b8f57937a93c572b5219cb2631585a4 From 92268affe540732397b78d63d82d73457cb8853a Mon Sep 17 00:00:00 2001 From: Eugene Liu Date: Mon, 25 Apr 2022 11:34:49 +0100 Subject: [PATCH 03/11] update mmdetection branch --- external/mmdetection/submodule | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/mmdetection/submodule b/external/mmdetection/submodule index 4474a63b8b8..ca19197237a 160000 --- a/external/mmdetection/submodule +++ b/external/mmdetection/submodule @@ -1 +1 @@ -Subproject commit 4474a63b8b8f57937a93c572b5219cb2631585a4 +Subproject commit ca19197237ab5ecb7e7a27514bd4f464f6d379a6 From 981dea269d60f61afdbd094716b449623c607fd0 Mon Sep 17 00:00:00 2001 From: AlbertvanHouten Date: Wed, 11 May 2022 13:34:46 +0200 Subject: [PATCH 04/11] Removed labels from shape and id from annotation --- ote_sdk/ote_sdk/entities/annotation.py | 34 +-- ote_sdk/ote_sdk/entities/datasets.py | 4 - ote_sdk/ote_sdk/entities/shapes/ellipse.py | 19 +- ote_sdk/ote_sdk/entities/shapes/polygon.py | 11 +- ote_sdk/ote_sdk/entities/shapes/rectangle.py | 45 ++-- ote_sdk/ote_sdk/entities/shapes/shape.py | 53 +--- .../tests/entities/shapes/test_ellipse.py | 1 - .../tests/entities/shapes/test_rectangle.py | 31 +-- .../tests/entities/shapes/test_shape.py | 234 +----------------- .../ote_sdk/tests/entities/test_annotation.py | 7 +- .../tests/entities/test_dataset_item.py | 12 - .../ote_sdk/tests/entities/test_datasets.py | 1 - .../tests/entities/test_result_media.py | 2 - ote_sdk/ote_sdk/tests/entities/test_subset.py | 13 +- .../ote_sdk/tests/utils/test_shape_drawer.py | 4 - ote_sdk/ote_sdk/utils/segmentation_utils.py | 3 - 16 files changed, 49 insertions(+), 425 deletions(-) diff --git a/ote_sdk/ote_sdk/entities/annotation.py b/ote_sdk/ote_sdk/entities/annotation.py index 5e929d8dc97..8aff0b1dce7 100644 --- a/ote_sdk/ote_sdk/entities/annotation.py +++ b/ote_sdk/ote_sdk/entities/annotation.py @@ -8,8 +8,6 @@ from enum import Enum from typing import Dict, List, Optional, Set -from bson import ObjectId - from ote_sdk.entities.id import ID from ote_sdk.entities.label import LabelEntity from ote_sdk.entities.scored_label import ScoredLabel @@ -23,10 +21,7 @@ class Annotation(metaclass=abc.ABCMeta): """ # pylint: disable=redefined-builtin; - def __init__( - self, shape: ShapeEntity, labels: List[ScoredLabel], id: Optional[ID] = None - ): - self.__id_ = ID(ObjectId()) if id is None else id + def __init__(self, shape: ShapeEntity, labels: List[ScoredLabel]): self.__shape = shape self.__labels = labels @@ -34,31 +29,9 @@ def __repr__(self): return ( f"{self.__class__.__name__}(" f"shape={self.shape}, " - f"labels={self.get_labels(True)}, " - f"id={self.id_})" + f"labels={self.get_labels(True)}" ) - @property - def id_(self): - """ - Returns the id for the annotation - """ - return self.__id_ - - @id_.setter - def id_(self, value): - self.__id_ = value - - @property - def id(self): - """DEPRECATED""" - return self.__id_ - - @id.setter - def id(self, value): - """DEPRECATED""" - self.__id_ = value - @property def shape(self): """ @@ -113,8 +86,7 @@ def set_labels(self, labels: List[ScoredLabel]): def __eq__(self, other): if isinstance(other, Annotation): return ( - self.id_ == other.id_ - and self.get_labels(True) == other.get_labels(True) + self.get_labels(True) == other.get_labels(True) and self.shape == other.shape ) return False diff --git a/ote_sdk/ote_sdk/entities/datasets.py b/ote_sdk/ote_sdk/entities/datasets.py index 59953cb25ed..6abfb65ce2e 100644 --- a/ote_sdk/ote_sdk/entities/datasets.py +++ b/ote_sdk/ote_sdk/entities/datasets.py @@ -12,11 +12,8 @@ from enum import Enum from typing import Iterator, List, Optional, Sequence, Union, overload -from bson.objectid import ObjectId - from ote_sdk.entities.annotation import AnnotationSceneEntity, AnnotationSceneKind from ote_sdk.entities.dataset_item import DatasetItemEntity -from ote_sdk.entities.id import ID from ote_sdk.entities.label import LabelEntity from ote_sdk.entities.subset import Subset @@ -277,7 +274,6 @@ def with_empty_annotations( # reset ROI roi = copy.copy(dataset_item.roi) - roi.id_ = ID(ObjectId()) roi.set_labels([]) new_dataset_item = DatasetItemEntity( diff --git a/ote_sdk/ote_sdk/entities/shapes/ellipse.py b/ote_sdk/ote_sdk/entities/shapes/ellipse.py index 863f5136c9b..b8b7e99507b 100644 --- a/ote_sdk/ote_sdk/entities/shapes/ellipse.py +++ b/ote_sdk/ote_sdk/entities/shapes/ellipse.py @@ -14,7 +14,6 @@ from scipy import optimize, special from shapely.geometry import Polygon as shapely_polygon -from ote_sdk.entities.scored_label import ScoredLabel from ote_sdk.entities.shapes.rectangle import Rectangle from ote_sdk.entities.shapes.shape import Shape, ShapeType from ote_sdk.utils.time_utils import now @@ -33,7 +32,6 @@ class Ellipse(Shape): :param y1: top y coordinate of encapsulating rectangle :param x2: right x coordinate of encapsulating rectangle :param y2: bottom y coordinate of encapsulating rectangle - :param labels: list of the ScoredLabel's for the Ellipse :param modification_date: last modified date """ @@ -44,14 +42,11 @@ def __init__( y1: float, x2: float, y2: float, - labels: Optional[List[ScoredLabel]] = None, modification_date: Optional[datetime.datetime] = None, ): - labels = [] if labels is None else labels modification_date = now() if modification_date is None else modification_date super().__init__( - type=ShapeType.ELLIPSE, - labels=labels, + shape_type=ShapeType.ELLIPSE, modification_date=modification_date, ) @@ -92,7 +87,7 @@ def width(self): :example: - >>> e1 = Ellipse(x1=0.5, x2=1.0, y1=0.0, y2=0.5, labels = []) + >>> e1 = Ellipse(x1=0.5, x2=1.0, y1=0.0, y2=0.5) >>> e1.width 0.5 @@ -107,7 +102,7 @@ def height(self): :example: - >>> e1 = Ellipse(x1=0.5, x2=1.0, y1=0.0, y2=0.5, labels = []) + >>> e1 = Ellipse(x1=0.5, x2=1.0, y1=0.0, y2=0.5) >>> e1.height 0.5 @@ -136,7 +131,7 @@ def minor_axis(self) -> float: :example: - >>> e1 = Ellipse(x1=0.5, x2=1.0, y1=0.0, y2=0.4, labels = []) + >>> e1 = Ellipse(x1=0.5, x2=1.0, y1=0.0, y2=0.4) >>> e1.minor_axis 0.2 @@ -153,7 +148,7 @@ def major_axis(self) -> float: :example: - >>> e1 = Ellipse(x1=0.5, x2=1.0, y1=0.0, y2=0.4, labels = []) + >>> e1 = Ellipse(x1=0.5, x2=1.0, y1=0.0, y2=0.4) >>> e1.major_axis 0.25 @@ -179,7 +174,7 @@ def normalize_wrt_roi_shape(self, roi_shape: Rectangle) -> "Ellipse": >>> roi = Rectangle(x1=0.0, x2=0.5, y1=0.0, y2=0.5) >>> normalized = c1.normalize_wrt_roi_shape(roi_shape) >>> normalized - Ellipse(, x1=0.25, y1=0.25, x2=0.3, y2=0.3, scored_labels=[]) + Ellipse(, x1=0.25, y1=0.25, x2=0.3, y2=0.3) :param roi_shape: Region of Interest :return: New polygon in the image coordinate system @@ -214,7 +209,7 @@ def denormalize_wrt_roi_shape(self, roi_shape: Rectangle) -> "Ellipse": >>> roi = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=1.0) # the half-right >>> normalized = c1.denormalize_wrt_roi_shape(roi_shape) # should return top half >>> normalized - Ellipse(, x1=0.0, y1=0.0, x2=1.0, y2=0.5, scored_labels=[]) + Ellipse(, x1=0.0, y1=0.0, x2=1.0, y2=0.5) :param roi_shape: Region of Interest :return: New polygon in the ROI coordinate system diff --git a/ote_sdk/ote_sdk/entities/shapes/polygon.py b/ote_sdk/ote_sdk/entities/shapes/polygon.py index a6fe12c5213..2abc660a7dd 100644 --- a/ote_sdk/ote_sdk/entities/shapes/polygon.py +++ b/ote_sdk/ote_sdk/entities/shapes/polygon.py @@ -14,7 +14,6 @@ from shapely.geometry import Polygon as shapely_polygon -from ote_sdk.entities.scored_label import ScoredLabel from ote_sdk.entities.shapes.rectangle import Rectangle from ote_sdk.entities.shapes.shape import Shape, ShapeType from ote_sdk.utils.time_utils import now @@ -80,7 +79,6 @@ class Polygon(Shape): NB Freehand drawings are also stored as polygons. :param points: list of Point's forming the polygon - :param labels: list of the ScoredLabel's for the Polygon :param modification_date: last modified date """ @@ -88,14 +86,11 @@ class Polygon(Shape): def __init__( self, points: List[Point], - labels: Optional[List[ScoredLabel]] = None, modification_date: Optional[datetime.datetime] = None, ): - labels = [] if labels is None else labels modification_date = now() if modification_date is None else modification_date super().__init__( - type=ShapeType.POLYGON, - labels=labels, + shape_type=ShapeType.POLYGON, modification_date=modification_date, ) @@ -151,7 +146,7 @@ def normalize_wrt_roi_shape(self, roi_shape: Rectangle) -> "Polygon": >>> roi = Rectangle(x1=0.0, x2=0.5, y1=0.0, y2=0.5) >>> normalized = p1.normalize_wrt_roi_shape(roi_shape) >>> normalized - Polygon(, len(points)=3, scored_labels=[]) + Polygon(, len(points)=3) :param roi_shape: Region of Interest :return: New polygon in the image coordinate system @@ -181,7 +176,7 @@ def denormalize_wrt_roi_shape(self, roi_shape: Rectangle) -> "Polygon": >>> roi = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=1.0) # the half-right >>> normalized = p1.denormalize_wrt_roi_shape(roi_shape) >>> normalized - Polygon(, len(points)=3, scored_labels=[]) + Polygon(, len(points)=3) :param roi_shape: Region of Interest :return: New polygon in the ROI coordinate system diff --git a/ote_sdk/ote_sdk/entities/shapes/rectangle.py b/ote_sdk/ote_sdk/entities/shapes/rectangle.py index fe73135d729..6ea82453dec 100644 --- a/ote_sdk/ote_sdk/entities/shapes/rectangle.py +++ b/ote_sdk/ote_sdk/entities/shapes/rectangle.py @@ -9,12 +9,11 @@ import datetime import math import warnings -from typing import List, Optional +from typing import Optional import numpy as np from shapely.geometry import Polygon as shapely_polygon -from ote_sdk.entities.scored_label import ScoredLabel from ote_sdk.entities.shapes.shape import Shape, ShapeEntity, ShapeType from ote_sdk.utils.time_utils import now @@ -36,7 +35,6 @@ class Rectangle(Shape): :param y1: see above :param x2: see above :param y2: see above - :param labels: list of the ScoredLabel's for the rectangle :param modification_date: last modified date """ @@ -47,14 +45,11 @@ def __init__( y1: float, x2: float, y2: float, - labels: Optional[List[ScoredLabel]] = None, modification_date: Optional[datetime.datetime] = None, ): - labels = [] if labels is None else labels modification_date = now() if modification_date is None else modification_date super().__init__( - type=ShapeType.RECTANGLE, - labels=labels, + shape_type=ShapeType.RECTANGLE, modification_date=modification_date, ) @@ -106,7 +101,7 @@ def clip_to_visible_region(self) -> "Rectangle": x2 = min(max(0.0, self.x2), 1.0) y2 = min(max(0.0, self.y2), 1.0) - return Rectangle(x1, y1, x2, y2, [], self.modification_date) + return Rectangle(x1, y1, x2, y2, self.modification_date) def normalize_wrt_roi_shape(self, roi_shape: "Rectangle") -> "Rectangle": """ @@ -122,7 +117,7 @@ def normalize_wrt_roi_shape(self, roi_shape: "Rectangle") -> "Rectangle": >>> roi = Rectangle(x1=0.0, x2=0.5, y1=0.0, y2=0.5) >>> normalized = b1.normalize_wrt_roi_shape(roi_shape) >>> normalized - Box(, x=0.25, y=0.0, width=0.25, height=0.25, scored_labels=[]) + Box(, x=0.25, y=0.0, width=0.25, height=0.25) :param roi_shape: Region of Interest :return: New polygon in the image coordinate system @@ -152,14 +147,14 @@ def denormalize_wrt_roi_shape(self, roi_shape: "Rectangle") -> "Rectangle": Box denormalized to a rectangle as ROI >>> from ote_sdk.entities.annotation import Annotation - >>> b1 = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=0.5, labels = []) + >>> b1 = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=0.5) # the top-right - >>> roi = Annotation(Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=1.0), labels = []) + >>> roi = Annotation(Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=1.0)) # the half-right >>> normalized = b1.denormalize_wrt_roi_shape(roi_shape) # should return top half >>> normalized - Box(, x=0.0, y=0.0, width=1.0, height=0.5, scored_labels=[]) + Box(, x=0.0, y=0.0, width=1.0, height=0.5) :param roi_shape: Region of Interest :return: New polygon in the ROI coordinate system @@ -193,26 +188,18 @@ def _as_shapely_polygon(self) -> shapely_polygon: return shapely_polygon(points) @classmethod - def generate_full_box( - cls, labels: Optional[List[ScoredLabel]] = None - ) -> "Rectangle": + def generate_full_box(cls) -> "Rectangle": """ - Returns a rectangle that fully encapsulates the normalized coordinate space, - with `labels` + Returns a rectangle that fully encapsulates the normalized coordinate space :example: >>> Rectangle.generate_full_box() - Box(, x=0.0, y=0.0, width=1.0, height=1.0, scored_labels=[]) - - :param labels: labels to assigned to the output rectangle + Box(, x=0.0, y=0.0, width=1.0, height=1.0) :return: a rectangle that fully encapsulates the normalized coordinate space, - with `labels` """ - if labels is None: - labels = [] - return cls(x1=0.0, y1=0.0, x2=1.0, y2=1.0, labels=labels) + return cls(x1=0.0, y1=0.0, x2=1.0, y2=1.0) @staticmethod def is_full_box(rectangle: ShapeEntity) -> bool: @@ -222,11 +209,11 @@ def is_full_box(rectangle: ShapeEntity) -> bool: :example: - >>> b1 = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=1.0, labels = []) + >>> b1 = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=1.0) >>> Rectangle.is_full_box(b1) False - >>> b2 = Rectangle(x1=0.0, x2=1.0, y1=0.0, y2=1.0, labels = []) + >>> b2 = Rectangle(x1=0.0, x2=1.0, y1=0.0, y2=1.0) >>> Rectangle.is_full_box(b2) True @@ -271,7 +258,7 @@ def width(self): :example: - >>> b1 = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=0.5, labels = []) + >>> b1 = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=0.5) >>> b1.width 0.5 @@ -286,7 +273,7 @@ def height(self): :example: - >>> b1 = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=0.5, labels = []) + >>> b1 = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=0.5) >>> b1.height 0.5 @@ -301,7 +288,7 @@ def diagonal(self): :example: - >>> b1 = Rectangle(x1=0.0, x2=0.3, y1=0.0, y2=0.4, labels = []) + >>> b1 = Rectangle(x1=0.0, x2=0.3, y1=0.0, y2=0.4) >>> b1.diagonal 0.5 diff --git a/ote_sdk/ote_sdk/entities/shapes/shape.py b/ote_sdk/ote_sdk/entities/shapes/shape.py index 48f4dde1ad6..600d8b606f2 100644 --- a/ote_sdk/ote_sdk/entities/shapes/shape.py +++ b/ote_sdk/ote_sdk/entities/shapes/shape.py @@ -5,15 +5,14 @@ # import abc +import datetime import warnings from enum import IntEnum, auto -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING from shapely.errors import PredicateError, TopologicalError from shapely.geometry import Polygon as shapely_polygon -from ote_sdk.entities.scored_label import ScoredLabel - if TYPE_CHECKING: from ote_sdk.entities.shapes.rectangle import Rectangle @@ -37,9 +36,8 @@ class ShapeEntity(metaclass=abc.ABCMeta): """ # pylint: disable=redefined-builtin - def __init__(self, type: ShapeType, labels: List[ScoredLabel]): - self._type = type - self._labels = labels + def __init__(self, shape_type: ShapeType): + self._type = shape_type @property def type(self): @@ -76,34 +74,6 @@ def contains_center(self, other: "ShapeEntity") -> bool: """ raise NotImplementedError - @abc.abstractmethod - def get_labels(self, include_empty: bool = False): - """ - Get scored labels that are assigned to this shape - - :param include_empty: set to True to include empty label (if exists) in the output. - :return: List of labels in shape - """ - raise NotImplementedError - - @abc.abstractmethod - def append_label(self, label: ScoredLabel): - """ - Appends the scored label to the shape. - - :param label: the scored label to be appended to the shape - """ - raise NotImplementedError - - @abc.abstractmethod - def set_labels(self, labels: List[ScoredLabel]): - """ - Sets the labels of the shape to be the input of the function. - - :param labels: the scored labels to be set as shape labels - """ - raise NotImplementedError - @abc.abstractmethod def normalize_wrt_roi_shape(self, roi_shape: "Rectangle") -> "Shape": """ @@ -146,8 +116,8 @@ class Shape(ShapeEntity): """ # pylint: disable=redefined-builtin, too-many-arguments; Requires refactor - def __init__(self, type: ShapeType, labels: List[ScoredLabel], modification_date): - super().__init__(type=type, labels=labels) + def __init__(self, shape_type: ShapeType, modification_date: datetime.datetime): + super().__init__(shape_type=shape_type) self.modification_date = modification_date def __repr__(self): @@ -180,17 +150,6 @@ def contains_center(self, other: "ShapeEntity") -> bool: polygon_shape = other._as_shapely_polygon() return polygon_roi.contains(polygon_shape.centroid) - def get_labels(self, include_empty: bool = False) -> List[ScoredLabel]: - return [ - label for label in self._labels if include_empty or (not label.is_empty) - ] - - def append_label(self, label: ScoredLabel): - self._labels.append(label) - - def set_labels(self, labels: List[ScoredLabel]): - self._labels = labels - def _validate_coordinates(self, x: float, y: float) -> bool: """ Checks whether the values for a given x,y coordinate pair lie within the range of (0,1) that is expected for diff --git a/ote_sdk/ote_sdk/tests/entities/shapes/test_ellipse.py b/ote_sdk/ote_sdk/tests/entities/shapes/test_ellipse.py index 323ca0b8516..65524153a44 100644 --- a/ote_sdk/ote_sdk/tests/entities/shapes/test_ellipse.py +++ b/ote_sdk/ote_sdk/tests/entities/shapes/test_ellipse.py @@ -78,7 +78,6 @@ def test_ellipse(self): assert ellipse.y_center == 0.25 assert ellipse.minor_axis == 0.25 assert ellipse.major_axis == 0.25 - assert ellipse._labels == [] assert ellipse.modification_date == modification_date incorrect_ellipse_params = { diff --git a/ote_sdk/ote_sdk/tests/entities/shapes/test_rectangle.py b/ote_sdk/ote_sdk/tests/entities/shapes/test_rectangle.py index cbe18e4a0b4..c02366f3a7e 100644 --- a/ote_sdk/ote_sdk/tests/entities/shapes/test_rectangle.py +++ b/ote_sdk/ote_sdk/tests/entities/shapes/test_rectangle.py @@ -67,7 +67,6 @@ def vertical_rectangle_params(self) -> dict: "y1": 0.1, "x2": 0.3, "y2": 0.4, - "labels": self.rectangle_labels(), "modification_date": datetime( year=2020, month=1, day=1, hour=9, minute=30, second=15, microsecond=2 ), @@ -130,11 +129,9 @@ class object initiation with optional parameters """ # Checking default values of optional parameters default_params_rectangle = self.horizontal_rectangle() - assert default_params_rectangle._labels == [] assert isinstance(default_params_rectangle.modification_date, datetime) # check for specified values of optional parameters specified_params_rectangle = self.vertical_rectangle() - assert specified_params_rectangle._labels == self.rectangle_labels() assert specified_params_rectangle.modification_date == datetime( year=2020, month=1, day=1, hour=9, minute=30, second=15, microsecond=2 ) @@ -237,11 +234,6 @@ def test_rectangle_eq(self): # Check for different types branch assert rectangle != str # Check for unequal labels parameters. Expected that different labels are not affecting equality - unequal_label = LabelEntity( - name="Unequal label", domain=Domain.SEGMENTATION, id=ID("unequal_label_1") - ) - unequal_scored_label = ScoredLabel(label=unequal_label) - equal_rectangle._labels.append(unequal_scored_label) assert rectangle == equal_rectangle # Check for instances with unequal parameters combinations # Generating all possible scenarios of parameter values submission @@ -321,7 +313,6 @@ def test_rectangle_clip_to_visible_region(self): "y1": 0.2, "x2": 0.6, "y2": 0.4, - "labels": self.rectangle_labels(), }, "params_expected": {"x1": 0.3, "y1": 0.2, "x2": 0.6, "y2": 0.4}, }, @@ -331,7 +322,6 @@ def test_rectangle_clip_to_visible_region(self): "y1": -0.3, "x2": 1.6, "y2": 1.4, - "labels": self.rectangle_labels(), }, "params_expected": {"x1": 0.0, "y1": 0.0, "x2": 1.0, "y2": 1.0}, }, @@ -341,7 +331,6 @@ def test_rectangle_clip_to_visible_region(self): "y1": 0.0, "x2": 1.0, "y2": 1.0, - "labels": self.rectangle_labels(), }, "params_expected": {"x1": 0.0, "y1": 0.0, "x2": 1.0, "y2": 1.0}, }, @@ -464,29 +453,17 @@ def test_rectangle_generate_full_box(self): Description: Check Rectangle generate_full_box method - Input data: - Labels specified for full_box instance of Rectangle class - Expected results: Test passes if generate_full_box method returns instance of Rectangle class with coordinates (x1=0.0, y1=0.0, x2=1.0, y2=1.0) Steps 1. Check generate_full_box method for Rectangle instance with no labels specified - 2. Check generate_full_box method for Rectangle instance with labels specified """ - detection_label = ScoredLabel( - LabelEntity(name="detection", domain=Domain.DETECTION) - ) - for label_actual, label_expected in [ - (None, []), - ([detection_label], [detection_label]), - ]: - full_box = Rectangle.generate_full_box(label_actual) - assert full_box.type == ShapeType.RECTANGLE - assert full_box.x1 == full_box.y1 == 0.0 - assert full_box.x2 == full_box.y2 == 1.0 - assert full_box._labels == label_expected + full_box = Rectangle.generate_full_box() + assert full_box.type == ShapeType.RECTANGLE + assert full_box.x1 == full_box.y1 == 0.0 + assert full_box.x2 == full_box.y2 == 1.0 @pytest.mark.priority_medium @pytest.mark.unit diff --git a/ote_sdk/ote_sdk/tests/entities/shapes/test_shape.py b/ote_sdk/ote_sdk/tests/entities/shapes/test_shape.py index f1ee3de2c2c..3dcb8a76c9d 100644 --- a/ote_sdk/ote_sdk/tests/entities/shapes/test_shape.py +++ b/ote_sdk/ote_sdk/tests/entities/shapes/test_shape.py @@ -18,9 +18,6 @@ import pytest -from ote_sdk.entities.color import Color -from ote_sdk.entities.label import LabelEntity -from ote_sdk.entities.scored_label import Domain, ScoredLabel from ote_sdk.entities.shapes.ellipse import Ellipse from ote_sdk.entities.shapes.polygon import Point, Polygon from ote_sdk.entities.shapes.rectangle import Rectangle @@ -84,24 +81,6 @@ def test_shape_entity_not_implemented_methods(self): ShapeEntity.intersects(shape, shape) with pytest.raises(NotImplementedError): ShapeEntity.contains_center(shape, shape) - with pytest.raises(NotImplementedError): - ShapeEntity.get_labels(shape) - with pytest.raises(NotImplementedError): - ShapeEntity.append_label( - shape, - ScoredLabel( - LabelEntity(name="classification", domain=Domain.CLASSIFICATION) - ), - ) - with pytest.raises(NotImplementedError): - ShapeEntity.set_labels( - shape, - [ - ScoredLabel( - LabelEntity(name="detection", domain=Domain.DETECTION) - ) - ], - ) with pytest.raises(NotImplementedError): ShapeEntity.normalize_wrt_roi_shape(shape, rectangle_entity) with pytest.raises(NotImplementedError): @@ -133,59 +112,11 @@ def fully_covering_polygon() -> Polygon: ] ) - @staticmethod - def generate_labels_list(include_empty: bool = True) -> list: - classification_label = ScoredLabel( - LabelEntity( - name="classification", - domain=Domain.CLASSIFICATION, - color=Color(red=187, green=28, blue=28), - creation_date=datetime(year=2021, month=10, day=25), - ) - ) - detection_label = ScoredLabel( - LabelEntity( - name="detection", - domain=Domain.DETECTION, - color=Color(red=180, green=30, blue=24), - creation_date=datetime(year=2021, month=9, day=24), - ) - ) - empty_label = ScoredLabel( - LabelEntity( - name="empty_rectangle_label", - domain=Domain.CLASSIFICATION, - color=Color(red=178, green=25, blue=30), - creation_date=datetime(year=2021, month=7, day=26), - is_empty=True, - ) - ) - labels_list = [classification_label, detection_label] - if include_empty: - labels_list.append(empty_label) - return labels_list - - @staticmethod - def appendable_label(empty=False) -> ScoredLabel: - return ScoredLabel( - LabelEntity( - name="appended_label", - domain=Domain.CLASSIFICATION, - color=Color(red=181, green=28, blue=31), - creation_date=datetime(year=2021, month=11, day=22), - is_empty=empty, - ) - ) - def rectangle(self) -> Rectangle: - return Rectangle( - x1=0.2, y1=0.2, x2=0.6, y2=0.7, labels=self.generate_labels_list() - ) + return Rectangle(x1=0.2, y1=0.2, x2=0.6, y2=0.7) def ellipse(self) -> Ellipse: - return Ellipse( - x1=0.4, y1=0.1, x2=0.9, y2=0.8, labels=self.generate_labels_list() - ) + return Ellipse(x1=0.4, y1=0.1, x2=0.9, y2=0.8) def polygon(self) -> Polygon: return Polygon( @@ -197,7 +128,6 @@ def polygon(self) -> Polygon: Point(0.8, 0.4), Point(0.3, 0.4), ], - labels=self.generate_labels_list(), ) @staticmethod @@ -403,166 +333,6 @@ def test_shape_contains_center(self): for shape_outside in shapes_outside: assert not rectangle_part.contains_center(shape_outside) - @pytest.mark.priority_medium - @pytest.mark.unit - @pytest.mark.reqids(Requirements.REQ_1) - def test_shape_get_labels(self): - """ - Description: - Check Shape get_labels method for Rectangle, Ellipse and Polygon objects - - Expected results: - Test passes if get_labels method returns expected values - - Steps - 1. Check get_labels method for Shapes with no labels specified - 2. Check get_labels method for Shapes with specified labels and include_empty parameter set to False - 3. Check get_labels method for Shapes with specified labels and include_empty parameter set to True - """ - # Checks for no labels specified - for no_labels_shape in [ - self.fully_covering_rectangle(), - self.fully_covering_ellipse(), - self.fully_covering_polygon(), - ]: - assert no_labels_shape.get_labels() == [] - # Checks for labels specified and include_empty set to False - expected_false_include_empty_labels = self.generate_labels_list( - include_empty=False - ) - for false_include_empty_labels_shape in [ - self.rectangle(), - self.ellipse(), - self.polygon(), - ]: - assert ( - false_include_empty_labels_shape.get_labels() - == expected_false_include_empty_labels - ) - # Checks for labels specified and include_empty set to True - expected_include_empty_labels = self.generate_labels_list(include_empty=True) - for include_empty_labels_shape in [ - self.rectangle(), - self.ellipse(), - self.polygon(), - ]: - assert ( - include_empty_labels_shape.get_labels(include_empty=True) - == expected_include_empty_labels - ) - - @pytest.mark.priority_medium - @pytest.mark.unit - @pytest.mark.reqids(Requirements.REQ_1) - def test_shape_append_label(self): - """ - Description: - Check Shape get_labels method for Rectangle, Ellipse and Polygon objects - - Expected results: - Test passes if append_label method returns expected values - - Steps - 1. Check append_label method to add label to Shape object with no labels specified - 2. Check append_label method to add empty label to Shape object with no labels specified - 3. Check append_label method to add label to Shape object with specified labels - 4. Check append_label method to add empty label to Shape object with specified labels - """ - appendable_label = self.appendable_label() - empty_appendable_label = self.appendable_label(empty=True) - # Check for adding label to Shape with no labels specified - for no_labels_shape in [ - self.fully_covering_rectangle(), - self.fully_covering_ellipse(), - self.fully_covering_polygon(), - ]: - no_labels_shape.append_label(appendable_label) - assert no_labels_shape.get_labels() == [appendable_label] - # Check for adding empty label to Shape with no labels specified - for no_labels_shape in [ - self.fully_covering_rectangle(), - self.fully_covering_ellipse(), - self.fully_covering_polygon(), - ]: - no_labels_shape.append_label(empty_appendable_label) - assert no_labels_shape.get_labels() == [] - assert no_labels_shape.get_labels(include_empty=True) == [ - empty_appendable_label - ] - # Check for adding label to Shape with labels specified - for shape in [self.rectangle(), self.ellipse(), self.polygon()]: - expected_labels = shape.get_labels() - expected_labels.append(appendable_label) - shape.append_label(appendable_label) - # Check for adding empty label to Shape with labels specified - expected_labels_false_empty = self.generate_labels_list(include_empty=False) - expected_include_empty_labels = self.generate_labels_list(include_empty=True) - expected_include_empty_labels.append(empty_appendable_label) - for shape in [self.rectangle(), self.ellipse(), self.polygon()]: - shape.append_label(empty_appendable_label) - assert shape.get_labels() == expected_labels_false_empty - assert shape.get_labels(include_empty=True) == expected_include_empty_labels - - @pytest.mark.priority_medium - @pytest.mark.unit - @pytest.mark.reqids(Requirements.REQ_1) - def test_shape_set_labels(self): - """ - Description: - Check Shape set_labels method for Rectangle, Ellipse and Polygon objects - - Expected results: - Test passes if set_labels method returns expected values - - Steps - 1. Check set_labels method to add labels list to Shape object with no labels specified - 2. Check set_labels method to add empty labels list to Shape object with no labels specified - 3. Check set_labels method to add labels list to Shape object with labels specified - 4. Check set_labels method to add empty labels list to Shape object with labels specified - """ - not_empty_label = self.appendable_label() - new_labels_list = [ - not_empty_label, - ScoredLabel( - LabelEntity( - name="new_label", - domain=Domain.CLASSIFICATION, - color=Color(red=183, green=31, blue=28), - creation_date=datetime(year=2021, month=9, day=25), - is_empty=True, - ) - ), - ] - expected_not_empty_labels_list = [not_empty_label] - # Check for adding labels list to Shape with no labels specified - for no_labels_shape in [ - self.fully_covering_rectangle(), - self.fully_covering_ellipse(), - self.fully_covering_polygon(), - ]: - no_labels_shape.set_labels(new_labels_list) - assert no_labels_shape.get_labels() == expected_not_empty_labels_list - assert no_labels_shape.get_labels(include_empty=True) == new_labels_list - # Check for adding empty labels list to Shape with no labels specified - for no_labels_shape in [ - self.fully_covering_rectangle(), - self.fully_covering_ellipse(), - self.fully_covering_polygon(), - ]: - no_labels_shape.set_labels([]) - assert no_labels_shape.get_labels() == [] - assert no_labels_shape.get_labels(include_empty=True) == [] - # Check for adding labels list to Shape with labels specified - for shape in [self.rectangle(), self.ellipse(), self.polygon()]: - shape.set_labels(new_labels_list) - assert shape.get_labels() == expected_not_empty_labels_list - assert shape.get_labels(include_empty=True) == new_labels_list - # Check for adding empty labels list to Shape with labels specified - for shape in [self.rectangle(), self.ellipse(), self.polygon()]: - shape.set_labels([]) - assert shape.get_labels() == [] - assert shape.get_labels(include_empty=True) == [] - @pytest.mark.priority_medium @pytest.mark.unit @pytest.mark.reqids(Requirements.REQ_1) diff --git a/ote_sdk/ote_sdk/tests/entities/test_annotation.py b/ote_sdk/ote_sdk/tests/entities/test_annotation.py index 4d2177a23f8..c1ebb3a8937 100644 --- a/ote_sdk/ote_sdk/tests/entities/test_annotation.py +++ b/ote_sdk/ote_sdk/tests/entities/test_annotation.py @@ -81,8 +81,6 @@ def test_annotation_default_property(self): annotation = self.annotation - assert type(annotation.id_) == ID - assert annotation.id_ is not None assert str(annotation.shape) == "Rectangle(x=0.5, y=0.0, width=0.5, height=0.5)" assert annotation.get_labels() == [] @@ -109,9 +107,6 @@ def test_annotation_setters(self): annotation = self.annotation ellipse = Ellipse(x1=0.5, y1=0.1, x2=0.8, y2=0.3) annotation.shape = ellipse - annotation.id_ = ID(123456789) - - assert annotation.id_ == ID(123456789) assert annotation.shape == ellipse @pytest.mark.priority_medium @@ -144,7 +139,7 @@ def test_annotation_magic_methods(self): assert ( repr(annotation) - == "Annotation(shape=Ellipse(x1=0.5, y1=0.1, x2=0.8, y2=0.3), labels=[], id=123456789)" + == "Annotation(shape=Ellipse(x1=0.5, y1=0.1, x2=0.8, y2=0.3), labels=[]" ) assert annotation == other_annotation assert annotation != third_annotation diff --git a/ote_sdk/ote_sdk/tests/entities/test_dataset_item.py b/ote_sdk/ote_sdk/tests/entities/test_dataset_item.py index 71fd3ed1053..6ac0e5ab1bd 100644 --- a/ote_sdk/ote_sdk/tests/entities/test_dataset_item.py +++ b/ote_sdk/ote_sdk/tests/entities/test_dataset_item.py @@ -66,12 +66,10 @@ def annotations(self) -> List[Annotation]: detection_annotation = Annotation( shape=rectangle, labels=[ScoredLabel(label=labels[0])], - id=ID("detection_annotation_1"), ) segmentation_annotation = Annotation( shape=other_rectangle, labels=[ScoredLabel(label=labels[1])], - id=ID("segmentation_annotation_1"), ) return [detection_annotation, segmentation_annotation] @@ -109,7 +107,6 @@ def roi(self): modification_date=datetime.datetime(year=2021, month=12, day=9), ), labels=self.roi_scored_labels(), - id=ID("roi_annotation"), ) return roi @@ -161,7 +158,6 @@ def compare_denormalized_annotations( expected_annotation = expected_annotations[index] # Redefining id and modification_date required because of new Annotation objects created after shape # denormalize - actual_annotation.id_ = expected_annotation.id_ actual_annotation.shape.modification_date = ( expected_annotation.shape.modification_date ) @@ -191,12 +187,10 @@ def annotations_to_add(self) -> List[Annotation]: annotation_to_add = Annotation( shape=Rectangle(x1=0.1, y1=0.1, x2=0.7, y2=0.8), labels=[ScoredLabel(label=labels_to_add[0])], - id=ID("added_annotation_1"), ) other_annotation_to_add = Annotation( shape=Rectangle(x1=0.2, y1=0.3, x2=0.8, y2=0.9), labels=[ScoredLabel(label=labels_to_add[1])], - id=ID("added_annotation_2"), ) return [annotation_to_add, other_annotation_to_add] @@ -399,7 +393,6 @@ def test_dataset_item_roi_numpy(self): rectangle_roi = Annotation( Rectangle(x1=0.2, y1=0.1, x2=0.8, y2=0.9), [ScoredLabel(roi_label)], - ID("rectangle_roi"), ) assert np.array_equal( dataset_item.roi_numpy(rectangle_roi), media.numpy[1:9, 3:13] @@ -408,7 +401,6 @@ def test_dataset_item_roi_numpy(self): ellipse_roi = Annotation( Ellipse(x1=0.1, y1=0.0, x2=0.9, y2=0.8), [ScoredLabel(roi_label)], - ID("ellipse_roi"), ) assert np.array_equal( dataset_item.roi_numpy(ellipse_roi), media.numpy[0:8, 2:14] @@ -425,7 +417,6 @@ def test_dataset_item_roi_numpy(self): ] ), labels=[], - id=ID("polygon_roi"), ) assert np.array_equal( dataset_item.roi_numpy(polygon_roi), media.numpy[4:8, 5:13] @@ -625,8 +616,6 @@ def test_dataset_item_append_annotations(self): ) dataset_item.append_annotations(annotations_to_add) # Random id is generated for normalized annotations - normalized_annotations[0].id_ = dataset_item.annotation_scene.annotations[2].id_ - normalized_annotations[1].id_ = dataset_item.annotation_scene.annotations[3].id_ assert ( dataset_item.annotation_scene.annotations == full_box_annotations + normalized_annotations @@ -644,7 +633,6 @@ def test_dataset_item_append_annotations(self): incorrect_shape_annotation = Annotation( shape=incorrect_polygon, labels=[ScoredLabel(incorrect_shape_label)], - id=ID("incorrect_shape_annotation"), ) dataset_item.append_annotations([incorrect_shape_annotation]) assert ( diff --git a/ote_sdk/ote_sdk/tests/entities/test_datasets.py b/ote_sdk/ote_sdk/tests/entities/test_datasets.py index c5c0522c2de..abed07c140d 100644 --- a/ote_sdk/ote_sdk/tests/entities/test_datasets.py +++ b/ote_sdk/ote_sdk/tests/entities/test_datasets.py @@ -454,7 +454,6 @@ def check_empty_annotations_dataset( assert actual_item.media is expected_item.media assert actual_item.annotation_scene.annotations == [] assert actual_item.annotation_scene.kind == expected_kind - assert actual_item.roi.id_ != expected_item.roi.id_ assert actual_item.roi.shape is expected_item.roi.shape assert actual_item.roi.get_labels() == [] assert actual_item.subset is expected_item.subset diff --git a/ote_sdk/ote_sdk/tests/entities/test_result_media.py b/ote_sdk/ote_sdk/tests/entities/test_result_media.py index 2bddc58ccbf..73950605afb 100644 --- a/ote_sdk/ote_sdk/tests/entities/test_result_media.py +++ b/ote_sdk/ote_sdk/tests/entities/test_result_media.py @@ -38,7 +38,6 @@ def default_result_media_parameters() -> dict: rectangle_annotation = Annotation( shape=Rectangle(x1=0.1, y1=0.4, x2=0.4, y2=0.9), labels=[ScoredLabel(rectangle_label)], - id=ID("rectangle_annotation"), ) annotation_scene = AnnotationSceneEntity( annotations=[rectangle_annotation], @@ -65,7 +64,6 @@ def optional_result_media_parameters(self) -> dict: roi = Annotation( shape=Rectangle(x1=0.3, y1=0.2, x2=0.7, y2=0.6), labels=[ScoredLabel(roi_label)], - id=ID("roi_annotation"), ) result_media_label = LabelEntity( "ResultMedia label", diff --git a/ote_sdk/ote_sdk/tests/entities/test_subset.py b/ote_sdk/ote_sdk/tests/entities/test_subset.py index a5ace251aea..ca00c0b7b5d 100644 --- a/ote_sdk/ote_sdk/tests/entities/test_subset.py +++ b/ote_sdk/ote_sdk/tests/entities/test_subset.py @@ -43,6 +43,7 @@ def test_subset_members(self): TESTING = 3 UNLABELED = 4 PSEUDOLABELED = 5 + UNASSIGNED = 6 Steps 1. Create enum instance @@ -50,14 +51,14 @@ def test_subset_members(self): """ test_instance = Subset - for i in range(0, 6): + for i in range(0, 7): assert test_instance(i) in list(Subset) with pytest.raises(AttributeError): test_instance.WRONG with pytest.raises(ValueError): - test_instance(6) + test_instance(7) @pytest.mark.priority_medium @pytest.mark.unit @@ -81,14 +82,14 @@ def test_subset_magic_str(self): test_instance = Subset magic_str_list = [str(i) for i in list(Subset)] - for i in range(0, 6): + for i in range(0, 7): assert str(test_instance(i)) in magic_str_list with pytest.raises(AttributeError): str(test_instance.WRONG) with pytest.raises(ValueError): - str(test_instance(6)) + str(test_instance(7)) assert len(set(magic_str_list)) == len(magic_str_list) @@ -113,13 +114,13 @@ def test_subset_magic_repr(self): test_instance = Subset magic_repr_list = [repr(i) for i in list(Subset)] - for i in range(0, 6): + for i in range(0, 7): assert repr(test_instance(i)) in magic_repr_list with pytest.raises(AttributeError): repr(test_instance.WRONG) with pytest.raises(ValueError): - repr(test_instance(6)) + repr(test_instance(7)) assert len(set(magic_repr_list)) == len(magic_repr_list) diff --git a/ote_sdk/ote_sdk/tests/utils/test_shape_drawer.py b/ote_sdk/ote_sdk/tests/utils/test_shape_drawer.py index 7d55c7d980c..222f0e77cc0 100644 --- a/ote_sdk/ote_sdk/tests/utils/test_shape_drawer.py +++ b/ote_sdk/ote_sdk/tests/utils/test_shape_drawer.py @@ -600,7 +600,6 @@ def full_rectangle_annotation(self) -> Annotation: return Annotation( shape=Rectangle(x1=0, y1=0, x2=1, y2=1), labels=self.full_rectangle_scored_labels(), - id=ID("full_rectangle_annotation"), ) @staticmethod @@ -629,7 +628,6 @@ def rectangle_annotation(self) -> Annotation: return Annotation( shape=Rectangle(x1=0.1, y1=0.4, x2=0.4, y2=0.9), labels=self.rectangle_scored_labels(), - id=ID("rectangle_annotation"), ) @staticmethod @@ -666,7 +664,6 @@ def polygon_annotation(self) -> Annotation: ] ), labels=self.polygon_scored_labels(), - id=ID("polygon_annotation"), ) @staticmethod @@ -695,7 +692,6 @@ def ellipse_annotation(self) -> Annotation: return Annotation( shape=Ellipse(x1=0.5, y1=0.0, x2=1.0, y2=0.5), labels=self.ellipse_scored_labels(), - id=ID("ellipse_annotation"), ) def annotation_scene(self) -> AnnotationSceneEntity: diff --git a/ote_sdk/ote_sdk/utils/segmentation_utils.py b/ote_sdk/ote_sdk/utils/segmentation_utils.py index 3e1c1111fe5..e19b8d698c0 100644 --- a/ote_sdk/ote_sdk/utils/segmentation_utils.py +++ b/ote_sdk/ote_sdk/utils/segmentation_utils.py @@ -12,11 +12,9 @@ import cv2 import numpy as np -from bson import ObjectId from ote_sdk.entities.annotation import Annotation from ote_sdk.entities.dataset_item import DatasetItemEntity -from ote_sdk.entities.id import ID from ote_sdk.entities.label import LabelEntity from ote_sdk.entities.scored_label import ScoredLabel from ote_sdk.entities.shapes.polygon import Point, Polygon @@ -254,7 +252,6 @@ def create_annotation_from_segmentation_map( Annotation( shape=polygon, labels=[ScoredLabel(label, probability)], - id=ID(ObjectId()), ) ) else: From 2286e217a285bc1e911629dc900d701c30d9fba4 Mon Sep 17 00:00:00 2001 From: AlbertvanHouten Date: Thu, 12 May 2022 09:21:13 +0200 Subject: [PATCH 05/11] Addressed typing in shape/dataset/annotation --- ote_sdk/ote_sdk/entities/annotation.py | 37 ++++++++++--------- ote_sdk/ote_sdk/entities/datasets.py | 8 ++-- ote_sdk/ote_sdk/entities/shapes/ellipse.py | 8 ++-- ote_sdk/ote_sdk/entities/shapes/polygon.py | 2 +- ote_sdk/ote_sdk/entities/shapes/rectangle.py | 12 +++--- ote_sdk/ote_sdk/entities/shapes/shape.py | 2 +- .../tests/entities/shapes/test_rectangle.py | 13 +++---- 7 files changed, 42 insertions(+), 40 deletions(-) diff --git a/ote_sdk/ote_sdk/entities/annotation.py b/ote_sdk/ote_sdk/entities/annotation.py index 8aff0b1dce7..8f2facdefed 100644 --- a/ote_sdk/ote_sdk/entities/annotation.py +++ b/ote_sdk/ote_sdk/entities/annotation.py @@ -29,18 +29,18 @@ def __repr__(self): return ( f"{self.__class__.__name__}(" f"shape={self.shape}, " - f"labels={self.get_labels(True)}" + f"labels={self.get_labels(include_empty=True)}" ) @property - def shape(self): + def shape(self) -> ShapeEntity: """ Returns the shape that is in the annotation """ return self.__shape @shape.setter - def shape(self, value): + def shape(self, value) -> None: self.__shape = value def get_labels(self, include_empty: bool = False) -> List[ScoredLabel]: @@ -67,7 +67,7 @@ def get_label_ids(self, include_empty: bool = False) -> Set[ID]: if include_empty or (not label.is_empty) } - def append_label(self, label: ScoredLabel): + def append_label(self, label: ScoredLabel) -> None: """ Appends the scored label to the annotation. @@ -75,7 +75,7 @@ def append_label(self, label: ScoredLabel): """ self.__labels.append(label) - def set_labels(self, labels: List[ScoredLabel]): + def set_labels(self, labels: List[ScoredLabel]) -> None: """ Sets the labels of the annotation to be the input of the function. @@ -86,7 +86,8 @@ def set_labels(self, labels: List[ScoredLabel]): def __eq__(self, other): if isinstance(other, Annotation): return ( - self.get_labels(True) == other.get_labels(True) + self.get_labels(include_empty=True) + == other.get_labels(include_empty=True) and self.shape == other.shape ) return False @@ -160,14 +161,14 @@ def __repr__(self): ) @property - def id_(self): + def id_(self) -> ID: """ Returns the ID of the AnnotationSceneEntity. """ return self.__id_ @id_.setter - def id_(self, value): + def id_(self, value) -> None: self.__id_ = value @property @@ -181,36 +182,36 @@ def id(self, value): self.__id_ = value @property - def kind(self): + def kind(self) -> AnnotationSceneKind: """ Returns the AnnotationSceneKind of the AnnotationSceneEntity. """ return self.__kind @kind.setter - def kind(self, value): + def kind(self, value) -> None: self.__kind = value @property - def editor_name(self): + def editor_name(self) -> str: """ Returns the editor's name that made the AnnotationSceneEntity object. """ return self.__editor @editor_name.setter - def editor_name(self, value): + def editor_name(self, value) -> None: self.__editor = value @property - def creation_date(self): + def creation_date(self) -> datetime.datetime: """ Returns the creation date of the AnnotationSceneEntity object. """ return self.__creation_date @creation_date.setter - def creation_date(self, value): + def creation_date(self, value) -> None: self.__creation_date = value @property @@ -231,7 +232,7 @@ def shapes(self) -> List[ShapeEntity]: """ return [annotation.shape for annotation in self.annotations] - def contains_any(self, labels: List[LabelEntity]): + def contains_any(self, labels: List[LabelEntity]) -> bool: """ Checks whether the annotation contains any labels in the input parameter. @@ -249,13 +250,13 @@ def contains_any(self, labels: List[LabelEntity]): != 0 ) - def append_annotation(self, annotation: Annotation): + def append_annotation(self, annotation: Annotation) -> None: """ Appends the passed annotation to the list of annotations present in the AnnotationSceneEntity object. """ self.annotations.append(annotation) - def append_annotations(self, annotations: List[Annotation]): + def append_annotations(self, annotations: List[Annotation]) -> None: """ Adds a list of annotations to the annotation scene. """ @@ -272,7 +273,7 @@ def get_labels(self, include_empty: bool = False) -> List[LabelEntity]: labels: Dict[str, LabelEntity] = {} for annotation in self.annotations: - for label in annotation.get_labels(include_empty): + for label in annotation.get_labels(include_empty=include_empty): id_ = label.id_ if id_ not in labels: labels[id_] = label.get_label() diff --git a/ote_sdk/ote_sdk/entities/datasets.py b/ote_sdk/ote_sdk/entities/datasets.py index 6abfb65ce2e..d64df0eb7e0 100644 --- a/ote_sdk/ote_sdk/entities/datasets.py +++ b/ote_sdk/ote_sdk/entities/datasets.py @@ -137,7 +137,7 @@ def purpose(self) -> DatasetPurpose: return self._purpose @purpose.setter - def purpose(self, value: DatasetPurpose): + def purpose(self, value: DatasetPurpose) -> None: self._purpose = value def _fetch(self, key): @@ -306,7 +306,7 @@ def get_subset(self, subset: Subset) -> "DatasetEntity": ) return dataset - def remove(self, item: DatasetItemEntity): + def remove(self, item: DatasetItemEntity) -> None: """ Remove an item from the items. This function calls remove_at_indices function. @@ -339,14 +339,14 @@ def append(self, item: DatasetItemEntity) -> None: raise ValueError("Media in dataset item cannot be None") self._items.append(item) - def sort_items(self): + def sort_items(self) -> None: """ Order the dataset items. Does nothing here, but may be overrided in child classes. :return: None """ - def remove_at_indices(self, indices: List[int]): + def remove_at_indices(self, indices: List[int]) -> None: """ Delete items based on the `indices`. diff --git a/ote_sdk/ote_sdk/entities/shapes/ellipse.py b/ote_sdk/ote_sdk/entities/shapes/ellipse.py index b8b7e99507b..cbd133a482d 100644 --- a/ote_sdk/ote_sdk/entities/shapes/ellipse.py +++ b/ote_sdk/ote_sdk/entities/shapes/ellipse.py @@ -81,7 +81,7 @@ def __hash__(self): return hash(str(self)) @property - def width(self): + def width(self) -> float: """ Returns the width of the ellipse. (x-axis) @@ -96,7 +96,7 @@ def width(self): return self.x2 - self.x1 @property - def height(self): + def height(self) -> float: """ Returns the height of the ellipse. (y-axis) @@ -111,14 +111,14 @@ def height(self): return self.y2 - self.y1 @property - def x_center(self): + def x_center(self) -> float: """ Returns the x coordinate in the center of the ellipse. """ return self.x1 + self.width / 2 @property - def y_center(self): + def y_center(self) -> float: """ Returns the y coordinate in the center of the ellipse. """ diff --git a/ote_sdk/ote_sdk/entities/shapes/polygon.py b/ote_sdk/ote_sdk/entities/shapes/polygon.py index 2abc660a7dd..ff7acc47710 100644 --- a/ote_sdk/ote_sdk/entities/shapes/polygon.py +++ b/ote_sdk/ote_sdk/entities/shapes/polygon.py @@ -55,7 +55,7 @@ def normalize_wrt_roi(self, roi_shape: Rectangle) -> "Point": y1 = roi_shape.y1 return Point(x=self.x * width + x1, y=self.y * height + y1) - def denormalize_wrt_roi_shape(self, roi_shape: Rectangle): + def denormalize_wrt_roi_shape(self, roi_shape: Rectangle) -> "Point": """ The inverse of normalize_wrt_roi_shape. Transforming Polygon from the normalized coordinate system to the `roi` coordinate system. diff --git a/ote_sdk/ote_sdk/entities/shapes/rectangle.py b/ote_sdk/ote_sdk/entities/shapes/rectangle.py index 6ea82453dec..165c2f57142 100644 --- a/ote_sdk/ote_sdk/entities/shapes/rectangle.py +++ b/ote_sdk/ote_sdk/entities/shapes/rectangle.py @@ -101,7 +101,9 @@ def clip_to_visible_region(self) -> "Rectangle": x2 = min(max(0.0, self.x2), 1.0) y2 = min(max(0.0, self.y2), 1.0) - return Rectangle(x1, y1, x2, y2, self.modification_date) + return Rectangle( + x1=x1, y1=y1, x2=x2, y2=y2, modification_date=self.modification_date + ) def normalize_wrt_roi_shape(self, roi_shape: "Rectangle") -> "Rectangle": """ @@ -230,7 +232,7 @@ def is_full_box(rectangle: ShapeEntity) -> bool: return True return False - def crop_numpy_array(self, data: np.ndarray): + def crop_numpy_array(self, data: np.ndarray) -> np.ndarray: """ Crop the given Numpy array to the region of interest represented by this rectangle. @@ -252,7 +254,7 @@ def crop_numpy_array(self, data: np.ndarray): return data[y1:y2, x1:x2, ::] @property - def width(self): + def width(self) -> float: """ Returns the width of the rectangle. (x-axis) @@ -267,7 +269,7 @@ def width(self): return self.x2 - self.x1 @property - def height(self): + def height(self) -> float: """ Returns the height of the rectangle. (y-axis) @@ -282,7 +284,7 @@ def height(self): return self.y2 - self.y1 @property - def diagonal(self): + def diagonal(self) -> float: """ Returns the diagonal size/hypotenuse of the rectangle. (x-axis) diff --git a/ote_sdk/ote_sdk/entities/shapes/shape.py b/ote_sdk/ote_sdk/entities/shapes/shape.py index 600d8b606f2..860461929aa 100644 --- a/ote_sdk/ote_sdk/entities/shapes/shape.py +++ b/ote_sdk/ote_sdk/entities/shapes/shape.py @@ -40,7 +40,7 @@ def __init__(self, shape_type: ShapeType): self._type = shape_type @property - def type(self): + def type(self) -> ShapeType: """ Get the type of Shape that this Shape represents """ diff --git a/ote_sdk/ote_sdk/tests/entities/shapes/test_rectangle.py b/ote_sdk/ote_sdk/tests/entities/shapes/test_rectangle.py index c02366f3a7e..40c2dfb5795 100644 --- a/ote_sdk/ote_sdk/tests/entities/shapes/test_rectangle.py +++ b/ote_sdk/ote_sdk/tests/entities/shapes/test_rectangle.py @@ -118,13 +118,13 @@ def test_rectangle_optional_parameters(self): Instance of Rectangle class Expected results: - Test passes if Rectangle instance has expected labels and modification attributes specified during Rectangle + Test passes if Rectangle instance has expected modification attributes specified during Rectangle class object initiation with optional parameters Steps - 1. Compare default label Rectangle instance attribute with expected value + 1. Compare default Rectangle instance attribute with expected value 2. Check type of default modification_date Rectangle instance attribute - 3. Compare specified label Rectangle instance attribute with expected value + 3. Compare specified Rectangle instance attribute with expected value 4. Compare specified modification_date Rectangle instance attribute with expected value """ # Checking default values of optional parameters @@ -221,8 +221,7 @@ def test_rectangle_eq(self): 1. Check __eq__ method for instances of Rectangle class with equal parameters 2. Check __eq__ method for different instances of Rectangle class 3. Check __eq__ method for instances of different classes - 4. Check __eq__ method for instances of Rectangle class with unequal labels attribute - 5. Check __eq__ method for instances of Rectangle class with unequal x1, y1, x2, y2 and + 4. Check __eq__ method for instances of Rectangle class with unequal x1, y1, x2, y2 and modification_date attributes """ rectangle = self.vertical_rectangle() @@ -233,7 +232,7 @@ def test_rectangle_eq(self): assert rectangle != self.horizontal_rectangle() # Check for different types branch assert rectangle != str - # Check for unequal labels parameters. Expected that different labels are not affecting equality + assert rectangle == equal_rectangle # Check for instances with unequal parameters combinations # Generating all possible scenarios of parameter values submission @@ -458,7 +457,7 @@ def test_rectangle_generate_full_box(self): (x1=0.0, y1=0.0, x2=1.0, y2=1.0) Steps - 1. Check generate_full_box method for Rectangle instance with no labels specified + 1. Check generate_full_box method for Rectangle instance """ full_box = Rectangle.generate_full_box() assert full_box.type == ShapeType.RECTANGLE From 9e916f11ecef769231e31d6020ddaf830d67f5da Mon Sep 17 00:00:00 2001 From: AlbertvanHouten Date: Thu, 12 May 2022 09:48:12 +0200 Subject: [PATCH 06/11] reverted removing id from annotation.py --- ote_sdk/ote_sdk/entities/annotation.py | 37 ++++++++++++++++--- ote_sdk/ote_sdk/entities/datasets.py | 4 ++ .../ote_sdk/tests/entities/test_annotation.py | 7 +++- .../tests/entities/test_dataset_item.py | 12 ++++++ .../ote_sdk/tests/entities/test_datasets.py | 1 + .../tests/entities/test_result_media.py | 2 + .../ote_sdk/tests/utils/test_shape_drawer.py | 4 ++ ote_sdk/ote_sdk/utils/segmentation_utils.py | 3 ++ 8 files changed, 64 insertions(+), 6 deletions(-) diff --git a/ote_sdk/ote_sdk/entities/annotation.py b/ote_sdk/ote_sdk/entities/annotation.py index 8f2facdefed..0b8758a65eb 100644 --- a/ote_sdk/ote_sdk/entities/annotation.py +++ b/ote_sdk/ote_sdk/entities/annotation.py @@ -8,6 +8,8 @@ from enum import Enum from typing import Dict, List, Optional, Set +from bson import ObjectId + from ote_sdk.entities.id import ID from ote_sdk.entities.label import LabelEntity from ote_sdk.entities.scored_label import ScoredLabel @@ -21,7 +23,10 @@ class Annotation(metaclass=abc.ABCMeta): """ # pylint: disable=redefined-builtin; - def __init__(self, shape: ShapeEntity, labels: List[ScoredLabel]): + def __init__( + self, shape: ShapeEntity, labels: List[ScoredLabel], id: Optional[ID] = None + ): + self.__id_ = ID(ObjectId()) if id is None else id self.__shape = shape self.__labels = labels @@ -29,11 +34,33 @@ def __repr__(self): return ( f"{self.__class__.__name__}(" f"shape={self.shape}, " - f"labels={self.get_labels(include_empty=True)}" + f"labels={self.get_labels(include_empty=True)}, " + f"id={self.id_})" ) @property - def shape(self) -> ShapeEntity: + def id_(self): + """ + Returns the id for the annotation + """ + return self.__id_ + + @id_.setter + def id_(self, value): + self.__id_ = value + + @property + def id(self): + """DEPRECATED""" + return self.__id_ + + @id.setter + def id(self, value): + """DEPRECATED""" + self.__id_ = value + + @property + def shape(self): """ Returns the shape that is in the annotation """ @@ -86,8 +113,8 @@ def set_labels(self, labels: List[ScoredLabel]) -> None: def __eq__(self, other): if isinstance(other, Annotation): return ( - self.get_labels(include_empty=True) - == other.get_labels(include_empty=True) + self.id_ == other.id_ + and self.get_labels(True) == other.get_labels(True) and self.shape == other.shape ) return False diff --git a/ote_sdk/ote_sdk/entities/datasets.py b/ote_sdk/ote_sdk/entities/datasets.py index d64df0eb7e0..33f32bc58c0 100644 --- a/ote_sdk/ote_sdk/entities/datasets.py +++ b/ote_sdk/ote_sdk/entities/datasets.py @@ -12,8 +12,11 @@ from enum import Enum from typing import Iterator, List, Optional, Sequence, Union, overload +from bson.objectid import ObjectId + from ote_sdk.entities.annotation import AnnotationSceneEntity, AnnotationSceneKind from ote_sdk.entities.dataset_item import DatasetItemEntity +from ote_sdk.entities.id import ID from ote_sdk.entities.label import LabelEntity from ote_sdk.entities.subset import Subset @@ -274,6 +277,7 @@ def with_empty_annotations( # reset ROI roi = copy.copy(dataset_item.roi) + roi.id_ = ID(ObjectId()) roi.set_labels([]) new_dataset_item = DatasetItemEntity( diff --git a/ote_sdk/ote_sdk/tests/entities/test_annotation.py b/ote_sdk/ote_sdk/tests/entities/test_annotation.py index c1ebb3a8937..4d2177a23f8 100644 --- a/ote_sdk/ote_sdk/tests/entities/test_annotation.py +++ b/ote_sdk/ote_sdk/tests/entities/test_annotation.py @@ -81,6 +81,8 @@ def test_annotation_default_property(self): annotation = self.annotation + assert type(annotation.id_) == ID + assert annotation.id_ is not None assert str(annotation.shape) == "Rectangle(x=0.5, y=0.0, width=0.5, height=0.5)" assert annotation.get_labels() == [] @@ -107,6 +109,9 @@ def test_annotation_setters(self): annotation = self.annotation ellipse = Ellipse(x1=0.5, y1=0.1, x2=0.8, y2=0.3) annotation.shape = ellipse + annotation.id_ = ID(123456789) + + assert annotation.id_ == ID(123456789) assert annotation.shape == ellipse @pytest.mark.priority_medium @@ -139,7 +144,7 @@ def test_annotation_magic_methods(self): assert ( repr(annotation) - == "Annotation(shape=Ellipse(x1=0.5, y1=0.1, x2=0.8, y2=0.3), labels=[]" + == "Annotation(shape=Ellipse(x1=0.5, y1=0.1, x2=0.8, y2=0.3), labels=[], id=123456789)" ) assert annotation == other_annotation assert annotation != third_annotation diff --git a/ote_sdk/ote_sdk/tests/entities/test_dataset_item.py b/ote_sdk/ote_sdk/tests/entities/test_dataset_item.py index 6ac0e5ab1bd..71fd3ed1053 100644 --- a/ote_sdk/ote_sdk/tests/entities/test_dataset_item.py +++ b/ote_sdk/ote_sdk/tests/entities/test_dataset_item.py @@ -66,10 +66,12 @@ def annotations(self) -> List[Annotation]: detection_annotation = Annotation( shape=rectangle, labels=[ScoredLabel(label=labels[0])], + id=ID("detection_annotation_1"), ) segmentation_annotation = Annotation( shape=other_rectangle, labels=[ScoredLabel(label=labels[1])], + id=ID("segmentation_annotation_1"), ) return [detection_annotation, segmentation_annotation] @@ -107,6 +109,7 @@ def roi(self): modification_date=datetime.datetime(year=2021, month=12, day=9), ), labels=self.roi_scored_labels(), + id=ID("roi_annotation"), ) return roi @@ -158,6 +161,7 @@ def compare_denormalized_annotations( expected_annotation = expected_annotations[index] # Redefining id and modification_date required because of new Annotation objects created after shape # denormalize + actual_annotation.id_ = expected_annotation.id_ actual_annotation.shape.modification_date = ( expected_annotation.shape.modification_date ) @@ -187,10 +191,12 @@ def annotations_to_add(self) -> List[Annotation]: annotation_to_add = Annotation( shape=Rectangle(x1=0.1, y1=0.1, x2=0.7, y2=0.8), labels=[ScoredLabel(label=labels_to_add[0])], + id=ID("added_annotation_1"), ) other_annotation_to_add = Annotation( shape=Rectangle(x1=0.2, y1=0.3, x2=0.8, y2=0.9), labels=[ScoredLabel(label=labels_to_add[1])], + id=ID("added_annotation_2"), ) return [annotation_to_add, other_annotation_to_add] @@ -393,6 +399,7 @@ def test_dataset_item_roi_numpy(self): rectangle_roi = Annotation( Rectangle(x1=0.2, y1=0.1, x2=0.8, y2=0.9), [ScoredLabel(roi_label)], + ID("rectangle_roi"), ) assert np.array_equal( dataset_item.roi_numpy(rectangle_roi), media.numpy[1:9, 3:13] @@ -401,6 +408,7 @@ def test_dataset_item_roi_numpy(self): ellipse_roi = Annotation( Ellipse(x1=0.1, y1=0.0, x2=0.9, y2=0.8), [ScoredLabel(roi_label)], + ID("ellipse_roi"), ) assert np.array_equal( dataset_item.roi_numpy(ellipse_roi), media.numpy[0:8, 2:14] @@ -417,6 +425,7 @@ def test_dataset_item_roi_numpy(self): ] ), labels=[], + id=ID("polygon_roi"), ) assert np.array_equal( dataset_item.roi_numpy(polygon_roi), media.numpy[4:8, 5:13] @@ -616,6 +625,8 @@ def test_dataset_item_append_annotations(self): ) dataset_item.append_annotations(annotations_to_add) # Random id is generated for normalized annotations + normalized_annotations[0].id_ = dataset_item.annotation_scene.annotations[2].id_ + normalized_annotations[1].id_ = dataset_item.annotation_scene.annotations[3].id_ assert ( dataset_item.annotation_scene.annotations == full_box_annotations + normalized_annotations @@ -633,6 +644,7 @@ def test_dataset_item_append_annotations(self): incorrect_shape_annotation = Annotation( shape=incorrect_polygon, labels=[ScoredLabel(incorrect_shape_label)], + id=ID("incorrect_shape_annotation"), ) dataset_item.append_annotations([incorrect_shape_annotation]) assert ( diff --git a/ote_sdk/ote_sdk/tests/entities/test_datasets.py b/ote_sdk/ote_sdk/tests/entities/test_datasets.py index abed07c140d..c5c0522c2de 100644 --- a/ote_sdk/ote_sdk/tests/entities/test_datasets.py +++ b/ote_sdk/ote_sdk/tests/entities/test_datasets.py @@ -454,6 +454,7 @@ def check_empty_annotations_dataset( assert actual_item.media is expected_item.media assert actual_item.annotation_scene.annotations == [] assert actual_item.annotation_scene.kind == expected_kind + assert actual_item.roi.id_ != expected_item.roi.id_ assert actual_item.roi.shape is expected_item.roi.shape assert actual_item.roi.get_labels() == [] assert actual_item.subset is expected_item.subset diff --git a/ote_sdk/ote_sdk/tests/entities/test_result_media.py b/ote_sdk/ote_sdk/tests/entities/test_result_media.py index 73950605afb..2bddc58ccbf 100644 --- a/ote_sdk/ote_sdk/tests/entities/test_result_media.py +++ b/ote_sdk/ote_sdk/tests/entities/test_result_media.py @@ -38,6 +38,7 @@ def default_result_media_parameters() -> dict: rectangle_annotation = Annotation( shape=Rectangle(x1=0.1, y1=0.4, x2=0.4, y2=0.9), labels=[ScoredLabel(rectangle_label)], + id=ID("rectangle_annotation"), ) annotation_scene = AnnotationSceneEntity( annotations=[rectangle_annotation], @@ -64,6 +65,7 @@ def optional_result_media_parameters(self) -> dict: roi = Annotation( shape=Rectangle(x1=0.3, y1=0.2, x2=0.7, y2=0.6), labels=[ScoredLabel(roi_label)], + id=ID("roi_annotation"), ) result_media_label = LabelEntity( "ResultMedia label", diff --git a/ote_sdk/ote_sdk/tests/utils/test_shape_drawer.py b/ote_sdk/ote_sdk/tests/utils/test_shape_drawer.py index 222f0e77cc0..7d55c7d980c 100644 --- a/ote_sdk/ote_sdk/tests/utils/test_shape_drawer.py +++ b/ote_sdk/ote_sdk/tests/utils/test_shape_drawer.py @@ -600,6 +600,7 @@ def full_rectangle_annotation(self) -> Annotation: return Annotation( shape=Rectangle(x1=0, y1=0, x2=1, y2=1), labels=self.full_rectangle_scored_labels(), + id=ID("full_rectangle_annotation"), ) @staticmethod @@ -628,6 +629,7 @@ def rectangle_annotation(self) -> Annotation: return Annotation( shape=Rectangle(x1=0.1, y1=0.4, x2=0.4, y2=0.9), labels=self.rectangle_scored_labels(), + id=ID("rectangle_annotation"), ) @staticmethod @@ -664,6 +666,7 @@ def polygon_annotation(self) -> Annotation: ] ), labels=self.polygon_scored_labels(), + id=ID("polygon_annotation"), ) @staticmethod @@ -692,6 +695,7 @@ def ellipse_annotation(self) -> Annotation: return Annotation( shape=Ellipse(x1=0.5, y1=0.0, x2=1.0, y2=0.5), labels=self.ellipse_scored_labels(), + id=ID("ellipse_annotation"), ) def annotation_scene(self) -> AnnotationSceneEntity: diff --git a/ote_sdk/ote_sdk/utils/segmentation_utils.py b/ote_sdk/ote_sdk/utils/segmentation_utils.py index e19b8d698c0..3e1c1111fe5 100644 --- a/ote_sdk/ote_sdk/utils/segmentation_utils.py +++ b/ote_sdk/ote_sdk/utils/segmentation_utils.py @@ -12,9 +12,11 @@ import cv2 import numpy as np +from bson import ObjectId from ote_sdk.entities.annotation import Annotation from ote_sdk.entities.dataset_item import DatasetItemEntity +from ote_sdk.entities.id import ID from ote_sdk.entities.label import LabelEntity from ote_sdk.entities.scored_label import ScoredLabel from ote_sdk.entities.shapes.polygon import Point, Polygon @@ -252,6 +254,7 @@ def create_annotation_from_segmentation_map( Annotation( shape=polygon, labels=[ScoredLabel(label, probability)], + id=ID(ObjectId()), ) ) else: From 6e534c6b3c3c6f68044329649c29babe9bc1cc8a Mon Sep 17 00:00:00 2001 From: AlbertvanHouten Date: Thu, 12 May 2022 09:52:05 +0200 Subject: [PATCH 07/11] compare shape instead of annotation for appending labels --- ote_sdk/ote_sdk/entities/dataset_item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ote_sdk/ote_sdk/entities/dataset_item.py b/ote_sdk/ote_sdk/entities/dataset_item.py index f18d1f22476..a37b48c9dfe 100644 --- a/ote_sdk/ote_sdk/entities/dataset_item.py +++ b/ote_sdk/ote_sdk/entities/dataset_item.py @@ -401,7 +401,7 @@ def append_labels(self, labels: List[ScoredLabel]): roi_annotation = None for annotation in self.annotation_scene.annotations: - if annotation == self.roi: + if annotation.shape == self.roi.shape: roi_annotation = annotation break From 496cd9b6716545ed00428920263cf3fafe1537be Mon Sep 17 00:00:00 2001 From: Eugene Liu Date: Fri, 13 May 2022 15:00:19 +0100 Subject: [PATCH 08/11] remove dist/build --- .../build/lib/detection_tasks/__init__.py | 0 .../lib/detection_tasks/apis/__init__.py | 0 .../apis/detection/__init__.py | 42 -- .../apis/detection/config_utils.py | 339 ------------ .../apis/detection/configuration.py | 173 ------ .../apis/detection/configuration_enums.py | 22 - .../apis/detection/inference_task.py | 408 -------------- .../apis/detection/model_wrappers/__init__.py | 15 - .../apis/detection/nncf_task.py | 283 ---------- .../apis/detection/openvino_task.py | 367 ------------- .../apis/detection/ote_utils.py | 167 ------ .../apis/detection/train_task.py | 224 -------- .../lib/detection_tasks/extension/__init__.py | 17 - .../extension/datasets/__init__.py | 18 - .../extension/datasets/data_utils.py | 390 -------------- .../extension/datasets/mmdataset.py | 214 -------- .../extension/utils/__init__.py | 22 - .../detection_tasks/extension/utils/hooks.py | 503 ------------------ .../extension/utils/pipelines.py | 120 ----- .../detection_tasks/extension/utils/runner.py | 127 ----- .../dist/detection_tasks-0.0.0-py3.8.egg | Bin 108797 -> 0 bytes 21 files changed, 3451 deletions(-) delete mode 100644 external/mmdetection/build/lib/detection_tasks/__init__.py delete mode 100644 external/mmdetection/build/lib/detection_tasks/apis/__init__.py delete mode 100644 external/mmdetection/build/lib/detection_tasks/apis/detection/__init__.py delete mode 100644 external/mmdetection/build/lib/detection_tasks/apis/detection/config_utils.py delete mode 100644 external/mmdetection/build/lib/detection_tasks/apis/detection/configuration.py delete mode 100644 external/mmdetection/build/lib/detection_tasks/apis/detection/configuration_enums.py delete mode 100644 external/mmdetection/build/lib/detection_tasks/apis/detection/inference_task.py delete mode 100644 external/mmdetection/build/lib/detection_tasks/apis/detection/model_wrappers/__init__.py delete mode 100644 external/mmdetection/build/lib/detection_tasks/apis/detection/nncf_task.py delete mode 100644 external/mmdetection/build/lib/detection_tasks/apis/detection/openvino_task.py delete mode 100644 external/mmdetection/build/lib/detection_tasks/apis/detection/ote_utils.py delete mode 100644 external/mmdetection/build/lib/detection_tasks/apis/detection/train_task.py delete mode 100644 external/mmdetection/build/lib/detection_tasks/extension/__init__.py delete mode 100644 external/mmdetection/build/lib/detection_tasks/extension/datasets/__init__.py delete mode 100644 external/mmdetection/build/lib/detection_tasks/extension/datasets/data_utils.py delete mode 100644 external/mmdetection/build/lib/detection_tasks/extension/datasets/mmdataset.py delete mode 100644 external/mmdetection/build/lib/detection_tasks/extension/utils/__init__.py delete mode 100644 external/mmdetection/build/lib/detection_tasks/extension/utils/hooks.py delete mode 100644 external/mmdetection/build/lib/detection_tasks/extension/utils/pipelines.py delete mode 100644 external/mmdetection/build/lib/detection_tasks/extension/utils/runner.py delete mode 100644 external/mmdetection/dist/detection_tasks-0.0.0-py3.8.egg diff --git a/external/mmdetection/build/lib/detection_tasks/__init__.py b/external/mmdetection/build/lib/detection_tasks/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/external/mmdetection/build/lib/detection_tasks/apis/__init__.py b/external/mmdetection/build/lib/detection_tasks/apis/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/external/mmdetection/build/lib/detection_tasks/apis/detection/__init__.py b/external/mmdetection/build/lib/detection_tasks/apis/detection/__init__.py deleted file mode 100644 index 03c0dce1f69..00000000000 --- a/external/mmdetection/build/lib/detection_tasks/apis/detection/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright (C) 2021 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. - - -from .config_utils import (config_from_string, config_to_string, patch_config, - prepare_for_testing, prepare_for_training, - save_config_to_file, set_hyperparams) -from .configuration import OTEDetectionConfig -from .inference_task import OTEDetectionInferenceTask -from .nncf_task import OTEDetectionNNCFTask -from .openvino_task import OpenVINODetectionTask -from .ote_utils import generate_label_schema, get_task_class, load_template -from .train_task import OTEDetectionTrainingTask - -__all__ = [ - config_from_string, - config_to_string, - generate_label_schema, - get_task_class, - load_template, - OpenVINODetectionTask, - OTEDetectionConfig, - OTEDetectionInferenceTask, - OTEDetectionNNCFTask, - OTEDetectionTrainingTask, - patch_config, - prepare_for_testing, - prepare_for_training, - save_config_to_file, - set_hyperparams, - ] diff --git a/external/mmdetection/build/lib/detection_tasks/apis/detection/config_utils.py b/external/mmdetection/build/lib/detection_tasks/apis/detection/config_utils.py deleted file mode 100644 index 57eabc94bf2..00000000000 --- a/external/mmdetection/build/lib/detection_tasks/apis/detection/config_utils.py +++ /dev/null @@ -1,339 +0,0 @@ -# Copyright (C) 2021 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. - -import copy -import glob -import math -import os -import tempfile -from collections import defaultdict -from typing import List, Optional - -import torch -from mmcv import Config, ConfigDict -from ote_sdk.entities.datasets import DatasetEntity -from ote_sdk.entities.label import LabelEntity, Domain -from ote_sdk.usecases.reporting.time_monitor_callback import TimeMonitorCallback - -from detection_tasks.extension.datasets.data_utils import get_anchor_boxes, \ - get_sizes_from_dataset_entity, format_list_to_str -from mmdet.models.detectors import BaseDetector -from mmdet.utils.logger import get_root_logger - -from .configuration import OTEDetectionConfig - -try: - from sklearn.cluster import KMeans - kmeans_import = True -except ImportError: - kmeans_import = False - - -logger = get_root_logger() - - -def is_epoch_based_runner(runner_config: ConfigDict): - return 'Epoch' in runner_config.type - - -def patch_config(config: Config, work_dir: str, labels: List[LabelEntity], domain: Domain, random_seed: Optional[int] = None): - # Set runner if not defined. - if 'runner' not in config: - config.runner = {'type': 'EpochBasedRunner'} - - # Check that there is no conflict in specification of number of training epochs. - # Move global definition of epochs inside runner config. - if 'total_epochs' in config: - if is_epoch_based_runner(config.runner): - if config.runner.max_epochs != config.total_epochs: - logger.warning('Conflicting declaration of training epochs number.') - config.runner.max_epochs = config.total_epochs - else: - logger.warning(f'Total number of epochs set for an iteration based runner {config.runner.type}.') - remove_from_config(config, 'total_epochs') - - # Change runner's type. - if is_epoch_based_runner(config.runner): - logger.info(f'Replacing runner from {config.runner.type} to EpochRunnerWithCancel.') - config.runner.type = 'EpochRunnerWithCancel' - else: - logger.info(f'Replacing runner from {config.runner.type} to IterBasedRunnerWithCancel.') - config.runner.type = 'IterBasedRunnerWithCancel' - - # Add training cancelation hook. - if 'custom_hooks' not in config: - config.custom_hooks = [] - if 'CancelTrainingHook' not in {hook.type for hook in config.custom_hooks}: - config.custom_hooks.append({'type': 'CancelTrainingHook'}) - - # Remove high level data pipelines definition leaving them only inside `data` section. - remove_from_config(config, 'train_pipeline') - remove_from_config(config, 'test_pipeline') - - # Patch data pipeline, making it OTE-compatible. - patch_datasets(config, domain) - - # Remove FP16 config if running on CPU device and revert to FP32 - # https://github.com/pytorch/pytorch/issues/23377 - if not torch.cuda.is_available() and 'fp16' in config: - logger.info(f'Revert FP16 to FP32 on CPU device') - remove_from_config(config, 'fp16') - - if 'log_config' not in config: - config.log_config = ConfigDict() - # config.log_config.hooks = [] - - if 'evaluation' not in config: - config.evaluation = ConfigDict() - evaluation_metric = config.evaluation.get('metric') - if evaluation_metric is not None: - config.evaluation.save_best = evaluation_metric - - if 'checkpoint_config' not in config: - config.checkpoint_config = ConfigDict() - config.checkpoint_config.max_keep_ckpts = 5 - config.checkpoint_config.interval = config.evaluation.get('interval', 1) - - set_data_classes(config, labels) - - config.gpu_ids = range(1) - config.work_dir = work_dir - config.seed = random_seed - - -def set_hyperparams(config: Config, hyperparams: OTEDetectionConfig): - config.optimizer.lr = float(hyperparams.learning_parameters.learning_rate) - config.lr_config.warmup_iters = int(hyperparams.learning_parameters.learning_rate_warmup_iters) - if config.lr_config.warmup_iters == 0: - config.lr_config.warmup = None - config.data.samples_per_gpu = int(hyperparams.learning_parameters.batch_size) - config.data.workers_per_gpu = int(hyperparams.learning_parameters.num_workers) - total_iterations = int(hyperparams.learning_parameters.num_iters) - if is_epoch_based_runner(config.runner): - config.runner.max_epochs = total_iterations - else: - config.runner.max_iters = total_iterations - - -def patch_adaptive_repeat_dataset(config: Config, num_samples: int, - decay: float = -0.002, factor: float = 30): - """ Patch the repeat times and training epochs adatively - - Frequent dataloading inits and evaluation slow down training when the - sample size is small. Adjusting epoch and dataset repetition based on - empirical exponential decay improves the training time by applying high - repeat value to small sample size dataset and low repeat value to large - sample. - - :param config: mmcv config - :param num_samples: number of training samples - :param decay: decaying rate - :param factor: base repeat factor - """ - data_train = config.data.train - if data_train.type == 'MultiImageMixDataset': - data_train = data_train.dataset - if data_train.type == 'RepeatDataset' and getattr(data_train, 'adaptive_repeat_times', False): - if is_epoch_based_runner(config.runner): - cur_epoch = config.runner.max_epochs - new_repeat = max(round(math.exp(decay * num_samples) * factor), 1) - new_epoch = math.ceil(cur_epoch / new_repeat) - if new_epoch == 1: - return - config.runner.max_epochs = new_epoch - data_train.times = new_repeat - - -def prepare_for_testing(config: Config, dataset: DatasetEntity) -> Config: - config = copy.deepcopy(config) - # FIXME. Should working directories be modified here? - config.data.test.ote_dataset = dataset - return config - - -def prepare_for_training(config: Config, train_dataset: DatasetEntity, val_dataset: DatasetEntity, - time_monitor: TimeMonitorCallback, learning_curves: defaultdict) -> Config: - config = copy.deepcopy(config) - prepare_work_dir(config) - data_train = get_data_cfg(config) - data_train.ote_dataset = train_dataset - config.data.val.ote_dataset = val_dataset - patch_adaptive_repeat_dataset(config, len(train_dataset)) - config.custom_hooks.append({'type': 'OTEProgressHook', 'time_monitor': time_monitor, 'verbose': True}) - config.log_config.hooks.append({'type': 'OTELoggerHook', 'curves': learning_curves}) - return config - - -def config_to_string(config: Config) -> str: - """ - Convert a full mmdetection config to a string. - - :param config: configuration object to convert - :return str: string representation of the configuration - """ - config_copy = copy.deepcopy(config) - # Clean config up by removing dataset as this causes the pretty text parsing to fail. - config_copy.data.test.ote_dataset = None - config_copy.data.test.labels = None - config_copy.data.val.ote_dataset = None - config_copy.data.val.labels = None - data_train = get_data_cfg(config_copy) - data_train.ote_dataset = None - data_train.labels = None - return Config(config_copy).pretty_text - - -def config_from_string(config_string: str) -> Config: - """ - Generate an mmdetection config dict object from a string. - - :param config_string: string to parse - :return config: configuration object - """ - with tempfile.NamedTemporaryFile('w', suffix='.py') as temp_file: - temp_file.write(config_string) - temp_file.flush() - return Config.fromfile(temp_file.name) - - -def save_config_to_file(config: Config): - """ Dump the full config to a file. Filename is 'config.py', it is saved in the current work_dir. """ - filepath = os.path.join(config.work_dir, 'config.py') - config_string = config_to_string(config) - with open(filepath, 'w') as f: - f.write(config_string) - - -def prepare_work_dir(config: Config) -> str: - base_work_dir = config.work_dir - checkpoint_dirs = glob.glob(os.path.join(base_work_dir, "checkpoints_round_*")) - train_round_checkpoint_dir = os.path.join(base_work_dir, f"checkpoints_round_{len(checkpoint_dirs)}") - os.makedirs(train_round_checkpoint_dir) - logger.info(f"Checkpoints and logs for this training run are stored in {train_round_checkpoint_dir}") - config.work_dir = train_round_checkpoint_dir - if 'meta' not in config.runner: - config.runner.meta = ConfigDict() - config.runner.meta.exp_name = f"train_round_{len(checkpoint_dirs)}" - # Save training config for debugging. It is saved in the checkpoint dir for this training round. - # save_config_to_file(config) - return train_round_checkpoint_dir - - -def set_data_classes(config: Config, labels: List[LabelEntity]): - # Save labels in data configs. - for subset in ('train', 'val', 'test'): - cfg = get_data_cfg(config, subset) - cfg.labels = labels - config.data[subset].labels = labels - - # Set proper number of classes in model's detection heads. - head_names = ('mask_head', 'bbox_head', 'segm_head') - num_classes = len(labels) - if 'roi_head' in config.model: - for head_name in head_names: - if head_name in config.model.roi_head: - if isinstance(config.model.roi_head[head_name], List): - for head in config.model.roi_head[head_name]: - head.num_classes = num_classes - else: - config.model.roi_head[head_name].num_classes = num_classes - else: - for head_name in head_names: - if head_name in config.model: - config.model[head_name].num_classes = num_classes - # FIXME. ? - # self.config.model.CLASSES = label_names - - -def patch_datasets(config: Config, domain): - - def patch_color_conversion(pipeline): - # Default data format for OTE is RGB, while mmdet uses BGR, so negate the color conversion flag. - for pipeline_step in pipeline: - if pipeline_step.type == 'Normalize': - to_rgb = False - if 'to_rgb' in pipeline_step: - to_rgb = pipeline_step.to_rgb - to_rgb = not bool(to_rgb) - pipeline_step.to_rgb = to_rgb - elif pipeline_step.type == 'MultiScaleFlipAug': - patch_color_conversion(pipeline_step.transforms) - - assert 'data' in config - for subset in ('train', 'val', 'test'): - cfg = get_data_cfg(config, subset) - cfg.type = 'OTEDataset' - cfg.domain = domain - cfg.ote_dataset = None - cfg.labels = None - remove_from_config(cfg, 'ann_file') - remove_from_config(cfg, 'img_prefix') - for pipeline_step in cfg.pipeline: - if pipeline_step.type == 'LoadImageFromFile': - pipeline_step.type = 'LoadImageFromOTEDataset' - if pipeline_step.type == 'LoadAnnotations': - pipeline_step.type = 'LoadAnnotationFromOTEDataset' - pipeline_step.domain = domain - pipeline_step.min_size = cfg.pop('min_size', -1) - patch_color_conversion(cfg.pipeline) - - -def remove_from_config(config, key: str): - if key in config: - if isinstance(config, Config): - del config._cfg_dict[key] - elif isinstance(config, ConfigDict): - del config[key] - else: - raise ValueError(f'Unknown config type {type(config)}') - -def cluster_anchors(config: Config, dataset: DatasetEntity, model: BaseDetector): - if not kmeans_import: - raise ImportError('Sklearn package is not installed. To enable anchor boxes clustering, please install ' - 'packages from requirements/optional.txt or just scikit-learn package.') - - logger.info('Collecting statistics from training dataset to cluster anchor boxes...') - [target_wh] = [transforms.img_scale for transforms in config.data.test.pipeline - if transforms.type == 'MultiScaleFlipAug'] - prev_generator = config.model.bbox_head.anchor_generator - group_as = [len(width) for width in prev_generator.widths] - wh_stats = get_sizes_from_dataset_entity(dataset, target_wh) - - if len(wh_stats) < sum(group_as): - logger.warning(f'There are not enough objects to cluster: {len(wh_stats)} were detected, while it should be ' - f'at least {sum(group_as)}. Anchor box clustering was skipped.') - return config, model - - widths, heights = get_anchor_boxes(wh_stats, group_as) - logger.info(f'Anchor boxes widths have been updated from {format_list_to_str(prev_generator.widths)} ' - f'to {format_list_to_str(widths)}') - logger.info(f'Anchor boxes heights have been updated from {format_list_to_str(prev_generator.heights)} ' - f'to {format_list_to_str(heights)}') - config_generator = config.model.bbox_head.anchor_generator - config_generator.widths, config_generator.heights = widths, heights - - model_generator = model.bbox_head.anchor_generator - model_generator.widths, model_generator.heights = widths, heights - model_generator.base_anchors = model_generator.gen_base_anchors() - - config.model.bbox_head.anchor_generator = config_generator - model.bbox_head.anchor_generator = model_generator - return config, model - - -def get_data_cfg(config: Config, subset: str = 'train') -> Config: - data_cfg = config.data[subset] - while 'dataset' in data_cfg: - data_cfg = data_cfg.dataset - return data_cfg diff --git a/external/mmdetection/build/lib/detection_tasks/apis/detection/configuration.py b/external/mmdetection/build/lib/detection_tasks/apis/detection/configuration.py deleted file mode 100644 index de8ba9f8eb5..00000000000 --- a/external/mmdetection/build/lib/detection_tasks/apis/detection/configuration.py +++ /dev/null @@ -1,173 +0,0 @@ -# Copyright (C) 2021 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. - -from attr import attrs -from sys import maxsize - -from ote_sdk.configuration.elements import (ParameterGroup, - add_parameter_group, - boolean_attribute, - configurable_boolean, - configurable_float, - configurable_integer, - selectable, - string_attribute) -from ote_sdk.configuration import ConfigurableParameters -from ote_sdk.configuration.enums import ModelLifecycle, AutoHPOState - -from .configuration_enums import POTQuantizationPreset - - -@attrs -class OTEDetectionConfig(ConfigurableParameters): - header = string_attribute("Configuration for an object detection task") - description = header - - @attrs - class __LearningParameters(ParameterGroup): - header = string_attribute("Learning Parameters") - description = header - - batch_size = configurable_integer( - default_value=5, - min_value=1, - max_value=512, - header="Batch size", - description="The number of training samples seen in each iteration of training. Increasing this value " - "improves training time and may make the training more stable. A larger batch size has higher " - "memory requirements.", - warning="Increasing this value may cause the system to use more memory than available, " - "potentially causing out of memory errors, please update with caution.", - affects_outcome_of=ModelLifecycle.TRAINING, - auto_hpo_state=AutoHPOState.POSSIBLE - ) - - num_iters = configurable_integer( - default_value=1, - min_value=1, - max_value=100000, - header="Number of training iterations", - description="Increasing this value causes the results to be more robust but training time will be longer.", - affects_outcome_of=ModelLifecycle.TRAINING - ) - - learning_rate = configurable_float( - default_value=0.01, - min_value=1e-07, - max_value=1e-01, - header="Learning rate", - description="Increasing this value will speed up training convergence but might make it unstable.", - affects_outcome_of=ModelLifecycle.TRAINING, - auto_hpo_state=AutoHPOState.POSSIBLE - ) - - learning_rate_warmup_iters = configurable_integer( - default_value=100, - min_value=0, - max_value=10000, - header="Number of iterations for learning rate warmup", - description="", - affects_outcome_of=ModelLifecycle.TRAINING - ) - - num_workers = configurable_integer( - default_value=4, - min_value=0, - max_value=36, - header="Number of cpu threads to use during batch generation", - description="Increasing this value might improve training speed however it might cause out of memory " - "errors. If the number of workers is set to zero, data loading will happen in the main " - "training thread.", - affects_outcome_of=ModelLifecycle.NONE - ) - - @attrs - class __Postprocessing(ParameterGroup): - header = string_attribute("Postprocessing") - description = header - - result_based_confidence_threshold = configurable_boolean( - default_value=True, - header="Result based confidence threshold", - description="Confidence threshold is derived from the results", - affects_outcome_of=ModelLifecycle.INFERENCE - ) - - confidence_threshold = configurable_float( - default_value=0.35, - min_value=0, - max_value=1, - header="Confidence threshold", - description="This threshold only takes effect if the threshold is not set based on the result.", - affects_outcome_of=ModelLifecycle.INFERENCE - ) - - @attrs - class __NNCFOptimization(ParameterGroup): - header = string_attribute("Optimization by NNCF") - description = header - visible_in_ui = boolean_attribute(False) - - enable_quantization = configurable_boolean( - default_value=True, - header="Enable quantization algorithm", - description="Enable quantization algorithm", - affects_outcome_of=ModelLifecycle.TRAINING - ) - - enable_pruning = configurable_boolean( - default_value=False, - header="Enable filter pruning algorithm", - description="Enable filter pruning algorithm", - affects_outcome_of=ModelLifecycle.TRAINING - ) - - pruning_supported = configurable_boolean( - default_value=False, - header="Whether filter pruning is supported", - description="Whether filter pruning is supported", - affects_outcome_of=ModelLifecycle.TRAINING - ) - - maximal_accuracy_degradation = configurable_float( - default_value=1.0, - min_value=0.0, - max_value=100.0, - header="Maximum accuracy degradation", - description="The maximal allowed accuracy metric drop", - affects_outcome_of=ModelLifecycle.TRAINING - ) - - @attrs - class __POTParameter(ParameterGroup): - header = string_attribute("POT Parameters") - description = header - visible_in_ui = boolean_attribute(False) - - stat_subset_size = configurable_integer( - header="Number of data samples", - description="Number of data samples used for post-training optimization", - default_value=300, - min_value=1, - max_value=maxsize - ) - - preset = selectable(default_value=POTQuantizationPreset.PERFORMANCE, header="Preset", - description="Quantization preset that defines quantization scheme", - editable=True, visible_in_ui=True) - - learning_parameters = add_parameter_group(__LearningParameters) - postprocessing = add_parameter_group(__Postprocessing) - nncf_optimization = add_parameter_group(__NNCFOptimization) - pot_parameters = add_parameter_group(__POTParameter) diff --git a/external/mmdetection/build/lib/detection_tasks/apis/detection/configuration_enums.py b/external/mmdetection/build/lib/detection_tasks/apis/detection/configuration_enums.py deleted file mode 100644 index 067e193a374..00000000000 --- a/external/mmdetection/build/lib/detection_tasks/apis/detection/configuration_enums.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (C) 2021 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. - -from ote_sdk.configuration import ConfigurableEnum - -class POTQuantizationPreset(ConfigurableEnum): - """ - This Enum represents the quantization preset for post training optimization - """ - PERFORMANCE = 'Performance' - MIXED = 'Mixed' diff --git a/external/mmdetection/build/lib/detection_tasks/apis/detection/inference_task.py b/external/mmdetection/build/lib/detection_tasks/apis/detection/inference_task.py deleted file mode 100644 index 667fcca55bb..00000000000 --- a/external/mmdetection/build/lib/detection_tasks/apis/detection/inference_task.py +++ /dev/null @@ -1,408 +0,0 @@ -# Copyright (C) 2021 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. - -import copy -import io -import os -import shutil -import tempfile -import warnings -from subprocess import run # nosec -from typing import List, Optional, Tuple - -import cv2 -import numpy as np -import torch -from mmcv.parallel import MMDataParallel -from mmcv.runner import load_checkpoint, load_state_dict -from mmcv.utils import Config -from ote_sdk.entities.annotation import Annotation -from ote_sdk.entities.datasets import DatasetEntity -from ote_sdk.entities.id import ID -from ote_sdk.entities.inference_parameters import InferenceParameters, default_progress_callback -from ote_sdk.entities.model import ModelEntity, ModelFormat, ModelOptimizationType, ModelPrecision, OptimizationMethod -from ote_sdk.entities.model_template import TaskType, task_type_to_label_domain -from ote_sdk.entities.resultset import ResultSetEntity -from ote_sdk.entities.scored_label import ScoredLabel -from ote_sdk.entities.shapes.polygon import Point, Polygon -from ote_sdk.entities.shapes.rectangle import Rectangle -from ote_sdk.entities.task_environment import TaskEnvironment -from ote_sdk.entities.tensor import TensorEntity -from ote_sdk.usecases.evaluation.metrics_helper import MetricsHelper -from ote_sdk.usecases.tasks.interfaces.evaluate_interface import IEvaluationTask -from ote_sdk.usecases.tasks.interfaces.export_interface import ExportType, IExportTask -from ote_sdk.usecases.tasks.interfaces.inference_interface import IInferenceTask -from ote_sdk.usecases.tasks.interfaces.unload_interface import IUnload -from ote_sdk.serialization.label_mapper import label_schema_to_bytes - -from mmdet.apis import export_model -from detection_tasks.apis.detection.config_utils import patch_config, prepare_for_testing, set_hyperparams -from detection_tasks.apis.detection.configuration import OTEDetectionConfig -from detection_tasks.apis.detection.ote_utils import InferenceProgressCallback -from mmdet.datasets import build_dataloader, build_dataset -from mmdet.models import build_detector -from mmdet.parallel import MMDataCPU -from mmdet.utils.collect_env import collect_env -from mmdet.utils.logger import get_root_logger - -logger = get_root_logger() - - -class OTEDetectionInferenceTask(IInferenceTask, IExportTask, IEvaluationTask, IUnload): - - _task_environment: TaskEnvironment - - def __init__(self, task_environment: TaskEnvironment): - """" - Task for inference object detection models using OTEDetection. - """ - logger.info('Loading OTEDetectionTask') - - print('ENVIRONMENT:') - for name, val in collect_env().items(): - print(f'{name}: {val}') - print('pip list:') - run('pip list', shell=True, check=True) - - self._task_environment = task_environment - self._task_type = task_environment.model_template.task_type - self._scratch_space = tempfile.mkdtemp(prefix="ote-det-scratch-") - logger.info(f'Scratch space created at {self._scratch_space}') - - self._model_name = task_environment.model_template.name - self._labels = task_environment.get_labels(False) - - template_file_path = task_environment.model_template.model_template_path - - # Get and prepare mmdet config. - self._base_dir = os.path.abspath(os.path.dirname(template_file_path)) - config_file_path = os.path.join(self._base_dir, "model.py") - self._config = Config.fromfile(config_file_path) - patch_config(self._config, self._scratch_space, self._labels, task_type_to_label_domain(self._task_type), random_seed=42) - set_hyperparams(self._config, self._hyperparams) - self.confidence_threshold: float = self._hyperparams.postprocessing.confidence_threshold - - # Set default model attributes. - self._optimization_methods = [] - self._precision = [ModelPrecision.FP32] - self._optimization_type = ModelOptimizationType.MO - - # Create and initialize PyTorch model. - logger.info('Loading the model') - self._model = self._load_model(task_environment.model) - - # Extra control variables. - self._training_work_dir = None - self._is_training = False - self._should_stop = False - logger.info('Task initialization completed') - - @property - def _hyperparams(self): - return self._task_environment.get_hyper_parameters(OTEDetectionConfig) - - def _load_model(self, model: ModelEntity): - if model is not None: - # If a model has been trained and saved for the task already, create empty model and load weights here - buffer = io.BytesIO(model.get_data("weights.pth")) - model_data = torch.load(buffer, map_location=torch.device('cpu')) - - self.confidence_threshold = model_data.get('confidence_threshold', self.confidence_threshold) - if model_data.get('anchors'): - anchors = model_data['anchors'] - self._config.model.bbox_head.anchor_generator.heights = anchors['heights'] - self._config.model.bbox_head.anchor_generator.widths = anchors['widths'] - - model = self._create_model(self._config, from_scratch=True) - - try: - load_state_dict(model, model_data['model']) - logger.info(f"Loaded model weights from Task Environment") - logger.info(f"Model architecture: {self._model_name}") - except BaseException as ex: - raise ValueError("Could not load the saved model. The model file structure is invalid.") \ - from ex - else: - # If there is no trained model yet, create model with pretrained weights as defined in the model config - # file. - model = self._create_model(self._config, from_scratch=False) - logger.info(f"No trained model in project yet. Created new model with '{self._model_name}' " - f"architecture and general-purpose pretrained weights.") - return model - - - @staticmethod - def _create_model(config: Config, from_scratch: bool = False): - """ - Creates a model, based on the configuration in config - - :param config: mmdetection configuration from which the model has to be built - :param from_scratch: bool, if True does not load any weights - - :return model: ModelEntity in training mode - """ - model_cfg = copy.deepcopy(config.model) - - init_from = None if from_scratch else config.get('load_from', None) - logger.warning(init_from) - if init_from is not None: - # No need to initialize backbone separately, if all weights are provided. - model_cfg.pretrained = None - logger.warning('build detector') - model = build_detector(model_cfg) - # Load all weights. - logger.warning('load checkpoint') - load_checkpoint(model, init_from, map_location='cpu') - else: - logger.warning('build detector') - model = build_detector(model_cfg) - return model - - - def _add_predictions_to_dataset(self, prediction_results, dataset, confidence_threshold=0.0): - """ Loop over dataset again to assign predictions. Convert from MMDetection format to OTE format. """ - for dataset_item, (all_results, feature_vector) in zip(dataset, prediction_results): - width = dataset_item.width - height = dataset_item.height - - shapes = [] - if self._task_type == TaskType.DETECTION: - for label_idx, detections in enumerate(all_results): - for i in range(detections.shape[0]): - probability = float(detections[i, 4]) - coords = detections[i, :4].astype(float).copy() - coords /= np.array([width, height, width, height], dtype=float) - coords = np.clip(coords, 0, 1) - - if probability < confidence_threshold: - continue - - assigned_label = [ScoredLabel(self._labels[label_idx], - probability=probability)] - if coords[3] - coords[1] <= 0 or coords[2] - coords[0] <= 0: - continue - - shapes.append(Annotation( - Rectangle(x1=coords[0], y1=coords[1], x2=coords[2], y2=coords[3]), - labels=assigned_label)) - elif self._task_type in {TaskType.INSTANCE_SEGMENTATION, TaskType.ROTATED_DETECTION}: - for label_idx, (boxes, masks) in enumerate(zip(*all_results)): - for mask, probability in zip(masks, boxes[:, 4]): - mask = mask.astype(np.uint8) - probability = float(probability) - contours, hierarchies = cv2.findContours(mask, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) - if hierarchies is None: - continue - for contour, hierarchy in zip(contours, hierarchies[0]): - if hierarchy[3] != -1: - continue - if len(contour) <= 2 or probability < confidence_threshold: - continue - if self._task_type == TaskType.INSTANCE_SEGMENTATION: - points = [Point(x=point[0][0] / width, y=point[0][1] / height) for point in contour] - else: - box_points = cv2.boxPoints(cv2.minAreaRect(contour)) - points = [Point(x=point[0] / width, y=point[1] / height) for point in box_points] - labels = [ScoredLabel(self._labels[label_idx], probability=probability)] - polygon = Polygon(points=points) - if polygon.get_area() > 1e-12: - shapes.append(Annotation(polygon, labels=labels, id=ID(f"{label_idx:08}"))) - else: - raise RuntimeError( - f"Detection results assignment not implemented for task: {self._task_type}") - - dataset_item.append_annotations(shapes) - - if feature_vector is not None: - active_score = TensorEntity(name="representation_vector", numpy=feature_vector) - dataset_item.append_metadata_item(active_score, model=self._task_environment.model) - - - def infer(self, dataset: DatasetEntity, inference_parameters: Optional[InferenceParameters] = None) -> DatasetEntity: - """ Analyzes a dataset using the latest inference model. """ - - logger.info('Infer the model on the dataset') - set_hyperparams(self._config, self._hyperparams) - # There is no need to have many workers for a couple of images. - self._config.data.workers_per_gpu = max(min(self._config.data.workers_per_gpu, len(dataset) - 1), 0) - - # If confidence threshold is adaptive then up-to-date value should be stored in the model - # and should not be changed during inference. Otherwise user-specified value should be taken. - if not self._hyperparams.postprocessing.result_based_confidence_threshold: - self.confidence_threshold = self._hyperparams.postprocessing.confidence_threshold - - update_progress_callback = default_progress_callback - if inference_parameters is not None: - update_progress_callback = inference_parameters.update_progress - - time_monitor = InferenceProgressCallback(len(dataset), update_progress_callback) - - def pre_hook(module, input): - time_monitor.on_test_batch_begin(None, None) - - def hook(module, input, output): - time_monitor.on_test_batch_end(None, None) - - logger.info(f'Confidence threshold {self.confidence_threshold}') - model = self._model - with model.register_forward_pre_hook(pre_hook), model.register_forward_hook(hook): - prediction_results, _ = self._infer_detector(model, self._config, dataset, dump_features=True, eval=False) - self._add_predictions_to_dataset(prediction_results, dataset, self.confidence_threshold) - - logger.info('Inference completed') - return dataset - - - @staticmethod - def _infer_detector(model: torch.nn.Module, config: Config, dataset: DatasetEntity, dump_features: bool = False, - eval: Optional[bool] = False, metric_name: Optional[str] = 'mAP') -> Tuple[List, float]: - model.eval() - test_config = prepare_for_testing(config, dataset) - mm_val_dataset = build_dataset(test_config.data.test) - batch_size = 1 - mm_val_dataloader = build_dataloader(mm_val_dataset, - samples_per_gpu=batch_size, - workers_per_gpu=test_config.data.workers_per_gpu, - num_gpus=1, - dist=False, - shuffle=False) - if torch.cuda.is_available(): - eval_model = MMDataParallel(model.cuda(test_config.gpu_ids[0]), - device_ids=test_config.gpu_ids) - else: - eval_model = MMDataCPU(model) - - eval_predictions = [] - feature_vectors = [] - - def dump_features_hook(mod, inp, out): - with torch.no_grad(): - feature_map = out[-1] - feature_vector = torch.nn.functional.adaptive_avg_pool2d(feature_map, (1, 1)) - assert feature_vector.size(0) == 1 - feature_vectors.append(feature_vector.view(-1).detach().cpu().numpy()) - - def dummy_dump_features_hook(mod, inp, out): - feature_vectors.append(None) - - hook = dump_features_hook if dump_features else dummy_dump_features_hook - - # Use a single gpu for testing. Set in both mm_val_dataloader and eval_model - with eval_model.module.backbone.register_forward_hook(hook): - for data in mm_val_dataloader: - with torch.no_grad(): - result = eval_model(return_loss=False, rescale=True, **data) - eval_predictions.extend(result) - - # hard-code way to remove EvalHook args - for key in [ - 'interval', 'tmpdir', 'start', 'gpu_collect', 'save_best', - 'rule', 'dynamic_intervals' - ]: - config.evaluation.pop(key, None) - - metric = None - if eval: - metric = mm_val_dataset.evaluate(eval_predictions, **config.evaluation)[metric_name] - - assert len(eval_predictions) == len(feature_vectors), f'{len(eval_predictions)} != {len(feature_vectors)}' - eval_predictions = zip(eval_predictions, feature_vectors) - return eval_predictions, metric - - - def evaluate(self, - output_result_set: ResultSetEntity, - evaluation_metric: Optional[str] = None): - """ Computes performance on a resultset """ - logger.info('Evaluating the metric') - if evaluation_metric is not None: - logger.warning(f'Requested to use {evaluation_metric} metric, but parameter is ignored. Use F-measure instead.') - metric = MetricsHelper.compute_f_measure(output_result_set) - logger.info(f"F-measure after evaluation: {metric.f_measure.value}") - output_result_set.performance = metric.get_performance() - logger.info('Evaluation completed') - - - @staticmethod - def _is_docker(): - """ - Checks whether the task runs in docker container - - :return bool: True if task runs in docker - """ - path = '/proc/self/cgroup' - is_in_docker = False - if os.path.isfile(path): - with open(path) as f: - is_in_docker = is_in_docker or any('docker' in line for line in f) - is_in_docker = is_in_docker or os.path.exists('/.dockerenv') - return is_in_docker - - def unload(self): - """ - Unload the task - """ - self._delete_scratch_space() - if self._is_docker(): - logger.warning( - "Got unload request. Unloading models. Throwing Segmentation Fault on purpose") - import ctypes - ctypes.string_at(0) - else: - logger.warning("Got unload request, but not on Docker. Only clearing CUDA cache") - torch.cuda.empty_cache() - logger.warning(f"Done unloading. " - f"Torch is still occupying {torch.cuda.memory_allocated()} bytes of GPU memory") - - def export(self, - export_type: ExportType, - output_model: ModelEntity): - logger.info('Exporting the model') - assert export_type == ExportType.OPENVINO - output_model.model_format = ModelFormat.OPENVINO - output_model.optimization_type = self._optimization_type - with tempfile.TemporaryDirectory() as tempdir: - optimized_model_dir = os.path.join(tempdir, 'export') - logger.info(f'Optimized model will be temporarily saved to "{optimized_model_dir}"') - os.makedirs(optimized_model_dir, exist_ok=True) - try: - from torch.jit._trace import TracerWarning - warnings.filterwarnings('ignore', category=TracerWarning) - if torch.cuda.is_available(): - model = self._model.cuda(self._config.gpu_ids[0]) - else: - model = self._model.cpu() - pruning_transformation = OptimizationMethod.FILTER_PRUNING in self._optimization_methods - export_model(model, self._config, tempdir, target='openvino', - pruning_transformation=pruning_transformation) - bin_file = [f for f in os.listdir(tempdir) if f.endswith('.bin')][0] - xml_file = [f for f in os.listdir(tempdir) if f.endswith('.xml')][0] - with open(os.path.join(tempdir, bin_file), "rb") as f: - output_model.set_data('openvino.bin', f.read()) - with open(os.path.join(tempdir, xml_file), "rb") as f: - output_model.set_data('openvino.xml', f.read()) - output_model.set_data('confidence_threshold', np.array([self.confidence_threshold], dtype=np.float32).tobytes()) - output_model.precision = self._precision - output_model.optimization_methods = self._optimization_methods - except Exception as ex: - raise RuntimeError('Optimization was unsuccessful.') from ex - output_model.set_data("label_schema.json", label_schema_to_bytes(self._task_environment.label_schema)) - logger.info('Exporting completed') - - def _delete_scratch_space(self): - """ - Remove model checkpoints and mmdet logs - """ - if os.path.exists(self._scratch_space): - shutil.rmtree(self._scratch_space, ignore_errors=False) diff --git a/external/mmdetection/build/lib/detection_tasks/apis/detection/model_wrappers/__init__.py b/external/mmdetection/build/lib/detection_tasks/apis/detection/model_wrappers/__init__.py deleted file mode 100644 index 570552ae1af..00000000000 --- a/external/mmdetection/build/lib/detection_tasks/apis/detection/model_wrappers/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (C) 2021 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. - -__all__ = [] diff --git a/external/mmdetection/build/lib/detection_tasks/apis/detection/nncf_task.py b/external/mmdetection/build/lib/detection_tasks/apis/detection/nncf_task.py deleted file mode 100644 index 2d6aa61c8db..00000000000 --- a/external/mmdetection/build/lib/detection_tasks/apis/detection/nncf_task.py +++ /dev/null @@ -1,283 +0,0 @@ -# Copyright (C) 2021 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. - -import io -import json -import os -from collections import defaultdict -from typing import Optional - -import torch -from ote_sdk.configuration import cfg_helper -from ote_sdk.configuration.helper.utils import ids_to_strings -from ote_sdk.entities.datasets import DatasetEntity -from ote_sdk.entities.model import ( - ModelEntity, - ModelFormat, - ModelOptimizationType, - ModelPrecision, - OptimizationMethod, -) -from ote_sdk.entities.optimization_parameters import default_progress_callback, OptimizationParameters -from ote_sdk.entities.subset import Subset -from ote_sdk.entities.task_environment import TaskEnvironment -from ote_sdk.serialization.label_mapper import label_schema_to_bytes -from ote_sdk.usecases.tasks.interfaces.export_interface import ExportType -from ote_sdk.usecases.tasks.interfaces.optimization_interface import IOptimizationTask -from ote_sdk.usecases.tasks.interfaces.optimization_interface import OptimizationType - -from mmdet.apis import train_detector -from mmdet.apis.fake_input import get_fake_input -from detection_tasks.apis.detection.config_utils import prepare_for_training -from detection_tasks.apis.detection.configuration import OTEDetectionConfig -from detection_tasks.apis.detection.inference_task import OTEDetectionInferenceTask -from detection_tasks.apis.detection.ote_utils import OptimizationProgressCallback -from detection_tasks.extension.utils.hooks import OTELoggerHook -from mmdet.apis.train import build_val_dataloader -from mmdet.datasets import build_dataloader, build_dataset -from mmdet.integration.nncf import check_nncf_is_enabled -from mmdet.integration.nncf import is_state_nncf -from mmdet.integration.nncf import wrap_nncf_model -from mmdet.integration.nncf import is_accuracy_aware_training_set -from mmdet.integration.nncf.config import compose_nncf_config -from mmdet.utils.logger import get_root_logger - - -logger = get_root_logger() - - -class OTEDetectionNNCFTask(OTEDetectionInferenceTask, IOptimizationTask): - - def __init__(self, task_environment: TaskEnvironment): - """" - Task for compressing object detection models using NNCF. - """ - self._val_dataloader = None - self._compression_ctrl = None - self._nncf_preset = "nncf_quantization" - check_nncf_is_enabled() - super().__init__(task_environment) - self._optimization_type = ModelOptimizationType.NNCF - - def _set_attributes_by_hyperparams(self): - quantization = self._hyperparams.nncf_optimization.enable_quantization - pruning = self._hyperparams.nncf_optimization.enable_pruning - if quantization and pruning: - self._nncf_preset = "nncf_quantization_pruning" - self._optimization_methods = [OptimizationMethod.QUANTIZATION, OptimizationMethod.FILTER_PRUNING] - self._precision = [ModelPrecision.INT8] - return - if quantization and not pruning: - self._nncf_preset = "nncf_quantization" - self._optimization_methods = [OptimizationMethod.QUANTIZATION] - self._precision = [ModelPrecision.INT8] - return - if not quantization and pruning: - self._nncf_preset = "nncf_pruning" - self._optimization_methods = [OptimizationMethod.FILTER_PRUNING] - self._precision = [ModelPrecision.FP32] - return - raise RuntimeError('Not selected optimization algorithm') - - def _load_model(self, model: ModelEntity): - # NNCF parts - nncf_config_path = os.path.join(self._base_dir, "compression_config.json") - - with open(nncf_config_path) as nncf_config_file: - common_nncf_config = json.load(nncf_config_file) - - self._set_attributes_by_hyperparams() - - optimization_config = compose_nncf_config(common_nncf_config, [self._nncf_preset]) - - max_acc_drop = self._hyperparams.nncf_optimization.maximal_accuracy_degradation / 100 - if "accuracy_aware_training" in optimization_config["nncf_config"]: - # Update maximal_absolute_accuracy_degradation - (optimization_config["nncf_config"]["accuracy_aware_training"] - ["params"]["maximal_absolute_accuracy_degradation"]) = max_acc_drop - # Force evaluation interval - self._config.evaluation.interval = 1 - else: - logger.info("NNCF config has no accuracy_aware_training parameters") - - self._config.update(optimization_config) - - compression_ctrl = None - if model is not None: - # If a model has been trained and saved for the task already, create empty model and load weights here - buffer = io.BytesIO(model.get_data("weights.pth")) - model_data = torch.load(buffer, map_location=torch.device('cpu')) - - self.confidence_threshold = model_data.get('confidence_threshold', - self._hyperparams.postprocessing.confidence_threshold) - if model_data.get('anchors'): - anchors = model_data['anchors'] - self._config.model.bbox_head.anchor_generator.heights = anchors['heights'] - self._config.model.bbox_head.anchor_generator.widths = anchors['widths'] - - model = self._create_model(self._config, from_scratch=True) - try: - if is_state_nncf(model_data): - compression_ctrl, model = wrap_nncf_model( - model, - self._config, - init_state_dict=model_data, - get_fake_input_func=get_fake_input - ) - logger.info("Loaded model weights from Task Environment and wrapped by NNCF") - else: - try: - model.load_state_dict(model_data['model']) - logger.info(f"Loaded model weights from Task Environment") - logger.info(f"Model architecture: {self._model_name}") - except BaseException as ex: - raise ValueError("Could not load the saved model. The model file structure is invalid.") \ - from ex - - logger.info(f"Loaded model weights from Task Environment") - logger.info(f"Model architecture: {self._model_name}") - except BaseException as ex: - raise ValueError("Could not load the saved model. The model file structure is invalid.") \ - from ex - else: - raise ValueError(f"No trained model in project. NNCF require pretrained weights to compress the model") - - self._compression_ctrl = compression_ctrl - return model - - def _create_compressed_model(self, dataset, config): - init_dataloader = build_dataloader( - dataset, - config.data.samples_per_gpu, - config.data.workers_per_gpu, - len(config.gpu_ids), - dist=False, - seed=config.seed) - is_acc_aware_training_set = is_accuracy_aware_training_set(config.get("nncf_config")) - - if is_acc_aware_training_set: - self._val_dataloader = build_val_dataloader(config, False) - - self._compression_ctrl, self._model = wrap_nncf_model( - self._model, - config, - val_dataloader=self._val_dataloader, - dataloader_for_init=init_dataloader, - get_fake_input_func=get_fake_input, - is_accuracy_aware=is_acc_aware_training_set) - - def optimize( - self, - optimization_type: OptimizationType, - dataset: DatasetEntity, - output_model: ModelEntity, - optimization_parameters: Optional[OptimizationParameters], - ): - if optimization_type is not OptimizationType.NNCF: - raise RuntimeError("NNCF is the only supported optimization") - - train_dataset = dataset.get_subset(Subset.TRAINING) - val_dataset = dataset.get_subset(Subset.VALIDATION) - - config = self._config - - if optimization_parameters is not None: - update_progress_callback = optimization_parameters.update_progress - else: - update_progress_callback = default_progress_callback - - time_monitor = OptimizationProgressCallback(update_progress_callback, - loading_stage_progress_percentage=5, - initialization_stage_progress_percentage=5) - learning_curves = defaultdict(OTELoggerHook.Curve) - training_config = prepare_for_training(config, train_dataset, val_dataset, time_monitor, learning_curves) - mm_train_dataset = build_dataset(training_config.data.train) - - if torch.cuda.is_available(): - self._model.cuda(training_config.gpu_ids[0]) - - # Initialize NNCF parts if start from not compressed model - if not self._compression_ctrl: - self._create_compressed_model(mm_train_dataset, training_config) - - time_monitor.on_initialization_end() - - # Run training. - self._training_work_dir = training_config.work_dir - self._is_training = True - self._model.train() - - train_detector(model=self._model, - dataset=mm_train_dataset, - cfg=training_config, - validate=True, - val_dataloader=self._val_dataloader, - compression_ctrl=self._compression_ctrl) - - # Check for stop signal when training has stopped. If should_stop is true, training was cancelled - if self._should_stop: - logger.info('Training cancelled.') - self._should_stop = False - self._is_training = False - return - - self.save_model(output_model) - - output_model.model_format = ModelFormat.BASE_FRAMEWORK - output_model.optimization_type = self._optimization_type - output_model.optimization_methods = self._optimization_methods - output_model.precision = self._precision - - self._is_training = False - - def export(self, export_type: ExportType, output_model: ModelEntity): - if self._compression_ctrl is None: - super().export(export_type, output_model) - else: - self._compression_ctrl.prepare_for_export() - self._model.disable_dynamic_graph_building() - super().export(export_type, output_model) - self._model.enable_dynamic_graph_building() - - def save_model(self, output_model: ModelEntity): - buffer = io.BytesIO() - hyperparams = self._task_environment.get_hyper_parameters(OTEDetectionConfig) - hyperparams_str = ids_to_strings(cfg_helper.convert(hyperparams, dict, enum_to_str=True)) - labels = {label.name: label.color.rgb_tuple for label in self._labels} - # WA for scheduler resetting in NNCF - compression_state = self._compression_ctrl.get_compression_state() - for algo_state in compression_state.get('ctrl_state', {}).values(): - if not algo_state.get('scheduler_state'): - algo_state['scheduler_state'] = {'current_step': 0, 'current_epoch': 0} - modelinfo = { - 'compression_state': compression_state, - 'meta': { - 'config': self._config, - 'nncf_enable_compression': True, - }, - 'model': self._model.state_dict(), - 'config': hyperparams_str, - 'labels': labels, - 'confidence_threshold': self.confidence_threshold, - 'VERSION': 1, - } - - if hasattr(self._config.model, 'bbox_head') and hasattr(self._config.model.bbox_head, 'anchor_generator'): - if getattr(self._config.model.bbox_head.anchor_generator, 'reclustering_anchors', False): - generator = self._model.bbox_head.anchor_generator - modelinfo['anchors'] = {'heights': generator.heights, 'widths': generator.widths} - - torch.save(modelinfo, buffer) - output_model.set_data("weights.pth", buffer.getvalue()) - output_model.set_data("label_schema.json", label_schema_to_bytes(self._task_environment.label_schema)) diff --git a/external/mmdetection/build/lib/detection_tasks/apis/detection/openvino_task.py b/external/mmdetection/build/lib/detection_tasks/apis/detection/openvino_task.py deleted file mode 100644 index 9c84aa40fdf..00000000000 --- a/external/mmdetection/build/lib/detection_tasks/apis/detection/openvino_task.py +++ /dev/null @@ -1,367 +0,0 @@ -# Copyright (C) 2021 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. - -import attr -import copy -import io -import json -import numpy as np -import os -import ote_sdk.usecases.exportable_code.demo as demo -import tempfile -from addict import Dict as ADDict -from compression.api import DataLoader -from compression.engines.ie_engine import IEEngine -from compression.graph import load_model, save_model -from compression.graph.model_utils import compress_model_weights, get_nodes_by_type -from compression.pipeline.initializer import create_pipeline -from openvino.model_zoo.model_api.adapters import OpenvinoAdapter, create_core -from openvino.model_zoo.model_api.models import Model -from ote_sdk.entities.annotation import AnnotationSceneEntity -from ote_sdk.entities.datasets import DatasetEntity -from ote_sdk.entities.inference_parameters import InferenceParameters, default_progress_callback -from ote_sdk.entities.label_schema import LabelSchemaEntity -from ote_sdk.entities.model import ( - ModelEntity, - ModelFormat, - ModelOptimizationType, - ModelPrecision, - OptimizationMethod, -) -from ote_sdk.entities.model_template import TaskType -from ote_sdk.entities.optimization_parameters import OptimizationParameters -from ote_sdk.entities.resultset import ResultSetEntity -from ote_sdk.entities.task_environment import TaskEnvironment -from ote_sdk.serialization.label_mapper import LabelSchemaMapper, label_schema_to_bytes -from ote_sdk.usecases.evaluation.metrics_helper import MetricsHelper -from ote_sdk.usecases.exportable_code.inference import BaseInferencer -from ote_sdk.usecases.exportable_code.prediction_to_annotation_converter import ( - DetectionBoxToAnnotationConverter, - MaskToAnnotationConverter, - RotatedRectToAnnotationConverter, -) -from ote_sdk.usecases.tasks.interfaces.deployment_interface import IDeploymentTask -from ote_sdk.usecases.tasks.interfaces.evaluate_interface import IEvaluationTask -from ote_sdk.usecases.tasks.interfaces.inference_interface import IInferenceTask -from ote_sdk.usecases.tasks.interfaces.optimization_interface import IOptimizationTask, OptimizationType -from typing import Any, Dict, List, Optional, Tuple, Union -from zipfile import ZipFile - -from mmdet.utils.logger import get_root_logger -from .configuration import OTEDetectionConfig - -logger = get_root_logger() - - -class BaseInferencerWithConverter(BaseInferencer): - - def __init__(self, configuration, model, converter) -> None: - self.configuration = configuration - self.model = model - self.converter = converter - - def pre_process(self, image: np.ndarray) -> Tuple[Dict[str, np.ndarray], Dict[str, Any]]: - return self.model.preprocess(image) - - def post_process(self, prediction: Dict[str, np.ndarray], metadata: Dict[str, Any]) -> AnnotationSceneEntity: - detections = self.model.postprocess(prediction, metadata) - - return self.converter.convert_to_annotation(detections, metadata) - - def forward(self, inputs: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]: - return self.model.infer_sync(inputs) - - -class OpenVINODetectionInferencer(BaseInferencerWithConverter): - def __init__( - self, - hparams: OTEDetectionConfig, - label_schema: LabelSchemaEntity, - model_file: Union[str, bytes], - weight_file: Union[str, bytes, None] = None, - device: str = "CPU", - num_requests: int = 1, - ): - """ - Inferencer implementation for OTEDetection using OpenVINO backend. - - :param hparams: Hyper parameters that the model should use. - :param label_schema: LabelSchemaEntity that was used during model training. - :param model_file: Path OpenVINO IR model definition file. - :param weight_file: Path OpenVINO IR model weights file. - :param device: Device to run inference on, such as CPU, GPU or MYRIAD. Defaults to "CPU". - :param num_requests: Maximum number of requests that the inferencer can make. Defaults to 1. - - """ - - model_adapter = OpenvinoAdapter(create_core(), model_file, weight_file, device=device, max_num_requests=num_requests) - configuration = {**attr.asdict(hparams.postprocessing, - filter=lambda attr, value: attr.name not in ['header', 'description', 'type', 'visible_in_ui'])} - model = Model.create_model('ssd', model_adapter, configuration, preload=True) - converter = DetectionBoxToAnnotationConverter(label_schema) - - super().__init__(configuration, model, converter) - - -class OpenVINOMaskInferencer(BaseInferencerWithConverter): - def __init__( - self, - hparams: OTEDetectionConfig, - label_schema: LabelSchemaEntity, - model_file: Union[str, bytes], - weight_file: Union[str, bytes, None] = None, - device: str = "CPU", - num_requests: int = 1, - ): - model_adapter = OpenvinoAdapter( - create_core(), - model_file, - weight_file, - device=device, - max_num_requests=num_requests) - - configuration = { - **attr.asdict( - hparams.postprocessing, - filter=lambda attr, value: attr.name not in [ - 'header', 'description', 'type', 'visible_in_ui'])} - - model = Model.create_model( - 'maskrcnn', - model_adapter, - configuration, - preload=True) - - converter = MaskToAnnotationConverter(label_schema) - - super().__init__(configuration, model, converter) - - -class OpenVINORotatedRectInferencer(BaseInferencerWithConverter): - def __init__( - self, - hparams: OTEDetectionConfig, - label_schema: LabelSchemaEntity, - model_file: Union[str, bytes], - weight_file: Union[str, bytes, None] = None, - device: str = "CPU", - num_requests: int = 1, - ): - model_adapter = OpenvinoAdapter( - create_core(), - model_file, - weight_file, - device=device, - max_num_requests=num_requests) - - configuration = { - **attr.asdict( - hparams.postprocessing, - filter=lambda attr, value: attr.name not in [ - 'header', 'description', 'type', 'visible_in_ui'])} - - model = Model.create_model( - 'maskrcnn', - model_adapter, - configuration, - preload=True) - - converter = RotatedRectToAnnotationConverter(label_schema) - - super().__init__(configuration, model, converter) - - -class OTEOpenVinoDataLoader(DataLoader): - def __init__(self, dataset: DatasetEntity, inferencer: BaseInferencer): - self.dataset = dataset - self.inferencer = inferencer - - def __getitem__(self, index): - image = self.dataset[index].numpy - annotation = self.dataset[index].annotation_scene - inputs, metadata = self.inferencer.pre_process(image) - - return (index, annotation), inputs, metadata - - def __len__(self): - return len(self.dataset) - - -class OpenVINODetectionTask(IDeploymentTask, IInferenceTask, IEvaluationTask, IOptimizationTask): - def __init__(self, task_environment: TaskEnvironment): - logger.info('Loading OpenVINO OTEDetectionTask') - self.task_environment = task_environment - self.model = self.task_environment.model - self.task_type = self.task_environment.model_template.task_type - self.confidence_threshold: float = 0.0 - self.inferencer = self.load_inferencer() - logger.info('OpenVINO task initialization completed') - - @property - def hparams(self): - return self.task_environment.get_hyper_parameters(OTEDetectionConfig) - - def load_inferencer(self) -> Union[OpenVINODetectionInferencer, OpenVINOMaskInferencer] : - _hparams = copy.deepcopy(self.hparams) - self.confidence_threshold = float(np.frombuffer(self.model.get_data("confidence_threshold"), dtype=np.float32)[0]) - _hparams.postprocessing.confidence_threshold = self.confidence_threshold - args = [ - _hparams, - self.task_environment.label_schema, - self.model.get_data("openvino.xml"), - self.model.get_data("openvino.bin"), - ] - if self.task_type == TaskType.DETECTION: - return OpenVINODetectionInferencer(*args) - if self.task_type == TaskType.INSTANCE_SEGMENTATION: - return OpenVINOMaskInferencer(*args) - if self.task_type == TaskType.ROTATED_DETECTION: - return OpenVINORotatedRectInferencer(*args) - raise RuntimeError(f"Unknown OpenVINO Inferencer TaskType: {self.task_type}") - - def infer(self, dataset: DatasetEntity, inference_parameters: Optional[InferenceParameters] = None) -> DatasetEntity: - logger.info('Start OpenVINO inference') - update_progress_callback = default_progress_callback - if inference_parameters is not None: - update_progress_callback = inference_parameters.update_progress - dataset_size = len(dataset) - for i, dataset_item in enumerate(dataset, 1): - predicted_scene = self.inferencer.predict(dataset_item.numpy) - dataset_item.append_annotations(predicted_scene.annotations) - update_progress_callback(int(i / dataset_size * 100)) - logger.info('OpenVINO inference completed') - return dataset - - def evaluate(self, - output_result_set: ResultSetEntity, - evaluation_metric: Optional[str] = None): - logger.info('Start OpenVINO metric evaluation') - if evaluation_metric is not None: - logger.warning(f'Requested to use {evaluation_metric} metric, but parameter is ignored. Use F-measure instead.') - output_result_set.performance = MetricsHelper.compute_f_measure(output_result_set).get_performance() - logger.info('OpenVINO metric evaluation completed') - - def deploy(self, - output_model: ModelEntity) -> None: - logger.info('Deploying the model') - - work_dir = os.path.dirname(demo.__file__) - parameters = {} - parameters['type_of_model'] = self.inferencer.model.__model__ - parameters['converter_type'] = str(self.task_type) - parameters['model_parameters'] = self.inferencer.configuration - parameters['model_parameters']['labels'] = LabelSchemaMapper.forward(self.task_environment.label_schema) - - zip_buffer = io.BytesIO() - with ZipFile(zip_buffer, 'w') as arch: - # model files - arch.writestr(os.path.join("model", "model.xml"), self.model.get_data("openvino.xml")) - arch.writestr(os.path.join("model", "model.bin"), self.model.get_data("openvino.bin")) - arch.writestr( - os.path.join("model", "config.json"), json.dumps(parameters, ensure_ascii=False, indent=4) - ) - # python files - arch.write(os.path.join(work_dir, "requirements.txt"), os.path.join("python", "requirements.txt")) - arch.write(os.path.join(work_dir, "LICENSE"), os.path.join("python", "LICENSE")) - arch.write(os.path.join(work_dir, "README.md"), os.path.join("python", "README.md")) - arch.write(os.path.join(work_dir, "demo.py"), os.path.join("python", "demo.py")) - output_model.exportable_code = zip_buffer.getvalue() - logger.info('Deploying completed') - - def optimize(self, - optimization_type: OptimizationType, - dataset: DatasetEntity, - output_model: ModelEntity, - optimization_parameters: Optional[OptimizationParameters]): - logger.info('Start POT optimization') - - if optimization_type is not OptimizationType.POT: - raise ValueError('POT is the only supported optimization type for OpenVino models') - - data_loader = OTEOpenVinoDataLoader(dataset, self.inferencer) - - with tempfile.TemporaryDirectory() as tempdir: - xml_path = os.path.join(tempdir, "model.xml") - bin_path = os.path.join(tempdir, "model.bin") - with open(xml_path, "wb") as f: - f.write(self.model.get_data("openvino.xml")) - with open(bin_path, "wb") as f: - f.write(self.model.get_data("openvino.bin")) - - model_config = ADDict({ - 'model_name': 'openvino_model', - 'model': xml_path, - 'weights': bin_path - }) - - model = load_model(model_config) - - if get_nodes_by_type(model, ['FakeQuantize']): - raise RuntimeError("Model is already optimized by POT") - - if optimization_parameters is not None: - optimization_parameters.update_progress(10) - - engine_config = ADDict({ - 'device': 'CPU' - }) - - stat_subset_size = self.hparams.pot_parameters.stat_subset_size - preset = self.hparams.pot_parameters.preset.name.lower() - - algorithms = [ - { - 'name': 'DefaultQuantization', - 'params': { - 'target_device': 'ANY', - 'preset': preset, - 'stat_subset_size': min(stat_subset_size, len(data_loader)), - 'shuffle_data': True - } - } - ] - - engine = IEEngine(config=engine_config, data_loader=data_loader, metric=None) - - pipeline = create_pipeline(algorithms, engine) - - compressed_model = pipeline.run(model) - - compress_model_weights(compressed_model) - - if optimization_parameters is not None: - optimization_parameters.update_progress(90) - - with tempfile.TemporaryDirectory() as tempdir: - save_model(compressed_model, tempdir, model_name="model") - with open(os.path.join(tempdir, "model.xml"), "rb") as f: - output_model.set_data("openvino.xml", f.read()) - with open(os.path.join(tempdir, "model.bin"), "rb") as f: - output_model.set_data("openvino.bin", f.read()) - output_model.set_data("confidence_threshold", np.array([self.confidence_threshold], dtype=np.float32).tobytes()) - - output_model.set_data("label_schema.json", label_schema_to_bytes(self.task_environment.label_schema)) - - # set model attributes for quantized model - output_model.model_format = ModelFormat.OPENVINO - output_model.optimization_type = ModelOptimizationType.POT - output_model.optimization_methods = [OptimizationMethod.QUANTIZATION] - output_model.precision = [ModelPrecision.INT8] - - self.model = output_model - self.inferencer = self.load_inferencer() - logger.info('POT optimization completed') - - if optimization_parameters is not None: - optimization_parameters.update_progress(100) diff --git a/external/mmdetection/build/lib/detection_tasks/apis/detection/ote_utils.py b/external/mmdetection/build/lib/detection_tasks/apis/detection/ote_utils.py deleted file mode 100644 index 991efaaa52b..00000000000 --- a/external/mmdetection/build/lib/detection_tasks/apis/detection/ote_utils.py +++ /dev/null @@ -1,167 +0,0 @@ -# Copyright (C) 2021 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. - -import time -import colorsys -import importlib -import random -from typing import Callable, Union - -import numpy as np -import yaml -from ote_sdk.entities.color import Color -from ote_sdk.entities.id import ID -from ote_sdk.entities.label import Domain, LabelEntity -from ote_sdk.entities.label_schema import LabelGroup, LabelGroupType, LabelSchemaEntity -from ote_sdk.entities.train_parameters import UpdateProgressCallback -from ote_sdk.usecases.reporting.time_monitor_callback import TimeMonitorCallback - - -class ColorPalette: - def __init__(self, n, rng=None): - assert n > 0 - - if rng is None: - rng = random.Random(0xACE) - - candidates_num = 100 - hsv_colors = [(1.0, 1.0, 1.0)] - for _ in range(1, n): - colors_candidates = [(rng.random(), rng.uniform(0.8, 1.0), rng.uniform(0.5, 1.0)) - for _ in range(candidates_num)] - min_distances = [self.min_distance(hsv_colors, c) for c in colors_candidates] - arg_max = np.argmax(min_distances) - hsv_colors.append(colors_candidates[arg_max]) - - self.palette = [Color(*self.hsv2rgb(*hsv)) for hsv in hsv_colors] - - @staticmethod - def dist(c1, c2): - dh = min(abs(c1[0] - c2[0]), 1 - abs(c1[0] - c2[0])) * 2 - ds = abs(c1[1] - c2[1]) - dv = abs(c1[2] - c2[2]) - return dh * dh + ds * ds + dv * dv - - @classmethod - def min_distance(cls, colors_set, color_candidate): - distances = [cls.dist(o, color_candidate) for o in colors_set] - return np.min(distances) - - @staticmethod - def hsv2rgb(h, s, v): - return tuple(round(c * 255) for c in colorsys.hsv_to_rgb(h, s, v)) - - def __getitem__(self, n): - return self.palette[n % len(self.palette)] - - def __len__(self): - return len(self.palette) - - -def generate_label_schema(label_names, label_domain=Domain.DETECTION): - colors = ColorPalette(len(label_names)) if len(label_names) > 0 else [] - not_empty_labels = [LabelEntity(name=name, color=colors[i], domain=label_domain, id=ID(f"{i:08}")) for i, name in - enumerate(label_names)] - emptylabel = LabelEntity(name=f"Empty label", color=Color(42, 43, 46), - is_empty=True, domain=label_domain, id=ID(f"{len(not_empty_labels):08}")) - - label_schema = LabelSchemaEntity() - exclusive_group = LabelGroup(name="labels", labels=not_empty_labels, group_type=LabelGroupType.EXCLUSIVE) - empty_group = LabelGroup(name="empty", labels=[emptylabel], group_type=LabelGroupType.EMPTY_LABEL) - label_schema.add_group(exclusive_group) - label_schema.add_group(empty_group) - return label_schema - - -def load_template(path): - with open(path) as f: - template = yaml.safe_load(f) - return template - - -def get_task_class(path): - module_name, class_name = path.rsplit('.', 1) - module = importlib.import_module(module_name) - return getattr(module, class_name) - - -class TrainingProgressCallback(TimeMonitorCallback): - def __init__(self, update_progress_callback: UpdateProgressCallback): - super().__init__(0, 0, 0, 0, update_progress_callback=update_progress_callback) - - def on_train_batch_end(self, batch, logs=None): - super().on_train_batch_end(batch, logs) - self.update_progress_callback(self.get_progress()) - - def on_epoch_end(self, epoch, logs=None): - self.past_epoch_duration.append(time.time() - self.start_epoch_time) - self._calculate_average_epoch() - score = None - if hasattr(self.update_progress_callback, 'metric') and isinstance(logs, dict): - score = logs.get(self.update_progress_callback.metric, None) - score = float(score) if score is not None else None - self.update_progress_callback(self.get_progress(), score=score) - - -class InferenceProgressCallback(TimeMonitorCallback): - def __init__(self, num_test_steps, update_progress_callback: Callable[[int], None]): - super().__init__( - num_epoch=0, - num_train_steps=0, - num_val_steps=0, - num_test_steps=num_test_steps, - update_progress_callback=update_progress_callback) - - def on_test_batch_end(self, batch=None, logs=None): - super().on_test_batch_end(batch, logs) - self.update_progress_callback(int(self.get_progress())) - - -class OptimizationProgressCallback(TrainingProgressCallback): - """ Progress callback used for optimization using NNCF - There are three stages to the progress bar: - - 5 % model is loaded - - 10 % compressed model is initialized - - 10-100 % compressed model is being fine-tuned - """ - def __init__(self, update_progress_callback: UpdateProgressCallback, loading_stage_progress_percentage: int = 5, - initialization_stage_progress_percentage: int = 5): - super().__init__(update_progress_callback=update_progress_callback) - if loading_stage_progress_percentage + initialization_stage_progress_percentage >= 100: - raise RuntimeError('Total optimization progress percentage is more than 100%') - - self.loading_stage_progress_percentage = loading_stage_progress_percentage - self.initialization_stage_progress_percentage = initialization_stage_progress_percentage - - # set loading_stage_progress_percentage from the start as the model is already loaded at this point - self.update_progress_callback(loading_stage_progress_percentage) - - def on_train_begin(self, logs=None): - super().on_train_begin(logs) - # Callback initialization takes place here after OTEProgressHook.before_run() is called - train_percentage = 100 - self.loading_stage_progress_percentage - self.initialization_stage_progress_percentage - loading_stage_steps = self.total_steps * self.loading_stage_progress_percentage / train_percentage - initialization_stage_steps = self.total_steps * self.initialization_stage_progress_percentage / train_percentage - self.total_steps += loading_stage_steps + initialization_stage_steps - - self.current_step = loading_stage_steps + initialization_stage_steps - self.update_progress_callback(self.get_progress()) - - def on_train_end(self, logs=None): - super().on_train_end(logs) - self.update_progress_callback(self.get_progress(), score=logs) - - def on_initialization_end(self): - self.update_progress_callback(self.loading_stage_progress_percentage + - self.initialization_stage_progress_percentage) diff --git a/external/mmdetection/build/lib/detection_tasks/apis/detection/train_task.py b/external/mmdetection/build/lib/detection_tasks/apis/detection/train_task.py deleted file mode 100644 index 9d837cd2f82..00000000000 --- a/external/mmdetection/build/lib/detection_tasks/apis/detection/train_task.py +++ /dev/null @@ -1,224 +0,0 @@ -# Copyright (C) 2021 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. - -import copy -import io -import os -from collections import defaultdict -from glob import glob -from typing import List, Optional - -import numpy as np -import torch -from ote_sdk.configuration import cfg_helper -from ote_sdk.configuration.helper.utils import ids_to_strings -from ote_sdk.entities.datasets import DatasetEntity -from ote_sdk.entities.metrics import (BarChartInfo, BarMetricsGroup, CurveMetric, LineChartInfo, LineMetricsGroup, MetricsGroup, - ScoreMetric, VisualizationType) -from ote_sdk.entities.model import ModelEntity, ModelPrecision -from ote_sdk.entities.resultset import ResultSetEntity -from ote_sdk.entities.subset import Subset -from ote_sdk.entities.train_parameters import TrainParameters, default_progress_callback -from ote_sdk.serialization.label_mapper import label_schema_to_bytes -from ote_sdk.usecases.evaluation.metrics_helper import MetricsHelper -from ote_sdk.usecases.tasks.interfaces.training_interface import ITrainingTask - -from mmdet.apis import train_detector -from detection_tasks.apis.detection.config_utils import cluster_anchors, prepare_for_training, set_hyperparams -from detection_tasks.apis.detection.inference_task import OTEDetectionInferenceTask -from detection_tasks.apis.detection.ote_utils import TrainingProgressCallback -from detection_tasks.extension.utils.hooks import OTELoggerHook -from mmdet.datasets import build_dataset -from mmdet.utils.logger import get_root_logger - -logger = get_root_logger() - - -class OTEDetectionTrainingTask(OTEDetectionInferenceTask, ITrainingTask): - - def _generate_training_metrics(self, learning_curves, map) -> Optional[List[MetricsGroup]]: - """ - Parses the mmdetection logs to get metrics from the latest training run - - :return output List[MetricsGroup] - """ - output: List[MetricsGroup] = [] - - # Learning curves. - for key, curve in learning_curves.items(): - n, m = len(curve.x), len(curve.y) - if n != m: - logger.warning(f"Learning curve {key} has inconsistent number of coordinates ({n} vs {m}.") - n = min(n, m) - curve.x = curve.x[:n] - curve.y = curve.y[:n] - metric_curve = CurveMetric( - xs=np.nan_to_num(curve.x).tolist(), - ys=np.nan_to_num(curve.y).tolist(), - name=key) - visualization_info = LineChartInfo(name=key, x_axis_label="Epoch", y_axis_label=key) - output.append(LineMetricsGroup(metrics=[metric_curve], visualization_info=visualization_info)) - - # Final mAP value on the validation set. - output.append( - BarMetricsGroup( - metrics=[ScoreMetric(value=map, name="mAP")], - visualization_info=BarChartInfo("Validation score", visualization_type=VisualizationType.RADIAL_BAR) - ) - ) - - return output - - - def train(self, dataset: DatasetEntity, output_model: ModelEntity, train_parameters: Optional[TrainParameters] = None): - """ Trains a model on a dataset """ - - logger.info('Training the model') - set_hyperparams(self._config, self._hyperparams) - - train_dataset = dataset.get_subset(Subset.TRAINING) - val_dataset = dataset.get_subset(Subset.VALIDATION) - - # Do clustering for SSD model - if hasattr(self._config.model, 'bbox_head') and hasattr(self._config.model.bbox_head, 'anchor_generator'): - if getattr(self._config.model.bbox_head.anchor_generator, 'reclustering_anchors', False): - self._config, self._model = cluster_anchors(self._config, train_dataset, self._model) - - config = self._config - - # Create a copy of the network. - old_model = copy.deepcopy(self._model) - - # Check for stop signal between pre-eval and training. If training is cancelled at this point, - # old_model should be restored. - if self._should_stop: - logger.info('Training cancelled.') - self._model = old_model - self._should_stop = False - self._is_training = False - self._training_work_dir = None - return - - # Run training. - update_progress_callback = default_progress_callback - if train_parameters is not None: - update_progress_callback = train_parameters.update_progress - time_monitor = TrainingProgressCallback(update_progress_callback) - learning_curves = defaultdict(OTELoggerHook.Curve) - training_config = prepare_for_training(config, train_dataset, val_dataset, time_monitor, learning_curves) - self._training_work_dir = training_config.work_dir - mm_train_dataset = build_dataset(training_config.data.train) - self._is_training = True - self._model.train() - logger.info('Start training') - train_detector(model=self._model, dataset=mm_train_dataset, cfg=training_config, validate=True) - logger.info('Training completed') - - # Check for stop signal when training has stopped. If should_stop is true, training was cancelled and no new - # model should be returned. Old train model is restored. - if self._should_stop: - logger.info('Training cancelled.') - self._model = old_model - self._should_stop = False - self._is_training = False - return - - # Load best weights. - checkpoint_file_path = glob(os.path.join(training_config.work_dir, 'best*pth')) - if len(checkpoint_file_path) == 0: - checkpoint_file_path = os.path.join(training_config.work_dir, 'latest.pth') - elif len(checkpoint_file_path) > 1: - logger.warning(f'Multiple candidates for the best checkpoint found: {checkpoint_file_path}') - checkpoint_file_path = checkpoint_file_path[0] - else: - checkpoint_file_path = checkpoint_file_path[0] - logger.info(f'Use {checkpoint_file_path} for final model weights.') - checkpoint = torch.load(checkpoint_file_path) - self._model.load_state_dict(checkpoint['state_dict']) - - # Get predictions on the validation set. - val_preds, val_map = self._infer_detector( - self._model, - config, - val_dataset, - metric_name=config.evaluation.metric, - dump_features=False, - eval=True - ) - preds_val_dataset = val_dataset.with_empty_annotations() - self._add_predictions_to_dataset(val_preds, preds_val_dataset, 0.0) - resultset = ResultSetEntity( - model=output_model, - ground_truth_dataset=val_dataset, - prediction_dataset=preds_val_dataset, - ) - - # Adjust confidence threshold. - adaptive_threshold = self._hyperparams.postprocessing.result_based_confidence_threshold - if adaptive_threshold: - logger.info('Adjusting the confidence threshold') - metric = MetricsHelper.compute_f_measure(resultset, vary_confidence_threshold=True) - best_confidence_threshold = metric.best_confidence_threshold.value - if best_confidence_threshold is None: - raise ValueError(f"Cannot compute metrics: Invalid confidence threshold!") - logger.info(f"Setting confidence threshold to {best_confidence_threshold} based on results") - self.confidence_threshold = best_confidence_threshold - else: - metric = MetricsHelper.compute_f_measure(resultset, vary_confidence_threshold=False) - - # Compose performance statistics. - # TODO[EUGENE]: ADD MAE CURVE FOR TaskType.COUNTING - performance = metric.get_performance() - performance.dashboard_metrics.extend(self._generate_training_metrics(learning_curves, val_map)) - logger.info(f'Final model performance: {str(performance)}') - - # Save resulting model. - self.save_model(output_model) - output_model.performance = performance - - self._is_training = False - logger.info('Training the model [done]') - - - def save_model(self, output_model: ModelEntity): - buffer = io.BytesIO() - hyperparams_str = ids_to_strings(cfg_helper.convert(self._hyperparams, dict, enum_to_str=True)) - - modelinfo = {'model': self._model.state_dict(), - 'config': hyperparams_str, - 'confidence_threshold': self.confidence_threshold, - 'VERSION': 1} - - if hasattr(self._config.model, 'bbox_head') and hasattr(self._config.model.bbox_head, 'anchor_generator'): - if getattr(self._config.model.bbox_head.anchor_generator, 'reclustering_anchors', False): - generator = self._model.bbox_head.anchor_generator - modelinfo['anchors'] = {'heights': generator.heights, 'widths': generator.widths} - - torch.save(modelinfo, buffer) - output_model.set_data("weights.pth", buffer.getvalue()) - output_model.set_data("label_schema.json", label_schema_to_bytes(self._task_environment.label_schema)) - output_model.precision = [ModelPrecision.FP32] - - - def cancel_training(self): - """ - Sends a cancel training signal to gracefully stop the optimizer. The signal consists of creating a - '.stop_training' file in the current work_dir. The runner checks for this file periodically. - The stopping mechanism allows stopping after each iteration, but validation will still be carried out. Stopping - will therefore take some time. - """ - logger.info("Cancel training requested.") - self._should_stop = True - stop_training_filepath = os.path.join(self._training_work_dir, '.stop_training') - open(stop_training_filepath, 'a').close() diff --git a/external/mmdetection/build/lib/detection_tasks/extension/__init__.py b/external/mmdetection/build/lib/detection_tasks/extension/__init__.py deleted file mode 100644 index 90eb8894330..00000000000 --- a/external/mmdetection/build/lib/detection_tasks/extension/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (C) 2021 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. - -from .datasets import OTEDataset, get_annotation_mmdet_format -from .utils import (CancelTrainingHook, FixedMomentumUpdaterHook, LoadImageFromOTEDataset, EpochRunnerWithCancel, - LoadAnnotationFromOTEDataset, OTELoggerHook, OTEProgressHook, EarlyStoppingHook, ReduceLROnPlateauLrUpdaterHook) diff --git a/external/mmdetection/build/lib/detection_tasks/extension/datasets/__init__.py b/external/mmdetection/build/lib/detection_tasks/extension/datasets/__init__.py deleted file mode 100644 index e9bc5ab37d1..00000000000 --- a/external/mmdetection/build/lib/detection_tasks/extension/datasets/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (C) 2021 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. - -from .data_utils import get_anchor_boxes, get_sizes_from_dataset_entity, format_list_to_str -from .mmdataset import OTEDataset, get_annotation_mmdet_format - -__all__ = [OTEDataset, get_annotation_mmdet_format, get_anchor_boxes, get_sizes_from_dataset_entity, format_list_to_str] diff --git a/external/mmdetection/build/lib/detection_tasks/extension/datasets/data_utils.py b/external/mmdetection/build/lib/detection_tasks/extension/datasets/data_utils.py deleted file mode 100644 index ff38a3243af..00000000000 --- a/external/mmdetection/build/lib/detection_tasks/extension/datasets/data_utils.py +++ /dev/null @@ -1,390 +0,0 @@ -# Copyright (C) 2020-2021 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -import json -import os.path as osp -from typing import List, Optional - -import numpy as np -from ote_sdk.entities.annotation import Annotation, AnnotationSceneEntity, AnnotationSceneKind -from ote_sdk.entities.dataset_item import DatasetItemEntity -from ote_sdk.entities.datasets import DatasetEntity -from ote_sdk.entities.id import ID -from ote_sdk.entities.image import Image -from ote_sdk.entities.label import Domain, LabelEntity -from ote_sdk.entities.scored_label import ScoredLabel -from ote_sdk.entities.shapes.polygon import Polygon, Point -from ote_sdk.entities.shapes.rectangle import Rectangle -from ote_sdk.entities.subset import Subset -from ote_sdk.utils.shape_factory import ShapeFactory -from pycocotools.coco import COCO - -from mmdet.core import BitmapMasks, PolygonMasks - -def get_classes_from_annotation(path): - with open(path) as read_file: - content = json.load(read_file) - categories = [ - v["name"] for v in sorted(content["categories"], key=lambda x: x["id"]) - ] - return categories - - -class LoadAnnotations: - def __init__(self, with_bbox=True, with_label=True, with_mask=False): - self.with_bbox = with_bbox - self.with_label = with_label - self.with_mask = with_mask - - def _load_bboxes(self, results): - ann_info = results["ann_info"] - results["gt_bboxes"] = ann_info["bboxes"].copy() - - gt_bboxes_ignore = ann_info.get("bboxes_ignore", None) - if gt_bboxes_ignore is not None: - results["gt_bboxes_ignore"] = gt_bboxes_ignore.copy() - results["bbox_fields"].append("gt_bboxes_ignore") - results["bbox_fields"].append("gt_bboxes") - return results - - def _load_labels(self, results): - results["gt_labels"] = results["ann_info"]["labels"].copy() - return results - - def _load_masks(self, results): - gt_masks = results['ann_info']['masks'] - results['gt_masks'] = gt_masks - results['mask_fields'].append('gt_masks') - return results - - def __call__(self, results): - if self.with_bbox: - results = self._load_bboxes(results) - if results is None: - return None - if self.with_label: - results = self._load_labels(results) - if self.with_mask: - results = self._load_masks(results) - - return results - - def __repr__(self): - repr_str = self.__class__.__name__ - repr_str += f"(with_bbox={self.with_bbox}, " - repr_str += f"with_label={self.with_label})" - return repr_str - - -class CocoDataset: - def __init__( - self, - ann_file, - classes=None, - data_root=None, - img_prefix="", - test_mode=False, - filter_empty_gt=True, - min_size=None, - with_mask=False, - ): - self.ann_file = ann_file - self.data_root = data_root - self.img_prefix = img_prefix - self.test_mode = test_mode - self.filter_empty_gt = filter_empty_gt - self.classes = self.get_classes(classes) - self.min_size = min_size - self.with_mask = with_mask - - if self.data_root is not None: - # if not osp.isabs(self.ann_file): - # self.ann_file = osp.join(self.data_root, self.ann_file) - if not (self.img_prefix is None or osp.isabs(self.img_prefix)): - self.img_prefix = osp.join(self.data_root, self.img_prefix) - - self.data_infos = self.load_annotations(self.ann_file) - - if not test_mode: - valid_inds = self._filter_imgs() - self.data_infos = [self.data_infos[i] for i in valid_inds] - - def __len__(self): - return len(self.data_infos) - - def pre_pipeline(self, results): - results["img_prefix"] = self.img_prefix - results["bbox_fields"] = [] - results["mask_fields"] = [] - results["seg_fields"] = [] - - def _rand_another(self, idx): - pool = np.where(self.flag == self.flag[idx])[0] - return np.random.choice(pool) - - def __getitem__(self, idx): - return self.prepare_img(idx) - - def __iter__(self): - for i in range(len(self)): - yield self[i] - - def prepare_img(self, idx): - img_info = self.data_infos[idx] - ann_info = self.get_ann_info(idx) - results = dict(img_info=img_info, ann_info=ann_info) - self.pre_pipeline(results) - return LoadAnnotations(with_mask=self.with_mask)(results) - - def get_classes(self, classes=None): - if classes is None: - return get_classes_from_annotation(self.ann_file) - - if isinstance(classes, (tuple, list)): - return classes - - raise ValueError(f"Unsupported type {type(classes)} of classes.") - - def load_annotations(self, ann_file): - self.coco = COCO(ann_file) - self.cat_ids = self.coco.get_cat_ids(cat_names=self.classes) - self.cat2label = {cat_id: i for i, cat_id in enumerate(self.cat_ids)} - self.img_ids = self.coco.get_img_ids() - data_infos = [] - for i in self.img_ids: - info = self.coco.load_imgs([i])[0] - info["filename"] = info["file_name"] - data_infos.append(info) - return data_infos - - def get_ann_info(self, idx): - img_id = self.data_infos[idx]["id"] - ann_ids = self.coco.get_ann_ids(img_ids=[img_id]) - ann_info = self.coco.load_anns(ann_ids) - return self._parse_ann_info(self.data_infos[idx], ann_info) - - def get_cat_ids(self, idx): - img_id = self.data_infos[idx]["id"] - ann_ids = self.coco.get_ann_ids(img_ids=[img_id]) - ann_info = self.coco.load_anns(ann_ids) - return [ann["category_id"] for ann in ann_info] - - def _filter_imgs(self, min_size=32): - """Filter images too small or without ground truths.""" - valid_inds = [] - # obtain images that contain annotation - ids_with_ann = set(_["image_id"] for _ in self.coco.anns.values()) - # obtain images that contain annotations of the required categories - ids_in_cat = set() - for i, class_id in enumerate(self.cat_ids): - ids_in_cat |= set(self.coco.cat_img_map[class_id]) - # merge the image id sets of the two conditions and use the merged set - # to filter out images if self.filter_empty_gt=True - ids_in_cat &= ids_with_ann - - valid_img_ids = [] - for i, img_info in enumerate(self.data_infos): - img_id = self.img_ids[i] - if self.filter_empty_gt and img_id not in ids_in_cat: - continue - if min(img_info["width"], img_info["height"]) >= min_size: - valid_inds.append(i) - valid_img_ids.append(img_id) - self.img_ids = valid_img_ids - return valid_inds - - def _parse_ann_info(self, img_info, ann_info): - gt_bboxes = [] - gt_labels = [] - gt_bboxes_ignore = [] - gt_masks_ann = [] - for ann in ann_info: - if ann.get("ignore", False): - continue - x1, y1, w, h = ann["bbox"] - inter_w = max(0, min(x1 + w, img_info["width"]) - max(x1, 0)) - inter_h = max(0, min(y1 + h, img_info["height"]) - max(y1, 0)) - if inter_w * inter_h == 0: - continue - if ann["area"] <= 0 or w < 1 or h < 1: - continue - if self.min_size is not None: - if w < self.min_size or h < self.min_size: - continue - if ann["category_id"] not in self.cat_ids: - continue - bbox = [x1, y1, x1 + w, y1 + h] - if ann.get("iscrowd", False): - gt_bboxes_ignore.append(bbox) - else: - gt_bboxes.append(bbox) - gt_labels.append(self.cat2label[ann["category_id"]]) - gt_masks_ann.append(ann.get("segmentation", None)) - - if gt_bboxes: - gt_bboxes = np.array(gt_bboxes, dtype=np.float32) - gt_labels = np.array(gt_labels, dtype=np.int64) - else: - gt_bboxes = np.zeros((0, 4), dtype=np.float32) - gt_labels = np.array([], dtype=np.int64) - - if gt_bboxes_ignore: - gt_bboxes_ignore = np.array(gt_bboxes_ignore, dtype=np.float32) - else: - gt_bboxes_ignore = np.zeros((0, 4), dtype=np.float32) - - seg_map = img_info["filename"].replace("jpg", "png") - - ann = dict( - bboxes=gt_bboxes, - labels=gt_labels, - bboxes_ignore=gt_bboxes_ignore, - masks=gt_masks_ann, - seg_map=seg_map, - ) - - return ann - - -def find_label_by_name(labels, name, domain): - matching_labels = [label for label in labels if label.name == name] - if len(matching_labels) == 1: - return matching_labels[0] - elif len(matching_labels) == 0: - label = LabelEntity(name=name, domain=domain, id=ID(len(labels))) - labels.append(label) - return label - else: - raise ValueError("Found multiple matching labels") - - -def load_dataset_items_coco_format( - ann_file_path: str, - data_root_dir: str, - domain: Domain, - subset: Subset = Subset.NONE, - labels_list: Optional[List[LabelEntity]] = None, - with_mask: bool = False, -): - test_mode = subset in {Subset.VALIDATION, Subset.TESTING} - - coco_dataset = CocoDataset( - ann_file=ann_file_path, - data_root=data_root_dir, - classes=None, - test_mode=test_mode, - with_mask=with_mask, - ) - coco_dataset.test_mode = False - for label_name in coco_dataset.classes: - find_label_by_name(labels_list, label_name, domain) - - dataset_items = [] - for item in coco_dataset: - - def create_gt_box(x1, y1, x2, y2, label_name): - return Annotation( - Rectangle(x1=x1, y1=y1, x2=x2, y2=y2), - labels=[ScoredLabel(label=find_label_by_name(labels_list, label_name, domain))], - ) - - def create_gt_polygon(polygon_group, label_name): - if len(polygon_group) != 1: - raise RuntimeError("Complex instance segmentation masks consisting of several polygons are not supported.") - - return Annotation( - Polygon(points=polygon_group[0]), - labels=[ScoredLabel(label=find_label_by_name(labels_list, label_name, domain))], - ) - - img_height = item["img_info"].get("height") - img_width = item["img_info"].get("width") - divisor = np.array( - [img_width, img_height, img_width, img_height], - dtype=item["gt_bboxes"].dtype, - ) - bboxes = item["gt_bboxes"] / divisor - labels = item["gt_labels"] - - assert len(bboxes) == len(labels) - if with_mask: - polygons = item["gt_masks"] - assert len(bboxes) == len(polygons) - normalized_polygons = [] - for polygon_group in polygons: - normalized_polygons.append([]) - for polygon in polygon_group: - normalized_polygon = [p / divisor[i % 2] for i, p in enumerate(polygon)] - points = [Point(normalized_polygon[i], normalized_polygon[i + 1]) for i in range(0, len(polygon), 2)] - normalized_polygons[-1].append(points) - - if item["img_prefix"] is not None: - filename = osp.join(item["img_prefix"], item["img_info"]["filename"]) - else: - filename = item["img_info"]["filename"] - - if with_mask: - shapes = [ - create_gt_polygon(polygon_group, coco_dataset.classes[label_id]) - for polygon_group, label_id in zip(normalized_polygons, labels) - ] - else: - shapes = [ - create_gt_box(x1, y1, x2, y2, coco_dataset.classes[label_id]) - for (x1, y1, x2, y2), label_id in zip(bboxes, labels) - ] - - dataset_item = DatasetItemEntity( - media=Image(file_path=filename), - annotation_scene=AnnotationSceneEntity( - annotations=shapes, kind=AnnotationSceneKind.ANNOTATION - ), - subset=subset, - ) - dataset_items.append(dataset_item) - - return dataset_items - - -def get_sizes_from_dataset_entity(dataset: DatasetEntity, target_wh: list): - """ - Function to get sizes of instances in DatasetEntity and to resize it to the target size. - - :param dataset: DatasetEntity in which to get statistics - :param target_wh: target width and height of the dataset - :return list: tuples with width and height of each instance - """ - wh_stats = [] - for item in dataset: - for ann in item.get_annotations(include_empty=False): - has_detection_labels = any(label.domain == Domain.DETECTION for label in ann.get_labels(include_empty=False)) - if has_detection_labels: - box = ShapeFactory.shape_as_rectangle(ann.shape) - w = box.width * target_wh[0] - h = box.height * target_wh[1] - wh_stats.append((w, h)) - return wh_stats - - -def get_anchor_boxes(wh_stats, group_as): - from sklearn.cluster import KMeans - kmeans = KMeans(init='k-means++', n_clusters=sum(group_as), random_state=0).fit(wh_stats) - centers = kmeans.cluster_centers_ - - areas = np.sqrt(np.prod(centers, axis=1)) - idx = np.argsort(areas) - - widths = centers[idx, 0] - heights = centers[idx, 1] - - group_as = np.cumsum(group_as[:-1]) - widths, heights = np.split(widths, group_as), np.split(heights, group_as) - return widths, heights - - -def format_list_to_str(value_lists): - """ Decrease floating point digits in logs """ - str_value = '' - for value_list in value_lists: - str_value += '[' + ', '.join(f'{value:.2f}' for value in value_list) + '], ' - return f'[{str_value[:-2]}]' diff --git a/external/mmdetection/build/lib/detection_tasks/extension/datasets/mmdataset.py b/external/mmdetection/build/lib/detection_tasks/extension/datasets/mmdataset.py deleted file mode 100644 index ba7ef08c136..00000000000 --- a/external/mmdetection/build/lib/detection_tasks/extension/datasets/mmdataset.py +++ /dev/null @@ -1,214 +0,0 @@ -# Copyright (C) 2021 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. - -from copy import deepcopy -from typing import List - -import numpy as np -from ote_sdk.entities.dataset_item import DatasetItemEntity -from ote_sdk.entities.datasets import DatasetEntity -from ote_sdk.entities.label import Domain, LabelEntity -from ote_sdk.utils.shape_factory import ShapeFactory - -from mmdet.core import PolygonMasks -from mmdet.datasets.builder import DATASETS -from mmdet.datasets.custom import CustomDataset -from mmdet.datasets.pipelines import Compose - - -def get_annotation_mmdet_format( - dataset_item: DatasetItemEntity, - labels: List[LabelEntity], - domain: Domain, - min_size: int = -1, -) -> dict: - """ - Function to convert a OTE annotation to mmdetection format. This is used both in the OTEDataset class defined in - this file as in the custom pipeline element 'LoadAnnotationFromOTEDataset' - - :param dataset_item: DatasetItem for which to get annotations - :param labels: List of labels that are used in the task - :return dict: annotation information dict in mmdet format - """ - width, height = dataset_item.width, dataset_item.height - - # load annotations for item - gt_bboxes = [] - gt_labels = [] - gt_polygons = [] - - label_idx = {label.id: i for i, label in enumerate(labels)} - - for annotation in dataset_item.get_annotations(labels=labels, include_empty=False): - - box = ShapeFactory.shape_as_rectangle(annotation.shape) - - if min(box.width * width, box.height * height) < min_size: - continue - - class_indices = [ - label_idx[label.id] - for label in annotation.get_labels(include_empty=False) - if label.domain == domain - ] - - n = len(class_indices) - gt_bboxes.extend([[box.x1 * width, box.y1 * height, box.x2 * width, box.y2 * height] for _ in range(n)]) - if domain != Domain.DETECTION: - polygon = ShapeFactory.shape_as_polygon(annotation.shape) - polygon = np.array([p for point in polygon.points for p in [point.x * width, point.y * height]]) - gt_polygons.extend([[polygon] for _ in range(n)]) - gt_labels.extend(class_indices) - - if len(gt_bboxes) > 0: - ann_info = dict( - bboxes=np.array(gt_bboxes, dtype=np.float32).reshape(-1, 4), - labels=np.array(gt_labels, dtype=int), - masks=PolygonMasks( - gt_polygons, height=height, width=width) if gt_polygons else []) - else: - ann_info = dict( - bboxes=np.zeros((0, 4), dtype=np.float32), - labels=np.array([], dtype=int), - masks=[]) - return ann_info - - -@DATASETS.register_module() -class OTEDataset(CustomDataset): - """ - Wrapper that allows using a OTE dataset to train mmdetection models. This wrapper is not based on the filesystem, - but instead loads the items here directly from the OTE DatasetEntity object. - - The wrapper overwrites some methods of the CustomDataset class: prepare_train_img, prepare_test_img and prepipeline - Naming of certain attributes might seem a bit peculiar but this is due to the conventions set in CustomDataset. For - instance, CustomDatasets expects the dataset items to be stored in the attribute data_infos, which is why it is - named like that and not dataset_items. - - """ - - class _DataInfoProxy: - """ - This class is intended to be a wrapper to use it in CustomDataset-derived class as `self.data_infos`. - Instead of using list `data_infos` as in CustomDataset, our implementation of dataset OTEDataset - uses this proxy class with overriden __len__ and __getitem__; this proxy class - forwards data access operations to ote_dataset and converts the dataset items to the view - convenient for mmdetection. - """ - def __init__(self, ote_dataset, labels): - self.ote_dataset = ote_dataset - self.labels = labels - - def __len__(self): - return len(self.ote_dataset) - - def __getitem__(self, index): - """ - Prepare a dict 'data_info' that is expected by the mmdet pipeline to handle images and annotations - :return data_info: dictionary that contains the image and image metadata, as well as the labels of the objects - in the image - """ - - dataset = self.ote_dataset - item = dataset[index] - - height, width = item.height, item.width - - data_info = dict(dataset_item=item, width=width, height=height, index=index, - ann_info=dict(label_list=self.labels)) - - return data_info - - def __init__(self, ote_dataset: DatasetEntity, labels: List[LabelEntity], pipeline, domain, test_mode: bool = False): - self.ote_dataset = ote_dataset - self.labels = labels - self.CLASSES = list(label.name for label in labels) - self.domain = domain - self.test_mode = test_mode - - # Instead of using list data_infos as in CustomDataset, this implementation of dataset - # uses a proxy class with overriden __len__ and __getitem__; this proxy class - # forwards data access operations to ote_dataset. - # Note that list `data_infos` cannot be used here, since OTE dataset class does not have interface to - # get only annotation of a data item, so we would load the whole data item (including image) - # even if we need only checking aspect ratio of the image; due to it - # this implementation of dataset does not uses such tricks as skipping images with wrong aspect ratios or - # small image size, since otherwise reading the whole dataset during initialization will be required. - self.data_infos = OTEDataset._DataInfoProxy(ote_dataset, labels) - - self.proposals = None # Attribute expected by mmdet but not used for OTE datasets - - if not test_mode: - self._set_group_flag() - - self.pipeline = Compose(pipeline) - - def _set_group_flag(self): - """Set flag for grouping images. - - Originally, in Custom dataset, images with aspect ratio greater than 1 will be set as group 1, - otherwise group 0. - This implementation will set group 0 for every image. - """ - self.flag = np.zeros(len(self), dtype=np.uint8) - - def _rand_another(self, idx): - return np.random.choice(len(self)) - - # In contrast with CustomDataset this implementation of dataset - # does not filter images w.r.t. the min size - def _filter_imgs(self, min_size=32): - raise NotImplementedError - - def prepare_train_img(self, idx: int) -> dict: - """Get training data and annotations after pipeline. - - :param idx: int, Index of data. - :return dict: Training data and annotation after pipeline with new keys introduced by pipeline. - """ - item = deepcopy(self.data_infos[idx]) - self.pre_pipeline(item) - return self.pipeline(item) - - def prepare_test_img(self, idx: int) -> dict: - """Get testing data after pipeline. - - :param idx: int, Index of data. - :return dict: Testing data after pipeline with new keys introduced by pipeline. - """ - # FIXME. - # item = deepcopy(self.data_infos[idx]) - item = self.data_infos[idx] - self.pre_pipeline(item) - return self.pipeline(item) - - @staticmethod - def pre_pipeline(results: dict): - """Prepare results dict for pipeline. Add expected keys to the dict. """ - results['bbox_fields'] = [] - results['mask_fields'] = [] - results['seg_fields'] = [] - - def get_ann_info(self, idx): - """ - This method is used for evaluation of predictions. The CustomDataset class implements a method - CustomDataset.evaluate, which uses the class method get_ann_info to retrieve annotations. - - :param idx: index of the dataset item for which to get the annotations - :return ann_info: dict that contains the coordinates of the bboxes and their corresponding labels - """ - dataset_item = self.ote_dataset[idx] - labels = self.labels - return get_annotation_mmdet_format(dataset_item, labels, self.domain) - diff --git a/external/mmdetection/build/lib/detection_tasks/extension/utils/__init__.py b/external/mmdetection/build/lib/detection_tasks/extension/utils/__init__.py deleted file mode 100644 index 144196cf57b..00000000000 --- a/external/mmdetection/build/lib/detection_tasks/extension/utils/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (C) 2021-2022 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. - -from .hooks import CancelTrainingHook, FixedMomentumUpdaterHook, OTELoggerHook, OTEProgressHook -from .hooks import EarlyStoppingHook, ReduceLROnPlateauLrUpdaterHook, StopLossNanTrainingHook -from .pipelines import LoadImageFromOTEDataset, LoadAnnotationFromOTEDataset -from .runner import EpochRunnerWithCancel - -__all__ = [CancelTrainingHook, FixedMomentumUpdaterHook, LoadImageFromOTEDataset, EpochRunnerWithCancel, - LoadAnnotationFromOTEDataset, OTELoggerHook, OTEProgressHook, EarlyStoppingHook, - ReduceLROnPlateauLrUpdaterHook, StopLossNanTrainingHook] diff --git a/external/mmdetection/build/lib/detection_tasks/extension/utils/hooks.py b/external/mmdetection/build/lib/detection_tasks/extension/utils/hooks.py deleted file mode 100644 index dbde2753aa0..00000000000 --- a/external/mmdetection/build/lib/detection_tasks/extension/utils/hooks.py +++ /dev/null @@ -1,503 +0,0 @@ -# Copyright (C) 2021-2022 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. - -import logging -import math -import os -from math import inf, isnan -from collections import defaultdict - -from mmcv.runner.hooks import HOOKS, Hook, LoggerHook, LrUpdaterHook -from mmcv.runner import BaseRunner, EpochBasedRunner -from mmcv.runner.dist_utils import master_only -from mmcv.utils import print_log - -from mmdet.utils.logger import get_root_logger - - -logger = get_root_logger() - - -@HOOKS.register_module() -class CancelTrainingHook(Hook): - def __init__(self, interval: int = 5): - """ - Periodically check whether whether a stop signal is sent to the runner during model training. - Every 'check_interval' iterations, the work_dir for the runner is checked to see if a file '.stop_training' - is present. If it is, training is stopped. - - :param interval: Period for checking for stop signal, given in iterations. - - """ - self.interval = interval - - @staticmethod - def _check_for_stop_signal(runner: BaseRunner): - work_dir = runner.work_dir - stop_filepath = os.path.join(work_dir, '.stop_training') - if os.path.exists(stop_filepath): - if isinstance(runner, EpochBasedRunner): - epoch = runner.epoch - runner._max_epochs = epoch # Force runner to stop by pretending it has reached it's max_epoch - runner.should_stop = True # Set this flag to true to stop the current training epoch - os.remove(stop_filepath) - - def after_train_iter(self, runner: BaseRunner): - if not self.every_n_iters(runner, self.interval): - return - self._check_for_stop_signal(runner) - - -@HOOKS.register_module() -class FixedMomentumUpdaterHook(Hook): - def __init__(self): - """ - This hook does nothing, as the momentum is fixed by default. The hook is here to streamline switching between - different LR schedules. - """ - pass - - def before_run(self, runner): - pass - - -@HOOKS.register_module() -class EnsureCorrectBestCheckpointHook(Hook): - def __init__(self): - """ - This hook makes sure that the 'best_mAP' checkpoint points properly to the best model, even if the best model is - created in the last epoch. - """ - pass - - def after_run(self, runner): - runner.call_hook('after_train_epoch') - - -@HOOKS.register_module() -class OTELoggerHook(LoggerHook): - - class Curve: - def __init__(self): - self.x = [] - self.y = [] - - def __repr__(self): - points = [] - for x, y in zip(self.x, self.y): - points.append(f'({x},{y})') - return 'curve[' + ','.join(points) + ']' - - def __init__(self, - curves=None, - interval=10, - ignore_last=True, - reset_flag=True, - by_epoch=True): - super().__init__(interval, ignore_last, reset_flag, by_epoch) - self.curves = curves if curves is not None else defaultdict(self.Curve) - - @master_only - def log(self, runner): - tags = self.get_loggable_tags(runner, allow_text=False) - if runner.max_epochs is not None: - normalized_iter = self.get_iter(runner) / runner.max_iters * runner.max_epochs - else: - normalized_iter = self.get_iter(runner) - for tag, value in tags.items(): - curve = self.curves[tag] - # Remove duplicates. - if len(curve.x) > 0 and curve.x[-1] == normalized_iter: - curve.x.pop() - curve.y.pop() - curve.x.append(normalized_iter) - curve.y.append(value) - - def after_train_epoch(self, runner): - # Iteration counter is increased right after the last iteration in the epoch, - # temporarily decrease it back. - runner._iter -= 1 - super().after_train_epoch(runner) - runner._iter += 1 - - -@HOOKS.register_module() -class OTEProgressHook(Hook): - def __init__(self, time_monitor, verbose=False): - super().__init__() - self.time_monitor = time_monitor - self.verbose = verbose - self.print_threshold = 1 - - def before_run(self, runner): - total_epochs = runner.max_epochs if runner.max_epochs is not None else 1 - self.time_monitor.total_epochs = total_epochs - self.time_monitor.train_steps = runner.max_iters // total_epochs if total_epochs else 1 - self.time_monitor.steps_per_epoch = self.time_monitor.train_steps + self.time_monitor.val_steps - self.time_monitor.total_steps = max(math.ceil(self.time_monitor.steps_per_epoch * total_epochs), 1) - self.time_monitor.current_step = 0 - self.time_monitor.current_epoch = 0 - self.time_monitor.on_train_begin() - - def before_epoch(self, runner): - self.time_monitor.on_epoch_begin(runner.epoch) - - def after_epoch(self, runner): - self.time_monitor.on_epoch_end(runner.epoch, runner.log_buffer.output) - - def before_iter(self, runner): - self.time_monitor.on_train_batch_begin(1) - - def after_iter(self, runner): - self.time_monitor.on_train_batch_end(1) - if self.verbose: - progress = self.progress - if progress >= self.print_threshold: - logger.warning(f'training progress {progress:.0f}%') - self.print_threshold = (progress + 10) // 10 * 10 - - def before_val_iter(self, runner): - self.time_monitor.on_test_batch_begin(1) - - def after_val_iter(self, runner): - self.time_monitor.on_test_batch_end(1) - - def after_run(self, runner): - self.time_monitor.on_train_end(1) - self.time_monitor.update_progress_callback(int(self.time_monitor.get_progress())) - - @property - def progress(self): - return self.time_monitor.get_progress() - - -@HOOKS.register_module() -class EarlyStoppingHook(Hook): - """ - Cancel training when a metric has stopped improving. - - Early Stopping hook monitors a metric quantity and if no improvement is seen for a ‘patience’ - number of epochs, the training is cancelled. - - :param interval: the number of intervals for checking early stop. The interval number should be - the same as the evaluation interval - the `interval` variable set in - `evaluation` config. - :param metric: the metric name to be monitored - :param rule: greater or less. In `less` mode, training will stop when the metric has stopped - decreasing and in `greater` mode it will stop when the metric has stopped - increasing. - :param patience: Number of epochs with no improvement after which the training will be reduced. - For example, if patience = 2, then we will ignore the first 2 epochs with no - improvement, and will only cancel the training after the 3rd epoch if the - metric still hasn’t improved then - :param iteration_patience: Number of iterations must be trained after the last improvement - before training stops. The same as patience but the training - continues if the number of iteration is lower than iteration_patience - This variable makes sure a model is trained enough for some - iterations after the last improvement before stopping. - :param min_delta: Minimal decay applied to lr. If the difference between new and old lr is - smaller than eps, the update is ignored - """ - rule_map = {'greater': lambda x, y: x > y, 'less': lambda x, y: x < y} - init_value_map = {'greater': -inf, 'less': inf} - greater_keys = [ - 'acc', 'top', 'AR@', 'auc', 'precision', 'mAP', 'mDice', 'mIoU', - 'mAcc', 'aAcc' - ] - less_keys = ['loss'] - - def __init__(self, - interval: int, - metric: str = 'bbox_mAP', - rule: str = None, - patience: int = 5, - iteration_patience: int = 500, - min_delta: float = 0.0): - super().__init__() - self.patience = patience - self.iteration_patience = iteration_patience - self.interval = interval - self.min_delta = min_delta - self._init_rule(rule, metric) - - self.min_delta *= 1 if self.rule == 'greater' else -1 - self.last_iter = 0 - self.wait_count = 0 - self.best_score = self.init_value_map[self.rule] - - def _init_rule(self, rule, key_indicator): - """Initialize rule, key_indicator, comparison_func, and best score. - - Here is the rule to determine which rule is used for key indicator - when the rule is not specific: - 1. If the key indicator is in ``self.greater_keys``, the rule will be - specified as 'greater'. - 2. Or if the key indicator is in ``self.less_keys``, the rule will be - specified as 'less'. - 3. Or if the key indicator is equal to the substring in any one item - in ``self.greater_keys``, the rule will be specified as 'greater'. - 4. Or if the key indicator is equal to the substring in any one item - in ``self.less_keys``, the rule will be specified as 'less'. - - Args: - rule (str | None): Comparison rule for best score. - key_indicator (str | None): Key indicator to determine the - comparison rule. - """ - if rule not in self.rule_map and rule is not None: - raise KeyError(f'rule must be greater, less or None, ' - f'but got {rule}.') - - if rule is None: - if key_indicator in self.greater_keys or any( - key in key_indicator for key in self.greater_keys): - rule = 'greater' - elif key_indicator in self.less_keys or any( - key in key_indicator for key in self.less_keys): - rule = 'less' - else: - raise ValueError(f'Cannot infer the rule for key ' - f'{key_indicator}, thus a specific rule ' - f'must be specified.') - self.rule = rule - self.key_indicator = key_indicator - self.compare_func = self.rule_map[self.rule] - - def before_run(self, runner): - self.by_epoch = False if runner.max_epochs is None else True - for hook in runner.hooks: - if isinstance(hook, LrUpdaterHook): - self.warmup_iters = hook.warmup_iters - break - - def after_train_iter(self, runner): - """Called after every training iter to evaluate the results.""" - if not self.by_epoch: - self._do_check_stopping(runner) - - def after_train_epoch(self, runner): - """Called after every training epoch to evaluate the results.""" - if self.by_epoch: - self._do_check_stopping(runner) - - def _do_check_stopping(self, runner): - if not self._should_check_stopping( - runner) or self.warmup_iters > runner.iter: - return - - if runner.rank == 0: - if self.key_indicator not in runner.log_buffer.output: - raise KeyError( - f'metric {self.key_indicator} does not exist in buffer. Please check ' - f'{self.key_indicator} is cached in evaluation output buffer' - ) - - key_score = runner.log_buffer.output[self.key_indicator] - if self.compare_func(key_score - self.min_delta, self.best_score): - self.best_score = key_score - self.wait_count = 0 - self.last_iter = runner.iter - else: - self.wait_count += 1 - if self.wait_count >= self.patience: - if runner.iter - self.last_iter < self.iteration_patience: - print_log( - f"\nSkip early stopping. Accumulated iteration " - f"{runner.iter - self.last_iter} from the last " - f"improvement must be larger than {self.iteration_patience} to trigger " - f"Early Stopping.", - logger=runner.logger) - return - stop_point = runner.epoch if self.by_epoch else runner.iter - print_log( - f"\nEarly Stopping at :{stop_point} with " - f"best {self.key_indicator}: {self.best_score}", - logger=runner.logger) - runner.should_stop = True - - def _should_check_stopping(self, runner): - check_time = self.every_n_epochs if self.by_epoch else self.every_n_iters - if not check_time(runner, self.interval): - # No evaluation during the interval. - return False - return True - - -@HOOKS.register_module() -class ReduceLROnPlateauLrUpdaterHook(LrUpdaterHook): - """ - Reduce learning rate when a metric has stopped improving. - - Models often benefit from reducing the learning rate by a factor of 2-10 once learning stagnates. - This scheduler reads a metrics quantity and if no improvement is seen for a ‘patience’ - number of epochs, the learning rate is reduced. - - :param min_lr: minimum learning rate. The lower bound of the desired learning rate. - :param interval: the number of intervals for checking the hook. The interval number should be - the same as the evaluation interval - the `interval` variable set in - `evaluation` config. - :param metric: the metric name to be monitored - :param rule: greater or less. In `less` mode, learning rate will be dropped if the metric has - stopped decreasing and in `greater` mode it will be dropped when the metric has - stopped increasing. - :param patience: Number of epochs with no improvement after which learning rate will be reduced. - For example, if patience = 2, then we will ignore the first 2 epochs with no - improvement, and will only drop LR after the 3rd epoch if the metric still - hasn’t improved then - :param iteration_patience: Number of iterations must be trained after the last improvement - before LR drops. The same as patience but the LR remains the same if - the number of iteration is lower than iteration_patience. This - variable makes sure a model is trained enough for some iterations - after the last improvement before dropping the LR. - :param factor: Factor to be multiply with the learning rate. - For example, new_lr = current_lr * factor - """ - rule_map = {'greater': lambda x, y: x > y, 'less': lambda x, y: x < y} - init_value_map = {'greater': -inf, 'less': inf} - greater_keys = [ - 'acc', 'top', 'AR@', 'auc', 'precision', 'mAP', 'mDice', 'mIoU', - 'mAcc', 'aAcc' - ] - less_keys = ['loss'] - - def __init__(self, - min_lr, - interval, - metric='bbox_mAP', - rule=None, - factor=0.1, - patience=3, - iteration_patience=300, - **kwargs): - super().__init__(**kwargs) - self.interval = interval - self.min_lr = min_lr - self.factor = factor - self.patience = patience - self.iteration_patience = iteration_patience - self.metric = metric - self.bad_count = 0 - self.last_iter = 0 - self.current_lr = None - self._init_rule(rule, metric) - self.best_score = self.init_value_map[self.rule] - - def _init_rule(self, rule, key_indicator): - """Initialize rule, key_indicator, comparison_func, and best score. - - Here is the rule to determine which rule is used for key indicator - when the rule is not specific: - 1. If the key indicator is in ``self.greater_keys``, the rule will be - specified as 'greater'. - 2. Or if the key indicator is in ``self.less_keys``, the rule will be - specified as 'less'. - 3. Or if the key indicator is equal to the substring in any one item - in ``self.greater_keys``, the rule will be specified as 'greater'. - 4. Or if the key indicator is equal to the substring in any one item - in ``self.less_keys``, the rule will be specified as 'less'. - - Args: - rule (str | None): Comparison rule for best score. - key_indicator (str | None): Key indicator to determine the - comparison rule. - """ - if rule not in self.rule_map and rule is not None: - raise KeyError(f'rule must be greater, less or None, ' - f'but got {rule}.') - - if rule is None: - if key_indicator in self.greater_keys or any( - key in key_indicator for key in self.greater_keys): - rule = 'greater' - elif key_indicator in self.less_keys or any( - key in key_indicator for key in self.less_keys): - rule = 'less' - else: - raise ValueError(f'Cannot infer the rule for key ' - f'{key_indicator}, thus a specific rule ' - f'must be specified.') - self.rule = rule - self.key_indicator = key_indicator - self.compare_func = self.rule_map[self.rule] - - def _should_check_stopping(self, runner): - check_time = self.every_n_epochs if self.by_epoch else self.every_n_iters - if not check_time(runner, self.interval): - # No evaluation during the interval. - return False - return True - - def get_lr(self, runner, base_lr): - if not self._should_check_stopping( - runner) or self.warmup_iters > runner.iter: - return base_lr - - if self.current_lr is None: - self.current_lr = base_lr - - if hasattr(runner, self.metric): - score = getattr(runner, self.metric, 0.0) - else: - return self.current_lr - - print_log( - f"\nBest Score: {self.best_score}, Current Score: {score}, Patience: {self.patience} " - f"Count: {self.bad_count}", - logger=runner.logger) - if self.compare_func(score, self.best_score): - self.best_score = score - self.bad_count = 0 - self.last_iter = runner.iter - else: - self.bad_count += 1 - - if self.bad_count >= self.patience: - if runner.iter - self.last_iter < self.iteration_patience: - print_log( - f"\nSkip LR dropping. Accumulated iteration " - f"{runner.iter - self.last_iter} from the last " - f"improvement must be larger than {self.iteration_patience} to trigger " - f"LR dropping.", - logger=runner.logger) - return self.current_lr - self.last_iter = runner.iter - self.bad_count = 0 - print_log( - f"\nDrop LR from: {self.current_lr}, to: " - f"{max(self.current_lr * self.factor, self.min_lr)}", - logger=runner.logger) - self.current_lr = max(self.current_lr * self.factor, self.min_lr) - return self.current_lr - - def before_run(self, runner): - # TODO: remove overloaded method after fixing the issue - # https://github.com/open-mmlab/mmdetection/issues/6572 - for group in runner.optimizer.param_groups: - group.setdefault('initial_lr', group['lr']) - self.base_lr = [ - group['lr'] for group in runner.optimizer.param_groups - ] - self.bad_count = 0 - self.last_iter = 0 - self.current_lr = None - self.best_score = self.init_value_map[self.rule] - - -@HOOKS.register_module() -class StopLossNanTrainingHook(Hook): - - def after_train_iter(self, runner): - if isnan(runner.outputs['loss'].item()): - logger.warning(f"Early Stopping since loss is NaN") - runner.should_stop = True diff --git a/external/mmdetection/build/lib/detection_tasks/extension/utils/pipelines.py b/external/mmdetection/build/lib/detection_tasks/extension/utils/pipelines.py deleted file mode 100644 index 82629e49973..00000000000 --- a/external/mmdetection/build/lib/detection_tasks/extension/utils/pipelines.py +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright (C) 2021 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. - -import copy - -import numpy as np - -from mmdet.datasets.builder import PIPELINES - -from ..datasets import get_annotation_mmdet_format - - -@PIPELINES.register_module() -class LoadImageFromOTEDataset: - """ - Pipeline element that loads an image from a OTE Dataset on the fly. Can do conversion to float 32 if needed. - - Expected entries in the 'results' dict that should be passed to this pipeline element are: - results['dataset_item']: dataset_item from which to load the image - results['dataset_id']: id of the dataset to which the item belongs - results['index']: index of the item in the dataset - - :param to_float32: optional bool, True to convert images to fp32. defaults to False - """ - - def __init__(self, to_float32: bool = False): - self.to_float32 = to_float32 - - def __call__(self, results): - dataset_item = results['dataset_item'] - img = dataset_item.numpy - shape = img.shape - - assert img.shape[0] == results['height'], f"{img.shape[0]} != {results['height']}" - assert img.shape[1] == results['width'], f"{img.shape[1]} != {results['width']}" - - filename = f"Dataset item index {results['index']}" - results['filename'] = filename - results['ori_filename'] = filename - results['img'] = img - results['img_shape'] = shape - results['ori_shape'] = shape - # Set initial values for default meta_keys - results['pad_shape'] = shape - num_channels = 1 if len(shape) < 3 else shape[2] - results['img_norm_cfg'] = dict( - mean=np.zeros(num_channels, dtype=np.float32), - std=np.ones(num_channels, dtype=np.float32), - to_rgb=False) - results['img_fields'] = ['img'] - - if self.to_float32: - results['img'] = results['img'].astype(np.float32) - - return results - - -@PIPELINES.register_module() -class LoadAnnotationFromOTEDataset: - """ - Pipeline element that loads an annotation from a OTE Dataset on the fly. - - Expected entries in the 'results' dict that should be passed to this pipeline element are: - results['dataset_item']: dataset_item from which to load the annotation - results['ann_info']['label_list']: list of all labels in the project - - """ - - def __init__(self, min_size : int, with_bbox: bool = True, with_label: bool = True, with_mask: bool = False, with_seg: bool = False, - poly2mask: bool = True, with_text: bool = False, domain=None): - self.with_bbox = with_bbox - self.with_label = with_label - self.with_mask = with_mask - self.with_seg = with_seg - self.poly2mask = poly2mask - self.with_text = with_text - self.domain = domain - self.min_size = min_size - - @staticmethod - def _load_bboxes(results, ann_info): - results['bbox_fields'].append('gt_bboxes') - results['gt_bboxes'] = copy.deepcopy(ann_info['bboxes']) - return results - - @staticmethod - def _load_labels(results, ann_info): - results['gt_labels'] = copy.deepcopy(ann_info['labels']) - return results - - @staticmethod - def _load_masks(results, ann_info): - results['mask_fields'].append('gt_masks') - results['gt_masks'] = copy.deepcopy(ann_info['masks']) - return results - - def __call__(self, results): - dataset_item = results['dataset_item'] - label_list = results['ann_info']['label_list'] - ann_info = get_annotation_mmdet_format(dataset_item, label_list, self.domain, self.min_size) - if self.with_bbox: - results = self._load_bboxes(results, ann_info) - if results is None or len(results['gt_bboxes']) == 0: - return None - if self.with_label: - results = self._load_labels(results, ann_info) - if self.with_mask: - results = self._load_masks(results, ann_info) - return results diff --git a/external/mmdetection/build/lib/detection_tasks/extension/utils/runner.py b/external/mmdetection/build/lib/detection_tasks/extension/utils/runner.py deleted file mode 100644 index 94889b2d1d2..00000000000 --- a/external/mmdetection/build/lib/detection_tasks/extension/utils/runner.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright (c) 2018-2021 OpenMMLab -# SPDX-License-Identifier: Apache-2.0 -# -# Copyright (C) 2020-2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -# Is based on -# * https://github.com/open-mmlab/mmcv/blob/master/mmcv/runner/epoch_based_runner.py -# * https://github.com/open-mmlab/mmcv/blob/master/mmcv/runner/iter_based_runner.py - -import time -import warnings - -import mmcv -import torch.distributed as dist -from mmcv.runner.utils import get_host_info -from mmcv.runner import RUNNERS, EpochBasedRunner, IterBasedRunner, IterLoader, get_dist_info - - -@RUNNERS.register_module() -class EpochRunnerWithCancel(EpochBasedRunner): - """ - Simple modification to EpochBasedRunner to allow cancelling the training during an epoch. - A stopping hook should set the runner.should_stop flag to True if stopping is required. - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.should_stop = False - _, world_size = get_dist_info() - self.distributed = True if world_size > 1 else False - - def stop(self) -> bool: - """ Returning a boolean to break the training loop - This method supports distributed training by broadcasting should_stop to other ranks - :return: a cancellation bool - """ - broadcast_obj = [False] - if self.rank == 0 and self.should_stop: - broadcast_obj = [True] - - if self.distributed: - dist.broadcast_object_list(broadcast_obj, src=0) - if broadcast_obj[0]: - self._max_epochs = self.epoch - return broadcast_obj[0] - - def train(self, data_loader, **kwargs): - self.model.train() - self.mode = 'train' - self.data_loader = data_loader - self._max_iters = self._max_epochs * len(self.data_loader) - self.call_hook('before_train_epoch') - time.sleep(2) # Prevent possible deadlock during epoch transition - for i, data_batch in enumerate(self.data_loader): - self._inner_iter = i - self.call_hook('before_train_iter') - self.run_iter(data_batch, train_mode=True, **kwargs) - self.call_hook('after_train_iter') - if self.stop(): - break - self._iter += 1 - self.call_hook('after_train_epoch') - self.stop() - self._epoch += 1 - - -@RUNNERS.register_module() -class IterBasedRunnerWithCancel(IterBasedRunner): - """ - Simple modification to IterBasedRunner to allow cancelling the training. The cancel training hook - should set the runner.should_stop flag to True if stopping is required. - - # TODO: Implement cancelling of training via keyboard interrupt signal, instead of should_stop - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.should_stop = False - - def main_loop(self, workflow, iter_loaders, **kwargs): - while self.iter < self._max_iters: - for i, flow in enumerate(workflow): - self._inner_iter = 0 - mode, iters = flow - if not isinstance(mode, str) or not hasattr(self, mode): - raise ValueError( - 'runner has no method named "{}" to run a workflow'. - format(mode)) - iter_runner = getattr(self, mode) - for _ in range(iters): - if mode == 'train' and self.iter >= self._max_iters: - break - iter_runner(iter_loaders[i], **kwargs) - if self.should_stop: - return - - def run(self, data_loaders, workflow, max_iters=None, **kwargs): - assert isinstance(data_loaders, list) - assert mmcv.is_list_of(workflow, tuple) - assert len(data_loaders) == len(workflow) - if max_iters is not None: - warnings.warn( - 'setting max_iters in run is deprecated, ' - 'please set max_iters in runner_config', DeprecationWarning) - self._max_iters = max_iters - assert self._max_iters is not None, ( - 'max_iters must be specified during instantiation') - - work_dir = self.work_dir if self.work_dir is not None else 'NONE' - self.logger.info('Start running, host: %s, work_dir: %s', - get_host_info(), work_dir) - self.logger.info('workflow: %s, max: %d iters', workflow, - self._max_iters) - self.call_hook('before_run') - - iter_loaders = [IterLoader(x) for x in data_loaders] - - self.call_hook('before_epoch') - - self.should_stop = False - self.main_loop(workflow, iter_loaders, **kwargs) - self.should_stop = False - - time.sleep(1) # wait for some hooks like loggers to finish - self.call_hook('after_epoch') - self.call_hook('after_run') diff --git a/external/mmdetection/dist/detection_tasks-0.0.0-py3.8.egg b/external/mmdetection/dist/detection_tasks-0.0.0-py3.8.egg deleted file mode 100644 index fb1e0ec450d36430cdcb17413c7cb4035e45386a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 108797 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 From f2c8525cd0d5560011166b55b4ef2e8fecce1d7e Mon Sep 17 00:00:00 2001 From: AlbertvanHouten Date: Tue, 17 May 2022 09:17:18 +0200 Subject: [PATCH 09/11] revert subset test update --- ote_sdk/ote_sdk/tests/entities/test_subset.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/ote_sdk/ote_sdk/tests/entities/test_subset.py b/ote_sdk/ote_sdk/tests/entities/test_subset.py index ca00c0b7b5d..a5ace251aea 100644 --- a/ote_sdk/ote_sdk/tests/entities/test_subset.py +++ b/ote_sdk/ote_sdk/tests/entities/test_subset.py @@ -43,7 +43,6 @@ def test_subset_members(self): TESTING = 3 UNLABELED = 4 PSEUDOLABELED = 5 - UNASSIGNED = 6 Steps 1. Create enum instance @@ -51,14 +50,14 @@ def test_subset_members(self): """ test_instance = Subset - for i in range(0, 7): + for i in range(0, 6): assert test_instance(i) in list(Subset) with pytest.raises(AttributeError): test_instance.WRONG with pytest.raises(ValueError): - test_instance(7) + test_instance(6) @pytest.mark.priority_medium @pytest.mark.unit @@ -82,14 +81,14 @@ def test_subset_magic_str(self): test_instance = Subset magic_str_list = [str(i) for i in list(Subset)] - for i in range(0, 7): + for i in range(0, 6): assert str(test_instance(i)) in magic_str_list with pytest.raises(AttributeError): str(test_instance.WRONG) with pytest.raises(ValueError): - str(test_instance(7)) + str(test_instance(6)) assert len(set(magic_str_list)) == len(magic_str_list) @@ -114,13 +113,13 @@ def test_subset_magic_repr(self): test_instance = Subset magic_repr_list = [repr(i) for i in list(Subset)] - for i in range(0, 7): + for i in range(0, 6): assert repr(test_instance(i)) in magic_repr_list with pytest.raises(AttributeError): repr(test_instance.WRONG) with pytest.raises(ValueError): - repr(test_instance(7)) + repr(test_instance(6)) assert len(set(magic_repr_list)) == len(magic_repr_list) From 13d29b02c613bd4728c807910d2c79f9f8dd9423 Mon Sep 17 00:00:00 2001 From: JihwanEom Date: Mon, 23 May 2022 13:20:39 +0900 Subject: [PATCH 10/11] Update mmseg commit --- external/mmsegmentation/submodule | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/mmsegmentation/submodule b/external/mmsegmentation/submodule index 571a0d38069..515a523632d 160000 --- a/external/mmsegmentation/submodule +++ b/external/mmsegmentation/submodule @@ -1 +1 @@ -Subproject commit 571a0d38069588787916c682d94888614352972b +Subproject commit 515a523632dd5db5a476618541b3cfcdb4e9fba3 From 082b963037b2c29354fcfb0803dcf63455977c7b Mon Sep 17 00:00:00 2001 From: sooahleex Date: Thu, 26 May 2022 01:48:20 +0900 Subject: [PATCH 11/11] update mmseg submodule --- external/mmsegmentation/submodule | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/mmsegmentation/submodule b/external/mmsegmentation/submodule index 515a523632d..cb5485e24ab 160000 --- a/external/mmsegmentation/submodule +++ b/external/mmsegmentation/submodule @@ -1 +1 @@ -Subproject commit 515a523632dd5db5a476618541b3cfcdb4e9fba3 +Subproject commit cb5485e24abda4b17261b061108719b3752462f0