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