learning-loop-node 0.10.12__tar.gz → 0.10.14__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 (98) hide show
  1. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/PKG-INFO +16 -15
  2. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/README.md +15 -14
  3. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/annotation/annotator_node.py +11 -10
  4. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/data_classes/detections.py +34 -25
  5. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/data_classes/general.py +27 -17
  6. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/data_exchanger.py +6 -5
  7. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/detector/detector_logic.py +10 -4
  8. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/detector/detector_node.py +80 -54
  9. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/detector/inbox_filter/relevance_filter.py +9 -3
  10. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/detector/outbox.py +8 -1
  11. learning_loop_node-0.10.14/learning_loop_node/detector/rest/about.py +50 -0
  12. learning_loop_node-0.10.14/learning_loop_node/detector/rest/backdoor_controls.py +37 -0
  13. learning_loop_node-0.10.14/learning_loop_node/detector/rest/detect.py +53 -0
  14. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/detector/rest/model_version_control.py +30 -13
  15. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/detector/rest/operation_mode.py +11 -5
  16. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/detector/rest/outbox_mode.py +7 -1
  17. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/helpers/log_conf.py +5 -0
  18. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/node.py +97 -49
  19. learning_loop_node-0.10.14/learning_loop_node/rest.py +55 -0
  20. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/detector/conftest.py +36 -2
  21. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/detector/test_client_communication.py +21 -19
  22. learning_loop_node-0.10.14/learning_loop_node/tests/detector/test_detector_node.py +86 -0
  23. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/trainer/conftest.py +4 -4
  24. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/trainer/states/test_state_detecting.py +8 -9
  25. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/trainer/states/test_state_download_train_model.py +8 -8
  26. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/trainer/states/test_state_prepare.py +6 -7
  27. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/trainer/states/test_state_sync_confusion_matrix.py +21 -18
  28. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/trainer/states/test_state_train.py +6 -8
  29. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/trainer/states/test_state_upload_detections.py +7 -9
  30. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/trainer/states/test_state_upload_model.py +7 -8
  31. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/trainer/test_errors.py +2 -2
  32. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/trainer/io_helpers.py +3 -6
  33. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/trainer/rest/backdoor_controls.py +19 -40
  34. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/trainer/trainer_logic.py +4 -4
  35. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/trainer/trainer_logic_generic.py +15 -12
  36. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/trainer/trainer_node.py +5 -4
  37. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/pyproject.toml +1 -1
  38. learning_loop_node-0.10.12/learning_loop_node/detector/rest/about.py +0 -25
  39. learning_loop_node-0.10.12/learning_loop_node/detector/rest/backdoor_controls.py +0 -56
  40. learning_loop_node-0.10.12/learning_loop_node/detector/rest/detect.py +0 -45
  41. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/__init__.py +0 -0
  42. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/annotation/__init__.py +0 -0
  43. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/annotation/annotator_logic.py +0 -0
  44. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/data_classes/__init__.py +0 -0
  45. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/data_classes/annotations.py +0 -0
  46. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/data_classes/socket_response.py +0 -0
  47. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/data_classes/training.py +0 -0
  48. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/detector/__init__.py +0 -0
  49. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/detector/inbox_filter/__init__.py +0 -0
  50. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/detector/inbox_filter/cam_observation_history.py +0 -0
  51. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/detector/rest/__init__.py +0 -0
  52. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/detector/rest/upload.py +0 -0
  53. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/examples/novelty_score_updater.py +0 -0
  54. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/globals.py +0 -0
  55. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/helpers/__init__.py +0 -0
  56. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/helpers/environment_reader.py +0 -0
  57. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/helpers/gdrive_downloader.py +0 -0
  58. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/helpers/misc.py +0 -0
  59. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/loop_communication.py +0 -0
  60. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/py.typed +0 -0
  61. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/__init__.py +0 -0
  62. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/annotator/__init__.py +0 -0
  63. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/annotator/conftest.py +0 -0
  64. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/annotator/pytest.ini +0 -0
  65. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/annotator/test_annotator_node.py +0 -0
  66. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/detector/__init__.py +0 -0
  67. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/detector/inbox_filter/__init__.py +0 -0
  68. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/detector/inbox_filter/test_observation.py +0 -0
  69. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/detector/inbox_filter/test_relevance_group.py +0 -0
  70. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/detector/inbox_filter/test_unexpected_observations_count.py +0 -0
  71. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/detector/pytest.ini +0 -0
  72. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/detector/test.jpg +0 -0
  73. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/detector/test_outbox.py +0 -0
  74. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/detector/test_relevance_filter.py +0 -0
  75. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/detector/testing_detector.py +0 -0
  76. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/general/__init__.py +0 -0
  77. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/general/conftest.py +0 -0
  78. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/general/pytest.ini +0 -0
  79. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/general/test_data/file_1.txt +0 -0
  80. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/general/test_data/file_2.txt +0 -0
  81. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/general/test_data/model.json +0 -0
  82. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/general/test_data_classes.py +0 -0
  83. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/general/test_downloader.py +0 -0
  84. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/general/test_learning_loop_node.py +0 -0
  85. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/test_helper.py +0 -0
  86. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/trainer/__init__.py +0 -0
  87. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/trainer/pytest.ini +0 -0
  88. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/trainer/state_helper.py +0 -0
  89. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/trainer/states/__init__.py +0 -0
  90. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/trainer/states/test_state_cleanup.py +0 -0
  91. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/trainer/test_trainer_states.py +0 -0
  92. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/tests/trainer/testing_trainer_logic.py +0 -0
  93. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/trainer/__init__.py +0 -0
  94. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/trainer/downloader.py +0 -0
  95. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/trainer/exceptions.py +0 -0
  96. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/trainer/executor.py +0 -0
  97. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/trainer/rest/__init__.py +0 -0
  98. {learning_loop_node-0.10.12 → learning_loop_node-0.10.14}/learning_loop_node/trainer/test_executor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: learning-loop-node
