learning-loop-node 0.10.17__tar.gz → 0.11.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of learning-loop-node might be problematic. Click here for more details.
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/PKG-INFO +1 -1
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/data_classes/__init__.py +3 -2
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/data_classes/detections.py +5 -12
- learning_loop_node-0.11.1/learning_loop_node/data_classes/image_metadata.py +37 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/detector_logic.py +3 -3
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/detector_node.py +23 -20
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/inbox_filter/cam_observation_history.py +3 -3
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/inbox_filter/relevance_filter.py +7 -6
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/outbox.py +24 -10
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/rest/detect.py +5 -4
- learning_loop_node-0.11.1/learning_loop_node/detector/rest/upload.py +29 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/loop_communication.py +3 -3
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/conftest.py +9 -9
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/inbox_filter/test_relevance_group.py +7 -7
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/inbox_filter/test_unexpected_observations_count.py +6 -6
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/test_client_communication.py +46 -46
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/test_detector_node.py +3 -1
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/test_outbox.py +2 -2
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/test_relevance_filter.py +2 -2
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/testing_detector.py +3 -3
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/general/test_downloader.py +4 -4
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/test_helper.py +1 -2
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/io_helpers.py +4 -4
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/trainer_logic_generic.py +8 -4
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/pyproject.toml +1 -1
- learning_loop_node-0.10.17/learning_loop_node/detector/rest/upload.py +0 -21
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/README.md +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/__init__.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/annotation/__init__.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/annotation/annotator_logic.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/annotation/annotator_node.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/data_classes/annotations.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/data_classes/general.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/data_classes/socket_response.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/data_classes/training.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/data_exchanger.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/__init__.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/inbox_filter/__init__.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/rest/__init__.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/rest/about.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/rest/backdoor_controls.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/rest/model_version_control.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/rest/operation_mode.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/rest/outbox_mode.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/examples/novelty_score_updater.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/globals.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/helpers/__init__.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/helpers/environment_reader.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/helpers/gdrive_downloader.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/helpers/log_conf.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/helpers/misc.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/node.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/py.typed +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/rest.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/__init__.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/annotator/__init__.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/annotator/conftest.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/annotator/pytest.ini +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/annotator/test_annotator_node.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/__init__.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/inbox_filter/__init__.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/inbox_filter/test_observation.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/pytest.ini +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/test.jpg +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/general/__init__.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/general/conftest.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/general/pytest.ini +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/general/test_data/file_1.txt +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/general/test_data/file_2.txt +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/general/test_data/model.json +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/general/test_data_classes.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/general/test_learning_loop_node.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/__init__.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/conftest.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/pytest.ini +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/state_helper.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/states/__init__.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/states/test_state_cleanup.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/states/test_state_detecting.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/states/test_state_download_train_model.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/states/test_state_prepare.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/states/test_state_sync_confusion_matrix.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/states/test_state_train.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/states/test_state_upload_detections.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/states/test_state_upload_model.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/test_errors.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/test_trainer_states.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/testing_trainer_logic.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/__init__.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/downloader.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/exceptions.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/executor.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/rest/__init__.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/rest/backdoor_controls.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/test_executor.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/trainer_logic.py +0 -0
- {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/trainer_node.py +0 -0
{learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/data_classes/__init__.py
RENAMED
|
@@ -3,14 +3,15 @@ from .detections import (BoxDetection, ClassificationDetection, Detections, Obse
|
|
|
3
3
|
SegmentationDetection, Shape)
|
|
4
4
|
from .general import (AnnotationNodeStatus, Category, CategoryType, Context, DetectionStatus, ErrorConfiguration,
|
|
5
5
|
ModelInformation, NodeState, NodeStatus)
|
|
6
|
+
from .image_metadata import ImageMetadata
|
|
6
7
|
from .socket_response import SocketResponse
|
|
7
8
|
from .training import (Errors, Hyperparameter, Model, PretrainedModel, TrainerState, Training, TrainingData,
|
|
8
9
|
TrainingError, TrainingOut, TrainingStateData, TrainingStatus)
|
|
9
10
|
|
|
10
11
|
__all__ = [
|
|
11
12
|
'AnnotationData', 'AnnotationEventType', 'SegmentationAnnotation', 'ToolOutput', 'UserInput',
|
|
12
|
-
'BoxDetection', 'ClassificationDetection', '
|
|
13
|
-
'SegmentationDetection', 'Shape',
|
|
13
|
+
'BoxDetection', 'ClassificationDetection', 'ImageMetadata', 'Observation', 'Point', 'PointDetection',
|
|
14
|
+
'SegmentationDetection', 'Shape', 'Detections',
|
|
14
15
|
'AnnotationNodeStatus', 'Category', 'CategoryType', 'Context', 'DetectionStatus', 'ErrorConfiguration',
|
|
15
16
|
'ModelInformation', 'NodeState', 'NodeStatus',
|
|
16
17
|
'SocketResponse',
|
|
@@ -6,11 +6,13 @@ from typing import List, Optional, Union
|
|
|
6
6
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
|
|
9
|
-
# pylint: disable=too-many-instance-attributes
|
|
10
|
-
|
|
11
9
|
KWONLY_SLOTS = {'kw_only': True, 'slots': True} if sys.version_info >= (3, 10) else {}
|
|
12
10
|
|
|
13
11
|
|
|
12
|
+
def current_datetime():
|
|
13
|
+
return datetime.now().isoformat(sep='_', timespec='milliseconds')
|
|
14
|
+
|
|
15
|
+
|
|
14
16
|
@dataclass(**KWONLY_SLOTS)
|
|
15
17
|
class BoxDetection():
|
|
16
18
|
"""Coordinates according to COCO format. x,y is the top left corner of the box.
|
|
@@ -106,10 +108,6 @@ class SegmentationDetection():
|
|
|
106
108
|
return f'shape:{str(self.shape)}, c: {self.confidence:.2f} -> {self.category_name}'
|
|
107
109
|
|
|
108
110
|
|
|
109
|
-
def current_datetime():
|
|
110
|
-
return datetime.now().isoformat(sep='_', timespec='milliseconds')
|
|
111
|
-
|
|
112
|
-
|
|
113
111
|
@dataclass(**KWONLY_SLOTS)
|
|
114
112
|
class Detections():
|
|
115
113
|
box_detections: List[BoxDetection] = field(default_factory=list, metadata={
|
|
@@ -120,14 +118,9 @@ class Detections():
|
|
|
120
118
|
'description': 'List of segmentation detections'})
|
|
121
119
|
classification_detections: List[ClassificationDetection] = field(default_factory=list, metadata={
|
|
122
120
|
'description': 'List of classification detections'})
|
|
123
|
-
|
|
124
|
-
'description': 'List of tags'})
|
|
125
|
-
date: Optional[str] = field(default_factory=current_datetime, metadata={
|
|
126
|
-
'description': 'Date of the detections'})
|
|
121
|
+
|
|
127
122
|
image_id: Optional[str] = field(default=None, metadata={
|
|
128
123
|
'description': 'Image uuid'})
|
|
129
|
-
source: Optional[str] = field(default=None, metadata={
|
|
130
|
-
'description': 'Source of the detections'})
|
|
131
124
|
|
|
132
125
|
def __len__(self):
|
|
133
126
|
return len(self.box_detections) + len(self.point_detections) + len(self.segmentation_detections) + len(self.classification_detections)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
|
|
2
|
+
import sys
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
from .detections import BoxDetection, ClassificationDetection, PointDetection, SegmentationDetection
|
|
8
|
+
|
|
9
|
+
# pylint: disable=too-many-instance-attributes
|
|
10
|
+
|
|
11
|
+
KWONLY_SLOTS = {'kw_only': True, 'slots': True} if sys.version_info >= (3, 10) else {}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def current_datetime():
|
|
15
|
+
return datetime.now().isoformat(sep='_', timespec='milliseconds')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(**KWONLY_SLOTS)
|
|
19
|
+
class ImageMetadata():
|
|
20
|
+
box_detections: List[BoxDetection] = field(default_factory=list, metadata={
|
|
21
|
+
'description': 'List of box detections'})
|
|
22
|
+
point_detections: List[PointDetection] = field(default_factory=list, metadata={
|
|
23
|
+
'description': 'List of point detections'})
|
|
24
|
+
segmentation_detections: List[SegmentationDetection] = field(default_factory=list, metadata={
|
|
25
|
+
'description': 'List of segmentation detections'})
|
|
26
|
+
classification_detections: List[ClassificationDetection] = field(default_factory=list, metadata={
|
|
27
|
+
'description': 'List of classification detections'})
|
|
28
|
+
tags: List[str] = field(default_factory=list, metadata={
|
|
29
|
+
'description': 'List of tags'})
|
|
30
|
+
|
|
31
|
+
created: Optional[str] = field(default_factory=current_datetime, metadata={
|
|
32
|
+
'description': 'Creation date of the image'})
|
|
33
|
+
source: Optional[str] = field(default=None, metadata={
|
|
34
|
+
'description': 'Source of the image'})
|
|
35
|
+
|
|
36
|
+
def __len__(self):
|
|
37
|
+
return len(self.box_detections) + len(self.point_detections) + len(self.segmentation_detections) + len(self.classification_detections)
|
|
@@ -4,7 +4,7 @@ from typing import List, Optional
|
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
|
|
7
|
-
from ..data_classes import
|
|
7
|
+
from ..data_classes import ImageMetadata, ModelInformation
|
|
8
8
|
from ..globals import GLOBALS
|
|
9
9
|
|
|
10
10
|
|
|
@@ -46,13 +46,13 @@ class DetectorLogic():
|
|
|
46
46
|
def init(self):
|
|
47
47
|
"""Called when a (new) model was loaded. Initialize the model. Model information available via `self.model_info`"""
|
|
48
48
|
|
|
49
|
-
def evaluate_with_all_info(self, image: np.ndarray, tags: List[str], source: Optional[str] = None) ->
|
|
49
|
+
def evaluate_with_all_info(self, image: np.ndarray, tags: List[str], source: Optional[str] = None, creation_date: Optional[str] = None) -> ImageMetadata: # pylint: disable=unused-argument
|
|
50
50
|
"""Called by the detector node when an image should be evaluated (REST or SocketIO).
|
|
51
51
|
Tags, source come from the caller and may be used in this function.
|
|
52
52
|
By default, this function simply calls `evaluate`"""
|
|
53
53
|
return self.evaluate(image)
|
|
54
54
|
|
|
55
55
|
@abstractmethod
|
|
56
|
-
def evaluate(self, image: np.ndarray) ->
|
|
56
|
+
def evaluate(self, image: np.ndarray) -> ImageMetadata:
|
|
57
57
|
"""Evaluate the image and return the detections.
|
|
58
58
|
The object should return empty detections if it is not initialized"""
|
|
@@ -14,7 +14,7 @@ from dacite import from_dict
|
|
|
14
14
|
from fastapi.encoders import jsonable_encoder
|
|
15
15
|
from socketio import AsyncClient
|
|
16
16
|
|
|
17
|
-
from ..data_classes import Category, Context,
|
|
17
|
+
from ..data_classes import Category, Context, DetectionStatus, ImageMetadata, ModelInformation, Shape
|
|
18
18
|
from ..data_classes.socket_response import SocketResponse
|
|
19
19
|
from ..data_exchanger import DataExchanger, DownloadError
|
|
20
20
|
from ..globals import GLOBALS
|
|
@@ -140,7 +140,6 @@ class DetectorNode(Node):
|
|
|
140
140
|
|
|
141
141
|
@self.sio.event
|
|
142
142
|
async def detect(sid, data: Dict) -> Dict:
|
|
143
|
-
self.log.debug('running detect via socketio')
|
|
144
143
|
try:
|
|
145
144
|
np_image = np.frombuffer(data['image'], np.uint8)
|
|
146
145
|
det = await self.get_detections(
|
|
@@ -153,7 +152,6 @@ class DetectorNode(Node):
|
|
|
153
152
|
if det is None:
|
|
154
153
|
return {'error': 'no model loaded'}
|
|
155
154
|
detection_dict = jsonable_encoder(asdict(det))
|
|
156
|
-
self.log.debug('detect via socketio finished')
|
|
157
155
|
return detection_dict
|
|
158
156
|
except Exception as e:
|
|
159
157
|
self.log.exception('could not detect via socketio')
|
|
@@ -174,22 +172,26 @@ class DetectorNode(Node):
|
|
|
174
172
|
detection_data = data.get('detections', {})
|
|
175
173
|
if detection_data and self.detector_logic.is_initialized:
|
|
176
174
|
try:
|
|
177
|
-
|
|
175
|
+
image_metadata = from_dict(data_class=ImageMetadata, data=detection_data)
|
|
178
176
|
except Exception as e:
|
|
179
177
|
self.log.exception('could not parse detections')
|
|
180
178
|
return {'error': str(e)}
|
|
181
|
-
|
|
179
|
+
image_metadata = self.add_category_id_to_detections(self.detector_logic.model_info, image_metadata)
|
|
182
180
|
else:
|
|
183
|
-
|
|
181
|
+
image_metadata = ImageMetadata()
|
|
184
182
|
|
|
185
183
|
tags = data.get('tags', [])
|
|
186
184
|
tags.append('picked_by_system')
|
|
187
185
|
|
|
188
186
|
source = data.get('source', None)
|
|
187
|
+
creation_date = data.get('creation_date', None)
|
|
188
|
+
|
|
189
|
+
self.log.debug('running upload via socketio. tags: %s, source: %s, creation_date: %s',
|
|
190
|
+
tags, source, creation_date)
|
|
189
191
|
|
|
190
192
|
loop = asyncio.get_event_loop()
|
|
191
193
|
try:
|
|
192
|
-
await loop.run_in_executor(None, self.outbox.save, data['image'],
|
|
194
|
+
await loop.run_in_executor(None, self.outbox.save, data['image'], image_metadata, tags, source, creation_date)
|
|
193
195
|
except Exception as e:
|
|
194
196
|
self.log.exception('could not upload via socketio')
|
|
195
197
|
return {'error': str(e)}
|
|
@@ -343,7 +345,8 @@ class DetectorNode(Node):
|
|
|
343
345
|
camera_id: Optional[str],
|
|
344
346
|
tags: List[str],
|
|
345
347
|
source: Optional[str] = None,
|
|
346
|
-
autoupload: Optional[str] = None
|
|
348
|
+
autoupload: Optional[str] = None,
|
|
349
|
+
creation_date: Optional[str] = None) -> ImageMetadata:
|
|
347
350
|
""" Main processing function for the detector node when an image is received via REST or SocketIO.
|
|
348
351
|
This function infers the detections from the image, cares about uploading to the loop and returns the detections as a dictionary.
|
|
349
352
|
Note: raw_image is a numpy array of type uint8, but not in the correct shape!
|
|
@@ -351,7 +354,7 @@ class DetectorNode(Node):
|
|
|
351
354
|
|
|
352
355
|
await self.detection_lock.acquire()
|
|
353
356
|
loop = asyncio.get_event_loop()
|
|
354
|
-
detections = await loop.run_in_executor(None, self.detector_logic.evaluate_with_all_info, raw_image, tags, source)
|
|
357
|
+
detections = await loop.run_in_executor(None, self.detector_logic.evaluate_with_all_info, raw_image, tags, source, creation_date)
|
|
355
358
|
self.detection_lock.release()
|
|
356
359
|
|
|
357
360
|
fix_shape_detections(detections)
|
|
@@ -361,42 +364,42 @@ class DetectorNode(Node):
|
|
|
361
364
|
|
|
362
365
|
if autoupload is None or autoupload == 'filtered': # NOTE default is filtered
|
|
363
366
|
Thread(target=self.relevance_filter.may_upload_detections,
|
|
364
|
-
args=(detections, camera_id, raw_image, tags, source)).start()
|
|
367
|
+
args=(detections, camera_id, raw_image, tags, source, creation_date)).start()
|
|
365
368
|
elif autoupload == 'all':
|
|
366
|
-
Thread(target=self.outbox.save, args=(raw_image, detections, tags, source)).start()
|
|
369
|
+
Thread(target=self.outbox.save, args=(raw_image, detections, tags, source, creation_date)).start()
|
|
367
370
|
elif autoupload == 'disabled':
|
|
368
371
|
pass
|
|
369
372
|
else:
|
|
370
373
|
self.log.error('unknown autoupload value %s', autoupload)
|
|
371
374
|
return detections
|
|
372
375
|
|
|
373
|
-
async def upload_images(self, images: List[bytes]):
|
|
376
|
+
async def upload_images(self, images: List[bytes], source: Optional[str], creation_date: Optional[str]):
|
|
374
377
|
loop = asyncio.get_event_loop()
|
|
375
378
|
for image in images:
|
|
376
|
-
await loop.run_in_executor(None, self.outbox.save, image,
|
|
379
|
+
await loop.run_in_executor(None, self.outbox.save, image, ImageMetadata(), ['picked_by_system'], source, creation_date)
|
|
377
380
|
|
|
378
|
-
def add_category_id_to_detections(self, model_info: ModelInformation,
|
|
381
|
+
def add_category_id_to_detections(self, model_info: ModelInformation, image_metadata: ImageMetadata):
|
|
379
382
|
def find_category_id_by_name(categories: List[Category], category_name: str):
|
|
380
383
|
category_id = [category.id for category in categories if category.name == category_name]
|
|
381
384
|
return category_id[0] if category_id else ''
|
|
382
385
|
|
|
383
|
-
for box_detection in
|
|
386
|
+
for box_detection in image_metadata.box_detections:
|
|
384
387
|
category_name = box_detection.category_name
|
|
385
388
|
category_id = find_category_id_by_name(model_info.categories, category_name)
|
|
386
389
|
box_detection.category_id = category_id
|
|
387
|
-
for point_detection in
|
|
390
|
+
for point_detection in image_metadata.point_detections:
|
|
388
391
|
category_name = point_detection.category_name
|
|
389
392
|
category_id = find_category_id_by_name(model_info.categories, category_name)
|
|
390
393
|
point_detection.category_id = category_id
|
|
391
|
-
for segmentation_detection in
|
|
394
|
+
for segmentation_detection in image_metadata.segmentation_detections:
|
|
392
395
|
category_name = segmentation_detection.category_name
|
|
393
396
|
category_id = find_category_id_by_name(model_info.categories, category_name)
|
|
394
397
|
segmentation_detection.category_id = category_id
|
|
395
|
-
for classification_detection in
|
|
398
|
+
for classification_detection in image_metadata.classification_detections:
|
|
396
399
|
category_name = classification_detection.category_name
|
|
397
400
|
category_id = find_category_id_by_name(model_info.categories, category_name)
|
|
398
401
|
classification_detection.category_id = category_id
|
|
399
|
-
return
|
|
402
|
+
return image_metadata
|
|
400
403
|
|
|
401
404
|
def register_sio_events(self, sio_client: AsyncClient):
|
|
402
405
|
pass
|
|
@@ -412,7 +415,7 @@ def step_into(new_dir):
|
|
|
412
415
|
os.chdir(previous_dir)
|
|
413
416
|
|
|
414
417
|
|
|
415
|
-
def fix_shape_detections(detections:
|
|
418
|
+
def fix_shape_detections(detections: ImageMetadata):
|
|
416
419
|
# TODO This is a quick fix.. check how loop upload detections deals with this
|
|
417
420
|
for seg_detection in detections.segmentation_detections:
|
|
418
421
|
if isinstance(seg_detection.shape, Shape):
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from typing import List, Union
|
|
3
3
|
|
|
4
|
-
from learning_loop_node.data_classes import (BoxDetection, ClassificationDetection,
|
|
4
|
+
from learning_loop_node.data_classes import (BoxDetection, ClassificationDetection, ImageMetadata, Observation,
|
|
5
5
|
PointDetection, SegmentationDetection)
|
|
6
6
|
|
|
7
7
|
|
|
@@ -16,9 +16,9 @@ class CamObservationHistory:
|
|
|
16
16
|
for detection in self.recent_observations
|
|
17
17
|
if not detection.is_older_than(self.reset_time)]
|
|
18
18
|
|
|
19
|
-
def get_causes_to_upload(self,
|
|
19
|
+
def get_causes_to_upload(self, image_metadata: ImageMetadata) -> List[str]:
|
|
20
20
|
causes = set()
|
|
21
|
-
for detection in
|
|
21
|
+
for detection in image_metadata.box_detections + image_metadata.point_detections + image_metadata.segmentation_detections + image_metadata.classification_detections:
|
|
22
22
|
if isinstance(detection, SegmentationDetection):
|
|
23
23
|
# self.recent_observations.append(Observation(detection))
|
|
24
24
|
causes.add('segmentation_detection')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Dict, List, Optional
|
|
2
2
|
|
|
3
|
-
from ...data_classes.
|
|
3
|
+
from ...data_classes.image_metadata import ImageMetadata
|
|
4
4
|
from ..outbox import Outbox
|
|
5
5
|
from .cam_observation_history import CamObservationHistory
|
|
6
6
|
|
|
@@ -12,22 +12,23 @@ class RelevanceFilter():
|
|
|
12
12
|
self.outbox: Outbox = outbox
|
|
13
13
|
|
|
14
14
|
def may_upload_detections(self,
|
|
15
|
-
|
|
15
|
+
image_metadata: ImageMetadata,
|
|
16
16
|
cam_id: str,
|
|
17
17
|
raw_image: bytes,
|
|
18
18
|
tags: List[str],
|
|
19
|
-
source: Optional[str] = None
|
|
19
|
+
source: Optional[str] = None,
|
|
20
|
+
creation_date: Optional[str] = None
|
|
20
21
|
) -> List[str]:
|
|
21
22
|
for group in self.cam_histories.values():
|
|
22
23
|
group.forget_old_detections()
|
|
23
24
|
|
|
24
25
|
if cam_id not in self.cam_histories:
|
|
25
26
|
self.cam_histories[cam_id] = CamObservationHistory()
|
|
26
|
-
causes = self.cam_histories[cam_id].get_causes_to_upload(
|
|
27
|
-
if len(
|
|
27
|
+
causes = self.cam_histories[cam_id].get_causes_to_upload(image_metadata)
|
|
28
|
+
if len(image_metadata) >= 80:
|
|
28
29
|
causes.append('unexpected_observations_count')
|
|
29
30
|
if len(causes) > 0:
|
|
30
31
|
tags = tags if tags is not None else []
|
|
31
32
|
tags.extend(causes)
|
|
32
|
-
self.outbox.save(raw_image,
|
|
33
|
+
self.outbox.save(raw_image, image_metadata, tags, source, creation_date)
|
|
33
34
|
return causes
|
{learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/outbox.py
RENAMED
|
@@ -19,7 +19,7 @@ import PIL
|
|
|
19
19
|
import PIL.Image # type: ignore
|
|
20
20
|
from fastapi.encoders import jsonable_encoder
|
|
21
21
|
|
|
22
|
-
from ..data_classes import
|
|
22
|
+
from ..data_classes import ImageMetadata
|
|
23
23
|
from ..globals import GLOBALS
|
|
24
24
|
from ..helpers import environment_reader
|
|
25
25
|
|
|
@@ -56,17 +56,18 @@ class Outbox():
|
|
|
56
56
|
|
|
57
57
|
def save(self,
|
|
58
58
|
image: bytes,
|
|
59
|
-
|
|
59
|
+
image_metadata: Optional[ImageMetadata] = None,
|
|
60
60
|
tags: Optional[List[str]] = None,
|
|
61
|
-
source: Optional[str] = None
|
|
61
|
+
source: Optional[str] = None,
|
|
62
|
+
creation_date: Optional[str] = None
|
|
62
63
|
) -> None:
|
|
63
64
|
|
|
64
65
|
if not self._is_valid_jpg(image):
|
|
65
66
|
self.log.error('Invalid jpg image')
|
|
66
67
|
return
|
|
67
68
|
|
|
68
|
-
if
|
|
69
|
-
|
|
69
|
+
if image_metadata is None:
|
|
70
|
+
image_metadata = ImageMetadata()
|
|
70
71
|
if not tags:
|
|
71
72
|
tags = []
|
|
72
73
|
identifier = datetime.now().isoformat(sep='_', timespec='microseconds')
|
|
@@ -74,13 +75,17 @@ class Outbox():
|
|
|
74
75
|
self.log.error('Directory with identifier %s already exists', identifier)
|
|
75
76
|
return
|
|
76
77
|
tmp = f'{GLOBALS.data_folder}/tmp/{identifier}'
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
image_metadata.tags = tags
|
|
79
|
+
if self._is_valid_isoformat(creation_date):
|
|
80
|
+
image_metadata.created = creation_date
|
|
81
|
+
else:
|
|
82
|
+
image_metadata.created = identifier
|
|
83
|
+
|
|
84
|
+
image_metadata.source = source or 'unknown'
|
|
80
85
|
os.makedirs(tmp, exist_ok=True)
|
|
81
86
|
|
|
82
87
|
with open(tmp + '/image.json', 'w') as f:
|
|
83
|
-
json.dump(jsonable_encoder(asdict(
|
|
88
|
+
json.dump(jsonable_encoder(asdict(image_metadata)), f)
|
|
84
89
|
|
|
85
90
|
with open(tmp + '/image.jpg', 'wb') as f:
|
|
86
91
|
f.write(image)
|
|
@@ -90,6 +95,15 @@ class Outbox():
|
|
|
90
95
|
else:
|
|
91
96
|
self.log.error('Could not rename %s to %s', tmp, self.path + '/' + identifier)
|
|
92
97
|
|
|
98
|
+
def _is_valid_isoformat(self, date: Optional[str]) -> bool:
|
|
99
|
+
if date is None:
|
|
100
|
+
return False
|
|
101
|
+
try:
|
|
102
|
+
datetime.fromisoformat(date)
|
|
103
|
+
return True
|
|
104
|
+
except Exception:
|
|
105
|
+
return False
|
|
106
|
+
|
|
93
107
|
def get_data_files(self):
|
|
94
108
|
return glob(f'{self.path}/*')
|
|
95
109
|
|
|
@@ -142,7 +156,7 @@ class Outbox():
|
|
|
142
156
|
self.log.exception('Could not upload images')
|
|
143
157
|
return
|
|
144
158
|
finally:
|
|
145
|
-
self.log.
|
|
159
|
+
self.log.debug('Closing files')
|
|
146
160
|
for _, file in data:
|
|
147
161
|
file.close()
|
|
148
162
|
|
{learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/rest/detect.py
RENAMED
|
@@ -3,9 +3,8 @@ from typing import TYPE_CHECKING, Optional
|
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
from fastapi import APIRouter, File, Header, Request, UploadFile
|
|
6
|
-
from fastapi.responses import JSONResponse
|
|
7
6
|
|
|
8
|
-
from ...data_classes.
|
|
7
|
+
from ...data_classes.image_metadata import ImageMetadata
|
|
9
8
|
|
|
10
9
|
if TYPE_CHECKING:
|
|
11
10
|
from ..detector_node import DetectorNode
|
|
@@ -13,7 +12,7 @@ if TYPE_CHECKING:
|
|
|
13
12
|
router = APIRouter()
|
|
14
13
|
|
|
15
14
|
|
|
16
|
-
@router.post("/detect", response_model=
|
|
15
|
+
@router.post("/detect", response_model=ImageMetadata)
|
|
17
16
|
async def http_detect(
|
|
18
17
|
request: Request,
|
|
19
18
|
file: UploadFile = File(..., description='The image file to run detection on'),
|
|
@@ -23,6 +22,7 @@ async def http_detect(
|
|
|
23
22
|
source: Optional[str] = Header(None, description='The source of the image (used by learning loop)'),
|
|
24
23
|
autoupload: Optional[str] = Header(None, description='Mode to decide whether to upload the image to the learning loop',
|
|
25
24
|
examples=['filtered', 'all', 'disabled']),
|
|
25
|
+
creation_date: Optional[str] = Header(None, description='The creation date of the image (used by learning loop)')
|
|
26
26
|
):
|
|
27
27
|
"""
|
|
28
28
|
Single image example:
|
|
@@ -46,7 +46,8 @@ async def http_detect(
|
|
|
46
46
|
camera_id=camera_id or mac or None,
|
|
47
47
|
tags=tags.split(',') if tags else [],
|
|
48
48
|
source=source,
|
|
49
|
-
autoupload=autoupload
|
|
49
|
+
autoupload=autoupload,
|
|
50
|
+
creation_date=creation_date)
|
|
50
51
|
except Exception as exc:
|
|
51
52
|
logging.exception('Error during detection of image %s.', file.filename)
|
|
52
53
|
raise Exception(f'Error during detection of image {file.filename}.') from exc
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, File, Query, Request, UploadFile
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from ..detector_node import DetectorNode
|
|
7
|
+
|
|
8
|
+
router = APIRouter()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@router.post("/upload")
|
|
12
|
+
async def upload_image(request: Request,
|
|
13
|
+
files: List[UploadFile] = File(...),
|
|
14
|
+
source: Optional[str] = Query(None, description='Source of the image'),
|
|
15
|
+
creation_date: Optional[str] = Query(None, description='Creation date of the image')):
|
|
16
|
+
"""
|
|
17
|
+
Upload an image or multiple images to the learning loop.
|
|
18
|
+
|
|
19
|
+
The image source and the image creation date are optional query parameters.
|
|
20
|
+
Images are automatically tagged with 'picked_by_system'.
|
|
21
|
+
|
|
22
|
+
Example Usage
|
|
23
|
+
|
|
24
|
+
curl -X POST -F 'files=@test.jpg' "http://localhost:/upload?source=test&creation_date=2024-01-01T00:00:00"
|
|
25
|
+
"""
|
|
26
|
+
raw_files = [await file.read() for file in files]
|
|
27
|
+
node: DetectorNode = request.app
|
|
28
|
+
await node.upload_images(raw_files, source, creation_date)
|
|
29
|
+
return 200, "OK"
|
{learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/loop_communication.py
RENAMED
|
@@ -35,7 +35,7 @@ class LoopCommunicator():
|
|
|
35
35
|
else:
|
|
36
36
|
self.async_client = httpx.AsyncClient(base_url=self.base_url, timeout=Timeout(60.0))
|
|
37
37
|
|
|
38
|
-
logging.info(
|
|
38
|
+
logging.info('Loop interface initialized with base_url: %s / user: %s', self.base_url, self.username)
|
|
39
39
|
|
|
40
40
|
def websocket_url(self) -> str:
|
|
41
41
|
return f'ws{"s" if "learning-loop.ai" in self.host else ""}://' + self.host
|
|
@@ -48,7 +48,7 @@ class LoopCommunicator():
|
|
|
48
48
|
self.async_client.cookies.clear()
|
|
49
49
|
response = await self.async_client.post('/api/login', data={'username': self.username, 'password': self.password})
|
|
50
50
|
if response.status_code != 200:
|
|
51
|
-
logging.info(
|
|
51
|
+
logging.info('Login failed with response: %s', response)
|
|
52
52
|
raise LoopCommunicationException('Login failed with response: ' + str(response))
|
|
53
53
|
self.async_client.cookies.update(response.cookies)
|
|
54
54
|
|
|
@@ -57,7 +57,7 @@ class LoopCommunicator():
|
|
|
57
57
|
|
|
58
58
|
response = await self.async_client.post('/api/logout')
|
|
59
59
|
if response.status_code != 200:
|
|
60
|
-
logging.info(
|
|
60
|
+
logging.info('Logout failed with response: %s', response)
|
|
61
61
|
raise LoopCommunicationException('Logout failed with response: ' + str(response))
|
|
62
62
|
self.async_client.cookies.clear()
|
|
63
63
|
|
|
@@ -6,13 +6,14 @@ import shutil
|
|
|
6
6
|
import socket
|
|
7
7
|
from glob import glob
|
|
8
8
|
from multiprocessing import Process, log_to_stderr
|
|
9
|
-
from typing import AsyncGenerator
|
|
9
|
+
from typing import AsyncGenerator, List, Optional
|
|
10
10
|
|
|
11
|
+
import numpy as np
|
|
11
12
|
import pytest
|
|
12
13
|
import socketio
|
|
13
14
|
import uvicorn
|
|
14
15
|
|
|
15
|
-
from learning_loop_node.data_classes import BoxDetection,
|
|
16
|
+
from learning_loop_node.data_classes import BoxDetection, ImageMetadata
|
|
16
17
|
from learning_loop_node.detector.detector_logic import DetectorLogic
|
|
17
18
|
|
|
18
19
|
from ...detector.detector_node import DetectorNode
|
|
@@ -100,8 +101,8 @@ async def sio_client() -> AsyncGenerator[socketio.AsyncClient, None]:
|
|
|
100
101
|
try:
|
|
101
102
|
await sio.connect(f"ws://localhost:{detector_port}", socketio_path="/ws/socket.io")
|
|
102
103
|
try_connect = False
|
|
103
|
-
except Exception
|
|
104
|
-
logging.
|
|
104
|
+
except Exception:
|
|
105
|
+
logging.exception("Connection failed with error:")
|
|
105
106
|
logging.warning('trying again')
|
|
106
107
|
await asyncio.sleep(5)
|
|
107
108
|
retry_count += 1
|
|
@@ -122,21 +123,20 @@ def mock_detector_logic():
|
|
|
122
123
|
class MockDetectorLogic(DetectorLogic): # pylint: disable=abstract-method
|
|
123
124
|
def __init__(self):
|
|
124
125
|
super().__init__('mock')
|
|
125
|
-
self.
|
|
126
|
+
self.image_metadata = ImageMetadata(
|
|
126
127
|
box_detections=[BoxDetection(category_name="test",
|
|
127
128
|
category_id="1",
|
|
128
129
|
confidence=0.9,
|
|
129
130
|
x=0, y=0, width=10, height=10,
|
|
130
131
|
model_name="mock",
|
|
131
|
-
)]
|
|
132
|
-
)
|
|
132
|
+
)])
|
|
133
133
|
|
|
134
134
|
@property
|
|
135
135
|
def is_initialized(self):
|
|
136
136
|
return True
|
|
137
137
|
|
|
138
|
-
def evaluate_with_all_info(self, image, tags, source
|
|
139
|
-
return self.
|
|
138
|
+
def evaluate_with_all_info(self, image: np.ndarray, tags: List[str], source: Optional[str] = None, creation_date: Optional[str] = None):
|
|
139
|
+
return self.image_metadata
|
|
140
140
|
|
|
141
141
|
return MockDetectorLogic()
|
|
142
142
|
|
|
@@ -5,7 +5,7 @@ from typing import List
|
|
|
5
5
|
|
|
6
6
|
from dacite import from_dict
|
|
7
7
|
|
|
8
|
-
from ....data_classes
|
|
8
|
+
from ....data_classes import BoxDetection, ImageMetadata, Point, PointDetection, SegmentationDetection, Shape
|
|
9
9
|
from ....detector.inbox_filter.cam_observation_history import CamObservationHistory
|
|
10
10
|
|
|
11
11
|
dirt_detection = BoxDetection(category_name='dirt', x=0, y=0, width=100, height=100,
|
|
@@ -18,16 +18,16 @@ conf_too_low_detection = BoxDetection(category_name='dirt', x=0, y=0, width=100,
|
|
|
18
18
|
height=100, category_id='xyz', model_name='test_model', confidence=.29)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def det_from_boxes(box_detections: List[BoxDetection]) ->
|
|
22
|
-
return
|
|
21
|
+
def det_from_boxes(box_detections: List[BoxDetection]) -> ImageMetadata:
|
|
22
|
+
return ImageMetadata(box_detections=box_detections)
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
def det_from_points(point_detections: List[PointDetection]) ->
|
|
26
|
-
return
|
|
25
|
+
def det_from_points(point_detections: List[PointDetection]) -> ImageMetadata:
|
|
26
|
+
return ImageMetadata(point_detections=point_detections)
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def det_from_seg(seg_detections: List[SegmentationDetection]) ->
|
|
30
|
-
return
|
|
29
|
+
def det_from_seg(seg_detections: List[SegmentationDetection]) -> ImageMetadata:
|
|
30
|
+
return ImageMetadata(segmentation_detections=seg_detections)
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def test_group_confidence():
|
|
@@ -3,7 +3,7 @@ from typing import List
|
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
6
|
-
from ....data_classes.
|
|
6
|
+
from ....data_classes.image_metadata import BoxDetection, ImageMetadata, PointDetection
|
|
7
7
|
from ....detector.inbox_filter.relevance_filter import RelevanceFilter
|
|
8
8
|
from ....detector.outbox import Outbox
|
|
9
9
|
|
|
@@ -17,14 +17,14 @@ l_conf_point_det = PointDetection(category_name='point', x=100, y=100,
|
|
|
17
17
|
|
|
18
18
|
@pytest.mark.parametrize(
|
|
19
19
|
"detections,reason",
|
|
20
|
-
[(
|
|
20
|
+
[(ImageMetadata(box_detections=[h_conf_box_det] * 40, point_detections=[h_conf_point_det] * 40),
|
|
21
21
|
['unexpected_observations_count']),
|
|
22
|
-
(
|
|
23
|
-
(
|
|
22
|
+
(ImageMetadata(box_detections=[h_conf_box_det], point_detections=[h_conf_point_det]), []),
|
|
23
|
+
(ImageMetadata(box_detections=[h_conf_box_det] * 40, point_detections=[l_conf_point_det] * 40),
|
|
24
24
|
['uncertain', 'unexpected_observations_count']),
|
|
25
|
-
(
|
|
25
|
+
(ImageMetadata(box_detections=[h_conf_box_det], point_detections=[l_conf_point_det]),
|
|
26
26
|
['uncertain'])])
|
|
27
|
-
def test_unexpected_observations_count(detections:
|
|
27
|
+
def test_unexpected_observations_count(detections: ImageMetadata, reason: List[str]):
|
|
28
28
|
os.environ['LOOP_ORGANIZATION'] = 'zauberzeug'
|
|
29
29
|
os.environ['LOOP_PROJECT'] = 'demo'
|
|
30
30
|
outbox = Outbox()
|