learning-loop-node 0.18.3__tar.gz → 0.18.5__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.
- {learning_loop_node-0.18.3 → learning_loop_node-0.18.5}/PKG-INFO +3 -5
- learning_loop_node-0.18.5/learning_loop_node/annotation/annotator_logic.py +29 -0
- learning_loop_node-0.18.5/learning_loop_node/annotation/annotator_node.py +116 -0
- learning_loop_node-0.18.5/learning_loop_node/data_classes/__init__.py +50 -0
- learning_loop_node-0.18.5/learning_loop_node/data_classes/annotation_data.py +43 -0
- learning_loop_node-0.18.5/learning_loop_node/data_classes/annotations.py +44 -0
- learning_loop_node-0.18.5/learning_loop_node/data_classes/detections.py +136 -0
- learning_loop_node-0.18.5/learning_loop_node/data_classes/general.py +187 -0
- learning_loop_node-0.18.5/learning_loop_node/data_classes/image_metadata.py +56 -0
- learning_loop_node-0.18.5/learning_loop_node/data_classes/socket_response.py +24 -0
- learning_loop_node-0.18.5/learning_loop_node/data_classes/training.py +168 -0
- learning_loop_node-0.18.5/learning_loop_node/detector/__init__.py +0 -0
- learning_loop_node-0.18.5/learning_loop_node/detector/detector_logic.py +80 -0
- learning_loop_node-0.18.5/learning_loop_node/detector/detector_node.py +699 -0
- learning_loop_node-0.18.5/learning_loop_node/detector/exceptions.py +7 -0
- learning_loop_node-0.18.5/learning_loop_node/detector/inbox_filter/__init__.py +0 -0
- learning_loop_node-0.18.5/learning_loop_node/detector/inbox_filter/cam_observation_history.py +82 -0
- learning_loop_node-0.18.5/learning_loop_node/detector/inbox_filter/relevance_filter.py +40 -0
- learning_loop_node-0.18.5/learning_loop_node/detector/outbox.py +323 -0
- learning_loop_node-0.18.5/learning_loop_node/detector/rest/__init__.py +0 -0
- learning_loop_node-0.18.5/learning_loop_node/detector/rest/about.py +26 -0
- learning_loop_node-0.18.5/learning_loop_node/detector/rest/backdoor_controls.py +37 -0
- learning_loop_node-0.18.5/learning_loop_node/detector/rest/detect.py +54 -0
- learning_loop_node-0.18.5/learning_loop_node/detector/rest/model_version_control.py +46 -0
- learning_loop_node-0.18.5/learning_loop_node/detector/rest/operation_mode.py +54 -0
- learning_loop_node-0.18.5/learning_loop_node/detector/rest/outbox_mode.py +41 -0
- learning_loop_node-0.18.5/learning_loop_node/detector/rest/upload.py +44 -0
- learning_loop_node-0.18.5/learning_loop_node/enums/__init__.py +6 -0
- learning_loop_node-0.18.5/learning_loop_node/enums/annotator.py +11 -0
- learning_loop_node-0.18.5/learning_loop_node/enums/detector.py +18 -0
- learning_loop_node-0.18.5/learning_loop_node/enums/general.py +9 -0
- learning_loop_node-0.18.5/learning_loop_node/enums/trainer.py +22 -0
- learning_loop_node-0.18.5/learning_loop_node/helpers/__init__.py +0 -0
- learning_loop_node-0.18.5/learning_loop_node/helpers/background_tasks.py +77 -0
- learning_loop_node-0.18.5/learning_loop_node/helpers/environment_reader.py +49 -0
- learning_loop_node-0.18.5/learning_loop_node/helpers/gdrive_downloader.py +43 -0
- learning_loop_node-0.18.5/learning_loop_node/helpers/log_conf.py +39 -0
- learning_loop_node-0.18.5/learning_loop_node/helpers/misc.py +250 -0
- learning_loop_node-0.18.5/learning_loop_node/helpers/run.py +21 -0
- learning_loop_node-0.18.5/learning_loop_node/py.typed +0 -0
- learning_loop_node-0.18.5/learning_loop_node/trainer/__init__.py +0 -0
- learning_loop_node-0.18.5/learning_loop_node/trainer/downloader.py +31 -0
- learning_loop_node-0.18.5/learning_loop_node/trainer/exceptions.py +12 -0
- learning_loop_node-0.18.5/learning_loop_node/trainer/executor.py +109 -0
- learning_loop_node-0.18.5/learning_loop_node/trainer/io_helpers.py +181 -0
- learning_loop_node-0.18.5/learning_loop_node/trainer/rest/__init__.py +0 -0
- learning_loop_node-0.18.5/learning_loop_node/trainer/rest/backdoor_controls.py +120 -0
- learning_loop_node-0.18.5/learning_loop_node/trainer/test_executor.py +66 -0
- learning_loop_node-0.18.5/learning_loop_node/trainer/trainer_logic.py +192 -0
- learning_loop_node-0.18.5/learning_loop_node/trainer/trainer_logic_generic.py +560 -0
- learning_loop_node-0.18.5/learning_loop_node/trainer/trainer_node.py +106 -0
- {learning_loop_node-0.18.3 → learning_loop_node-0.18.5}/learning_loop_node.egg-info/PKG-INFO +3 -5
- learning_loop_node-0.18.5/learning_loop_node.egg-info/SOURCES.txt +64 -0
- {learning_loop_node-0.18.3 → learning_loop_node-0.18.5}/learning_loop_node.egg-info/requires.txt +1 -1
- {learning_loop_node-0.18.3 → learning_loop_node-0.18.5}/pyproject.toml +7 -6
- learning_loop_node-0.18.3/learning_loop_node.egg-info/SOURCES.txt +0 -15
- learning_loop_node-0.18.3/setup.py +0 -10
- {learning_loop_node-0.18.3 → learning_loop_node-0.18.5}/README.md +0 -0
- {learning_loop_node-0.18.3 → learning_loop_node-0.18.5}/learning_loop_node/__init__.py +0 -0
- /learning_loop_node-0.18.3/learning_loop_node/py.typed → /learning_loop_node-0.18.5/learning_loop_node/annotation/__init__.py +0 -0
- {learning_loop_node-0.18.3 → learning_loop_node-0.18.5}/learning_loop_node/data_exchanger.py +0 -0
- {learning_loop_node-0.18.3 → learning_loop_node-0.18.5}/learning_loop_node/globals.py +0 -0
- {learning_loop_node-0.18.3 → learning_loop_node-0.18.5}/learning_loop_node/loop_communication.py +0 -0
- {learning_loop_node-0.18.3 → learning_loop_node-0.18.5}/learning_loop_node/node.py +0 -0
- {learning_loop_node-0.18.3 → learning_loop_node-0.18.5}/learning_loop_node/rest.py +0 -0
- {learning_loop_node-0.18.3 → learning_loop_node-0.18.5}/learning_loop_node.egg-info/dependency_links.txt +0 -0
- {learning_loop_node-0.18.3 → learning_loop_node-0.18.5}/learning_loop_node.egg-info/top_level.txt +0 -0
- {learning_loop_node-0.18.3 → learning_loop_node-0.18.5}/setup.cfg +0 -0
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learning_loop_node
|
|
3
|
-
Version: 0.18.
|
|
3
|
+
Version: 0.18.5
|
|
4
4
|
Summary: Python Library for Nodes which connect to the Zauberzeug Learning Loop
|
|
5
|
-
Author: Zauberzeug
|
|
6
5
|
Author-email: Zauberzeug GmbH <info@zauberzeug.com>
|
|
7
6
|
License: MIT
|
|
8
7
|
Project-URL: Repository, https://github.com/zauberzeug/learning_loop_node
|
|
9
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.10
|
|
10
9
|
Description-Content-Type: text/markdown
|
|
11
10
|
Requires-Dist: fastapi<1.0,>=0.128
|
|
12
11
|
Requires-Dist: uvicorn[standard]<0.23.0,>=0.22.0
|
|
@@ -18,7 +17,7 @@ Requires-Dist: icecream<3.0.0,>=2.1.0
|
|
|
18
17
|
Requires-Dist: aiofiles<0.8.0,>=0.7.0
|
|
19
18
|
Requires-Dist: async_generator<2.0.0,>=1.10
|
|
20
19
|
Requires-Dist: pytest-watch<5.0.0,>=4.2.0
|
|
21
|
-
Requires-Dist: python-multipart
|
|
20
|
+
Requires-Dist: python-multipart>=0.0.22
|
|
22
21
|
Requires-Dist: psutil<6.0.0,>=5.8.0
|
|
23
22
|
Requires-Dist: numpy<3.0,>=2.0
|
|
24
23
|
Requires-Dist: Pillow<12.0.0,>=11.0.0
|
|
@@ -34,7 +33,6 @@ Requires-Dist: retry<0.10.0,>=0.9.2; extra == "dev"
|
|
|
34
33
|
Requires-Dist: debugpy<2.0.0,>=1.6.7.post1; extra == "dev"
|
|
35
34
|
Requires-Dist: pytest-asyncio<0.22.0,>=0.21.1; extra == "dev"
|
|
36
35
|
Requires-Dist: autopep8<3.0.0,>=2.0.2; extra == "dev"
|
|
37
|
-
Dynamic: author
|
|
38
36
|
|
|
39
37
|
# Learning Loop Node
|
|
40
38
|
|
|
@@ -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
|