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.

Files changed (169) hide show
  1. lightly_studio/__init__.py +4 -4
  2. lightly_studio/api/app.py +7 -5
  3. lightly_studio/api/db_tables.py +0 -3
  4. lightly_studio/api/routes/api/annotation.py +32 -16
  5. lightly_studio/api/routes/api/annotation_label.py +2 -5
  6. lightly_studio/api/routes/api/annotations/__init__.py +7 -0
  7. lightly_studio/api/routes/api/annotations/create_annotation.py +52 -0
  8. lightly_studio/api/routes/api/classifier.py +2 -5
  9. lightly_studio/api/routes/api/dataset.py +5 -8
  10. lightly_studio/api/routes/api/dataset_tag.py +2 -3
  11. lightly_studio/api/routes/api/embeddings2d.py +104 -0
  12. lightly_studio/api/routes/api/export.py +73 -0
  13. lightly_studio/api/routes/api/metadata.py +2 -4
  14. lightly_studio/api/routes/api/sample.py +5 -13
  15. lightly_studio/api/routes/api/selection.py +87 -0
  16. lightly_studio/api/routes/api/settings.py +2 -6
  17. lightly_studio/api/routes/images.py +6 -6
  18. lightly_studio/core/add_samples.py +374 -0
  19. lightly_studio/core/dataset.py +272 -400
  20. lightly_studio/core/dataset_query/boolean_expression.py +67 -0
  21. lightly_studio/core/dataset_query/dataset_query.py +216 -0
  22. lightly_studio/core/dataset_query/field.py +113 -0
  23. lightly_studio/core/dataset_query/field_expression.py +79 -0
  24. lightly_studio/core/dataset_query/match_expression.py +23 -0
  25. lightly_studio/core/dataset_query/order_by.py +79 -0
  26. lightly_studio/core/dataset_query/sample_field.py +28 -0
  27. lightly_studio/core/dataset_query/tags_expression.py +46 -0
  28. lightly_studio/core/sample.py +159 -32
  29. lightly_studio/core/start_gui.py +35 -0
  30. lightly_studio/dataset/edge_embedding_generator.py +13 -8
  31. lightly_studio/dataset/embedding_generator.py +2 -3
  32. lightly_studio/dataset/embedding_manager.py +74 -6
  33. lightly_studio/dataset/env.py +4 -0
  34. lightly_studio/dataset/file_utils.py +13 -2
  35. lightly_studio/dataset/fsspec_lister.py +275 -0
  36. lightly_studio/dataset/loader.py +49 -84
  37. lightly_studio/dataset/mobileclip_embedding_generator.py +9 -6
  38. lightly_studio/db_manager.py +145 -0
  39. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/0.CA_CXIBb.css +1 -0
  40. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_layout.DS78jgNY.css +1 -0
  41. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/index.BVs_sZj9.css +1 -0
  42. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/transform.D487hwJk.css +1 -0
  43. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/6t3IJ0vQ.js +1 -0
  44. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{D6su9Aln.js → 8NsknIT2.js} +1 -1
  45. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{x9G_hzyY.js → BND_-4Kp.js} +1 -1
  46. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{BylOuP6i.js → BdfTHw61.js} +1 -1
  47. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{DOlTMNyt.js → BfHVnyNT.js} +1 -1
  48. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BjkP1AHA.js +1 -0
  49. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BuuNVL9G.js +1 -0
  50. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{O-EABkf9.js → BzKGpnl4.js} +1 -1
  51. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CCx7Ho51.js +1 -0
  52. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{l7KrR96u.js → CH6P3X75.js} +1 -1
  53. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{D5-A_Ffd.js → CR2upx_Q.js} +2 -2
  54. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CWPZrTTJ.js +1 -0
  55. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{C8I8rFJQ.js → Cs1XmhiF.js} +1 -1
  56. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{CDnpyLsT.js → CwPowJfP.js} +1 -1
  57. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CxFKfZ9T.js +1 -0
  58. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Cxevwdid.js +1 -0
  59. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{DjfY96ND.js → D4whDBUi.js} +1 -1
  60. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6r9vr07.js +1 -0
  61. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DA6bFLPR.js +1 -0
  62. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DEgUu98i.js +3 -0
  63. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DGTPl6Gk.js +1 -0
  64. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DKGxBSlK.js +1 -0
  65. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DQXoLcsF.js +1 -0
  66. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DQe_kdRt.js +92 -0
  67. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DcY4jgG3.js +1 -0
  68. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{Bu7uvVrG.js → RmD8FzRo.js} +1 -1
  69. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/V-MnMC1X.js +1 -0
  70. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{Bsi3UGy5.js → keKYsoph.js} +1 -1
  71. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/app.BVr6DYqP.js +2 -0
  72. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/start.u7zsVvqp.js +1 -0
  73. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/0.Da2agmdd.js +1 -0
  74. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{1.B4rNYwVp.js → 1.B11tVRJV.js} +1 -1
  75. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/10.l30Zud4h.js +1 -0
  76. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/12.CgKPGcAP.js +1 -0
  77. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/2.C8HLK8mj.js +857 -0
  78. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{3.CWHpKonm.js → 3.CLvg3QcJ.js} +1 -1
  79. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{4.OUWOLQeV.js → 4.BQhDtXUI.js} +1 -1
  80. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/5.-6XqWX5G.js +1 -0
  81. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/6.uBV1Lhat.js +1 -0
  82. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/7.BXsgoQZh.js +1 -0
  83. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/8.BkbcnUs8.js +1 -0
  84. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{9.CPu3CiBc.js → 9.Bkrv-Vww.js} +1 -1
  85. lightly_studio/dist_lightly_studio_view_app/_app/immutable/workers/clustering.worker-DKqeLtG0.js +2 -0
  86. lightly_studio/dist_lightly_studio_view_app/_app/immutable/workers/search.worker-vNSty3B0.js +1 -0
  87. lightly_studio/dist_lightly_studio_view_app/_app/version.json +1 -1
  88. lightly_studio/dist_lightly_studio_view_app/index.html +14 -14
  89. lightly_studio/examples/example.py +13 -12
  90. lightly_studio/examples/example_coco.py +13 -0
  91. lightly_studio/examples/example_metadata.py +83 -98
  92. lightly_studio/examples/example_selection.py +7 -19
  93. lightly_studio/examples/example_split_work.py +12 -36
  94. lightly_studio/examples/{example_v2.py → example_yolo.py} +3 -4
  95. lightly_studio/export/export_dataset.py +65 -0
  96. lightly_studio/export/lightly_studio_label_input.py +120 -0
  97. lightly_studio/few_shot_classifier/classifier_manager.py +5 -26
  98. lightly_studio/metadata/compute_typicality.py +67 -0
  99. lightly_studio/models/annotation/annotation_base.py +18 -20
  100. lightly_studio/models/annotation/instance_segmentation.py +8 -8
  101. lightly_studio/models/annotation/object_detection.py +4 -4
  102. lightly_studio/models/dataset.py +6 -2
  103. lightly_studio/models/sample.py +10 -3
  104. lightly_studio/resolvers/annotation_label_resolver/__init__.py +2 -1
  105. lightly_studio/resolvers/annotation_label_resolver/get_all.py +15 -0
  106. lightly_studio/resolvers/annotation_resolver/__init__.py +2 -3
  107. lightly_studio/resolvers/annotation_resolver/create_many.py +3 -3
  108. lightly_studio/resolvers/annotation_resolver/delete_annotation.py +1 -1
  109. lightly_studio/resolvers/annotation_resolver/delete_annotations.py +7 -3
  110. lightly_studio/resolvers/annotation_resolver/get_by_id.py +19 -1
  111. lightly_studio/resolvers/annotation_resolver/update_annotation_label.py +0 -1
  112. lightly_studio/resolvers/annotations/annotations_filter.py +1 -11
  113. lightly_studio/resolvers/dataset_resolver.py +10 -0
  114. lightly_studio/resolvers/embedding_model_resolver.py +22 -0
  115. lightly_studio/resolvers/sample_resolver.py +53 -9
  116. lightly_studio/resolvers/tag_resolver.py +23 -0
  117. lightly_studio/selection/mundig.py +7 -10
  118. lightly_studio/selection/select.py +55 -46
  119. lightly_studio/selection/select_via_db.py +23 -19
  120. lightly_studio/selection/selection_config.py +10 -4
  121. lightly_studio/services/annotations_service/__init__.py +12 -0
  122. lightly_studio/services/annotations_service/create_annotation.py +63 -0
  123. lightly_studio/services/annotations_service/delete_annotation.py +22 -0
  124. lightly_studio/services/annotations_service/update_annotation.py +21 -32
  125. lightly_studio/services/annotations_service/update_annotation_bounding_box.py +36 -0
  126. lightly_studio-0.3.3.dist-info/METADATA +814 -0
  127. {lightly_studio-0.3.1.dist-info → lightly_studio-0.3.3.dist-info}/RECORD +130 -113
  128. lightly_studio/api/db.py +0 -133
  129. lightly_studio/api/routes/api/annotation_task.py +0 -38
  130. lightly_studio/api/routes/api/metrics.py +0 -80
  131. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/0.DenzbfeK.css +0 -1
  132. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.OwPEPQZu.css +0 -1
  133. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.b653GmVf.css +0 -1
  134. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_layout.T-zjSUd3.css +0 -1
  135. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B2FVR0s0.js +0 -1
  136. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B9zumHo5.js +0 -1
  137. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BJXwVxaE.js +0 -1
  138. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bx1xMsFy.js +0 -1
  139. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CcaPhhk3.js +0 -1
  140. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CvOmgdoc.js +0 -93
  141. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CxtLVaYz.js +0 -3
  142. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6RI2Zrd.js +0 -1
  143. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D98V7j6A.js +0 -1
  144. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DIRAtgl0.js +0 -1
  145. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DjUWrjOv.js +0 -1
  146. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/XO7A28GO.js +0 -1
  147. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/hQVEETDE.js +0 -1
  148. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/nAHhluT7.js +0 -1
  149. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/r64xT6ao.js +0 -1
  150. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/vC4nQVEB.js +0 -1
  151. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/app.CjnvpsmS.js +0 -2
  152. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/start.0o1H7wM9.js +0 -1
  153. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/0.XRq_TUwu.js +0 -1
  154. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/10.DfBwOEhN.js +0 -1
  155. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/12.CwF2_8mP.js +0 -1
  156. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/2.CS4muRY-.js +0 -6
  157. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/5.Dm6t9F5W.js +0 -1
  158. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/6.Bw5ck4gK.js +0 -1
  159. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/7.CF0EDTR6.js +0 -1
  160. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/8.Cw30LEcV.js +0 -1
  161. lightly_studio/metrics/detection/__init__.py +0 -0
  162. lightly_studio/metrics/detection/map.py +0 -268
  163. lightly_studio/models/annotation_task.py +0 -28
  164. lightly_studio/resolvers/annotation_resolver/create.py +0 -19
  165. lightly_studio/resolvers/annotation_task_resolver.py +0 -31
  166. lightly_studio-0.3.1.dist-info/METADATA +0 -520
  167. /lightly_studio/{metrics → core/dataset_query}/__init__.py +0 -0
  168. /lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/{OpenSans- → OpenSans-Medium.DVUZMR_6.ttf} +0 -0
  169. {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
- from lightly_studio import DatasetLoader
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 DatasetLoader instance
18
- loader = DatasetLoader()
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 = loader.from_yolo(
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
- for reviewer in reviewers.split(","):
41
- tags.append(
42
- tag_resolver.create(
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, tag in enumerate(tags):
55
- # allocate all samples for this tag
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
- loader.start_gui()
43
+ ls.start_gui()
@@ -1,4 +1,4 @@
1
- """Example of how to load samples from path with the dataset class."""
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("clothing_small_test")
19
- dataset.add_samples_from_path(path=dataset_path)
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
- _create_annotation_task_and_labels_for_classifier(
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 task or labels are available
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 _create_annotation_task_and_labels_for_classifier(
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 task and labels for the classifier.
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
- annotation_task_id: UUID = Field(
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[float] = None
111
- y: Optional[float] = None
112
- width: Optional[float] = None
113
- height: Optional[float] = None
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: float
38
- y: float
39
- width: float
40
- height: float
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: float
53
- y: float
54
- width: float
55
- height: float
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: float
36
- y: float
37
- width: float
38
- height: float
35
+ x: int
36
+ y: int
37
+ width: int
38
+ height: int
39
39
 
40
40
 
41
41
  class ObjectDetectionAnnotationView(SQLModel):
@@ -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
- offset=offset,
80
- limit=limit,
84
+ pagination=pagination,
81
85
  filters=filters,
82
86
  text_embedding=text_embedding,
83
87
  sample_ids=sample_ids,
@@ -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(SQLModel):
180
+ class SampleViewsWithCount(BaseModel):
177
181
  """Response model for counted samples."""
178
182
 
179
- data: List[SampleView]
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",