3
- Version: 0.10.12
3
+ Version: 0.10.14
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
@@ -57,20 +57,21 @@ To start a node you have to implement the logic by inheriting from the correspon
57
57
 
58
58
  You can configure connection to our Learning Loop by specifying the following environment variables before starting:
59
59
 
60
- | Name | Alias | Purpose | Required by |
61
- | ------------------------ | ------------ | ------------------------------------------------------------ | -------------------- |
62
- | LOOP_HOST | HOST | Learning Loop address (e.g. learning-loop.ai) | all |
63
- | LOOP_USERNAME | USERNAME | Learning Loop user name | all besides Detector |
64
- | LOOP_PASSWORD | PASSWORD | Learning Loop password | all besides Detector |
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 |
68
- | MIN_UNCERTAIN_THRESHOLD | PROJECT | smallest confidence (float) at which auto-upload will happen | Detector |
69
- | MAX_UNCERTAIN_THRESHOLD | PROJECT | largest confidence (float) at which auto-upload will happen | Detector |
70
- | INFERENCE_BATCH_SIZE | - | Batch size of trainer when calculating detections | Trainer (opt.) |
71
- | RESTART_AFTER_TRAINING | - | Restart the trainer after training (set to 1) | Trainer (opt.) |
72
- | KEEP_OLD_TRAININGS | - | Do not delete old trainings (set to 1) | Trainer (opt.) |
73
- | TRAINER_IDLE_TIMEOUT_SEC | - | Automatically shutdown trainer after timeout (in seconds) | Trainer (opt.) |
60
+ | Name | Alias | Purpose | Required by |
61
+ | ------------------------ | ------------ | ------------------------------------------------------------ | ------------------------- |
62
+ | LOOP_HOST | HOST | Learning Loop address (e.g. learning-loop.ai) | all |
63
+ | LOOP_USERNAME | USERNAME | Learning Loop user name | all besides Detector |
64
+ | LOOP_PASSWORD | PASSWORD | Learning Loop password | all besides Detector |
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.) |
68
+ | MIN_UNCERTAIN_THRESHOLD | - | smallest confidence (float) at which auto-upload will happen | Detector (opt.) |
69
+ | MAX_UNCERTAIN_THRESHOLD | - | largest confidence (float) at which auto-upload will happen | Detector (opt.) |
70
+ | INFERENCE_BATCH_SIZE | - | Batch size of trainer when calculating detections | Trainer (opt.) |
71
+ | RESTART_AFTER_TRAINING | - | Restart the trainer after training (set to 1) | Trainer (opt.) |
72
+ | KEEP_OLD_TRAININGS | - | Do not delete old trainings (set to 1) | Trainer (opt.) |
73
+ | TRAINER_IDLE_TIMEOUT_SEC | - | Automatically shutdown trainer after timeout (in seconds) | Trainer (opt.) |
74
+ | USE_BACKDOOR_CONTROLS | - | Always enable backdoor controls (set to 1) | Trainer / Detector (opt.) |
74
75
 
75
76
  #### Testing
76
77
 
@@ -17,20 +17,21 @@ To start a node you have to implement the logic by inheriting from the correspon
17
17
 
18
18
  You can configure connection to our Learning Loop by specifying the following environment variables before starting:
19
19
 
