learning-loop-node 0.13.6__tar.gz → 0.14.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.

Potentially problematic release.


This version of learning-loop-node might be problematic. Click here for more details.

Files changed (104) hide show
  1. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/PKG-INFO +37 -40
  2. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/README.md +36 -39
  3. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/data_exchanger.py +1 -1
  4. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/detector/detector_logic.py +2 -4
  5. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/detector/detector_node.py +69 -43
  6. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/detector/inbox_filter/relevance_filter.py +11 -9
  7. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/detector/outbox.py +134 -44
  8. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/detector/rest/detect.py +3 -3
  9. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/detector/rest/upload.py +4 -3
  10. learning_loop_node-0.14.0/learning_loop_node/helpers/background_tasks.py +78 -0
  11. learning_loop_node-0.14.0/learning_loop_node/helpers/run.py +21 -0
  12. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/loop_communication.py +18 -18
  13. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/node.py +11 -4
  14. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/annotator/conftest.py +9 -4
  15. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/annotator/test_annotator_node.py +10 -2
  16. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/detector/inbox_filter/test_unexpected_observations_count.py +4 -3
  17. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/detector/test_client_communication.py +1 -23
  18. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/detector/test_outbox.py +7 -16
  19. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/general/conftest.py +8 -2
  20. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/pyproject.toml +1 -1
  21. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/__init__.py +0 -0
  22. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/annotation/__init__.py +0 -0
  23. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/annotation/annotator_logic.py +0 -0
  24. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/annotation/annotator_node.py +0 -0
  25. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/data_classes/__init__.py +0 -0
  26. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/data_classes/annotations.py +0 -0
  27. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/data_classes/detections.py +0 -0
  28. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/data_classes/general.py +0 -0
  29. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/data_classes/image_metadata.py +0 -0
  30. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/data_classes/socket_response.py +0 -0
  31. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/data_classes/training.py +0 -0
  32. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/detector/__init__.py +0 -0
  33. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/detector/exceptions.py +0 -0
  34. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/detector/inbox_filter/__init__.py +0 -0
  35. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/detector/inbox_filter/cam_observation_history.py +0 -0
  36. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/detector/rest/__init__.py +0 -0
  37. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/detector/rest/about.py +0 -0
  38. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/detector/rest/backdoor_controls.py +0 -0
  39. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/detector/rest/model_version_control.py +0 -0
  40. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/detector/rest/operation_mode.py +0 -0
  41. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/detector/rest/outbox_mode.py +0 -0
  42. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/enums/__init__.py +0 -0
  43. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/enums/annotator.py +0 -0
  44. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/enums/detector.py +0 -0
  45. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/enums/general.py +0 -0
  46. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/enums/trainer.py +0 -0
  47. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/examples/novelty_score_updater.py +0 -0
  48. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/globals.py +0 -0
  49. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/helpers/__init__.py +0 -0
  50. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/helpers/environment_reader.py +0 -0
  51. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/helpers/gdrive_downloader.py +0 -0
  52. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/helpers/log_conf.py +0 -0
  53. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/helpers/misc.py +0 -0
  54. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/py.typed +0 -0
  55. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/rest.py +0 -0
  56. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/__init__.py +0 -0
  57. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/annotator/__init__.py +0 -0
  58. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/annotator/pytest.ini +0 -0
  59. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/detector/__init__.py +0 -0
  60. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/detector/conftest.py +0 -0
  61. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/detector/inbox_filter/__init__.py +0 -0
  62. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/detector/inbox_filter/test_observation.py +0 -0
  63. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/detector/inbox_filter/test_relevance_group.py +0 -0
  64. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/detector/pytest.ini +0 -0
  65. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/detector/test.jpg +0 -0
  66. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/detector/test_detector_node.py +0 -0
  67. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/detector/test_relevance_filter.py +0 -0
  68. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/detector/testing_detector.py +0 -0
  69. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/general/__init__.py +0 -0
  70. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/general/pytest.ini +0 -0
  71. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/general/test_data/file_1.txt +0 -0
  72. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/general/test_data/file_2.txt +0 -0
  73. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/general/test_data/model.json +0 -0
  74. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/general/test_data_classes.py +0 -0
  75. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/general/test_downloader.py +0 -0
  76. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/general/test_learning_loop_node.py +0 -0
  77. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/test_helper.py +0 -0
  78. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/trainer/__init__.py +0 -0
  79. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/trainer/conftest.py +0 -0
  80. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/trainer/pytest.ini +0 -0
  81. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/trainer/state_helper.py +0 -0
  82. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/trainer/states/__init__.py +0 -0
  83. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/trainer/states/test_state_cleanup.py +0 -0
  84. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/trainer/states/test_state_detecting.py +0 -0
  85. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/trainer/states/test_state_download_train_model.py +0 -0
  86. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/trainer/states/test_state_prepare.py +0 -0
  87. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/trainer/states/test_state_sync_confusion_matrix.py +0 -0
  88. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/trainer/states/test_state_train.py +0 -0
  89. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/trainer/states/test_state_upload_detections.py +0 -0
  90. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/trainer/states/test_state_upload_model.py +0 -0
  91. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/trainer/test_errors.py +0 -0
  92. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/trainer/test_trainer_states.py +0 -0
  93. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/tests/trainer/testing_trainer_logic.py +0 -0
  94. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/trainer/__init__.py +0 -0
  95. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/trainer/downloader.py +0 -0
  96. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/trainer/exceptions.py +0 -0
  97. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/trainer/executor.py +0 -0
  98. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/trainer/io_helpers.py +0 -0
  99. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/trainer/rest/__init__.py +0 -0
  100. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/trainer/rest/backdoor_controls.py +0 -0
  101. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/trainer/test_executor.py +0 -0
  102. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/trainer/trainer_logic.py +0 -0
  103. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/trainer/trainer_logic_generic.py +0 -0
  104. {learning_loop_node-0.13.6 → learning_loop_node-0.14.0}/learning_loop_node/trainer/trainer_node.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: learning-loop-node
