lightly-studio 0.3.3__py3-none-any.whl → 0.4.0__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 (137) hide show
  1. lightly_studio/api/app.py +2 -0
  2. lightly_studio/api/features.py +3 -5
  3. lightly_studio/api/routes/api/caption.py +30 -0
  4. lightly_studio/api/routes/api/dataset_tag.py +10 -0
  5. lightly_studio/api/routes/api/embeddings2d.py +42 -39
  6. lightly_studio/api/routes/api/metadata.py +57 -1
  7. lightly_studio/core/add_samples.py +138 -0
  8. lightly_studio/core/dataset.py +232 -18
  9. lightly_studio/core/dataset_query/__init__.py +14 -0
  10. lightly_studio/core/sample.py +33 -1
  11. lightly_studio/dataset/loader.py +2 -8
  12. lightly_studio/db_manager.py +14 -6
  13. lightly_studio/dist_lightly_studio_view_app/_app/env.js +1 -1
  14. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/0.CN4hnTks.css +1 -0
  15. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/2.CkOblLn7.css +1 -0
  16. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/Samples.C0_eo9eP.css +1 -0
  17. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/{useFeatureFlags.CV-KWLNP.css → _layout.CefECEWA.css} +1 -1
  18. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_layout.kFFGI0zL.css +1 -0
  19. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/transform.sLzR40om.css +1 -0
  20. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{6t3IJ0vQ.js → BOmrKuMn.js} +1 -1
  21. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{Cs1XmhiF.js → BPpOWbDa.js} +1 -1
  22. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BaFFwDFr.js +1 -0
  23. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BiGQqqJP.js +1 -0
  24. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BrNKoXwc.js +20 -0
  25. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BsaJCCG_.js +96 -0
  26. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BtXGzlpP.js +20 -0
  27. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/C1FmrZbK.js +1 -0
  28. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/C3xJX0nD.js +1 -0
  29. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CANX9QXL.js +1 -0
  30. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CAPx0Bfm.js +1 -0
  31. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CP9M7pei.js +39 -0
  32. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CWuDkrMZ.js +436 -0
  33. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/ChlxSwqI.js +1 -0
  34. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Cj4nZbtb.js +1 -0
  35. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/ClzkJBWk.js +1 -0
  36. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CpbA3HU7.js +2 -0
  37. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D8ZGoCPm.js +3 -0
  38. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DMJzr1NB.js +1 -0
  39. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{BdfTHw61.js → DNJnBfHs.js} +1 -1
  40. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{keKYsoph.js → DUtlYNuP.js} +1 -1
  41. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DVxjPOJB.js +1 -0
  42. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DmGM9V9Q.js +1 -0
  43. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DoEId1MK.js +1 -0
  44. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DthpwYR_.js +2 -0
  45. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DyIcJj6J.js +1 -0
  46. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/SiegjVo0.js +1 -0
  47. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{BfHVnyNT.js → WEyXQRi6.js} +1 -1
  48. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/gBp1tBnA.js +1 -0
  49. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/xQhUoIl9.js +1 -0
  50. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/app.Y-sSoz5q.js +2 -0
  51. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/start.CvxVp0Cu.js +1 -0
  52. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/0.0Fm6E-5B.js +4 -0
  53. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/1.DB-0vkHb.js +1 -0
  54. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/10.vaUePh5k.js +1 -0
  55. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/11.7i7ljNVT.js +1 -0
  56. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/13.9qy3WtZv.js +1 -0
  57. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{2.C8HLK8mj.js → 2.Drwwdm7A.js} +267 -111
  58. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{3.CLvg3QcJ.js → 3.D3X_-Wan.js} +1 -1
  59. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{4.BQhDtXUI.js → 4.C9TqY3tA.js} +1 -1
  60. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/5.iRw6HCWX.js +39 -0
  61. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{6.uBV1Lhat.js → 6.fqfYR7dB.js} +1 -1
  62. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/7.C7gMM-gk.js +1 -0
  63. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/8.C4v1w-oS.js +20 -0
  64. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/9.DbHcSiMn.js +1 -0
  65. lightly_studio/dist_lightly_studio_view_app/_app/version.json +1 -1
  66. lightly_studio/dist_lightly_studio_view_app/index.html +15 -14
  67. lightly_studio/examples/example.py +4 -0
  68. lightly_studio/examples/example_coco.py +4 -0
  69. lightly_studio/examples/example_coco_caption.py +24 -0
  70. lightly_studio/examples/example_metadata.py +4 -1
  71. lightly_studio/examples/example_selection.py +4 -0
  72. lightly_studio/examples/example_split_work.py +4 -0
  73. lightly_studio/examples/example_yolo.py +4 -0
  74. lightly_studio/export/export_dataset.py +11 -3
  75. lightly_studio/metadata/compute_typicality.py +1 -1
  76. lightly_studio/models/caption.py +74 -0
  77. lightly_studio/models/dataset.py +1 -2
  78. lightly_studio/models/metadata.py +1 -1
  79. lightly_studio/models/sample.py +9 -2
  80. lightly_studio/models/settings.py +5 -0
  81. lightly_studio/resolvers/caption_resolver.py +80 -0
  82. lightly_studio/resolvers/dataset_resolver.py +6 -11
  83. lightly_studio/resolvers/metadata_resolver/__init__.py +2 -2
  84. lightly_studio/resolvers/metadata_resolver/sample/__init__.py +3 -3
  85. lightly_studio/resolvers/metadata_resolver/sample/bulk_update_metadata.py +46 -0
  86. lightly_studio/resolvers/sample_resolver.py +1 -0
  87. lightly_studio/resolvers/samples_filter.py +18 -10
  88. lightly_studio/resolvers/settings_resolver.py +3 -0
  89. lightly_studio/resolvers/twodim_embedding_resolver.py +29 -0
  90. lightly_studio/selection/__init__.py +1 -0
  91. lightly_studio/selection/mundig.py +41 -0
  92. lightly_studio/type_definitions.py +2 -0
  93. lightly_studio-0.4.0.dist-info/METADATA +78 -0
  94. {lightly_studio-0.3.3.dist-info → lightly_studio-0.4.0.dist-info}/RECORD +96 -88
  95. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/0.CA_CXIBb.css +0 -1
  96. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_layout.DS78jgNY.css +0 -1
  97. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/index.BVs_sZj9.css +0 -1
  98. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/transform.D487hwJk.css +0 -1
  99. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/8NsknIT2.js +0 -1
  100. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BND_-4Kp.js +0 -1
  101. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BjkP1AHA.js +0 -1
  102. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BuuNVL9G.js +0 -1
  103. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BzKGpnl4.js +0 -1
  104. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CCx7Ho51.js +0 -1
  105. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CH6P3X75.js +0 -1
  106. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CR2upx_Q.js +0 -4
  107. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CWPZrTTJ.js +0 -1
  108. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CwPowJfP.js +0 -1
  109. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CxFKfZ9T.js +0 -1
  110. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Cxevwdid.js +0 -1
  111. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D4whDBUi.js +0 -1
  112. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6r9vr07.js +0 -1
  113. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DA6bFLPR.js +0 -1
  114. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DEgUu98i.js +0 -3
  115. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DGTPl6Gk.js +0 -1
  116. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DKGxBSlK.js +0 -1
  117. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DQXoLcsF.js +0 -1
  118. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DQe_kdRt.js +0 -92
  119. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DcY4jgG3.js +0 -1
  120. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/H7C68rOM.js +0 -1
  121. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/RmD8FzRo.js +0 -1
  122. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/V-MnMC1X.js +0 -1
  123. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/app.BVr6DYqP.js +0 -2
  124. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/start.u7zsVvqp.js +0 -1
  125. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/0.Da2agmdd.js +0 -1
  126. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/1.B11tVRJV.js +0 -1
  127. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/10.l30Zud4h.js +0 -1
  128. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/12.CgKPGcAP.js +0 -1
  129. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/5.-6XqWX5G.js +0 -1
  130. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/7.BXsgoQZh.js +0 -1
  131. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/8.BkbcnUs8.js +0 -1
  132. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/9.Bkrv-Vww.js +0 -1
  133. lightly_studio/resolvers/metadata_resolver/sample/bulk_set_metadata.py +0 -48
  134. lightly_studio/selection/README.md +0 -6
  135. lightly_studio-0.3.3.dist-info/METADATA +0 -814
  136. /lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{11.CWG1ehzT.js → 12.CWG1ehzT.js} +0 -0
  137. {lightly_studio-0.3.3.dist-info → lightly_studio-0.4.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,80 @@
1
+ """Resolvers for caption."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from uuid import UUID
7
+
8
+ from pydantic import BaseModel
9
+ from sqlmodel import Session, col, func, select
10
+
11
+ from lightly_studio.api.routes.api.validators import Paginated
12
+ from lightly_studio.models.caption import CaptionCreate, CaptionTable
13
+
14
+
15
+ class GetAllCaptionsResult(BaseModel):
16
+ """Result wrapper for caption listings."""
17
+
18
+ captions: Sequence[CaptionTable]
19
+ total_count: int
20
+ next_cursor: int | None = None
21
+
22
+
23
+ def create_many(session: Session, captions: Sequence[CaptionCreate]) -> list[CaptionTable]:
24
+ """Create many captions in bulk.
25
+
26
+ Args:
27
+ session: Database session
28
+ captions: The captions to create
29
+
30
+ Returns:
31
+ The created captions
32
+ """
33
+ if not captions:
34
+ return []
35
+
36
+ db_captions = [CaptionTable.model_validate(caption) for caption in captions]
37
+ session.bulk_save_objects(db_captions)
38
+ session.commit()
39
+ return db_captions
40
+
41
+
42
+ def get_all(
43
+ session: Session,
44
+ dataset_id: UUID,
45
+ pagination: Paginated | None = None,
46
+ ) -> GetAllCaptionsResult:
47
+ """Get all captions from the database.
48
+
49
+ Args:
50
+ session: Database session
51
+ dataset_id: dataset_id parameter to filter the query
52
+ pagination: Optional pagination parameters
53
+
54
+ Returns:
55
+ List of captions matching the filters, total number of captions, next cursor (pagination)
56
+ """
57
+ query = select(CaptionTable).order_by(
58
+ col(CaptionTable.created_at).asc(),
59
+ col(CaptionTable.caption_id).asc(),
60
+ )
61
+ count_query = select(func.count()).select_from(CaptionTable)
62
+
63
+ query = query.where(CaptionTable.dataset_id == dataset_id)
64
+ count_query = count_query.where(CaptionTable.dataset_id == dataset_id)
65
+
66
+ if pagination is not None:
67
+ query = query.offset(pagination.offset).limit(pagination.limit)
68
+
69
+ captions = session.exec(query).all()
70
+ total_count = session.exec(count_query).one()
71
+
72
+ next_cursor: int | None = None
73
+ if pagination and pagination.offset + pagination.limit < total_count:
74
+ next_cursor = pagination.offset + pagination.limit
75
+
76
+ return GetAllCaptionsResult(
77
+ captions=captions,
78
+ total_count=total_count,
79
+ next_cursor=next_cursor,
80
+ )
@@ -41,6 +41,9 @@ class ExportFilter(BaseModel):
41
41
 
42
42
  def create(session: Session, dataset: DatasetCreate) -> DatasetTable:
43
43
  """Create a new dataset in the database."""
44
+ existing = get_by_name(session=session, name=dataset.name)
45
+ if existing:
46
+ raise ValueError(f"Dataset with name '{dataset.name}' already exists.")
44
47
  db_dataset = DatasetTable.model_validate(dataset)
45
48
  session.add(db_dataset)
46
49
  session.commit()
@@ -69,12 +72,7 @@ def get_by_id(session: Session, dataset_id: UUID) -> DatasetTable | None:
69
72
 
70
73
  def get_by_name(session: Session, name: str) -> DatasetTable | None:
71
74
  """Retrieve a single dataset by name."""
72
- datasets = session.exec(select(DatasetTable).where(DatasetTable.name == name)).all()
73
- if len(datasets) == 0:
74
- return None
75
- if len(datasets) > 1:
76
- raise ValueError(f"Cannot retrieve a dataset, found multiple with name '{name}'.")
77
- return datasets[0]
75
+ return session.exec(select(DatasetTable).where(DatasetTable.name == name)).one_or_none()
78
76
 
79
77
 
80
78
  def update(session: Session, dataset_id: UUID, dataset_data: DatasetCreate) -> DatasetTable:
@@ -84,7 +82,6 @@ def update(session: Session, dataset_id: UUID, dataset_data: DatasetCreate) -> D
84
82
  raise ValueError(f"Dataset ID was not found '{dataset_id}'.")
85
83
 
86
84
  dataset.name = dataset_data.name
87
- dataset.directory = dataset_data.directory
88
85
  dataset.updated_at = datetime.now(timezone.utc)
89
86
 
90
87
  session.commit()
@@ -232,10 +229,8 @@ def _build_export_query( # noqa: C901
232
229
  raise ValueError("Invalid include or export filter combination.")
233
230
 
234
231
 
235
- # TODO: this fn should be moved to a "business logic" layer outside of the
236
- # resolvers and abstracted in to reusable components.
237
- # https://linear.app/lightly/issue/LIG-6196/figure-out-architecture-follow-up-changes-in-python-package
238
- # TODO: this should be abstracted to allow different export formats.
232
+ # TODO(Michal, 10/2025): Consider moving the export logic to a separate service.
233
+ # This is a legacy code from the initial implementation of the export feature.
239
234
  def export(
240
235
  session: Session,
241
236
  dataset_id: UUID,
@@ -1,14 +1,14 @@
1
1
  """Metadata resolver module."""
2
2
 
3
3
  from lightly_studio.resolvers.metadata_resolver.sample import (
4
- bulk_set_metadata,
4
+ bulk_update_metadata,
5
5
  get_by_sample_id,
6
6
  get_value_for_sample,
7
7
  set_value_for_sample,
8
8
  )
9
9
 
10
10
  __all__ = [
11
- "bulk_set_metadata",
11
+ "bulk_update_metadata",
12
12
  "get_by_sample_id",
13
13
  "get_value_for_sample",
14
14
  "set_value_for_sample",
@@ -1,7 +1,7 @@
1
1
  """Resolvers for metadata operations."""
2
2
 
3
- from .bulk_set_metadata import (
4
- bulk_set_metadata,
3
+ from .bulk_update_metadata import (
4
+ bulk_update_metadata,
5
5
  )
6
6
  from .get_by_sample_id import (
7
7
  get_by_sample_id,
@@ -14,7 +14,7 @@ from .set_value_for_sample import (
14
14
  )
15
15
 
16
16
  __all__ = [
17
- "bulk_set_metadata",
17
+ "bulk_update_metadata",
18
18
  "get_by_sample_id",
19
19
  "get_value_for_sample",
20
20
  "set_value_for_sample",
@@ -0,0 +1,46 @@
1
+ """Resolver for operations for setting metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+ from uuid import UUID
7
+
8
+ from sqlmodel import Session, col, select
9
+
10
+ from lightly_studio.models.metadata import SampleMetadataTable
11
+
12
+
13
+ def bulk_update_metadata(
14
+ session: Session,
15
+ sample_metadata: list[tuple[UUID, dict[str, Any]]],
16
+ ) -> None:
17
+ """Bulk insert or update metadata for multiple samples.
18
+
19
+ If a sample does not have metadata, a new metadata row is created.
20
+ If a sample already has metadata, the new key-value pairs are merged with the existing metadata.
21
+
22
+ Args:
23
+ session: The database session.
24
+ sample_metadata: List of (sample_id, metadata_dict) tuples.
25
+ """
26
+ # TODO(Mihnea, 10/2025): Consider using SQLAlchemy's bulk operations
27
+ # (Session.bulk_insert/update_mappings) if performance becomes a bottleneck.
28
+ if not sample_metadata:
29
+ return
30
+
31
+ # Get all existing metadata rows for the given sample IDs.
32
+ sample_ids = [s[0] for s in sample_metadata]
33
+ existing_metadata = session.exec(
34
+ select(SampleMetadataTable).where(col(SampleMetadataTable.sample_id).in_(sample_ids))
35
+ ).all()
36
+ sample_id_to_existing_metadata = {meta.sample_id: meta for meta in existing_metadata}
37
+
38
+ for sample_id, new_metadata in sample_metadata:
39
+ metadata = sample_id_to_existing_metadata.get(
40
+ sample_id, SampleMetadataTable(sample_id=sample_id)
41
+ )
42
+ for key, value in new_metadata.items():
43
+ metadata.set_value(key, value)
44
+ session.add(metadata)
45
+
46
+ session.commit()
@@ -109,6 +109,7 @@ def get_all_by_dataset_id( # noqa: PLR0913
109
109
  joinedload(AnnotationBaseTable.instance_segmentation_details),
110
110
  joinedload(AnnotationBaseTable.semantic_segmentation_details),
111
111
  ),
112
+ selectinload(SampleTable.captions),
112
113
  selectinload(SampleTable.tags),
113
114
  # Ignore type checker error below as it's a false positive caused by TYPE_CHECKING.
114
115
  joinedload(SampleTable.metadata_dict), # type: ignore[arg-type]
@@ -33,20 +33,15 @@ class SampleFilter(BaseModel):
33
33
  annotation_label_ids: Optional[List[UUID]] = None
34
34
  tag_ids: Optional[List[UUID]] = None
35
35
  metadata_filters: Optional[List[MetadataFilter]] = None
36
+ sample_ids: Optional[List[UUID]] = None
36
37
 
37
38
  def apply(self, query: QueryType) -> QueryType:
38
39
  """Apply the filters to the given query."""
40
+ if self.sample_ids:
41
+ query = query.where(col(SampleTable.sample_id).in_(self.sample_ids))
42
+
39
43
  # Apply dimension-based filters to the query.
40
- if self.width:
41
- if self.width.min is not None:
42
- query = query.where(SampleTable.width >= self.width.min)
43
- if self.width.max is not None:
44
- query = query.where(SampleTable.width <= self.width.max)
45
- if self.height:
46
- if self.height.min is not None:
47
- query = query.where(SampleTable.height >= self.height.min)
48
- if self.height.max is not None:
49
- query = query.where(SampleTable.height <= self.height.max)
44
+ query = self._apply_dimension_filters(query)
50
45
 
51
46
  # Apply annotation label filters to the query.
52
47
  if self.annotation_label_ids:
@@ -79,3 +74,16 @@ class SampleFilter(BaseModel):
79
74
  metadata_join_condition=SampleMetadataTable.sample_id == SampleTable.sample_id,
80
75
  )
