learning-loop-node 0.10.13__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 -26
- learning_loop_node/data_classes/general.py +27 -17
- learning_loop_node/data_exchanger.py +6 -5
- learning_loop_node/detector/detector_logic.py +3 -3
- learning_loop_node/detector/detector_node.py +21 -15
- learning_loop_node/detector/rest/about.py +34 -10
- learning_loop_node/detector/rest/backdoor_controls.py +9 -26
- learning_loop_node/detector/rest/detect.py +17 -16
- 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/node.py +93 -48
- learning_loop_node/rest.py +25 -2
- learning_loop_node/tests/detector/conftest.py +4 -4
- learning_loop_node/tests/detector/test_client_communication.py +21 -19
- learning_loop_node/tests/detector/test_detector_node.py +3 -3
- 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/rest/backdoor_controls.py +19 -40
- learning_loop_node/trainer/trainer_logic_generic.py +7 -4
- learning_loop_node/trainer/trainer_node.py +4 -3
- {learning_loop_node-0.10.13.dist-info → learning_loop_node-0.10.14.dist-info}/METADATA +1 -1
- {learning_loop_node-0.10.13.dist-info → learning_loop_node-0.10.14.dist-info}/RECORD +32 -32
- {learning_loop_node-0.10.13.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,14 +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
|
-
|
|
122
|
-
|
|
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'})
|
|
123
131
|
|
|
124
132
|
def __len__(self):
|
|
125
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]}'
|
|
@@ -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,9 +37,9 @@ 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
|
|
@@ -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
|
|
@@ -151,8 +152,9 @@ class DetectorNode(Node):
|
|
|
151
152
|
)
|
|
152
153
|
if det is None:
|
|
153
154
|
return {'error': 'no model loaded'}
|
|
155
|
+
detection_dict = jsonable_encoder(asdict(det))
|
|
154
156
|
self.log.debug('detect via socketio finished')
|
|
155
|
-
return
|
|
157
|
+
return detection_dict
|
|
156
158
|
except Exception as e:
|
|
157
159
|
self.log.exception('could not detect via socketio')
|
|
158
160
|
with open('/tmp/bad_img_from_socket_io.jpg', 'wb') as f:
|
|
@@ -198,23 +200,21 @@ class DetectorNode(Node):
|
|
|
198
200
|
self.connected_clients.append(sid)
|
|
199
201
|
|
|
200
202
|
async def _check_for_update(self) -> None:
|
|
201
|
-
if self.operation_mode == OperationMode.Startup:
|
|
202
|
-
return
|
|
203
203
|
try:
|
|
204
|
-
self.log.
|
|
204
|
+
self.log.debug('Current operation mode is %s', self.operation_mode)
|
|
205
205
|
try:
|
|
206
206
|
await self.sync_status_with_learning_loop()
|
|
207
|
-
except Exception
|
|
208
|
-
self.log.
|
|
207
|
+
except Exception:
|
|
208
|
+
self.log.exception('Sync with learning loop failed (could not check for updates):')
|
|
209
209
|
return
|
|
210
210
|
|
|
211
211
|
if self.operation_mode != OperationMode.Idle:
|
|
212
|
-
self.log.
|
|
212
|
+
self.log.debug('not checking for updates; operation mode is %s', self.operation_mode)
|
|
213
213
|
return
|
|
214
214
|
|
|
215
215
|
self.status.reset_error('update_model')
|
|
216
216
|
if self.target_model is None:
|
|
217
|
-
self.log.
|
|
217
|
+
self.log.debug('not checking for updates; no target model selected')
|
|
218
218
|
return
|
|
219
219
|
|
|
220
220
|
current_version = self.detector_logic._model_info.version if self.detector_logic._model_info is not None else None # pylint: disable=protected-access
|
|
@@ -290,12 +290,15 @@ class DetectorNode(Node):
|
|
|
290
290
|
model_format=self.detector_logic.model_format,
|
|
291
291
|
)
|
|
292
292
|
|
|
293
|
-
self.log.
|
|
293
|
+
self.log.debug('sending status %s', status)
|
|
294
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
|
|
295
298
|
|
|
296
|
-
assert response is not None
|
|
297
299
|
socket_response = from_dict(data_class=SocketResponse, data=response)
|
|
298
300
|
if not socket_response.success:
|
|
301
|
+
self.socket_connection_broken = True
|
|
299
302
|
self.log.error('Statusupdate failed: %s', response)
|
|
300
303
|
raise Exception(f'Statusupdate failed: {response}')
|
|
301
304
|
|
|
@@ -308,9 +311,12 @@ class DetectorNode(Node):
|
|
|
308
311
|
id=deployment_target_model_id,
|
|
309
312
|
version=deployment_target_model_version)
|
|
310
313
|
|
|
311
|
-
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
|
|
312
317
|
self.target_model = self.loop_deployment_target
|
|
313
|
-
self.log.info('After sending status. Target_model
|
|
318
|
+
self.log.info('After sending status. Target_model changed from %s to %s',
|
|
319
|
+
old_target_model_version, self.target_model.version)
|
|
314
320
|
|
|
315
321
|
async def set_operation_mode(self, mode: OperationMode):
|
|
316
322
|
self.operation_mode = mode
|
|
@@ -337,7 +343,7 @@ class DetectorNode(Node):
|
|
|
337
343
|
camera_id: Optional[str],
|
|
338
344
|
tags: List[str],
|
|
339
345
|
source: Optional[str] = None,
|
|
340
|
-
autoupload: Optional[str] = None) ->
|
|
346
|
+
autoupload: Optional[str] = None) -> Detections:
|
|
341
347
|
""" Main processing function for the detector node when an image is received via REST or SocketIO.
|
|
342
348
|
This function infers the detections from the image, cares about uploading to the loop and returns the detections as a dictionary.
|
|
343
349
|
Note: raw_image is a numpy array of type uint8, but not in the correct shape!
|
|
@@ -362,7 +368,7 @@ class DetectorNode(Node):
|
|
|
362
368
|
pass
|
|
363
369
|
else:
|
|
364
370
|
self.log.error('unknown autoupload value %s', autoupload)
|
|
365
|
-
return
|
|
371
|
+
return detections
|
|
366
372
|
|
|
367
373
|
async def upload_images(self, images: List[bytes]):
|
|
368
374
|
loop = asyncio.get_event_loop()
|
|
@@ -1,26 +1,50 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
2
|
+
import sys
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import TYPE_CHECKING, Optional
|
|
3
5
|
|
|
4
6
|
from fastapi import APIRouter, Request
|
|
5
7
|
|
|
8
|
+
from ...data_classes import ModelInformation
|
|
9
|
+
|
|
6
10
|
if TYPE_CHECKING:
|
|
7
11
|
from ..detector_node import DetectorNode
|
|
12
|
+
KWONLY_SLOTS = {'kw_only': True, 'slots': True} if sys.version_info >= (3, 10) else {}
|
|
8
13
|
|
|
9
14
|
router = APIRouter()
|
|
10
15
|
|
|
11
16
|
|
|
12
|
-
@
|
|
17
|
+
@dataclass(**KWONLY_SLOTS)
|
|
18
|
+
class AboutResponse:
|
|
19
|
+
operation_mode: str = field(metadata={"description": "The operation mode of the detector node"})
|
|
20
|
+
state: Optional[str] = field(metadata={
|
|
21
|
+
"description": "The state of the detector node",
|
|
22
|
+
"example": "idle, online, detecting"})
|
|
23
|
+
model_info: Optional[ModelInformation] = field(metadata={
|
|
24
|
+
"description": "Information about the model of the detector node"})
|
|
25
|
+
target_model: Optional[str] = field(metadata={"description": "The target model of the detector node"})
|
|
26
|
+
version_control: str = field(metadata={
|
|
27
|
+
"description": "The version control mode of the detector node",
|
|
28
|
+
"example": "follow_loop, specific_version, pause"})
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@router.get("/about", response_model=AboutResponse)
|
|
13
32
|
async def get_about(request: Request):
|
|
14
33
|
'''
|
|
34
|
+
Get information about the detector node.
|
|
35
|
+
|
|
15
36
|
Example Usage
|
|
16
|
-
|
|
37
|
+
|
|
38
|
+
curl http://hosturl/about
|
|
17
39
|
'''
|
|
18
40
|
app: 'DetectorNode' = request.app
|
|
19
41
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
42
|
+
response = AboutResponse(
|
|
43
|
+
operation_mode=app.operation_mode.value,
|
|
44
|
+
state=app.status.state,
|
|
45
|
+
model_info=app.detector_logic._model_info, # pylint: disable=protected-access
|
|
46
|
+
target_model=app.target_model.version if app.target_model is not None else None,
|
|
47
|
+
version_control=app.version_control.value
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return response
|
|
@@ -14,41 +14,24 @@ if TYPE_CHECKING:
|
|
|
14
14
|
router = APIRouter()
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
@router.
|
|
18
|
-
async def
|
|
17
|
+
@router.post("/reset")
|
|
18
|
+
async def _reset(request: Request):
|
|
19
19
|
'''
|
|
20
|
+
Soft-Reset the detector node.
|
|
21
|
+
|
|
20
22
|
Example Usage
|
|
21
23
|
|
|
22
|
-
curl -X
|
|
24
|
+
curl -X POST http://hosturl/reset
|
|
23
25
|
'''
|
|
24
|
-
|
|
26
|
+
logging.info('BC: reset')
|
|
25
27
|
detector_node: 'DetectorNode' = request.app
|
|
26
28
|
|
|
27
|
-
if state == 'off':
|
|
28
|
-
logging.info('BC: turning socketio off')
|
|
29
|
-
await detector_node.sio_client.disconnect()
|
|
30
|
-
if state == 'on':
|
|
31
|
-
logging.info('BC: turning socketio on')
|
|
32
|
-
await detector_node.connect_sio()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@router.post("/reset")
|
|
36
|
-
async def _reset(request: Request):
|
|
37
|
-
logging.info('BC: reset')
|
|
38
29
|
try:
|
|
39
30
|
shutil.rmtree(GLOBALS.data_folder, ignore_errors=True)
|
|
40
31
|
os.makedirs(GLOBALS.data_folder, exist_ok=True)
|
|
41
32
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
# restart_path.touch()
|
|
46
|
-
# assert isinstance(request.app, 'DetectorNode')
|
|
47
|
-
await request.app.soft_reload()
|
|
48
|
-
|
|
49
|
-
# assert isinstance(request.app, DetectorNode)
|
|
50
|
-
# request.app.reload(reason='------- reset was called from backdoor controls',)
|
|
51
|
-
except Exception as e:
|
|
52
|
-
logging.error(f'BC: could not reset: {e}')
|
|
33
|
+
await detector_node.soft_reload()
|
|
34
|
+
except Exception:
|
|
35
|
+
logging.exception('BC: could not reset:')
|
|
53
36
|
return False
|
|
54
37
|
return True
|
|
@@ -5,38 +5,39 @@ import numpy as np
|
|
|
5
5
|
from fastapi import APIRouter, File, Header, Request, UploadFile
|
|
6
6
|
from fastapi.responses import JSONResponse
|
|
7
7
|
|
|
8
|
+
from ...data_classes.detections import Detections
|
|
9
|
+
|
|
8
10
|
if TYPE_CHECKING:
|
|
9
11
|
from ..detector_node import DetectorNode
|
|
10
12
|
|
|
11
|
-
|
|
12
13
|
router = APIRouter()
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
@router.post("/detect")
|
|
16
|
+
@router.post("/detect", response_model=Detections)
|
|
16
17
|
async def http_detect(
|
|
17
18
|
request: Request,
|
|
18
|
-
file: UploadFile = File(
|
|
19
|
-
camera_id: Optional[str] = Header(None),
|
|
20
|
-
mac: Optional[str] = Header(None),
|
|
21
|
-
tags: Optional[str] = Header(None),
|
|
22
|
-
source: Optional[str] = Header(None),
|
|
23
|
-
autoupload: Optional[str] = Header(None
|
|
19
|
+
file: UploadFile = File(..., description='The image file to run detection on'),
|
|
20
|
+
camera_id: Optional[str] = Header(None, description='The camera id (used by learning loop)'),
|
|
21
|
+
mac: Optional[str] = Header(None, description='The camera mac address (used by learning loop)'),
|
|
22
|
+
tags: Optional[str] = Header(None, description='Tags to add to the image (used by learning loop)'),
|
|
23
|
+
source: Optional[str] = Header(None, description='The source of the image (used by learning loop)'),
|
|
24
|
+
autoupload: Optional[str] = Header(None, description='Mode to decide whether to upload the image to the learning loop',
|
|
25
|
+
examples=['filtered', 'all', 'disabled']),
|
|
24
26
|
):
|
|
25
27
|
"""
|
|
26
|
-
|
|
28
|
+
Single image example:
|
|
29
|
+
|
|
30
|
+
curl --request POST -F 'file=@test.jpg' localhost:8004/detect -H 'autoupload: all' -H 'camera-id: front_cam' -H 'source: test' -H 'tags: test,test2'
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
Multiple images example:
|
|
29
33
|
|
|
30
34
|
for i in `seq 1 10`; do time curl --request POST -F 'file=@test.jpg' localhost:8004/detect; done
|
|
31
35
|
|
|
32
|
-
You can additionally provide the following camera parameters:
|
|
33
|
-
- `autoupload`: configures auto-submission to the learning loop; `filtered` (default), `all`, `disabled` (example curl parameter `-H 'autoupload: all'`)
|
|
34
|
-
- `camera-id`: a string which groups images for submission together (example curl parameter `-H 'camera-id: front_cam'`)
|
|
35
36
|
"""
|
|
36
37
|
try:
|
|
37
38
|
np_image = np.fromfile(file.file, np.uint8)
|
|
38
39
|
except Exception as exc:
|
|
39
|
-
logging.exception(
|
|
40
|
+
logging.exception('Error during reading of image %s.', file.filename)
|
|
40
41
|
raise Exception(f'Uploaded file {file.filename} is no image file.') from exc
|
|
41
42
|
|
|
42
43
|
try:
|
|
@@ -47,6 +48,6 @@ async def http_detect(
|
|
|
47
48
|
source=source,
|
|
48
49
|
autoupload=autoupload)
|
|
49
50
|
except Exception as exc:
|
|
50
|
-
logging.exception(
|
|
51
|
+
logging.exception('Error during detection of image %s.', file.filename)
|
|
51
52
|
raise Exception(f'Error during detection of image {file.filename}.') from exc
|
|
52
|
-
return
|
|
53
|
+
return detections
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
|
|
2
2
|
import os
|
|
3
|
+
import sys
|
|
4
|
+
from dataclasses import dataclass, field
|
|
3
5
|
from enum import Enum
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
6
|
+
from typing import TYPE_CHECKING, List
|
|
5
7
|
|
|
6
8
|
from fastapi import APIRouter, HTTPException, Request
|
|
7
9
|
|
|
@@ -10,6 +12,7 @@ from ...globals import GLOBALS
|
|
|
10
12
|
|
|
11
13
|
if TYPE_CHECKING:
|
|
12
14
|
from ..detector_node import DetectorNode
|
|
15
|
+
KWONLY_SLOTS = {'kw_only': True, 'slots': True} if sys.version_info >= (3, 10) else {}
|
|
13
16
|
|
|
14
17
|
router = APIRouter()
|
|
15
18
|
|
|
@@ -20,9 +23,20 @@ class VersionMode(str, Enum):
|
|
|
20
23
|
Pause = 'pause' # will pause the updates
|
|
21
24
|
|
|
22
25
|
|
|
26
|
+
@dataclass(**KWONLY_SLOTS)
|
|
27
|
+
class ModelVersionResponse:
|
|
28
|
+
current_version: str = field(metadata={"description": "The version of the model currently used by the detector."})
|
|
29
|
+
target_version: str = field(metadata={"description": "The target model version set in the detector."})
|
|
30
|
+
loop_version: str = field(metadata={"description": "The target model version specified by the loop."})
|
|
31
|
+
local_versions: List[str] = field(metadata={"description": "The locally available versions of the model."})
|
|
32
|
+
version_control: str = field(metadata={"description": "The version control mode."})
|
|
33
|
+
|
|
34
|
+
|
|
23
35
|
@router.get("/model_version")
|
|
24
36
|
async def get_version(request: Request):
|
|
25
37
|
'''
|
|
38
|
+
Get information about the model version control and the current model version.
|
|
39
|
+
|
|
26
40
|
Example Usage
|
|
27
41
|
curl http://localhost/model_version
|
|
28
42
|
'''
|
|
@@ -35,28 +49,31 @@ async def get_version(request: Request):
|
|
|
35
49
|
loop_version = app.loop_deployment_target.version if app.loop_deployment_target is not None else 'None'
|
|
36
50
|
|
|
37
51
|
local_versions: list[str] = []
|
|
38
|
-
|
|
39
|
-
local_models = os.listdir(os.path.
|
|
52
|
+
models_path = os.path.join(GLOBALS.data_folder, 'models')
|
|
53
|
+
local_models = os.listdir(models_path) if os.path.exists(models_path) else []
|
|
40
54
|
for model in local_models:
|
|
41
55
|
if model.replace('.', '').isdigit():
|
|
42
56
|
local_versions.append(model)
|
|
43
57
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
response = ModelVersionResponse(
|
|
59
|
+
current_version=current_version,
|
|
60
|
+
target_version=target_version,
|
|
61
|
+
loop_version=loop_version,
|
|
62
|
+
local_versions=local_versions,
|
|
63
|
+
version_control=app.version_control.value,
|
|
64
|
+
)
|
|
65
|
+
return response
|
|
51
66
|
|
|
52
67
|
|
|
53
68
|
@router.put("/model_version")
|
|
54
69
|
async def put_version(request: Request):
|
|
55
70
|
'''
|
|
71
|
+
Set the model version control mode.
|
|
72
|
+
|
|
56
73
|
Example Usage
|
|
57
|
-
curl -X PUT -d "follow_loop" http://
|
|
58
|
-
curl -X PUT -d "pause" http://
|
|
59
|
-
curl -X PUT -d "13.6" http://
|
|
74
|
+
curl -X PUT -d "follow_loop" http://hosturl/model_version
|
|
75
|
+
curl -X PUT -d "pause" http://hosturl/model_version
|
|
76
|
+
curl -X PUT -d "13.6" http://hosturl/model_version
|
|
60
77
|
'''
|
|
61
78
|
app: 'DetectorNode' = request.app
|
|
62
79
|
content = str(await request.body(), 'utf-8')
|
|
@@ -22,7 +22,10 @@ class OperationMode(str, Enum):
|
|
|
22
22
|
@router.put("/operation_mode")
|
|
23
23
|
async def put_operation_mode(request: Request):
|
|
24
24
|
'''
|
|
25
|
+
Set the operation mode of the detector node.
|
|
26
|
+
|
|
25
27
|
Example Usage
|
|
28
|
+
|
|
26
29
|
curl -X PUT -d "check_for_updates" http://localhost/operation_mode
|
|
27
30
|
curl -X PUT -d "detecting" http://localhost/operation_mode
|
|
28
31
|
'''
|
|
@@ -34,22 +37,25 @@ async def put_operation_mode(request: Request):
|
|
|
34
37
|
raise HTTPException(422, str(exc)) from exc
|
|
35
38
|
node: DetectorNode = request.app
|
|
36
39
|
|
|
37
|
-
logging.info(
|
|
38
|
-
logging.info(
|
|
39
|
-
logging.info(
|
|
40
|
+
logging.info('current node state : %s', node.status.state)
|
|
41
|
+
logging.info('current operation mode : %s', node.operation_mode.value)
|
|
42
|
+
logging.info('target operation mode : %s', target_mode)
|
|
40
43
|
if target_mode == node.operation_mode:
|
|
41
44
|
logging.info('operation mode already set')
|
|
42
45
|
return "OK"
|
|
43
46
|
|
|
44
47
|
await node.set_operation_mode(target_mode)
|
|
45
|
-
logging.info(
|
|
48
|
+
logging.info('operation mode set to : %s', target_mode)
|
|
46
49
|
return "OK"
|
|
47
50
|
|
|
48
51
|
|
|
49
|
-
@router.get("/operation_mode")
|
|
52
|
+
@router.get("/operation_mode", response_class=PlainTextResponse)
|
|
50
53
|
async def get_operation_mode(request: Request):
|
|
51
54
|
'''
|
|
55
|
+
Get the operation mode of the detector node.
|
|
56
|
+
|
|
52
57
|
Example Usage
|
|
58
|
+
|
|
53
59
|
curl http://localhost/operation_mode
|
|
54
60
|
'''
|
|
55
61
|
return PlainTextResponse(request.app.operation_mode.value)
|