learning-loop-node 0.10.12__py3-none-any.whl → 0.10.14__py3-none-any.whl
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/annotation/annotator_node.py +11 -10
- learning_loop_node/data_classes/detections.py +34 -25
- learning_loop_node/data_classes/general.py +27 -17
- learning_loop_node/data_exchanger.py +6 -5
- learning_loop_node/detector/detector_logic.py +10 -4
- learning_loop_node/detector/detector_node.py +80 -54
- learning_loop_node/detector/inbox_filter/relevance_filter.py +9 -3
- learning_loop_node/detector/outbox.py +8 -1
- learning_loop_node/detector/rest/about.py +34 -9
- learning_loop_node/detector/rest/backdoor_controls.py +10 -29
- learning_loop_node/detector/rest/detect.py +27 -19
- learning_loop_node/detector/rest/model_version_control.py +30 -13
- learning_loop_node/detector/rest/operation_mode.py +11 -5
- learning_loop_node/detector/rest/outbox_mode.py +7 -1
- learning_loop_node/helpers/log_conf.py +5 -0
- learning_loop_node/node.py +97 -49
- learning_loop_node/rest.py +55 -0
- learning_loop_node/tests/detector/conftest.py +36 -2
- learning_loop_node/tests/detector/test_client_communication.py +21 -19
- learning_loop_node/tests/detector/test_detector_node.py +86 -0
- learning_loop_node/tests/trainer/conftest.py +4 -4
- learning_loop_node/tests/trainer/states/test_state_detecting.py +8 -9
- learning_loop_node/tests/trainer/states/test_state_download_train_model.py +8 -8
- learning_loop_node/tests/trainer/states/test_state_prepare.py +6 -7
- learning_loop_node/tests/trainer/states/test_state_sync_confusion_matrix.py +21 -18
- learning_loop_node/tests/trainer/states/test_state_train.py +6 -8
- learning_loop_node/tests/trainer/states/test_state_upload_detections.py +7 -9
- learning_loop_node/tests/trainer/states/test_state_upload_model.py +7 -8
- learning_loop_node/tests/trainer/test_errors.py +2 -2
- learning_loop_node/trainer/io_helpers.py +3 -6
- learning_loop_node/trainer/rest/backdoor_controls.py +19 -40
- learning_loop_node/trainer/trainer_logic.py +4 -4
- learning_loop_node/trainer/trainer_logic_generic.py +15 -12
- learning_loop_node/trainer/trainer_node.py +5 -4
- {learning_loop_node-0.10.12.dist-info → learning_loop_node-0.10.14.dist-info}/METADATA +16 -15
- {learning_loop_node-0.10.12.dist-info → learning_loop_node-0.10.14.dist-info}/RECORD +37 -35
- {learning_loop_node-0.10.12.dist-info → learning_loop_node-0.10.14.dist-info}/WHEEL +0 -0
|
@@ -22,7 +22,6 @@ class AnnotatorNode(Node):
|
|
|
22
22
|
self.tool = annotator_logic
|
|
23
23
|
self.histories: Dict = {}
|
|
24
24
|
annotator_logic.init(self)
|
|
25
|
-
self.status_sent = False
|
|
26
25
|
|
|
27
26
|
def register_sio_events(self, sio_client: AsyncClient):
|
|
28
27
|
|
|
@@ -66,8 +65,6 @@ class AnnotatorNode(Node):
|
|
|
66
65
|
return self.histories.setdefault(frontend_id, self.tool.create_empty_history())
|
|
67
66
|
|
|
68
67
|
async def send_status(self):
|
|
69
|
-
if self.status_sent:
|
|
70
|
-
return
|
|
71
68
|
|
|
72
69
|
status = AnnotationNodeStatus(
|
|
73
70
|
id=self.uuid,
|
|
@@ -76,20 +73,24 @@ class AnnotatorNode(Node):
|
|
|
76
73
|
capabilities=['segmentation']
|
|
77
74
|
)
|
|
78
75
|
|
|
79
|
-
self.log.
|
|
76
|
+
self.log.debug('Sending status %s', status)
|
|
80
77
|
try:
|
|
81
78
|
result = await self.sio_client.call('update_annotation_node', jsonable_encoder(asdict(status)), timeout=10)
|
|
82
|
-
except Exception
|
|
83
|
-
self.
|
|
79
|
+
except Exception:
|
|
80
|
+
self.socket_connection_broken = True
|
|
81
|
+
self.log.exception('Error for updating:')
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
if not isinstance(result, Dict):
|
|
85
|
+
self.socket_connection_broken = True
|
|
86
|
+
self.log.error('Expected Dict, got %s', type(result))
|
|
84
87
|
return
|
|
85
88
|
|
|
86
|
-
assert isinstance(result, Dict)
|
|
87
89
|
response = from_dict(data_class=SocketResponse, data=result)
|
|
88
90
|
|
|
89
91
|
if not response.success:
|
|
90
|
-
self.
|
|
91
|
-
|
|
92
|
-
self.status_sent = True
|
|
92
|
+
self.socket_connection_broken = True
|
|
93
|
+
self.log.error('Response from loop was: %s', asdict(response))
|
|
93
94
|
|
|
94
95
|
async def download_image(self, context: Context, uuid: str):
|
|
95
96
|
project_folder = create_project_folder(context)
|
|
@@ -16,14 +16,14 @@ class BoxDetection():
|
|
|
16
16
|
"""Coordinates according to COCO format. x,y is the top left corner of the box.
|
|
17
17
|
x increases to the right, y increases downwards.
|
|
18
18
|
"""
|
|
19
|
-
category_name: str
|
|
20
|
-
x: int
|
|
21
|
-
y: int
|
|
22
|
-
width: int
|
|
23
|
-
height: int
|
|
24
|
-
model_name: str
|
|
25
|
-
confidence: float
|
|
26
|
-
category_id: Optional[str] = None
|
|
19
|
+
category_name: str = field(metadata={'description': 'Category name'})
|
|
20
|
+
x: int = field(metadata={'description': 'X coordinate (left to right)'})
|
|
21
|
+
y: int = field(metadata={'description': 'Y coordinate (top to bottom)'})
|
|
22
|
+
width: int = field(metadata={'description': 'Width'})
|
|
23
|
+
height: int = field(metadata={'description': 'Height'})
|
|
24
|
+
model_name: str = field(metadata={'description': 'Model name'})
|
|
25
|
+
confidence: float = field(metadata={'description': 'Confidence'})
|
|
26
|
+
category_id: Optional[str] = field(default=None, metadata={'description': 'Category ID'})
|
|
27
27
|
|
|
28
28
|
def intersection_over_union(self, other_detection: 'BoxDetection') -> float:
|
|
29
29
|
# https://www.pyimagesearch.com/2016/11/07/intersection-over-union-iou-for-object-detection/
|
|
@@ -52,12 +52,12 @@ class BoxDetection():
|
|
|
52
52
|
class PointDetection():
|
|
53
53
|
"""Coordinates according to COCO format. x,y is the center of the point.
|
|
54
54
|
x increases to the right, y increases downwards."""
|
|
55
|
-
category_name: str
|
|
56
|
-
x: float
|
|
57
|
-
y: float
|
|
58
|
-
model_name: str
|
|
59
|
-
confidence: float
|
|
60
|
-
category_id: Optional[str] = None
|
|
55
|
+
category_name: str = field(metadata={'description': 'Category name'})
|
|
56
|
+
x: float = field(metadata={'description': 'X coordinate (right)'})
|
|
57
|
+
y: float = field(metadata={'description': 'Y coordinate (down)'})
|
|
58
|
+
model_name: str = field(metadata={'description': 'Model name'})
|
|
59
|
+
confidence: float = field(metadata={'description': 'Confidence'})
|
|
60
|
+
category_id: Optional[str] = field(default=None, metadata={'description': 'Category ID'})
|
|
61
61
|
|
|
62
62
|
def distance(self, other: 'PointDetection') -> float:
|
|
63
63
|
return np.sqrt((other.x - self.x)**2 + (other.y - self.y)**2)
|
|
@@ -68,10 +68,10 @@ class PointDetection():
|
|
|
68
68
|
|
|
69
69
|
@dataclass(**KWONLY_SLOTS)
|
|
70
70
|
class ClassificationDetection():
|
|
71
|
-
category_name: str
|
|
72
|
-
model_name: str
|
|
73
|
-
confidence: float
|
|
74
|
-
category_id: Optional[str] = None
|
|
71
|
+
category_name: str = field(metadata={'description': 'Category name'})
|
|
72
|
+
model_name: str = field(metadata={'description': 'Model name'})
|
|
73
|
+
confidence: float = field(metadata={'description': 'Confidence'})
|
|
74
|
+
category_id: Optional[str] = field(default=None, metadata={'description': 'Category ID'})
|
|
75
75
|
|
|
76
76
|
def __str__(self):
|
|
77
77
|
return f'c: {self.confidence:.2f} -> {self.category_name}'
|
|
@@ -112,13 +112,22 @@ def current_datetime():
|
|
|
112
112
|
|
|
113
113
|
@dataclass(**KWONLY_SLOTS)
|
|
114
114
|
class Detections():
|
|
115
|
-
box_detections: List[BoxDetection] = field(default_factory=list
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
box_detections: List[BoxDetection] = field(default_factory=list, metadata={
|
|
116
|
+
'description': 'List of box detections'})
|
|
117
|
+
point_detections: List[PointDetection] = field(default_factory=list, metadata={
|
|
118
|
+
'description': 'List of point detections'})
|
|
119
|
+
segmentation_detections: List[SegmentationDetection] = field(default_factory=list, metadata={
|
|
120
|
+
'description': 'List of segmentation detections'})
|
|
121
|
+
classification_detections: List[ClassificationDetection] = field(default_factory=list, metadata={
|
|
122
|
+
'description': 'List of classification detections'})
|
|
123
|
+
tags: List[str] = field(default_factory=list, metadata={
|
|
124
|
+
'description': 'List of tags'})
|
|
125
|
+
date: Optional[str] = field(default_factory=current_datetime, metadata={
|
|
126
|
+
'description': 'Date of the detections'})
|
|
127
|
+
image_id: Optional[str] = field(default=None, metadata={
|
|
128
|
+
'description': 'Image uuid'})
|
|
129
|
+
source: Optional[str] = field(default=None, metadata={
|
|
130
|
+
'description': 'Source of the detections'})
|
|
122
131
|
|
|
123
132
|
def __len__(self):
|
|
124
133
|
return len(self.box_detections) + len(self.point_detections) + len(self.segmentation_detections) + len(self.classification_detections)
|
|
@@ -20,14 +20,19 @@ class CategoryType(str, Enum):
|
|
|
20
20
|
|
|
21
21
|
@dataclass(**KWONLY_SLOTS)
|
|
22
22
|
class Category():
|
|
23
|
-
id: str
|
|
24
|
-
name: str
|
|
25
|
-
description: Optional[str] = None
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
id: str = field(metadata={"description": "The uuid of the category."})
|
|
24
|
+
name: str = field(metadata={"description": "The name of the category."})
|
|
25
|
+
description: Optional[str] = field(default=None, metadata={
|
|
26
|
+
"description": "An optional description of the category."})
|
|
27
|
+
hotkey: Optional[str] = field(default=None, metadata={
|
|
28
|
+
"description": "The key shortcut of the category when annotating in the Learning Loop UI."})
|
|
29
|
+
color: Optional[str] = field(default=None, metadata={
|
|
30
|
+
"description": "The color of the category when displayed in the Learning Loop UI."})
|
|
31
|
+
point_size: Optional[int] = field(default=None, metadata={
|
|
32
|
+
"description": "The point size of the category in pixels. Represents the uncertainty of the category."})
|
|
33
|
+
type: Optional[Union[CategoryType, str]] = field(default=None, metadata={
|
|
34
|
+
"description": "The type of the category",
|
|
35
|
+
"example": "box, point, segmentation, classification"})
|
|
31
36
|
|
|
32
37
|
@staticmethod
|
|
33
38
|
def from_list(values: List[dict]) -> List['Category']:
|
|
@@ -45,15 +50,20 @@ class Context():
|
|
|
45
50
|
|
|
46
51
|
@dataclass(**KWONLY_SLOTS)
|
|
47
52
|
class ModelInformation():
|
|
48
|
-
id: str
|
|
49
|
-
host: Optional[str]
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
id: str = field(metadata={"description": "The uuid of the model."})
|
|
54
|
+
host: Optional[str] = field(metadata={"description": "The hostname that started the training.",
|
|
55
|
+
"example": "learning-loop.ai"})
|
|
56
|
+
organization: str = field(metadata={"description": "The owner organization of the model."})
|
|
57
|
+
project: str = field(metadata={"description": "The project of the model."})
|
|
58
|
+
version: str = field(metadata={"description": "The version of the model."})
|
|
59
|
+
categories: List[Category] = field(default_factory=list, metadata={
|
|
60
|
+
"description": "The categories used in the model."})
|
|
61
|
+
resolution: Optional[int] = field(default=None, metadata={
|
|
62
|
+
"description": "The resolution of the model (width and height of the image after preprocessing in pixels)."})
|
|
63
|
+
model_root_path: Optional[str] = field(
|
|
64
|
+
default=None, metadata={"description": "The path of the parent directory of the model in the file system."})
|
|
65
|
+
model_size: Optional[str] = field(default=None, metadata={
|
|
66
|
+
"description": "The size of the model (i.e. the specification or variant of the model architecture)."})
|
|
57
67
|
|
|
58
68
|
@property
|
|
59
69
|
def context(self):
|
|
@@ -140,20 +140,21 @@ class DataExchanger():
|
|
|
140
140
|
"""Downloads a model (and additional meta data like model.json) and returns the paths of the downloaded files.
|
|
141
141
|
Used before training a model (when continuing a finished training) or before detecting images.
|
|
142
142
|
"""
|
|
143
|
-
logging.info(
|
|
143
|
+
logging.info('Downloading model data for uuid %s from the loop to %s..', model_uuid, target_folder)
|
|
144
144
|
|
|
145
145
|
path = f'/{context.organization}/projects/{context.project}/models/{model_uuid}/{model_format}/file'
|
|
146
146
|
response = await self.loop_communicator.get(path, requires_login=False)
|
|
147
147
|
if response.status_code != 200:
|
|
148
|
-
|
|
149
|
-
logging.error(
|
|
150
|
-
|
|
148
|
+
decoded_content = response.content.decode('utf-8')
|
|
149
|
+
logging.error('could not download loop/%s: %s, content: %s', path,
|
|
150
|
+
response.status_code, decoded_content)
|
|
151
|
+
raise DownloadError(decoded_content)
|
|
151
152
|
try:
|
|
152
153
|
provided_filename = response.headers.get(
|
|
153
154
|
"Content-Disposition").split("filename=")[1].strip('"')
|
|
154
155
|
content = response.content
|
|
155
156
|
except:
|
|
156
|
-
logging.exception(
|
|
157
|
+
logging.exception('Error while downloading model %s:', path)
|
|
157
158
|
raise
|
|
158
159
|
|
|
159
160
|
tmp_path = f'/tmp/{os.path.splitext(provided_filename)[0]}'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from abc import abstractmethod
|
|
3
|
-
from typing import Optional
|
|
3
|
+
from typing import List, Optional
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
|
|
@@ -28,7 +28,7 @@ class DetectorLogic():
|
|
|
28
28
|
return self._model_info is not None
|
|
29
29
|
|
|
30
30
|
def load_model(self):
|
|
31
|
-
logging.info(
|
|
31
|
+
logging.info('Loading model from %s', GLOBALS.data_folder)
|
|
32
32
|
model_info = ModelInformation.load_from_disk(f'{GLOBALS.data_folder}/model')
|
|
33
33
|
if model_info is None:
|
|
34
34
|
logging.warning('No model found')
|
|
@@ -37,15 +37,21 @@ class DetectorLogic():
|
|
|
37
37
|
try:
|
|
38
38
|
self._model_info = model_info
|
|
39
39
|
self.init()
|
|
40
|
-
logging.info(
|
|
40
|
+
logging.info('Successfully loaded model %s', self._model_info)
|
|
41
41
|
except Exception:
|
|
42
|
-
logging.error(
|
|
42
|
+
logging.error('Could not init model %s', model_info)
|
|
43
43
|
raise
|
|
44
44
|
|
|
45
45
|
@abstractmethod
|
|
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) -> Detections: # pylint: disable=unused-argument
|
|
50
|
+
"""Called by the detector node when an image should be evaluated (REST or SocketIO).
|
|
51
|
+
Tags, source come from the caller and may be used in this function.
|
|
52
|
+
By default, this function simply calls `evaluate`"""
|
|
53
|
+
return self.evaluate(image)
|
|
54
|
+
|
|
49
55
|
@abstractmethod
|
|
50
56
|
def evaluate(self, image: np.ndarray) -> Detections:
|
|
51
57
|
"""Evaluate the image and return the detections.
|
|
@@ -9,9 +9,9 @@ from threading import Thread
|
|
|
9
9
|
from typing import Dict, List, Optional, Union
|
|
10
10
|
|
|
11
11
|
import numpy as np
|
|
12
|
+
import socketio
|
|
12
13
|
from dacite import from_dict
|
|
13
14
|
from fastapi.encoders import jsonable_encoder
|
|
14
|
-
from fastapi_socketio import SocketManager
|
|
15
15
|
from socketio import AsyncClient
|
|
16
16
|
|
|
17
17
|
from ..data_classes import Category, Context, Detections, DetectionStatus, ModelInformation, Shape
|
|
@@ -41,7 +41,7 @@ class DetectorNode(Node):
|
|
|
41
41
|
self.organization = environment_reader.organization()
|
|
42
42
|
self.project = environment_reader.project()
|
|
43
43
|
assert self.organization and self.project, 'Detector node needs an organization and an project'
|
|
44
|
-
self.log.info(
|
|
44
|
+
self.log.info('Using %s/%s', self.organization, self.project)
|
|
45
45
|
self.operation_mode: OperationMode = OperationMode.Startup
|
|
46
46
|
self.connected_clients: List[str] = []
|
|
47
47
|
|
|
@@ -70,7 +70,7 @@ class DetectorNode(Node):
|
|
|
70
70
|
self.include_router(rest_outbox_mode.router, tags=["outbox_mode"])
|
|
71
71
|
self.include_router(rest_version_control.router, tags=["model_version"])
|
|
72
72
|
|
|
73
|
-
if use_backdoor_controls:
|
|
73
|
+
if use_backdoor_controls or os.environ.get('USE_BACKDOOR_CONTROLS', '0').lower() in ('1', 'true'):
|
|
74
74
|
self.include_router(backdoor_controls.router)
|
|
75
75
|
|
|
76
76
|
self.setup_sio_server()
|
|
@@ -93,7 +93,8 @@ class DetectorNode(Node):
|
|
|
93
93
|
# simulate super().startup
|
|
94
94
|
await self.loop_communicator.backend_ready()
|
|
95
95
|
# await self.loop_communicator.ensure_login()
|
|
96
|
-
|
|
96
|
+
self.set_skip_repeat_loop(False)
|
|
97
|
+
self.socket_connection_broken = True
|
|
97
98
|
await self.on_startup()
|
|
98
99
|
|
|
99
100
|
# simulate startup
|
|
@@ -126,35 +127,48 @@ class DetectorNode(Node):
|
|
|
126
127
|
|
|
127
128
|
def setup_sio_server(self) -> None:
|
|
128
129
|
"""The DetectorNode acts as a SocketIO server. This method sets up the server and defines the event handlers."""
|
|
129
|
-
|
|
130
130
|
# pylint: disable=unused-argument
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
# Initialize the Socket.IO server
|
|
133
|
+
self.sio = socketio.AsyncServer(async_mode='asgi')
|
|
134
|
+
# Initialize and mount the ASGI app
|
|
135
|
+
self.sio_app = socketio.ASGIApp(self.sio, socketio_path='/socket.io')
|
|
136
|
+
self.mount('/ws', self.sio_app)
|
|
137
|
+
# Register event handlers
|
|
138
|
+
|
|
139
|
+
self.log.info('>>>>>>>>>>>>>>>>>>>>>>> Setting up the SIO server')
|
|
140
|
+
|
|
141
|
+
@self.sio.event
|
|
142
|
+
async def detect(sid, data: Dict) -> Dict:
|
|
143
|
+
self.log.debug('running detect via socketio')
|
|
134
144
|
try:
|
|
135
145
|
np_image = np.frombuffer(data['image'], np.uint8)
|
|
136
146
|
det = await self.get_detections(
|
|
137
147
|
raw_image=np_image,
|
|
138
148
|
camera_id=data.get('camera-id', None) or data.get('mac', None),
|
|
139
149
|
tags=data.get('tags', []),
|
|
140
|
-
|
|
150
|
+
source=data.get('source', None),
|
|
151
|
+
autoupload=data.get('autoupload', None)
|
|
141
152
|
)
|
|
142
153
|
if det is None:
|
|
143
154
|
return {'error': 'no model loaded'}
|
|
144
|
-
|
|
145
|
-
|
|
155
|
+
detection_dict = jsonable_encoder(asdict(det))
|
|
156
|
+
self.log.debug('detect via socketio finished')
|
|
157
|
+
return detection_dict
|
|
146
158
|
except Exception as e:
|
|
147
159
|
self.log.exception('could not detect via socketio')
|
|
148
160
|
with open('/tmp/bad_img_from_socket_io.jpg', 'wb') as f:
|
|
149
161
|
f.write(data['image'])
|
|
150
162
|
return {'error': str(e)}
|
|
151
163
|
|
|
152
|
-
|
|
164
|
+
@self.sio.event
|
|
165
|
+
async def info(sid) -> Union[str, Dict]:
|
|
153
166
|
if self.detector_logic.is_initialized:
|
|
154
167
|
return asdict(self.detector_logic.model_info)
|
|
155
168
|
return 'No model loaded'
|
|
156
169
|
|
|
157
|
-
|
|
170
|
+
@self.sio.event
|
|
171
|
+
async def upload(sid, data: Dict) -> Optional[Dict]:
|
|
158
172
|
'''upload an image with detections'''
|
|
159
173
|
|
|
160
174
|
detection_data = data.get('detections', {})
|
|
@@ -171,50 +185,43 @@ class DetectorNode(Node):
|
|
|
171
185
|
tags = data.get('tags', [])
|
|
172
186
|
tags.append('picked_by_system')
|
|
173
187
|
|
|
188
|
+
source = data.get('source', None)
|
|
189
|
+
|
|
174
190
|
loop = asyncio.get_event_loop()
|
|
175
191
|
try:
|
|
176
|
-
await loop.run_in_executor(None, self.outbox.save, data['image'], detections, tags)
|
|
192
|
+
await loop.run_in_executor(None, self.outbox.save, data['image'], detections, tags, source)
|
|
177
193
|
except Exception as e:
|
|
178
194
|
self.log.exception('could not upload via socketio')
|
|
179
195
|
return {'error': str(e)}
|
|
180
196
|
return None
|
|
181
197
|
|
|
182
|
-
|
|
198
|
+
@self.sio.event
|
|
199
|
+
def connect(sid, environ, auth) -> None:
|
|
183
200
|
self.connected_clients.append(sid)
|
|
184
201
|
|
|
185
|
-
print('>>>>>>>>>>>>>>>>>>>>>>> setting up sio server', flush=True)
|
|
186
|
-
|
|
187
|
-
self.sio_server = SocketManager(app=self)
|
|
188
|
-
self.sio_server.on('detect', _detect)
|
|
189
|
-
self.sio_server.on('info', _info)
|
|
190
|
-
self.sio_server.on('upload', _upload)
|
|
191
|
-
self.sio_server.on('connect', _connect)
|
|
192
|
-
|
|
193
202
|
async def _check_for_update(self) -> None:
|
|
194
|
-
if self.operation_mode == OperationMode.Startup:
|
|
195
|
-
return
|
|
196
203
|
try:
|
|
197
|
-
self.log.
|
|
204
|
+
self.log.debug('Current operation mode is %s', self.operation_mode)
|
|
198
205
|
try:
|
|
199
206
|
await self.sync_status_with_learning_loop()
|
|
200
|
-
except Exception
|
|
201
|
-
self.log.
|
|
207
|
+
except Exception:
|
|
208
|
+
self.log.exception('Sync with learning loop failed (could not check for updates):')
|
|
202
209
|
return
|
|
203
210
|
|
|
204
211
|
if self.operation_mode != OperationMode.Idle:
|
|
205
|
-
self.log.
|
|
212
|
+
self.log.debug('not checking for updates; operation mode is %s', self.operation_mode)
|
|
206
213
|
return
|
|
207
214
|
|
|
208
215
|
self.status.reset_error('update_model')
|
|
209
216
|
if self.target_model is None:
|
|
210
|
-
self.log.
|
|
217
|
+
self.log.debug('not checking for updates; no target model selected')
|
|
211
218
|
return
|
|
212
219
|
|
|
213
|
-
current_version = self.detector_logic._model_info.version if self.detector_logic._model_info is not None else None
|
|
220
|
+
current_version = self.detector_logic._model_info.version if self.detector_logic._model_info is not None else None # pylint: disable=protected-access
|
|
214
221
|
|
|
215
222
|
if not self.detector_logic.is_initialized or self.target_model.version != current_version:
|
|
216
|
-
self.log.info(
|
|
217
|
-
|
|
223
|
+
self.log.info('Current model "%s" needs to be updated to %s',
|
|
224
|
+
current_version or "-", self.target_model.version)
|
|
218
225
|
|
|
219
226
|
with step_into(GLOBALS.data_folder):
|
|
220
227
|
model_symlink = 'model'
|
|
@@ -232,7 +239,7 @@ class DetectorNode(Node):
|
|
|
232
239
|
except Exception:
|
|
233
240
|
pass
|
|
234
241
|
os.symlink(target_model_folder, model_symlink)
|
|
235
|
-
self.log.info(
|
|
242
|
+
self.log.info('Updated symlink for model to %s', os.readlink(model_symlink))
|
|
236
243
|
|
|
237
244
|
self.detector_logic.load_model()
|
|
238
245
|
try:
|
|
@@ -283,13 +290,16 @@ class DetectorNode(Node):
|
|
|
283
290
|
model_format=self.detector_logic.model_format,
|
|
284
291
|
)
|
|
285
292
|
|
|
286
|
-
self.log.
|
|
293
|
+
self.log.debug('sending status %s', status)
|
|
287
294
|
response = await self.sio_client.call('update_detector', (self.organization, self.project, jsonable_encoder(asdict(status))))
|
|
295
|
+
if not response:
|
|
296
|
+
self.socket_connection_broken = True
|
|
297
|
+
return
|
|
288
298
|
|
|
289
|
-
assert response is not None
|
|
290
299
|
socket_response = from_dict(data_class=SocketResponse, data=response)
|
|
291
300
|
if not socket_response.success:
|
|
292
|
-
self.
|
|
301
|
+
self.socket_connection_broken = True
|
|
302
|
+
self.log.error('Statusupdate failed: %s', response)
|
|
293
303
|
raise Exception(f'Statusupdate failed: {response}')
|
|
294
304
|
|
|
295
305
|
assert socket_response.payload is not None
|
|
@@ -301,21 +311,24 @@ class DetectorNode(Node):
|
|
|
301
311
|
id=deployment_target_model_id,
|
|
302
312
|
version=deployment_target_model_version)
|
|
303
313
|
|
|
304
|
-
if self.version_control == rest_version_control.VersionMode.FollowLoop
|
|
314
|
+
if (self.version_control == rest_version_control.VersionMode.FollowLoop and
|
|
315
|
+
self.target_model != self.loop_deployment_target):
|
|
316
|
+
old_target_model_version = self.target_model.version if self.target_model else None
|
|
305
317
|
self.target_model = self.loop_deployment_target
|
|
306
|
-
self.log.info(
|
|
318
|
+
self.log.info('After sending status. Target_model changed from %s to %s',
|
|
319
|
+
old_target_model_version, self.target_model.version)
|
|
307
320
|
|
|
308
321
|
async def set_operation_mode(self, mode: OperationMode):
|
|
309
322
|
self.operation_mode = mode
|
|
310
323
|
try:
|
|
311
324
|
await self.sync_status_with_learning_loop()
|
|
312
325
|
except Exception as e:
|
|
313
|
-
self.log.warning(
|
|
326
|
+
self.log.warning('Operation mode set to %s, but sync failed: %s', mode, e)
|
|
314
327
|
|
|
315
328
|
def reload(self, reason: str):
|
|
316
329
|
'''provide a cause for the reload'''
|
|
317
330
|
|
|
318
|
-
self.log.info(
|
|
331
|
+
self.log.info('########## reloading app because %s', reason)
|
|
319
332
|
if os.path.isfile('/app/app_code/restart/restart.py'):
|
|
320
333
|
subprocess.call(['touch', '/app/app_code/restart/restart.py'])
|
|
321
334
|
elif os.path.isfile('/app/main.py'):
|
|
@@ -325,33 +338,37 @@ class DetectorNode(Node):
|
|
|
325
338
|
else:
|
|
326
339
|
self.log.error('could not reload app')
|
|
327
340
|
|
|
328
|
-
async def get_detections(self,
|
|
329
|
-
|
|
341
|
+
async def get_detections(self,
|
|
342
|
+
raw_image: np.ndarray,
|
|
343
|
+
camera_id: Optional[str],
|
|
344
|
+
tags: List[str],
|
|
345
|
+
source: Optional[str] = None,
|
|
346
|
+
autoupload: Optional[str] = None) -> Detections:
|
|
347
|
+
""" Main processing function for the detector node when an image is received via REST or SocketIO.
|
|
348
|
+
This function infers the detections from the image, cares about uploading to the loop and returns the detections as a dictionary.
|
|
349
|
+
Note: raw_image is a numpy array of type uint8, but not in the correct shape!
|
|
330
350
|
It can be converted e.g. using cv2.imdecode(raw_image, cv2.IMREAD_COLOR)"""
|
|
331
|
-
|
|
351
|
+
|
|
332
352
|
await self.detection_lock.acquire()
|
|
333
|
-
|
|
353
|
+
loop = asyncio.get_event_loop()
|
|
354
|
+
detections = await loop.run_in_executor(None, self.detector_logic.evaluate_with_all_info, raw_image, tags, source)
|
|
334
355
|
self.detection_lock.release()
|
|
335
|
-
for seg_detection in detections.segmentation_detections:
|
|
336
|
-
if isinstance(seg_detection.shape, Shape):
|
|
337
|
-
shapes = ','.join([str(value) for p in seg_detection.shape.points for _,
|
|
338
|
-
value in asdict(p).items()])
|
|
339
|
-
seg_detection.shape = shapes # TODO This seems to be a quick fix.. check how loop upload detections deals with this
|
|
340
356
|
|
|
357
|
+
fix_shape_detections(detections)
|
|
341
358
|
n_bo, n_cl = len(detections.box_detections), len(detections.classification_detections)
|
|
342
359
|
n_po, n_se = len(detections.point_detections), len(detections.segmentation_detections)
|
|
343
|
-
self.log.
|
|
360
|
+
self.log.debug('Detected: %d boxes, %d points, %d segs, %d classes', n_bo, n_po, n_se, n_cl)
|
|
344
361
|
|
|
345
362
|
if autoupload is None or autoupload == 'filtered': # NOTE default is filtered
|
|
346
363
|
Thread(target=self.relevance_filter.may_upload_detections,
|
|
347
|
-
args=(detections, camera_id, raw_image, tags)).start()
|
|
364
|
+
args=(detections, camera_id, raw_image, tags, source)).start()
|
|
348
365
|
elif autoupload == 'all':
|
|
349
|
-
Thread(target=self.outbox.save, args=(raw_image, detections, tags)).start()
|
|
366
|
+
Thread(target=self.outbox.save, args=(raw_image, detections, tags, source)).start()
|
|
350
367
|
elif autoupload == 'disabled':
|
|
351
368
|
pass
|
|
352
369
|
else:
|
|
353
|
-
self.log.error(
|
|
354
|
-
return
|
|
370
|
+
self.log.error('unknown autoupload value %s', autoupload)
|
|
371
|
+
return detections
|
|
355
372
|
|
|
356
373
|
async def upload_images(self, images: List[bytes]):
|
|
357
374
|
loop = asyncio.get_event_loop()
|
|
@@ -393,3 +410,12 @@ def step_into(new_dir):
|
|
|
393
410
|
yield
|
|
394
411
|
finally:
|
|
395
412
|
os.chdir(previous_dir)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def fix_shape_detections(detections: Detections):
|
|
416
|
+
# TODO This is a quick fix.. check how loop upload detections deals with this
|
|
417
|
+
for seg_detection in detections.segmentation_detections:
|
|
418
|
+
if isinstance(seg_detection.shape, Shape):
|
|
419
|
+
points = ','.join([str(value) for p in seg_detection.shape.points for _,
|
|
420
|
+
value in asdict(p).items()])
|
|
421
|
+
seg_detection.shape = points
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Dict, List
|
|
1
|
+
from typing import Dict, List, Optional
|
|
2
2
|
|
|
3
3
|
from ...data_classes.detections import Detections
|
|
4
4
|
from ..outbox import Outbox
|
|
@@ -11,7 +11,13 @@ class RelevanceFilter():
|
|
|
11
11
|
self.cam_histories: Dict[str, CamObservationHistory] = {}
|
|
12
12
|
self.outbox: Outbox = outbox
|
|
13
13
|
|
|
14
|
-
def may_upload_detections(self,
|
|
14
|
+
def may_upload_detections(self,
|
|
15
|
+
dets: Detections,
|
|
16
|
+
cam_id: str,
|
|
17
|
+
raw_image: bytes,
|
|
18
|
+
tags: List[str],
|
|
19
|
+
source: Optional[str] = None
|
|
20
|
+
) -> List[str]:
|
|
15
21
|
for group in self.cam_histories.values():
|
|
16
22
|
group.forget_old_detections()
|
|
17
23
|
|
|
@@ -23,5 +29,5 @@ class RelevanceFilter():
|
|
|
23
29
|
if len(causes) > 0:
|
|
24
30
|
tags = tags if tags is not None else []
|
|
25
31
|
tags.extend(causes)
|
|
26
|
-
self.outbox.save(raw_image, dets, tags)
|
|
32
|
+
self.outbox.save(raw_image, dets, tags, source)
|
|
27
33
|
return causes
|
|
@@ -55,7 +55,13 @@ class Outbox():
|
|
|
55
55
|
|
|
56
56
|
self.upload_counter = 0
|
|
57
57
|
|
|
58
|
-
def save(self,
|
|
58
|
+
def save(self,
|
|
59
|
+
image: bytes,
|
|
60
|
+
detections: Optional[Detections] = None,
|
|
61
|
+
tags: Optional[List[str]] = None,
|
|
62
|
+
source: Optional[str] = None
|
|
63
|
+
) -> None:
|
|
64
|
+
|
|
59
65
|
if not self._is_valid_jpg(image):
|
|
60
66
|
self.log.error('Invalid jpg image')
|
|
61
67
|
return
|
|
@@ -71,6 +77,7 @@ class Outbox():
|
|
|
71
77
|
tmp = f'{GLOBALS.data_folder}/tmp/{identifier}'
|
|
72
78
|
detections.tags = tags
|
|
73
79
|
detections.date = identifier
|
|
80
|
+
detections.source = source or 'unknown'
|
|
74
81
|
os.makedirs(tmp, exist_ok=True)
|
|
75
82
|
|
|
76
83
|
with open(tmp + '/image.json', 'w') as f:
|