81
76
  return query
77
+
78
+ def _apply_dimension_filters(self, query: QueryType) -> QueryType:
79
+ if self.width:
80
+ if self.width.min is not None:
81
+ query = query.where(SampleTable.width >= self.width.min)
82
+ if self.width.max is not None:
83
+ query = query.where(SampleTable.width <= self.width.max)
84
+ if self.height:
85
+ if self.height.min is not None:
86
+ query = query.where(SampleTable.height >= self.height.min)
87
+ if self.height.max is not None:
88
+ query = query.where(SampleTable.height <= self.height.max)
89
+ return query
@@ -52,6 +52,9 @@ def set_settings(session: Session, settings: SettingView) -> SettingView:
52
52
  # Update show annotation text labels
53
53
  current_settings.show_annotation_text_labels = settings.show_annotation_text_labels
54
54
 
55
+ # Update sample filename visibility
56
+ current_settings.show_sample_filenames = settings.show_sample_filenames
57
+
55
58
  session.commit()
56
59
  session.refresh(current_settings)
57
60
 
@@ -0,0 +1,29 @@
1
+ """Handler for getting 2D embeddings from high-dimensional embeddings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from lightly_mundig import TwoDimEmbedding # type: ignore[import-untyped]
6
+
7
+ from lightly_studio.dataset.env import LIGHTLY_STUDIO_LICENSE_KEY
8
+
9
+
10
+ # TODO(Malte, 10/2025): Add the get_twodim_embeddings function here that handles the
11
+ # caching in the DB and calls _calculate_2d_embeddings when needed.
12
+ def _calculate_2d_embeddings(embedding_values: list[list[float]]) -> list[tuple[float, float]]:
13
+ n_samples = len(embedding_values)
14
+ # For 0, 1 or 2 samples we hard-code deterministic coordinates.
15
+ if n_samples == 0:
16
+ return []
17
+ if n_samples == 1:
18
+ return [(0.0, 0.0)]
19
+ if n_samples == 2: # noqa: PLR2004
20
+ return [(0.0, 0.0), (1.0, 1.0)]
21
+
22
+ license_key = LIGHTLY_STUDIO_LICENSE_KEY
23
+ if license_key is None:
24
+ raise ValueError(
25
+ "LIGHTLY_STUDIO_LICENSE_KEY environment variable is not set. "
26
+ "Please set it to your LightlyStudio license key."
27
+ )
28
+ embedding_calculator = TwoDimEmbedding(embedding_values, license_key)
29
+ return embedding_calculator.calculate_2d_embedding() # type: ignore[no-any-return]
@@ -0,0 +1 @@
1
+ """Selection package."""
@@ -81,6 +81,47 @@ class Mundig:
81
81
  self._check_consistent_input_size(weights_ndarray.shape[0])
82
82
  self.mundig.add_weighting_strategy(weights=weights_ndarray, strength=strength)
83
83
 
84
+ def add_class_balancing(
85
+ self,
86
+ class_distributions: Iterable[Iterable[float]],
87
+ target: Iterable[float],
88
+ strength: float = 1.0,
89
+ ) -> None:
90
+ """Add a class balancing strategy.
91
+
92
+ This strategy aims to select a subset of samples such that the
93
+ distribution of classes in the subset is close to the target
94
+ distribution.
95
+
96
+ Args:
97
+ class_distributions:
98
+ First dimension is over all samples, second one is the distribution per sample over
99
+ the classes.
100
+ target:
101
+ The desired target distribution for the classes in the selected subset of samples.
102
+ The length of the target must match the number of classes in the class
103
+ distributions.
104
+ strength:
105
+ The strength of the balancing strategy.
106
+ """
107
+ # Convert to ndarray with float32 dtype if not already
108
+ if isinstance(class_distributions, np.ndarray) and class_distributions.dtype == np.float32:
109
+ class_distributions_nparray = class_distributions
110
+ else:
111
+ class_distributions_nparray = np.array(class_distributions, dtype=np.float32)
112
+ self._check_consistent_input_size(class_distributions_nparray.shape[0])
113
+ target_nparray = np.array(target, dtype=np.float32)
114
+ if class_distributions_nparray.shape[1] != target_nparray.shape[0]:
115
+ raise ValueError(
116
+ f"The length of 'target' {target_nparray.shape[0]} doesn't match the width of "
117
+ f"'class_distributions': {class_distributions_nparray.shape[0]}"
118
+ )
119
+ self.mundig.add_balancing_strategy(
120
+ class_distributions=class_distributions_nparray,
121
+ target=target_nparray,
122
+ strength=strength,
123
+ )
124
+
84
125
  def _check_consistent_input_size(self, n_input_samples_strategy: int) -> None:
85
126
  """Assert that input samples count is consistent across strategies.
