lightly-studio 0.3.1__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 (219) hide show
  1. lightly_studio/__init__.py +11 -0
  2. lightly_studio/api/__init__.py +0 -0
  3. lightly_studio/api/app.py +110 -0
  4. lightly_studio/api/cache.py +77 -0
  5. lightly_studio/api/db.py +133 -0
  6. lightly_studio/api/db_tables.py +32 -0
  7. lightly_studio/api/features.py +7 -0
  8. lightly_studio/api/routes/api/annotation.py +233 -0
  9. lightly_studio/api/routes/api/annotation_label.py +90 -0
  10. lightly_studio/api/routes/api/annotation_task.py +38 -0
  11. lightly_studio/api/routes/api/classifier.py +387 -0
  12. lightly_studio/api/routes/api/dataset.py +182 -0
  13. lightly_studio/api/routes/api/dataset_tag.py +257 -0
  14. lightly_studio/api/routes/api/exceptions.py +96 -0
  15. lightly_studio/api/routes/api/features.py +17 -0
  16. lightly_studio/api/routes/api/metadata.py +37 -0
  17. lightly_studio/api/routes/api/metrics.py +80 -0
  18. lightly_studio/api/routes/api/sample.py +196 -0
  19. lightly_studio/api/routes/api/settings.py +45 -0
  20. lightly_studio/api/routes/api/status.py +19 -0
  21. lightly_studio/api/routes/api/text_embedding.py +48 -0
  22. lightly_studio/api/routes/api/validators.py +17 -0
  23. lightly_studio/api/routes/healthz.py +13 -0
  24. lightly_studio/api/routes/images.py +104 -0
  25. lightly_studio/api/routes/webapp.py +51 -0
  26. lightly_studio/api/server.py +82 -0
  27. lightly_studio/core/__init__.py +0 -0
  28. lightly_studio/core/dataset.py +523 -0
  29. lightly_studio/core/sample.py +77 -0
  30. lightly_studio/core/start_gui.py +15 -0
  31. lightly_studio/dataset/__init__.py +0 -0
  32. lightly_studio/dataset/edge_embedding_generator.py +144 -0
  33. lightly_studio/dataset/embedding_generator.py +91 -0
  34. lightly_studio/dataset/embedding_manager.py +163 -0
  35. lightly_studio/dataset/env.py +16 -0
  36. lightly_studio/dataset/file_utils.py +35 -0
  37. lightly_studio/dataset/loader.py +622 -0
  38. lightly_studio/dataset/mobileclip_embedding_generator.py +144 -0
  39. lightly_studio/dist_lightly_studio_view_app/_app/env.js +1 -0
  40. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/0.DenzbfeK.css +1 -0
  41. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/LightlyLogo.BNjCIww-.png +0 -0
  42. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans- +0 -0
  43. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Bold.DGvYQtcs.ttf +0 -0
  44. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Italic-VariableFont_wdth_wght.B4AZ-wl6.ttf +0 -0
  45. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Regular.DxJTClRG.ttf +0 -0
  46. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-SemiBold.D3TTYgdB.ttf +0 -0
  47. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-VariableFont_wdth_wght.BZBpG5Iz.ttf +0 -0
  48. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.OwPEPQZu.css +1 -0
  49. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.b653GmVf.css +1 -0
  50. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_layout.T-zjSUd3.css +1 -0
  51. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/useFeatureFlags.CV-KWLNP.css +1 -0
  52. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/69_IOA4Y.js +1 -0
  53. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B2FVR0s0.js +1 -0
  54. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B90CZVMX.js +1 -0
  55. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B9zumHo5.js +1 -0
  56. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BJXwVxaE.js +1 -0
  57. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bsi3UGy5.js +1 -0
  58. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bu7uvVrG.js +1 -0
  59. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bx1xMsFy.js +1 -0
  60. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BylOuP6i.js +1 -0
  61. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/C8I8rFJQ.js +1 -0
  62. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CDnpyLsT.js +1 -0
  63. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CWj6FrbW.js +1 -0
  64. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CYgJF_JY.js +1 -0
  65. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CcaPhhk3.js +1 -0
  66. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CvOmgdoc.js +93 -0
  67. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CxtLVaYz.js +3 -0
  68. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D5-A_Ffd.js +4 -0
  69. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6RI2Zrd.js +1 -0
  70. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6su9Aln.js +1 -0
  71. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D98V7j6A.js +1 -0
  72. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DIRAtgl0.js +1 -0
  73. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DIeogL5L.js +1 -0
  74. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DOlTMNyt.js +1 -0
  75. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DjUWrjOv.js +1 -0
  76. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DjfY96ND.js +1 -0
  77. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/H7C68rOM.js +1 -0
  78. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/O-EABkf9.js +1 -0
  79. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/XO7A28GO.js +1 -0
  80. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/hQVEETDE.js +1 -0
  81. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/l7KrR96u.js +1 -0
  82. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/nAHhluT7.js +1 -0
  83. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/r64xT6ao.js +1 -0
  84. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/vC4nQVEB.js +1 -0
  85. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/x9G_hzyY.js +1 -0
  86. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/app.CjnvpsmS.js +2 -0
  87. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/start.0o1H7wM9.js +1 -0
  88. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/0.XRq_TUwu.js +1 -0
  89. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/1.B4rNYwVp.js +1 -0
  90. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/10.DfBwOEhN.js +1 -0
  91. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/11.CWG1ehzT.js +1 -0
  92. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/12.CwF2_8mP.js +1 -0
  93. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/2.CS4muRY-.js +6 -0
  94. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/3.CWHpKonm.js +1 -0
  95. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/4.OUWOLQeV.js +1 -0
  96. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/5.Dm6t9F5W.js +1 -0
  97. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/6.Bw5ck4gK.js +1 -0
  98. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/7.CF0EDTR6.js +1 -0
  99. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/8.Cw30LEcV.js +1 -0
  100. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/9.CPu3CiBc.js +1 -0
  101. lightly_studio/dist_lightly_studio_view_app/_app/version.json +1 -0
  102. lightly_studio/dist_lightly_studio_view_app/apple-touch-icon-precomposed.png +0 -0
  103. lightly_studio/dist_lightly_studio_view_app/apple-touch-icon.png +0 -0
  104. lightly_studio/dist_lightly_studio_view_app/favicon.png +0 -0
  105. lightly_studio/dist_lightly_studio_view_app/index.html +44 -0
  106. lightly_studio/examples/example.py +23 -0
  107. lightly_studio/examples/example_metadata.py +338 -0
  108. lightly_studio/examples/example_selection.py +39 -0
  109. lightly_studio/examples/example_split_work.py +67 -0
  110. lightly_studio/examples/example_v2.py +21 -0
  111. lightly_studio/export_schema.py +18 -0
  112. lightly_studio/few_shot_classifier/__init__.py +0 -0
  113. lightly_studio/few_shot_classifier/classifier.py +80 -0
  114. lightly_studio/few_shot_classifier/classifier_manager.py +663 -0
  115. lightly_studio/few_shot_classifier/random_forest_classifier.py +489 -0
  116. lightly_studio/metadata/complex_metadata.py +47 -0
  117. lightly_studio/metadata/gps_coordinate.py +41 -0
  118. lightly_studio/metadata/metadata_protocol.py +17 -0
  119. lightly_studio/metrics/__init__.py +0 -0
  120. lightly_studio/metrics/detection/__init__.py +0 -0
  121. lightly_studio/metrics/detection/map.py +268 -0
  122. lightly_studio/models/__init__.py +1 -0
  123. lightly_studio/models/annotation/__init__.py +0 -0
  124. lightly_studio/models/annotation/annotation_base.py +171 -0
  125. lightly_studio/models/annotation/instance_segmentation.py +56 -0
  126. lightly_studio/models/annotation/links.py +17 -0
  127. lightly_studio/models/annotation/object_detection.py +47 -0
  128. lightly_studio/models/annotation/semantic_segmentation.py +44 -0
  129. lightly_studio/models/annotation_label.py +47 -0
  130. lightly_studio/models/annotation_task.py +28 -0
  131. lightly_studio/models/classifier.py +20 -0
  132. lightly_studio/models/dataset.py +84 -0
  133. lightly_studio/models/embedding_model.py +30 -0
  134. lightly_studio/models/metadata.py +208 -0
  135. lightly_studio/models/sample.py +180 -0
  136. lightly_studio/models/sample_embedding.py +37 -0
  137. lightly_studio/models/settings.py +60 -0
  138. lightly_studio/models/tag.py +96 -0
  139. lightly_studio/py.typed +0 -0
  140. lightly_studio/resolvers/__init__.py +7 -0
  141. lightly_studio/resolvers/annotation_label_resolver/__init__.py +21 -0
  142. lightly_studio/resolvers/annotation_label_resolver/create.py +27 -0
  143. lightly_studio/resolvers/annotation_label_resolver/delete.py +28 -0
  144. lightly_studio/resolvers/annotation_label_resolver/get_all.py +22 -0
  145. lightly_studio/resolvers/annotation_label_resolver/get_by_id.py +24 -0
  146. lightly_studio/resolvers/annotation_label_resolver/get_by_ids.py +25 -0
  147. lightly_studio/resolvers/annotation_label_resolver/get_by_label_name.py +24 -0
  148. lightly_studio/resolvers/annotation_label_resolver/names_by_ids.py +25 -0
  149. lightly_studio/resolvers/annotation_label_resolver/update.py +38 -0
  150. lightly_studio/resolvers/annotation_resolver/__init__.py +33 -0
  151. lightly_studio/resolvers/annotation_resolver/count_annotations_by_dataset.py +120 -0
  152. lightly_studio/resolvers/annotation_resolver/create.py +19 -0
  153. lightly_studio/resolvers/annotation_resolver/create_many.py +96 -0
  154. lightly_studio/resolvers/annotation_resolver/delete_annotation.py +45 -0
  155. lightly_studio/resolvers/annotation_resolver/delete_annotations.py +56 -0
  156. lightly_studio/resolvers/annotation_resolver/get_all.py +74 -0
  157. lightly_studio/resolvers/annotation_resolver/get_by_id.py +18 -0
  158. lightly_studio/resolvers/annotation_resolver/update_annotation_label.py +144 -0
  159. lightly_studio/resolvers/annotation_resolver/update_bounding_box.py +68 -0
  160. lightly_studio/resolvers/annotation_task_resolver.py +31 -0
  161. lightly_studio/resolvers/annotations/__init__.py +1 -0
  162. lightly_studio/resolvers/annotations/annotations_filter.py +89 -0
  163. lightly_studio/resolvers/dataset_resolver.py +278 -0
  164. lightly_studio/resolvers/embedding_model_resolver.py +100 -0
  165. lightly_studio/resolvers/metadata_resolver/__init__.py +15 -0
  166. lightly_studio/resolvers/metadata_resolver/metadata_filter.py +163 -0
  167. lightly_studio/resolvers/metadata_resolver/sample/__init__.py +21 -0
  168. lightly_studio/resolvers/metadata_resolver/sample/bulk_set_metadata.py +48 -0
  169. lightly_studio/resolvers/metadata_resolver/sample/get_by_sample_id.py +24 -0
  170. lightly_studio/resolvers/metadata_resolver/sample/get_metadata_info.py +104 -0
  171. lightly_studio/resolvers/metadata_resolver/sample/get_value_for_sample.py +27 -0
  172. lightly_studio/resolvers/metadata_resolver/sample/set_value_for_sample.py +53 -0
  173. lightly_studio/resolvers/sample_embedding_resolver.py +86 -0
  174. lightly_studio/resolvers/sample_resolver.py +249 -0
  175. lightly_studio/resolvers/samples_filter.py +81 -0
  176. lightly_studio/resolvers/settings_resolver.py +58 -0
  177. lightly_studio/resolvers/tag_resolver.py +276 -0
  178. lightly_studio/selection/README.md +6 -0
  179. lightly_studio/selection/mundig.py +105 -0
  180. lightly_studio/selection/select.py +96 -0
  181. lightly_studio/selection/select_via_db.py +93 -0
  182. lightly_studio/selection/selection_config.py +31 -0
  183. lightly_studio/services/annotations_service/__init__.py +21 -0
  184. lightly_studio/services/annotations_service/get_annotation_by_id.py +31 -0
  185. lightly_studio/services/annotations_service/update_annotation.py +65 -0
  186. lightly_studio/services/annotations_service/update_annotation_label.py +48 -0
  187. lightly_studio/services/annotations_service/update_annotations.py +29 -0
  188. lightly_studio/setup_logging.py +19 -0
  189. lightly_studio/type_definitions.py +19 -0
  190. lightly_studio/vendor/ACKNOWLEDGEMENTS +422 -0
  191. lightly_studio/vendor/LICENSE +31 -0
  192. lightly_studio/vendor/LICENSE_weights_data +50 -0
  193. lightly_studio/vendor/README.md +5 -0
  194. lightly_studio/vendor/__init__.py +1 -0
  195. lightly_studio/vendor/mobileclip/__init__.py +96 -0
  196. lightly_studio/vendor/mobileclip/clip.py +77 -0
  197. lightly_studio/vendor/mobileclip/configs/mobileclip_b.json +18 -0
  198. lightly_studio/vendor/mobileclip/configs/mobileclip_s0.json +18 -0
  199. lightly_studio/vendor/mobileclip/configs/mobileclip_s1.json +18 -0
  200. lightly_studio/vendor/mobileclip/configs/mobileclip_s2.json +18 -0
  201. lightly_studio/vendor/mobileclip/image_encoder.py +67 -0
  202. lightly_studio/vendor/mobileclip/logger.py +154 -0
  203. lightly_studio/vendor/mobileclip/models/__init__.py +10 -0
  204. lightly_studio/vendor/mobileclip/models/mci.py +933 -0
  205. lightly_studio/vendor/mobileclip/models/vit.py +433 -0
  206. lightly_studio/vendor/mobileclip/modules/__init__.py +4 -0
  207. lightly_studio/vendor/mobileclip/modules/common/__init__.py +4 -0
  208. lightly_studio/vendor/mobileclip/modules/common/mobileone.py +341 -0
  209. lightly_studio/vendor/mobileclip/modules/common/transformer.py +451 -0
  210. lightly_studio/vendor/mobileclip/modules/image/__init__.py +4 -0
  211. lightly_studio/vendor/mobileclip/modules/image/image_projection.py +113 -0
  212. lightly_studio/vendor/mobileclip/modules/image/replknet.py +188 -0
  213. lightly_studio/vendor/mobileclip/modules/text/__init__.py +4 -0
  214. lightly_studio/vendor/mobileclip/modules/text/repmixer.py +281 -0
  215. lightly_studio/vendor/mobileclip/modules/text/tokenizer.py +38 -0
  216. lightly_studio/vendor/mobileclip/text_encoder.py +245 -0
  217. lightly_studio-0.3.1.dist-info/METADATA +520 -0
  218. lightly_studio-0.3.1.dist-info/RECORD +219 -0
  219. lightly_studio-0.3.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,104 @@