3
- Version: 0.13.6
3
+ Version: 0.14.0
4
4
  Summary: Python Library for Nodes which connect to the Zauberzeug Learning Loop
5
5
  Home-page: https://github.com/zauberzeug/learning_loop_node
6
6
  License: MIT
@@ -63,8 +63,8 @@ You can configure connection to our Learning Loop by specifying the following en
63
63
  | LOOP_USERNAME | USERNAME | Learning Loop user name | all besides Detector |
64
64
  | LOOP_PASSWORD | PASSWORD | Learning Loop password | all besides Detector |
65
65
  | LOOP_SSL_CERT_PATH | - | Path to the SSL certificate | all (opt.) |
66
- | LOOP_ORGANIZATION | ORGANIZATION | Organization name | Detector |
67
- | LOOP_PROJECT | PROJECT | Project name | Detector (opt.) |
66
+ | LOOP_ORGANIZATION | ORGANIZATION | Organization ID | Detector |
67
+ | LOOP_PROJECT | PROJECT | Project ID | Detector (opt.) |
68
68
  | MIN_UNCERTAIN_THRESHOLD | - | smallest confidence (float) at which auto-upload will happen | Detector (opt.) |
69
69
  | MAX_UNCERTAIN_THRESHOLD | - | largest confidence (float) at which auto-upload will happen | Detector (opt.) |
70
70
  | INFERENCE_BATCH_SIZE | - | Batch size of trainer when calculating detections | Trainer (opt.) |
@@ -73,6 +73,8 @@ You can configure connection to our Learning Loop by specifying the following en
73
73
  | TRAINER_IDLE_TIMEOUT_SEC | - | Automatically shutdown trainer after timeout (in seconds) | Trainer (opt.) |
74
74
  | USE_BACKDOOR_CONTROLS | - | Always enable backdoor controls (set to 1) | Trainer / Detector (opt.) |
75
75
 
