lightly-studio 0.3.1__py3-none-any.whl → 0.3.3__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 lightly-studio might be problematic. Click here for more details.
- lightly_studio/__init__.py +4 -4
- lightly_studio/api/app.py +7 -5
- lightly_studio/api/db_tables.py +0 -3
- lightly_studio/api/routes/api/annotation.py +32 -16
- lightly_studio/api/routes/api/annotation_label.py +2 -5
- lightly_studio/api/routes/api/annotations/__init__.py +7 -0
- lightly_studio/api/routes/api/annotations/create_annotation.py +52 -0
- lightly_studio/api/routes/api/classifier.py +2 -5
- lightly_studio/api/routes/api/dataset.py +5 -8
- lightly_studio/api/routes/api/dataset_tag.py +2 -3
- lightly_studio/api/routes/api/embeddings2d.py +104 -0
- lightly_studio/api/routes/api/export.py +73 -0
- lightly_studio/api/routes/api/metadata.py +2 -4
- lightly_studio/api/routes/api/sample.py +5 -13
- lightly_studio/api/routes/api/selection.py +87 -0
- lightly_studio/api/routes/api/settings.py +2 -6
- lightly_studio/api/routes/images.py +6 -6
- lightly_studio/core/add_samples.py +374 -0
- lightly_studio/core/dataset.py +272 -400
- lightly_studio/core/dataset_query/boolean_expression.py +67 -0
- lightly_studio/core/dataset_query/dataset_query.py +216 -0
- lightly_studio/core/dataset_query/field.py +113 -0
- lightly_studio/core/dataset_query/field_expression.py +79 -0
- lightly_studio/core/dataset_query/match_expression.py +23 -0
- lightly_studio/core/dataset_query/order_by.py +79 -0
- lightly_studio/core/dataset_query/sample_field.py +28 -0
- lightly_studio/core/dataset_query/tags_expression.py +46 -0
- lightly_studio/core/sample.py +159 -32
- lightly_studio/core/start_gui.py +35 -0
- lightly_studio/dataset/edge_embedding_generator.py +13 -8
- lightly_studio/dataset/embedding_generator.py +2 -3
- lightly_studio/dataset/embedding_manager.py +74 -6
- lightly_studio/dataset/env.py +4 -0
- lightly_studio/dataset/file_utils.py +13 -2
- lightly_studio/dataset/fsspec_lister.py +275 -0
- lightly_studio/dataset/loader.py +49 -84
- lightly_studio/dataset/mobileclip_embedding_generator.py +9 -6
- lightly_studio/db_manager.py +145 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/0.CA_CXIBb.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_layout.DS78jgNY.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/index.BVs_sZj9.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/transform.D487hwJk.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/6t3IJ0vQ.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{D6su9Aln.js → 8NsknIT2.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{x9G_hzyY.js → BND_-4Kp.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{BylOuP6i.js → BdfTHw61.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{DOlTMNyt.js → BfHVnyNT.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BjkP1AHA.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BuuNVL9G.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{O-EABkf9.js → BzKGpnl4.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CCx7Ho51.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{l7KrR96u.js → CH6P3X75.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{D5-A_Ffd.js → CR2upx_Q.js} +2 -2
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CWPZrTTJ.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{C8I8rFJQ.js → Cs1XmhiF.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{CDnpyLsT.js → CwPowJfP.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CxFKfZ9T.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Cxevwdid.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{DjfY96ND.js → D4whDBUi.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6r9vr07.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DA6bFLPR.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DEgUu98i.js +3 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DGTPl6Gk.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DKGxBSlK.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DQXoLcsF.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DQe_kdRt.js +92 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DcY4jgG3.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{Bu7uvVrG.js → RmD8FzRo.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/V-MnMC1X.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{Bsi3UGy5.js → keKYsoph.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/app.BVr6DYqP.js +2 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/start.u7zsVvqp.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/0.Da2agmdd.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{1.B4rNYwVp.js → 1.B11tVRJV.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/10.l30Zud4h.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/12.CgKPGcAP.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/2.C8HLK8mj.js +857 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{3.CWHpKonm.js → 3.CLvg3QcJ.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{4.OUWOLQeV.js → 4.BQhDtXUI.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/5.-6XqWX5G.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/6.uBV1Lhat.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/7.BXsgoQZh.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/8.BkbcnUs8.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{9.CPu3CiBc.js → 9.Bkrv-Vww.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/workers/clustering.worker-DKqeLtG0.js +2 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/workers/search.worker-vNSty3B0.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/version.json +1 -1
- lightly_studio/dist_lightly_studio_view_app/index.html +14 -14
- lightly_studio/examples/example.py +13 -12
- lightly_studio/examples/example_coco.py +13 -0
- lightly_studio/examples/example_metadata.py +83 -98
- lightly_studio/examples/example_selection.py +7 -19
- lightly_studio/examples/example_split_work.py +12 -36
- lightly_studio/examples/{example_v2.py → example_yolo.py} +3 -4
- lightly_studio/export/export_dataset.py +65 -0
- lightly_studio/export/lightly_studio_label_input.py +120 -0
- lightly_studio/few_shot_classifier/classifier_manager.py +5 -26
- lightly_studio/metadata/compute_typicality.py +67 -0
- lightly_studio/models/annotation/annotation_base.py +18 -20
- lightly_studio/models/annotation/instance_segmentation.py +8 -8
- lightly_studio/models/annotation/object_detection.py +4 -4
- lightly_studio/models/dataset.py +6 -2
- lightly_studio/models/sample.py +10 -3
- lightly_studio/resolvers/annotation_label_resolver/__init__.py +2 -1
- lightly_studio/resolvers/annotation_label_resolver/get_all.py +15 -0
- lightly_studio/resolvers/annotation_resolver/__init__.py +2 -3
- lightly_studio/resolvers/annotation_resolver/create_many.py +3 -3
- lightly_studio/resolvers/annotation_resolver/delete_annotation.py +1 -1
- lightly_studio/resolvers/annotation_resolver/delete_annotations.py +7 -3
- lightly_studio/resolvers/annotation_resolver/get_by_id.py +19 -1
- lightly_studio/resolvers/annotation_resolver/update_annotation_label.py +0 -1
- lightly_studio/resolvers/annotations/annotations_filter.py +1 -11
- lightly_studio/resolvers/dataset_resolver.py +10 -0
- lightly_studio/resolvers/embedding_model_resolver.py +22 -0
- lightly_studio/resolvers/sample_resolver.py +53 -9
- lightly_studio/resolvers/tag_resolver.py +23 -0
- lightly_studio/selection/mundig.py +7 -10
- lightly_studio/selection/select.py +55 -46
- lightly_studio/selection/select_via_db.py +23 -19
- lightly_studio/selection/selection_config.py +10 -4
- lightly_studio/services/annotations_service/__init__.py +12 -0
- lightly_studio/services/annotations_service/create_annotation.py +63 -0
- lightly_studio/services/annotations_service/delete_annotation.py +22 -0
- lightly_studio/services/annotations_service/update_annotation.py +21 -32
- lightly_studio/services/annotations_service/update_annotation_bounding_box.py +36 -0
- lightly_studio-0.3.3.dist-info/METADATA +814 -0
- {lightly_studio-0.3.1.dist-info → lightly_studio-0.3.3.dist-info}/RECORD +130 -113
- lightly_studio/api/db.py +0 -133
- lightly_studio/api/routes/api/annotation_task.py +0 -38
- lightly_studio/api/routes/api/metrics.py +0 -80
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/0.DenzbfeK.css +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.OwPEPQZu.css +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.b653GmVf.css +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_layout.T-zjSUd3.css +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B2FVR0s0.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B9zumHo5.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BJXwVxaE.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bx1xMsFy.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CcaPhhk3.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CvOmgdoc.js +0 -93
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CxtLVaYz.js +0 -3
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6RI2Zrd.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D98V7j6A.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DIRAtgl0.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DjUWrjOv.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/XO7A28GO.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/hQVEETDE.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/nAHhluT7.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/r64xT6ao.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/vC4nQVEB.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/app.CjnvpsmS.js +0 -2
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/start.0o1H7wM9.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/0.XRq_TUwu.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/10.DfBwOEhN.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/12.CwF2_8mP.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/2.CS4muRY-.js +0 -6
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/5.Dm6t9F5W.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/6.Bw5ck4gK.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/7.CF0EDTR6.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/8.Cw30LEcV.js +0 -1
- lightly_studio/metrics/detection/__init__.py +0 -0
- lightly_studio/metrics/detection/map.py +0 -268
- lightly_studio/models/annotation_task.py +0 -28
- lightly_studio/resolvers/annotation_resolver/create.py +0 -19
- lightly_studio/resolvers/annotation_task_resolver.py +0 -31
- lightly_studio-0.3.1.dist-info/METADATA +0 -520
- /lightly_studio/{metrics → core/dataset_query}/__init__.py +0 -0
- /lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/{OpenSans- → OpenSans-Medium.DVUZMR_6.ttf} +0 -0
- {lightly_studio-0.3.1.dist-info → lightly_studio-0.3.3.dist-info}/WHEEL +0 -0
|
@@ -4,25 +4,21 @@ import math
|
|
|
4
4
|
|
|
5
5
|
from environs import Env
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
from lightly_studio.models.tag import TagCreate
|
|
9
|
-
from lightly_studio.resolvers import (
|
|
10
|
-
tag_resolver,
|
|
11
|
-
)
|
|
7
|
+
import lightly_studio as ls
|
|
12
8
|
|
|
13
9
|
# Read environment variables
|
|
14
10
|
env = Env()
|
|
15
11
|
env.read_env()
|
|
16
12
|
|
|
17
|
-
# Create a
|
|
18
|
-
|
|
13
|
+
# Create a Dataset instance
|
|
14
|
+
dataset = ls.Dataset.create()
|
|
19
15
|
|
|
20
16
|
# Define the path to the dataset (folder containing data.yaml)
|
|
21
17
|
dataset_path = env.path("DATASET_PATH", "/path/to/your/yolo/dataset/data.yaml")
|
|
22
18
|
|
|
23
19
|
# Load YOLO dataset using data.yaml path
|
|
24
|
-
dataset
|
|
25
|
-
str(dataset_path),
|
|
20
|
+
dataset.add_samples_from_yolo(
|
|
21
|
+
data_yaml=str(dataset_path),
|
|
26
22
|
input_split=env.str("LIGHTLY_STUDIO_DATASET_SPLIT", "test"),
|
|
27
23
|
)
|
|
28
24
|
|
|
@@ -32,36 +28,16 @@ dataset = loader.from_yolo(
|
|
|
32
28
|
# to work on.
|
|
33
29
|
reviewers = env.str("DATASET_REVIEWERS", "Alice, Bob, Charlie, David")
|
|
34
30
|
|
|
35
|
-
# Get all samples from the db
|
|
36
|
-
samples = dataset.get_samples()
|
|
37
|
-
|
|
38
31
|
# Create a tag for each reviewer to work on
|
|
39
|
-
tags = []
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
session=loader.session,
|
|
44
|
-
tag=TagCreate(
|
|
45
|
-
dataset_id=dataset.dataset_id,
|
|
46
|
-
name=f"""{reviewer.strip()} tasks""",
|
|
47
|
-
kind="sample",
|
|
48
|
-
),
|
|
49
|
-
)
|
|
50
|
-
)
|
|
32
|
+
tags = [reviewer.strip() for reviewer in reviewers.split(",")]
|
|
33
|
+
|
|
34
|
+
# Get all samples from the db
|
|
35
|
+
samples = dataset.query().to_list()
|
|
51
36
|
|
|
52
37
|
# Chunk the samples into portions equally divided among the reviewers.
|
|
53
38
|
chunk_size = math.ceil(len(samples) / len(tags))
|
|
54
|
-
for i,
|
|
55
|
-
|
|
56
|
-
sample_ids = [sample.sample_id for sample in samples[i * chunk_size : (i + 1) * chunk_size]]
|
|
57
|
-
|
|
58
|
-
# Add sample_ids to the tag
|
|
59
|
-
tag_resolver.add_sample_ids_to_tag_id(
|
|
60
|
-
session=loader.session,
|
|
61
|
-
tag_id=tag.tag_id,
|
|
62
|
-
sample_ids=sample_ids,
|
|
63
|
-
)
|
|
64
|
-
|
|
39
|
+
for i, sample in enumerate(samples):
|
|
40
|
+
sample.add_tag(tags[i // chunk_size])
|
|
65
41
|
|
|
66
42
|
# Launch the server to load data
|
|
67
|
-
|
|
43
|
+
ls.start_gui()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Example of how to
|
|
1
|
+
"""Example of how to add samples in yolo format to a dataset."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
@@ -12,10 +12,9 @@ env.read_env()
|
|
|
12
12
|
|
|
13
13
|
# Define the path to the dataset directory
|
|
14
14
|
dataset_path = Path(env.path("DATASET_PATH", "/path/to/your/dataset"))
|
|
15
|
-
dataset_path = dataset_path.parent if dataset_path.is_file() else dataset_path
|
|
16
15
|
|
|
17
16
|
# Create a DatasetLoader from a path
|
|
18
|
-
dataset = ls.Dataset(
|
|
19
|
-
dataset.
|
|
17
|
+
dataset = ls.Dataset.create()
|
|
18
|
+
dataset.add_samples_from_yolo(data_yaml=dataset_path, input_split="train")
|
|
20
19
|
|
|
21
20
|
ls.start_gui()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Exports datasets from Lightly Studio into various formats."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Iterable
|
|
5
|
+
|
|
6
|
+
from labelformat.formats import COCOObjectDetectionOutput
|
|
7
|
+
from sqlmodel import Session
|
|
8
|
+
|
|
9
|
+
from lightly_studio.core.sample import Sample
|
|
10
|
+
from lightly_studio.export.lightly_studio_label_input import LightlyStudioObjectDetectionInput
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DatasetExport:
|
|
14
|
+
"""Provides methods to export a dataset or a subset of it.
|
|
15
|
+
|
|
16
|
+
This class is typically not instantiated directly but returned by `Dataset.export()`.
|
|
17
|
+
It allows exporting data in various formats.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, session: Session, samples: Iterable[Sample]):
|
|
21
|
+
"""Initializes the DatasetExport object.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
session: The database session.
|
|
25
|
+
samples: Samples to export.
|
|
26
|
+
"""
|
|
27
|
+
self.session = session
|
|
28
|
+
self.samples = samples
|
|
29
|
+
|
|
30
|
+
def to_coco_object_detections(self, output_json: Path) -> None:
|
|
31
|
+
"""Exports object detection annotations to a COCO format JSON file.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
output_json: The path to the output COCO JSON file.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ValueError: If the annotation task with the given name does not exist.
|
|
38
|
+
"""
|
|
39
|
+
to_coco_object_detections(
|
|
40
|
+
session=self.session,
|
|
41
|
+
samples=self.samples,
|
|
42
|
+
output_json=output_json,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def to_coco_object_detections(
|
|
47
|
+
session: Session,
|
|
48
|
+
samples: Iterable[Sample],
|
|
49
|
+
output_json: Path,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Exports object detection annotations to a COCO format JSON file.
|
|
52
|
+
|
|
53
|
+
This function is for internal use. Use `Dataset.query().export().to_coco_object_detections()`
|
|
54
|
+
instead.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
session: The database session.
|
|
58
|
+
samples: The samples to export.
|
|
59
|
+
output_json: The path to save the output JSON file.
|
|
60
|
+
"""
|
|
61
|
+
export_input = LightlyStudioObjectDetectionInput(
|
|
62
|
+
session=session,
|
|
63
|
+
samples=samples,
|
|
64
|
+
)
|
|
65
|
+
COCOObjectDetectionOutput(output_file=output_json).save(label_input=export_input)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Converts annotations from Lightly Studio to Labelformat format."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from argparse import ArgumentParser
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from labelformat.model.bounding_box import BoundingBox
|
|
10
|
+
from labelformat.model.category import Category
|
|
11
|
+
from labelformat.model.image import Image
|
|
12
|
+
from labelformat.model.object_detection import (
|
|
13
|
+
ImageObjectDetection,
|
|
14
|
+
ObjectDetectionInput,
|
|
15
|
+
SingleObjectDetection,
|
|
16
|
+
)
|
|
17
|
+
from sqlmodel import Session
|
|
18
|
+
|
|
19
|
+
from lightly_studio.core.sample import Sample
|
|
20
|
+
from lightly_studio.models.annotation.annotation_base import AnnotationBaseTable, AnnotationType
|
|
21
|
+
from lightly_studio.resolvers import annotation_label_resolver
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LightlyStudioObjectDetectionInput(ObjectDetectionInput):
|
|
25
|
+
"""Labelformat adapter backed by dataset samples and annotations."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, session: Session, samples: Iterable[Sample]) -> None:
|
|
28
|
+
"""Initializes the LightlyStudioObjectDetectionInput.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
session: The SQLModel session to use for database access. Used only in the
|
|
32
|
+
constructor to fetch the labels for the given annotation task.
|
|
33
|
+
samples: Dataset samples.
|
|
34
|
+
"""
|
|
35
|
+
self._samples = list(samples)
|
|
36
|
+
self._label_id_to_category = _build_label_id_to_category(session=session)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def add_cli_arguments(parser: ArgumentParser) -> None:
|
|
40
|
+
"""Adds CLI arguments."""
|
|
41
|
+
# Add CLI arguments implementation is not needed for this class. We need it only
|
|
42
|
+
# to satisfy the interface.
|
|
43
|
+
raise NotImplementedError()
|
|
44
|
+
|
|
45
|
+
def get_categories(self) -> Iterable[Category]:
|
|
46
|
+
"""Returns the categories for export."""
|
|
47
|
+
return self._label_id_to_category.values()
|
|
48
|
+
|
|
49
|
+
def get_images(self) -> Iterable[Image]:
|
|
50
|
+
"""Returns the images for export."""
|
|
51
|
+
for idx, sample in enumerate(self._samples):
|
|
52
|
+
yield _sample_to_image(sample=sample, image_id=idx)
|
|
53
|
+
|
|
54
|
+
def get_labels(self) -> Iterable[ImageObjectDetection]:
|
|
55
|
+
"""Returns the labels for export."""
|
|
56
|
+
for idx, sample in enumerate(self._samples):
|
|
57
|
+
yield _sample_to_image_obj_det(
|
|
58
|
+
sample=sample,
|
|
59
|
+
image_id=idx,
|
|
60
|
+
label_id_to_category=self._label_id_to_category,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _build_label_id_to_category(session: Session) -> dict[UUID, Category]:
|
|
65
|
+
labels = annotation_label_resolver.get_all_sorted_alphabetically(
|
|
66
|
+
session=session,
|
|
67
|
+
)
|
|
68
|
+
# TODO(Horatiu, 09/2025): We should get only labels that are attached to Object Detection
|
|
69
|
+
# annotations.
|
|
70
|
+
return {
|
|
71
|
+
label.annotation_label_id: Category(id=idx, name=label.annotation_label_name)
|
|
72
|
+
for idx, label in enumerate(labels)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _sample_to_image(sample: Sample, image_id: int) -> Image:
|
|
77
|
+
return Image(
|
|
78
|
+
id=image_id,
|
|
79
|
+
filename=sample.file_path_abs,
|
|
80
|
+
width=sample.width,
|
|
81
|
+
height=sample.height,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _sample_to_image_obj_det(
|
|
86
|
+
sample: Sample,
|
|
87
|
+
image_id: int,
|
|
88
|
+
label_id_to_category: dict[UUID, Category],
|
|
89
|
+
) -> ImageObjectDetection:
|
|
90
|
+
# TODO(Michal, 09/2025): We can optimise in the future to filter annotations in a DB query.
|
|
91
|
+
objects = [
|
|
92
|
+
_annotation_to_single_obj_det(
|
|
93
|
+
annotation=annotation,
|
|
94
|
+
label_id_to_category=label_id_to_category,
|
|
95
|
+
)
|
|
96
|
+
for annotation in sample.inner.annotations
|
|
97
|
+
if annotation.annotation_type == AnnotationType.OBJECT_DETECTION
|
|
98
|
+
]
|
|
99
|
+
return ImageObjectDetection(
|
|
100
|
+
image=_sample_to_image(sample=sample, image_id=image_id),
|
|
101
|
+
objects=objects,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _annotation_to_single_obj_det(
|
|
106
|
+
annotation: AnnotationBaseTable, label_id_to_category: dict[UUID, Category]
|
|
107
|
+
) -> SingleObjectDetection:
|
|
108
|
+
assert annotation.object_detection_details is not None
|
|
109
|
+
box = BoundingBox(
|
|
110
|
+
xmin=annotation.object_detection_details.x,
|
|
111
|
+
ymin=annotation.object_detection_details.y,
|
|
112
|
+
xmax=annotation.object_detection_details.x + annotation.object_detection_details.width,
|
|
113
|
+
ymax=annotation.object_detection_details.y + annotation.object_detection_details.height,
|
|
114
|
+
)
|
|
115
|
+
category = label_id_to_category[annotation.annotation_label.annotation_label_id]
|
|
116
|
+
return SingleObjectDetection(
|
|
117
|
+
category=category,
|
|
118
|
+
box=box,
|
|
119
|
+
confidence=annotation.confidence,
|
|
120
|
+
)
|
|
@@ -22,20 +22,16 @@ from lightly_studio.few_shot_classifier.random_forest_classifier import (
|
|
|
22
22
|
)
|
|
23
23
|
from lightly_studio.models.annotation.annotation_base import (
|
|
24
24
|
AnnotationCreate,
|
|
25
|
+
AnnotationType,
|
|
25
26
|
)
|
|
26
27
|
from lightly_studio.models.annotation_label import (
|
|
27
28
|
AnnotationLabelCreate,
|
|
28
29
|
)
|
|
29
|
-
from lightly_studio.models.annotation_task import (
|
|
30
|
-
AnnotationTaskTable,
|
|
31
|
-
AnnotationType,
|
|
32
|
-
)
|
|
33
30
|
from lightly_studio.models.classifier import EmbeddingClassifier
|
|
34
31
|
from lightly_studio.models.sample import SampleTable
|
|
35
32
|
from lightly_studio.resolvers import (
|
|
36
33
|
annotation_label_resolver,
|
|
37
34
|
annotation_resolver,
|
|
38
|
-
annotation_task_resolver,
|
|
39
35
|
embedding_model_resolver,
|
|
40
36
|
sample_embedding_resolver,
|
|
41
37
|
sample_resolver,
|
|
@@ -92,7 +88,6 @@ class ClassifierEntry:
|
|
|
92
88
|
# will be false.
|
|
93
89
|
is_active: bool = False
|
|
94
90
|
|
|
95
|
-
annotation_task_id: UUID | None = None
|
|
96
91
|
annotation_label_ids: list[UUID] | None = None
|
|
97
92
|
|
|
98
93
|
|
|
@@ -469,16 +464,14 @@ class ClassifierManager:
|
|
|
469
464
|
embeddings = [se.embedding for se in sample_embeddings]
|
|
470
465
|
predictions = classifier.few_shot_classifier.predict(embeddings)
|
|
471
466
|
if len(predictions):
|
|
472
|
-
|
|
467
|
+
_create_annotation_labels_for_classifier(
|
|
473
468
|
classifier=classifier,
|
|
474
469
|
session=session,
|
|
475
470
|
dataset_id=dataset_id,
|
|
476
471
|
)
|
|
477
472
|
else:
|
|
478
473
|
raise ValueError(f"Predict returned empty list for classifier:'{classifier_id}'")
|
|
479
|
-
# Check if annotation
|
|
480
|
-
if not classifier.annotation_task_id:
|
|
481
|
-
raise ValueError(f"Classifier with ID '{classifier_id}' has no annotation task.")
|
|
474
|
+
# Check if annotation labels are available
|
|
482
475
|
if not classifier.annotation_label_ids:
|
|
483
476
|
raise ValueError(f"Classifier with ID '{classifier_id}' has no annotation labels")
|
|
484
477
|
|
|
@@ -490,7 +483,6 @@ class ClassifierManager:
|
|
|
490
483
|
classification_annotations.append(
|
|
491
484
|
AnnotationCreate(
|
|
492
485
|
sample_id=sample_embedding.sample_id,
|
|
493
|
-
annotation_task_id=classifier.annotation_task_id,
|
|
494
486
|
dataset_id=dataset_id,
|
|
495
487
|
annotation_label_id=classifier.annotation_label_ids[max_index],
|
|
496
488
|
annotation_type=AnnotationType.CLASSIFICATION,
|
|
@@ -500,7 +492,6 @@ class ClassifierManager:
|
|
|
500
492
|
# Clear previous annotations by this classifier
|
|
501
493
|
annotation_resolver.delete_annotations(
|
|
502
494
|
session=session,
|
|
503
|
-
annotation_task_ids=[classifier.annotation_task_id],
|
|
504
495
|
annotation_label_ids=classifier.annotation_label_ids,
|
|
505
496
|
)
|
|
506
497
|
annotation_resolver.create_many(session=session, annotations=classification_annotations)
|
|
@@ -597,30 +588,18 @@ class ClassifierManager:
|
|
|
597
588
|
return self._classifiers[classifier_id]
|
|
598
589
|
|
|
599
590
|
|
|
600
|
-
def
|
|
591
|
+
def _create_annotation_labels_for_classifier(
|
|
601
592
|
session: Session,
|
|
602
593
|
dataset_id: UUID,
|
|
603
594
|
classifier: ClassifierEntry,
|
|
604
595
|
) -> None:
|
|
605
|
-
"""Create annotation
|
|
596
|
+
"""Create annotation labels for the classifier.
|
|
606
597
|
|
|
607
598
|
Args:
|
|
608
599
|
session: Database session.
|
|
609
600
|
dataset_id: The dataset ID to which the samples belong.
|
|
610
601
|
classifier: The classifier object to update.
|
|
611
602
|
"""
|
|
612
|
-
# Check if the annotation task exists and if not create it.
|
|
613
|
-
if classifier.annotation_task_id is None:
|
|
614
|
-
annotation_task = annotation_task_resolver.create(
|
|
615
|
-
session=session,
|
|
616
|
-
annotation_task=AnnotationTaskTable(
|
|
617
|
-
name=FSC_ANNOTATION_TASK_PREFIX + classifier.few_shot_classifier.name,
|
|
618
|
-
annotation_type=AnnotationType.CLASSIFICATION,
|
|
619
|
-
is_prediction=True,
|
|
620
|
-
),
|
|
621
|
-
)
|
|
622
|
-
classifier.annotation_task_id = annotation_task.annotation_task_id
|
|
623
|
-
|
|
624
603
|
# Check if the annotation label with the classifier name and class
|
|
625
604
|
# names exists and if not create it.
|
|
626
605
|
if classifier.annotation_label_ids is None:
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Computes typicality from embeddings."""
|
|
2
|
+
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from lightly_mundig import Typicality # type: ignore[import-untyped]
|
|
6
|
+
from sqlmodel import Session
|
|
7
|
+
|
|
8
|
+
from lightly_studio.dataset.env import LIGHTLY_STUDIO_LICENSE_KEY
|
|
9
|
+
from lightly_studio.resolvers import (
|
|
10
|
+
metadata_resolver,
|
|
11
|
+
sample_embedding_resolver,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
DEFAULT_NUM_NEAREST_NEIGHBORS = 20
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def compute_typicality_metadata(
|
|
18
|
+
session: Session,
|
|
19
|
+
dataset_id: UUID,
|
|
20
|
+
embedding_model_id: UUID,
|
|
21
|
+
metadata_name: str = "typicality",
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Computes typicality for each sample in the dataset from embeddings.
|
|
24
|
+
|
|
25
|
+
Typicality is a measure of how representative a sample is of the dataset.
|
|
26
|
+
It is calculated for each sample from its K-nearest neighbors in the
|
|
27
|
+
embedding space.
|
|
28
|
+
|
|
29
|
+
The computed typicality values are stored as metadata for each sample.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
session:
|
|
33
|
+
The database session.
|
|
34
|
+
dataset_id:
|
|
35
|
+
The ID of the dataset for which to compute the typicality.
|
|
36
|
+
embedding_model_id:
|
|
37
|
+
The ID of the embedding model to use for the computation.
|
|
38
|
+
metadata_name:
|
|
39
|
+
The name of the metadata field to store the typicality values in.
|
|
40
|
+
Defaults to "typicality".
|
|
41
|
+
"""
|
|
42
|
+
license_key = LIGHTLY_STUDIO_LICENSE_KEY
|
|
43
|
+
if license_key is None:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
"LIGHTLY_STUDIO_LICENSE_KEY environment variable is not set. "
|
|
46
|
+
"Please set it to your LightlyStudio license key."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
samples = sample_embedding_resolver.get_all_by_dataset_id(
|
|
50
|
+
session=session, dataset_id=dataset_id, embedding_model_id=embedding_model_id
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
embeddings = [sample.embedding for sample in samples]
|
|
54
|
+
typicality = Typicality(embeddings=embeddings, token=license_key)
|
|
55
|
+
typicality_values = typicality.calculate_typicality(
|
|
56
|
+
num_nearest_neighbors=DEFAULT_NUM_NEAREST_NEIGHBORS
|
|
57
|
+
)
|
|
58
|
+
assert len(samples) == len(typicality_values), (
|
|
59
|
+
"The number of samples and computed typicality values must match"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
metadata = [
|
|
63
|
+
(sample.sample_id, {metadata_name: typicality})
|
|
64
|
+
for sample, typicality in zip(samples, typicality_values)
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
metadata_resolver.bulk_set_metadata(session, metadata)
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""This module defines the base annotation model."""
|
|
2
2
|
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
|
+
from enum import Enum
|
|
4
5
|
from typing import TYPE_CHECKING, List, Optional
|
|
5
6
|
from uuid import UUID, uuid4
|
|
6
7
|
|
|
7
|
-
from pydantic import BaseModel
|
|
8
|
+
from pydantic import BaseModel, ConfigDict
|
|
8
9
|
from pydantic import Field as PydanticField
|
|
9
10
|
from sqlalchemy.orm import Mapped
|
|
10
11
|
from sqlmodel import Field, Relationship, SQLModel
|
|
@@ -22,10 +23,6 @@ from lightly_studio.models.annotation.semantic_segmentation import (
|
|
|
22
23
|
SemanticSegmentationAnnotationTable,
|
|
23
24
|
SemanticSegmentationAnnotationView,
|
|
24
25
|
)
|
|
25
|
-
from lightly_studio.models.annotation_task import (
|
|
26
|
-
AnnotationTaskTable,
|
|
27
|
-
AnnotationType,
|
|
28
|
-
)
|
|
29
26
|
|
|
30
27
|
if TYPE_CHECKING:
|
|
31
28
|
from lightly_studio.models.annotation_label import (
|
|
@@ -41,6 +38,15 @@ else:
|
|
|
41
38
|
AnnotationLabelTable = object
|
|
42
39
|
|
|
43
40
|
|
|
41
|
+
class AnnotationType(str, Enum):
|
|
42
|
+
"""The type of annotation task."""
|
|
43
|
+
|
|
44
|
+
CLASSIFICATION = "classification"
|
|
45
|
+
SEMANTIC_SEGMENTATION = "semantic_segmentation"
|
|
46
|
+
INSTANCE_SEGMENTATION = "instance_segmentation"
|
|
47
|
+
OBJECT_DETECTION = "object_detection"
|
|
48
|
+
|
|
49
|
+
|
|
44
50
|
class AnnotationBaseTable(SQLModel, table=True):
|
|
45
51
|
"""Base class for all annotation models."""
|
|
46
52
|
|
|
@@ -51,9 +57,7 @@ class AnnotationBaseTable(SQLModel, table=True):
|
|
|
51
57
|
annotation_id: UUID = Field(default_factory=uuid4, primary_key=True)
|
|
52
58
|
annotation_type: AnnotationType
|
|
53
59
|
annotation_label_id: UUID = Field(foreign_key="annotation_labels.annotation_label_id")
|
|
54
|
-
|
|
55
|
-
foreign_key="annotation_tasks.annotation_task_id",
|
|
56
|
-
)
|
|
60
|
+
|
|
57
61
|
confidence: Optional[float] = None
|
|
58
62
|
dataset_id: UUID = Field(foreign_key="datasets.dataset_id")
|
|
59
63
|
sample_id: UUID = Field(foreign_key="samples.sample_id")
|
|
@@ -61,9 +65,6 @@ class AnnotationBaseTable(SQLModel, table=True):
|
|
|
61
65
|
annotation_label: Mapped["AnnotationLabelTable"] = Relationship(
|
|
62
66
|
sa_relationship_kwargs={"lazy": "select"},
|
|
63
67
|
)
|
|
64
|
-
annotation_task: Mapped["AnnotationTaskTable"] = Relationship(
|
|
65
|
-
sa_relationship_kwargs={"lazy": "select"},
|
|
66
|
-
)
|
|
67
68
|
sample: Mapped[Optional["SampleTable"]] = Relationship(
|
|
68
69
|
sa_relationship_kwargs={"lazy": "select"},
|
|
69
70
|
)
|
|
@@ -101,16 +102,15 @@ class AnnotationCreate(SQLModel):
|
|
|
101
102
|
""" Required properties for all annotations. """
|
|
102
103
|
annotation_label_id: UUID
|
|
103
104
|
annotation_type: AnnotationType
|
|
104
|
-
annotation_task_id: UUID
|
|
105
105
|
confidence: Optional[float] = None
|
|
106
106
|
dataset_id: UUID
|
|
107
107
|
sample_id: UUID
|
|
108
108
|
|
|
109
109
|
""" Optional properties for object detection. """
|
|
110
|
-
x: Optional[
|
|
111
|
-
y: Optional[
|
|
112
|
-
width: Optional[
|
|
113
|
-
height: Optional[
|
|
110
|
+
x: Optional[int] = None
|
|
111
|
+
y: Optional[int] = None
|
|
112
|
+
width: Optional[int] = None
|
|
113
|
+
height: Optional[int] = None
|
|
114
114
|
|
|
115
115
|
""" Optional properties for instance and semantic segmentation. """
|
|
116
116
|
segmentation_mask: Optional[List[int]] = None
|
|
@@ -140,7 +140,6 @@ class AnnotationView(SQLModel):
|
|
|
140
140
|
annotation_id: UUID
|
|
141
141
|
annotation_type: AnnotationType
|
|
142
142
|
annotation_label: AnnotationLabel
|
|
143
|
-
annotation_task_id: UUID
|
|
144
143
|
confidence: Optional[float] = None
|
|
145
144
|
|
|
146
145
|
object_detection_details: Optional[ObjectDetectionAnnotationView] = None
|
|
@@ -157,13 +156,12 @@ class AnnotationWithSampleView(AnnotationView):
|
|
|
157
156
|
class AnnotationViewsWithCount(BaseModel):
|
|
158
157
|
"""Response model for counted annotations."""
|
|
159
158
|
|
|
159
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
160
|
+
|
|
160
161
|
annotations: List[AnnotationWithSampleView] = PydanticField(..., alias="data")
|
|
161
162
|
total_count: int
|
|
162
163
|
next_cursor: Optional[int] = PydanticField(..., alias="nextCursor")
|
|
163
164
|
|
|
164
|
-
class Config: # noqa: D106
|
|
165
|
-
populate_by_name = True
|
|
166
|
-
|
|
167
165
|
|
|
168
166
|
class AnnotationDetailsView(AnnotationView):
|
|
169
167
|
"""Representing detailed view of an annotation."""
|
|
@@ -34,10 +34,10 @@ class InstanceSegmentationAnnotationTable(SQLModel, table=True):
|
|
|
34
34
|
back_populates="instance_segmentation_details"
|
|
35
35
|
)
|
|
36
36
|
|
|
37
|
-
x:
|
|
38
|
-
y:
|
|
39
|
-
width:
|
|
40
|
-
height:
|
|
37
|
+
x: int
|
|
38
|
+
y: int
|
|
39
|
+
width: int
|
|
40
|
+
height: int
|
|
41
41
|
# TODO(Kondrat 06/2025): We need to fix logic in the loader,
|
|
42
42
|
# because it shouldn't be optional.
|
|
43
43
|
# lightly_studio/dataset/loader.py#L148
|
|
@@ -49,8 +49,8 @@ class InstanceSegmentationAnnotationTable(SQLModel, table=True):
|
|
|
49
49
|
class InstanceSegmentationAnnotationView(SQLModel):
|
|
50
50
|
"""API response model for instance segmentation annotations."""
|
|
51
51
|
|
|
52
|
-
x:
|
|
53
|
-
y:
|
|
54
|
-
width:
|
|
55
|
-
height:
|
|
52
|
+
x: int
|
|
53
|
+
y: int
|
|
54
|
+
width: int
|
|
55
|
+
height: int
|
|
56
56
|
segmentation_mask: Optional[List[int]] = None
|
|
@@ -32,10 +32,10 @@ class ObjectDetectionAnnotationTable(SQLModel, table=True):
|
|
|
32
32
|
back_populates="object_detection_details"
|
|
33
33
|
)
|
|
34
34
|
|
|
35
|
-
x:
|
|
36
|
-
y:
|
|
37
|
-
width:
|
|
38
|
-
height:
|
|
35
|
+
x: int
|
|
36
|
+
y: int
|
|
37
|
+
width: int
|
|
38
|
+
height: int
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
class ObjectDetectionAnnotationView(SQLModel):
|
lightly_studio/models/dataset.py
CHANGED
|
@@ -10,6 +10,7 @@ from uuid import UUID, uuid4
|
|
|
10
10
|
from sqlalchemy.orm import Session as SQLAlchemySession
|
|
11
11
|
from sqlmodel import Field, Session, SQLModel
|
|
12
12
|
|
|
13
|
+
from lightly_studio.api.routes.api.validators import Paginated
|
|
13
14
|
from lightly_studio.models.sample import SampleTable
|
|
14
15
|
from lightly_studio.resolvers import sample_resolver
|
|
15
16
|
from lightly_studio.resolvers.samples_filter import SampleFilter
|
|
@@ -73,11 +74,14 @@ class DatasetTable(DatasetBase, table=True):
|
|
|
73
74
|
if session is None:
|
|
74
75
|
raise RuntimeError("No database session found for this instance")
|
|
75
76
|
|
|
77
|
+
pagination = None
|
|
78
|
+
if limit is not None:
|
|
79
|
+
pagination = Paginated(offset=offset, limit=limit)
|
|
80
|
+
|
|
76
81
|
return sample_resolver.get_all_by_dataset_id(
|
|
77
82
|
session=session,
|
|
78
83
|
dataset_id=self.dataset_id,
|
|
79
|
-
|
|
80
|
-
limit=limit,
|
|
84
|
+
pagination=pagination,
|
|
81
85
|
filters=filters,
|
|
82
86
|
text_embedding=text_embedding,
|
|
83
87
|
sample_ids=sample_ids,
|
lightly_studio/models/sample.py
CHANGED
|
@@ -4,6 +4,8 @@ from datetime import datetime, timezone
|
|
|
4
4
|
from typing import TYPE_CHECKING, Any, List, Literal, Optional
|
|
5
5
|
from uuid import UUID, uuid4
|
|
6
6
|
|
|
7
|
+
from pydantic import BaseModel, ConfigDict
|
|
8
|
+
from pydantic import Field as PydanticField
|
|
7
9
|
from sqlalchemy.orm import Mapped, Session
|
|
8
10
|
from sqlmodel import Field, Relationship, SQLModel
|
|
9
11
|
|
|
@@ -44,7 +46,7 @@ class SampleBase(SQLModel):
|
|
|
44
46
|
dataset_id: UUID = Field(default=None, foreign_key="datasets.dataset_id")
|
|
45
47
|
|
|
46
48
|
"""The dataset image path."""
|
|
47
|
-
file_path_abs: str
|
|
49
|
+
file_path_abs: str = Field(default=None, unique=True)
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
class SampleCreate(SampleBase):
|
|
@@ -97,6 +99,7 @@ class SampleTable(SampleBase, table=True):
|
|
|
97
99
|
embeddings: Mapped[List["SampleEmbeddingTable"]] = Relationship(back_populates="sample")
|
|
98
100
|
metadata_dict: "SampleMetadataTable" = Relationship(back_populates="sample")
|
|
99
101
|
|
|
102
|
+
# TODO(Michal, 9/2025): Remove this function in favour of Sample.metadata.
|
|
100
103
|
def __getitem__(self, key: str) -> Any:
|
|
101
104
|
"""Provides dict-like access to sample metadata.
|
|
102
105
|
|
|
@@ -111,6 +114,7 @@ class SampleTable(SampleBase, table=True):
|
|
|
111
114
|
return None
|
|
112
115
|
return self.metadata_dict.get_value(key)
|
|
113
116
|
|
|
117
|
+
# TODO(Michal, 9/2025): Remove this function in favour of Sample.metadata.
|
|
114
118
|
def __setitem__(self, key: str, value: Any) -> None:
|
|
115
119
|
"""Sets a metadata key-value pair for this sample.
|
|
116
120
|
|
|
@@ -173,8 +177,11 @@ class SampleView(SQLModel):
|
|
|
173
177
|
height: int
|
|
174
178
|
|
|
175
179
|
|
|
176
|
-
class SampleViewsWithCount(
|
|
180
|
+
class SampleViewsWithCount(BaseModel):
|
|
177
181
|
"""Response model for counted samples."""
|
|
178
182
|
|
|
179
|
-
|
|
183
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
184
|
+
|
|
185
|
+
samples: List[SampleView] = PydanticField(..., alias="data")
|
|
180
186
|
total_count: int
|
|
187
|
+
next_cursor: Optional[int] = PydanticField(None, alias="nextCursor")
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from .create import create
|
|
4
4
|
from .delete import delete
|
|
5
|
-
from .get_all import get_all
|
|
5
|
+
from .get_all import get_all, get_all_sorted_alphabetically
|
|
6
6
|
from .get_by_id import get_by_id
|
|
7
7
|
from .get_by_ids import get_by_ids
|
|
8
8
|
from .get_by_label_name import get_by_label_name
|
|
@@ -13,6 +13,7 @@ __all__ = [
|
|
|
13
13
|
"create",
|
|
14
14
|
"delete",
|
|
15
15
|
"get_all",
|
|
16
|
+
"get_all_sorted_alphabetically",
|
|
16
17
|
"get_by_id",
|
|
17
18
|
"get_by_ids",
|
|
18
19
|
"get_by_label_name",
|