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
@@ -1,11 +1,11 @@
1
1
  # Set up logging before importing any other modules.
2
2
  # Add noqa to silence unused import and unsorted imports linter warnings.
3
3
  from . import setup_logging # noqa: F401 I001
4
-
5
- # TODO (Jonas 08/25): This will be removed as soon as the new interface is used in the examples
6
- from lightly_studio.dataset.loader import DatasetLoader
7
4
  from lightly_studio.core.dataset import Dataset
8
5
  from lightly_studio.core.start_gui import start_gui
9
6
 
7
+ # TODO (Jonas 08/25): This will be removed as soon as the new interface is used in the examples
8
+ from lightly_studio.dataset.loader import DatasetLoader
9
+ from lightly_studio.models.annotation.annotation_base import AnnotationType
10
10
 
11
- __all__ = ["Dataset", "DatasetLoader", "start_gui"]
11
+ __all__ = ["AnnotationType", "Dataset", "DatasetLoader", "start_gui"]
lightly_studio/api/app.py CHANGED
@@ -11,19 +11,20 @@ from fastapi.routing import APIRoute
11
11
  from sqlmodel import Session
12
12
  from typing_extensions import Annotated
13
13
 
14
- from lightly_studio.api.db import db_manager
14
+ from lightly_studio import db_manager
15
15
  from lightly_studio.api.routes import healthz, images, webapp
16
16
  from lightly_studio.api.routes.api import (
17
17
  annotation,
18
18
  annotation_label,
19
- annotation_task,
20
19
  classifier,
21
20
  dataset,
22
21
  dataset_tag,
22
+ embeddings2d,
23
+ export,
23
24
  features,
24
25
  metadata,
25
- metrics,
26
26
  sample,
27
+ selection,
27
28
  settings,
28
29
  text_embedding,
29
30
  )
@@ -84,16 +85,17 @@ api_router = APIRouter(prefix="/api", tags=["api"])
84
85
 
85
86
  api_router.include_router(dataset.dataset_router)
86
87
  api_router.include_router(dataset_tag.tag_router)
88
+ api_router.include_router(export.export_router)
87
89
  api_router.include_router(sample.samples_router)
88
90
  api_router.include_router(annotation_label.annotations_label_router)
89
91
  api_router.include_router(annotation.annotations_router)
90
92
  api_router.include_router(text_embedding.text_embedding_router)
91
- api_router.include_router(annotation_task.router)
92
93
  api_router.include_router(settings.settings_router)
93
94
  api_router.include_router(classifier.classifier_router)
95
+ api_router.include_router(embeddings2d.embeddings2d_router)
94
96
  api_router.include_router(features.features_router)
95
97
  api_router.include_router(metadata.metadata_router)
96
- api_router.include_router(metrics.metrics_router)
98
+ api_router.include_router(selection.selection_router)
97
99
 
98
100
 
99
101
  app.include_router(api_router)
