exhale-xrf 1.5.0__tar.gz

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.
Files changed (247) hide show
  1. exhale_xrf-1.5.0/.ruff.toml +22 -0
  2. exhale_xrf-1.5.0/LICENSE +21 -0
  3. exhale_xrf-1.5.0/MANIFEST.in +21 -0
  4. exhale_xrf-1.5.0/PKG-INFO +229 -0
  5. exhale_xrf-1.5.0/README.md +189 -0
  6. exhale_xrf-1.5.0/artwork/generate/57-mask-y.png +0 -0
  7. exhale_xrf-1.5.0/artwork/generate/57-masks.png +0 -0
  8. exhale_xrf-1.5.0/artwork/generate/extract_xrf_icon.py +190 -0
  9. exhale_xrf-1.5.0/artwork/generate/generate_xrf_guide.py +680 -0
  10. exhale_xrf-1.5.0/artwork/generate/install_xrf_splash.py +58 -0
  11. exhale_xrf-1.5.0/artwork/generate/sprinkle_mask_points.py +144 -0
  12. exhale_xrf-1.5.0/artwork/generate/xrf_cells_x_imagegen_prompt.md +143 -0
  13. exhale_xrf-1.5.0/artwork/generate/xrf_guide_57.png +0 -0
  14. exhale_xrf-1.5.0/artwork/generate/xrf_icon_x_dark.png +0 -0
  15. exhale_xrf-1.5.0/artwork/generate/xrf_icon_x_mask.png +0 -0
  16. exhale_xrf-1.5.0/artwork/generate/xrf_icon_x_region.png +0 -0
  17. exhale_xrf-1.5.0/artwork/generate/xrf_icon_x_transparent.png +0 -0
  18. exhale_xrf-1.5.0/artwork/generate/xrf_imagegen_57.png +0 -0
  19. exhale_xrf-1.5.0/artwork/generate/xrf_reference.png +0 -0
  20. exhale_xrf-1.5.0/artwork/screenshots/segmentation_20261023.png +0 -0
  21. exhale_xrf-1.5.0/artwork/unused-icons/LICENSE-icons.txt +21 -0
  22. exhale_xrf-1.5.0/artwork/unused-icons/atom-checkmark.svg +13 -0
  23. exhale_xrf-1.5.0/artwork/unused-icons/atom-square.svg +9 -0
  24. exhale_xrf-1.5.0/artwork/unused-icons/atom-x-mark.svg +12 -0
  25. exhale_xrf-1.5.0/artwork/unused-icons/checkbox-denied.svg +8 -0
  26. exhale_xrf-1.5.0/artwork/unused-icons/floppy-pen.svg +6 -0
  27. exhale_xrf-1.5.0/artwork/unused-icons/photo-plus.svg +10 -0
  28. exhale_xrf-1.5.0/artwork/unused-icons/photo.svg +2 -0
  29. exhale_xrf-1.5.0/docs/README.md +13 -0
  30. exhale_xrf-1.5.0/docs/architecture.md +619 -0
  31. exhale_xrf-1.5.0/docs/boundary-fitting.md +121 -0
  32. exhale_xrf-1.5.0/docs/code-overview.md +673 -0
  33. exhale_xrf-1.5.0/docs/development.md +561 -0
  34. exhale_xrf-1.5.0/exhale/__init__.py +55 -0
  35. exhale_xrf-1.5.0/exhale/__main__.py +315 -0
  36. exhale_xrf-1.5.0/exhale/analysis/__init__.py +1 -0
  37. exhale_xrf-1.5.0/exhale/analysis/analysis_settings_dialog.py +13 -0
  38. exhale_xrf-1.5.0/exhale/analysis/analysis_settings_dialog_ui.py +93 -0
  39. exhale_xrf-1.5.0/exhale/analysis/analysisworker.py +264 -0
  40. exhale_xrf-1.5.0/exhale/analysis/boundarycalibration.py +189 -0
  41. exhale_xrf-1.5.0/exhale/analysis/boundaryfitting.py +296 -0
  42. exhale_xrf-1.5.0/exhale/analysis/cluster_distances.py +230 -0
  43. exhale_xrf-1.5.0/exhale/analysis/clustering_controller.py +380 -0
  44. exhale_xrf-1.5.0/exhale/analysis/controller.py +1860 -0
  45. exhale_xrf-1.5.0/exhale/analysis/correlation_controller.py +904 -0
  46. exhale_xrf-1.5.0/exhale/analysis/correlations.py +188 -0
  47. exhale_xrf-1.5.0/exhale/analysis/correlationwidgets.py +908 -0
  48. exhale_xrf-1.5.0/exhale/analysis/layerwidgets.py +668 -0
  49. exhale_xrf-1.5.0/exhale/analysis/matrix_export_dialog.py +114 -0
  50. exhale_xrf-1.5.0/exhale/analysis/matrix_export_dialog_ui.py +145 -0
  51. exhale_xrf-1.5.0/exhale/analysis/matrixpdf.py +499 -0
  52. exhale_xrf-1.5.0/exhale/analysis/matrixplots.py +247 -0
  53. exhale_xrf-1.5.0/exhale/analysis/model_store.py +162 -0
  54. exhale_xrf-1.5.0/exhale/analysis/naparihelper.py +957 -0
  55. exhale_xrf-1.5.0/exhale/analysis/segmentation_backends.py +192 -0
  56. exhale_xrf-1.5.0/exhale/analysis/segmentation_batch_dialog.py +110 -0
  57. exhale_xrf-1.5.0/exhale/analysis/segmentation_batch_dialog_ui.py +59 -0
  58. exhale_xrf-1.5.0/exhale/analysis/segmentation_help_dialog.py +72 -0
  59. exhale_xrf-1.5.0/exhale/analysis/segmentation_monai.py +142 -0
  60. exhale_xrf-1.5.0/exhale/analysis/segmentation_options_dialog.py +86 -0
  61. exhale_xrf-1.5.0/exhale/analysis/segmentation_options_dialog_ui.py +120 -0
  62. exhale_xrf-1.5.0/exhale/analysis/segmentation_viewer.py +401 -0
  63. exhale_xrf-1.5.0/exhale/analysis/segmentationbatch.py +211 -0
  64. exhale_xrf-1.5.0/exhale/analysis/segmentationcursor.py +334 -0
  65. exhale_xrf-1.5.0/exhale/analysis/segmentationio.py +312 -0
  66. exhale_xrf-1.5.0/exhale/analysis/segmentationmethods.py +86 -0
  67. exhale_xrf-1.5.0/exhale/analysis/trainingio.py +129 -0
  68. exhale_xrf-1.5.0/exhale/appearance.py +156 -0
  69. exhale_xrf-1.5.0/exhale/data/__init__.py +1 -0
  70. exhale_xrf-1.5.0/exhale/data/channeldata.py +105 -0
  71. exhale_xrf-1.5.0/exhale/data/constants.py +12 -0
  72. exhale_xrf-1.5.0/exhale/data/elementsettings.py +211 -0
  73. exhale_xrf-1.5.0/exhale/data/imagesettings.py +295 -0
  74. exhale_xrf-1.5.0/exhale/data/matrixpalettes.py +143 -0
  75. exhale_xrf-1.5.0/exhale/data/plotsettings.py +162 -0
  76. exhale_xrf-1.5.0/exhale/data/projectdata.py +932 -0
  77. exhale_xrf-1.5.0/exhale/data/segmentation.py +98 -0
  78. exhale_xrf-1.5.0/exhale/data/segmentationbatch.py +47 -0
  79. exhale_xrf-1.5.0/exhale/data/segmentationediting.py +555 -0
  80. exhale_xrf-1.5.0/exhale/data/segmentationhistory.py +435 -0
  81. exhale_xrf-1.5.0/exhale/data/segmentationref.py +96 -0
  82. exhale_xrf-1.5.0/exhale/data/source_refs.py +287 -0
  83. exhale_xrf-1.5.0/exhale/data/tiffimport.py +64 -0
  84. exhale_xrf-1.5.0/exhale/exceptiondialog.py +73 -0
  85. exhale_xrf-1.5.0/exhale/fileio.py +35 -0
  86. exhale_xrf-1.5.0/exhale/icons.py +207 -0
  87. exhale_xrf-1.5.0/exhale/imaging/__init__.py +1 -0
  88. exhale_xrf-1.5.0/exhale/imaging/element_controller.py +635 -0
  89. exhale_xrf-1.5.0/exhale/imaging/histogramwidget.py +280 -0
  90. exhale_xrf-1.5.0/exhale/imaging/image_controller.py +601 -0
  91. exhale_xrf-1.5.0/exhale/imaging/image_settings_dialog.py +13 -0
  92. exhale_xrf-1.5.0/exhale/imaging/image_settings_dialog_ui.py +161 -0
  93. exhale_xrf-1.5.0/exhale/imaging/imagecanvas.py +467 -0
  94. exhale_xrf-1.5.0/exhale/imaging/imagecomposer.py +685 -0
  95. exhale_xrf-1.5.0/exhale/imaging/listwidgets.py +353 -0
  96. exhale_xrf-1.5.0/exhale/imaging/pdfexport.py +127 -0
  97. exhale_xrf-1.5.0/exhale/main_window.py +518 -0
  98. exhale_xrf-1.5.0/exhale/main_window_ui.py +1548 -0
  99. exhale_xrf-1.5.0/exhale/model_settings.py +23 -0
  100. exhale_xrf-1.5.0/exhale/overridecursor.py +18 -0
  101. exhale_xrf-1.5.0/exhale/project/__init__.py +1 -0
  102. exhale_xrf-1.5.0/exhale/project/project_tab.py +593 -0
  103. exhale_xrf-1.5.0/exhale/project/projectio.py +926 -0
  104. exhale_xrf-1.5.0/exhale/project/projectmanager.py +297 -0
  105. exhale_xrf-1.5.0/exhale/project/projectstatemanager.py +286 -0
  106. exhale_xrf-1.5.0/exhale/project/sourcemanager.py +197 -0
  107. exhale_xrf-1.5.0/exhale/resources/icons/LICENSE-icons.txt +21 -0
  108. exhale_xrf-1.5.0/exhale/resources/icons/adjustments.svg +13 -0
  109. exhale_xrf-1.5.0/exhale/resources/icons/arrow-back-up.svg +5 -0
  110. exhale_xrf-1.5.0/exhale/resources/icons/arrow-forward-up.svg +5 -0
  111. exhale_xrf-1.5.0/exhale/resources/icons/arrows-join.svg +6 -0
  112. exhale_xrf-1.5.0/exhale/resources/icons/arrows-maximize.svg +12 -0
  113. exhale_xrf-1.5.0/exhale/resources/icons/atom.svg +7 -0
  114. exhale_xrf-1.5.0/exhale/resources/icons/bolt.svg +5 -0
  115. exhale_xrf-1.5.0/exhale/resources/icons/brush.svg +7 -0
  116. exhale_xrf-1.5.0/exhale/resources/icons/checkbox-default-marked.svg +7 -0
  117. exhale_xrf-1.5.0/exhale/resources/icons/checkbox-denied-extended.svg +8 -0
  118. exhale_xrf-1.5.0/exhale/resources/icons/checkbox-marked.svg +8 -0
  119. exhale_xrf-1.5.0/exhale/resources/icons/checkbox.svg +4 -0
  120. exhale_xrf-1.5.0/exhale/resources/icons/checkmark.svg +6 -0
  121. exhale_xrf-1.5.0/exhale/resources/icons/clipboard-copy.svg +7 -0
  122. exhale_xrf-1.5.0/exhale/resources/icons/cut.svg +7 -0
  123. exhale_xrf-1.5.0/exhale/resources/icons/eraser.svg +5 -0
  124. exhale_xrf-1.5.0/exhale/resources/icons/file-dots.svg +9 -0
  125. exhale_xrf-1.5.0/exhale/resources/icons/floppy.svg +6 -0
  126. exhale_xrf-1.5.0/exhale/resources/icons/folder-open.svg +5 -0
  127. exhale_xrf-1.5.0/exhale/resources/icons/help.svg +6 -0
  128. exhale_xrf-1.5.0/exhale/resources/icons/info-circle.svg +5 -0
  129. exhale_xrf-1.5.0/exhale/resources/icons/lasso.svg +6 -0
  130. exhale_xrf-1.5.0/exhale/resources/icons/layer-one.svg +7 -0
  131. exhale_xrf-1.5.0/exhale/resources/icons/layer-plus.svg +7 -0
  132. exhale_xrf-1.5.0/exhale/resources/icons/layers-atom.svg +13 -0
  133. exhale_xrf-1.5.0/exhale/resources/icons/layers-plus.svg +9 -0
  134. exhale_xrf-1.5.0/exhale/resources/icons/layers.svg +9 -0
  135. exhale_xrf-1.5.0/exhale/resources/icons/lock.svg +6 -0
  136. exhale_xrf-1.5.0/exhale/resources/icons/open.svg +5 -0
  137. exhale_xrf-1.5.0/exhale/resources/icons/player-stop.svg +5 -0
  138. exhale_xrf-1.5.0/exhale/resources/icons/pointer.svg +4 -0
  139. exhale_xrf-1.5.0/exhale/resources/icons/rays.svg +12 -0
  140. exhale_xrf-1.5.0/exhale/resources/icons/refresh.svg +6 -0
  141. exhale_xrf-1.5.0/exhale/resources/icons/screenshot.svg +13 -0
  142. exhale_xrf-1.5.0/exhale/resources/icons/settings.svg +7 -0
  143. exhale_xrf-1.5.0/exhale/resources/icons/squares-composed-plus.svg +12 -0
  144. exhale_xrf-1.5.0/exhale/resources/icons/squares-composed.svg +11 -0
  145. exhale_xrf-1.5.0/exhale/resources/icons/stack-2.svg +7 -0
  146. exhale_xrf-1.5.0/exhale/resources/icons/stack-bolt.svg +9 -0
  147. exhale_xrf-1.5.0/exhale/resources/icons/trash.svg +9 -0
  148. exhale_xrf-1.5.0/exhale/resources/icons/x-mark.svg +6 -0
  149. exhale_xrf-1.5.0/exhale/resources/icons/xrf.png +0 -0
  150. exhale_xrf-1.5.0/exhale/resources/licenses/DINOv3-LICENSE.md +66 -0
  151. exhale_xrf-1.5.0/exhale/resources/linux/io.github.ctroein.Exhale.desktop +12 -0
  152. exhale_xrf-1.5.0/exhale/resources/models/2D_versatile_fluo_copy/config.json +1 -0
  153. exhale_xrf-1.5.0/exhale/resources/models/2D_versatile_fluo_copy/thresholds.json +1 -0
  154. exhale_xrf-1.5.0/exhale/resources/models/2D_versatile_fluo_copy/weights_best.h5 +0 -0
  155. exhale_xrf-1.5.0/exhale/resources/models/monai_unet/metadata.json +35 -0
  156. exhale_xrf-1.5.0/exhale/resources/models/monai_unet/model.pt +0 -0
  157. exhale_xrf-1.5.0/exhale/resources/xrf_splash.jpg +0 -0
  158. exhale_xrf-1.5.0/exhale/runtimeinfo.py +65 -0
  159. exhale_xrf-1.5.0/exhale/settings_dialog.py +84 -0
  160. exhale_xrf-1.5.0/exhale/settings_dialog_ui.py +94 -0
  161. exhale_xrf-1.5.0/exhale/tooltips.py +228 -0
  162. exhale_xrf-1.5.0/exhale/update_controller.py +189 -0
  163. exhale_xrf-1.5.0/exhale/updates.py +304 -0
  164. exhale_xrf-1.5.0/exhale/widgets.py +189 -0
  165. exhale_xrf-1.5.0/exhale/xrf_refcopy/xrf_element_channel.py +123 -0
  166. exhale_xrf-1.5.0/exhale/xrf_refcopy/xrf_other_channel.py +108 -0
  167. exhale_xrf-1.5.0/exhale/xrf_refcopy/xrf_sample_class.py +579 -0
  168. exhale_xrf-1.5.0/exhale/xrf_refcopy/xrf_utils.py +454 -0
  169. exhale_xrf-1.5.0/exhale_xrf.egg-info/PKG-INFO +229 -0
  170. exhale_xrf-1.5.0/exhale_xrf.egg-info/SOURCES.txt +245 -0
  171. exhale_xrf-1.5.0/exhale_xrf.egg-info/dependency_links.txt +1 -0
  172. exhale_xrf-1.5.0/exhale_xrf.egg-info/entry_points.txt +2 -0
  173. exhale_xrf-1.5.0/exhale_xrf.egg-info/requires.txt +35 -0
  174. exhale_xrf-1.5.0/exhale_xrf.egg-info/scm_file_list.json +263 -0
  175. exhale_xrf-1.5.0/exhale_xrf.egg-info/scm_version.json +8 -0
  176. exhale_xrf-1.5.0/exhale_xrf.egg-info/top_level.txt +7 -0
  177. exhale_xrf-1.5.0/pyproject.toml +85 -0
  178. exhale_xrf-1.5.0/requirements-cellpose.txt +3 -0
  179. exhale_xrf-1.5.0/run_exhale.py +6 -0
  180. exhale_xrf-1.5.0/setup.cfg +4 -0
  181. exhale_xrf-1.5.0/tests/test_analysis_stages.py +110 -0
  182. exhale_xrf-1.5.0/tests/test_analysisworker.py +40 -0
  183. exhale_xrf-1.5.0/tests/test_application_update_trial.py +123 -0
  184. exhale_xrf-1.5.0/tests/test_artwork_generation.py +169 -0
  185. exhale_xrf-1.5.0/tests/test_boundary_calibration_cli.py +100 -0
  186. exhale_xrf-1.5.0/tests/test_boundaryfitting.py +132 -0
  187. exhale_xrf-1.5.0/tests/test_cluster_distance_worker.py +220 -0
  188. exhale_xrf-1.5.0/tests/test_cluster_distances.py +179 -0
  189. exhale_xrf-1.5.0/tests/test_correlations.py +252 -0
  190. exhale_xrf-1.5.0/tests/test_correlationwidgets.py +435 -0
  191. exhale_xrf-1.5.0/tests/test_elementsettings_defaults.py +84 -0
  192. exhale_xrf-1.5.0/tests/test_entrypoints.py +109 -0
  193. exhale_xrf-1.5.0/tests/test_histogramwidget.py +183 -0
  194. exhale_xrf-1.5.0/tests/test_icon_check_delegate.py +142 -0
  195. exhale_xrf-1.5.0/tests/test_imagecanvas.py +234 -0
  196. exhale_xrf-1.5.0/tests/test_imagecomposer_mapping.py +239 -0
  197. exhale_xrf-1.5.0/tests/test_intensity_partition.py +94 -0
  198. exhale_xrf-1.5.0/tests/test_layer_solo.py +231 -0
  199. exhale_xrf-1.5.0/tests/test_macos_runtime.py +263 -0
  200. exhale_xrf-1.5.0/tests/test_matrix_export_dialog.py +87 -0
  201. exhale_xrf-1.5.0/tests/test_matrix_export_routing.py +111 -0
  202. exhale_xrf-1.5.0/tests/test_matrixpdf.py +218 -0
  203. exhale_xrf-1.5.0/tests/test_model_settings.py +34 -0
  204. exhale_xrf-1.5.0/tests/test_model_store.py +224 -0
  205. exhale_xrf-1.5.0/tests/test_napari_initial_fit.py +94 -0
  206. exhale_xrf-1.5.0/tests/test_naparihelper.py +473 -0
  207. exhale_xrf-1.5.0/tests/test_native_updates.py +57 -0
  208. exhale_xrf-1.5.0/tests/test_outlinefitting.py +112 -0
  209. exhale_xrf-1.5.0/tests/test_output_permissions.py +82 -0
  210. exhale_xrf-1.5.0/tests/test_plot_integration.py +263 -0
  211. exhale_xrf-1.5.0/tests/test_plotsettings.py +89 -0
  212. exhale_xrf-1.5.0/tests/test_project_paths.py +71 -0
  213. exhale_xrf-1.5.0/tests/test_projectdata.py +251 -0
  214. exhale_xrf-1.5.0/tests/test_qt_compat.py +95 -0
  215. exhale_xrf-1.5.0/tests/test_samples.py +212 -0
  216. exhale_xrf-1.5.0/tests/test_segmentation_backends.py +139 -0
  217. exhale_xrf-1.5.0/tests/test_segmentation_core.py +197 -0
  218. exhale_xrf-1.5.0/tests/test_segmentation_editing_controller.py +92 -0
  219. exhale_xrf-1.5.0/tests/test_segmentation_history_io.py +373 -0
  220. exhale_xrf-1.5.0/tests/test_segmentation_lifecycle.py +107 -0
  221. exhale_xrf-1.5.0/tests/test_segmentation_monai.py +230 -0
  222. exhale_xrf-1.5.0/tests/test_segmentation_postprocessing.py +139 -0
  223. exhale_xrf-1.5.0/tests/test_segmentation_preferences.py +97 -0
  224. exhale_xrf-1.5.0/tests/test_segmentation_selection_controller.py +80 -0
  225. exhale_xrf-1.5.0/tests/test_segmentation_viewer.py +552 -0
  226. exhale_xrf-1.5.0/tests/test_segmentationbatch.py +373 -0
  227. exhale_xrf-1.5.0/tests/test_segmentationcursor.py +155 -0
  228. exhale_xrf-1.5.0/tests/test_segmentationediting.py +577 -0
  229. exhale_xrf-1.5.0/tests/test_segmentationhistory.py +346 -0
  230. exhale_xrf-1.5.0/tests/test_segmentationio.py +75 -0
  231. exhale_xrf-1.5.0/tests/test_segmentationref.py +156 -0
  232. exhale_xrf-1.5.0/tests/test_smartoutline.py +129 -0
  233. exhale_xrf-1.5.0/tests/test_source_lifecycle.py +1994 -0
  234. exhale_xrf-1.5.0/tests/test_source_readers.py +179 -0
  235. exhale_xrf-1.5.0/tests/test_tensorflow_cuda.py +30 -0
  236. exhale_xrf-1.5.0/tests/test_training_review_controller.py +63 -0
  237. exhale_xrf-1.5.0/tests/test_trainingio.py +149 -0
  238. exhale_xrf-1.5.0/tests/test_update_controller.py +90 -0
  239. exhale_xrf-1.5.0/tests/test_updates.py +202 -0
  240. exhale_xrf-1.5.0/tests/test_version.py +38 -0
  241. exhale_xrf-1.5.0/tests/test_widgets.py +65 -0
  242. exhale_xrf-1.5.0/tests/test_windows_installer.py +74 -0
  243. exhale_xrf-1.5.0/tools/fit_boundary_parameters.py +278 -0
  244. exhale_xrf-1.5.0/tools/prepare_model_release.py +51 -0
  245. exhale_xrf-1.5.0/tools/try_application_update.py +208 -0
  246. exhale_xrf-1.5.0/tools/try_dependency_updates.py +129 -0
  247. exhale_xrf-1.5.0/version_bump.sh +93 -0