1
+ """Resolver for operations for retrieving metadata info."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from uuid import UUID
6
+
7
+ from sqlalchemy import Float, func
8
+ from sqlmodel import Session, col, select
9
+
10
+ from lightly_studio.models.metadata import (
11
+ MetadataInfoView,
12
+ SampleMetadataTable,
13
+ )
14
+ from lightly_studio.models.sample import SampleTable
15
+
16
+
17
+ def get_all_metadata_keys_and_schema(
18
+ session: Session,
19
+ dataset_id: UUID,
20
+ ) -> list[MetadataInfoView]:
21
+ """Get all unique metadata keys and their schema for a dataset.
22
+
23
+ Args:
24
+ session: The database session.
25
+ dataset_id: The dataset's UUID.
26
+
27
+ Returns:
28
+ List of dicts with 'name', 'type', and optionally 'min'/'max' for numerical types.
29
+ """
30
+ # Query all metadata_schema dicts for samples in the dataset
31
+ rows = session.exec(
32
+ select(SampleMetadataTable.metadata_schema)
33
+ .select_from(SampleTable)
34
+ .join(
35
+ SampleMetadataTable,
36
+ col(SampleMetadataTable.sample_id) == col(SampleTable.sample_id),
37
+ )
38
+ .where(SampleTable.dataset_id == dataset_id)
39
+ ).all()
40
+ # Merge all schemas
41
+ merged: dict[str, str] = {}
42
+ for schema_dict in rows:
43
+ merged.update(schema_dict)
44
+
45
+ # Get min and max values for numerical metadata
46
+ result = []
47
+ for key, metadata_type in merged.items():
48
+ metadata_info = MetadataInfoView(name=key, type=metadata_type)
49
+
50
+ # Add min and max for numerical types
51
+ if metadata_type in ["integer", "float"]:
52
+ min_max_values = _get_metadata_min_max_values(session, dataset_id, key, metadata_type)
53
+ if min_max_values:
54
+ metadata_info.min = min_max_values[0]
55
+ metadata_info.max = min_max_values[1]
56
+
57
+ result.append(metadata_info)
58
+
59
+ return result
60
+
61
+
62
+ def _get_metadata_min_max_values(
63
+ session: Session,
64
+ dataset_id: UUID,
65
+ metadata_key: str,
66
+ metadata_type: str,
67
+ ) -> tuple[int, int] | tuple[float, float] | None:
68
+ """Get min and max values for a specific numerical metadata key.
69
+
70
+ Args:
71
+ session: The database session.
72
+ dataset_id: The dataset's UUID.
73
+ metadata_key: The metadata key to get min/max for.
74
+ metadata_type: The metadata type ("integer" or "float").
75
+
76
+ Returns:
77
+ Tuple with 'min' and 'max' values, or None if no values found.
78
+ """
79
+ # Build JSON path for the metadata key.
80
+ json_path = f"$.{metadata_key}"
81
+
82
+ query = (
83
+ select(
84
+ func.min(func.cast(func.json_extract(SampleMetadataTable.data, json_path), Float)),
85
+ func.max(func.cast(func.json_extract(SampleMetadataTable.data, json_path), Float)),
86
+ )
87
+ .select_from(SampleTable)
88
+ .join(SampleMetadataTable, col(SampleMetadataTable.sample_id) == col(SampleTable.sample_id))
89
+ .where(
90
+ SampleTable.dataset_id == dataset_id,
91
+ func.json_extract(SampleMetadataTable.data, json_path).is_not(None),
92
+ )
93
+ )
94
+
95
+ result = session.exec(query).first()
96
+
97
+ if result and result[0] is not None and result[1] is not None:
98
+ # Convert to appropriate type
99
+ if metadata_type == "integer":
100
+ return int(result[0]), int(result[1])
101
+ if metadata_type == "float":
102
+ return float(result[0]), float(result[1])
103
+
104
+ return None
@@ -0,0 +1,27 @@
1
+ """Resolver for operations for retrieving metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+ from uuid import UUID
7
+
8
+ from sqlmodel import Session
9
+
10
+ from .get_by_sample_id import get_by_sample_id
11
+
12
+
13
+ def get_value_for_sample(session: Session, sample_id: UUID, key: str) -> Any | None:
14
+ """Get a specific metadata value for a sample.
15
+
16
+ Args:
17
+ session: The database session.
18
+ sample_id: The sample's UUID.
19
+ key: The metadata key.
20
+
21
+ Returns:
22
+ The value for the given key, or None if not found.
23
+ """
24
+ metadata = get_by_sample_id(session=session, sample_id=sample_id)
25
+ if metadata is None:
26
+ return None
27
+ return metadata.data.get(key)
@@ -0,0 +1,53 @@
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
9
+
10
+ from lightly_studio.models.metadata import (
11
+ SampleMetadataTable,
12
+ )
13
+ from lightly_studio.resolvers.metadata_resolver.sample.get_by_sample_id import (
14
+ get_by_sample_id,
15
+ )
16
+
17
+
18
+ def set_value_for_sample(
19
+ session: Session,
20
+ sample_id: UUID,
21
+ key: str,
22
+ value: Any,
23
+ ) -> SampleMetadataTable:
24
+ """Set a specific metadata value for a sample.
25
+
26
+ Args:
27
+ session: The database session.
28
+ sample_id: The sample's UUID.
29
+ key: The metadata key.
30
+ value: The value to set.
31
+
32
+ Returns:
33
+ The updated CustomMetadataTable instance.
34
+
35
+ Raises:
36
+ ValueError: If the value type doesn't match the schema.
37
+ """
38
+ metadata = get_by_sample_id(session=session, sample_id=sample_id)
39
+ if metadata is None:
40
+ # Create new metadata row if it does not exist
41
+ metadata = SampleMetadataTable(
42
+ sample_id=sample_id,
43
+ data={},
44
+ metadata_schema={},
45
+ )
46
+ session.add(metadata)
47
+
48
+ metadata.set_value(key, value)
49
+
50
+ # Commit changes and refresh the object
51
+ session.commit()
52
+ session.refresh(metadata)
53
+ return metadata
@@ -0,0 +1,86 @@
1
+ """Handler for database operations related to sample embeddings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from uuid import UUID
6
+
7
+ from sqlalchemy import String, cast
8
+ from sqlmodel import Session, col, select
9
+
10
+ from lightly_studio.models.sample import SampleTable
11
+ from lightly_studio.models.sample_embedding import (
12
+ SampleEmbeddingCreate,
13
+ SampleEmbeddingTable,
14
+ )
15
+
16
+
17
+ def create(session: Session, sample_embedding: SampleEmbeddingCreate) -> SampleEmbeddingTable:
18
+ """Create a new SampleEmbedding in the database."""
19
+ db_sample_embedding = SampleEmbeddingTable.model_validate(sample_embedding)
20
+ session.add(db_sample_embedding)
21
+ session.commit()
22
+ session.refresh(db_sample_embedding)
23
+ return db_sample_embedding
24
+
25
+
26
+ def create_many(session: Session, sample_embeddings: list[SampleEmbeddingCreate]) -> None:
27
+ """Create many sample embeddings in a single database commit."""
28
+ db_sample_embeddings = [SampleEmbeddingTable.model_validate(e) for e in sample_embeddings]
29
+ session.bulk_save_objects(db_sample_embeddings)
30
+ session.commit()
31
+
32
+
33
+ def get_by_sample_ids(
34
+ session: Session,
35
+ sample_ids: list[UUID],
36
+ embedding_model_id: UUID,
37
+ ) -> list[SampleEmbeddingTable]:
38
+ """Get sample embeddings for the specified sample IDs.
39
+
40
+ Output order matches the input order.
41
+
42
+ Args:
43
+ session: The database session.
44
+ sample_ids: List of sample IDs to get embeddings for.
45
+ embedding_model_id: The embedding model ID to filter by.
46
+
47
+ Returns:
48
+ List of sample embeddings associated with the provided IDs.
49
+ """
50
+ string_ids = [str(id_) for id_ in sample_ids]
51
+ results = list(
52
+ session.exec(
53
+ select(SampleEmbeddingTable)
54
+ .where(cast(SampleEmbeddingTable.sample_id, String).in_(string_ids))
55
+ .where(SampleEmbeddingTable.embedding_model_id == embedding_model_id)
56
+ ).all()
57
+ )
58
+ # Return embeddings in the same order as the input IDs
59
+ embedding_map = {embedding.sample_id: embedding for embedding in results}
60
+ return [embedding_map[id_] for id_ in sample_ids if id_ in embedding_map]
61
+
62
+
63
+ def get_all_by_dataset_id(
64
+ session: Session,
65
+ dataset_id: UUID,
66
+ embedding_model_id: UUID,
67
+ ) -> list[SampleEmbeddingTable]:
68
+ """Get all sample embeddings for samples in a specific dataset.
69
+
70
+ Args:
71
+ session: The database session.
72
+ dataset_id: The dataset ID to filter by.
73
+ embedding_model_id: The embedding model ID to filter by.
74
+
75
+ Returns:
76
+ List of sample embeddings associated with the dataset.
77
+ """
78
+ query = (
79
+ select(SampleEmbeddingTable)
80
+ .join(SampleTable)
81
+ .where(SampleEmbeddingTable.sample_id == SampleTable.sample_id)
82
+ .where(SampleTable.dataset_id == dataset_id)
83
+ .where(SampleEmbeddingTable.embedding_model_id == embedding_model_id)
84
+ .order_by(col(SampleTable.created_at).asc())
85
+ )
86
+ return list(session.exec(query).all())
@@ -0,0 +1,249 @@
1
+ """Handler for database operations related to samples."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from datetime import datetime, timezone
7
+ from uuid import UUID
8
+
9
+ from pydantic import BaseModel
10
+ from sqlmodel import Session, col, func, select
11
+ from sqlmodel.sql.expression import Select
12
+
13
+ from lightly_studio.models.annotation.annotation_base import AnnotationBaseTable
14
+ from lightly_studio.models.annotation_label import AnnotationLabelTable
15
+ from lightly_studio.models.embedding_model import EmbeddingModelTable
16
+ from lightly_studio.models.sample import SampleCreate, SampleTable
17
+ from lightly_studio.models.sample_embedding import SampleEmbeddingTable
18
+ from lightly_studio.models.tag import TagTable
19
+ from lightly_studio.resolvers.samples_filter import SampleFilter
20
+
21
+
22
+ def create(session: Session, sample: SampleCreate) -> SampleTable:
23
+ """Create a new sample in the database."""
24
+ db_sample = SampleTable.model_validate(sample)
25
+ session.add(db_sample)
26
+ session.commit()
27
+ session.refresh(db_sample)
28
+ return db_sample
29
+
30
+
31
+ def create_many(session: Session, samples: list[SampleCreate]) -> list[SampleTable]:
32
+ """Create multiple samples in a single database commit."""
33
+ db_samples = [SampleTable.model_validate(sample) for sample in samples]
34
+ session.bulk_save_objects(db_samples)
35
+ session.commit()
36
+ return db_samples
37
+
38
+
39
+ def get_by_id(session: Session, dataset_id: UUID, sample_id: UUID) -> SampleTable | None:
40
+ """Retrieve a single sample by ID."""
41
+ return session.exec(
42
+ select(SampleTable).where(
43
+ SampleTable.sample_id == sample_id, SampleTable.dataset_id == dataset_id
44
+ )
45
+ ).one_or_none()
46
+
47
+
48
+ def get_many_by_id(session: Session, sample_ids: list[UUID]) -> list[SampleTable]:
49
+ """Retrieve multiple samples by their IDs.
50
+
51
+ Output order matches the input order.
52
+ """
53
+ results = session.exec(
54
+ select(SampleTable).where(col(SampleTable.sample_id).in_(sample_ids))
55
+ ).all()
56
+ # Return samples in the same order as the input IDs
57
+ sample_map = {sample.sample_id: sample for sample in results}
58
+ return [sample_map[id_] for id_ in sample_ids if id_ in sample_map]
59
+
60
+
61
+ class GetAllSamplesByDatasetIdResult(BaseModel):
62
+ """Result of getting all samples."""
63
+
64
+ samples: Sequence[SampleTable]
65
+ total_count: int
66
+
67
+
68
+ def get_all_by_dataset_id( # noqa: PLR0913
69
+ session: Session,
70
+ dataset_id: UUID,
71
+ offset: int = 0,
72
+ limit: int | None = None,
73
+ filters: SampleFilter | None = None,
74
+ text_embedding: list[float] | None = None,
75
+ sample_ids: list[UUID] | None = None,
76
+ ) -> GetAllSamplesByDatasetIdResult:
77
+ """Retrieve samples for a specific dataset with optional filtering."""
78
+ samples_query = select(SampleTable).where(SampleTable.dataset_id == dataset_id)
79
+ total_count_query = (
80
+ select(func.count()).select_from(SampleTable).where(SampleTable.dataset_id == dataset_id)
81
+ )
82
+
83
+ if filters:
84
+ samples_query = filters.apply(samples_query)
85
+ total_count_query = filters.apply(total_count_query)
86
+
87
+ # TODO(Michal, 06/2025): Consider adding sample_ids to the filters.
88
+ if sample_ids:
89
+ samples_query = samples_query.where(col(SampleTable.sample_id).in_(sample_ids))
90
+ total_count_query = total_count_query.where(col(SampleTable.sample_id).in_(sample_ids))
91
+
92
+ if text_embedding:
93
+ # Fetch the first embedding_model_id for the given dataset_id
94
+ embedding_model_id = session.exec(
95
+ select(EmbeddingModelTable.embedding_model_id)
96
+ .where(EmbeddingModelTable.dataset_id == dataset_id)
97
+ .limit(1)
98
+ ).first()
99
+ if embedding_model_id:
100
+ # Join with SampleEmbedding table to access embeddings
101
+ samples_query = (
102
+ samples_query.join(
103
+ SampleEmbeddingTable,
104
+ col(SampleTable.sample_id) == col(SampleEmbeddingTable.sample_id),
105
+ )
106
+ .where(SampleEmbeddingTable.embedding_model_id == embedding_model_id)
107
+ .order_by(
108
+ func.list_cosine_distance(
109
+ SampleEmbeddingTable.embedding,
110
+ text_embedding,
111
+ )
112
+ )
113
+ )
114
+ total_count_query = total_count_query.join(
115
+ SampleEmbeddingTable,
116
+ col(SampleTable.sample_id) == col(SampleEmbeddingTable.sample_id),
117
+ ).where(SampleEmbeddingTable.embedding_model_id == embedding_model_id)
118
+ else:
119
+ samples_query = samples_query.order_by(
120
+ col(SampleTable.created_at).asc(), col(SampleTable.sample_id).asc()
121
+ )
122
+
123
+ # paginate query when offset or limit are set/positive
124
+ if offset > 0:
125
+ samples_query = samples_query.offset(offset)
126
+ if limit is not None:
127
+ samples_query = samples_query.limit(limit)
128
+
129
+ return GetAllSamplesByDatasetIdResult(
130
+ samples=session.exec(samples_query).all(),
131
+ total_count=session.exec(total_count_query).one(),
132
+ )
133
+
134
+
135
+ def get_dimension_bounds(
136
+ session: Session,
137
+ dataset_id: UUID,
138
+ annotation_label_ids: list[UUID] | None = None,
139
+ tag_ids: list[UUID] | None = None,
140
+ ) -> dict[str, int]:
141
+ """Get min and max dimensions of samples in a dataset."""
142
+ # Prepare the base query for dimensions
143
+ query: Select[tuple[int | None, int | None, int | None, int | None]] = select(
144
+ func.min(SampleTable.width).label("min_width"),
145
+ func.max(SampleTable.width).label("max_width"),
146
+ func.min(SampleTable.height).label("min_height"),
147
+ func.max(SampleTable.height).label("max_height"),
148
+ )
149
+
150
+ if annotation_label_ids:
151
+ # Subquery to filter samples matching all annotation labels
152
+ label_filter = (
153
+ select(SampleTable.sample_id)
154
+ .join(
155
+ AnnotationBaseTable,
156
+ col(SampleTable.sample_id) == col(AnnotationBaseTable.sample_id),
157
+ )
158
+ .join(
159
+ AnnotationLabelTable,
160
+ col(AnnotationBaseTable.annotation_label_id)
161
+ == col(AnnotationLabelTable.annotation_label_id),
162
+ )
163
+ .where(
164
+ SampleTable.dataset_id == dataset_id,
165
+ col(AnnotationLabelTable.annotation_label_id).in_(annotation_label_ids),
166
+ )
167
+ .group_by(col(SampleTable.sample_id))
168
+ .having(
169
+ func.count(col(AnnotationLabelTable.annotation_label_id).distinct())
170
+ == len(annotation_label_ids)
171
+ )
172
+ )
173
+ # Filter the dimension query based on the subquery
174
+ query = query.where(col(SampleTable.sample_id).in_(label_filter))
175
+ else:
176
+ # If no labels specified, filter dimensions
177
+ # for all samples in the dataset
178
+ query = query.where(SampleTable.dataset_id == dataset_id)
179
+
180
+ if tag_ids:
181
+ query = (
182
+ query.join(SampleTable.tags)
183
+ .where(SampleTable.tags.any(col(TagTable.tag_id).in_(tag_ids)))
184
+ .distinct()
185
+ )
186
+
187
+ # Note: We use SQLAlchemy's session.execute instead of SQLModel's
188
+ # ession.exec to be able to fetch the columns with names with the
189
+ # `mappings()` method.
190
+ result = session.execute(query).mappings().one()
191
+ return {key: value for key, value in result.items() if value is not None}
192
+
193
+
194
+ def update(session: Session, sample_id: UUID, sample_data: SampleCreate) -> SampleTable | None:
195
+ """Update an existing sample."""
196
+ sample = get_by_id(session=session, dataset_id=sample_data.dataset_id, sample_id=sample_id)
197
+ if not sample:
198
+ return None
199
+
200
+ sample.file_name = sample_data.file_name
201
+ sample.width = sample_data.width
202
+ sample.height = sample_data.height
203
+ sample.updated_at = datetime.now(timezone.utc)
204
+
205
+ session.commit()
206
+ session.refresh(sample)
207
+ return sample
208
+
209
+
210
+ def delete(session: Session, dataset_id: UUID, sample_id: UUID) -> bool:
211
+ """Delete a sample."""
212
+ sample = get_by_id(session=session, dataset_id=dataset_id, sample_id=sample_id)
213
+ if not sample:
214
+ return False
215
+
216
+ session.delete(sample)
217
+ session.commit()
218
+ return True
219
+
220
+
221
+ def get_samples_excluding(
222
+ session: Session,
223
+ dataset_id: UUID,
224
+ excluded_sample_ids: list[UUID],
225
+ limit: int | None = None,
226
+ ) -> Sequence[SampleTable]:
227
+ """Get random samples excluding specified sample IDs.
228
+
229
+ Args:
230
+ session: The database session.
231
+ dataset_id: The dataset ID to filter by.
232
+ excluded_sample_ids: List of sample IDs to exclude from the result.
233
+ limit: Maximum number of samples to return.
234
+ If None, returns all matches.
235
+
236
+ Returns:
237
+ List of samples not associated with the excluded IDs.
238
+ """
239
+ query = (
240
+ select(SampleTable)
241
+ .where(SampleTable.dataset_id == dataset_id)
242
+ .where(col(SampleTable.sample_id).not_in(excluded_sample_ids))
243
+ .order_by(func.random())
244
+ )
245
+
246
+ if limit is not None:
247
+ query = query.limit(limit)
248
+
249
+ return session.exec(query).all()
@@ -0,0 +1,81 @@
1
+ """Utility functions for building database queries."""
2
+
3
+ from typing import List, Optional
4
+ from uuid import UUID
5
+
6
+ from pydantic import BaseModel
7
+ from sqlmodel import col, select
8
+
9
+ from lightly_studio.models.annotation.annotation_base import AnnotationBaseTable
10
+ from lightly_studio.models.annotation_label import AnnotationLabelTable
11
+ from lightly_studio.models.metadata import SampleMetadataTable
12
+ from lightly_studio.models.sample import SampleTable
13
+ from lightly_studio.models.tag import TagTable
14
+ from lightly_studio.resolvers.metadata_resolver.metadata_filter import (
15
+ MetadataFilter,
16
+ apply_metadata_filters,
17
+ )
18
+ from lightly_studio.type_definitions import QueryType
19
+
20
+
21
+ class FilterDimensions(BaseModel):
22
+ """Encapsulates dimension-based filter parameters for querying samples."""
23
+
24
+ min: Optional[int] = None
25
+ max: Optional[int] = None
26
+
27
+
28
+ class SampleFilter(BaseModel):
29
+ """Encapsulates filter parameters for querying samples."""
30
+
31
+ width: Optional[FilterDimensions] = None
32
+ height: Optional[FilterDimensions] = None
33
+ annotation_label_ids: Optional[List[UUID]] = None
34
+ tag_ids: Optional[List[UUID]] = None
35
+ metadata_filters: Optional[List[MetadataFilter]] = None
36
+
37
+ def apply(self, query: QueryType) -> QueryType:
38
+ """Apply the filters to the given query."""
39
+ # 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)
50
+
51
+ # Apply annotation label filters to the query.
52
+ if self.annotation_label_ids:
53
+ sample_ids_subquery = (
54
+ select(AnnotationBaseTable.sample_id)
55
+ .select_from(AnnotationBaseTable)
56
+ .join(AnnotationBaseTable.annotation_label)
57
+ .where(col(AnnotationLabelTable.annotation_label_id).in_(self.annotation_label_ids))
58
+ .distinct()
59
+ )
60
+ query = query.where(col(SampleTable.sample_id).in_(sample_ids_subquery))
61
+
62
+ # Apply tag filters to the query.
63
+ if self.tag_ids:
64
+ sample_ids_subquery = (
65
+ select(SampleTable.sample_id)
66
+ .select_from(SampleTable)
67
+ .join(SampleTable.tags)
68
+ .where(col(TagTable.tag_id).in_(self.tag_ids))
69
+ .distinct()
70
+ )
71
+ query = query.where(col(SampleTable.sample_id).in_(sample_ids_subquery))
72
+
73
+ # Apply metadata filters to the query.
74
+ if self.metadata_filters:
75
+ query = apply_metadata_filters(
76
+ query,
77
+ self.metadata_filters,
78
+ metadata_model=SampleMetadataTable,
79
+ metadata_join_condition=SampleMetadataTable.sample_id == SampleTable.sample_id,
80
+ )
81
+ return query
@@ -0,0 +1,58 @@
1
+ """This module contains the resolvers for user settings."""
2
+
3
+ from sqlmodel import Session, select
4
+
5
+ from lightly_studio.models.settings import SettingTable, SettingView
6
+
7
+
8
+ def get_settings(session: Session) -> SettingView:
9
+ """Get current settings.
10
+
11
+ Args:
12
+ session: Database session.
13
+
14
+ Returns:
15
+ The current settings.
16
+ """
17
+ statement = select(SettingTable)
18
+ result = session.exec(statement).first()
19
+
20
+ # If no settings exist, create default settings
21
+ if result is None:
22
+ result = SettingTable()
23
+ session.add(result)
24
+ session.commit()
25
+ session.refresh(result)
26
+
27
+ return SettingView.model_validate(result)
28
+
29
+
30
+ def set_settings(session: Session, settings: SettingView) -> SettingView:
31
+ """Update settings.
32
+
33
+ Args:
34
+ session: Database session.
35
+ settings: New settings to apply.
36
+
37
+ Returns:
38
+ Updated settings.
39
+ """
40
+ current_settings = session.exec(select(SettingTable)).first()
41
+ if current_settings is None:
42
+ current_settings = SettingTable()
43
+ session.add(current_settings)
44
+
45
+ # Update grid view sample rendering
46
+ current_settings.grid_view_sample_rendering = settings.grid_view_sample_rendering
47
+
48
+ # Update keyboard shortcut mapping
49
+ current_settings.key_hide_annotations = settings.key_hide_annotations
50
+ current_settings.key_go_back = settings.key_go_back
51
+
52
+ # Update show annotation text labels
53
+ current_settings.show_annotation_text_labels = settings.show_annotation_text_labels
54
+
55
+ session.commit()
56
+ session.refresh(current_settings)
57
+
58
+ return SettingView.model_validate(current_settings)