76
+ Note that organization and project IDs are always lower case and may differ from the names in the Learning Loop which can have uppercase letters.
77
+
76
78
  #### Testing
77
79
 
78
80
  We use github actions for CI. Tests can also be executed locally by running
@@ -103,6 +105,9 @@ The detector also has a sio **upload endpoint** that can be used to upload image
103
105
  - `image`: the image data in jpg format
104
106
  - `tags`: a list of strings. If not provided the tag is `picked_by_system`
105
107
  - `detections`: a dictionary representing the detections. UUIDs for the classes are automatically determined based on the category names. This field is optional. If not provided, no detections are uploaded.
108
+ - `source`: optional source identifier for the image
109
+ - `creation_date`: optional creation date for the image
110
+ - `upload_priority`: boolean flag to prioritize the upload (defaults to False)
106
111
 
107
112
  The endpoint returns None if the upload was successful and an error message otherwise.
108
113
 
@@ -185,58 +190,52 @@ Upload a model with
185
190
  The model should now be available for the format 'format_a'
186
191
  `curl "https://learning-loop.ai/api/zauberzeug/projects/demo/models?format=format_a"`
187
192
 
188
- ````
189
-
193
+ ```json
190
194
  {
191
- "models": [
192
- {
193
- "id": "3c20d807-f71c-40dc-a996-8a8968aa5431",
194
- "version": "4.0",
195
- "formats": [
196
- "format_a"
197
- ],
198
- "created": "2021-06-01T06:28:21.289092",
199
- "comment": "uploaded at 2021-06-01 06:28:21.288442",
200
- ...
195
+ "models": [
196
+ {
197
+ "id": "3c20d807-f71c-40dc-a996-8a8968aa5431",
198
+ "version": "4.0",
199
+ "formats": [
200
+ "format_a"
201
+ ],
202
+ "created": "2021-06-01T06:28:21.289092",
203
+ "comment": "uploaded at 2021-06-01 06:28:21.288442",
204
+ ...
205
+ }
206
+ ]
201
207
  }
202
- ]
203
- }
204
-
205
208
  ```
206
209
 
207
210
  but not in the format_b
208
211
  `curl "https://learning-loop.ai/api/zauberzeug/projects/demo/models?format=format_b"`
209
212
 
210
- ```
211
-
213
+ ```json
212
214
  {
213
- "models": []
215
+ "models": []
214
216
  }
215
-
216
217
  ```
217
218
 
218
219
  Connect the Node to the Learning Loop by simply starting the container.
219
220
  After a short time the converted model should be available as well.
220
221
  `curl https://learning-loop.ai/api/zauberzeug/projects/demo/models?format=format_b`
221
222
 
222
- ```
223
-
224
- {
225
- "models": [
223
+ ```json
226
224
  {
227
- "id": "3c20d807-f71c-40dc-a996-8a8968aa5431",
228
- "version": "4.0",
229
- "formats": [
230
- "format_a",
231
- "format_b",
232
- ],
233
- "created": "2021-06-01T06:28:21.289092",
234
- "comment": "uploaded at 2021-06-01 06:28:21.288442",
235
- ...
236
- }
237
- ]
225
+ "models": [
226
+ {
227
+ "id": "3c20d807-f71c-40dc-a996-8a8968aa5431",
228
+ "version": "4.0",
229
+ "formats": [
230
+ "format_a",
231
+ "format_b",
232
+ ],
233
+ "created": "2021-06-01T06:28:21.289092",
234
+ "comment": "uploaded at 2021-06-01 06:28:21.288442",
235
+ ...
236
+ }
237
+ ]
238
238
  }
239
-
240
239
  ```
241
240
 
242
241
  ## About Models (the currency between Nodes)
@@ -255,6 +254,4 @@ After a short time the converted model should be available as well.
255
254
  - Nodes add properties to `model.json`, which contains all the information which are needed by subsequent nodes. These are typically the properties:
256
255
  - `resolution`: resolution in which the model expects images (as `int`, since the resolution is mostly square - later, ` resolution_x`` resolution_y ` would also be conceivable or `resolutions` to give a list of possible resolutions)
257
256
  - `categories`: list of categories with name, id, (later also type), in the order in which they are used by the model -- this is neccessary to be robust about renamings
258
- ```
259
- ````
260
257
 