20
- | Name | Alias | Purpose | Required by |
21
- | ------------------------ | ------------ | ------------------------------------------------------------ | -------------------- |
22
- | LOOP_HOST | HOST | Learning Loop address (e.g. learning-loop.ai) | all |
23
- | LOOP_USERNAME | USERNAME | Learning Loop user name | all besides Detector |
24
- | LOOP_PASSWORD | PASSWORD | Learning Loop password | all besides Detector |
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 |
28
- | MIN_UNCERTAIN_THRESHOLD | PROJECT | smallest confidence (float) at which auto-upload will happen | Detector |
29
- | MAX_UNCERTAIN_THRESHOLD | PROJECT | largest confidence (float) at which auto-upload will happen | Detector |
30
- | INFERENCE_BATCH_SIZE | - | Batch size of trainer when calculating detections | Trainer (opt.) |
31
- | RESTART_AFTER_TRAINING | - | Restart the trainer after training (set to 1) | Trainer (opt.) |
32
- | KEEP_OLD_TRAININGS | - | Do not delete old trainings (set to 1) | Trainer (opt.) |
33
- | TRAINER_IDLE_TIMEOUT_SEC | - | Automatically shutdown trainer after timeout (in seconds) | Trainer (opt.) |
20
+ | Name | Alias | Purpose | Required by |
21
+ | ------------------------ | ------------ | ------------------------------------------------------------ | ------------------------- |
22
+ | LOOP_HOST | HOST | Learning Loop address (e.g. learning-loop.ai) | all |
23
+ | LOOP_USERNAME | USERNAME | Learning Loop user name | all besides Detector |
24
+ | LOOP_PASSWORD | PASSWORD | Learning Loop password | all besides Detector |
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.) |
28
+ | MIN_UNCERTAIN_THRESHOLD | - | smallest confidence (float) at which auto-upload will happen | Detector (opt.) |
29
+ | MAX_UNCERTAIN_THRESHOLD | - | largest confidence (float) at which auto-upload will happen | Detector (opt.) |
30
+ | INFERENCE_BATCH_SIZE | - | Batch size of trainer when calculating detections | Trainer (opt.) |
31
+ | RESTART_AFTER_TRAINING | - | Restart the trainer after training (set to 1) | Trainer (opt.) |
32
+ | KEEP_OLD_TRAININGS | - | Do not delete old trainings (set to 1) | Trainer (opt.) |
33
+ | TRAINER_IDLE_TIMEOUT_SEC | - | Automatically shutdown trainer after timeout (in seconds) | Trainer (opt.) |
34
+ | USE_BACKDOOR_CONTROLS | - | Always enable backdoor controls (set to 1) | Trainer / Detector (opt.) |
34
35
 
35
36
  #### Testing
36
37
 
@@ -22,7 +22,6 @@ class AnnotatorNode(Node):
22
22
  self.tool = annotator_logic
23
23
  self.histories: Dict = {}
24
24
  annotator_logic.init(self)
25
- self.status_sent = False
26
25
 
27
26
  def register_sio_events(self, sio_client: AsyncClient):
28
27
 
@@ -66,8 +65,6 @@ class AnnotatorNode(Node):
66
65
  return self.histories.setdefault(frontend_id, self.tool.create_empty_history())
67
66
 
68
67
  async def send_status(self):
69
- if self.status_sent:
70
- return
71
68
 
72
69
  status = AnnotationNodeStatus(
73
70
  id=self.uuid,
@@ -76,20 +73,24 @@ class AnnotatorNode(Node):
76
73
  capabilities=['segmentation']
77
74
  )
78
75
 
79
- self.log.info(f'Sending status {status}')
76
+ self.log.debug('Sending status %s', status)
80
77
  try:
81
78
  result = await self.sio_client.call('update_annotation_node', jsonable_encoder(asdict(status)), timeout=10)
82
- except Exception as e:
83
- self.log.error(f'Error for updating: {str(e)}')
79
+ except Exception:
80
+ self.socket_connection_broken = True
81
+ self.log.exception('Error for updating:')
82
+ return
83
+
84
+ if not isinstance(result, Dict):
85
+ self.socket_connection_broken = True
86
+ self.log.error('Expected Dict, got %s', type(result))
84
87
  return
85
88
 
86
- assert isinstance(result, Dict)
87
89
  response = from_dict(data_class=SocketResponse, data=result)
88
90
 
89
91
  if not response.success:
90
- self.log.error(f'Error for updating: Response from loop was : {asdict(response)}')
91
- else:
92
- self.status_sent = True
92
+ self.socket_connection_broken = True
93
+ self.log.error('Response from loop was: %s', asdict(response))
93
94
 
94
95
  async def download_image(self, context: Context, uuid: str):
95
96
  project_folder = create_project_folder(context)
@@ -16,14 +16,14 @@ class BoxDetection():
16
16
  """Coordinates according to COCO format. x,y is the top left corner of the box.
17
17
  x increases to the right, y increases downwards.
18
18
  """
19
- category_name: str
20
- x: int
21
- y: int
22
- width: int
23
- height: int
24
- model_name: str
25
- confidence: float
26
- category_id: Optional[str] = None
19
+ category_name: str = field(metadata={'description': 'Category name'})
20
+ x: int = field(metadata={'description': 'X coordinate (left to right)'})
21
+ y: int = field(metadata={'description': 'Y coordinate (top to bottom)'})
22
+ width: int = field(metadata={'description': 'Width'})
23
+ height: int = field(metadata={'description': 'Height'})
24
+ model_name: str = field(metadata={'description': 'Model name'})
25
+ confidence: float = field(metadata={'description': 'Confidence'})
26
+ category_id: Optional[str] = field(default=None, metadata={'description': 'Category ID'})
27
27
 