@@ -6,9 +6,6 @@ from lightly_studio.models.annotation.annotation_base import (
6
6
  from lightly_studio.models.annotation_label import (
7
7
  AnnotationLabelTable, # noqa: F401, required for SQLModel to work properly
8
8
  )
9
- from lightly_studio.models.annotation_task import (
10
- AnnotationTaskTable, # noqa: F401, required for SQLModel to work properly
11
- )
12
9
  from lightly_studio.models.dataset import (
13
10
  DatasetTable, # noqa: F401, required for SQLModel to work properly
14
11
  )
@@ -7,16 +7,16 @@ from uuid import UUID
7
7
  from fastapi import APIRouter, Body, Depends, HTTPException, Path
8
8
  from fastapi.params import Query
9
9
  from pydantic import BaseModel
10
- from sqlmodel import Session
11
10
  from typing_extensions import Annotated
12
11
 
13
- from lightly_studio.api.db import get_session
12
+ from lightly_studio.api.routes.api import annotations as annotations_module
14
13
  from lightly_studio.api.routes.api.dataset import get_and_validate_dataset_id
15
14
  from lightly_studio.api.routes.api.status import (
16
15
  HTTP_STATUS_CREATED,
17
16
  HTTP_STATUS_NOT_FOUND,
18
17
  )
19
18
  from lightly_studio.api.routes.api.validators import Paginated, PaginatedWithCursor
19
+ from lightly_studio.db_manager import SessionDep
20
20
  from lightly_studio.models.annotation.annotation_base import (
21
21
  AnnotationBaseTable,
22
22
  AnnotationDetailsView,
@@ -27,6 +27,7 @@ from lightly_studio.resolvers import annotation_resolver, tag_resolver
27
27
  from lightly_studio.resolvers.annotation_resolver.get_all import (
28
28
  GetAllAnnotationsResult,
29
29
  )
30
+ from lightly_studio.resolvers.annotation_resolver.update_bounding_box import BoundingBoxCoordinates
30
31
  from lightly_studio.resolvers.annotations.annotations_filter import (
31
32
  AnnotationsFilter,
32
33
  )
@@ -36,7 +37,7 @@ from lightly_studio.services.annotations_service.update_annotation import (
36
37
  )
37
38
 
38
39
  annotations_router = APIRouter(prefix="/datasets/{dataset_id}", tags=["annotations"])
39
- SessionDep = Annotated[Session, Depends(get_session)]
40
+ annotations_router.include_router(annotations_module.create_annotation_router)
40
41
 
41
42
 
42
43
  @annotations_router.get("/annotations/count")
@@ -134,11 +135,8 @@ class AnnotationUpdateInput(BaseModel):
134
135
 
135
136
  annotation_id: UUID
136
137
  dataset_id: UUID
137
- label_name: str | None
138
- x: int | None = None
139
- y: int | None = None
140
- width: int | None = None
141
- height: int | None = None
138
+ label_name: str | None = None
139
+ bounding_box: BoundingBoxCoordinates | None = None
142
140
 
143
141
 
144
142
  @annotations_router.put("/annotations/{annotation_id}")
@@ -161,10 +159,7 @@ def update_annotation(
161
159
  annotation_id=annotation_id,
162
160
  dataset_id=dataset_id,
163
161
  label_name=annotation_update_input.label_name,
164
- x=annotation_update_input.x,
165
- y=annotation_update_input.y,
166
- width=annotation_update_input.width,
167
- height=annotation_update_input.height,
162
+ bounding_box=annotation_update_input.bounding_box,
168
163
  ),
169
164
  )
170
165
 
@@ -188,10 +183,7 @@ def update_annotations(
188
183
  annotation_id=annotation_update_input.annotation_id,
189
184
  dataset_id=dataset_id,
190
185
  label_name=annotation_update_input.label_name,
191
- x=annotation_update_input.x,
192
- y=annotation_update_input.y,
193
- width=annotation_update_input.width,
194
- height=annotation_update_input.height,
186
+ bounding_box=annotation_update_input.bounding_box,
195
187
  )
196
188
  for annotation_update_input in annotation_update_inputs
197
189
  ],
