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,276 @@
1
+ """Handler for database operations related to tags."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from uuid import UUID
7
+
8
+ import sqlmodel
9
+ from sqlmodel import Session, col, select
10
+
11
+ from lightly_studio.models.annotation.annotation_base import AnnotationBaseTable
12
+ from lightly_studio.models.annotation.links import AnnotationTagLinkTable
13
+ from lightly_studio.models.sample import SampleTable, SampleTagLinkTable
14
+ from lightly_studio.models.tag import TagCreate, TagTable, TagUpdate
15
+
16
+
17
+ def create(session: Session, tag: TagCreate) -> TagTable:
18
+ """Create a new tag in the database."""
19
+ db_tag = TagTable.model_validate(tag)
20
+ session.add(db_tag)
21
+ session.commit()
22
+ session.refresh(db_tag)
23
+ return db_tag
24
+
25
+
26
+ # TODO(Michal, 06/2025): Use Paginated struct instead of offset/limit.
27
+ def get_all_by_dataset_id(
28
+ session: Session, dataset_id: UUID, offset: int = 0, limit: int | None = None
29
+ ) -> list[TagTable]:
30
+ """Retrieve all tags with pagination."""
31
+ query = (
32
+ select(TagTable)
33
+ .where(TagTable.dataset_id == dataset_id)
34
+ .order_by(col(TagTable.created_at).asc(), col(TagTable.tag_id).asc())
35
+ .offset(offset)
36
+ )
37
+ if limit is not None:
38
+ query = query.limit(limit)
39
+ tags = session.exec(query).all()
40
+ return list(tags) if tags else []
41
+
42
+
43
+ def get_by_id(session: Session, tag_id: UUID) -> TagTable | None:
44
+ """Retrieve a single tag by ID."""
45
+ return session.exec(select(TagTable).where(TagTable.tag_id == tag_id)).one_or_none()
46
+
47
+
48
+ def get_by_name(session: Session, tag_name: str, dataset_id: UUID | None) -> TagTable | None:
49
+ """Retrieve a single tag by ID."""
50
+ if dataset_id:
51
+ return session.exec(
52
+ select(TagTable)
53
+ .where(TagTable.dataset_id == dataset_id)
54
+ .where(TagTable.name == tag_name)
55
+ ).one_or_none()
56
+ return session.exec(select(TagTable).where(TagTable.name == tag_name)).one_or_none()
57
+
58
+
59
+ def update(session: Session, tag_id: UUID, tag_data: TagUpdate) -> TagTable | None:
60
+ """Update an existing tag."""
61
+ tag = get_by_id(session=session, tag_id=tag_id)
62
+ if not tag:
63
+ return None
64
+
65
+ # due to duckdb/OLAP optimisations, update operations effecting unique
66
+ # constraints (e.g colums) will lead to a unique constraint violation.
67
+ # This is due to a update is implemented as delete+insert. The error
68
+ # happens only within the same session.
69
+ # To fix it, we can delete, commit + insert a new tag.
70
+ # https://duckdb.org/docs/sql/indexes#over-eager-unique-constraint-checking
71
+ session.delete(tag)
72
+ session.commit()
73
+
74
+ # create clone of tag with updated values
75
+ tag_updated = TagTable.model_validate(tag)
76
+ tag_updated.name = tag_data.name
77
+ tag_updated.description = tag_data.description
78
+ tag_updated.updated_at = datetime.now(timezone.utc)
79
+
80
+ session.add(tag_updated)
81
+ session.commit()
82
+ session.refresh(tag_updated)
83
+ return tag_updated
84
+
85
+
86
+ def delete(session: Session, tag_id: UUID) -> bool:
87
+ """Delete a tag."""
88
+ tag = get_by_id(session=session, tag_id=tag_id)
89
+ if not tag:
90
+ return False
91
+
92
+ session.delete(tag)
93
+ session.commit()
94
+ return True
95
+
96
+
97
+ def add_tag_to_sample(
98
+ session: Session,
99
+ tag_id: UUID,
100
+ sample: SampleTable,
101
+ ) -> SampleTable | None:
102
+ """Add a tag to a sample."""
103
+ tag = get_by_id(session=session, tag_id=tag_id)
104
+ if not tag or not tag.tag_id:
105
+ return None
106
+ if tag.kind != "sample":
107
+ raise ValueError(f"Tag {tag_id} is not of kind 'sample'")
108
+
109
+ sample.tags.append(tag)
110
+ session.add(sample)
111
+ session.commit()
112
+ session.refresh(sample)
113
+ return sample
114
+
115
+
116
+ def remove_tag_from_sample(
117
+ session: Session,
118
+ tag_id: UUID,
119
+ sample: SampleTable,
120
+ ) -> SampleTable | None:
121
+ """Remove a tag from a sample."""
122
+ tag = get_by_id(session=session, tag_id=tag_id)
123
+ if not tag or not tag.tag_id:
124
+ return None
125
+ if tag.kind != "sample":
126
+ raise ValueError(f"Tag {tag_id} is not of kind 'sample'")
127
+
128
+ sample.tags.remove(tag)
129
+ session.add(sample)
130
+ session.commit()
131
+ session.refresh(sample)
132
+ return sample
133
+
134
+
135
+ def add_tag_to_annotation(
136
+ session: Session,
137
+ tag_id: UUID,
138
+ annotation: AnnotationBaseTable,
139
+ ) -> AnnotationBaseTable | None:
140
+ """Add a tag to a annotation."""
141
+ tag = get_by_id(session=session, tag_id=tag_id)
142
+ if not tag or not tag.tag_id:
143
+ return None
144
+ if tag.kind != "annotation":
145
+ raise ValueError(f"Tag {tag_id} is not of kind 'annotation'")
146
+
147
+ annotation.tags.append(tag)
148
+ session.add(annotation)
149
+ session.commit()
150
+ session.refresh(annotation)
151
+ return annotation
152
+
153
+
154
+ def assign_tag_to_annotation(
155
+ session: Session,
156
+ tag: TagTable,
157
+ annotation: AnnotationBaseTable,
158
+ ) -> AnnotationBaseTable:
159
+ """Add a tag to a annotation."""
160
+ annotation.tags.append(tag)
161
+ session.add(annotation)
162
+ session.commit()
163
+ session.refresh(annotation)
164
+ return annotation
165
+
166
+
167
+ def remove_tag_from_annotation(
168
+ session: Session,
169
+ tag_id: UUID,
170
+ annotation: AnnotationBaseTable,
171
+ ) -> AnnotationBaseTable | None:
172
+ """Remove a tag from a annotation."""
173
+ tag = get_by_id(session=session, tag_id=tag_id)
174
+ if not tag or not tag.tag_id:
175
+ return None
176
+ if tag.kind != "annotation":
177
+ raise ValueError(f"Tag {tag_id} is not of kind 'annotation'")
178
+
179
+ annotation.tags.remove(tag)
180
+ session.add(annotation)
181
+ session.commit()
182
+ session.refresh(annotation)
183
+ return annotation
184
+
185
+
186
+ def add_sample_ids_to_tag_id(
187
+ session: Session,
188
+ tag_id: UUID,
189
+ sample_ids: list[UUID],
190
+ ) -> TagTable | None:
191
+ """Add a list of sample_ids to a tag."""
192
+ tag = get_by_id(session=session, tag_id=tag_id)
193
+ if not tag or not tag.tag_id:
194
+ return None
195
+ if tag.kind != "sample":
196
+ raise ValueError(f"Tag {tag_id} is not of kind 'sample'")
197
+
198
+ for sample_id in sample_ids:
199
+ session.merge(SampleTagLinkTable(sample_id=sample_id, tag_id=tag_id))
200
+
201
+ session.commit()
202
+ session.refresh(tag)
203
+ return tag
204
+
205
+
206
+ def remove_sample_ids_from_tag_id(
207
+ session: Session,
208
+ tag_id: UUID,
209
+ sample_ids: list[UUID],
210
+ ) -> TagTable | None:
211
+ """Remove a list of sample_ids to a tag."""
212
+ tag = get_by_id(session=session, tag_id=tag_id)
213
+ if not tag or not tag.tag_id:
214
+ return None
215
+ if tag.kind != "sample":
216
+ raise ValueError(f"Tag {tag_id} is not of kind 'sample'")
217
+
218
+ session.exec( # type:ignore[call-overload]
219
+ sqlmodel.delete(SampleTagLinkTable).where(
220
+ col(SampleTagLinkTable.tag_id) == tag_id,
221
+ col(SampleTagLinkTable.sample_id).in_(sample_ids),
222
+ )
223
+ )
224
+
225
+ session.commit()
226
+ session.refresh(tag)
227
+ return tag
228
+
229
+
230
+ def add_annotation_ids_to_tag_id(
231
+ session: Session,
232
+ tag_id: UUID,
233
+ annotation_ids: list[UUID],
234
+ ) -> TagTable | None:
235
+ """Add a list of annotation_ids to a tag."""
236
+ tag = get_by_id(session=session, tag_id=tag_id)
237
+ if not tag or not tag.tag_id:
238
+ return None
239
+ if tag.kind != "annotation":
240
+ raise ValueError(f"Tag {tag_id} is not of kind 'annotation'")
241
+
242
+ for annotation_id in annotation_ids:
243
+ session.merge(
244
+ AnnotationTagLinkTable(
245
+ tag_id=tag_id,
246
+ annotation_id=annotation_id,
247
+ )
248
+ )
249
+
250
+ session.commit()
251
+ session.refresh(tag)
252
+ return tag
253
+
254
+
255
+ def remove_annotation_ids_from_tag_id(
256
+ session: Session,
257
+ tag_id: UUID,
258
+ annotation_ids: list[UUID],
259
+ ) -> TagTable | None:
260
+ """Remove a list of things to a tag."""
261
+ tag = get_by_id(session=session, tag_id=tag_id)
262
+ if not tag or not tag.tag_id:
263
+ return None
264
+ if tag.kind != "annotation":
265
+ raise ValueError(f"Tag {tag_id} is not of kind 'annotation'")
266
+
267
+ session.exec( # type:ignore[call-overload]
268
+ sqlmodel.delete(AnnotationTagLinkTable).where(
269
+ col(AnnotationTagLinkTable.tag_id) == tag_id,
270
+ col(AnnotationTagLinkTable.annotation_id).in_(annotation_ids),
271
+ )
272
+ )
273
+
274
+ session.commit()
275
+ session.refresh(tag)
276
+ return tag
@@ -0,0 +1,6 @@
1
+ # Selection
2
+
3
+ The architecture follows the design document.
4
+ See https://docs.google.com/document/d/1ZRICdFmfJmxUBy3FFoeUWsAgsCNWDHg8CK5MJiGmX74/edit?tab=t.kbfvnrepsuf#bookmark=id.4wbh2eqhgokk
5
+ The design document also contains an architecture diagram showing the
6
+ relationship between the various components of the system.
@@ -0,0 +1,105 @@
1
+ """Python interface to the Mundig selection algorithm."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Iterable
6
+
7
+ # TODO(Malte, 08/2025): About the type ignore:
8
+ # Use pyo3 typing stubs once they are implemented.
9
+ # See https://github.com/PyO3/pyo3/issues/510
10
+ # Or remove the type ignore once typing stubs were added manually.
11
+ import lightly_mundig # type: ignore[import-untyped]
12
+ import numpy as np
13
+ from environs import Env
14
+
15
+
16
+ class Mundig:
17
+ """Python wrapper for the Mundig selection algorithm.
18
+
19
+ This class provides a Python interface to the lightly_mundig Rust library
20
+ for sample selection.
21
+ """
22
+
23
+ def __init__(self) -> None:
24
+ """Initialize the Mundig selection interface."""
25
+ # Read LIGHTLY_STUDIO_LICENSE_KEY with .env file support
26
+ env = Env()
27
+ env.read_env()
28
+ license_key = env.str("LIGHTLY_STUDIO_LICENSE_KEY", default=None)
29
+ if license_key is None:
30
+ raise ValueError(
31
+ "LIGHTLY_STUDIO_LICENSE_KEY environment variable is not set. "
32
+ "Please set it to your LightlyStudio license key."
33
+ )
34
+
35
+ self.mundig = lightly_mundig.Selection(token=license_key)
36
+
37
+ self.n_input_samples: int | None = None
38
+
39
+ def run(self, n_samples: int) -> list[int]:
40
+ """Run the selection algorithm and return selected sample indices.
41
+
42
+ Args:
43
+ n_samples: The number of samples to select.
44
+
45
+ Returns:
46
+ A list of indices of the selected samples.
47
+ """
48
+ selected: list[int] = self.mundig.run_selection(
49
+ n_total_samples=self.n_input_samples, n_samples_to_select=n_samples
50
+ )
51
+ return selected
52
+
53
+ def add_diversity(self, embeddings: Iterable[Iterable[float]], strength: float = 1.0) -> None:
54
+ """Add diversity-based selection using sample embeddings.
55
+
56
+ Args:
57
+ embeddings:
58
+ The embeddings of each sample.
59
+ First dimension is over the samples, the second dimension is
60
+ the embedding size. The embedding size must be the same for
61
+ all samples.
62
+ strength:
63
+ The strength of the diversity strategy.
64
+
65
+ """
66
+ # Convert to ndarray with float32 dtype if not already
67
+ if isinstance(embeddings, np.ndarray) and embeddings.dtype == np.float32:
68
+ embeddings_ndarray = embeddings
69
+ else:
70
+ embeddings_ndarray = np.array(embeddings, dtype=np.float32)
71
+ self._check_consistent_input_size(embeddings_ndarray.shape[0])
72
+ self.mundig.add_diversifying_strategy(embeddings=embeddings_ndarray, strength=strength)
73
+
74
+ def add_weighting(self, weights: Iterable[float], strength: float = 1.0) -> None:
75
+ """Add a weighting strategy.
76
+
77
+ Args:
78
+ weights:
79
+ The weight or importance or utility of each sample.
80
+ strength:
81
+ The strength of the weighting strategy.
82
+ """
83
+ weights_ndarray = np.array(weights, dtype=np.float32)
84
+ self._check_consistent_input_size(weights_ndarray.shape[0])
85
+ self.mundig.add_weighting_strategy(weights=weights_ndarray, strength=strength)
86
+
87
+ def _check_consistent_input_size(self, n_input_samples_strategy: int) -> None:
88
+ """Assert that input samples count is consistent across strategies.
89
+
90
+ Args:
91
+ n_input_samples_strategy:
92
+ The number of input samples in the currently added strategy.
93
+
94
+ Raises:
95
+ ValueError:
96
+ If the number of input samples in the new strategy differs
97
+ from the one used in previous strategies.
98
+ """
99
+ if self.n_input_samples is None:
100
+ self.n_input_samples = n_input_samples_strategy
101
+ elif self.n_input_samples != n_input_samples_strategy:
102
+ raise ValueError(
103
+ f"Expected {self.n_input_samples} input samples, "
104
+ f"but the latest strategy passed {n_input_samples_strategy}."
105
+ )
@@ -0,0 +1,96 @@
1
+ """Provides the user python interface to selection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from uuid import UUID
6
+
7
+ from sqlmodel import Session
8
+
9
+ from lightly_studio.resolvers.samples_filter import SampleFilter
10
+ from lightly_studio.selection.select_via_db import select_via_database
11
+ from lightly_studio.selection.selection_config import (
12
+ EmbeddingDiversityStrategy,
13
+ SelectionConfig,
14
+ SelectionStrategy,
15
+ )
16
+
17
+
18
+ class Selection:
19
+ """User selection interface for the dataset."""
20
+
21
+ # TODO(Malte, 08/2025): Create this class within the DatasetView.
22
+ # Then the arguments can be passed directly from the DatasetView.
23
+ # Example:
24
+ # class DatasetView:
25
+ # def __init__(self, dataset_id: UUID, session: Session):
26
+ # self.select = Select(dataset_id, session)
27
+ # User interface:
28
+ # dataset_view = ...
29
+ # dataset_view.select.diverse(...)
30
+ #
31
+ # See https://docs.google.com/document/d/1ZRICdFmfJmxUBy3FFoeUWsAgsCNWDHg8CK5MJiGmX74/edit?tab=t.kbfvnrepsuf#bookmark=id.8klhhwr5q4dp
32
+
33
+ def __init__(self, dataset_id: UUID, session: Session):
34
+ """Creates the interface to run selection.
35
+
36
+ Args:
37
+ dataset_id: The ID of the dataset to select from.
38
+ session: The database session to use for selection.
39
+
40
+ """
41
+ self.dataset_id = dataset_id
42
+ self.session = session
43
+
44
+ def diverse(
45
+ self,
46
+ n_samples_to_select: int,
47
+ selection_result_tag_name: str,
48
+ embedding_model_name: str | None = None,
49
+ sample_filter: SampleFilter | None = None,
50
+ ) -> None:
51
+ """Selects a diverse subset of the dataset.
52
+
53
+ Args:
54
+ n_samples_to_select: The number of samples to select.
55
+ selection_result_tag_name: The tag name to use for the selection result.
56
+ embedding_model_name:
57
+ The name of the embedding model to use.
58
+ If None, assert that there is only one embedding model and uses it.
59
+ sample_filter: An optional filter to apply to the samples.
60
+ """
61
+ strategy = EmbeddingDiversityStrategy(embedding_model_name=embedding_model_name)
62
+ selection_config = SelectionConfig(
63
+ dataset_id=self.dataset_id,
64
+ n_samples_to_select=n_samples_to_select,
65
+ selection_result_tag_name=selection_result_tag_name,
66
+ sample_filter=sample_filter,
67
+ strategies=[strategy],
68
+ )
69
+ select_via_database(session=self.session, config=selection_config)
70
+
71
+ def multi_strategies(
72
+ self,
73
+ n_samples_to_select: int,
74
+ selection_result_tag_name: str,
75
+ selection_strategies: list[SelectionStrategy],
76
+ sample_filter: SampleFilter | None = None,
77
+ ) -> None:
78
+ """Select a subset of the dataset based on multiple selection strategies.
79
+
80
+ Args:
81
+ n_samples_to_select: The number of samples to select.
82
+ selection_result_tag_name: The tag name to use for the selection result.
83
+ selection_strategies:
84
+ Selection strategies to use for the selection. They can be created after
85
+ importing them from `lightly_studio.selection.selection_config`.
86
+ sample_filter: An optional filter to apply to the samples.
87
+
88
+ """
89
+ config = SelectionConfig(
90
+ dataset_id=self.dataset_id,
91
+ n_samples_to_select=n_samples_to_select,
92
+ selection_result_tag_name=selection_result_tag_name,
93
+ sample_filter=sample_filter,
94
+ strategies=selection_strategies,
95
+ )
96
+ select_via_database(session=self.session, config=config)
@@ -0,0 +1,93 @@
1
+ """Database selection functions for the selection process."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime
6
+
7
+ from sqlmodel import Session
8
+
9
+ from lightly_studio.models.tag import TagCreate
10
+ from lightly_studio.resolvers import (
11
+ embedding_model_resolver,
12
+ sample_embedding_resolver,
13
+ sample_resolver,
14
+ tag_resolver,
15
+ )
16
+ from lightly_studio.selection.mundig import Mundig
17
+ from lightly_studio.selection.selection_config import (
18
+ EmbeddingDiversityStrategy,
19
+ SelectionConfig,
20
+ )
21
+
22
+
23
+ def select_via_database(session: Session, config: SelectionConfig) -> None:
24
+ """Runs selection and all database interactions of it.
25
+
26
+ First resolves the selection config to actual database values.
27
+ Then calls Mundig to run the selection with pure values.
28
+ Last creates a tag for the selected set.
29
+ """
30
+ # Check if the tag name is already used
31
+ existing_tag = tag_resolver.get_by_name(
32
+ session=session,
33
+ tag_name=config.selection_result_tag_name,
34
+ dataset_id=config.dataset_id,
35
+ )
36
+ if existing_tag:
37
+ msg = (
38
+ f"Tag with name {config.selection_result_tag_name} already exists in the "
39
+ f"dataset {config.dataset_id}. Please use a different tag name."
40
+ )
41
+ raise ValueError(msg)
42
+
43
+ # TODO(Malte, 08/2025): Use a DatasetQuery instead of SampleFilter once
44
+ # the latter is implemented.
45
+ # See https://linear.app/lightly/issue/LIG-7292/story-python-ui-mvp1-without-datasetquery-and-sample
46
+ samples = sample_resolver.get_all_by_dataset_id(
47
+ session,
48
+ limit=None,
49
+ dataset_id=config.dataset_id,
50
+ filters=config.sample_filter,
51
+ ).samples
52
+ sample_ids = [s.sample_id for s in samples]
53
+
54
+ n_samples_to_select = min(config.n_samples_to_select, len(sample_ids))
55
+ if n_samples_to_select == 0:
56
+ print("No samples available for selection.")
57
+ return
58
+
59
+ mundig = Mundig()
60
+ for strat in config.strategies:
61
+ if isinstance(strat, EmbeddingDiversityStrategy):
62
+ embedding_model_id = embedding_model_resolver.get_by_name(
63
+ session=session,
64
+ dataset_id=config.dataset_id,
65
+ embedding_model_name=strat.embedding_model_name,
66
+ ).embedding_model_id
67
+ embedding_tables = sample_embedding_resolver.get_by_sample_ids(
68
+ session=session,
69
+ sample_ids=sample_ids,
70
+ embedding_model_id=embedding_model_id,
71
+ )
72
+ embeddings = [e.embedding for e in embedding_tables]
73
+ mundig.add_diversity(embeddings=embeddings, strength=strat.strength)
74
+ else:
75
+ raise ValueError(f"Selection strategy of type {type(strat)} is unknown.")
76
+
77
+ selected_indices = mundig.run(n_samples=n_samples_to_select)
78
+ selected_sample_ids = [sample_ids[i] for i in selected_indices]
79
+
80
+ datetime_str = datetime.datetime.now(tz=datetime.timezone.utc).isoformat()
81
+ tag_description = f"Selected at {datetime_str} UTC"
82
+ tag = tag_resolver.create(
83
+ session=session,
84
+ tag=TagCreate(
85
+ dataset_id=config.dataset_id,
86
+ name=config.selection_result_tag_name,
87
+ kind="sample",
88
+ description=tag_description,
89
+ ),
90
+ )
91
+ tag_resolver.add_sample_ids_to_tag_id(
92
+ session=session, tag_id=tag.tag_id, sample_ids=selected_sample_ids
93
+ )
@@ -0,0 +1,31 @@
1
+ """Pydantic models for the Selection configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from uuid import UUID
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from lightly_studio.resolvers.samples_filter import SampleFilter
10
+
11
+
12
+ class SelectionConfig(BaseModel):
13
+ """Configuration for the selection process."""
14
+
15
+ dataset_id: UUID
16
+ sample_filter: SampleFilter | None = None
17
+ n_samples_to_select: int
18
+ selection_result_tag_name: str
19
+ strategies: list[SelectionStrategy]
20
+
21
+
22
+ class SelectionStrategy(BaseModel):
23
+ """Base class for selection strategies."""
24
+
25
+ strength: float = 1.0
26
+
27
+
28
+ class EmbeddingDiversityStrategy(SelectionStrategy):
29
+ """Selection strategy based on embedding diversity."""
30
+
31
+ embedding_model_name: str | None
@@ -0,0 +1,21 @@
1
+ """Services for annotations operations."""
2
+
3
+ from lightly_studio.services.annotations_service.get_annotation_by_id import (
4
+ get_annotation_by_id,
5
+ )
6
+ from lightly_studio.services.annotations_service.update_annotation import (
7
+ update_annotation,
8
+ )
9
+ from lightly_studio.services.annotations_service.update_annotation_label import (
10
+ update_annotation_label,
11
+ )
12
+ from lightly_studio.services.annotations_service.update_annotations import (
13
+ update_annotations,
14
+ )
15
+
16
+ __all__ = [
17
+ "get_annotation_by_id",
18
+ "update_annotation",
19
+ "update_annotation_label",
20
+ "update_annotations",
21
+ ]
@@ -0,0 +1,31 @@
1
+ """Get an annotation by its ID."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from uuid import UUID
6
+
7
+ from sqlmodel import Session
8
+
9
+ from lightly_studio.models.annotation.annotation_base import (
10
+ AnnotationBaseTable,
11
+ )
12
+ from lightly_studio.resolvers import (
13
+ annotation_resolver,
14
+ )
15
+
16
+
17
+ def get_annotation_by_id(session: Session, annotation_id: UUID) -> AnnotationBaseTable:
18
+ """Retrieve an annotation by its ID.
19
+
20
+ Args:
21
+ session: Database session for executing the operation.
22
+ annotation_id: ID of the annotation to retrieve.
23
+
24
+ Returns:
25
+ The retrieved annotation.
26
+ """
27
+ annotation = annotation_resolver.get_by_id(session=session, annotation_id=annotation_id)
28
+ if not annotation:
29
+ raise ValueError(f"Annotation {annotation_id} not found")
30
+
31
+ return annotation