learning-loop-node 0.18.3__py3-none-any.whl → 0.18.4__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.
Files changed (55) hide show
  1. learning_loop_node/annotation/__init__.py +0 -0
  2. learning_loop_node/annotation/annotator_logic.py +29 -0
  3. learning_loop_node/annotation/annotator_node.py +116 -0
  4. learning_loop_node/data_classes/__init__.py +50 -0
  5. learning_loop_node/data_classes/annotation_data.py +43 -0
  6. learning_loop_node/data_classes/annotations.py +44 -0
  7. learning_loop_node/data_classes/detections.py +136 -0
  8. learning_loop_node/data_classes/general.py +187 -0
  9. learning_loop_node/data_classes/image_metadata.py +56 -0
  10. learning_loop_node/data_classes/socket_response.py +24 -0
  11. learning_loop_node/data_classes/training.py +168 -0
  12. learning_loop_node/detector/__init__.py +0 -0
  13. learning_loop_node/detector/detector_logic.py +79 -0
  14. learning_loop_node/detector/detector_node.py +699 -0
  15. learning_loop_node/detector/exceptions.py +7 -0
  16. learning_loop_node/detector/inbox_filter/__init__.py +0 -0
  17. learning_loop_node/detector/inbox_filter/cam_observation_history.py +82 -0
  18. learning_loop_node/detector/inbox_filter/relevance_filter.py +40 -0
  19. learning_loop_node/detector/outbox.py +323 -0
  20. learning_loop_node/detector/rest/__init__.py +0 -0
  21. learning_loop_node/detector/rest/about.py +26 -0
  22. learning_loop_node/detector/rest/backdoor_controls.py +37 -0
  23. learning_loop_node/detector/rest/detect.py +54 -0
  24. learning_loop_node/detector/rest/model_version_control.py +46 -0
  25. learning_loop_node/detector/rest/operation_mode.py +54 -0
  26. learning_loop_node/detector/rest/outbox_mode.py +41 -0
  27. learning_loop_node/detector/rest/upload.py +44 -0
  28. learning_loop_node/enums/__init__.py +6 -0
  29. learning_loop_node/enums/annotator.py +11 -0
  30. learning_loop_node/enums/detector.py +18 -0
  31. learning_loop_node/enums/general.py +9 -0
  32. learning_loop_node/enums/trainer.py +22 -0
  33. learning_loop_node/helpers/__init__.py +0 -0
  34. learning_loop_node/helpers/background_tasks.py +77 -0
  35. learning_loop_node/helpers/environment_reader.py +49 -0
  36. learning_loop_node/helpers/gdrive_downloader.py +43 -0
  37. learning_loop_node/helpers/log_conf.py +39 -0
  38. learning_loop_node/helpers/misc.py +239 -0
  39. learning_loop_node/helpers/run.py +21 -0
  40. learning_loop_node/trainer/__init__.py +0 -0
  41. learning_loop_node/trainer/downloader.py +31 -0
  42. learning_loop_node/trainer/exceptions.py +12 -0
  43. learning_loop_node/trainer/executor.py +109 -0
  44. learning_loop_node/trainer/io_helpers.py +181 -0
  45. learning_loop_node/trainer/rest/__init__.py +0 -0
  46. learning_loop_node/trainer/rest/backdoor_controls.py +120 -0
  47. learning_loop_node/trainer/test_executor.py +66 -0
  48. learning_loop_node/trainer/trainer_logic.py +192 -0
  49. learning_loop_node/trainer/trainer_logic_generic.py +560 -0
  50. learning_loop_node/trainer/trainer_node.py +106 -0
  51. {learning_loop_node-0.18.3.dist-info → learning_loop_node-0.18.4.dist-info}/METADATA +1 -3
  52. learning_loop_node-0.18.4.dist-info/RECORD +61 -0
  53. learning_loop_node-0.18.3.dist-info/RECORD +0 -11
  54. {learning_loop_node-0.18.3.dist-info → learning_loop_node-0.18.4.dist-info}/WHEEL +0 -0
  55. {learning_loop_node-0.18.3.dist-info → learning_loop_node-0.18.4.dist-info}/top_level.txt +0 -0
