Skip to content

Commit

Permalink
add sio eps to control detector versioning mode (#46)
Browse files Browse the repository at this point in the history
also adds an sio ep that returns the full 'about' information

BREAKING CHANGE: Responses of detectors SIO endpoints are now always
dicts (recommended by AI for interface consisstency)

---------

Co-authored-by: Niklas Neugebauer <[email protected]>
  • Loading branch information
denniswittich and NiklasNeugebauer authored Dec 6, 2024
1 parent f28d540 commit dbda78d
Show file tree
Hide file tree
Showing 33 changed files with 280 additions and 213 deletions.
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@ The detector also has a sio **upload endpoint** that can be used to upload image

The endpoint returns None if the upload was successful and an error message otherwise.

### Changing the model version
### Changing the model versioning mode

The detector can be configured to one of the following behaviors:

- download use a specific model version
- use a specific model version
- automatically update the model version according to the learning loop deployment target
- pause the model updates and use the version that was last loaded

Expand All @@ -84,6 +84,15 @@ The model versioning configuration can be accessed/changed via a REST endpoint.
Note that the configuration is not persistent, however, the default behavior on startup can be configured via the environment variable `VERSION_CONTROL_DEFAULT`.
If the environment variable is set to `VERSION_CONTROL_DEFAULT=PAUSE`, the detector will pause the model updates on startup. Otherwise, the detector will automatically follow the loop deployment target.

The model versioning configuration can also be changed via a socketio event:

- Configure the detector to use a specific model version: `sio.emit('set_model_version_mode', '1.0')`
- Configure the detector to automatically update the model version: `sio.emit('set_model_version_mode', 'follow_loop')`
- Pause the model updates: `sio.emit('set_model_version_mode', 'pause')`

There is also a GET endpoint to fetch the current model versioning configuration:
`sio.emit('get_model_version')` or `curl http://localhost/model_version`

### Changing the outbox mode

If the autoupload is set to `all` or `filtered` (selected) images and the corresponding detections are saved on HDD (the outbox). A background thread will upload the images and detections to the Learning Loop. The outbox is located in the `outbox` folder in the root directory of the node. The outbox can be cleared by deleting the files in the folder.
Expand All @@ -98,6 +107,16 @@ Example Usage:
The current state can be queried via a GET request:
`curl http://localhost/outbox_mode`

Alternatively, the outbox mode can be changed via a socketio event:

- Enable upload: `sio.emit('set_outbox_mode', 'continuous_upload')`
- Disable upload: `sio.emit('set_outbox_mode', 'stopped')`

The outbox mode can also be queried via:

- HTTP: `curl http://localhost/outbox_mode`
- SocketIO: `sio.emit('get_outbox_mode')`

### Explicit upload

The detector has a REST endpoint to upload images (and detections) to the Learning Loop. The endpoint takes a POST request with the image and optionally the detections. The image is expected to be in jpg format. The detections are expected to be a json dictionary. Example:
Expand Down
8 changes: 4 additions & 4 deletions demo_segmentation_tool/app_code/annotation_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@
import numpy as np

from learning_loop_node.annotation.annotator_logic import AnnotatorLogic
from learning_loop_node.data_classes import (AnnotationEventType, Point,
SegmentationAnnotation, Shape,
ToolOutput, UserInput)
from learning_loop_node.data_classes import Point, SegmentationAnnotation, Shape, ToolOutput, UserInput
from learning_loop_node.enums import AnnotationEventType

KWONLY_SLOTS = {'kw_only': True, 'slots': True} if sys.version_info >= (3, 10) else {}

Expand Down Expand Up @@ -63,7 +62,8 @@ def create_path(pixel: List[Point]) -> str:


class SegmentationTool(AnnotatorLogic):
async def handle_user_input(self, user_input: UserInput, history: History) -> ToolOutput:
# TODO: fix signature
async def handle_user_input(self, user_input: UserInput, history: History) -> ToolOutput: # type: ignore
coordinate = user_input.data.coordinate
output = ToolOutput(svg="", annotation=None)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from learning_loop_node.data_classes import (AnnotationData,
AnnotationEventType, Category,
CategoryType, Context, Point,
UserInput)
from learning_loop_node.data_classes import AnnotationData, Category, Context, Point, UserInput
from learning_loop_node.enums import AnnotationEventType, CategoryType


class MockAsyncClient(): # pylint: disable=too-few-public-methods
Expand Down
17 changes: 8 additions & 9 deletions learning_loop_node/data_classes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
from .annotations import AnnotationData, AnnotationEventType, SegmentationAnnotation, ToolOutput, UserInput
from .annotations import AnnotationData, SegmentationAnnotation, ToolOutput, UserInput
from .detections import (BoxDetection, ClassificationDetection, Detections, Observation, Point, PointDetection,
SegmentationDetection, Shape)
from .general import (AnnotationNodeStatus, Category, CategoryType, Context, DetectionStatus, ErrorConfiguration,
ModelInformation, NodeState, NodeStatus)
from .general import (AboutResponse, AnnotationNodeStatus, Category, Context, DetectionStatus, ErrorConfiguration,
ModelInformation, ModelVersionResponse, NodeState, NodeStatus)
from .image_metadata import ImageMetadata
from .socket_response import SocketResponse
from .training import (Errors, PretrainedModel, TrainerState, Training, TrainingError, TrainingOut, TrainingStateData,
TrainingStatus)
from .training import Errors, PretrainedModel, Training, TrainingError, TrainingOut, TrainingStateData, TrainingStatus

__all__ = [
'AnnotationData', 'AnnotationEventType', 'SegmentationAnnotation', 'ToolOutput', 'UserInput',
'AboutResponse', 'AnnotationData', 'SegmentationAnnotation', 'ToolOutput', 'UserInput',
'BoxDetection', 'ClassificationDetection', 'ImageMetadata', 'Observation', 'Point', 'PointDetection',
'SegmentationDetection', 'Shape', 'Detections',
'AnnotationNodeStatus', 'Category', 'CategoryType', 'Context', 'DetectionStatus', 'ErrorConfiguration',
'ModelInformation', 'NodeState', 'NodeStatus',
'AnnotationNodeStatus', 'Category', 'Context', 'DetectionStatus', 'ErrorConfiguration',
'ModelInformation', 'NodeState', 'NodeStatus', 'ModelVersionResponse',
'SocketResponse',
'Errors', 'PretrainedModel', 'TrainerState', 'Training',
'Errors', 'PretrainedModel', 'Training',
'TrainingError', 'TrainingOut', 'TrainingStateData', 'TrainingStatus',
]
12 changes: 1 addition & 11 deletions learning_loop_node/data_classes/annotations.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,14 @@
import sys
from dataclasses import dataclass
from enum import Enum
from typing import Optional, Union

from ..enums import AnnotationEventType
from .detections import Point, Shape
from .general import Category, Context

KWONLY_SLOTS = {'kw_only': True, 'slots': True} if sys.version_info >= (3, 10) else {}


class AnnotationEventType(str, Enum):
LeftMouseDown = 'left_mouse_down'
RightMouseDown = 'right_mouse_down'
MouseMove = 'mouse_move'
LeftMouseUp = 'left_mouse_up'
RightMouseUp = 'right_mouse_up'
KeyUp = 'key_up'
KeyDown = 'key_down'


@dataclass(**KWONLY_SLOTS)
class AnnotationData():
coordinate: Point
Expand Down
32 changes: 25 additions & 7 deletions learning_loop_node/data_classes/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,9 @@

from dacite import from_dict

KWONLY_SLOTS = {'kw_only': True, 'slots': True} if sys.version_info >= (3, 10) else {}

from ..enums import CategoryType

class CategoryType(str, Enum):
Box = 'box'
Point = 'point'
Segmentation = 'segmentation'
Classification = 'classification'
KWONLY_SLOTS = {'kw_only': True, 'slots': True} if sys.version_info >= (3, 10) else {}


@dataclass(**KWONLY_SLOTS)
Expand Down Expand Up @@ -104,6 +99,29 @@ def from_dict(data: Dict) -> 'ModelInformation':
return from_dict(ModelInformation, data=data)


@dataclass(**KWONLY_SLOTS)
class AboutResponse:
operation_mode: str = field(metadata={"description": "The operation mode of the detector node"})
state: Optional[str] = field(metadata={
"description": "The state of the detector node",
"example": "idle, online, detecting"})
model_info: Optional[ModelInformation] = field(metadata={
"description": "Information about the model of the detector node"})
target_model: Optional[str] = field(metadata={"description": "The target model of the detector node"})
version_control: str = field(metadata={
"description": "The version control mode of the detector node",
"example": "follow_loop, specific_version, pause"})


@dataclass(**KWONLY_SLOTS)
class ModelVersionResponse:
current_version: str = field(metadata={"description": "The version of the model currently used by the detector."})
target_version: str = field(metadata={"description": "The target model version set in the detector."})
loop_version: str = field(metadata={"description": "The target model version specified by the loop."})
local_versions: List[str] = field(metadata={"description": "The locally available versions of the model."})
version_control: str = field(metadata={"description": "The version control mode."})


@dataclass(**KWONLY_SLOTS)
class ErrorConfiguration():
begin_training: Optional[bool] = False
Expand Down
22 changes: 1 addition & 21 deletions learning_loop_node/data_classes/training.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
import sys
import time
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional
from uuid import uuid4

from ..enums import TrainerState
from ..helpers.misc import create_image_folder, create_training_folder
# pylint: disable=no-name-in-module
from .general import Category, Context
Expand All @@ -21,26 +21,6 @@ class PretrainedModel():
description: str


class TrainerState(str, Enum):
Idle = 'idle'
Initialized = 'initialized'
Preparing = 'preparing'
DataDownloading = 'data_downloading'
DataDownloaded = 'data_downloaded'
TrainModelDownloading = 'train_model_downloading'
TrainModelDownloaded = 'train_model_downloaded'
TrainingRunning = 'running'
TrainingFinished = 'training_finished'
ConfusionMatrixSyncing = 'confusion_matrix_syncing'
ConfusionMatrixSynced = 'confusion_matrix_synced'
TrainModelUploading = 'train_model_uploading'
TrainModelUploaded = 'train_model_uploaded'
Detecting = 'detecting'
Detected = 'detected'
DetectionUploading = 'detection_uploading'
ReadyForCleanup = 'ready_for_cleanup'


@dataclass(**KWONLY_SLOTS)
class TrainingStatus():
id: str # NOTE this must not be changed, but tests wont detect a change -> update tests!
Expand Down
Loading

0 comments on commit dbda78d

Please sign in to comment.