@@ -23,8 +23,8 @@ You can configure connection to our Learning Loop by specifying the following en
23
23
  | LOOP_USERNAME | USERNAME | Learning Loop user name | all besides Detector |
24
24
  | LOOP_PASSWORD | PASSWORD | Learning Loop password | all besides Detector |
25
25
  | LOOP_SSL_CERT_PATH | - | Path to the SSL certificate | all (opt.) |
26
- | LOOP_ORGANIZATION | ORGANIZATION | Organization name | Detector |
27
- | LOOP_PROJECT | PROJECT | Project name | Detector (opt.) |
26
+ | LOOP_ORGANIZATION | ORGANIZATION | Organization ID | Detector |
27
+ | LOOP_PROJECT | PROJECT | Project ID | Detector (opt.) |
28
28
  | MIN_UNCERTAIN_THRESHOLD | - | smallest confidence (float) at which auto-upload will happen | Detector (opt.) |
29
29
  | MAX_UNCERTAIN_THRESHOLD | - | largest confidence (float) at which auto-upload will happen | Detector (opt.) |
30
30
  | INFERENCE_BATCH_SIZE | - | Batch size of trainer when calculating detections | Trainer (opt.) |
@@ -33,6 +33,8 @@ You can configure connection to our Learning Loop by specifying the following en
33
33
  | TRAINER_IDLE_TIMEOUT_SEC | - | Automatically shutdown trainer after timeout (in seconds) | Trainer (opt.) |
34
34
  | USE_BACKDOOR_CONTROLS | - | Always enable backdoor controls (set to 1) | Trainer / Detector (opt.) |
35
35
 
36
+ Note that organization and project IDs are always lower case and may differ from the names in the Learning Loop which can have uppercase letters.
37
+
36
38
  #### Testing
37
39
 
38
40
  We use github actions for CI. Tests can also be executed locally by running
@@ -63,6 +65,9 @@ The detector also has a sio **upload endpoint** that can be used to upload image
63
65
  - `image`: the image data in jpg format
64
66
  - `tags`: a list of strings. If not provided the tag is `picked_by_system`
65
67
  - `detections`: a dictionary representing the detections. UUIDs for the classes are automatically determined based on the category names. This field is optional. If not provided, no detections are uploaded.
68
+ - `source`: optional source identifier for the image
69
+ - `creation_date`: optional creation date for the image
70
+ - `upload_priority`: boolean flag to prioritize the upload (defaults to False)
66
71
 
67
72
  The endpoint returns None if the upload was successful and an error message otherwise.
68
73
 
@@ -145,58 +150,52 @@ Upload a model with
145
150
  The model should now be available for the format 'format_a'
146
151
  `curl "https://learning-loop.ai/api/zauberzeug/projects/demo/models?format=format_a"`
147
152
 
148
- ````
149
-
153
+ ```json
150
154
  {
151
- "models": [
152
- {
153
- "id": "3c20d807-f71c-40dc-a996-8a8968aa5431",
154
- "version": "4.0",
155
- "formats": [
156
- "format_a"
157
- ],
158
- "created": "2021-06-01T06:28:21.289092",
159
- "comment": "uploaded at 2021-06-01 06:28:21.288442",
160
- ...
155
+ "models": [
156
+ {
157
+ "id": "3c20d807-f71c-40dc-a996-8a8968aa5431",
158
+ "version": "4.0",
159
+ "formats": [
160
+ "format_a"
161
+ ],
162
+ "created": "2021-06-01T06:28:21.289092",
163
+ "comment": "uploaded at 2021-06-01 06:28:21.288442",
164
+ ...
165
+ }
166
+ ]
161
167
  }
162
- ]
163
- }
164
-
165
168
  ```