File without changes
@@ -0,0 +1,29 @@
1
+ from abc import abstractmethod
2
+ from typing import Dict, Optional
3
+
4
+ from ..data_classes import ToolOutput, UserInput
5
+ from ..node import Node
6
+
7
+
8
+ class AnnotatorLogic():
9
+
10
+ def __init__(self) -> None:
11
+ self._node: Optional[Node] = None
12
+
13
+ def init(self, node: Node) -> None:
14
+ self._node = node
15
+
16
+ @abstractmethod
17
+ async def handle_user_input(self, user_input: UserInput, history: Dict) -> ToolOutput:
18
+ """ This method is called when a user input is received from the client.
19
+ The function should return a ToolOutput object."""
20
+
21
+ @abstractmethod
22
+ def create_empty_history(self) -> Dict:
23
+ """ This method is called when a new annotation session is started.
24
+ The function should return an empty history object."""
25
+
26
+ @abstractmethod
27
+ def logout_user(self, sid: str) -> bool:
28
+ """ This method is called when a user disconnects from the server.
29
+ The function should return True if the user was logged out successfully."""
@@ -0,0 +1,116 @@
1
+ from dataclasses import asdict
2
+ from typing import Dict, Optional
3
+
4
+ from dacite import from_dict
5
+ from fastapi.encoders import jsonable_encoder
6
+ from socketio import AsyncClient
7
+
8
+ from ..data_classes import AnnotationNodeStatus, Context, NodeState, UserInput
9
+ from ..data_classes.socket_response import SocketResponse
10
+ from ..data_exchanger import DataExchanger
11
+ from ..helpers.misc import create_image_folder, create_project_folder
12
+ from ..node import Node
13
+ from .annotator_logic import AnnotatorLogic
14
+
15
+ # TODO: The use case 'segmentation' is hardcoded here. This should be more flexible.
16
+
17
+
18
+ class AnnotatorNode(Node):
19
+
20
+ def __init__(self, name: str, annotator_logic: AnnotatorLogic, uuid: Optional[str] = None):
21
+ super().__init__(name, uuid=uuid, node_type='annotation_node')
22
+ self.tool = annotator_logic
23
+ self.histories: Dict = {}
24
+ annotator_logic.init(self)
25
+
26
+ def register_sio_events(self, sio_client: AsyncClient):
27
+
28
+ @sio_client.event
29
+ async def handle_user_input(user_input_dict):
30
+ return await self._handle_user_input(user_input_dict)
31
+
32
+ @sio_client.event
33
+ async def user_logout(sid):
34
+ self.reset_history(sid)
35
+ return self.tool.logout_user(sid)
36
+
37
+ async def _handle_user_input(self, user_input_dict: Dict) -> str:
38
+ if not self.sio_client or not self.sio_client.connected:
39
+ raise ConnectionError('SocketIO client is not connected')
40
+
41
+ user_input = from_dict(data_class=UserInput, data=user_input_dict)
42
+
43
+ if user_input.data.key_up == 'Escape':
44
+ self.reset_history(user_input.frontend_id)
45
+ return ''
46
+
47
+ await self.download_image(user_input.data.context, user_input.data.image_uuid)
48
+
49
+ try:
50
+ tool_result = await self.tool.handle_user_input(user_input, self.get_history(user_input.frontend_id))
51
+ except Exception:
52
+ self.reset_history(user_input.frontend_id)
53
+ raise
54
+
55
+ if tool_result.annotation:
56
+ await self.sio_client.call('update_segmentation_annotation', (user_input.data.context.organization,
57
+ user_input.data.context.project,
58
+ jsonable_encoder(asdict(tool_result.annotation))), timeout=30)
59
+ return tool_result.svg
60
+
61
+ def reset_history(self, frontend_id: str) -> None:
62
+ """Reset the history for a given frontend_id."""
63
+ if frontend_id in self.histories:
64
+ del self.histories[frontend_id]
65
+
66
+ def get_history(self, frontend_id: str) -> Dict:
67
+ """Get the history for a given frontend_id. If no history exists, create a new one."""
68
+ return self.histories.setdefault(frontend_id, self.tool.create_empty_history())
69
+
70
+ async def send_status(self):
71
+
72
+ if not self.sio_client or not self.sio_client.connected:
73
+ raise ConnectionError('SocketIO client is not connected')
74
+
75
+ status = AnnotationNodeStatus(
76
+ id=self.uuid,
77
+ name=self.name,
78
+ state=NodeState.Online,
79
+ capabilities=['segmentation']
80
+ )
81
+
82
+ self.log_status_on_change(status.state.value if status.state else 'None', status)
83
+
84
+ try:
85
+ result = await self.sio_client.call('update_annotation_node', jsonable_encoder(asdict(status)), timeout=10)
86
+ except Exception:
87
+ self.socket_connection_broken = True
88
+ self.log.exception('Error for updating:')
89
+ return
90
+
91
+ if not isinstance(result, Dict):
92
+ self.socket_connection_broken = True
93
+ self.log.error('Expected Dict, got %s', type(result))
94
+ return
95
+
96
+ response = from_dict(data_class=SocketResponse, data=result)
97
+
98
+ if not response.success:
99
+ self.socket_connection_broken = True
100
+ self.log.error('Response from loop was: %s', asdict(response))
101
+
102
+ async def download_image(self, context: Context, uuid: str):
103
+ project_folder = create_project_folder(context)
104
+ images_folder = create_image_folder(project_folder)
105
+
106
+ downloader = DataExchanger(context=context, loop_communicator=self.loop_communicator)
107
+ await downloader.download_images([uuid], images_folder)
108
+
109
+ async def on_startup(self):
110
+ pass
111
+
112
+ async def on_shutdown(self):
113
+ pass
114
+
115
+ async def on_repeat(self):
116
+ await self.send_status()
@@ -0,0 +1,50 @@
1
+ from .annotation_data import (
2
+ AnnotationData,
3
+ SegmentationAnnotation,
4
+ ToolOutput,
5
+ UserInput,
6
+ )
7
+ from .detections import (
8
+ BoxDetection,
9
+ ClassificationDetection,
10
+ Detections,
11
+ Observation,
12
+ Point,
13
+ PointDetection,
14
+ SegmentationDetection,
15
+ Shape,
16
+ )
17
+ from .general import (
18
+ AboutResponse,
19
+ AnnotationNodeStatus,
20
+ Category,
21
+ Context,
22
+ DetectorStatus,
23
+ ErrorConfiguration,
24
+ ModelInformation,
25
+ ModelVersionResponse,
26
+ NodeState,
27
+ NodeStatus,
28
+ )
29
+ from .image_metadata import ImageMetadata, ImagesMetadata
30
+ from .socket_response import SocketResponse
31
+ from .training import (
32
+ Errors,
33
+ PretrainedModel,
34
+ Training,
35
+ TrainingError,
36
+ TrainingOut,
37
+ TrainingStateData,
38
+ TrainingStatus,
39
+ )
40
+
41
+ __all__ = [
42
+ 'AboutResponse', 'AnnotationData', 'SegmentationAnnotation', 'ToolOutput', 'UserInput',
43
+ 'BoxDetection', 'ClassificationDetection', 'ImageMetadata', 'Observation', 'Point', 'PointDetection',
44
+ 'SegmentationDetection', 'Shape', 'Detections',
45
+ 'AnnotationNodeStatus', 'Category', 'Context', 'DetectorStatus', 'ErrorConfiguration',
46
+ 'ModelInformation', 'NodeState', 'NodeStatus', 'ModelVersionResponse', 'ImagesMetadata',
47
+ 'SocketResponse',
48
+ 'Errors', 'PretrainedModel', 'Training',
49
+ 'TrainingError', 'TrainingOut', 'TrainingStateData', 'TrainingStatus',
50
+ ]
@@ -0,0 +1,43 @@
1
+ import sys
2
+ from dataclasses import dataclass
3
+ from typing import Optional, Union
4
+
5
+ from ..enums import AnnotationEventType
6
+ from .detections import Point, Shape
7
+ from .general import Category, Context
8
+
9
+ KWONLY_SLOTS = {'kw_only': True, 'slots': True} if sys.version_info >= (3, 10) else {}
10
+
11
+
12
+ @dataclass(**KWONLY_SLOTS)
13
+ class AnnotationData():
14
+ coordinate: Point
15
+ event_type: Union[AnnotationEventType, str]
16
+ context: Context
17
+ image_uuid: str
18
+ category: Category
19
+
20
+ key_up: Optional[str] = None
21
+ key_down: Optional[str] = None
22
+ epsilon: Optional[float] = None
23
+ is_shift_key_pressed: Optional[bool] = None
24
+
25
+
26
+ @dataclass(**KWONLY_SLOTS)
27
+ class SegmentationAnnotation():
28
+ id: str
29
+ shape: Shape
30
+ image_id: str
31
+ category_id: str
32
+
33
+
34
+ @dataclass(**KWONLY_SLOTS)
35
+ class UserInput():
36
+ frontend_id: str
37
+ data: AnnotationData
38
+
39
+
40
+ @dataclass(**KWONLY_SLOTS)
41
+ class ToolOutput():
42
+ svg: str
43
+ annotation: Optional[SegmentationAnnotation] = None
@@ -0,0 +1,44 @@
1
+
2
+ import sys
3
+ from dataclasses import dataclass, field
4
+ from typing import Optional
5
+
6
+ KWONLY_SLOTS = {'kw_only': True, 'slots': True} if sys.version_info >= (3, 10) else {}
7
+
8
+
9
+ @dataclass(**KWONLY_SLOTS)
10
+ class BoxAnnotation():
11
+ """Coordinates according to COCO format. x,y is the top left corner of the box.
12
+ x increases to the right, y increases downwards.
13
+ """
14
+ category_name: str = field(metadata={'description': 'Category name'})
15
+ x: int = field(metadata={'description': 'X coordinate (left to right)'})
16
+ y: int = field(metadata={'description': 'Y coordinate (top to bottom)'})
17
+ width: int = field(metadata={'description': 'Width'})
18
+ height: int = field(metadata={'description': 'Height'})
19
+ category_id: Optional[str] = field(default=None, metadata={'description': 'Category UUID'})
20
+
21
+ def __str__(self):
22
+ return f'x:{int(self.x)} y: {int(self.y)}, w: {int(self.width)} h: {int(self.height)} -> {self.category_name}'
23
+
24
+
25
+ @dataclass(**KWONLY_SLOTS)
26
+ class PointAnnotation():
27
+ """Coordinates according to COCO format. x,y is the center of the point.
28
+ x increases to the right, y increases downwards."""
29
+ category_name: str = field(metadata={'description': 'Category name'})
30
+ x: float = field(metadata={'description': 'X coordinate (right)'})
31
+ y: float = field(metadata={'description': 'Y coordinate (down)'})
32
+ category_id: Optional[str] = field(default=None, metadata={'description': 'Category UUID'})
33
+
34
+ def __str__(self):
35
+ return f'x:{int(self.x)} y: {int(self.y)}, -> {self.category_name}'
36
+
37
+
38
+ @dataclass(**KWONLY_SLOTS)
39
+ class ClassificationAnnotation():
40
+ category_name: str = field(metadata={'description': 'Category name'})
41
+ category_id: Optional[str] = field(default=None, metadata={'description': 'Category UUID'})
42
+
43
+ def __str__(self):
44
+ return f'-> {self.category_name}'
@@ -0,0 +1,136 @@
1
+
2
+ import sys
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime, timedelta
5
+ from typing import List, Optional, Union
6
+
7
+ import numpy as np
8
+
9
+ KWONLY_SLOTS = {'kw_only': True, 'slots': True} if sys.version_info >= (3, 10) else {}
10
+
11
+
12
+ @dataclass(**KWONLY_SLOTS)
13
+ class BoxDetection():
14
+ """Coordinates according to COCO format. x,y is the top left corner of the box.
15
+ x increases to the right, y increases downwards.
16
+ """
17
+ category_name: str = field(metadata={'description': 'Category name'})
18
+ x: int = field(metadata={'description': 'X coordinate (left to right)'})
19
+ y: int = field(metadata={'description': 'Y coordinate (top to bottom)'})
20
+ width: int = field(metadata={'description': 'Width'})
21
+ height: int = field(metadata={'description': 'Height'})
22
+ model_name: str = field(metadata={'description': 'Model name'})
23
+ confidence: float = field(metadata={'description': 'Confidence'})
24
+ category_id: Optional[str] = field(default=None, metadata={'description': 'Category UUID'})
25
+
26
+ def intersection_over_union(self, other_detection: 'BoxDetection') -> float:
27
+ # https://www.pyimagesearch.com/2016/11/07/intersection-over-union-iou-for-object-detection/
28
+ xA = max(self.x, other_detection.x)
29
+ yA = max(self.y, other_detection.y)
30
+ xB = min(self.x + self.width, other_detection.x + other_detection.width)
31
+ yB = min(self.y + self.height, other_detection.y +
32
+ other_detection.height)
33
+
34
+ interArea = max(xB - xA, 0) * max(yB - yA, 0)
35
+ union = float(self._get_area() + other_detection._get_area() - interArea) # pylint: disable=protected-access
36
+ if union == 0:
37
+ print("WARNING: something went wrong while calculating iou")
38
+ return 0
39
+
40
+ return interArea / union
41
+
42
+ def _get_area(self) -> int:
43
+ return self.width * self.height
44
+
45
+ def __str__(self):
46
+ return f'x:{int(self.x)} y: {int(self.y)}, w: {int(self.width)} h: {int(self.height)} c: {self.confidence:.2f} -> {self.category_name}'
47
+
48
+
49
+ @dataclass(**KWONLY_SLOTS)
50
+ class PointDetection():
51
+ """Coordinates according to COCO format. x,y is the center of the point.
52
+ x increases to the right, y increases downwards."""
53
+ category_name: str = field(metadata={'description': 'Category name'})
54
+ x: float = field(metadata={'description': 'X coordinate (right)'})
55
+ y: float = field(metadata={'description': 'Y coordinate (down)'})
56
+ model_name: str = field(metadata={'description': 'Model name'})
57
+ confidence: float = field(metadata={'description': 'Confidence'})
58
+ category_id: Optional[str] = field(default=None, metadata={'description': 'Category UUID'})
59
+
60
+ def distance(self, other: 'PointDetection') -> float:
61
+ return np.sqrt((other.x - self.x)**2 + (other.y - self.y)**2)
62
+
63
+ def __str__(self):
64
+ return f'x:{int(self.x)} y: {int(self.y)}, c: {self.confidence:.2f} -> {self.category_name}'
65
+
66
+
67
+ @dataclass(**KWONLY_SLOTS)
68
+ class ClassificationDetection():
69
+ category_name: str = field(metadata={'description': 'Category name'})
70
+ model_name: str = field(metadata={'description': 'Model name'})
71
+ confidence: float = field(metadata={'description': 'Confidence'})
72
+ category_id: Optional[str] = field(default=None, metadata={'description': 'Category UUID'})
73
+
74
+ def __str__(self):
75
+ return f'c: {self.confidence:.2f} -> {self.category_name}'
76
+
77
+
78
+ @dataclass(**KWONLY_SLOTS)
79
+ class Point():
80
+ x: int
81
+ y: int
82
+
83
+ def __str__(self):
84
+ return f'x:{self.x} y: {self.y}'
85
+
86
+
87
+ @dataclass(**KWONLY_SLOTS)
88
+ class Shape():
89
+ points: List[Point]
90
+
91
+ def __str__(self):
92
+ return ', '. join([str(s) for s in self.points])
93
+
94
+
95
+ @dataclass(**KWONLY_SLOTS)
96
+ class SegmentationDetection():
97
+ category_name: str
98
+ shape: Union[Shape, str]
99
+ model_name: str
100
+ confidence: float
101
+ category_id: Optional[str] = None
102
+
103
+ def __str__(self):
104
+ return f'shape:{str(self.shape)}, c: {self.confidence:.2f} -> {self.category_name}'
105
+
106
+
107
+ @dataclass(**KWONLY_SLOTS)
108
+ class Detections():
109
+ box_detections: List[BoxDetection] = field(default_factory=list, metadata={
110
+ 'description': 'List of box detections'})
111
+ point_detections: List[PointDetection] = field(default_factory=list, metadata={
112
+ 'description': 'List of point detections'})
113
+ segmentation_detections: List[SegmentationDetection] = field(default_factory=list, metadata={
114
+ 'description': 'List of segmentation detections'})
115
+ classification_detections: List[ClassificationDetection] = field(default_factory=list, metadata={
116
+ 'description': 'List of classification detections'})
117
+
118
+ image_id: Optional[str] = field(default=None, metadata={
119
+ 'description': 'Image uuid'})
120
+
121
+ def __len__(self):
122
+ return len(self.box_detections) + len(self.point_detections) + len(self.segmentation_detections) + len(self.classification_detections)
123
+
124
+
125
+ # TODO make dataclass
126
+ class Observation():
127
+
128
+ def __init__(self, detection: Union[BoxDetection, PointDetection, SegmentationDetection, ClassificationDetection]):
129
+ self.detection = detection
130
+ self.last_seen = datetime.now()
131
+
132
+ def update_last_seen(self):
133
+ self.last_seen = datetime.now()
134
+
135
+ def is_older_than(self, forget_time_in_seconds):
136
+ return self.last_seen < datetime.now() - timedelta(seconds=forget_time_in_seconds)
@@ -0,0 +1,187 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import sys
5
+ from dataclasses import asdict, dataclass, field
6
+ from enum import Enum
7
+ from typing import Dict, List, Optional, Union
8
+
9
+ from dacite import from_dict
10
+
11
+ from ..enums import CategoryType
12
+
13
+ KWONLY_SLOTS = {'kw_only': True, 'slots': True} if sys.version_info >= (3, 10) else {}
14
+
15
+
16
+ @dataclass(**KWONLY_SLOTS)
17
+ class Category():
18
+ id: str = field(metadata={"description": "The uuid of the category."})
19
+ name: str = field(metadata={"description": "The name of the category."})
20
+ description: Optional[str] = field(default=None, metadata={
21
+ "description": "An optional description of the category."})
22
+ hotkey: Optional[str] = field(default=None, metadata={
23
+ "description": "The key shortcut of the category when annotating in the Learning Loop UI."})
24
+ color: Optional[str] = field(default=None, metadata={
25
+ "description": "The color of the category when displayed in the Learning Loop UI."})
26
+ point_size: Optional[int] = field(default=None, metadata={
27
+ "description": "The point size of the category in pixels. Represents the uncertainty of the category."})
28
+ type: Optional[Union[CategoryType, str]] = field(default=None, metadata={
29
+ "description": "The type of the category",
30
+ "example": "box, point, segmentation, classification"})
31
+
32
+ @staticmethod
33
+ def from_list(values: List[dict]) -> List['Category']:
34
+ return [from_dict(data_class=Category, data=value) for value in values]
35
+
36
+
37
+ @dataclass(**KWONLY_SLOTS)
38
+ class Context():
39
+ organization: str
40
+ project: str
41
+
42
+
43
+ # pylint: disable=no-name-in-module
44
+
45
+
46
+ @dataclass(**KWONLY_SLOTS)
47
+ class ModelInformation():
48
+ id: str = field(metadata={"description": "The uuid of the model."})
49
+ host: Optional[str] = field(metadata={"description": "The hostname that started the training.",
50
+ "example": "learning-loop.ai"})
51
+ organization: str = field(metadata={"description": "The owner organization of the model."})
52
+ project: str = field(metadata={"description": "The project of the model."})
53
+ version: str = field(metadata={"description": "The version of the model."})
54
+ categories: List[Category] = field(default_factory=list, metadata={
55
+ "description": "The categories used in the model."})
56
+ resolution: Optional[int] = field(default=None, metadata={
57
+ "description": "The resolution of the model (width and height of the image after preprocessing in pixels)."})
58
+ model_root_path: Optional[str] = field(
59
+ default=None, metadata={"description": "The path of the parent directory of the model in the file system."})
60
+ model_size: Optional[str] = field(default=None, metadata={
61
+ "description": "The size of the model (i.e. the specification or variant of the model architecture)."})
62
+
63
+ @property
64
+ def context(self):
65
+ return Context(organization=self.organization, project=self.project)
66
+
67
+ @staticmethod
68
+ def load_from_disk(model_root_path: str) -> Optional['ModelInformation']:
69
+ """Load model.json from model_root_path and return ModelInformation object.
70
+ """
71
+ model_info_file_path = f'{model_root_path}/model.json'
72
+ if not os.path.exists(model_info_file_path):
73
+ logging.warning('could not find model information file %s', model_info_file_path)
74
+ return None
75
+ with open(model_info_file_path, 'r') as f:
76
+ try:
77
+ content = json.load(f)
78
+ except Exception as exc:
79
+ raise Exception(f"could not read model information from file '{model_info_file_path}'") from exc
80
+ try:
81
+ model_information = from_dict(data_class=ModelInformation, data=content)
82
+ model_information.model_root_path = model_root_path
83
+ except Exception as exc:
84
+ raise Exception(
85
+ f"could not parse model information from file '{model_info_file_path}'. \n {str(exc)}") from exc
86
+
87
+ return model_information
88
+
89
+ def save(self):
90
+ if not self.model_root_path:
91
+ raise Exception("model_root_path is not set")
92
+ with open(self.model_root_path + '/model.json', 'w') as f:
93
+ self_as_dict = asdict(self)
94
+ del self_as_dict['model_root_path']
95
+ f.write(json.dumps(self_as_dict))
96
+
97
+ @staticmethod
98
+ def from_dict(data: Dict) -> 'ModelInformation':
99
+ return from_dict(ModelInformation, data=data)
100
+
101
+
102
+ @dataclass(**KWONLY_SLOTS)
103
+ class AboutResponse:
104
+ operation_mode: str = field(metadata={"description": "The operation mode of the detector node"})
105
+ state: Optional[str] = field(metadata={
106
+ "description": "The state of the detector node",
107
+ "example": "idle, online, detecting"})
108
+ model_info: Optional[ModelInformation] = field(metadata={
109
+ "description": "Information about the model of the detector node"})
110
+ target_model: Optional[str] = field(metadata={"description": "The target model of the detector node"})
111
+ version_control: str = field(metadata={
112
+ "description": "The version control mode of the detector node",
113
+ "example": "follow_loop, specific_version, pause"})
114
+
115
+
116
+ @dataclass(**KWONLY_SLOTS)
117
+ class ModelVersionResponse:
118
+ current_version: str = field(metadata={"description": "The version of the model currently used by the detector."})
119
+ target_version: str = field(metadata={"description": "The target model version set in the detector."})
120
+ loop_version: str = field(metadata={"description": "The target model version specified by the loop."})
121
+ local_versions: List[str] = field(metadata={"description": "The locally available versions of the model."})
122
+ version_control: str = field(metadata={"description": "The version control mode."})
123
+
124
+
125
+ @dataclass(**KWONLY_SLOTS)
126
+ class ErrorConfiguration():
127
+ begin_training: Optional[bool] = False
128
+ save_model: Optional[bool] = False
129
+ get_new_model: Optional[bool] = False
130
+ crash_training: Optional[bool] = False
131
+
132
+
133
+ # pylint: disable=no-name-in-module
134
+
135
+
136
+ class NodeState(str, Enum):
137
+ Idle = "idle"
138
+ Offline = "offline"
139
+ Online = "online"
140
+ Preparing = "preparing"
141
+ Running = "running"
142
+ Stopping = "stopping"
143
+ Detecting = 'detecting'
144
+ Uploading = 'uploading'
145
+
146
+
147
+ @dataclass(**KWONLY_SLOTS)
148
+ class NodeStatus():
149
+ id: str
150
+ name: str
151
+ state: NodeState = NodeState.Online
152
+ uptime: int = 0
153
+ errors: Dict = field(default_factory=dict)
154
+ capabilities: List[str] = field(default_factory=list)
155
+
156
+ def set_error(self, key: str, value: str):
157
+ self.errors[key] = value
158
+
159
+ def reset_error(self, key: str):
160
+ try:
161
+ del self.errors[key]
162
+ except AttributeError:
163
+ pass
164
+ except KeyError:
165
+ pass
166
+
167
+ def reset_all_errors(self):
168
+ for key in list(self.errors.keys()):
169
+ self.reset_error(key)
170
+
171
+
172
+ @dataclass(**KWONLY_SLOTS)
173
+ class AnnotationNodeStatus(NodeStatus):
174
+ capabilities: List[str] = field(default_factory=list)
175
+
176
+
177
+ @dataclass(**KWONLY_SLOTS)
178
+ class DetectorStatus():
179
+ uuid: str
180
+ name: str
181
+ state: NodeState
182
+ uptime: int
183
+ model_format: str
184
+ current_model: Optional[str]
185
+ target_model: Optional[str]
186
+ errors: Dict
187
+ operation_mode: str
@@ -0,0 +1,56 @@
1
+
2
+ import sys
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from typing import List, Optional
6
+
7
+ from .annotations import BoxAnnotation, ClassificationAnnotation, PointAnnotation
8
+ from .detections import (
9
+ BoxDetection,
10
+ ClassificationDetection,
11
+ PointDetection,
12
+ SegmentationDetection,
13
+ )
14
+
15
+ # pylint: disable=too-many-instance-attributes
16
+
17
+ KWONLY_SLOTS = {'kw_only': True, 'slots': True} if sys.version_info >= (3, 10) else {}
18
+
19
+
20
+ def current_datetime():
21
+ return datetime.now().isoformat(sep='_', timespec='milliseconds')
22
+
23
+
24
+ @dataclass(**KWONLY_SLOTS)
25
+ class ImageMetadata():
26
+ box_detections: List[BoxDetection] = field(default_factory=list, metadata={
27
+ 'description': 'List of box detections'})
28
+ point_detections: List[PointDetection] = field(default_factory=list, metadata={
29
+ 'description': 'List of point detections'})
30
+ segmentation_detections: List[SegmentationDetection] = field(default_factory=list, metadata={
31
+ 'description': 'List of segmentation detections'})
32
+ classification_detections: List[ClassificationDetection] = field(default_factory=list, metadata={
33
+ 'description': 'List of classification detections'})
34
+
35
+ box_annotations: List[BoxAnnotation] = field(default_factory=list, metadata={
36
+ 'description': 'List of box annotations'})
37
+ point_annotations: List[PointAnnotation] = field(default_factory=list, metadata={
38
+ 'description': 'List of point annotations'})
39
+ classification_annotation: Optional[ClassificationAnnotation] = field(default=None, metadata={
40
+ 'description': 'Classification annotation'})
41
+
42
+ tags: List[str] = field(default_factory=list, metadata={
43
+ 'description': 'List of tags'})
44
+
45
+ created: Optional[str] = field(default_factory=current_datetime, metadata={
46
+ 'description': 'Creation date of the image'})
47
+ source: Optional[str] = field(default=None, metadata={
48
+ 'description': 'Source of the image'})
49
+
50
+ def __len__(self):
51
+ return len(self.box_detections) + len(self.point_detections) + len(self.segmentation_detections) + len(self.classification_detections)
52
+
53
+
54
+ @dataclass(**KWONLY_SLOTS)
55
+ class ImagesMetadata():
56
+ items: List[ImageMetadata] = field(default_factory=list, metadata={'description': 'List of image metadata'})