28
28
  def intersection_over_union(self, other_detection: 'BoxDetection') -> float:
29
29
  # https://www.pyimagesearch.com/2016/11/07/intersection-over-union-iou-for-object-detection/
@@ -52,12 +52,12 @@ class BoxDetection():
52
52
  class PointDetection():
53
53
  """Coordinates according to COCO format. x,y is the center of the point.
54
54
  x increases to the right, y increases downwards."""
55
- category_name: str
56
- x: float
57
- y: float
58
- model_name: str
59
- confidence: float
60
- category_id: Optional[str] = None
55
+ category_name: str = field(metadata={'description': 'Category name'})
56
+ x: float = field(metadata={'description': 'X coordinate (right)'})
57
+ y: float = field(metadata={'description': 'Y coordinate (down)'})
58
+ model_name: str = field(metadata={'description': 'Model name'})
59
+ confidence: float = field(metadata={'description': 'Confidence'})
60
+ category_id: Optional[str] = field(default=None, metadata={'description': 'Category ID'})
61
61
 
62
62
  def distance(self, other: 'PointDetection') -> float:
63
63
  return np.sqrt((other.x - self.x)**2 + (other.y - self.y)**2)
@@ -68,10 +68,10 @@ class PointDetection():
68
68
 
69
69
  @dataclass(**KWONLY_SLOTS)
70
70
  class ClassificationDetection():
71
- category_name: str
72
- model_name: str
73
- confidence: float
74
- category_id: Optional[str] = None
71
+ category_name: str = field(metadata={'description': 'Category name'})
72
+ model_name: str = field(metadata={'description': 'Model name'})
73
+ confidence: float = field(metadata={'description': 'Confidence'})
74
+ category_id: Optional[str] = field(default=None, metadata={'description': 'Category ID'})
75
75
 
76
76
  def __str__(self):
77
77
  return f'c: {self.confidence:.2f} -> {self.category_name}'
@@ -112,13 +112,22 @@ def current_datetime():
112
112
 
113
113
  @dataclass(**KWONLY_SLOTS)
114
114
  class Detections():
115
- box_detections: List[BoxDetection] = field(default_factory=list)
116
- point_detections: List[PointDetection] = field(default_factory=list)
117
- segmentation_detections: List[SegmentationDetection] = field(default_factory=list)
118
- classification_detections: List[ClassificationDetection] = field(default_factory=list)
119
- tags: List[str] = field(default_factory=list)
120
- date: Optional[str] = field(default_factory=current_datetime)
121
- image_id: Optional[str] = None # used for detection of trainers
115
+ box_detections: List[BoxDetection] = field(default_factory=list, metadata={
116
+ 'description': 'List of box detections'})
117
+ point_detections: List[PointDetection] = field(default_factory=list, metadata={
118
+ 'description': 'List of point detections'})
119
+ segmentation_detections: List[SegmentationDetection] = field(default_factory=list, metadata={
120
+ 'description': 'List of segmentation detections'})
121
+ classification_detections: List[ClassificationDetection] = field(default_factory=list, metadata={
122
+ 'description': 'List of classification detections'})
123
+ tags: List[str] = field(default_factory=list, metadata={
124
+ 'description': 'List of tags'})
125
+ date: Optional[str] = field(default_factory=current_datetime, metadata={
126
+ 'description': 'Date of the detections'})
127
+ image_id: Optional[str] = field(default=None, metadata={
128
+ 'description': 'Image uuid'})
129
+ source: Optional[str] = field(default=None, metadata={
130
+ 'description': 'Source of the detections'})
122
131
 
123
132
  def __len__(self):
124
133
  return len(self.box_detections) + len(self.point_detections) + len(self.segmentation_detections) + len(self.classification_detections)
@@ -20,14 +20,19 @@ class CategoryType(str, Enum):
20
20
 
21
21
  @dataclass(**KWONLY_SLOTS)
22
22
  class Category():
23
- id: str # TODO: rename to identifier or uuid (cannot be changed because of database / loop communication)
24
- name: str
25
- description: Optional[str] = None
26
- hotkey: Optional[str] = None
27
- color: Optional[str] = None
28
- point_size: Optional[int] = None
29
- # TODO: rename to ctype (cannot be changed because of database / loop communication)
30
- type: Optional[Union[CategoryType, str]] = None
23
+ id: str = field(metadata={"description": "The uuid of the category."})
24
+ name: str = field(metadata={"description": "The name of the category."})
25
+ description: Optional[str] = field(default=None, metadata={
26
+ "description": "An optional description of the category."})
27
+ hotkey: Optional[str] = field(default=None, metadata={
28
+ "description": "The key shortcut of the category when annotating in the Learning Loop UI."})
29
+ color: Optional[str] = field(default=None, metadata={
30
+ "description": "The color of the category when displayed in the Learning Loop UI."})
31
+ point_size: Optional[int] = field(default=None, metadata={
32
+ "description": "The point size of the category in pixels. Represents the uncertainty of the category."})
33
+ type: Optional[Union[CategoryType, str]] = field(default=None, metadata={
34
+ "description": "The type of the category",
35
+ "example": "box, point, segmentation, classification"})
31
36
 
