lightly-studio 0.3.2__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 (115) hide show
  1. lightly_studio/__init__.py +1 -1
  2. lightly_studio/api/app.py +6 -4
  3. lightly_studio/api/db_tables.py +0 -3
  4. lightly_studio/api/routes/api/annotation.py +26 -0
  5. lightly_studio/api/routes/api/annotations/__init__.py +7 -0
  6. lightly_studio/api/routes/api/annotations/create_annotation.py +52 -0
  7. lightly_studio/api/routes/api/dataset.py +3 -5
  8. lightly_studio/api/routes/api/embeddings2d.py +104 -0
  9. lightly_studio/api/routes/api/export.py +73 -0
  10. lightly_studio/api/routes/api/selection.py +87 -0
  11. lightly_studio/core/add_samples.py +0 -9
  12. lightly_studio/core/dataset.py +32 -48
  13. lightly_studio/core/dataset_query/dataset_query.py +5 -0
  14. lightly_studio/dataset/env.py +4 -0
  15. lightly_studio/dataset/file_utils.py +13 -2
  16. lightly_studio/dataset/loader.py +0 -54
  17. lightly_studio/dataset/mobileclip_embedding_generator.py +3 -2
  18. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/0.CA_CXIBb.css +1 -0
  19. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_layout.DS78jgNY.css +1 -0
  20. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/{SelectableSvgGroup.BNTuXSAe.css → index.BVs_sZj9.css} +1 -1
  21. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/{SelectableSvgGroup.BBm0IWdq.css → transform.D487hwJk.css} +1 -1
  22. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/6t3IJ0vQ.js +1 -0
  23. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{gLNdjSzu.js → 8NsknIT2.js} +1 -1
  24. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{Cur71c3O.js → BND_-4Kp.js} +1 -1
  25. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{DRZO-E-T.js → BdfTHw61.js} +1 -1
  26. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{BqBqV92V.js → BfHVnyNT.js} +1 -1
  27. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BjkP1AHA.js +1 -0
  28. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BuuNVL9G.js +1 -0
  29. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{7YNGEs1C.js → BzKGpnl4.js} +1 -1
  30. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{C0JiMuYn.js → CCx7Ho51.js} +1 -1
  31. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{DcGCxgpH.js → CH6P3X75.js} +1 -1
  32. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CR2upx_Q.js +4 -0
  33. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CWPZrTTJ.js +1 -0
  34. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{Ccq4ZD0B.js → Cs1XmhiF.js} +1 -1
  35. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{OH7-C_mc.js → CwPowJfP.js} +1 -1
  36. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CxFKfZ9T.js +1 -0
  37. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Cxevwdid.js +1 -0
  38. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{C98Hk3r5.js → D4whDBUi.js} +1 -1
  39. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6r9vr07.js +1 -0
  40. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{DqUGznj_.js → DA6bFLPR.js} +1 -1
  41. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{2O287xak.js → DEgUu98i.js} +2 -2
  42. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{KpAtIldw.js → DGTPl6Gk.js} +1 -1
  43. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{Cs31G8Qn.js → DKGxBSlK.js} +1 -1
  44. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{D8GZDMNN.js → DQXoLcsF.js} +1 -1
  45. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DQe_kdRt.js +92 -0
  46. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DcY4jgG3.js +1 -0
  47. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{Crk-jcvV.js → RmD8FzRo.js} +1 -1
  48. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/V-MnMC1X.js +1 -0
  49. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{Df3aMO5B.js → keKYsoph.js} +1 -1
  50. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/{app.BI-EA5gL.js → app.BVr6DYqP.js} +2 -2
  51. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/start.u7zsVvqp.js +1 -0
  52. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/0.Da2agmdd.js +1 -0
  53. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{1._I9GR805.js → 1.B11tVRJV.js} +1 -1
  54. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{10.J2RBFrSr.js → 10.l30Zud4h.js} +1 -1
  55. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{12.Cmqj25a-.js → 12.CgKPGcAP.js} +1 -1
  56. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/2.C8HLK8mj.js +857 -0
  57. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{3.w9g4AcAx.js → 3.CLvg3QcJ.js} +1 -1
  58. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{4.BBI8KwnD.js → 4.BQhDtXUI.js} +1 -1
  59. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/5.-6XqWX5G.js +1 -0
  60. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{6.CrbkRPam.js → 6.uBV1Lhat.js} +1 -1
  61. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{7.FomEdhD6.js → 7.BXsgoQZh.js} +1 -1
  62. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/8.BkbcnUs8.js +1 -0
  63. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{9.CajIG5ce.js → 9.Bkrv-Vww.js} +1 -1
  64. lightly_studio/dist_lightly_studio_view_app/_app/immutable/workers/clustering.worker-DKqeLtG0.js +2 -0
  65. lightly_studio/dist_lightly_studio_view_app/_app/immutable/workers/search.worker-vNSty3B0.js +1 -0
  66. lightly_studio/dist_lightly_studio_view_app/_app/version.json +1 -1
  67. lightly_studio/dist_lightly_studio_view_app/index.html +14 -14
  68. lightly_studio/export/export_dataset.py +65 -0
  69. lightly_studio/export/lightly_studio_label_input.py +120 -0
  70. lightly_studio/few_shot_classifier/classifier_manager.py +5 -26
  71. lightly_studio/metadata/compute_typicality.py +67 -0
  72. lightly_studio/models/annotation/annotation_base.py +11 -12
  73. lightly_studio/resolvers/annotation_label_resolver/__init__.py +2 -1
  74. lightly_studio/resolvers/annotation_label_resolver/get_all.py +15 -0
  75. lightly_studio/resolvers/annotation_resolver/__init__.py +2 -3
  76. lightly_studio/resolvers/annotation_resolver/create_many.py +3 -3
  77. lightly_studio/resolvers/annotation_resolver/delete_annotation.py +1 -1
  78. lightly_studio/resolvers/annotation_resolver/delete_annotations.py +7 -3
  79. lightly_studio/resolvers/annotation_resolver/get_by_id.py +19 -1
  80. lightly_studio/resolvers/annotation_resolver/update_annotation_label.py +0 -1
  81. lightly_studio/resolvers/annotations/annotations_filter.py +1 -11
  82. lightly_studio/selection/mundig.py +7 -10
  83. lightly_studio/selection/selection_config.py +4 -1
  84. lightly_studio/services/annotations_service/__init__.py +8 -0
  85. lightly_studio/services/annotations_service/create_annotation.py +63 -0
  86. lightly_studio/services/annotations_service/delete_annotation.py +22 -0
  87. {lightly_studio-0.3.2.dist-info → lightly_studio-0.3.3.dist-info}/METADATA +152 -27
  88. {lightly_studio-0.3.2.dist-info → lightly_studio-0.3.3.dist-info}/RECORD +89 -85
  89. lightly_studio/api/routes/api/annotation_task.py +0 -37
  90. lightly_studio/api/routes/api/metrics.py +0 -76
  91. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/0.DenzbfeK.css +0 -1
  92. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_layout.T-zjSUd3.css +0 -1
  93. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BBoGk9hq.js +0 -1
  94. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BRnH9v23.js +0 -92
  95. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bg1Y5eUZ.js +0 -1
  96. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CG0dMCJi.js +0 -1
  97. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Cpy-nab_.js +0 -1
  98. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CsKrY2zA.js +0 -1
  99. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CzgC3GFB.js +0 -1
  100. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DFRh-Spp.js +0 -1
  101. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DkR_EZ_B.js +0 -1
  102. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/M1Q1F7bw.js +0 -4
  103. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/i0ZZ4z06.js +0 -1
  104. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/start.CcsRl3cZ.js +0 -1
  105. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/0.BbO4Zc3r.js +0 -1
  106. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/2.C45iKJHA.js +0 -6
  107. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/5.huHuxdiF.js +0 -1
  108. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/8.Cb_ADSLk.js +0 -1
  109. lightly_studio/metrics/__init__.py +0 -0
  110. lightly_studio/metrics/detection/__init__.py +0 -0
  111. lightly_studio/metrics/detection/map.py +0 -268
  112. lightly_studio/models/annotation_task.py +0 -28
  113. lightly_studio/resolvers/annotation_resolver/create.py +0 -19
  114. lightly_studio/resolvers/annotation_task_resolver.py +0 -31
  115. {lightly_studio-0.3.2.dist-info → lightly_studio-0.3.3.dist-info}/WHEEL +0 -0