166
169
 
167
170
  but not in the format_b
168
171
  `curl "https://learning-loop.ai/api/zauberzeug/projects/demo/models?format=format_b"`
169
172
 
170
- ```
171
-
173
+ ```json
172
174
  {
173
- "models": []
175
+ "models": []
174
176
  }
175
-
176
177
  ```
177
178
 
178
179
  Connect the Node to the Learning Loop by simply starting the container.
179
180
  After a short time the converted model should be available as well.
180
181
  `curl https://learning-loop.ai/api/zauberzeug/projects/demo/models?format=format_b`
181
182
 
182
- ```
183
-
184
- {
185
- "models": [
183
+ ```json
186
184
  {
187
- "id": "3c20d807-f71c-40dc-a996-8a8968aa5431",
188
- "version": "4.0",
189
- "formats": [
190
- "format_a",
191
- "format_b",
192
- ],
193
- "created": "2021-06-01T06:28:21.289092",
194
- "comment": "uploaded at 2021-06-01 06:28:21.288442",
195
- ...
196
- }
197
- ]
185
+ "models": [
186
+ {
187
+ "id": "3c20d807-f71c-40dc-a996-8a8968aa5431",
188
+ "version": "4.0",
189
+ "formats": [
190
+ "format_a",
191
+ "format_b",
192
+ ],
193
+ "created": "2021-06-01T06:28:21.289092",
194
+ "comment": "uploaded at 2021-06-01 06:28:21.288442",
195
+ ...
196
+ }
197
+ ]
198
198
  }
199
-
200
199
  ```
201
200
 
202
201
  ## About Models (the currency between Nodes)
@@ -215,5 +214,3 @@ After a short time the converted model should be available as well.
215
214
  - Nodes add properties to `model.json`, which contains all the information which are needed by subsequent nodes. These are typically the properties:
216
215
  - `resolution`: resolution in which the model expects images (as `int`, since the resolution is mostly square - later, ` resolution_x`` resolution_y ` would also be conceivable or `resolutions` to give a list of possible resolutions)
217
216
  - `categories`: list of categories with name, id, (later also type), in the order in which they are used by the model -- this is neccessary to be robust about renamings
218
- ```
219
- ````
@@ -143,7 +143,7 @@ class DataExchanger():
143
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
- response = await self.loop_communicator.get(path, requires_login=False)
146
+ response = await self.loop_communicator.get(path, requires_login=False, timeout=60*10)
147
147
  if response.status_code != 200:
148
148
  decoded_content = response.content.decode('utf-8')