32
37
  @staticmethod
33
38
  def from_list(values: List[dict]) -> List['Category']:
@@ -45,15 +50,20 @@ class Context():
45
50
 
46
51
  @dataclass(**KWONLY_SLOTS)
47
52
  class ModelInformation():
48
- id: str
49
- host: Optional[str]
50
- organization: str
51
- project: str
52
- version: str
53
- categories: List[Category] = field(default_factory=list)
54
- resolution: Optional[int] = None
55
- model_root_path: Optional[str] = None
56
- model_size: Optional[str] = None
53
+ id: str = field(metadata={"description": "The uuid of the model."})
54
+ host: Optional[str] = field(metadata={"description": "The hostname that started the training.",
55
+ "example": "learning-loop.ai"})
56
+ organization: str = field(metadata={"description": "The owner organization of the model."})
57
+ project: str = field(metadata={"description": "The project of the model."})
58
+ version: str = field(metadata={"description": "The version of the model."})
59
+ categories: List[Category] = field(default_factory=list, metadata={
60
+ "description": "The categories used in the model."})
61
+ resolution: Optional[int] = field(default=None, metadata={
62
+ "description": "The resolution of the model (width and height of the image after preprocessing in pixels)."})
63
+ model_root_path: Optional[str] = field(
64
+ default=None, metadata={"description": "The path of the parent directory of the model in the file system."})
65
+ model_size: Optional[str] = field(default=None, metadata={
66
+ "description": "The size of the model (i.e. the specification or variant of the model architecture)."})
57
67
 
58
68
  @property
59
69
  def context(self):
@@ -140,20 +140,21 @@ class DataExchanger():
140
140
  """Downloads a model (and additional meta data like model.json) and returns the paths of the downloaded files.
141
141
  Used before training a model (when continuing a finished training) or before detecting images.
142
142
  """
143
- logging.info(f'Downloading model data for uuid {model_uuid} from the loop to {target_folder}..')
143
+ logging.info('Downloading model data for uuid %s from the loop to %s..', model_uuid, target_folder)
144
144
 
145
145
  path = f'/{context.organization}/projects/{context.project}/models/{model_uuid}/{model_format}/file'
146
146
  response = await self.loop_communicator.get(path, requires_login=False)
147
147
  if response.status_code != 200:
148
- content = response.json()
149
- logging.error(f'could not download loop/{path}: {response.status_code}, content: {content}')
150
- raise DownloadError(content['detail'])
148
+ decoded_content = response.content.decode('utf-8')
149
+ logging.error('could not download loop/%s: %s, content: %s', path,
150
+ response.status_code, decoded_content)
151
+ raise DownloadError(decoded_content)
151
152
  try:
152
153
  provided_filename = response.headers.get(
153
154
  "Content-Disposition").split("filename=")[1].strip('"')
154
155
  content = response.content
155
156
  except:
156
- logging.exception(f'Error during downloading model {path}:')
157
+ logging.exception('Error while downloading model %s:', path)
157
158
  raise
158
159
 
159
160
  tmp_path = f'/tmp/{os.path.splitext(provided_filename)[0]}'
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  from abc import abstractmethod
3
- from typing import Optional
3
+ from typing import List, Optional
4
4
 
5
5
  import numpy as np
6
6
 
@@ -28,7 +28,7 @@ class DetectorLogic():
28
28
  return self._model_info is not None
29
29
 
30
30
  def load_model(self):
31
- logging.info(f'Loading model from {GLOBALS.data_folder}/model')
31
+ logging.info('Loading model from %s', GLOBALS.data_folder)
32
32
  model_info = ModelInformation.load_from_disk(f'{GLOBALS.data_folder}/model')
33
33
  if model_info is None:
34
34
  logging.warning('No model found')
@@ -37,15 +37,21 @@ class DetectorLogic():
37
37
  try:
38
38
  self._model_info = model_info
39
39
  self.init()
40
- logging.info(f'Successfully loaded model {self._model_info}')
40
+ logging.info('Successfully loaded model %s', self._model_info)
41
41
  except Exception:
42
- logging.error(f'Could not init model {model_info}')
42
+ logging.error('Could not init model %s', model_info)
43
43
  raise
44
44
 
45
45
  @abstractmethod
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
50
+ """Called by the detector node when an image should be evaluated (REST or SocketIO).
51
+ Tags, source come from the caller and may be used in this function.
52
+ By default, this function simply calls `evaluate`"""
53
+ return self.evaluate(image)
54
+
49
55
  @abstractmethod
