learning-loop-node 0.10.17__tar.gz → 0.11.1__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.1}/PKG-INFO +1 -1
  2. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/data_classes/__init__.py +3 -2
  3. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/data_classes/detections.py +5 -12
  4. learning_loop_node-0.11.1/learning_loop_node/data_classes/image_metadata.py +37 -0
  5. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/detector_logic.py +3 -3
  6. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/detector_node.py +23 -20
  7. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/inbox_filter/cam_observation_history.py +3 -3
  8. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/inbox_filter/relevance_filter.py +7 -6
  9. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/outbox.py +24 -10
  10. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/rest/detect.py +5 -4
  11. learning_loop_node-0.11.1/learning_loop_node/detector/rest/upload.py +29 -0
  12. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/loop_communication.py +3 -3
  13. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/conftest.py +9 -9
  14. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/inbox_filter/test_relevance_group.py +7 -7
  15. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/inbox_filter/test_unexpected_observations_count.py +6 -6
  16. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/test_client_communication.py +46 -46
  17. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/test_detector_node.py +3 -1
  18. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/test_outbox.py +2 -2
  19. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/test_relevance_filter.py +2 -2
  20. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/testing_detector.py +3 -3
  21. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/general/test_downloader.py +4 -4
  22. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/test_helper.py +1 -2
  23. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/io_helpers.py +4 -4
  24. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/trainer_logic_generic.py +8 -4
  25. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/pyproject.toml +1 -1
  26. learning_loop_node-0.10.17/learning_loop_node/detector/rest/upload.py +0 -21
  27. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/README.md +0 -0
  28. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/__init__.py +0 -0
  29. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/annotation/__init__.py +0 -0
  30. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/annotation/annotator_logic.py +0 -0
  31. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/annotation/annotator_node.py +0 -0
  32. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/data_classes/annotations.py +0 -0
  33. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/data_classes/general.py +0 -0
  34. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/data_classes/socket_response.py +0 -0
  35. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/data_classes/training.py +0 -0
  36. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/data_exchanger.py +0 -0
  37. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/__init__.py +0 -0
  38. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/inbox_filter/__init__.py +0 -0
  39. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/rest/__init__.py +0 -0
  40. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/rest/about.py +0 -0
  41. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/rest/backdoor_controls.py +0 -0
  42. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/rest/model_version_control.py +0 -0
  43. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/rest/operation_mode.py +0 -0
  44. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/detector/rest/outbox_mode.py +0 -0
  45. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/examples/novelty_score_updater.py +0 -0
  46. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/globals.py +0 -0
  47. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/helpers/__init__.py +0 -0
  48. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/helpers/environment_reader.py +0 -0
  49. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/helpers/gdrive_downloader.py +0 -0
  50. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/helpers/log_conf.py +0 -0
  51. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/helpers/misc.py +0 -0
  52. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/node.py +0 -0
  53. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/py.typed +0 -0
  54. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/rest.py +0 -0
  55. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/__init__.py +0 -0
  56. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/annotator/__init__.py +0 -0
  57. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/annotator/conftest.py +0 -0
  58. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/annotator/pytest.ini +0 -0
  59. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/annotator/test_annotator_node.py +0 -0
  60. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/__init__.py +0 -0
  61. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/inbox_filter/__init__.py +0 -0
  62. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/inbox_filter/test_observation.py +0 -0
  63. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/pytest.ini +0 -0
  64. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/detector/test.jpg +0 -0
  65. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/general/__init__.py +0 -0
  66. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/general/conftest.py +0 -0
  67. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/general/pytest.ini +0 -0
  68. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/general/test_data/file_1.txt +0 -0
  69. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/general/test_data/file_2.txt +0 -0
  70. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/general/test_data/model.json +0 -0
  71. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/general/test_data_classes.py +0 -0
  72. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/general/test_learning_loop_node.py +0 -0
  73. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/__init__.py +0 -0
  74. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/conftest.py +0 -0
  75. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/pytest.ini +0 -0
  76. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/state_helper.py +0 -0
  77. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/states/__init__.py +0 -0
  78. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/states/test_state_cleanup.py +0 -0
  79. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/states/test_state_detecting.py +0 -0
  80. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/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.1}/learning_loop_node/tests/trainer/states/test_state_prepare.py +0 -0
  82. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/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.1}/learning_loop_node/tests/trainer/states/test_state_train.py +0 -0
  84. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/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.1}/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.1}/learning_loop_node/tests/trainer/test_errors.py +0 -0
  87. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/test_trainer_states.py +0 -0
  88. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/tests/trainer/testing_trainer_logic.py +0 -0
  89. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/__init__.py +0 -0
  90. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/downloader.py +0 -0
  91. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/exceptions.py +0 -0
  92. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/executor.py +0 -0
  93. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/rest/__init__.py +0 -0
  94. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/rest/backdoor_controls.py +0 -0
  95. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/test_executor.py +0 -0
  96. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/learning_loop_node/trainer/trainer_logic.py +0 -0
  97. {learning_loop_node-0.10.17 → learning_loop_node-0.11.1}/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.1
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
+ created: 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
@@ -140,7 +140,6 @@ class DetectorNode(Node):
140
140
 