149
149
  logging.error('could not download loop/%s: %s, content: %s', path,
@@ -2,8 +2,6 @@ import logging
2
2
  from abc import abstractmethod
3
3
  from typing import List, Optional
4
4
 
5
- import numpy as np
6
-
7
5
  from ..data_classes import ImageMetadata, ModelInformation
8
6
  from ..globals import GLOBALS
9
7
  from .exceptions import NodeNeedsRestartError
@@ -44,13 +42,13 @@ class DetectorLogic():
44
42
  def init(self):
45
43
  """Called when a (new) model was loaded. Initialize the model. Model information available via `self.model_info`"""
46
44
 
47
- def evaluate_with_all_info(self, image: np.ndarray, tags: List[str], source: Optional[str] = None, creation_date: Optional[str] = None) -> ImageMetadata: # pylint: disable=unused-argument
45
+ def evaluate_with_all_info(self, image: bytes, tags: List[str], source: Optional[str] = None, creation_date: Optional[str] = None) -> ImageMetadata: # pylint: disable=unused-argument
48
46
  """Called by the detector node when an image should be evaluated (REST or SocketIO).
49
47
  Tags, source come from the caller and may be used in this function.
50
48
  By default, this function simply calls `evaluate`"""
51
49
  return self.evaluate(image)
52
50
 
53
51
  @abstractmethod
54
- def evaluate(self, image: np.ndarray) -> ImageMetadata:
52
+ def evaluate(self, image: bytes) -> ImageMetadata:
55
53
  """Evaluate the image and return the detections.
56
54
  The object should return empty detections if it is not initialized"""
@@ -1,14 +1,12 @@
1
1
  import asyncio
2
2
  import contextlib
3
- import math
4
3
  import os
5
4
  import shutil
6
5
  import subprocess
7
6
  import sys
8
7
  from dataclasses import asdict
9
8
  from datetime import datetime
10
- from threading import Thread
11
- from typing import Dict, List, Optional
9
+ from typing import Dict, List, Optional, cast
12
10
 
13
11
  import numpy as np
14
12
  import socketio
@@ -30,7 +28,7 @@ from ..data_classes.socket_response import SocketResponse
30
28
  from ..data_exchanger import DataExchanger, DownloadError
31
29
  from ..enums import OperationMode, VersionMode
32
30
  from ..globals import GLOBALS
33
- from ..helpers import environment_reader
31
+ from ..helpers import background_tasks, environment_reader, run
34
32
  from ..node import Node
35
33
  from .detector_logic import DetectorLogic
36
34
  from .exceptions import NodeNeedsRestartError
@@ -227,7 +225,7 @@ class DetectorNode(Node):
227
225
  async def detect(sid, data: Dict) -> Dict:
228
226
  try:
229
227
  det = await self.get_detections(
230
- raw_image=np.frombuffer(data['image'], np.uint8),
228
+ raw_image=data['image'],
231
229
  camera_id=data.get('camera-id', None) or data.get('mac', None),
232
230
  tags=data.get('tags', []),
233
231
  source=data.get('source', None),
@@ -279,9 +277,10 @@ class DetectorNode(Node):
279
277
  return {'error': str(e)}
280
278
 
281
279
  @self.sio.event
282
- async def upload(sid, data: Dict) -> Optional[Dict]:
283
- '''upload an image with detections'''
280
+ async def upload(sid, data: Dict) -> Dict:
281
+ """Upload an image with detections"""
284
282
 
283
+ self.log.debug('Processing upload via socketio.')
285
284
  detection_data = data.get('detections', {})
286
285
  if detection_data and self.detector_logic.model_info is not None:
287
286
  try:
@@ -293,22 +292,19 @@ class DetectorNode(Node):
293
292
  else:
294
293
  image_metadata = ImageMetadata()
295
294
 
296
- tags = data.get('tags', [])
297
- tags.append('picked_by_system')
298
-
299
- source = data.get('source', None)
300
- creation_date = data.get('creation_date', None)
301
-
302
- self.log.debug('running upload via socketio. tags: %s, source: %s, creation_date: %s',
303
- tags, source, creation_date)
304
-
305
- loop = asyncio.get_event_loop()
306
295
  try:
307
- await loop.run_in_executor(None, self.outbox.save, data['image'], image_metadata, tags, source, creation_date)
296
+ await self.upload_images(
297
+ images=[data['image']],
298
+ image_metadata=image_metadata,
299
+ tags=data.get('tags', []),
300
+ source=data.get('source', None),
301
+ creation_date=data.get('creation_date', None),
302
+ upload_priority=data.get('upload_priority', False)
303
+ )
308
304
  except Exception as e:
309
305
  self.log.exception('could not upload via socketio')
310
306
  return {'error': str(e)}
311
- return None
307
+ return {'status': 'OK'}
312
308
 
313
309
  @self.sio.event
314
310
  def connect(sid, environ, auth) -> None:
@@ -344,16 +340,21 @@ class DetectorNode(Node):
344
340
  with step_into(GLOBALS.data_folder):
345
341
  model_symlink = 'model'
346
342
  target_model_folder = f'models/{self.target_model.version}'
347
- if not os.path.exists(target_model_folder):
348
- os.makedirs(target_model_folder)
349
- await self.data_exchanger.download_model(target_model_folder,
350
- Context(organization=self.organization,
351
- project=self.project),
352
- self.target_model.id,
353
- self.detector_logic.model_format)
354
- self.log.info('Downloaded model %s', self.target_model.version)
355
- else:
343
+ if os.path.exists(target_model_folder) and len(os.listdir(target_model_folder)) > 0:
356
344
  self.log.info('No need to download model %s (already exists)', self.target_model.version)
345
+ else:
346
+ os.makedirs(target_model_folder, exist_ok=True)
347
+ try:
348
+ await self.data_exchanger.download_model(target_model_folder,
349
+ Context(organization=self.organization,
350
+ project=self.project),
351
+ self.target_model.id,
352
+ self.detector_logic.model_format)
353
+ self.log.info('Downloaded model %s', self.target_model.version)
354
+ except Exception:
355
+ self.log.exception('Could not download model %s', self.target_model.version)
356
+ shutil.rmtree(target_model_folder, ignore_errors=True)
357
+ return
357
358
  try:
358
359
  os.unlink(model_symlink)
359
360
  os.remove(model_symlink)
@@ -422,16 +423,23 @@ class DetectorNode(Node):
422
423
  self.log_status_on_change(status.state or 'None', status)
423
424
 
424
425
  # NOTE: sending organization and project is no longer required!
425
- response = await self.sio_client.call('update_detector', (self.organization, self.project, jsonable_encoder(asdict(status))))
426
+ try:
427
+ response = await self.sio_client.call('update_detector', (self.organization, self.project, jsonable_encoder(asdict(status))))
428
+ except TimeoutError:
429
+ self.socket_connection_broken = True
430
+ self.log.exception('TimeoutError for sending status update (will try to reconnect):')
431
+ raise Exception('Status update failed due to timeout') from None
432
+
426
433
  if not response:
427
434
  self.socket_connection_broken = True
428
- return
435
+ self.log.error('Status update failed (will try to reconnect): %s', response)
436
+ raise Exception('Status update failed: Did not receive a response from the learning loop')
429
437
 
430
438
  socket_response = from_dict(data_class=SocketResponse, data=response)
431
439
  if not socket_response.success:
432
440
  self.socket_connection_broken = True
433
- self.log.error('Statusupdate failed: %s', response)
434
- raise Exception(f'Statusupdate failed: {response}')
441
+ self.log.error('Status update failed (will try to reconnect): %s', response)
442
+ raise Exception(f'Status update failed. Response from learning loop: {response}')
435
443
 
436
444
  assert socket_response.payload is not None
437
445
 
@@ -457,7 +465,7 @@ class DetectorNode(Node):
457
465
  self.log.warning('Operation mode set to %s, but sync failed: %s', mode, e)
458
466
 
459
467
  def reload(self, reason: str):
460
- '''provide a cause for the reload'''
468
+ """provide a cause for the reload"""
461
469
 
462
470
  self.log.info('########## reloading app because %s', reason)
463
471
  if os.path.isfile('/app/app_code/restart/restart.py'):
@@ -470,7 +478,7 @@ class DetectorNode(Node):
470
478
  self.log.error('could not reload app')
471
479
 
472
480
  async def get_detections(self,
473
- raw_image: np.ndarray,
481
+ raw_image: bytes,
474
482
  camera_id: Optional[str],
475
483
  tags: List[str],
476
484
  source: Optional[str] = None,
@@ -482,8 +490,7 @@ class DetectorNode(Node):
482
490
  It can be converted e.g. using cv2.imdecode(raw_image, cv2.IMREAD_COLOR)"""
483
491
 
484
492
  await self.detection_lock.acquire()
485
- loop = asyncio.get_event_loop()
486
- detections = await loop.run_in_executor(None, self.detector_logic.evaluate_with_all_info, raw_image, tags, source, creation_date)
493
+ detections = await run.io_bound(self.detector_logic.evaluate_with_all_info, raw_image, tags, source, creation_date)
487
494
  self.detection_lock.release()
488
495
 
489
496
  fix_shape_detections(detections)
@@ -491,21 +498,40 @@ class DetectorNode(Node):
491
498
  n_po, n_se = len(detections.point_detections), len(detections.segmentation_detections)
492
499
  self.log.debug('Detected: %d boxes, %d points, %d segs, %d classes', n_bo, n_po, n_se, n_cl)
493
500
 
494
- if autoupload is None or autoupload == 'filtered': # NOTE default is filtered
495
- Thread(target=self.relevance_filter.may_upload_detections,
496
- args=(detections, camera_id, raw_image, tags, source, creation_date)).start()
501
+ autoupload = autoupload or 'filtered'
502
+ if autoupload == 'filtered' and camera_id is not None:
503
+ background_tasks.create(self.relevance_filter.may_upload_detections(
504
+ detections, camera_id, raw_image, tags, source, creation_date
505
+ ))
497
506
  elif autoupload == 'all':
498
- Thread(target=self.outbox.save, args=(raw_image, detections, tags, source, creation_date)).start()
507
+ background_tasks.create(self.outbox.save(raw_image, detections, tags, source, creation_date))
499
508
  elif autoupload == 'disabled':
500
509
  pass
501
510
  else:
502
511
  self.log.error('unknown autoupload value %s', autoupload)
503
512
  return detections
504
513
 
505
- async def upload_images(self, images: List[bytes], source: Optional[str], creation_date: Optional[str]):
506
- loop = asyncio.get_event_loop()
514
+ async def upload_images(
515
+ self, *,
516
+ images: List[bytes],
517
+ image_metadata: Optional[ImageMetadata] = None,
518
+ tags: Optional[List[str]] = None,
519
+ source: Optional[str],
520
+ creation_date: Optional[str],
521
+ upload_priority: bool = False
522
+ ) -> None:
523
+ """Save images to the outbox using an asyncio executor.
524
+ Used by SIO and REST upload endpoints."""
525
+
526
+ if image_metadata is None:
527
+ image_metadata = ImageMetadata()
528
+ if tags is None:
529
+ tags = []
530
+
531
+ tags.append('picked_by_system')
532
+
507
533
  for image in images:
508
- await loop.run_in_executor(None, self.outbox.save, image, ImageMetadata(), ['picked_by_system'], source, creation_date)
534
+ await self.outbox.save(image, image_metadata, tags, source, creation_date, upload_priority)
509
535
 
510
536
  def add_category_id_to_detections(self, model_info: ModelInformation, image_metadata: ImageMetadata):
511
537
  def find_category_id_by_name(categories: List[Category], category_name: str):
@@ -11,14 +11,16 @@ class RelevanceFilter():
11
11
  self.cam_histories: Dict[str, CamObservationHistory] = {}
12
12
  self.outbox: Outbox = outbox
13
13
 
14
- def may_upload_detections(self,
15
- image_metadata: ImageMetadata,
16
- cam_id: str,
17
- raw_image: bytes,
18
- tags: List[str],
19
- source: Optional[str] = None,
20
- creation_date: Optional[str] = None
21
- ) -> List[str]:
14
+ async def may_upload_detections(self,
15
+ image_metadata: ImageMetadata,
16
+ cam_id: str,
17
+ raw_image: bytes,
18
+ tags: List[str],
19
+ source: Optional[str] = None,
20
+ creation_date: Optional[str] = None) -> List[str]:
21
+ """Check if the detection should be uploaded to the outbox.
22
+ If so, upload it and return the list of causes for the upload.
23
+ """
22
24
  for group in self.cam_histories.values():
23
25
  group.forget_old_detections()
24
26
 
@@ -30,5 +32,5 @@ class RelevanceFilter():
30
32
  if len(causes) > 0:
31
33
  tags = tags if tags is not None else []
32
34
  tags.extend(causes)
33
- self.outbox.save(raw_image, image_metadata, tags, source, creation_date)
35
+ await self.outbox.save(raw_image, image_metadata, tags, source, creation_date)
34
36
  return causes