@@ -0,0 +1,22 @@
1
+ target-version = "py310"
2
+ line-length = 100
3
+ extend-exclude = [
4
+ "exhale/*_ui.py",
5
+ ]
6
+
7
+ [lint]
8
+ select = [
9
+ "E301", "E302", "E303", "E304", "E305", "E306",
10
+ "E4", "E7", "E9", "F", "W",
11
+ "I", "B", "UP", "RUF100",
12
+ ]
13
+ ignore = ["E501"]
14
+ preview = true
15
+ explicit-preview-rules = true
16
+
17
+ [format]
18
+ quote-style = "double"
19
+ indent-style = "space"
20
+ line-ending = "auto"
21
+ skip-magic-trailing-comma = false
22
+ docstring-code-format = false
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Carl Troein, Tom Delaire, Bryan Falcones, Emanuel Larsson, Karina Thånell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ include README.md
2
+ include pyproject.toml
3
+ include MANIFEST.in
4
+ include run_exhale.py
5
+
6
+ recursive-include exhale *.py
7
+ recursive-include exhale/resources/icons *.png *.svg *.txt
8
+ recursive-include exhale/resources/linux *.desktop
9
+ recursive-include exhale/resources/licenses *
10
+ include exhale/resources/*.jpg
11
+ recursive-include exhale/resources/models *.json *.h5 *.pt
12
+
13
+ exclude .gitignore
14
+
15
+ prune .github
16
+ prune build
17
+ prune dist
18
+ prune packaging
19
+ prune exhale/resources/ui
20
+
21
+ global-exclude __pycache__ *.py[cod]
@@ -0,0 +1,229 @@
1
+ Metadata-Version: 2.4
2
+ Name: exhale-xrf
3
+ Version: 1.5.0
4
+ Summary: Exhale, Efficient X-ray Hub Aiding Lung Explorations
5
+ Author: Tom Delaire, Emanuel Larsson, Bryan Falcones, Karina Thånell
6
+ Author-email: Carl Troein <carl.troein@mgeo.lu.se>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/ctroein/exhale
9
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
10
+ Requires-Python: <3.14,>=3.11
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: packaging<27
14
+ Requires-Dist: numpy<2.6
15
+ Requires-Dist: scipy<1.19
16
+ Requires-Dist: scikit-learn<1.10
17
+ Requires-Dist: pandas<3.2
18
+ Requires-Dist: h5py<3.17
19
+ Requires-Dist: tifffile<2026.11
20
+ Requires-Dist: imagecodecs<2026.11
21
+ Requires-Dist: napari<0.11,>=0.10
22
+ Requires-Dist: imageio<2.39
23
+ Requires-Dist: pyqt6<6.13,>=6.7
24
+ Requires-Dist: qtpy<2.5
25
+ Requires-Dist: pyqtgraph<0.15,>=0.13.7
26
+ Provides-Extra: stardist
27
+ Requires-Dist: csbdeep<0.9; extra == "stardist"
28
+ Requires-Dist: tensorflow==2.15.*; sys_platform == "win32" and extra == "stardist"
29
+ Requires-Dist: tensorflow<2.21; sys_platform == "linux" and extra == "stardist"
30
+ Requires-Dist: tensorflow<2.21; sys_platform == "darwin" and extra == "stardist"
31
+ Requires-Dist: stardist<0.10; extra == "stardist"
32
+ Provides-Extra: cellpose
33
+ Requires-Dist: torch<2.16,>=2.8; extra == "cellpose"
34
+ Requires-Dist: torchvision<0.30,>=0.23; extra == "cellpose"
35
+ Requires-Dist: cellpose==4.2.1.1; extra == "cellpose"
36
+ Provides-Extra: monai
37
+ Requires-Dist: torch<2.16,>=2.8; extra == "monai"
38
+ Requires-Dist: monai<1.7,>=1.6.1; extra == "monai"
39
+ Dynamic: license-file
40
+
41
+ # Exhale
42
+ ## Efficient X-ray Hub Aiding Lung Explorations
43
+
44
+ <p align="center">
45
+ <img src="artwork/screenshots/segmentation_20261023.png"
46
+ alt="EXHALE segmentation results"
47
+ width="900">
48
+ </p>
49
+
50
+ This project aims to develop a specialized tool for investigating elemental aspects of chronic lung diseases.
51
+
52
+ From the [VINNOVA project
53
+ description](https://www.vinnova.se/en/p/exhale--efficient-x-ray-hub-aiding-lung-explorations/):
54
+ The purpose of this project is to advance understanding and treatment of
55
+ chronic lung diseases, leading causes of mortality worldwide, by
56
+ characterizing elements in lung tissue suspected to impact their
57
+ pathogenesis. The industrial partners TrulyLabs and AnaMar in collaboration
58
+ with MAX IV, Lung Biology and Correlative Image Processing and Analysis
59
+ (CIPA) fom Lund University, aim to streamline a workflow of X-ray imaging
60
+ studies for lung diseases, making these methods more accessible for
61
+ biomedical companies and clinical researchers.
62
+
63
+ ## Currect functionality
64
+
65
+ * Loading HDF5 files with XRF data from the NanoMAX beamline at MAX IV.
66
+ * Composing element maps into publication-ready figures.
67
+ * Segmentation of cells and clustering of areas in element maps.
68
+
69
+ ## Installation
70
+
71
+ ### Linux
72
+
73
+ Install [uv](https://docs.astral.sh/uv/getting-started/installation/) if needed,
74
+ then choose the segmentation backends you want.
75
+
76
+ For StarDist (TensorFlow):
77
+
78
+ ```bash
79
+ uv tool install --python 3.12 'exhale-xrf[stardist]'
80
+ ```
81
+
82
+ For Cellpose and MONAI U-Net (PyTorch; Cellpose also needs DINOv3):
83
+
84
+ ```bash
85
+ uv tool install --python 3.12 --with-requirements https://raw.githubusercontent.com/ctroein/exhale/main/requirements-cellpose.txt 'exhale-xrf[cellpose,monai]'
86
+ ```
87
+
88
+ For MONAI U-Net alone:
89
+
90
+ ```bash
91
+ uv tool install --python 3.12 'exhale-xrf[monai]'
92
+ ```
93
+
94
+ MONAI U-Net's trained weights are bundled; no model download is needed.
95
+
96
+ To also include StarDist, use `'exhale-xrf[stardist,cellpose,monai]'` with the same additional
97
+ requirements argument. Plain `exhale-xrf` installs viewing, composition and
98
+ analysis of existing results without neural segmentation backends. Unavailable
99
+ backends are disabled in the application.
100
+
101
+ Run `exhale` to start the application. To add it to the current user's desktop
102
+ environment and application menu:
103
+
104
+ ```bash
105
+ exhale --install-desktop
106
+ ```
107
+
108
+ To update the application and its selected backends:
109
+
110
+ ```bash
111
+ uv tool upgrade exhale-xrf
112
+ ```
113
+
114
+ On Linux, Settings also offers daily update checks and a Check now button.
115
+ Checks notify you of newer stable releases without installing anything. For a
116
+ writable uv tool installation owned by your account, Update now closes Exhale,
117
+ runs the upgrade, checks startup, and reopens it. Normal project saving applies
118
+ before closing. Other Exhale instances must be closed first. Update results and
119
+ the log location are shown after restarting. Automatic checks can be disabled
120
+ in Settings.
121
+
122
+ uv retains the installation settings, including extras and additional
123
+ requirements. To change backend selection, rerun `uv tool install` with the
124
+ wanted extras and, for Cellpose, the requirements argument.
125
+
126
+ To uninstall, remove the desktop/menu entry before removing the application:
127
+
128
+ ```bash
129
+ exhale --remove-desktop
130
+ uv tool uninstall exhale-xrf
131
+ ```
132
+
133
+ ### Windows
134
+
135
+ Windows 10 or later with 64-bit compatibility is required. Download the
136
+ `exhale-…-setup.exe` installer from the
137
+ [GitHub releases page](https://github.com/ctroein/exhale/releases) and run it.
138
+
139
+ The installer offers StarDist, Cellpose, both, or neither before setup. It
140
+ downloads a private Python environment and the selected packages; internet
141
+ access is required, but an existing Python or uv installation is not.
142
+ Use the Start menu or optional desktop shortcut to launch EXHALE.
143
+
144
+ Settings offers daily update checks, Check now, and Update now. Application
145
+ updates preserve the selected backends and validate a new runtime before
146
+ switching to it. You can also download and run a newer installer; use that to
147
+ update the launcher/setup tools, change backends, or repair the installation.
148
+ Uninstall through Windows Settings; the private runtime and shortcuts are
149
+ removed while projects, trained models and application settings are retained.
150
+
151
+ ### macOS
152
+
153
+ Download the Apple Silicon (`arm64`) or Intel (`x86_64`) DMG from the
154
+ [GitHub releases page](https://github.com/ctroein/exhale/releases), drag
155
+ EXHALE.app into Applications, and open it there. Setup downloads Python and the
156
+ selected segmentation backends; neither Python nor uv needs to be installed
157
+ beforehand. Intel Macs support StarDist but not the current Cellpose backend.
158
+ The launcher requires macOS 13 or newer; Cellpose's PyTorch requires at least
159
+ macOS 14 on Apple Silicon, and dependencies may require newer versions.
160
+
161
+ The Mac app is not Developer ID signed or notarized. macOS may block its first
162
+ launch; see [Apple's explanation and opening options](https://support.apple.com/102445).
163
+
164
+ Subsequent launches open the installed application. Hold Option while opening
165
+ EXHALE.app to change backends, repair the installation, or remove the private
166
+ runtime. Settings offers daily update checks and Update now, which preserves
167
+ the backends and validates a new runtime before switching to it. To update the
168
+ launcher/setup tools, quit EXHALE and replace the app with a newer release; setup
169
+ will install its bundled release with your previous backend choices. For removal, use
170
+ **Remove Runtime** in setup, then move EXHALE.app to Trash. Projects, model files,
171
+ and application settings are retained.
172
+
173
+ Alternatively, use the same uv installation, backend selection and update
174
+ commands as on Linux. Start the application with `exhale`. The desktop/menu
175
+ registration commands are Linux-only; to uninstall a Mac uv installation, run:
176
+
177
+ ```bash
178
+ uv tool uninstall exhale-xrf
179
+ ```
180
+
181
+ App-managed environments are separate from installations made with
182
+ `uv tool install`.
183
+
184
+ ### pip and source installations
185
+
186
+ Use a virtual environment with a supported Python version (3.11–3.13).
187
+ **Windows StarDist requires Python 3.11** because of its TensorFlow dependency.
188
+
189
+ For StarDist:
190
+
191
+ ```bash
192
+ python -m pip install 'exhale-xrf[stardist]'
193
+ ```
194
+
195
+ For Cellpose and MONAI U-Net (Apple Silicon required on macOS):
196
+
197
+ ```bash
198
+ python -m pip install 'exhale-xrf[cellpose,monai]' -r https://raw.githubusercontent.com/ctroein/exhale/main/requirements-cellpose.txt
199
+ ```
200
+
201
+ For MONAI U-Net alone, use `python -m pip install 'exhale-xrf[monai]'`; no
202
+ DINOv3 requirements file is needed. Use `'exhale-xrf[stardist,cellpose,monai]'` with
203
+ the same requirements argument to include all three methods. For a Git checkout:
204
+
205
+ ```bash
206
+ python -m pip install -e '.[stardist,cellpose,monai]' -r requirements-cellpose.txt
207
+ ./run_exhale.py
208
+ ```
209
+
210
+ Omit unwanted extras and the requirements file when Cellpose is not needed.
211
+ On Windows, launch the checkout with `python run_exhale.py`.
212
+
213
+ ### DINOv3 and trained models
214
+
215
+ The additional requirements file pins DINOv3 to a tested source archive; Git is
216
+ not required. For a particular EXHALE release, replace `main` in the requirements
217
+ URL with its `vVERSION` tag and append `==VERSION` to the package specification.
218
+ A version-pinned uv installation must be reinstalled with a newer version
219
+ specification to upgrade.
220
+
221
+ Trained Cellpose weights are downloaded on first use to the model directory in
222
+ application settings. The same mechanism is used for installed applications and
223
+ Git checkouts; the large weights are not included in the application installer.
224
+ DINOv3 code and derived model weights retain the
225
+ [DINOv3 license](exhale/resources/licenses/DINOv3-LICENSE.md).
226
+
227
+ ## Developer documentation
228
+
229
+ Developer documentation is located in [`docs/`](docs/README.md).
@@ -0,0 +1,189 @@
1
+ # Exhale
2
+ ## Efficient X-ray Hub Aiding Lung Explorations
3
+
4
+ <p align="center">
5
+ <img src="artwork/screenshots/segmentation_20261023.png"
6
+ alt="EXHALE segmentation results"
7
+ width="900">
8
+ </p>
9
+
10
+ This project aims to develop a specialized tool for investigating elemental aspects of chronic lung diseases.
11
+
12
+ From the [VINNOVA project
13
+ description](https://www.vinnova.se/en/p/exhale--efficient-x-ray-hub-aiding-lung-explorations/):
14
+ The purpose of this project is to advance understanding and treatment of
15
+ chronic lung diseases, leading causes of mortality worldwide, by
16
+ characterizing elements in lung tissue suspected to impact their
17
+ pathogenesis. The industrial partners TrulyLabs and AnaMar in collaboration
18
+ with MAX IV, Lung Biology and Correlative Image Processing and Analysis
19
+ (CIPA) fom Lund University, aim to streamline a workflow of X-ray imaging
20
+ studies for lung diseases, making these methods more accessible for
21
+ biomedical companies and clinical researchers.
22
+
23
+ ## Currect functionality
24
+
25
+ * Loading HDF5 files with XRF data from the NanoMAX beamline at MAX IV.
26
+ * Composing element maps into publication-ready figures.
27
+ * Segmentation of cells and clustering of areas in element maps.
28
+
29
+ ## Installation
30
+
31
+ ### Linux
32
+
33
+ Install [uv](https://docs.astral.sh/uv/getting-started/installation/) if needed,
34
+ then choose the segmentation backends you want.
35
+
36
+ For StarDist (TensorFlow):
37
+
38
+ ```bash
39
+ uv tool install --python 3.12 'exhale-xrf[stardist]'
40
+ ```
41
+
42
+ For Cellpose and MONAI U-Net (PyTorch; Cellpose also needs DINOv3):
43
+
44
+ ```bash
45
+ uv tool install --python 3.12 --with-requirements https://raw.githubusercontent.com/ctroein/exhale/main/requirements-cellpose.txt 'exhale-xrf[cellpose,monai]'
46
+ ```
47
+
48
+ For MONAI U-Net alone:
49
+
50
+ ```bash
51
+ uv tool install --python 3.12 'exhale-xrf[monai]'
52
+ ```
53
+
54
+ MONAI U-Net's trained weights are bundled; no model download is needed.
55
+
56
+ To also include StarDist, use `'exhale-xrf[stardist,cellpose,monai]'` with the same additional
57
+ requirements argument. Plain `exhale-xrf` installs viewing, composition and
58
+ analysis of existing results without neural segmentation backends. Unavailable
59
+ backends are disabled in the application.
60
+
61
+ Run `exhale` to start the application. To add it to the current user's desktop
62
+ environment and application menu:
63
+
64
+ ```bash
65
+ exhale --install-desktop
66
+ ```
67
+
68
+ To update the application and its selected backends:
69
+
70
+ ```bash
71
+ uv tool upgrade exhale-xrf
72
+ ```
73
+
74
+ On Linux, Settings also offers daily update checks and a Check now button.
75
+ Checks notify you of newer stable releases without installing anything. For a
76
+ writable uv tool installation owned by your account, Update now closes Exhale,
77
+ runs the upgrade, checks startup, and reopens it. Normal project saving applies
78
+ before closing. Other Exhale instances must be closed first. Update results and
79
+ the log location are shown after restarting. Automatic checks can be disabled
80
+ in Settings.
81
+
82
+ uv retains the installation settings, including extras and additional
83
+ requirements. To change backend selection, rerun `uv tool install` with the
84
+ wanted extras and, for Cellpose, the requirements argument.
85
+
86
+ To uninstall, remove the desktop/menu entry before removing the application:
87
+
88
+ ```bash
89
+ exhale --remove-desktop
90
+ uv tool uninstall exhale-xrf
91
+ ```
92
+
93
+ ### Windows
94
+
95
+ Windows 10 or later with 64-bit compatibility is required. Download the
96
+ `exhale-…-setup.exe` installer from the
97
+ [GitHub releases page](https://github.com/ctroein/exhale/releases) and run it.
98
+
99
+ The installer offers StarDist, Cellpose, both, or neither before setup. It
100
+ downloads a private Python environment and the selected packages; internet
101
+ access is required, but an existing Python or uv installation is not.
102
+ Use the Start menu or optional desktop shortcut to launch EXHALE.
103
+
104
+ Settings offers daily update checks, Check now, and Update now. Application
105
+ updates preserve the selected backends and validate a new runtime before
106
+ switching to it. You can also download and run a newer installer; use that to
107
+ update the launcher/setup tools, change backends, or repair the installation.
108
+ Uninstall through Windows Settings; the private runtime and shortcuts are
109
+ removed while projects, trained models and application settings are retained.
110
+
111
+ ### macOS
112
+
113
+ Download the Apple Silicon (`arm64`) or Intel (`x86_64`) DMG from the
114
+ [GitHub releases page](https://github.com/ctroein/exhale/releases), drag
115
+ EXHALE.app into Applications, and open it there. Setup downloads Python and the
116
+ selected segmentation backends; neither Python nor uv needs to be installed
117
+ beforehand. Intel Macs support StarDist but not the current Cellpose backend.
118
+ The launcher requires macOS 13 or newer; Cellpose's PyTorch requires at least
119
+ macOS 14 on Apple Silicon, and dependencies may require newer versions.
120
+
121
+ The Mac app is not Developer ID signed or notarized. macOS may block its first
122
+ launch; see [Apple's explanation and opening options](https://support.apple.com/102445).
123
+
124
+ Subsequent launches open the installed application. Hold Option while opening
125
+ EXHALE.app to change backends, repair the installation, or remove the private
126
+ runtime. Settings offers daily update checks and Update now, which preserves
127
+ the backends and validates a new runtime before switching to it. To update the
128
+ launcher/setup tools, quit EXHALE and replace the app with a newer release; setup
129
+ will install its bundled release with your previous backend choices. For removal, use
130
+ **Remove Runtime** in setup, then move EXHALE.app to Trash. Projects, model files,
131
+ and application settings are retained.
132
+
133
+ Alternatively, use the same uv installation, backend selection and update
134
+ commands as on Linux. Start the application with `exhale`. The desktop/menu
135
+ registration commands are Linux-only; to uninstall a Mac uv installation, run:
136
+
137
+ ```bash
138
+ uv tool uninstall exhale-xrf
139
+ ```
140
+
141
+ App-managed environments are separate from installations made with
142
+ `uv tool install`.
143
+
144
+ ### pip and source installations
145
+
146
+ Use a virtual environment with a supported Python version (3.11–3.13).
147
+ **Windows StarDist requires Python 3.11** because of its TensorFlow dependency.
148
+
149
+ For StarDist:
150
+
151
+ ```bash
152
+ python -m pip install 'exhale-xrf[stardist]'
153
+ ```
154
+
155
+ For Cellpose and MONAI U-Net (Apple Silicon required on macOS):
156
+
157
+ ```bash
158
+ python -m pip install 'exhale-xrf[cellpose,monai]' -r https://raw.githubusercontent.com/ctroein/exhale/main/requirements-cellpose.txt
159
+ ```
160
+
161
+ For MONAI U-Net alone, use `python -m pip install 'exhale-xrf[monai]'`; no
162
+ DINOv3 requirements file is needed. Use `'exhale-xrf[stardist,cellpose,monai]'` with
163
+ the same requirements argument to include all three methods. For a Git checkout:
164
+
165
+ ```bash
166
+ python -m pip install -e '.[stardist,cellpose,monai]' -r requirements-cellpose.txt
167
+ ./run_exhale.py
168
+ ```
169
+
170
+ Omit unwanted extras and the requirements file when Cellpose is not needed.
171
+ On Windows, launch the checkout with `python run_exhale.py`.
172
+
173
+ ### DINOv3 and trained models
174
+
175
+ The additional requirements file pins DINOv3 to a tested source archive; Git is
176
+ not required. For a particular EXHALE release, replace `main` in the requirements
177
+ URL with its `vVERSION` tag and append `==VERSION` to the package specification.
178
+ A version-pinned uv installation must be reinstalled with a newer version
179
+ specification to upgrade.
180
+
181
+ Trained Cellpose weights are downloaded on first use to the model directory in
182
+ application settings. The same mechanism is used for installed applications and
183
+ Git checkouts; the large weights are not included in the application installer.
184
+ DINOv3 code and derived model weights retain the
185
+ [DINOv3 license](exhale/resources/licenses/DINOv3-LICENSE.md).
186
+
187
+ ## Developer documentation
188
+
189
+ Developer documentation is located in [`docs/`](docs/README.md).
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env python3
2
+ """Extract the generated X artwork into reproducible icon-source variants."""
3
+
4
+ from argparse import ArgumentParser
5
+ from pathlib import Path
6
+
7
+ import numpy as np
8
+ from PIL import Image
9
+ from scipy.ndimage import distance_transform_edt, label
10
+
11
+ HERE = Path(__file__).resolve().parent
12
+ ROOT = HERE.parents[1]
13
+
14
+ SOURCE_X_RECT = (11, 6, 20, 20) # Inclusive x1, y1, x2, y2 in the 57-pixel mask.
15
+ GUIDE_RG_COLORS = ((230, 35, 0), (0, 190, 0), (218, 151, 18))
16
+ DARK_GREEN = (7, 31, 16)
17
+ RUNTIME_ICON = ROOT / "exhale" / "resources" / "icons" / "xrf.png"
18
+
19
+
20
+ def source_x_mask(source: np.ndarray) -> np.ndarray:
21
+ """Return red-or-green X pixels from its clean source rectangle."""
22
+ red_or_green = (source & 0b011) != 0
23
+ x1, y1, x2, y2 = SOURCE_X_RECT
24
+ mask = np.zeros_like(red_or_green)
25
+ mask[y1 : y2 + 1, x1 : x2 + 1] = red_or_green[y1 : y2 + 1, x1 : x2 + 1]
26
+ if not mask.any():
27
+ raise ValueError(f"no source X pixels found in {SOURCE_X_RECT}")
28
+ return mask
29
+
30
+
31
+ def fitted_x_mask(source: np.ndarray, guide: np.ndarray) -> np.ndarray:
32
+ """Match the source X to its fitted red, green, and yellow guide components."""
33
+ source_mask = source_x_mask(source)
34
+ if guide.shape[0] != guide.shape[1] or source.shape[0] != source.shape[1]:
35
+ raise ValueError("source mask and guide must be square")
36
+ scaled_source = (
37
+ np.asarray(
38
+ Image.fromarray(source_mask.astype(np.uint8) * 255).resize(
39
+ (guide.shape[1], guide.shape[0]),
40
+ Image.Resampling.NEAREST,
41
+ )
42
+ )
43
+ != 0
44
+ )
45
+
46
+ candidate = np.zeros(guide.shape[:2], dtype=bool)
47
+ for color in GUIDE_RG_COLORS:
48
+ candidate |= np.all(guide == color, axis=2)
49
+ components, _ = label(candidate, structure=np.ones((3, 3), dtype=np.uint8))
50
+ component_ids = np.unique(components[scaled_source & candidate])
51
+ component_ids = component_ids[component_ids != 0]
52
+ if not len(component_ids):
53
+ raise ValueError("no fitted guide components overlap the source X")
54
+ return np.isin(components, component_ids)
55
+
56
+
57
+ def expanded_masks(mask: np.ndarray, margin: float, feather: float) -> tuple[np.ndarray, ...]:
58
+ """Return round-cornered core and outer masks plus the exterior blend weight."""
59
+ if margin < 0 or feather < 0:
60
+ raise ValueError("margin and feather must not be negative")
61
+ distance = distance_transform_edt(~mask)
62
+ core = distance <= margin
63
+ outer = distance <= margin + feather
64
+ blend = np.clip((margin + feather - distance) / max(feather, 1.0), 0.0, 1.0)
65
+ blend[core] = 1.0
66
+ blend = blend * blend * (3.0 - 2.0 * blend)
67
+ return core, outer, blend
68
+
69
+
70
+ def square_crop_bounds(mask: np.ndarray, size: int) -> tuple[int, int, int, int]:
71
+ """Return a centered fixed-size crop around the mask."""
72
+ rows, columns = np.nonzero(mask)
73
+ if not len(rows):
74
+ raise ValueError("cannot crop an empty mask")
75
+ center_x = int(columns.min() + columns.max() + 1) // 2
76
+ center_y = int(rows.min() + rows.max() + 1) // 2
77
+ left = center_x - size // 2
78
+ upper = center_y - size // 2
79
+ if left < 0 or upper < 0 or left + size > mask.shape[1] or upper + size > mask.shape[0]:
80
+ raise ValueError("the centered icon crop extends beyond the image")
81
+ return left, upper, left + size, upper + size
82
+
83
+
84
+ def extract_icon_sources(
85
+ source_path: Path,
86
+ guide_path: Path,
87
+ image_path: Path,
88
+ output_directory: Path,
89
+ crop_size: int = 360,
90
+ margin: float = 12.0,
91
+ feather: float = 4.0,
92
+ install_runtime: bool = False,
93
+ ) -> tuple[Path, ...]:
94
+ """Write the mask, diagnostic, transparent, and opaque icon sources."""
95
+ with Image.open(source_path) as image:
96
+ source = np.asarray(image).copy()
97
+ with Image.open(guide_path) as image:
98
+ guide = np.asarray(image.convert("RGB"))
99
+ with Image.open(image_path) as image:
100
+ generated = np.asarray(image.convert("RGB"))
101
+ if generated.shape != guide.shape:
102
+ raise ValueError("guide and generated image dimensions differ")
103
+
104
+ fitted_mask = fitted_x_mask(source, guide)
105
+ core_mask, _outer_mask, blend = expanded_masks(fitted_mask, margin, feather)
106
+ bounds = square_crop_bounds(core_mask, crop_size)
107
+ left, upper, right, lower = bounds
108
+ cropped_mask = core_mask[upper:lower, left:right].copy()
109
+ cropped_mask[[0, -1], :] = False
110
+ cropped_mask[:, [0, -1]] = False
111
+ cropped_blend = blend[upper:lower, left:right, None]
112
+ cropped_image = generated[upper:lower, left:right]
113
+
114
+ output_directory.mkdir(parents=True, exist_ok=True)
115
+ mask_path = output_directory / "xrf_icon_x_mask.png"
116
+ diagnostic_path = output_directory / "xrf_icon_x_region.png"
117
+ transparent_path = output_directory / "xrf_icon_x_transparent.png"
118
+ dark_path = output_directory / "xrf_icon_x_dark.png"
119
+
120
+ Image.fromarray(cropped_mask.astype(np.uint8) * 255, "L").save(mask_path)
121
+
122
+ dark_color = np.asarray(DARK_GREEN, dtype=np.float32)
123
+ composited = np.round(
124
+ cropped_image.astype(np.float32) * cropped_blend + dark_color * (1.0 - cropped_blend)
125
+ ).astype(np.uint8)
126
+
127
+ luminance = np.clip(
128
+ 0.2126 * cropped_image[..., 0]
129
+ + 0.7152 * cropped_image[..., 1]
130
+ + 0.0722 * cropped_image[..., 2],
131
+ 0,
132
+ 255,
133
+ ).astype(np.uint8)
134
+ diagnostic = np.repeat(luminance[..., None], 3, axis=2)
135
+ diagnostic[cropped_mask] = composited[cropped_mask]
136
+ Image.fromarray(diagnostic, "RGB").save(diagnostic_path)
137
+
138
+ transparent = np.empty((*cropped_mask.shape, 4), dtype=np.uint8)
139
+ transparent[..., :3] = composited
140
+ transparent[..., 3] = cropped_mask.astype(np.uint8) * 255
141
+ Image.fromarray(transparent, "RGBA").save(transparent_path)
142
+
143
+ dark = np.empty_like(composited)
144
+ dark[:] = DARK_GREEN
145
+ dark[cropped_mask] = composited[cropped_mask]
146
+ Image.fromarray(dark, "RGB").save(dark_path)
147
+
148
+ if install_runtime:
149
+ RUNTIME_ICON.parent.mkdir(parents=True, exist_ok=True)
150
+ with Image.open(transparent_path) as image:
151
+ image.resize((512, 512), Image.Resampling.LANCZOS).save(RUNTIME_ICON)
152
+
153
+ return mask_path, diagnostic_path, transparent_path, dark_path
154
+
155
+
156
+ def parse_args() -> ArgumentParser:
157
+ parser = ArgumentParser(description=__doc__)
158
+ parser.add_argument("--source", type=Path, default=HERE / "57-masks.png")
159
+ parser.add_argument("--guide", type=Path, default=HERE / "xrf_guide_57.png")
160
+ parser.add_argument("--image", type=Path, default=HERE / "xrf_imagegen_57.png")
161
+ parser.add_argument("--output-directory", type=Path, default=HERE)
162
+ parser.add_argument("--crop-size", type=int, default=360)
163
+ parser.add_argument("--margin", type=float, default=12.0)
164
+ parser.add_argument("--feather", type=float, default=4.0)
165
+ parser.add_argument(
166
+ "--install-runtime",
167
+ action="store_true",
168
+ help="install the transparent 512-pixel application and Linux icon",
169
+ )
170
+ return parser
171
+
172
+
173
+ def main() -> None:
174
+ args = parse_args().parse_args()
175
+ paths = extract_icon_sources(
176
+ args.source,
177
+ args.guide,
178
+ args.image,
179
+ args.output_directory,
180
+ args.crop_size,
181
+ args.margin,
182
+ args.feather,
183
+ args.install_runtime,
184
+ )
185
+ for path in paths:
186
+ print(path)
187
+
188
+
189
+ if __name__ == "__main__":
190
+ main()