50
56
  def evaluate(self, image: np.ndarray) -> Detections:
51
57
  """Evaluate the image and return the detections.
@@ -9,9 +9,9 @@ from threading import Thread
9
9
  from typing import Dict, List, Optional, Union
10
10
 
11
11
  import numpy as np
12
+ import socketio
12
13
  from dacite import from_dict
13
14
  from fastapi.encoders import jsonable_encoder
14
- from fastapi_socketio import SocketManager
15
15
  from socketio import AsyncClient
16
16
 
17
17
  from ..data_classes import Category, Context, Detections, DetectionStatus, ModelInformation, Shape
@@ -41,7 +41,7 @@ class DetectorNode(Node):
41
41
  self.organization = environment_reader.organization()
42
42
  self.project = environment_reader.project()
43
43
  assert self.organization and self.project, 'Detector node needs an organization and an project'
44
- self.log.info(f'Using {self.organization}/{self.project}')
44
+ self.log.info('Using %s/%s', self.organization, self.project)
45
45
  self.operation_mode: OperationMode = OperationMode.Startup
46
46
  self.connected_clients: List[str] = []
47
47
 
@@ -70,7 +70,7 @@ class DetectorNode(Node):
70
70
  self.include_router(rest_outbox_mode.router, tags=["outbox_mode"])
71
71
  self.include_router(rest_version_control.router, tags=["model_version"])
72
72
 
73
- if use_backdoor_controls:
73
+ if use_backdoor_controls or os.environ.get('USE_BACKDOOR_CONTROLS', '0').lower() in ('1', 'true'):
74
74
  self.include_router(backdoor_controls.router)
75
75
 
76
76
  self.setup_sio_server()
@@ -93,7 +93,8 @@ class DetectorNode(Node):
93
93
  # simulate super().startup
94
94
  await self.loop_communicator.backend_ready()
95
95
  # await self.loop_communicator.ensure_login()
96
- await self.create_sio_client()
96
+ self.set_skip_repeat_loop(False)
97
+ self.socket_connection_broken = True
97
98
  await self.on_startup()
98
99
 
99
100
  # simulate startup
@@ -126,35 +127,48 @@ class DetectorNode(Node):
126
127
 
127
128
  def setup_sio_server(self) -> None:
128
129
  """The DetectorNode acts as a SocketIO server. This method sets up the server and defines the event handlers."""
129
-
130
130
  # pylint: disable=unused-argument
131
131
 
132
- async def _detect(sid, data: Dict) -> Dict:
133
- self.log.info('running detect via socketio')
132
+ # Initialize the Socket.IO server
133
+ self.sio = socketio.AsyncServer(async_mode='asgi')
134
+ # Initialize and mount the ASGI app
135
+ self.sio_app = socketio.ASGIApp(self.sio, socketio_path='/socket.io')
136
+ self.mount('/ws', self.sio_app)
137
+ # Register event handlers
138
+
139
+ self.log.info('>>>>>>>>>>>>>>>>>>>>>>> Setting up the SIO server')
140
+
141
+ @self.sio.event
142
+ async def detect(sid, data: Dict) -> Dict:
143
+ self.log.debug('running detect via socketio')
134
144
  try:
135
145
  np_image = np.frombuffer(data['image'], np.uint8)
136
146
  det = await self.get_detections(
137
147
  raw_image=np_image,
138
148
  camera_id=data.get('camera-id', None) or data.get('mac', None),
139
149
  tags=data.get('tags', []),
140
- autoupload=data.get('autoupload', None),
150
+ source=data.get('source', None),
151
+ autoupload=data.get('autoupload', None)
141
152
  )
142
153
  if det is None:
143
154
  return {'error': 'no model loaded'}
144
- self.log.info('detect via socketio finished')
145
- return det
155
+ detection_dict = jsonable_encoder(asdict(det))
156
+ self.log.debug('detect via socketio finished')
157
+ return detection_dict
146
158
  except Exception as e:
147
159
  self.log.exception('could not detect via socketio')
148
160
  with open('/tmp/bad_img_from_socket_io.jpg', 'wb') as f:
149
161
  f.write(data['image'])
150
162
  return {'error': str(e)}
151
163
 
152
- async def _info(sid) -> Union[str, Dict]:
164
+ @self.sio.event
165
+ async def info(sid) -> Union[str, Dict]:
153
166
  if self.detector_logic.is_initialized:
154
167
  return asdict(self.detector_logic.model_info)
155
168
  return 'No model loaded'
156
169
 
157
- async def _upload(sid, data: Dict) -> Optional[Dict]:
170
+ @self.sio.event
171
+ async def upload(sid, data: Dict) -> Optional[Dict]:
158
172
  '''upload an image with detections'''
159
173
 
160
174
  detection_data = data.get('detections', {})
@@ -171,50 +185,43 @@ class DetectorNode(Node):
171
185
  tags = data.get('tags', [])
172
186
  tags.append('picked_by_system')
173
187
 
188
+ source = data.get('source', None)
189
+
174
190
  loop = asyncio.get_event_loop()
175
191
  try:
176
- await loop.run_in_executor(None, self.outbox.save, data['image'], detections, tags)
192
+ await loop.run_in_executor(None, self.outbox.save, data['image'], detections, tags, source)
177
193
  except Exception as e:
178
194
  self.log.exception('could not upload via socketio')
179
195
  return {'error': str(e)}
180
196
  return None
181
197
 
182
- def _connect(sid, environ, auth) -> None:
198
+ @self.sio.event
199
+ def connect(sid, environ, auth) -> None:
183
200
  self.connected_clients.append(sid)
184
201
 
185
- print('>>>>>>>>>>>>>>>>>>>>>>> setting up sio server', flush=True)
186
-
187
- self.sio_server = SocketManager(app=self)
188
- self.sio_server.on('detect', _detect)
189
- self.sio_server.on('info', _info)
190
- self.sio_server.on('upload', _upload)
191
- self.sio_server.on('connect', _connect)
192
-
193
202
  async def _check_for_update(self) -> None:
194
- if self.operation_mode == OperationMode.Startup:
195
- return
196
203
  try:
197
- self.log.info(f'Current operation mode is {self.operation_mode}')
204
+ self.log.debug('Current operation mode is %s', self.operation_mode)
198
205
  try:
199
206
  await self.sync_status_with_learning_loop()
200
- except Exception as e:
201
- self.log.error(f'Could not check for updates: {e}')
207
+ except Exception:
208
+ self.log.exception('Sync with learning loop failed (could not check for updates):')
202
209
  return
203
210
 
204
211
  if self.operation_mode != OperationMode.Idle:
205
- self.log.info(f'not checking for updates; operation mode is {self.operation_mode}')
212
+ self.log.debug('not checking for updates; operation mode is %s', self.operation_mode)
206
213
  return
207
214
 
208
215
  self.status.reset_error('update_model')
209
216
  if self.target_model is None:
210
- self.log.info('not checking for updates; no target model selected')
217
+ self.log.debug('not checking for updates; no target model selected')
211
218
  return
212
219
 
213
- current_version = self.detector_logic._model_info.version if self.detector_logic._model_info is not None else None
220
+ current_version = self.detector_logic._model_info.version if self.detector_logic._model_info is not None else None # pylint: disable=protected-access
214
221
 
215
222
  if not self.detector_logic.is_initialized or self.target_model.version != current_version:
216
- self.log.info(
217
- f'Current model "{current_version or "-"}" needs to be updated to {self.target_model.version}')
223
+ self.log.info('Current model "%s" needs to be updated to %s',
224
+ current_version or "-", self.target_model.version)
218
225
 
219
226
  with step_into(GLOBALS.data_folder):
220
227
  model_symlink = 'model'
@@ -232,7 +239,7 @@ class DetectorNode(Node):
232
239
  except Exception:
233
240
  pass
234
241
  os.symlink(target_model_folder, model_symlink)
235
- self.log.info(f'Updated symlink for model to {os.readlink(model_symlink)}')
242
+ self.log.info('Updated symlink for model to %s', os.readlink(model_symlink))
236
243
 
237
244
  self.detector_logic.load_model()
238
245
  try:
@@ -283,13 +290,16 @@ class DetectorNode(Node):
283
290
  model_format=self.detector_logic.model_format,
284
291
  )
285
292
 
286
- self.log.info(f'sending status {status}')
293
+ self.log.debug('sending status %s', status)
287
294
  response = await self.sio_client.call('update_detector', (self.organization, self.project, jsonable_encoder(asdict(status))))
295
+ if not response:
296
+ self.socket_connection_broken = True
297
+ return
288
298
 
289
- assert response is not None
290
299
  socket_response = from_dict(data_class=SocketResponse, data=response)
291
300
  if not socket_response.success:
292
- self.log.error(f'Statusupdate failed: {response}')
301
+ self.socket_connection_broken = True
302
+ self.log.error('Statusupdate failed: %s', response)
293
303
  raise Exception(f'Statusupdate failed: {response}')
294
304
 
295
305
  assert socket_response.payload is not None
@@ -301,21 +311,24 @@ class DetectorNode(Node):
301
311
  id=deployment_target_model_id,
302
312
  version=deployment_target_model_version)
303
313
 
304
- if self.version_control == rest_version_control.VersionMode.FollowLoop:
314
+ if (self.version_control == rest_version_control.VersionMode.FollowLoop and
315
+ self.target_model != self.loop_deployment_target):
316
+ old_target_model_version = self.target_model.version if self.target_model else None
305
317
  self.target_model = self.loop_deployment_target
306
- self.log.info(f'After sending status. Target_model is {self.target_model.version}')
318
+ self.log.info('After sending status. Target_model changed from %s to %s',
319
+ old_target_model_version, self.target_model.version)
307
320
 
308
321
  async def set_operation_mode(self, mode: OperationMode):
309
322
  self.operation_mode = mode
310
323
  try:
311
324
  await self.sync_status_with_learning_loop()
312
325
  except Exception as e:
313
- self.log.warning(f'Operation mode set to {mode}, but sync failed: {e}')
326
+ self.log.warning('Operation mode set to %s, but sync failed: %s', mode, e)
314
327
 
315
328
  def reload(self, reason: str):
316
329
  '''provide a cause for the reload'''
317
330
 
318
- self.log.info(f'########## reloading app because {reason}')
331
+ self.log.info('########## reloading app because %s', reason)
319
332
  if os.path.isfile('/app/app_code/restart/restart.py'):
320
333
  subprocess.call(['touch', '/app/app_code/restart/restart.py'])
321
334
  elif os.path.isfile('/app/main.py'):
@@ -325,33 +338,37 @@ class DetectorNode(Node):
325
338
  else:
326
339
  self.log.error('could not reload app')
327
340
 
328
- async def get_detections(self, raw_image: np.ndarray, camera_id: Optional[str], tags: List[str], autoupload: Optional[str] = None) -> Optional[Dict]:
329
- """Note: raw_image is a numpy array of type uint8, but not in the correrct shape!
341
+ async def get_detections(self,
342
+ raw_image: np.ndarray,
343
+ camera_id: Optional[str],
344
+ tags: List[str],
345
+ source: Optional[str] = None,
346
+ autoupload: Optional[str] = None) -> Detections:
347
+ """ Main processing function for the detector node when an image is received via REST or SocketIO.
348
+ This function infers the detections from the image, cares about uploading to the loop and returns the detections as a dictionary.
349
+ Note: raw_image is a numpy array of type uint8, but not in the correct shape!
330
350
  It can be converted e.g. using cv2.imdecode(raw_image, cv2.IMREAD_COLOR)"""
