learning-loop-node 0.9.2__py3-none-any.whl → 0.10.0__py3-none-any.whl

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 (55) hide show
  1. learning_loop_node/__init__.py +2 -3
  2. learning_loop_node/annotation/annotator_logic.py +2 -2
  3. learning_loop_node/annotation/annotator_node.py +16 -15
  4. learning_loop_node/data_classes/__init__.py +17 -10
  5. learning_loop_node/data_classes/detections.py +7 -2
  6. learning_loop_node/data_classes/general.py +8 -5
  7. learning_loop_node/data_classes/training.py +49 -21
  8. learning_loop_node/data_exchanger.py +85 -139
  9. learning_loop_node/detector/__init__.py +0 -1
  10. learning_loop_node/detector/detector_logic.py +0 -2
  11. learning_loop_node/detector/detector_node.py +14 -15
  12. learning_loop_node/detector/inbox_filter/cam_observation_history.py +4 -7
  13. learning_loop_node/detector/outbox.py +0 -1
  14. learning_loop_node/detector/rest/about.py +25 -0
  15. learning_loop_node/detector/tests/conftest.py +4 -1
  16. learning_loop_node/detector/tests/test_client_communication.py +18 -0
  17. learning_loop_node/detector/tests/test_outbox.py +2 -0
  18. learning_loop_node/detector/tests/testing_detector.py +0 -7
  19. learning_loop_node/globals.py +2 -2
  20. learning_loop_node/helpers/gdrive_downloader.py +1 -1
  21. learning_loop_node/helpers/misc.py +124 -17
  22. learning_loop_node/loop_communication.py +57 -25
  23. learning_loop_node/node.py +62 -135
  24. learning_loop_node/tests/test_downloader.py +8 -7
  25. learning_loop_node/tests/test_executor.py +14 -11
  26. learning_loop_node/tests/test_helper.py +3 -5
  27. learning_loop_node/trainer/downloader.py +1 -1
  28. learning_loop_node/trainer/executor.py +87 -83
  29. learning_loop_node/trainer/io_helpers.py +66 -9
  30. learning_loop_node/trainer/rest/backdoor_controls.py +10 -5
  31. learning_loop_node/trainer/rest/controls.py +3 -1
  32. learning_loop_node/trainer/tests/conftest.py +19 -28
  33. learning_loop_node/trainer/tests/states/test_state_cleanup.py +5 -3
  34. learning_loop_node/trainer/tests/states/test_state_detecting.py +23 -20
  35. learning_loop_node/trainer/tests/states/test_state_download_train_model.py +18 -12
  36. learning_loop_node/trainer/tests/states/test_state_prepare.py +13 -12
  37. learning_loop_node/trainer/tests/states/test_state_sync_confusion_matrix.py +21 -18
  38. learning_loop_node/trainer/tests/states/test_state_train.py +27 -28
  39. learning_loop_node/trainer/tests/states/test_state_upload_detections.py +34 -32
  40. learning_loop_node/trainer/tests/states/test_state_upload_model.py +22 -20
  41. learning_loop_node/trainer/tests/test_errors.py +20 -12
  42. learning_loop_node/trainer/tests/test_trainer_states.py +4 -5
  43. learning_loop_node/trainer/tests/testing_trainer_logic.py +25 -30
  44. learning_loop_node/trainer/trainer_logic.py +80 -590
  45. learning_loop_node/trainer/trainer_logic_generic.py +495 -0
  46. learning_loop_node/trainer/trainer_node.py +27 -106
  47. {learning_loop_node-0.9.2.dist-info → learning_loop_node-0.10.0.dist-info}/METADATA +1 -1
  48. learning_loop_node-0.10.0.dist-info/RECORD +85 -0
  49. learning_loop_node/converter/converter_logic.py +0 -68
  50. learning_loop_node/converter/converter_node.py +0 -125
  51. learning_loop_node/converter/tests/test_converter.py +0 -55
  52. learning_loop_node/trainer/training_syncronizer.py +0 -52
  53. learning_loop_node-0.9.2.dist-info/RECORD +0 -87
  54. /learning_loop_node/{converter/__init__.py → py.typed} +0 -0
  55. {learning_loop_node-0.9.2.dist-info → learning_loop_node-0.10.0.dist-info}/WHEEL +0 -0
@@ -1,12 +1,11 @@
1
1
  import logging
2
- import os
3
- import sys
4
2
 
5
- from .converter.converter_node import ConverterNode
6
3
  # from . import log_conf
7
4
  from .detector.detector_logic import DetectorLogic