@@ -6,6 +6,6 @@ from lightly_studio.core.start_gui import start_gui
6
6
 
7
7
  # TODO (Jonas 08/25): This will be removed as soon as the new interface is used in the examples
8
8
  from lightly_studio.dataset.loader import DatasetLoader
9
- from lightly_studio.models.annotation_task import AnnotationType
9
+ from lightly_studio.models.annotation.annotation_base import AnnotationType
10
10
 
11
11
  __all__ = ["AnnotationType", "Dataset", "DatasetLoader", "start_gui"]
lightly_studio/api/app.py CHANGED
@@ -16,14 +16,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
  )
@@ -9,6 +9,7 @@ from fastapi.params import Query
9
9
  from pydantic import BaseModel
10
10
  from typing_extensions import Annotated
11
11
 
12
+ from lightly_studio.api.routes.api import annotations as annotations_module
12
13
  from lightly_studio.api.routes.api.dataset import get_and_validate_dataset_id
13
14
  from lightly_studio.api.routes.api.status import (
14
15
  HTTP_STATUS_CREATED,
@@ -36,6 +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"])
40
+ annotations_router.include_router(annotations_module.create_annotation_router)
39
41
 
40
42
 
41
43
  @annotations_router.get("/annotations/count")
@@ -221,3 +223,27 @@ def remove_tag_from_annotation(
221
223
  raise HTTPException(status_code=HTTP_STATUS_NOT_FOUND, detail=f"Tag {tag_id} not found")
222
224
 
223
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
@@ -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
+ )
@@ -108,6 +108,7 @@ def delete_dataset(
108
108
  return {"status": "deleted"}
109
109
 
110
110
 
111
+ # TODO(Michal, 09/2025): Move to export.py
111
112
  class ExportBody(BaseModel):
112
113
  """body parameters for including or excluding tag_ids or sample_ids."""
113
114
 
@@ -123,6 +124,7 @@ class ExportBody(BaseModel):
123
124
  # of sample_ids, it is a POST request to avoid URL length limitations.
124
125
  # A body with a GET request is supported by fastAPI however it has undefined
125
126
  # behavior: https://fastapi.tiangolo.com/tutorial/body/
127
+ # TODO(Michal, 09/2025): Move to export.py
126
128
  @dataset_router.post(
127
129
  "/datasets/{dataset_id}/export",
128
130
  )
@@ -155,11 +157,7 @@ def export_dataset_to_absolute_paths(
155
157
  return response
156
158
 
157
159
 
158
- """
159
- Endpoint to export samples from a dataset.
160
- """
161
-
162
-
160
+ # TODO(Michal, 09/2025): Move to export.py
163
161
  @dataset_router.post(
164
162
  "/datasets/{dataset_id}/export/stats",
165
163
  )
@@ -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()
@@ -0,0 +1,87 @@
1
+ """This module contains the API routes for managing selections."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Union
6
+
7
+ from fastapi import APIRouter, Depends, HTTPException
8
+ from pydantic import BaseModel, Field
9
+ from typing_extensions import Annotated
10
+
11
+ from lightly_studio.api.routes.api.dataset import get_and_validate_dataset_id
12
+ from lightly_studio.db_manager import SessionDep
13
+ from lightly_studio.models.dataset import DatasetTable
14
+ from lightly_studio.resolvers import sample_resolver
15
+ from lightly_studio.selection.select_via_db import select_via_database
16
+ from lightly_studio.selection.selection_config import (
17
+ EmbeddingDiversityStrategy,
18
+ MetadataWeightingStrategy,
19
+ SelectionConfig,
20
+ )
21
+
22
+ selection_router = APIRouter()
23
+
24
+ Strategy = Annotated[
25
+ Union[EmbeddingDiversityStrategy, MetadataWeightingStrategy],
26
+ Field(discriminator="strategy_name"),
27
+ ]
28
+
29
+
30
+ class SelectionRequest(BaseModel):
31
+ """Request model for selection."""
32
+
33
+ n_samples_to_select: int = Field(gt=0, description="Number of samples to select")
34
+ selection_result_tag_name: str = Field(min_length=1, description="Name for the result tag")
35
+ strategies: list[Strategy]
36
+
37
+
38
+ @selection_router.post(
39
+ "/datasets/{dataset_id}/selection",
40
+ status_code=204,
41
+ response_model=None,
42
+ )
43
+ def create_combination_selection(
44
+ session: SessionDep,
45
+ dataset: Annotated[
46
+ DatasetTable,
47
+ Depends(get_and_validate_dataset_id),
48
+ ],
49
+ request: SelectionRequest,
50
+ ) -> None:
51
+ """Create a combination selection on the dataset.
52
+
53
+ This endpoint performs combination selection using embeddings and metadata.
54
+ The selected samples are tagged with the specified tag name.
55
+
56
+ Args:
57
+ session: Database session dependency.
58
+ dataset: Dataset to perform selection on.
59
+ request: Selection parameters including sample count and tag name.
60
+
61
+ Returns:
62
+ None (204 No Content on success).
63
+
64
+ Raises:
65
+ HTTPException: 400 if selection fails due to invalid parameters or other errors.
66
+ """
67
+ # Get all samples in dataset as input for selection.
68
+ all_samples_result = sample_resolver.get_all_by_dataset_id(
69
+ session=session, dataset_id=dataset.dataset_id
70
+ )
71
+ input_sample_ids = [sample.sample_id for sample in all_samples_result.samples]
72
+ # Validate we have enough samples to select from.
73
+ if len(input_sample_ids) < request.n_samples_to_select:
74
+ raise HTTPException(
75
+ status_code=400,
76
+ detail=f"Dataset has only {len(input_sample_ids)} samples, "
77
+ f"cannot select {request.n_samples_to_select}",
78
+ )
79
+ # Create SelectionConfig with diversity strategy.
80
+ config = SelectionConfig(
81
+ dataset_id=dataset.dataset_id,
82
+ n_samples_to_select=request.n_samples_to_select,
83
+ selection_result_tag_name=request.selection_result_tag_name,
84
+ strategies=request.strategies,
85
+ )
86
+ # Perform selection via database.
87
+ select_via_database(session=session, config=config, input_sample_ids=input_sample_ids)
@@ -46,7 +46,6 @@ class _AnnotationProcessingContext:
46
46
  dataset_id: UUID
47
47
  sample_id: UUID
48
48
  label_map: dict[int, UUID]
49
- annotation_task_id: UUID
50
49
 
51
50
 
52
51
  @dataclass
@@ -137,7 +136,6 @@ def load_into_dataset_from_labelformat(
137
136
  dataset_id: UUID,
138
137
  input_labels: ObjectDetectionInput | InstanceSegmentationInput,
139
138
  images_path: Path,
140
- annotation_task_id: UUID,
141
139
  ) -> list[UUID]:
142
140
  """Load samples and their annotations from a labelformat input into the dataset.
143
141
 
@@ -146,7 +144,6 @@ def load_into_dataset_from_labelformat(
146
144
  dataset_id: The ID of the dataset to load samples into.
147
145
  input_labels: The labelformat input containing images and annotations.
148
146
  images_path: The path to the directory containing the images.
149
- annotation_task_id: The ID of the annotation task to associate with the annotations.
150
147
 
151
148
  Returns:
152
149
  A list of UUIDs of the created samples.
@@ -192,7 +189,6 @@ def load_into_dataset_from_labelformat(
192
189
  image_path_to_anno_data=image_path_to_anno_data,
193
190
  dataset_id=dataset_id,
194
191
  label_map=label_map,
195
- annotation_task_id=annotation_task_id,
196
192
  annotations_to_create=annotations_to_create,
197
193
  )
198
194
  samples_to_create.clear()
@@ -210,7 +206,6 @@ def load_into_dataset_from_labelformat(
210
206
  image_path_to_anno_data=image_path_to_anno_data,
211
207
  dataset_id=dataset_id,
212
208
  label_map=label_map,
213
- annotation_task_id=annotation_task_id,
214
209
  annotations_to_create=annotations_to_create,
215
210
  )
216
211
 
@@ -304,7 +299,6 @@ def _process_object_detection_annotations(
304
299
  width=int(width),
305
300
  height=int(height),
306
301
  confidence=obj.confidence,
307
- annotation_task_id=context.annotation_task_id,
308
302
  )
309
303
  )
310
304
  return new_annotations
@@ -339,7 +333,6 @@ def _process_instance_segmentation_annotations(
339
333
  width=int(width),
340
334
  height=int(height),
341
335
  segmentation_mask=segmentation_rle,
342
- annotation_task_id=context.annotation_task_id,
343
336
  )
344
337
  )
345
338
  return new_annotations
@@ -351,7 +344,6 @@ def _process_batch_annotations( # noqa: PLR0913
351
344
  image_path_to_anno_data: dict[str, ImageInstanceSegmentation | ImageObjectDetection],
352
345
  dataset_id: UUID,
353
346
  label_map: dict[int, UUID],
354
- annotation_task_id: UUID,
355
347
  annotations_to_create: list[AnnotationCreate],
356
348
  ) -> None:
357
349
  """Process annotations for a batch of samples."""
@@ -362,7 +354,6 @@ def _process_batch_annotations( # noqa: PLR0913
362
354
  dataset_id=dataset_id,
363
355
  sample_id=stored_sample.sample_id,
364
356
  label_map=label_map,
365
- annotation_task_id=annotation_task_id,
366
357
  )
367
358
 
368
359
  if isinstance(anno_data, ImageInstanceSegmentation):