331
- loop = asyncio.get_event_loop()
351
+
332
352
  await self.detection_lock.acquire()
333
- detections: Detections = await loop.run_in_executor(None, self.detector_logic.evaluate, raw_image)
353
+ loop = asyncio.get_event_loop()
354
+ detections = await loop.run_in_executor(None, self.detector_logic.evaluate_with_all_info, raw_image, tags, source)
334
355
  self.detection_lock.release()
335
- for seg_detection in detections.segmentation_detections:
336
- if isinstance(seg_detection.shape, Shape):
337
- shapes = ','.join([str(value) for p in seg_detection.shape.points for _,
338
- value in asdict(p).items()])
339
- seg_detection.shape = shapes # TODO This seems to be a quick fix.. check how loop upload detections deals with this
340
356
 
357
+ fix_shape_detections(detections)
341
358
  n_bo, n_cl = len(detections.box_detections), len(detections.classification_detections)
342
359
  n_po, n_se = len(detections.point_detections), len(detections.segmentation_detections)
343
- self.log.info(f'detected:{n_bo} boxes, {n_po} points, {n_se} segs, {n_cl} classes')
360
+ self.log.debug('Detected: %d boxes, %d points, %d segs, %d classes', n_bo, n_po, n_se, n_cl)
344
361
 