8
5
  from .detector.detector_node import DetectorNode
9
6
  from .globals import GLOBALS
10
7
  from .trainer.trainer_node import TrainerNode
11
8
 
9
+ __all__ = ['TrainerNode', 'DetectorNode', 'DetectorLogic', 'GLOBALS']
10
+
12
11
  logging.info('>>>>>>>>>>>>>>>>>> LOOP INITIALIZED <<<<<<<<<<<<<<<<<<<<<<<')
@@ -7,10 +7,10 @@ from ..node import Node
7
7
 
8
8
  class AnnotatorLogic():
9
9
 
10
- def __init__(self):
10
+ def __init__(self) -> None:
11
11
  self._node: Optional[Node] = None
12
12
 
13
- def init(self, node: Node):
13
+ def init(self, node: Node) -> None:
14
14
  self._node = node
15
15
 
16
16
  @abstractmethod
@@ -8,7 +8,7 @@ from socketio import AsyncClient
8
8
  from ..data_classes import AnnotationNodeStatus, Context, NodeState, UserInput
9
9
  from ..data_classes.socket_response import SocketResponse
10
10
  from ..data_exchanger import DataExchanger
11
- from ..helpers.misc import create_image_folder
11
+ from ..helpers.misc import create_image_folder, create_project_folder
12
12
  from ..node import Node
13
13
  from .annotator_logic import AnnotatorLogic
14
14
 
@@ -18,10 +18,11 @@ from .annotator_logic import AnnotatorLogic
18
18
  class AnnotatorNode(Node):
19
19
 
20
20
  def __init__(self, name: str, annotator_logic: AnnotatorLogic, uuid: Optional[str] = None):
21
- super().__init__(name, uuid)
21
+ super().__init__(name, uuid, 'annotation_node')
22
22
  self.tool = annotator_logic
23
23
  self.histories: Dict = {}
24
24
  annotator_logic.init(self)
25
+ self.status_sent = False
25
26
 
26
27
  def register_sio_events(self, sio_client: AsyncClient):
27
28
 
@@ -50,8 +51,6 @@ class AnnotatorNode(Node):
50
51
  raise
51
52
 
52
53
  if tool_result.annotation:
53
- if not self.sio_is_initialized():
54
- raise Exception('Socket client waas not initialized')
55
54
  await self.sio_client.call('update_segmentation_annotation', (user_input.data.context.organization,
56
55
  user_input.data.context.project,
57
56
  jsonable_encoder(asdict(tool_result.annotation))), timeout=30)
@@ -67,6 +66,9 @@ class AnnotatorNode(Node):
67
66
  return self.histories.setdefault(frontend_id, self.tool.create_empty_history())
68
67
 
69
68
  async def send_status(self):
69
+ if self.status_sent:
70
+ return
71
+
70
72
  status = AnnotationNodeStatus(
71
73
  id=self.uuid,
72
74
  name=self.name,
@@ -75,28 +77,27 @@ class AnnotatorNode(Node):
75
77
  )
76
78
 
77
79
  self.log.info(f'Sending status {status}')
78
- if self._sio_client is None:
79
- raise Exception('No socket client')
80
- result = await self._sio_client.call('update_annotation_node', jsonable_encoder(asdict(status)), timeout=10)
80
+ try:
81
+ 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)}')
84
+ return
85
+
81
86
  assert isinstance(result, Dict)
82
87
  response = from_dict(data_class=SocketResponse, data=result)
83
88
 
84
89
  if not response.success:
85
90
  self.log.error(f'Error for updating: Response from loop was : {asdict(response)}')
91
+ else:
92
+ self.status_sent = True
86
93
 
87
94
  async def download_image(self, context: Context, uuid: str):
88
- project_folder = Node.create_project_folder(context)
95
+ project_folder = create_project_folder(context)
89
96
  images_folder = create_image_folder(project_folder)
90
97
 
91
98
  downloader = DataExchanger(context=context, loop_communicator=self.loop_communicator)
92
99
  await downloader.download_images([uuid], images_folder)
93
100
 
94
- async def get_state(self):
95
- return NodeState.Online
96
-
97
- def get_node_type(self):
98
- return 'annotation_node'
99
-
100
101
  async def on_startup(self):
101
102
  pass
102
103
 
@@ -104,4 +105,4 @@ class AnnotatorNode(Node):
104
105
  pass
105
106
 
106
107
  async def on_repeat(self):