@@ -231,3 +223,27 @@ def remove_tag_from_annotation(
231
223
  raise HTTPException(status_code=HTTP_STATUS_NOT_FOUND, detail=f"Tag {tag_id} not found")
232
224
 
233
225
  return True
226
+
227
+
228
+ @annotations_router.delete("/annotations/{annotation_id}")
229
+ def delete_annotation(
230
+ session: SessionDep,
231
+ # We need dataset_id because generator doesn't include it
232
+ # actuall path for this route is /datasets/{dataset_id}/annotations/{annotation_id}
233
+ dataset_id: Annotated[ # noqa: ARG001
234
+ UUID,
235
+ Path(title="Dataset Id", description="The ID of the dataset"),
236
+ ],
237
+ annotation_id: Annotated[
238
+ UUID, Path(title="Annotation ID", description="ID of the annotation to delete")
239
+ ],
240
+ ) -> dict[str, str]:
241
+ """Delete an annotation from the database."""
242
+ try:
243
+ annotations_service.delete_annotation(session=session, annotation_id=annotation_id)
244
+ return {"status": "deleted"}
245
+ except ValueError as e:
246
+ raise HTTPException(
247
+ status_code=HTTP_STATUS_NOT_FOUND,
248
+ detail="Annotation not found",
249
+ ) from e
@@ -4,15 +4,13 @@ from __future__ import annotations
4
4
 
5
5
  from uuid import UUID
6
6
 
7
- from fastapi import APIRouter, Depends, HTTPException
8
- from sqlmodel import Session
9
- from typing_extensions import Annotated
7
+ from fastapi import APIRouter, HTTPException
10
8
 
11
- from lightly_studio.api.db import get_session
12
9
  from lightly_studio.api.routes.api.status import (
13
10
  HTTP_STATUS_CREATED,
14
11
  HTTP_STATUS_NOT_FOUND,
15
12
  )
13
+ from lightly_studio.db_manager import SessionDep
16
14
  from lightly_studio.models.annotation_label import (
17
15
  AnnotationLabelCreate,
18
16
  AnnotationLabelTable,
@@ -20,7 +18,6 @@ from lightly_studio.models.annotation_label import (
20
18
  from lightly_studio.resolvers import annotation_label_resolver
21
19
 
22
20
  annotations_label_router = APIRouter()
23
- SessionDep = Annotated[Session, Depends(get_session)]
24
21
 
25
22
 
26
23
  @annotations_label_router.post(
@@ -0,0 +1,7 @@
1
+ from .create_annotation import (
2
+ create_annotation_router,
3
+ )
4
+
5
+ __all__ = [
6
+ "create_annotation_router",
7
+ ]
@@ -0,0 +1,52 @@
1
+ """Create annotation route."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from uuid import UUID
6
+
7
+ from fastapi import APIRouter, Path
8
+ from fastapi.params import Body
9
+ from pydantic import BaseModel
10
+ from typing_extensions import Annotated
11
+
12
+ from lightly_studio.db_manager import SessionDep
13
+ from lightly_studio.models.annotation.annotation_base import (
14
+ AnnotationBaseTable,
15
+ AnnotationType,
16
+ AnnotationView,
17
+ )
18
+ from lightly_studio.services import annotations_service
19
+ from lightly_studio.services.annotations_service.create_annotation import AnnotationCreateParams
20
+
21
+ create_annotation_router = APIRouter()
22
+
23
+
24
+ class AnnotationCreateInput(BaseModel):
25
+ """API interface to create annotation."""
26
+
27
+ annotation_label_id: UUID
28
+ annotation_type: AnnotationType
29
+ sample_id: UUID
30
+ x: int | None = None
31
+ y: int | None = None
32
+ width: int | None = None
33
+ height: int | None = None
34
+ segmentation_mask: list[int] | None = None
35
+
36
+
37
+ @create_annotation_router.post(
38
+ "/annotations",
39
+ response_model=AnnotationView,
40
+ )
41
+ def create_annotation(
42
+ dataset_id: Annotated[UUID, Path(title="Dataset Id", description="The ID of the dataset")],
43
+ session: SessionDep,
44
+ create_annotation_input: Annotated[AnnotationCreateInput, Body()],
45
+ ) -> AnnotationBaseTable:
46
+ """Create a new annotation."""
47
+ return annotations_service.create_annotation(
48
+ session=session,
49
+ annotation=AnnotationCreateParams(
50
+ dataset_id=dataset_id, **create_annotation_input.model_dump()
51
+ ),
52
+ )
@@ -6,13 +6,11 @@ import io
6
6
  from pathlib import Path
7
7
  from uuid import UUID
8
8
 
9
- from fastapi import APIRouter, Depends, UploadFile
9
+ from fastapi import APIRouter, UploadFile
10
10
  from fastapi.responses import StreamingResponse
11
11
  from pydantic import BaseModel
12
- from sqlmodel import Session
13
- from typing_extensions import Annotated
14
12
 
15
- from lightly_studio.api.db import get_session
13
+ from lightly_studio.db_manager import SessionDep
16
14
  from lightly_studio.few_shot_classifier.classifier import (
17
15
  ExportType,
18
16
  )
@@ -22,7 +20,6 @@ from lightly_studio.few_shot_classifier.classifier_manager import (
22
20
  from lightly_studio.models.classifier import EmbeddingClassifier
23
21
 
24
22
  classifier_router = APIRouter()
25
- SessionDep = Annotated[Session, Depends(get_session)]
26
23
 
27
24
 
28
25
  class GetNegativeSamplesRequest(BaseModel):
@@ -9,12 +9,12 @@ from uuid import UUID
9
9
  from fastapi import APIRouter, Depends, HTTPException, Path, Query
10
10
  from fastapi.responses import PlainTextResponse
11
11
  from pydantic import BaseModel
12
- from sqlmodel import Field, Session
12
+ from sqlmodel import Field
13
13
  from typing_extensions import Annotated
14
14
 
15
- from lightly_studio.api.db import get_session
16
15
  from lightly_studio.api.routes.api.status import HTTP_STATUS_NOT_FOUND
17
16
  from lightly_studio.api.routes.api.validators import Paginated
17
+ from lightly_studio.db_manager import SessionDep
18
18
  from lightly_studio.models.dataset import (
19
19
  DatasetCreate,
20
20
  DatasetTable,
@@ -26,7 +26,6 @@ from lightly_studio.resolvers.dataset_resolver import (
26
26
  )
27
27
 
28
28
  dataset_router = APIRouter()
29
- SessionDep = Annotated[Session, Depends(get_session)]
30
29
 
31
30
 
32
31
  def get_and_validate_dataset_id(
@@ -109,6 +108,7 @@ def delete_dataset(
109
108
  return {"status": "deleted"}
110
109
 
111
110
 
111
+ # TODO(Michal, 09/2025): Move to export.py
112
112
  class ExportBody(BaseModel):
113
113
  """body parameters for including or excluding tag_ids or sample_ids."""
114
114
 
@@ -124,6 +124,7 @@ class ExportBody(BaseModel):
124
124
  # of sample_ids, it is a POST request to avoid URL length limitations.
125
125
  # A body with a GET request is supported by fastAPI however it has undefined
126
126
  # behavior: https://fastapi.tiangolo.com/tutorial/body/
127
+ # TODO(Michal, 09/2025): Move to export.py
127
128
  @dataset_router.post(
128
129
  "/datasets/{dataset_id}/export",
129
130
  )
@@ -156,11 +157,7 @@ def export_dataset_to_absolute_paths(
156
157
  return response
157
158
 
158
159
 
159
- """
160
- Endpoint to export samples from a dataset.
161
- """
162
-
163
-
160
+ # TODO(Michal, 09/2025): Move to export.py
164
161
  @dataset_router.post(
165
162
  "/datasets/{dataset_id}/export/stats",
166
163
  )
@@ -8,10 +8,9 @@ from uuid import UUID
8
8
  from fastapi import APIRouter, Depends, HTTPException, Path, Query
9
9
  from pydantic import BaseModel
10
10
  from sqlalchemy.exc import IntegrityError
11
- from sqlmodel import Field, Session
11
+ from sqlmodel import Field
12
12
  from typing_extensions import Annotated
13
13
 
14
- from lightly_studio.api.db import get_session
15
14
  from lightly_studio.api.routes.api.dataset import get_and_validate_dataset_id
16
15
  from lightly_studio.api.routes.api.status import (
17
16
  HTTP_STATUS_CONFLICT,
@@ -19,6 +18,7 @@ from lightly_studio.api.routes.api.status import (
19
18
  HTTP_STATUS_NOT_FOUND,
20
19
  )
21
20
  from lightly_studio.api.routes.api.validators import Paginated
21
+ from lightly_studio.db_manager import SessionDep
22
22
  from lightly_studio.models.dataset import DatasetTable
23
23
  from lightly_studio.models.tag import (
24
24
  TagCreate,
@@ -31,7 +31,6 @@ from lightly_studio.models.tag import (
31
31
  from lightly_studio.resolvers import tag_resolver
32
32
 
33
33
  tag_router = APIRouter()
34
- SessionDep = Annotated[Session, Depends(get_session)]
35
34
 
36
35
 
37
36
  @tag_router.post(
@@ -0,0 +1,104 @@
1
+ """Routes delivering 2D embeddings for visualization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+
7
+ import numpy as np
8
+ import pyarrow as pa
9
+ from fastapi import APIRouter, HTTPException, Response
10
+ from numpy.typing import NDArray
11
+ from pyarrow import ipc
12
+ from sklearn.manifold import TSNE
13
+ from sqlmodel import select
14
+
15
+ from lightly_studio.db_manager import SessionDep
16
+ from lightly_studio.models.dataset import DatasetTable
17
+ from lightly_studio.models.embedding_model import EmbeddingModelTable
18
+ from lightly_studio.resolvers import sample_embedding_resolver
19
+
20
+ embeddings2d_router = APIRouter()
21
+
22
+
23
+ @embeddings2d_router.get("/embeddings2d/tsne")
24
+ def get_embeddings2d__tsne(session: SessionDep) -> Response:
25
+ """Return 2D embeddings serialized as an Arrow stream."""
26
+ # TODO(Malte, 09/2025): Support choosing the dataset via API parameter.
27
+ dataset = session.exec(select(DatasetTable).limit(1)).first()
28
+ if dataset is None:
29
+ raise HTTPException(status_code=404, detail="No dataset configured.")
30
+
31
+ # TODO(Malte, 09/2025): Support choosing the embedding model via API parameter.
32
+ embedding_model = session.exec(
33
+ select(EmbeddingModelTable)
34
+ .where(EmbeddingModelTable.dataset_id == dataset.dataset_id)
35
+ .limit(1)
36
+ ).first()
37
+ if embedding_model is None:
38
+ raise HTTPException(status_code=404, detail="No embedding model configured.")
39
+
40
+ # TODO(Malte, 09/2025): Support choosing a subset of samples via API parameter.
41
+ embeddings = sample_embedding_resolver.get_all_by_dataset_id(
42
+ session=session,
43
+ dataset_id=dataset.dataset_id,
44
+ embedding_model_id=embedding_model.embedding_model_id,
45
+ )
46
+
47
+ embedding_values = np.asarray([e.embedding for e in embeddings], dtype=np.float32)
48
+ embedding_values_tsne = _calculate_tsne_embeddings(embedding_values)
49
+ x = embedding_values_tsne[:, 0]
50
+ y = embedding_values_tsne[:, 1]
51
+
52
+ # TODO(Malte, 09/2025): Save the 2D-embeddings in the database to avoid recomputing
53
+ # them on every request.
54
+
55
+ # TODO(Malte, 09/2025): Include a sample identifier in the returned payload.
56
+ table = pa.table(
57
+ {
58
+ "x": pa.array(x, type=pa.float32()),
59
+ "y": pa.array(y, type=pa.float32()),
60
+ }
61
+ )
62
+
63
+ buffer = io.BytesIO()
64
+ with ipc.new_stream(buffer, table.schema) as writer:
65
+ writer.write_table(table)
66
+ buffer.seek(0)
67
+
68
+ return Response(
69
+ content=buffer.getvalue(),
70
+ media_type="application/vnd.apache.arrow.stream",
71
+ headers={
72
+ "Content-Disposition": "inline; filename=embeddings2d.arrow",
73
+ "Content-Type": "application/vnd.apache.arrow.stream",
74
+ "X-Content-Type-Options": "nosniff",
75
+ },
76
+ )
77
+
78
+
79
+ def _calculate_tsne_embeddings(embedding_values: NDArray[np.float32]) -> NDArray[np.float32]:
80
+ # TODO(Malte, 10/2025): Switch to a better and faster projection method than
81
+ # scikit-learn's TSNE.
82
+ # See https://linear.app/lightly/issue/LIG-7678/embedding-plot-investigate-fasterandbetter-2d-computation-options
83
+ n_samples = embedding_values.shape[0]
84
+ # For 0, 1 or 2 samples we hard-code deterministic coordinates.
85
+ if n_samples == 0:
86
+ return np.zeros((0, 2), dtype=np.float32)
87
+ if n_samples == 1:
88
+ return np.asarray([[0.0, 0.0]], dtype=np.float32)
89
+ if n_samples == 2: # noqa: PLR2004
90
+ return np.asarray([[0.0, 0.0], [1.0, 1.0]], dtype=np.float32)
91
+
92
+ # Copied from lightly-core:
93
+ # https://github.com/lightly-ai/lightly-core/blob/b738952516e916eba42fdd28498491ff18df5c1e/appv2/packages/queueworker/src/jobs/embeddings2d/function-source/main.py#L179-L186
94
+ embeddings_2d: NDArray[np.float32] = TSNE(
95
+ init="pca", # changed in https://github.com/scikit-learn/scikit-learn/issues/18018
96
+ learning_rate="auto", # changed in https://github.com/scikit-learn/scikit-learn/issues/18018
97
+ n_components=2,
98
+ # Perplexity must be _less_ than the number of entries. 30 is the default value.
99
+ # https://scikit-learn.org/stable/modules/generated/sklearn.manifold.TSNE.html
100
+ perplexity=min(30.0, float(n_samples - 1)),
101
+ # Make the computation deterministic.
102
+ random_state=0,
103
+ ).fit_transform(embedding_values)
104
+ return embeddings_2d
@@ -0,0 +1,73 @@
1
+ """API routes for exporting dataset annotation tasks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Generator
6
+ from pathlib import Path as PathlibPath
7
+ from tempfile import TemporaryDirectory
8
+
9
+ from fastapi import APIRouter, Depends, Path
10
+ from fastapi.responses import StreamingResponse
11
+ from typing_extensions import Annotated
12
+
13
+ from lightly_studio.api.routes.api import dataset as dataset_api
14
+ from lightly_studio.core.dataset_query.dataset_query import DatasetQuery
15
+ from lightly_studio.db_manager import SessionDep
16
+ from lightly_studio.export import export_dataset
17
+ from lightly_studio.models.dataset import DatasetTable
18
+
19
+ export_router = APIRouter(prefix="/datasets/{dataset_id}", tags=["export"])
20
+
21
+
22
+ @export_router.get("/export/annotations")
23
+ def export_dataset_annotations(
24
+ dataset: Annotated[
25
+ DatasetTable,
26
+ Path(title="Dataset Id"),
27
+ Depends(dataset_api.get_and_validate_dataset_id),
28
+ ],
29
+ session: SessionDep,
30
+ ) -> StreamingResponse:
31
+ """Export dataset annotations for an object detection task in COCO format."""
32
+ # Query to export - all samples in the dataset.
33
+ dataset_query = DatasetQuery(dataset=dataset, session=session)
34
+
35
+ # Create the export in a temporary directory. We cannot use a context manager
36
+ # because the directory should be deleted only after the file has finished streaming.
37
+ temp_dir = TemporaryDirectory()
38
+ output_path = PathlibPath(temp_dir.name) / "coco_export.json"
39
+
40
+ try:
41
+ export_dataset.to_coco_object_detections(
42
+ session=session,
43
+ samples=dataset_query,
44
+ output_json=output_path,
45
+ )
46
+ except Exception:
47
+ temp_dir.cleanup()
48
+ # Reraise.
49
+ raise
50
+
51
+ return StreamingResponse(
52
+ content=_stream_export_file(
53
+ temp_dir=temp_dir,
54
+ file_path=output_path,
55
+ ),
56
+ media_type="application/json",
57
+ headers={
58
+ "Access-Control-Expose-Headers": "Content-Disposition",
59
+ "Content-Disposition": f"attachment; filename={output_path.name}",
60
+ },
61
+ )
62
+
63
+
64
+ def _stream_export_file(
65
+ temp_dir: TemporaryDirectory[str],
66
+ file_path: PathlibPath,
67
+ ) -> Generator[bytes, None, None]:
68
+ """Stream the export file and clean up the temporary directory afterwards."""
69
+ try:
70
+ with file_path.open("rb") as file:
71
+ yield from file
72
+ finally:
73
+ temp_dir.cleanup()
@@ -5,18 +5,16 @@ from __future__ import annotations
5
5
  from typing import List
6
6
  from uuid import UUID
7
7
 
8
- from fastapi import APIRouter, Depends, Path
9
- from sqlmodel import Session
8
+ from fastapi import APIRouter, Path
10
9
  from typing_extensions import Annotated
11
10
 
12
- from lightly_studio.api.db import get_session
11
+ from lightly_studio.db_manager import SessionDep
13
12
  from lightly_studio.models.metadata import MetadataInfoView
14
13
  from lightly_studio.resolvers.metadata_resolver.sample.get_metadata_info import (
15
14
  get_all_metadata_keys_and_schema,
16
15
  )
17
16
 
18
17
  metadata_router = APIRouter(prefix="/datasets/{dataset_id}", tags=["metadata"])
19
- SessionDep = Annotated[Session, Depends(get_session)]
20
18
 
21
19
 
22
20
  @metadata_router.get("/metadata/info", response_model=List[MetadataInfoView])
@@ -6,16 +6,15 @@ from uuid import UUID
6
6
 
7
7
  from fastapi import APIRouter, Depends, HTTPException, Path, Query
8
8
  from pydantic import BaseModel, Field
9
- from sqlmodel import Session
10
9
  from typing_extensions import Annotated
11
10
 
12
- from lightly_studio.api.db import get_session
13
11
  from lightly_studio.api.routes.api.dataset import get_and_validate_dataset_id
14
12
  from lightly_studio.api.routes.api.status import (
15
13
  HTTP_STATUS_CREATED,
16
14
  HTTP_STATUS_NOT_FOUND,
17
15
  )
18
16
  from lightly_studio.api.routes.api.validators import Paginated
17
+ from lightly_studio.db_manager import SessionDep
19
18
  from lightly_studio.models.dataset import DatasetTable
20
19
  from lightly_studio.models.sample import (
21
20
  SampleCreate,
@@ -27,12 +26,12 @@ from lightly_studio.resolvers import (
27
26
  sample_resolver,
28
27
  tag_resolver,
29
28
  )
29
+ from lightly_studio.resolvers.sample_resolver import GetAllSamplesByDatasetIdResult
30
30
  from lightly_studio.resolvers.samples_filter import (
31
31
  SampleFilter,
32
32
  )
33
33
 
34
34
  samples_router = APIRouter(prefix="/datasets/{dataset_id}", tags=["samples"])
35
- SessionDep = Annotated[Session, Depends(get_session)]
36
35
 
37
36
 
38
37
  @samples_router.post("/samples", response_model=SampleView)
@@ -60,7 +59,7 @@ def read_samples(
60
59
  session: SessionDep,
61
60
  dataset_id: Annotated[UUID, Path(title="Dataset Id")],
62
61
  body: ReadSamplesRequest,
63
- ) -> SampleViewsWithCount:
62
+ ) -> GetAllSamplesByDatasetIdResult:
64
63
  """Retrieve a list of samples from the database with optional filtering.
65
64
 
66
65
  Args:
@@ -71,22 +70,15 @@ def read_samples(
71
70
  Returns:
72
71
  A list of filtered samples.
73
72
  """
74
- result = sample_resolver.get_all_by_dataset_id(
73
+ return sample_resolver.get_all_by_dataset_id(
75
74
  session=session,
76
75
  dataset_id=dataset_id,
77
- # TODO(Michal, 06/2025): Pass the Paginated object directly.
78
- offset=body.pagination.offset if body.pagination else 0,
79
- limit=body.pagination.limit if body.pagination else 10,
76
+ pagination=body.pagination,
80
77
  filters=body.filters,
81
78
  text_embedding=body.text_embedding,
82
79
  sample_ids=body.sample_ids,
83
80
  )
84
81
 
85
- return SampleViewsWithCount(
86
- data=result.samples,
87
- total_count=result.total_count,
88
- )
89
-
90
82
 
91
83
  @samples_router.get("/samples/dimensions")
92
84
  def get_sample_dimensions(