345
362
  if autoupload is None or autoupload == 'filtered': # NOTE default is filtered
346
363
  Thread(target=self.relevance_filter.may_upload_detections,
347
- args=(detections, camera_id, raw_image, tags)).start()
364
+ args=(detections, camera_id, raw_image, tags, source)).start()
348
365
  elif autoupload == 'all':
349
- Thread(target=self.outbox.save, args=(raw_image, detections, tags)).start()
366
+ Thread(target=self.outbox.save, args=(raw_image, detections, tags, source)).start()
350
367
  elif autoupload == 'disabled':
351
368
  pass
352
369
  else:
353
- self.log.error(f'unknown autoupload value {autoupload}')
354
- return jsonable_encoder(asdict(detections))
370
+ self.log.error('unknown autoupload value %s', autoupload)
371
+ return detections
355
372
 
356
373
  async def upload_images(self, images: List[bytes]):
357
374
  loop = asyncio.get_event_loop()
@@ -393,3 +410,12 @@ def step_into(new_dir):
393
410
  yield
394
411
  finally:
395
412
  os.chdir(previous_dir)
413
+
414
+
415
+ def fix_shape_detections(detections: Detections):
416
+ # TODO This is a quick fix.. check how loop upload detections deals with this
417
+ for seg_detection in detections.segmentation_detections:
418
+ if isinstance(seg_detection.shape, Shape):
419
+ points = ','.join([str(value) for p in seg_detection.shape.points for _,
420
+ value in asdict(p).items()])
421
+ seg_detection.shape = points