107
- pass
108
+ await self.send_status()
@@ -1,12 +1,19 @@
1
- from .annotations import (AnnotationData, AnnotationEventType,
2
- SegmentationAnnotation, ToolOutput, UserInput)
3
- from .detections import (BoxDetection, ClassificationDetection, Detections,
4
- Observation, Point, PointDetection,
1
+ from .annotations import AnnotationData, AnnotationEventType, SegmentationAnnotation, ToolOutput, UserInput
2
+ from .detections import (BoxDetection, ClassificationDetection, Detections, Observation, Point, PointDetection,
5
3
  SegmentationDetection, Shape)
6
- from .general import (AnnotationNodeStatus, Category, CategoryType, Context,
7
- DetectionStatus, ErrorConfiguration, ModelInformation,
8
- NodeState, NodeStatus)
4
+ from .general import (AnnotationNodeStatus, Category, CategoryType, Context, DetectionStatus, ErrorConfiguration,
5
+ ModelInformation, NodeState, NodeStatus)
9
6
  from .socket_response import SocketResponse
10
- from .training import (BasicModel, Errors, Hyperparameter, Model,
11
- PretrainedModel, Training, TrainingData, TrainingError,
12
- TrainingOut, TrainingState, TrainingStatus)
7
+ from .training import (Errors, Hyperparameter, Model, PretrainedModel, TrainerState, Training, TrainingData,
8
+ TrainingError, TrainingOut, TrainingStateData, TrainingStatus)
9
+
10
+ __all__ = [
11
+ 'AnnotationData', 'AnnotationEventType', 'SegmentationAnnotation', 'ToolOutput', 'UserInput',
12
+ 'BoxDetection', 'ClassificationDetection', 'Detections', 'Observation', 'Point', 'PointDetection',
13
+ 'SegmentationDetection', 'Shape',
14
+ 'AnnotationNodeStatus', 'Category', 'CategoryType', 'Context', 'DetectionStatus', 'ErrorConfiguration',
15
+ 'ModelInformation', 'NodeState', 'NodeStatus',
16
+ 'SocketResponse',
17
+ 'Errors', 'Hyperparameter', 'Model', 'PretrainedModel', 'TrainerState', 'Training', 'TrainingData',
18
+ 'TrainingError', 'TrainingOut', 'TrainingStateData', 'TrainingStatus',
19
+ ]
@@ -13,8 +13,11 @@ KWONLY_SLOTS = {'kw_only': True, 'slots': True} if sys.version_info >= (3, 10) e
13
13
 
14
14
  @dataclass(**KWONLY_SLOTS)
15
15
  class BoxDetection():
16
+ """Coordinates according to COCO format. x,y is the top left corner of the box.
17
+ x increases to the right, y increases downwards.
18
+ """
16
19
  category_name: str
17
- x: int # TODO add definition of x,y,w,h
20
+ x: int
18
21
  y: int
19
22
  width: int
20
23
  height: int
@@ -47,6 +50,8 @@ class BoxDetection():
47
50
 
48
51
  @dataclass(**KWONLY_SLOTS)
49
52
  class PointDetection():
53
+ """Coordinates according to COCO format. x,y is the center of the point.
54
+ x increases to the right, y increases downwards."""
50
55
  category_name: str
51
56
  x: float
52
57
  y: float
@@ -111,7 +116,7 @@ class Detections():
111
116
  point_detections: List[PointDetection] = field(default_factory=list)
112
117
  segmentation_detections: List[SegmentationDetection] = field(default_factory=list)
113
118
  classification_detections: List[ClassificationDetection] = field(default_factory=list)
114
- tags: Optional[List[str]] = field(default_factory=list)
119
+ tags: List[str] = field(default_factory=list)
115
120
  date: Optional[str] = field(default_factory=current_datetime)
116
121
  image_id: Optional[str] = None # used for detection of trainers
117
122
 
@@ -34,10 +34,6 @@ class Category():
34
34
  return [from_dict(data_class=Category, data=value) for value in values]
35
35
 
36
36
 
37
- def create_category(identifier: str, name: str, ctype: Union[CategoryType, str]): # TODO: This is probably unused
38
- return Category(id=identifier, name=name, description='', hotkey='', color='', type=ctype, point_size=None)
39
-
40
-
41
37
  @dataclass(**KWONLY_SLOTS)
42
38
  class Context():
43
39
  organization: str
@@ -57,6 +53,7 @@ class ModelInformation():
57
53
  categories: List[Category]
58
54
  resolution: Optional[int] = None
59
55
  model_root_path: Optional[str] = None
56
+ model_size: Optional[str] = None
60
57
 
61
58
  @property
62
59
  def context(self):
@@ -64,6 +61,8 @@ class ModelInformation():
64
61
 
65
62
  @staticmethod
66
63
  def load_from_disk(model_root_path: str) -> Optional['ModelInformation']:
64
+ """Load model.json from model_root_path and return ModelInformation object.
65
+ """
67
66
  model_info_file_path = f'{model_root_path}/model.json'
68
67
  if not os.path.exists(model_info_file_path):
69
68
  logging.warning(f"could not find model information file '{model_info_file_path}'")
@@ -90,6 +89,10 @@ class ModelInformation():
90
89
  del self_as_dict['model_root_path']
91
90
  f.write(json.dumps(self_as_dict))
92
91
 
92
+ @staticmethod
93
+ def from_dict(data: Dict) -> 'ModelInformation':
94
+ return from_dict(ModelInformation, data=data)
95
+
93
96
 
94
97
  @dataclass(**KWONLY_SLOTS)
95
98
  class ErrorConfiguration():
@@ -117,7 +120,7 @@ class NodeState(str, Enum):
117
120
  class NodeStatus():
118
121
  id: str
119
122
  name: str
120
- state: Optional[NodeState] = NodeState.Offline
123
+ state: Optional[NodeState] = NodeState.Online
121
124
  uptime: Optional[int] = 0
122
125
  errors: Dict = field(default_factory=dict)
123
126
  capabilities: List[str] = field(default_factory=list)
@@ -1,8 +1,10 @@
1
1
 
2
2
  import sys
3
+ import time
3
4
  from dataclasses import dataclass, field
4
5
  from enum import Enum
5
- from typing import Dict, List, Optional, Union
6
+ from pathlib import Path
7
+ from typing import Dict, List, Optional
6
8
 
7
9
  # pylint: disable=no-name-in-module
8
10
  from .general import Category, Context
@@ -16,6 +18,14 @@ class Hyperparameter():
16
18
  flip_rl: bool
17
19
  flip_ud: bool
18
20
 
21
+ @staticmethod
22
+ def from_data(data: Dict):
23
+ return Hyperparameter(
24
+ resolution=data['resolution'],
25
+ flip_rl=data.get('flip_rl', False),
26
+ flip_ud=data.get('flip_ud', False)
27
+ )
28
+
19
29
 
20
30
  @dataclass(**KWONLY_SLOTS)
21
31
  class TrainingData():
@@ -41,14 +51,15 @@ class PretrainedModel():
41
51
  description: str
42
52
 
43
53
 
44
- class TrainingState(str, Enum):
54
+ class TrainerState(str, Enum):
55
+ Idle = 'idle'
45
56
  Initialized = 'initialized'
46
57
  Preparing = 'preparing'
47
58
  DataDownloading = 'data_downloading'
48
59
  DataDownloaded = 'data_downloaded'
49
60
  TrainModelDownloading = 'train_model_downloading'
50
61
  TrainModelDownloaded = 'train_model_downloaded'
51
- TrainingRunning = 'training_running'
62
+ TrainingRunning = 'running'
52
63
  TrainingFinished = 'training_finished'
53
64
  ConfusionMatrixSyncing = 'confusion_matrix_syncing'
54
65
  ConfusionMatrixSynced = 'confusion_matrix_synced'
@@ -62,9 +73,9 @@ class TrainingState(str, Enum):
62
73
 
63
74
  @dataclass(**KWONLY_SLOTS)
64
75
  class TrainingStatus():
65
- id: str # TODO this must not be changed, but tests wont detect it -> update tests!
76
+ id: str # NOTE this must not be changed, but tests wont detect a change -> update tests!
66
77
  name: str
67
- state: Union[Optional[TrainingState], str]
78
+ state: Optional[str]
68
79
  errors: Optional[Dict]
69
80
  uptime: Optional[float]
70
81
  progress: Optional[float]
@@ -77,13 +88,13 @@ class TrainingStatus():
77
88
  architecture: Optional[str] = None
78
89
  context: Optional[Context] = None
79
90
 
80
- def short_str(self):
91
+ def short_str(self) -> str:
81
92
  prgr = f'{self.progress * 100:.0f}%' if self.progress else ''
82
93
  trtesk = f'{self.train_image_count}/{self.test_image_count}/{self.skipped_image_count}' if self.train_image_count else 'n.a.'
83
94
  cntxt = f'{self.context.organization}/{self.context.project}' if self.context else ''
84
95
  hyps = f'({self.hyperparameters})' if self.hyperparameters else ''
85
96
  arch = f'.{self.architecture} - ' if self.architecture else ''
86
- return f'[{str(self.state)} {prgr}. {self.name}({self.id}). Tr/Ts/Tsk: {trtesk} {cntxt}{arch}{hyps}]'
97
+ return f'[{str(self.state).rsplit(".", maxsplit=1)[-1]} {prgr}. {self.name}({self.id}). Tr/Ts/Tsk: {trtesk} {cntxt}{arch}{hyps}]'
87
98
 
88
99
 
89
100
  @dataclass(**KWONLY_SLOTS)
@@ -91,21 +102,35 @@ class Training():
91
102
  id: str
92
103
  context: Context
93
104
 
94
- project_folder: str
95
- images_folder: str
96
- training_folder: str
105
+ project_folder: str # f'{GLOBALS.data_folder}/{context.organization}/{context.project}'
106
+ images_folder: str # f'{project_folder}/images'
107
+ training_folder: str # f'{project_folder}/trainings/{trainings_id}'
108
+ start_time: float = field(default_factory=time.time)
109
+
110
+ # model uuid to download (to continue training) | is not a uuid when training from scratch (blank or pt-name from provided_pretrained_models->name)
111
+ base_model_uuid_or_name: Optional[str] = None
97
112
 
98
- base_model_id: Optional[str] = None
99
113
  data: Optional[TrainingData] = None
100
114
  training_number: Optional[int] = None
101
- training_state: Optional[Union[TrainingState, str]] = None
102
- model_id_for_detecting: Optional[str] = None
115
+ training_state: Optional[str] = None
116
+ model_uuid_for_detecting: Optional[str] = None
103
117
  hyperparameters: Optional[Dict] = None
104
118
 
119
+ @property
120
+ def training_folder_path(self) -> Path:
121
+ return Path(self.training_folder)
122
+
123
+ def set_values_from_data(self, data: Dict) -> None:
124
+ self.data = TrainingData(categories=Category.from_list(data['categories']))
125
+ self.data.hyperparameter = Hyperparameter.from_data(data=data)
126
+ self.training_number = data['training_number']
127
+ self.base_model_uuid_or_name = data['id']
128
+ self.training_state = TrainerState.Initialized
129
+
105
130
 
106
131
  @dataclass(**KWONLY_SLOTS)
107
132
  class TrainingOut():
108
- confusion_matrix: Optional[Dict] = None
133
+ confusion_matrix: Optional[Dict] = None # This is actually just class-wise metrics
109
134
  train_image_count: Optional[int] = None
110
135
  test_image_count: Optional[int] = None
111
136
  trainer_id: Optional[str] = None
@@ -113,9 +138,9 @@ class TrainingOut():
113
138
 
114
139
 
115
140
  @dataclass(**KWONLY_SLOTS)
116
- class BasicModel():
117
- confusion_matrix: Optional[Dict] = None
118
- meta_information: Optional[Dict] = None
141
+ class TrainingStateData():
142
+ confusion_matrix: Dict = field(default_factory=dict)
143
+ meta_information: Dict = field(default_factory=dict)
119
144
 
120
145
 
121
146
  @dataclass(**KWONLY_SLOTS)
@@ -130,8 +155,8 @@ class Model():
130
155
 
131
156
 
132
157
  class Errors():
133
- def __init__(self):
134
- self._errors: Dict = {}
158
+ def __init__(self) -> None:
159
+ self._errors: Dict[str, str] = {}
135
160
 
136
161
  def set(self, key: str, value: str):
137
162
  self._errors[key] = value
@@ -140,7 +165,7 @@ class Errors():
140
165
  def errors(self) -> Dict:
141
166
  return self._errors
142
167
 
143
- def reset(self, key: str):
168
+ def reset(self, key: str) -> None:
144
169
  try:
145
170
  del self._errors[key]
146
171
  except AttributeError:
@@ -148,7 +173,7 @@ class Errors():
148
173
  except KeyError:
149
174
  pass
150
175
 
151
- def reset_all(self):
176
+ def reset_all(self) -> None:
152
177
  self._errors = {}
153
178
 
154
179
  def has_error_for(self, key: str) -> bool:
@@ -162,3 +187,6 @@ class TrainingError(Exception):
162
187
  def __init__(self, cause: str, *args: object) -> None:
163
188
  super().__init__(*args)
164
189
  self.cause = cause
190
+
191
+ def __str__(self) -> str:
192
+ return f'TrainingError: {self.cause}'