learning-loop-node 0.25.0__tar.gz → 0.26.0__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.
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/PKG-INFO +11 -7
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/README.md +10 -6
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/data_exchanger.py +36 -13
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/detector_node.py +37 -18
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/inbox_filter/relevance_filter.py +2 -3
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/outbox.py +5 -15
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/rest/detect.py +1 -2
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/rest/upload.py +2 -9
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/helpers/misc.py +33 -8
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/exceptions.py +6 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/io_helpers.py +12 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/trainer_logic_generic.py +26 -7
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node.egg-info/PKG-INFO +11 -7
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/pyproject.toml +1 -1
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/__init__.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/annotation/__init__.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/annotation/annotator_logic.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/annotation/annotator_node.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/data_classes/__init__.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/data_classes/annotation_data.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/data_classes/annotations.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/data_classes/detections.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/data_classes/general.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/data_classes/image_metadata.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/data_classes/socket_response.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/data_classes/training.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/__init__.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/categories.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/detector_logic.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/exceptions.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/geometry.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/inbox_filter/__init__.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/inbox_filter/cam_observation_history.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/postprocess.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/rest/__init__.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/rest/about.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/rest/backdoor_controls.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/rest/model_version_control.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/rest/operation_mode.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/rest/outbox_mode.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/enums/__init__.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/enums/annotator.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/enums/detector.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/enums/general.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/enums/trainer.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/globals.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/helpers/__init__.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/helpers/background_tasks.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/helpers/entrypoint.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/helpers/environment_reader.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/helpers/gdrive_downloader.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/helpers/log_conf.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/helpers/run.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/loop_communication.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/node.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/py.typed +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/rest.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/__init__.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/batch_size.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/cuda.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/downloader.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/executor.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/metrics.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/rest/__init__.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/rest/backdoor_controls.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/subprocess.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/test_executor.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/trainer_logic.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/trainer_node.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/training_slot.py +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node.egg-info/SOURCES.txt +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node.egg-info/dependency_links.txt +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node.egg-info/requires.txt +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node.egg-info/top_level.txt +0 -0
- {learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learning_loop_node
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.26.0
|
|
4
4
|
Summary: Python Library for Nodes which connect to the Zauberzeug Learning Loop
|
|
5
5
|
Author-email: Zauberzeug GmbH <info@zauberzeug.com>
|
|
6
6
|
License: MIT
|
|
@@ -114,9 +114,10 @@ Example usage:
|
|
|
114
114
|
To use the **SocketIO** inference EPs, the caller needs to connect to the detector node's SocketIO server and emit the `detect` or `batch_detect` event with the image data and image metadata. The `detect` endpoint receives a dictionary, with the following entries:
|
|
115
115
|
|
|
116
116
|
- `image`: The image data as dictionary with the following keys:
|
|
117
|
-
- `
|
|
118
|
-
- `
|
|
119
|
-
- `
|
|
117
|
+
- `format`: `raw` (default) or `jpeg`
|
|
118
|
+
- `bytes`: bytes of the ndarray (retrieved via `ndarray.tobytes(order='C')`), or the JPEG-encoded image for format `jpeg`
|
|
119
|
+
- `dtype`: data type of the ndarray as string (e.g. `uint8`, `float32`, etc.) (raw only)
|
|
120
|
+
- `shape`: shape of the ndarray as tuple of ints (e.g. `(480, 640, 3)`) (raw only)
|
|
120
121
|
- `camera_id`: optional camera identifier (string) used to improve the autoupload filtering
|
|
121
122
|
- `tags`: optional list of tags to add to the image in the learning loop
|
|
122
123
|
- `source`: optional source string
|
|
@@ -127,6 +128,8 @@ The `batch_detect` endpoint receives a dictionary, with the same entries as the
|
|
|
127
128
|
|
|
128
129
|
- `images`: List of image data dictionaries, each with the same structure as the `image` entry in the `detect` endpoint
|
|
129
130
|
|
|
131
|
+
Sending a camera's JPEG as it is (`format: 'jpeg'`) saves the bandwidth of a raw pixel array and spares the detector a re-encode for the upload.
|
|
132
|
+
|
|
130
133
|
Example code can be found [in the rosys implementation](https://github.com/zauberzeug/rosys/blob/main/rosys/vision/detector_hardware.py).
|
|
131
134
|
|
|
132
135
|
### Upload API
|
|
@@ -144,9 +147,10 @@ Example:
|
|
|
144
147
|
The detector also has a **SocketIO** upload endpoint that can be used to upload images and detections to the learning loop. The function receives a dictionary, with the following entries:
|
|
145
148
|
|
|
146
149
|
- `image`: the image data as dictionary with the following keys:
|
|
147
|
-
- `
|
|
148
|
-
- `
|
|
149
|
-
- `
|
|
150
|
+
- `format`: `raw` (default) or `jpeg`
|
|
151
|
+
- `bytes`: bytes of the ndarray (retrieved via `ndarray.tobytes(order='C')`), or the JPEG-encoded image for format `jpeg`
|
|
152
|
+
- `dtype`: data type of the ndarray as string (e.g. `uint8`, `float32`, etc.) (raw only)
|
|
153
|
+
- `shape`: shape of the ndarray as tuple of ints (e.g. `(480, 640, 3)`) (raw only)
|
|
150
154
|
- `metadata`: a dictionary representing the image metadata. If metadata contains detections and/or annotations, UUIDs for the classes are automatically determined based on the category names. Metadata should follow the schema of the `ImageMetadata` data class.
|
|
151
155
|
- `upload_priority`: Optional boolean flag to prioritize the upload (defaults to False)
|
|
152
156
|
|
|
@@ -81,9 +81,10 @@ Example usage:
|
|
|
81
81
|
To use the **SocketIO** inference EPs, the caller needs to connect to the detector node's SocketIO server and emit the `detect` or `batch_detect` event with the image data and image metadata. The `detect` endpoint receives a dictionary, with the following entries:
|
|
82
82
|
|
|
83
83
|
- `image`: The image data as dictionary with the following keys:
|
|
84
|
-
- `
|
|
85
|
-
- `
|
|
86
|
-
- `
|
|
84
|
+
- `format`: `raw` (default) or `jpeg`
|
|
85
|
+
- `bytes`: bytes of the ndarray (retrieved via `ndarray.tobytes(order='C')`), or the JPEG-encoded image for format `jpeg`
|
|
86
|
+
- `dtype`: data type of the ndarray as string (e.g. `uint8`, `float32`, etc.) (raw only)
|
|
87
|
+
- `shape`: shape of the ndarray as tuple of ints (e.g. `(480, 640, 3)`) (raw only)
|
|
87
88
|
- `camera_id`: optional camera identifier (string) used to improve the autoupload filtering
|
|
88
89
|
- `tags`: optional list of tags to add to the image in the learning loop
|
|
89
90
|
- `source`: optional source string
|
|
@@ -94,6 +95,8 @@ The `batch_detect` endpoint receives a dictionary, with the same entries as the
|
|
|
94
95
|
|
|
95
96
|
- `images`: List of image data dictionaries, each with the same structure as the `image` entry in the `detect` endpoint
|
|
96
97
|
|
|
98
|
+
Sending a camera's JPEG as it is (`format: 'jpeg'`) saves the bandwidth of a raw pixel array and spares the detector a re-encode for the upload.
|
|
99
|
+
|
|
97
100
|
Example code can be found [in the rosys implementation](https://github.com/zauberzeug/rosys/blob/main/rosys/vision/detector_hardware.py).
|
|
98
101
|
|
|
99
102
|
### Upload API
|
|
@@ -111,9 +114,10 @@ Example:
|
|
|
111
114
|
The detector also has a **SocketIO** upload endpoint that can be used to upload images and detections to the learning loop. The function receives a dictionary, with the following entries:
|
|
112
115
|
|
|
113
116
|
- `image`: the image data as dictionary with the following keys:
|
|
114
|
-
- `
|
|
115
|
-
- `
|
|
116
|
-
- `
|
|
117
|
+
- `format`: `raw` (default) or `jpeg`
|
|
118
|
+
- `bytes`: bytes of the ndarray (retrieved via `ndarray.tobytes(order='C')`), or the JPEG-encoded image for format `jpeg`
|
|
119
|
+
- `dtype`: data type of the ndarray as string (e.g. `uint8`, `float32`, etc.) (raw only)
|
|
120
|
+
- `shape`: shape of the ndarray as tuple of ints (e.g. `(480, 640, 3)`) (raw only)
|
|
117
121
|
- `metadata`: a dictionary representing the image metadata. If metadata contains detections and/or annotations, UUIDs for the classes are automatically determined based on the category names. Metadata should follow the schema of the `ImageMetadata` data class.
|
|
118
122
|
- `upload_priority`: Optional boolean flag to prioritize the upload (defaults to False)
|
|
119
123
|
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/data_exchanger.py
RENAMED
|
@@ -12,9 +12,11 @@ from typing import Any, Dict, List, Optional
|
|
|
12
12
|
import aiofiles # type: ignore
|
|
13
13
|
|
|
14
14
|
from .data_classes import Context
|
|
15
|
-
from .helpers.misc import create_resource_paths, create_task, is_valid_image
|
|
15
|
+
from .helpers.misc import create_resource_paths, create_task, is_valid_image, is_valid_uuid4
|
|
16
16
|
from .loop_communication import LoopCommunicator
|
|
17
|
-
from .trainer.exceptions import CriticalError
|
|
17
|
+
from .trainer.exceptions import CriticalError, ModelUploadFailedError
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
class DownloadError(Exception):
|
|
@@ -171,20 +173,41 @@ class DataExchanger():
|
|
|
171
173
|
logging.info('Downloaded model %s(%s) to %s.', model_uuid, model_format, target_folder)
|
|
172
174
|
return created_files
|
|
173
175
|
|
|
174
|
-
async def upload_model_get_uuid(self, context: Context, files: List[str], training_number: Optional[int], mformat: str) -> str:
|
|
176
|
+
async def upload_model_get_uuid(self, context: Context, files: List[str], training_number: Optional[int], mformat: str) -> str | None:
|
|
175
177
|
"""Used by the trainers. Function returns the new model uuid to use for detection.
|
|
178
|
+
A loop that already stores the model in this format answers 409 and should name the model's uuid there.
|
|
176
179
|
|
|
177
|
-
:return: The new model uuid.
|
|
178
|
-
:raise CriticalError: If the upload
|
|
180
|
+
:return: The new model uuid, or None if the loop already stores the model in this format but names no valid uuid.
|
|
181
|
+
:raise CriticalError: If the loop rejects the upload with a client error (other than 408 and 429).
|
|
182
|
+
:raise ModelUploadFailedError: If the loop could not store the model, e.g. on a server error.
|
|
179
183
|
"""
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
path = f'/{context.organization}/projects/{context.project}/trainings/{training_number}/models/latest/{mformat}/file'
|
|
185
|
+
response = await self.loop_communicator.put(path, files=files)
|
|
186
|
+
if response.status_code == HTTPStatus.CONFLICT:
|
|
187
|
+
try:
|
|
188
|
+
stored_model_id = response.json()['detail']['id']
|
|
189
|
+
except (ValueError, KeyError, TypeError): # older loops name no id
|
|
190
|
+
stored_model_id = None
|
|
191
|
+
if not is_valid_uuid4(stored_model_id):
|
|
192
|
+
logger.warning('The loop already stores the model of training %s in format %s, but names no id',
|
|
193
|
+
training_number, mformat)
|
|
194
|
+
return None
|
|
195
|
+
logger.info('The loop already stores model %s of training %s in format %s',
|
|
196
|
+
stored_model_id, training_number, mformat)
|
|
197
|
+
return stored_model_id
|
|
198
|
+
if response.status_code != HTTPStatus.OK:
|
|
199
|
+
logger.error('Could not upload model for training %s, format %s: %s %s',
|
|
200
|
+
training_number, mformat, response.status_code, response.text)
|
|
201
|
+
error = CriticalError if _rejects_upload(response.status_code) else ModelUploadFailedError
|
|
202
|
+
raise error(
|
|
203
|
+
f'Could not upload model for training {training_number}, format {mformat}: {response.status_code} {response.text}')
|
|
186
204
|
|
|
187
205
|
uploaded_model = response.json()
|
|
188
|
-
|
|
189
|
-
|
|
206
|
+
logger.info('Uploaded model for training %s, format %s. Response is: %s',
|
|
207
|
+
training_number, mformat, uploaded_model)
|
|
190
208
|
return uploaded_model['id']
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _rejects_upload(status_code: int) -> bool:
|
|
212
|
+
"""A client error other than 408 and 429 answers the same however often the upload is sent."""
|
|
213
|
+
return 400 <= status_code < 500 and status_code not in (HTTPStatus.REQUEST_TIMEOUT, HTTPStatus.TOO_MANY_REQUESTS)
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/detector_node.py
RENAMED
|
@@ -6,6 +6,7 @@ import os
|
|
|
6
6
|
import shutil
|
|
7
7
|
import subprocess
|
|
8
8
|
import sys
|
|
9
|
+
from collections.abc import Sequence
|
|
9
10
|
from dataclasses import asdict, dataclass
|
|
10
11
|
from datetime import datetime
|
|
11
12
|
from typing import Dict, List, Literal, Optional, Union
|
|
@@ -30,7 +31,7 @@ from ..data_exchanger import DataExchanger, DownloadError
|
|
|
30
31
|
from ..enums import OperationMode, VersionMode
|
|
31
32
|
from ..globals import GLOBALS
|
|
32
33
|
from ..helpers import background_tasks, environment_reader, run
|
|
33
|
-
from ..helpers.misc import
|
|
34
|
+
from ..helpers.misc import ImageData, image_from_dict, jpg_bytes_to_numpy_array
|
|
34
35
|
from ..node import Node
|
|
35
36
|
from .categories import category_by_name
|
|
36
37
|
from .detector_logic import DetectorLogic, DetectorLogicFactory
|
|
@@ -230,9 +231,10 @@ class DetectorNode(Node):
|
|
|
230
231
|
|
|
231
232
|
The data dict has the following schema:
|
|
232
233
|
- image: The image data as dictionary:
|
|
233
|
-
-
|
|
234
|
-
-
|
|
235
|
-
-
|
|
234
|
+
- format: 'raw' (default) or 'jpeg' (optional)
|
|
235
|
+
- bytes: bytes of the ndarray, or the JPEG-encoded image for format 'jpeg'
|
|
236
|
+
- dtype: data type of the ndarray (raw only)
|
|
237
|
+
- shape: shape of the ndarray (raw only)
|
|
236
238
|
- camera_id: Optional camera ID
|
|
237
239
|
- tags: Optional list of tags
|
|
238
240
|
- source: Optional source string
|
|
@@ -240,7 +242,7 @@ class DetectorNode(Node):
|
|
|
240
242
|
- creation_date: Optional creation date in isoformat string
|
|
241
243
|
"""
|
|
242
244
|
try:
|
|
243
|
-
image =
|
|
245
|
+
image = image_from_dict(data['image'])
|
|
244
246
|
except Exception:
|
|
245
247
|
self.log.exception('could not parse image from socketio')
|
|
246
248
|
return {'error': 'could not parse image from data'}
|
|
@@ -252,7 +254,7 @@ class DetectorNode(Node):
|
|
|
252
254
|
tags=data.get('tags', []),
|
|
253
255
|
source=data.get('source', None),
|
|
254
256
|
autoupload=data.get('autoupload', 'filtered'),
|
|
255
|
-
creation_date=data.get('creation_date', None)
|
|
257
|
+
creation_date=data.get('creation_date', None),
|
|
256
258
|
)
|
|
257
259
|
return jsonable_encoder(asdict(det))
|
|
258
260
|
except DetectorUnavailableError as e:
|
|
@@ -271,7 +273,7 @@ class DetectorNode(Node):
|
|
|
271
273
|
"""
|
|
272
274
|
try:
|
|
273
275
|
images_data = data['images']
|
|
274
|
-
images = [
|
|
276
|
+
images = [image_from_dict(image) for image in images_data]
|
|
275
277
|
except Exception:
|
|
276
278
|
self.log.exception('could not parse images from socketio')
|
|
277
279
|
return {'error': 'could not parse images from data'}
|
|
@@ -283,7 +285,7 @@ class DetectorNode(Node):
|
|
|
283
285
|
camera_id=data.get('camera_id', None),
|
|
284
286
|
source=data.get('source', None),
|
|
285
287
|
autoupload=data.get('autoupload', 'filtered'),
|
|
286
|
-
creation_date=data.get('creation_date', None)
|
|
288
|
+
creation_date=data.get('creation_date', None),
|
|
287
289
|
)
|
|
288
290
|
return jsonable_encoder(asdict(det))
|
|
289
291
|
except DetectorUnavailableError as e:
|
|
@@ -336,9 +338,11 @@ class DetectorNode(Node):
|
|
|
336
338
|
|
|
337
339
|
The data dict must contain:
|
|
338
340
|
- image: The image data as dictionary with the following keys:
|
|
339
|
-
-
|
|
340
|
-
-
|
|
341
|
-
|
|
341
|
+
- format: 'raw' (default) or 'jpeg' (optional)
|
|
342
|
+
- bytes: bytes of the ndarray (retrieved via `ndarray.tobytes(order='C')`),
|
|
343
|
+
or the JPEG-encoded image for format 'jpeg'
|
|
344
|
+
- dtype: data type of the ndarray as string (e.g. `uint8`, `float32`, etc.) (raw only)
|
|
345
|
+
- shape: shape of the ndarray as tuple of ints (e.g. `(480, 640, 3)`) (raw only)
|
|
342
346
|
- metadata: The metadata for the image (optional). Besides detections and annotations it
|
|
343
347
|
may carry `tags`, `source`, `created` and `state`. A `state` of e.g. "trash" makes the
|
|
344
348
|
loop file the image into that state instead of its default one.
|
|
@@ -362,7 +366,7 @@ class DetectorNode(Node):
|
|
|
362
366
|
image_metadata = ImageMetadata()
|
|
363
367
|
|
|
364
368
|
try:
|
|
365
|
-
image =
|
|
369
|
+
image = image_from_dict(data['image'])
|
|
366
370
|
except Exception:
|
|
367
371
|
self.log.exception('could not parse image from socketio')
|
|
368
372
|
return {'error': 'could not parse image from data'}
|
|
@@ -607,7 +611,7 @@ class DetectorNode(Node):
|
|
|
607
611
|
self.log.error('could not reload app')
|
|
608
612
|
|
|
609
613
|
async def get_detections(self,
|
|
610
|
-
image:
|
|
614
|
+
image: ImageData,
|
|
611
615
|
tags: List[str],
|
|
612
616
|
*,
|
|
613
617
|
camera_id: Optional[str] = None,
|
|
@@ -620,11 +624,15 @@ class DetectorNode(Node):
|
|
|
620
624
|
Used when an image is received via REST or SocketIO.
|
|
621
625
|
This function infers the detections from the image,
|
|
622
626
|
cares about uploading to the loop and returns the detections as ImageMetadata object.
|
|
627
|
+
The image is a pixel array or JPEG bytes; JPEG bytes are decoded for the inference
|
|
628
|
+
and stored as they are if the image is uploaded.
|
|
623
629
|
Raises exception if no model is loaded.
|
|
624
630
|
"""
|
|
631
|
+
_unwrap_detector(self._detector) # fails fast, before the decode
|
|
632
|
+
array = await _pixel_array(image)
|
|
625
633
|
async with self.detection_lock:
|
|
626
634
|
detector = _unwrap_detector(self._detector)
|
|
627
|
-
metadata = await run.io_bound(detector.logic.evaluate,
|
|
635
|
+
metadata = await run.io_bound(detector.logic.evaluate, array)
|
|
628
636
|
|
|
629
637
|
metadata.tags.extend(tags)
|
|
630
638
|
metadata.source = source
|
|
@@ -646,7 +654,7 @@ class DetectorNode(Node):
|
|
|
646
654
|
return metadata
|
|
647
655
|
|
|
648
656
|
async def get_batch_detections(self,
|
|
649
|
-
images:
|
|
657
|
+
images: Sequence[ImageData],
|
|
650
658
|
tags: List[str],
|
|
651
659
|
*,
|
|
652
660
|
camera_id: Optional[str] = None,
|
|
@@ -658,11 +666,15 @@ class DetectorNode(Node):
|
|
|
658
666
|
|
|
659
667
|
This function infers the detections from all images,
|
|
660
668
|
cares about uploading to the loop and returns the detections as a list of ImageMetadata.
|
|
669
|
+
Each image is a pixel array or JPEG bytes; JPEG bytes are decoded for the inference
|
|
670
|
+
and stored as they are if the image is uploaded.
|
|
661
671
|
Raises exception if no model is loaded.
|
|
662
672
|
"""
|
|
673
|
+
_unwrap_detector(self._detector) # fails fast, before the decode
|
|
674
|
+
arrays = await asyncio.gather(*(_pixel_array(image) for image in images))
|
|
663
675
|
async with self.detection_lock:
|
|
664
676
|
detector = _unwrap_detector(self._detector)
|
|
665
|
-
all_detections = await run.io_bound(detector.logic.batch_evaluate,
|
|
677
|
+
all_detections = await run.io_bound(detector.logic.batch_evaluate, arrays)
|
|
666
678
|
|
|
667
679
|
for metadata in all_detections.items:
|
|
668
680
|
metadata.tags.extend(tags)
|
|
@@ -687,14 +699,14 @@ class DetectorNode(Node):
|
|
|
687
699
|
|
|
688
700
|
async def upload_images(
|
|
689
701
|
self, *,
|
|
690
|
-
images:
|
|
702
|
+
images: Sequence[ImageData],
|
|
691
703
|
images_metadata: Optional[ImagesMetadata] = None,
|
|
692
704
|
upload_priority: bool = False
|
|
693
705
|
) -> None:
|
|
694
706
|
"""Save images to the outbox using an asyncio executor.
|
|
695
707
|
Used by SIO and REST upload endpoints.
|
|
696
708
|
|
|
697
|
-
:param images: List of images to upload
|
|
709
|
+
:param images: List of images to upload, pixel arrays or JPEG bytes
|
|
698
710
|
:param images_metadata: Optional metadata for all images
|
|
699
711
|
:param upload_priority: Whether to upload the images with priority
|
|
700
712
|
:raises ValueError: If the number of images and number of metadata items do not match
|
|
@@ -760,6 +772,13 @@ class DetectorUnavailableError(Exception):
|
|
|
760
772
|
pass
|
|
761
773
|
|
|
762
774
|
|
|
775
|
+
async def _pixel_array(image: ImageData) -> np.ndarray:
|
|
776
|
+
"""The pixels of an image; JPEG bytes are decoded in a thread, as that takes a while for a camera image."""
|
|
777
|
+
if isinstance(image, np.ndarray):
|
|
778
|
+
return image
|
|
779
|
+
return await run.io_bound(jpg_bytes_to_numpy_array, image)
|
|
780
|
+
|
|
781
|
+
|
|
763
782
|
def _unwrap_detector(state: '_DetectorState') -> _ActiveDetector:
|
|
764
783
|
"""Return the active detector or raise DetectorUnavailableError."""
|
|
765
784
|
match state:
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
from typing import Dict, List, Optional
|
|
2
2
|
|
|
3
|
-
import numpy as np
|
|
4
|
-
|
|
5
3
|
from ...data_classes.image_metadata import ImageMetadata
|
|
4
|
+
from ...helpers.misc import ImageData
|
|
6
5
|
from ..outbox import Outbox
|
|
7
6
|
from .cam_observation_history import CamObservationHistory
|
|
8
7
|
|
|
@@ -17,7 +16,7 @@ class RelevanceFilter():
|
|
|
17
16
|
async def may_upload_detections(self,
|
|
18
17
|
image_metadata: ImageMetadata,
|
|
19
18
|
cam_id: Optional[str],
|
|
20
|
-
image:
|
|
19
|
+
image: ImageData) -> List[str]:
|
|
21
20
|
"""Check if the detection should be uploaded to the outbox.
|
|
22
21
|
If so, upload it and return the list of causes for the upload.
|
|
23
22
|
"""
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/outbox.py
RENAMED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import io
|
|
3
2
|
import json
|
|
4
3
|
import logging
|
|
5
4
|
import os
|
|
@@ -17,15 +16,13 @@ from typing import List, Optional, Tuple, TypeVar, Union
|
|
|
17
16
|
|
|
18
17
|
import aiohttp
|
|
19
18
|
import numpy as np
|
|
20
|
-
import PIL
|
|
21
|
-
import PIL.Image # type: ignore
|
|
22
19
|
from fastapi.encoders import jsonable_encoder
|
|
23
20
|
|
|
24
21
|
from ..data_classes import ImageMetadata
|
|
25
22
|
from ..enums import OutboxMode
|
|
26
23
|
from ..globals import GLOBALS
|
|
27
24
|
from ..helpers import environment_reader, run
|
|
28
|
-
from ..helpers.misc import numpy_array_to_jpg_bytes
|
|
25
|
+
from ..helpers.misc import ImageData, numpy_array_to_jpg_bytes
|
|
29
26
|
|
|
30
27
|
T = TypeVar('T')
|
|
31
28
|
|
|
@@ -80,16 +77,17 @@ class Outbox():
|
|
|
80
77
|
self.upload_folders.append(file)
|
|
81
78
|
|
|
82
79
|
async def save(self,
|
|
83
|
-
image:
|
|
80
|
+
image: ImageData,
|
|
84
81
|
image_metadata: Optional[ImageMetadata] = None,
|
|
85
82
|
upload_priority: bool = False) -> None:
|
|
86
83
|
"""
|
|
87
|
-
Save an image and its metadata to disk.
|
|
84
|
+
Save an image and its metadata to disk.
|
|
88
85
|
|
|
86
|
+
A pixel array is encoded as JPEG here, image bytes are stored unchanged for the loop to validate.
|
|
89
87
|
The data will be picked up by the continuous upload process.
|
|
90
88
|
"""
|
|
91
89
|
|
|
92
|
-
jpg_bytes = numpy_array_to_jpg_bytes(image)
|
|
90
|
+
jpg_bytes = numpy_array_to_jpg_bytes(image) if isinstance(image, np.ndarray) else image
|
|
93
91
|
|
|
94
92
|
if image_metadata is None:
|
|
95
93
|
image_metadata = ImageMetadata()
|
|
@@ -262,14 +260,6 @@ class Outbox():
|
|
|
262
260
|
else:
|
|
263
261
|
self.log.error('Could not upload images: %s', response.content)
|
|
264
262
|
|
|
265
|
-
def _is_valid_jpg(self, image: bytes) -> bool:
|
|
266
|
-
try:
|
|
267
|
-
_ = PIL.Image.open(io.BytesIO(image), formats=['JPEG'])
|
|
268
|
-
return True
|
|
269
|
-
except Exception:
|
|
270
|
-
self.log.exception('Invalid jpg image')
|
|
271
|
-
return False
|
|
272
|
-
|
|
273
263
|
async def ensure_continuous_upload_stopped(self) -> bool:
|
|
274
264
|
self.log.debug('Outbox: Ensuring continuous upload')
|
|
275
265
|
if not self._upload_process_alive():
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/rest/detect.py
RENAMED
|
@@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, Literal, Optional
|
|
|
4
4
|
from fastapi import APIRouter, File, Header, Request, UploadFile
|
|
5
5
|
|
|
6
6
|
from ...data_classes.image_metadata import ImageMetadata
|
|
7
|
-
from ...helpers.misc import jpg_bytes_to_numpy_array
|
|
8
7
|
|
|
9
8
|
if TYPE_CHECKING:
|
|
10
9
|
from ..detector_node import DetectorNode
|
|
@@ -42,7 +41,7 @@ async def http_detect(
|
|
|
42
41
|
raise Exception(f'Uploaded file {file.filename} is no image file.') from exc
|
|
43
42
|
|
|
44
43
|
try:
|
|
45
|
-
detections = await node.get_detections(image=
|
|
44
|
+
detections = await node.get_detections(image=file_bytes,
|
|
46
45
|
camera_id=camera_id or None,
|
|
47
46
|
tags=tags.split(',') if tags else [],
|
|
48
47
|
source=source,
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/rest/upload.py
RENAMED
|
@@ -3,7 +3,6 @@ from typing import TYPE_CHECKING, List, Optional
|
|
|
3
3
|
from fastapi import APIRouter, File, Query, Request, UploadFile
|
|
4
4
|
|
|
5
5
|
from ...data_classes.image_metadata import ImageMetadata, ImagesMetadata
|
|
6
|
-
from ...helpers.misc import jpg_bytes_to_numpy_array
|
|
7
6
|
|
|
8
7
|
if TYPE_CHECKING:
|
|
9
8
|
from ..detector_node import DetectorNode
|
|
@@ -29,14 +28,8 @@ async def upload_image(request: Request,
|
|
|
29
28
|
"""
|
|
30
29
|
node: 'DetectorNode' = request.app
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
images = []
|
|
35
|
-
for file_bytes in files_bytes:
|
|
36
|
-
images.append(jpg_bytes_to_numpy_array(file_bytes))
|
|
37
|
-
image_metadatas.append(ImageMetadata(source=source, created=creation_date))
|
|
38
|
-
|
|
39
|
-
images_metadata = ImagesMetadata(items=image_metadatas)
|
|
31
|
+
images = [await file.read() for file in files]
|
|
32
|
+
images_metadata = ImagesMetadata(items=[ImageMetadata(source=source, created=creation_date) for _ in images])
|
|
40
33
|
|
|
41
34
|
await node.upload_images(images=images,
|
|
42
35
|
images_metadata=images_metadata,
|
|
@@ -10,12 +10,12 @@ import sys
|
|
|
10
10
|
from dataclasses import asdict
|
|
11
11
|
from glob import glob
|
|
12
12
|
from time import perf_counter
|
|
13
|
-
from typing import Any, Coroutine, Dict, List, Optional, Tuple, TypeVar
|
|
13
|
+
from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, TypeVar, Union, get_args
|
|
14
14
|
from uuid import UUID, uuid4
|
|
15
15
|
|
|
16
16
|
import numpy as np
|
|
17
17
|
import pynvml
|
|
18
|
-
from PIL import Image
|
|
18
|
+
from PIL import Image, ImageOps
|
|
19
19
|
|
|
20
20
|
from ..data_classes.general import Context
|
|
21
21
|
from ..data_classes.socket_response import SocketResponse
|
|
@@ -220,31 +220,56 @@ def create_training_folder(project_folder: str, trainings_id: str) -> str:
|
|
|
220
220
|
return training_folder
|
|
221
221
|
|
|
222
222
|
|
|
223
|
+
ImageData = Union[np.ndarray, bytes]
|
|
224
|
+
"""An image as it was received: either a pixel array or a JPEG-encoded image."""
|
|
225
|
+
|
|
226
|
+
ImageFormat = Literal['raw', 'jpeg']
|
|
227
|
+
"""How the `bytes` of an image dict are encoded: a pixel array in C order described by `dtype` and `shape`, or a JPEG."""
|
|
228
|
+
|
|
229
|
+
|
|
223
230
|
def jpg_bytes_to_numpy_array(jpg_bytes: bytes) -> np.ndarray:
|
|
224
|
-
"""
|
|
225
|
-
image = Image.open(io.BytesIO(jpg_bytes))
|
|
231
|
+
"""Decode image bytes into a pixel array, rotated as the EXIF orientation tag says."""
|
|
232
|
+
image = ImageOps.exif_transpose(Image.open(io.BytesIO(jpg_bytes)))
|
|
226
233
|
return np.array(image)
|
|
227
234
|
|
|
228
235
|
|
|
229
236
|
def numpy_array_to_jpg_bytes(image_array: np.ndarray) -> bytes:
|
|
230
|
-
"""
|
|
237
|
+
"""Encode a pixel array as JPEG."""
|
|
231
238
|
buffer = io.BytesIO()
|
|
232
239
|
Image.fromarray(image_array).save(buffer, format="JPEG")
|
|
233
240
|
jpg_bytes = buffer.getvalue()
|
|
234
241
|
return jpg_bytes
|
|
235
242
|
|
|
236
243
|
|
|
244
|
+
def image_format_of(image_data: Dict) -> ImageFormat:
|
|
245
|
+
"""The format an image dict declares, 'raw' if it declares none."""
|
|
246
|
+
image_format = image_data.get('format', 'raw')
|
|
247
|
+
if image_format not in get_args(ImageFormat):
|
|
248
|
+
raise ValueError(f'unknown image format: {image_format}')
|
|
249
|
+
return image_format
|
|
250
|
+
|
|
251
|
+
|
|
237
252
|
def numpy_image_from_dict(image_data: Dict) -> np.ndarray:
|
|
238
253
|
"""
|
|
239
254
|
Convert image dict to numpy array.
|
|
240
255
|
|
|
241
256
|
The image_data dict should have the following keys:
|
|
242
|
-
-
|
|
243
|
-
-
|
|
244
|
-
-
|
|
257
|
+
- format: 'raw' (default) or 'jpeg' (optional)
|
|
258
|
+
- bytes: bytes of the image in C order (row-major), or the JPEG-encoded image for format 'jpeg'
|
|
259
|
+
- dtype: data type of the pixel values (raw only)
|
|
260
|
+
- shape: shape of the image (height, width, channels) (raw only)
|
|
245
261
|
"""
|
|
262
|
+
if image_format_of(image_data) == 'jpeg':
|
|
263
|
+
return jpg_bytes_to_numpy_array(image_data['bytes'])
|
|
246
264
|
|
|
247
265
|
image_bytes = image_data['bytes']
|
|
248
266
|
image_dtype = image_data['dtype']
|
|
249
267
|
image_shape = image_data['shape']
|
|
250
268
|
return np.frombuffer(image_bytes, dtype=image_dtype).reshape(image_shape, order='C')
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def image_from_dict(image_data: Dict) -> ImageData:
|
|
272
|
+
"""The image of an image dict as received: the JPEG bytes for format 'jpeg', the pixel array otherwise."""
|
|
273
|
+
if image_format_of(image_data) == 'jpeg':
|
|
274
|
+
return image_data['bytes']
|
|
275
|
+
return numpy_image_from_dict(image_data)
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/exceptions.py
RENAMED
|
@@ -5,6 +5,12 @@ class CriticalError(Exception):
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
class ModelUploadFailedError(Exception):
|
|
9
|
+
"""Raised when the loop could not store an uploaded model, e.g. because it answered with a server error.
|
|
10
|
+
Unlike a CriticalError it keeps the training, so the upload is tried again later.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
|
|
8
14
|
class TrainingRefusedError(Exception):
|
|
9
15
|
"""Raised when a trainer cannot take on a training it was asked to begin; nothing was set up."""
|
|
10
16
|
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/io_helpers.py
RENAMED
|
@@ -55,6 +55,7 @@ class ActiveTrainingIO:
|
|
|
55
55
|
self.context = context
|
|
56
56
|
|
|
57
57
|
self.mup_path = f'{training_folder}/model_uploading_progress.txt'
|
|
58
|
+
self.muuid_path = f'{training_folder}/uploaded_model_uuid.txt'
|
|
58
59
|
# string with placeholder gor index
|
|
59
60
|
self.det_path = f'{training_folder}' + '/detections_{0}.json'
|
|
60
61
|
self.dufi_path = f'{training_folder}/detection_uploading_json_index.txt'
|
|
@@ -73,6 +74,17 @@ class ActiveTrainingIO:
|
|
|
73
74
|
with open(self.mup_path, 'r') as f:
|
|
74
75
|
return f.read().split(',')
|
|
75
76
|
|
|
77
|
+
def save_uploaded_model_uuid(self, model_uuid: str) -> None:
|
|
78
|
+
"""Replaces the file in one step, so a crash while writing cannot leave it empty."""
|
|
79
|
+
with open(f'{self.muuid_path}.tmp', 'w') as f:
|
|
80
|
+
f.write(model_uuid)
|
|
81
|
+
os.replace(f'{self.muuid_path}.tmp', self.muuid_path)
|
|
82
|
+
|
|
83
|
+
def load_uploaded_model_uuid(self) -> str:
|
|
84
|
+
""":raise FileNotFoundError: If no upload has named the model uuid yet."""
|
|
85
|
+
with open(self.muuid_path) as f:
|
|
86
|
+
return f.read()
|
|
87
|
+
|
|
76
88
|
# detections
|
|
77
89
|
|
|
78
90
|
def _get_detection_file_names(self) -> List[Path]:
|
|
@@ -33,9 +33,17 @@ from .training_slot import TrainingSlot, training_slot_from_env
|
|
|
33
33
|
if TYPE_CHECKING:
|
|
34
34
|
from .trainer_node import TrainerNode
|
|
35
35
|
|
|
36
|
+
BASE_RETRY_DELAY = 5 # seconds before the first retry of a failed state
|
|
37
|
+
MAX_RETRY_DELAY = 300 # seconds
|
|
38
|
+
|
|
36
39
|
logger = logging.getLogger('learning_loop_node.trainer_logic_generic')
|
|
37
40
|
|
|
38
41
|
|
|
42
|
+
def _retry_delay(failures: int) -> float:
|
|
43
|
+
"""The wait before the next attempt of a state that has failed `failures` times in a row."""
|
|
44
|
+
return min(BASE_RETRY_DELAY * 2 ** min(failures - 1, 16), MAX_RETRY_DELAY)
|
|
45
|
+
|
|
46
|
+
|
|
39
47
|
class TrainerLogicGeneric(ABC):
|
|
40
48
|
|
|
41
49
|
def __init__(self, model_format: str, training_slot: TrainingSlot | None = None) -> None:
|
|
@@ -45,6 +53,7 @@ class TrainerLogicGeneric(ABC):
|
|
|
45
53
|
# '/{context.organization}/projects/{context.project}/models/{model_id}/{model_format}/file'
|
|
46
54
|
self.model_format: str = model_format
|
|
47
55
|
self.errors = Errors()
|
|
56
|
+
self._state_failures: Dict[str, int] = {} # failed attempts in a row, by error key
|
|
48
57
|
|
|
49
58
|
self.training_task: Optional[asyncio.Task] = None
|
|
50
59
|
self.shutdown_event: asyncio.Event = asyncio.Event()
|
|
@@ -264,6 +273,7 @@ class TrainerLogicGeneric(ABC):
|
|
|
264
273
|
Either via `begin_training` or `try_continue_run_if_incomplete`.
|
|
265
274
|
"""
|
|
266
275
|
self.errors.reset_all()
|
|
276
|
+
self._state_failures.clear()
|
|
267
277
|
try:
|
|
268
278
|
await self.training_slot.wait_for(self._slot_claim(self.training.context, self.training.training_number))
|
|
269
279
|
await self._training_loop()
|
|
@@ -321,8 +331,11 @@ class TrainerLogicGeneric(ABC):
|
|
|
321
331
|
- If the loop sends a StopTraining event, this will raise a CancelledError.
|
|
322
332
|
- States can raise a CriticalError indicating that there is no point in retrying the state.
|
|
323
333
|
- If any other error occurs, the error is stored in the errors object and the state is reset to the previous state.
|
|
334
|
+
The state is retried after a wait until the training is aborted.
|
|
324
335
|
'''
|
|
325
336
|
|
|
337
|
+
if failures := self._state_failures.get(error_key):
|
|
338
|
+
await asyncio.sleep(_retry_delay(failures))
|
|
326
339
|
await asyncio.sleep(0.1)
|
|
327
340
|
logger.info('Performing state: %s', state_during)
|
|
328
341
|
previous_state = self.training.training_state
|
|
@@ -351,6 +364,7 @@ class TrainerLogicGeneric(ABC):
|
|
|
351
364
|
print('Error in %s - Exception: %s', state_during, e, flush=True)
|
|
352
365
|
self.errors.set(error_key, str(e))
|
|
353
366
|
logger.exception('Error in %s - Exception: %s', state_during, e)
|
|
367
|
+
self._state_failures[error_key] = self._state_failures.get(error_key, 0) + 1
|
|
354
368
|
self.training.training_state = previous_state
|
|
355
369
|
return
|
|
356
370
|
else:
|
|
@@ -359,6 +373,7 @@ class TrainerLogicGeneric(ABC):
|
|
|
359
373
|
self.errors.reset(error_key)
|
|
360
374
|
self.training.training_state = state_after
|
|
361
375
|
|
|
376
|
+
self._state_failures.pop(error_key, None)
|
|
362
377
|
self.last_training_io.save(self.training)
|
|
363
378
|
|
|
364
379
|
async def _prepare(self) -> None:
|
|
@@ -440,10 +455,13 @@ class TrainerLogicGeneric(ABC):
|
|
|
440
455
|
Note that with the latest trainers the conversion to (.wts) is done by the trainer.
|
|
441
456
|
The conversion from .wts to .engine is done by the detector (needs to be done on target hardware).
|
|
442
457
|
Note that trainer may train with different classes, which is why we send an initial model.json file.
|
|
458
|
+
A format the loop already stores counts as uploaded.
|
|
459
|
+
The model UUID is kept beside the upload progress, so a retry after a restart still knows it.
|
|
443
460
|
|
|
444
461
|
:return: The new model UUID.
|
|
445
|
-
:raise CriticalError: If the latest model files cannot be obtained
|
|
446
|
-
|
|
462
|
+
:raise CriticalError: If the latest model files cannot be obtained,
|
|
463
|
+
or the model is uploaded but its UUID is unknown.
|
|
464
|
+
:raise ModelUploadFailedError: If the loop could not store a model file; the state is retried.
|
|
447
465
|
"""
|
|
448
466
|
|
|
449
467
|
files = await self._get_latest_model_files()
|
|
@@ -456,20 +474,21 @@ class TrainerLogicGeneric(ABC):
|
|
|
456
474
|
|
|
457
475
|
already_uploaded_formats = self.active_training_io.load_model_upload_progress()
|
|
458
476
|
|
|
459
|
-
model_uuid = None
|
|
460
477
|
for file_format in [f for f in files if f not in already_uploaded_formats]:
|
|
461
478
|
_files = files[file_format] + [self._dump_categories_to_json()]
|
|
462
479
|
assert len([f for f in _files if 'model.json' in f]) == 1, "model.json must be included exactly once"
|
|
463
480
|
|
|
464
481
|
model_uuid = await self.node.data_exchanger.upload_model_get_uuid(context, _files, self.training.training_number, file_format)
|
|
482
|
+
if model_uuid is not None:
|
|
483
|
+
self.active_training_io.save_uploaded_model_uuid(model_uuid)
|
|
465
484
|
|
|
466
485
|
already_uploaded_formats.append(file_format)
|
|
467
486
|
self.active_training_io.save_model_upload_progress(already_uploaded_formats)
|
|
468
487
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
488
|
+
try:
|
|
489
|
+
return self.active_training_io.load_uploaded_model_uuid()
|
|
490
|
+
except FileNotFoundError as e:
|
|
491
|
+
raise CriticalError('The model is uploaded, but the loop never answered with its id') from e
|
|
473
492
|
|
|
474
493
|
def _dump_categories_to_json(self) -> str:
|
|
475
494
|
"""Dumps the categories to a json file and returns the path to the file.
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learning_loop_node
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.26.0
|
|
4
4
|
Summary: Python Library for Nodes which connect to the Zauberzeug Learning Loop
|
|
5
5
|
Author-email: Zauberzeug GmbH <info@zauberzeug.com>
|
|
6
6
|
License: MIT
|
|
@@ -114,9 +114,10 @@ Example usage:
|
|
|
114
114
|
To use the **SocketIO** inference EPs, the caller needs to connect to the detector node's SocketIO server and emit the `detect` or `batch_detect` event with the image data and image metadata. The `detect` endpoint receives a dictionary, with the following entries:
|
|
115
115
|
|
|
116
116
|
- `image`: The image data as dictionary with the following keys:
|
|
117
|
-
- `
|
|
118
|
-
- `
|
|
119
|
-
- `
|
|
117
|
+
- `format`: `raw` (default) or `jpeg`
|
|
118
|
+
- `bytes`: bytes of the ndarray (retrieved via `ndarray.tobytes(order='C')`), or the JPEG-encoded image for format `jpeg`
|
|
119
|
+
- `dtype`: data type of the ndarray as string (e.g. `uint8`, `float32`, etc.) (raw only)
|
|
120
|
+
- `shape`: shape of the ndarray as tuple of ints (e.g. `(480, 640, 3)`) (raw only)
|
|
120
121
|
- `camera_id`: optional camera identifier (string) used to improve the autoupload filtering
|
|
121
122
|
- `tags`: optional list of tags to add to the image in the learning loop
|
|
122
123
|
- `source`: optional source string
|
|
@@ -127,6 +128,8 @@ The `batch_detect` endpoint receives a dictionary, with the same entries as the
|
|
|
127
128
|
|
|
128
129
|
- `images`: List of image data dictionaries, each with the same structure as the `image` entry in the `detect` endpoint
|
|
129
130
|
|
|
131
|
+
Sending a camera's JPEG as it is (`format: 'jpeg'`) saves the bandwidth of a raw pixel array and spares the detector a re-encode for the upload.
|
|
132
|
+
|
|
130
133
|
Example code can be found [in the rosys implementation](https://github.com/zauberzeug/rosys/blob/main/rosys/vision/detector_hardware.py).
|
|
131
134
|
|
|
132
135
|
### Upload API
|
|
@@ -144,9 +147,10 @@ Example:
|
|
|
144
147
|
The detector also has a **SocketIO** upload endpoint that can be used to upload images and detections to the learning loop. The function receives a dictionary, with the following entries:
|
|
145
148
|
|
|
146
149
|
- `image`: the image data as dictionary with the following keys:
|
|
147
|
-
- `
|
|
148
|
-
- `
|
|
149
|
-
- `
|
|
150
|
+
- `format`: `raw` (default) or `jpeg`
|
|
151
|
+
- `bytes`: bytes of the ndarray (retrieved via `ndarray.tobytes(order='C')`), or the JPEG-encoded image for format `jpeg`
|
|
152
|
+
- `dtype`: data type of the ndarray as string (e.g. `uint8`, `float32`, etc.) (raw only)
|
|
153
|
+
- `shape`: shape of the ndarray as tuple of ints (e.g. `(480, 640, 3)`) (raw only)
|
|
150
154
|
- `metadata`: a dictionary representing the image metadata. If metadata contains detections and/or annotations, UUIDs for the classes are automatically determined based on the category names. Metadata should follow the schema of the `ImageMetadata` data class.
|
|
151
155
|
- `upload_priority`: Optional boolean flag to prioritize the upload (defaults to False)
|
|
152
156
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "learning_loop_node"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.26.0"
|
|
4
4
|
description = "Python Library for Nodes which connect to the Zauberzeug Learning Loop"
|
|
5
5
|
authors = [{ name = "Zauberzeug GmbH", email = "info@zauberzeug.com" }]
|
|
6
6
|
license = { text = "MIT" }
|
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/annotation/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/data_classes/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/data_classes/general.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/data_classes/training.py
RENAMED
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/__init__.py
RENAMED
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/categories.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/exceptions.py
RENAMED
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/geometry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/postprocess.py
RENAMED
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/rest/__init__.py
RENAMED
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/detector/rest/about.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/enums/__init__.py
RENAMED
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/enums/annotator.py
RENAMED
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/enums/detector.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/helpers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/helpers/entrypoint.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/helpers/log_conf.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/loop_communication.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/__init__.py
RENAMED
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/batch_size.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/downloader.py
RENAMED
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/executor.py
RENAMED
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/metrics.py
RENAMED
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/rest/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/subprocess.py
RENAMED
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/test_executor.py
RENAMED
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/trainer_logic.py
RENAMED
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/trainer_node.py
RENAMED
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node/trainer/training_slot.py
RENAMED
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node.egg-info/requires.txt
RENAMED
|
File without changes
|
{learning_loop_node-0.25.0 → learning_loop_node-0.26.0}/learning_loop_node.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|