141
141
  @self.sio.event
142
142
  async def detect(sid, data: Dict) -> Dict:
143
- self.log.debug('running detect via socketio')
144
143
  try:
145
144
  np_image = np.frombuffer(data['image'], np.uint8)
146
145
  det = await self.get_detections(
@@ -153,7 +152,6 @@ class DetectorNode(Node):
153
152
  if det is None:
154
153
  return {'error': 'no model loaded'}
155
154
  detection_dict = jsonable_encoder(asdict(det))
156
- self.log.debug('detect via socketio finished')
157
155
  return detection_dict
158
156
  except Exception as e:
159
157
  self.log.exception('could not detect via socketio')
@@ -174,22 +172,26 @@ class DetectorNode(Node):
174
172
  detection_data = data.get('detections', {})
175
173
  if detection_data and self.detector_logic.is_initialized:
176
174
  try:
177
- detections = from_dict(data_class=Detections, data=detection_data)
175
+ image_metadata = from_dict(data_class=ImageMetadata, data=detection_data)
178
176
  except Exception as e:
179
177
  self.log.exception('could not parse detections')
180
178
  return {'error': str(e)}
181
- detections = self.add_category_id_to_detections(self.detector_logic.model_info, detections)
179
+ image_metadata = self.add_category_id_to_detections(self.detector_logic.model_info, image_metadata)
182
180
  else:
183
- detections = Detections()
181
+ image_metadata = ImageMetadata()
184
182
 
185
183
  tags = data.get('tags', [])
186
184
  tags.append('picked_by_system')
187
185
 
188
186
  source = data.get('source', None)
187
+ creation_date = data.get('creation_date', None)
188
+
189
+ self.log.debug('running upload via socketio. tags: %s, source: %s, creation_date: %s',
190
+ tags, source, creation_date)
189
191
 
190
192
  loop = asyncio.get_event_loop()
191
193
  try:
192
- await loop.run_in_executor(None, self.outbox.save, data['image'], detections, tags, source)
194
+ await loop.run_in_executor(None, self.outbox.save, data['image'], image_metadata, tags, source, creation_date)
193
195
  except Exception as e:
194
196
  self.log.exception('could not upload via socketio')
195
197
  return {'error': str(e)}
@@ -343,7 +345,8 @@ class DetectorNode(Node):
343
345
  camera_id: Optional[str],
344
346
  tags: List[str],
345
347
  source: Optional[str] = None,
346
- autoupload: Optional[str] = None) -> Detections:
348
+ autoupload: Optional[str] = None,
349
+ creation_date: Optional[str] = None) -> ImageMetadata:
347
350
  """ Main processing function for the detector node when an image is received via REST or SocketIO.
348
351
  This function infers the detections from the image, cares about uploading to the loop and returns the detections as a dictionary.
349
352
  Note: raw_image is a numpy array of type uint8, but not in the correct shape!
@@ -351,7 +354,7 @@ class DetectorNode(Node):
351
354
 
352
355
  await self.detection_lock.acquire()
353
356
  loop = asyncio.get_event_loop()
354
- detections = await loop.run_in_executor(None, self.detector_logic.evaluate_with_all_info, raw_image, tags, source)
357
+ detections = await loop.run_in_executor(None, self.detector_logic.evaluate_with_all_info, raw_image, tags, source, creation_date)
355
358
  self.detection_lock.release()
356
359
 
357
360
  fix_shape_detections(detections)
@@ -361,42 +364,42 @@ class DetectorNode(Node):
361
364
 
362
365
  if autoupload is None or autoupload == 'filtered': # NOTE default is filtered
363
366
  Thread(target=self.relevance_filter.may_upload_detections,
364
- args=(detections, camera_id, raw_image, tags, source)).start()
367
+ args=(detections, camera_id, raw_image, tags, source, creation_date)).start()
365
368
  elif autoupload == 'all':
366
- Thread(target=self.outbox.save, args=(raw_image, detections, tags, source)).start()
369
+ Thread(target=self.outbox.save, args=(raw_image, detections, tags, source, creation_date)).start()
367
370
  elif autoupload == 'disabled':
368
371
  pass
369
372
  else:
370
373
  self.log.error('unknown autoupload value %s', autoupload)
371
374
  return detections
372
375
 
373
- async def upload_images(self, images: List[bytes]):
376
+ async def upload_images(self, images: List[bytes], source: Optional[str], creation_date: Optional[str]):
374
377
  loop = asyncio.get_event_loop()
375
378
  for image in images:
376
- await loop.run_in_executor(None, self.outbox.save, image, Detections(), ['picked_by_system'])
379
+ await loop.run_in_executor(None, self.outbox.save, image, ImageMetadata(), ['picked_by_system'], source, creation_date)
377
380
 
378
- def add_category_id_to_detections(self, model_info: ModelInformation, detections: Detections):
381
+ def add_category_id_to_detections(self, model_info: ModelInformation, image_metadata: ImageMetadata):
379
382
  def find_category_id_by_name(categories: List[Category], category_name: str):
380
383
  category_id = [category.id for category in categories if category.name == category_name]
381
384
  return category_id[0] if category_id else ''
382
385
 
383
- for box_detection in detections.box_detections:
386
+ for box_detection in image_metadata.box_detections:
384
387
  category_name = box_detection.category_name
385
388
  category_id = find_category_id_by_name(model_info.categories, category_name)
386
389
  box_detection.category_id = category_id
387
- for point_detection in detections.point_detections:
390
+ for point_detection in image_metadata.point_detections:
388
391
  category_name = point_detection.category_name
389
392
  category_id = find_category_id_by_name(model_info.categories, category_name)
390
393
  point_detection.category_id = category_id
391
- for segmentation_detection in detections.segmentation_detections:
394
+ for segmentation_detection in image_metadata.segmentation_detections:
392
395
  category_name = segmentation_detection.category_name
393
396
  category_id = find_category_id_by_name(model_info.categories, category_name)
394
397
  segmentation_detection.category_id = category_id
395
- for classification_detection in detections.classification_detections:
398
+ for classification_detection in image_metadata.classification_detections:
396
399
  category_name = classification_detection.category_name
397
400
  category_id = find_category_id_by_name(model_info.categories, category_name)
398
401
  classification_detection.category_id = category_id
399
- return detections
402
+ return image_metadata
400
403
 
401
404
  def register_sio_events(self, sio_client: AsyncClient):
402
405
  pass
@@ -412,7 +415,7 @@ def step_into(new_dir):
412
415
  os.chdir(previous_dir)
413
416
 
414
417
 
415
- def fix_shape_detections(detections: Detections):
418
+ def fix_shape_detections(detections: ImageMetadata):
416
419
  # TODO This is a quick fix.. check how loop upload detections deals with this
417
420
  for seg_detection in detections.segmentation_detections:
418
421
  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,17 @@ 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 self._is_valid_isoformat(creation_date):
80
+ image_metadata.created = creation_date
81
+ else:
82
+ image_metadata.created = identifier
83
+
84
+ image_metadata.source = source or 'unknown'
80
85
  os.makedirs(tmp, exist_ok=True)
81
86
 
82
87
  with open(tmp + '/image.json', 'w') as f:
83
- json.dump(jsonable_encoder(asdict(detections)), f)
88
+ json.dump(jsonable_encoder(asdict(image_metadata)), f)
84
89
 
85
90
  with open(tmp + '/image.jpg', 'wb') as f:
86
91
  f.write(image)
@@ -90,6 +95,15 @@ class Outbox():
90
95
  else:
91
96
  self.log.error('Could not rename %s to %s', tmp, self.path + '/' + identifier)
92
97
 
98
+ def _is_valid_isoformat(self, date: Optional[str]) -> bool:
99
+ if date is None:
100
+ return False
101
+ try:
102
+ datetime.fromisoformat(date)
103
+ return True
104
+ except Exception:
105
+ return False
106
+
93
107
  def get_data_files(self):
94
108
  return glob(f'{self.path}/*')
95
109
 
@@ -142,7 +156,7 @@ class Outbox():
142
156
  self.log.exception('Could not upload images')
143
157
  return
144
158
  finally:
145
- self.log.info('Closing files')
159
+ self.log.debug('Closing files')
146
160
  for _, file in data:
147
161
  file.close()
148
162
 
@@ -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"
@@ -35,7 +35,7 @@ class LoopCommunicator():
35
35
  else:
36
36
  self.async_client = httpx.AsyncClient(base_url=self.base_url, timeout=Timeout(60.0))
37
37
 
38
- logging.info(f'Loop interface initialized with base_url: {self.base_url} / user: {self.username}')
38
+ logging.info('Loop interface initialized with base_url: %s / user: %s', self.base_url, self.username)
39
39
 
40
40
  def websocket_url(self) -> str:
41
41
  return f'ws{"s" if "learning-loop.ai" in self.host else ""}://' + self.host
@@ -48,7 +48,7 @@ class LoopCommunicator():
48
48
  self.async_client.cookies.clear()
49
49
  response = await self.async_client.post('/api/login', data={'username': self.username, 'password': self.password})
50
50
  if response.status_code != 200:
51
- logging.info(f'Login failed with response: {response}')
51
+ logging.info('Login failed with response: %s', response)
52
52
  raise LoopCommunicationException('Login failed with response: ' + str(response))
53
53
  self.async_client.cookies.update(response.cookies)
54
54
 
@@ -57,7 +57,7 @@ class LoopCommunicator():
57
57
 
58
58
  response = await self.async_client.post('/api/logout')
59
59
  if response.status_code != 200:
60
- logging.info(f'Logout failed with response: {response}')
60
+ logging.info('Logout failed with response: %s', response)
61
61
  raise LoopCommunicationException('Logout failed with response: ' + str(response))
62
62
  self.async_client.cookies.clear()
63
63
 
@@ -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()