86
127
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  from pathlib import Path
4
4
  from typing import TypeVar, Union
5
+ from uuid import UUID
5
6
 
6
7
  from sqlmodel.sql.expression import SelectOfScalar
7
8
 
@@ -14,6 +15,7 @@ QueryType = TypeVar(
14
15
  SelectOfScalar[AnnotationBaseTable],
15
16
  SelectOfScalar[SampleTable],
16
17
  SelectOfScalar[int],
18
+ SelectOfScalar[UUID],
17
19
  )
18
20
 
19
21
  PathLike = Union[str, Path]
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: lightly-studio
3
+ Version: 0.4.0
4
+ Summary: LightlyStudio is a lightweight, fast, and easy-to-use data exploration tool for data scientists and engineers.
5
+ Classifier: Operating System :: MacOS :: MacOS X
6
+ Classifier: Operating System :: Microsoft :: Windows
7
+ Classifier: Operating System :: POSIX :: Linux
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.8
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Python: <3.13,>=3.8
15
+ Requires-Dist: annotated-types==0.7.0
16
+ Requires-Dist: duckdb-engine<0.17,>=0.15.0
17
+ Requires-Dist: duckdb<1.3,>=1.2.2
18
+ Requires-Dist: environs<12.0.0
19
+ Requires-Dist: eval-type-backport>=0.2.2
20
+ Requires-Dist: fastapi>=0.115.5
21
+ Requires-Dist: faster-coco-eval>=1.6.5
22
+ Requires-Dist: fsspec>=2023.1.0
23
+ Requires-Dist: labelformat>=0.1.7
24
+ Requires-Dist: lightly-mundig==0.1.7
25
+ Requires-Dist: open-clip-torch>=2.20.0
26
+ Requires-Dist: pyarrow>=17.0.0
27
+ Requires-Dist: python-multipart>=0.0.20
28
+ Requires-Dist: scikit-learn==1.3.2
29
+ Requires-Dist: sqlmodel>=0.0.22
30
+ Requires-Dist: tqdm>=4.65.0
31
+ Requires-Dist: typing-extensions>=4.12.2
32
+ Requires-Dist: uvicorn>=0.32.1
33
+ Requires-Dist: xxhash>=3.5.0
34
+ Provides-Extra: cloud-storage
35
+ Requires-Dist: adlfs>=2023.1.0; extra == 'cloud-storage'
36
+ Requires-Dist: gcsfs>=2023.1.0; extra == 'cloud-storage'
37
+ Requires-Dist: s3fs>=2023.1.0; extra == 'cloud-storage'
38
+ Provides-Extra: lightly-edge
39
+ Requires-Dist: lightly-edge-sdk>=1.0.1b2; extra == 'lightly-edge'
40
+ Requires-Dist: opencv-python; extra == 'lightly-edge'
41
+ Description-Content-Type: text/markdown
42
+
43
+ <p align="center">
44
+ <a href="https://lightly.ai/lightly-studio">
45
+ <img src="https://cdn.prod.website-files.com/62cd5ce03261cba217188442/66dac501a8e9a90495970876_Logo%20dark-short-p-800.png" height="50" alt="LightlyStudio logo" />
46
+ </a>
47
+ </p>
48
+ <p align="center"><strong>Curate, Annotate, and Manage Your Data in LightlyStudio.</strong></p>
49
+ <p align="center">
50
+ <a href="https://pypi.org/project/lightly-studio">
51
+ <img src="https://img.shields.io/pypi/pyversions/lightly-studio" alt="PyPI python" />
52
+ </a>
53
+ <a href="https://pypi.org/project/lightly-studio">
54
+ <img src="https://badge.fury.io/py/lightly-studio.svg" alt="PyPI version" />
55
+ </a>
56
+ <a href="LICENSE">
57
+ <img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License" />
58
+ </a>
59
+ <a href="https://docs.lightly.ai/studio">
60
+ <img src="https://img.shields.io/badge/Docs-blue" alt="Docs" />
61
+ </a>
62
+ </p>
63
+
64
+ ---
65
+
66
+ # Welcome to LightlyStudio!
67
+
68
+ We at **[Lightly](https://lightly.ai)** created **[LightlyStudio](https://www.lightly.ai/lightly-studio)**, an open-source tool designed to unify your data workflows from curation, annotation and management in a single tool. Since we're big fans of Rust we used it to speed things up. You can work with COCO and ImageNet on a Macbook Pro with M1 and 16GB of memory!
69
+
70
+ Explore [LightlyStudio on GitHub](https://github.com/lightly-ai/lightly-studio).
71
+
72
+ Learn more in our [documentation](https://docs.lightly.ai/studio).
73
+
74
+ <p align="center">
75
+ <img alt="LightlyStudio Overview" src="https://storage.googleapis.com/lightly-public/studio/studio_overview.gif" width="70%">
76
+ <br/>
77
+ <em>Curate, Annotate, and Manage Your Data in LightlyStudio.</em>
78
+ </p>