shinestacker 0.3.5__tar.gz → 0.3.6__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.

Potentially problematic release.


This version of shinestacker might be problematic. Click here for more details.

Files changed (136) hide show
  1. {shinestacker-0.3.5 → shinestacker-0.3.6}/CHANGELOG.md +12 -1
  2. {shinestacker-0.3.5 → shinestacker-0.3.6}/PKG-INFO +4 -8
  3. {shinestacker-0.3.5 → shinestacker-0.3.6}/README.md +3 -7
  4. {shinestacker-0.3.5 → shinestacker-0.3.6}/docs/main.md +2 -5
  5. shinestacker-0.3.6/src/shinestacker/_version.py +1 -0
  6. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/algorithms/pyramid.py +7 -4
  7. shinestacker-0.3.6/src/shinestacker/app/app_config.py +22 -0
  8. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/config/config.py +21 -16
  9. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/gui/action_config.py +10 -35
  10. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/gui/actions_window.py +18 -47
  11. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/gui/new_project.py +5 -22
  12. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/gui/project_editor.py +43 -20
  13. shinestacker-0.3.6/src/shinestacker/gui/select_path_widget.py +30 -0
  14. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/base_filter.py +12 -1
  15. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/denoise_filter.py +4 -10
  16. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/exif_data.py +3 -9
  17. shinestacker-0.3.6/src/shinestacker/retouch/icon_container.py +19 -0
  18. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/shortcuts_help.py +2 -13
  19. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/unsharp_mask_filter.py +3 -10
  20. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/white_balance_filter.py +5 -13
  21. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker.egg-info/PKG-INFO +4 -8
  22. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker.egg-info/SOURCES.txt +2 -3
  23. shinestacker-0.3.5/img/coins.gif +0 -0
  24. shinestacker-0.3.5/img/coins_stack.jpg +0 -0
  25. shinestacker-0.3.5/src/shinestacker/_version.py +0 -1
  26. shinestacker-0.3.5/src/shinestacker/algorithms/core_utils.py +0 -22
  27. shinestacker-0.3.5/src/shinestacker/app/app_config.py +0 -40
  28. {shinestacker-0.3.5 → shinestacker-0.3.6}/.coverage +0 -0
  29. {shinestacker-0.3.5 → shinestacker-0.3.6}/.coveragerc +0 -0
  30. {shinestacker-0.3.5 → shinestacker-0.3.6}/.flake8 +0 -0
  31. {shinestacker-0.3.5 → shinestacker-0.3.6}/.github/workflows/ci-multiplatform.yml +0 -0
  32. {shinestacker-0.3.5 → shinestacker-0.3.6}/.github/workflows/pylint.yml +0 -0
  33. {shinestacker-0.3.5 → shinestacker-0.3.6}/.github/workflows/pypi-publish.yml +0 -0
  34. {shinestacker-0.3.5 → shinestacker-0.3.6}/.github/workflows/release.yml +0 -0
  35. {shinestacker-0.3.5 → shinestacker-0.3.6}/.gitignore +0 -0
  36. {shinestacker-0.3.5 → shinestacker-0.3.6}/.pylintrc +0 -0
  37. {shinestacker-0.3.5 → shinestacker-0.3.6}/.readthedocs.yaml +0 -0
  38. {shinestacker-0.3.5 → shinestacker-0.3.6}/LICENSE +0 -0
  39. {shinestacker-0.3.5 → shinestacker-0.3.6}/MANIFEST.in +0 -0
  40. {shinestacker-0.3.5 → shinestacker-0.3.6}/THIRD_PARTY_LICENSES.txt +0 -0
  41. {shinestacker-0.3.5 → shinestacker-0.3.6}/docs/alignment.md +0 -0
  42. {shinestacker-0.3.5 → shinestacker-0.3.6}/docs/api.md +0 -0
  43. {shinestacker-0.3.5 → shinestacker-0.3.6}/docs/balancing.md +0 -0
  44. {shinestacker-0.3.5 → shinestacker-0.3.6}/docs/conf.py +0 -0
  45. {shinestacker-0.3.5 → shinestacker-0.3.6}/docs/focus_stacking.md +0 -0
  46. {shinestacker-0.3.5 → shinestacker-0.3.6}/docs/gui.md +0 -0
  47. {shinestacker-0.3.5 → shinestacker-0.3.6}/docs/index.md +0 -0
  48. {shinestacker-0.3.5 → shinestacker-0.3.6}/docs/job.md +0 -0
  49. {shinestacker-0.3.5 → shinestacker-0.3.6}/docs/multilayer.md +0 -0
  50. {shinestacker-0.3.5 → shinestacker-0.3.6}/docs/noise.md +0 -0
  51. {shinestacker-0.3.5 → shinestacker-0.3.6}/docs/requirements.txt +0 -0
  52. {shinestacker-0.3.5 → shinestacker-0.3.6}/docs/vignetting.md +0 -0
  53. {shinestacker-0.3.5 → shinestacker-0.3.6}/img/coffee.gif +0 -0
  54. {shinestacker-0.3.5 → shinestacker-0.3.6}/img/coffee_stack.jpg +0 -0
  55. {shinestacker-0.3.5 → shinestacker-0.3.6}/img/extreme-vignetting.jpg +0 -0
  56. {shinestacker-0.3.5 → shinestacker-0.3.6}/img/flies.gif +0 -0
  57. {shinestacker-0.3.5 → shinestacker-0.3.6}/img/flies_stack.jpg +0 -0
  58. {shinestacker-0.3.5 → shinestacker-0.3.6}/img/flow-diagram.png +0 -0
  59. {shinestacker-0.3.5 → shinestacker-0.3.6}/img/gui-finder.png +0 -0
  60. {shinestacker-0.3.5 → shinestacker-0.3.6}/img/gui-project-new.png +0 -0
  61. {shinestacker-0.3.5 → shinestacker-0.3.6}/img/gui-project-run.png +0 -0
  62. {shinestacker-0.3.5 → shinestacker-0.3.6}/img/gui-retouch.png +0 -0
  63. {shinestacker-0.3.5 → shinestacker-0.3.6}/pyproject.toml +0 -0
  64. {shinestacker-0.3.5 → shinestacker-0.3.6}/requirements.txt +0 -0
  65. {shinestacker-0.3.5 → shinestacker-0.3.6}/scripts/build_release.py +0 -0
  66. {shinestacker-0.3.5 → shinestacker-0.3.6}/scripts/validate-tomli.py +0 -0
  67. {shinestacker-0.3.5 → shinestacker-0.3.6}/setup.cfg +0 -0
  68. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/__init__.py +0 -0
  69. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/algorithms/__init__.py +0 -0
  70. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/algorithms/align.py +0 -0
  71. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/algorithms/balance.py +0 -0
  72. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/algorithms/base_stack_algo.py +0 -0
  73. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/algorithms/denoise.py +0 -0
  74. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/algorithms/depth_map.py +0 -0
  75. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/algorithms/exif.py +0 -0
  76. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/algorithms/multilayer.py +0 -0
  77. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/algorithms/noise_detection.py +0 -0
  78. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/algorithms/sharpen.py +0 -0
  79. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/algorithms/stack.py +0 -0
  80. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/algorithms/stack_framework.py +0 -0
  81. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/algorithms/utils.py +0 -0
  82. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/algorithms/vignetting.py +0 -0
  83. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/algorithms/white_balance.py +0 -0
  84. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/app/__init__.py +0 -0
  85. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/app/about_dialog.py +0 -0
  86. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/app/gui_utils.py +0 -0
  87. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/app/help_menu.py +0 -0
  88. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/app/main.py +0 -0
  89. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/app/open_frames.py +0 -0
  90. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/app/project.py +0 -0
  91. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/app/retouch.py +0 -0
  92. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/config/__init__.py +0 -0
  93. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/config/constants.py +0 -0
  94. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/config/gui_constants.py +0 -0
  95. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/core/__init__.py +0 -0
  96. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/core/colors.py +0 -0
  97. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/core/core_utils.py +0 -0
  98. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/core/exceptions.py +0 -0
  99. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/core/framework.py +0 -0
  100. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/core/logging.py +0 -0
  101. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/gui/__init__.py +0 -0
  102. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/gui/colors.py +0 -0
  103. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/gui/gui_images.py +0 -0
  104. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/gui/gui_logging.py +0 -0
  105. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/gui/gui_run.py +0 -0
  106. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
  107. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
  108. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
  109. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/gui/ico/shinestacker.png +0 -0
  110. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
  111. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
  112. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
  113. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
  114. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/gui/main_window.py +0 -0
  115. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/gui/project_converter.py +0 -0
  116. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/gui/project_model.py +0 -0
  117. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/__init__.py +0 -0
  118. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/brush.py +0 -0
  119. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/brush_gradient.py +0 -0
  120. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/brush_preview.py +0 -0
  121. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/brush_tool.py +0 -0
  122. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/display_manager.py +0 -0
  123. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/file_loader.py +0 -0
  124. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/filter_manager.py +0 -0
  125. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/image_editor.py +0 -0
  126. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/image_editor_ui.py +0 -0
  127. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/image_filters.py +0 -0
  128. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/image_viewer.py +0 -0
  129. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/io_gui_handler.py +0 -0
  130. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/io_manager.py +0 -0
  131. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/layer_collection.py +0 -0
  132. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker/retouch/undo_manager.py +0 -0
  133. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker.egg-info/dependency_links.txt +0 -0
  134. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker.egg-info/entry_points.txt +0 -0
  135. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker.egg-info/requires.txt +0 -0
  136. {shinestacker-0.3.5 → shinestacker-0.3.6}/src/shinestacker.egg-info/top_level.txt +0 -0
@@ -4,12 +4,23 @@ This page reports the main releases only and the main changes therein.
4
4
 
5
5
  ---
6
6
 
7
+ ## [v-.-.-] - 2025-08-17
8
+ **Unreleased changes, for next release round**
9
+
10
+ ### Changes
11
+
12
+ * fixed a bug that prevented a complete clean up when "New Project" action is called
13
+ * fixed the management of project file path while loading and saving
14
+ * removed duplicated code, code clean up
15
+
16
+ ---
17
+
7
18
  ## [v0.3.5] - 2025-08-17
8
19
  **Bug fixes**
9
20
 
10
21
  ### Changes
11
22
 
12
- * fixed a bug that prevented to add sub-actoins
23
+ * fixed a bug that prevented to add sub-actions
13
24
  * vignetting constrains model parameter in order to prevent searching for dark areas at the center of the image instead of at periphery
14
25
  * updated sample images and documentation
15
26
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 0.3.5
3
+ Version: 0.3.6
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -38,25 +38,21 @@ Dynamic: license-file
38
38
  [![Python Versions](https://img.shields.io/pypi/pyversions/shinestacker)](https://pypi.org/project/shinestacker/)
39
39
  [![Qt Versions](https://img.shields.io/badge/Qt-6-blue.svg?&logo=Qt&logoWidth=18&logoColor=white)](https://www.qt.io/qt-for-python)
40
40
  [![pylint](https://img.shields.io/badge/PyLint-9.98-yellow?logo=python&logoColor=white)](https://github.com/lucalista/shinestacker/blob/main/.github/workflows/pylint.yml)
41
- [![Documentation Status](https://readthedocs.org/projects/shinestacker/badge/?version=latest)](https://shinestacker.readthedocs.io/en/latest/?badge=latest)
42
- <!--
43
41
  [![codecov](https://codecov.io/github/lucalista/shinestacker/graph/badge.svg?token=Y5NKW6VH5G)](https://codecov.io/github/lucalista/shinestacker)
44
- -->
42
+ [![Documentation Status](https://readthedocs.org/projects/shinestacker/badge/?version=latest)](https://shinestacker.readthedocs.io/en/latest/?badge=latest)
45
43
 
46
44
 
47
45
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies_stack.jpg' width="400" referrerpolicy="no-referrer">
48
46
 
49
47
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coffee.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coffee_stack.jpg' width="400" referrerpolicy="no-referrer">
50
48
 
51
- <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coins.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coins_stack.jpg' width="400" referrerpolicy="no-referrer">
52
49
  > **Focus stacking** for microscopy, macro photography, and computational imaging
53
50
 
54
51
  ## Key Features
55
52
  - 🚀 **Batch Processing**: Align, balance, and stack hundreds of images
56
- - 🎨 **Hybrid Workflows**: Combine Python scripting with GUI refinement
57
53
  - 🧩 **Modular Architecture**: Mix-and-match processing modules
58
54
  - 🖌️ **Retouch Editing**: Final interactive retouch of stacked image from individual frames
59
- - 📊 **Jupyter Integration**: Reproducible research notebooks
55
+ - 📊 **Jupyter Integration**: Image processing python notebooks
60
56
 
61
57
  ## Interactive GUI
62
58
 
@@ -77,7 +73,7 @@ The GUI has two main working areas:
77
73
 
78
74
  # Credits
79
75
 
80
- The main pyramid stack algorithm was initially inspired by the [Laplacian pyramids method](https://github.com/sjawhar/focus-stacking) implementation by Sami Jawhar that was used under permission of the author for initial versions of this package. The implementation in the latest releases was rewritten from the original code.
76
+ The first version of the core focus stack algorithm was initially inspired by the [Laplacian pyramids method](https://github.com/sjawhar/focus-stacking) implementation by Sami Jawhar that was used under permission of the author. The implementation in the latest releases was rewritten from the original code.
81
77
 
82
78
  # Resources
83
79
 
@@ -7,25 +7,21 @@
7
7
  [![Python Versions](https://img.shields.io/pypi/pyversions/shinestacker)](https://pypi.org/project/shinestacker/)
8
8
  [![Qt Versions](https://img.shields.io/badge/Qt-6-blue.svg?&logo=Qt&logoWidth=18&logoColor=white)](https://www.qt.io/qt-for-python)
9
9
  [![pylint](https://img.shields.io/badge/PyLint-9.98-yellow?logo=python&logoColor=white)](https://github.com/lucalista/shinestacker/blob/main/.github/workflows/pylint.yml)
10
- [![Documentation Status](https://readthedocs.org/projects/shinestacker/badge/?version=latest)](https://shinestacker.readthedocs.io/en/latest/?badge=latest)
11
- <!--
12
10
  [![codecov](https://codecov.io/github/lucalista/shinestacker/graph/badge.svg?token=Y5NKW6VH5G)](https://codecov.io/github/lucalista/shinestacker)
13
- -->
11
+ [![Documentation Status](https://readthedocs.org/projects/shinestacker/badge/?version=latest)](https://shinestacker.readthedocs.io/en/latest/?badge=latest)
14
12
 
15
13
 
16
14
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies_stack.jpg' width="400" referrerpolicy="no-referrer">
17
15
 
18
16
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coffee.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coffee_stack.jpg' width="400" referrerpolicy="no-referrer">
19
17
 
20
- <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coins.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coins_stack.jpg' width="400" referrerpolicy="no-referrer">
21
18
  > **Focus stacking** for microscopy, macro photography, and computational imaging
22
19
 
23
20
  ## Key Features
24
21
  - 🚀 **Batch Processing**: Align, balance, and stack hundreds of images
25
- - 🎨 **Hybrid Workflows**: Combine Python scripting with GUI refinement
26
22
  - 🧩 **Modular Architecture**: Mix-and-match processing modules
27
23
  - 🖌️ **Retouch Editing**: Final interactive retouch of stacked image from individual frames
28
- - 📊 **Jupyter Integration**: Reproducible research notebooks
24
+ - 📊 **Jupyter Integration**: Image processing python notebooks
29
25
 
30
26
  ## Interactive GUI
31
27
 
@@ -46,7 +42,7 @@ The GUI has two main working areas:
46
42
 
47
43
  # Credits
48
44
 
49
- The main pyramid stack algorithm was initially inspired by the [Laplacian pyramids method](https://github.com/sjawhar/focus-stacking) implementation by Sami Jawhar that was used under permission of the author for initial versions of this package. The implementation in the latest releases was rewritten from the original code.
45
+ The first version of the core focus stack algorithm was initially inspired by the [Laplacian pyramids method](https://github.com/sjawhar/focus-stacking) implementation by Sami Jawhar that was used under permission of the author. The implementation in the latest releases was rewritten from the original code.
50
46
 
51
47
  # Resources
52
48
 
@@ -6,16 +6,13 @@
6
6
 
7
7
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coffee.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coffee_stack.jpg' width="400" referrerpolicy="no-referrer">
8
8
 
9
- <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coins.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coins_stack.jpg' width="400" referrerpolicy="no-referrer">
10
-
11
9
  > **Focus stacking** for microscopy, macro photography, and computational imaging
12
10
 
13
11
  ## Key Features
14
12
  - 🚀 **Batch Processing**: Align, balance, and stack hundreds of images
15
- - 🎨 **Hybrid Workflows**: Combine Python scripting with GUI refinement
16
13
  - 🧩 **Modular Architecture**: Mix-and-match processing modules
17
- - 🖌️ **Non-Destructive Editing**: Save multilayer TIFFs for retouching
18
- - 📊 **Jupyter Integration**: Reproducible research notebooks
14
+ - 🖌️ **Retouch Editing**: Final interactive retouch of stacked image from individual frames
15
+ - 📊 **Jupyter Integration**: Image processing python notebooks
19
16
 
20
17
 
21
18
  ## Quick start
@@ -0,0 +1 @@
1
+ __version__ = '0.3.6'
@@ -12,7 +12,7 @@ class PyramidBase(BaseStackAlgo):
12
12
  kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
13
13
  gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
14
14
  float_type=constants.DEFAULT_PY_FLOAT):
15
- super().__init__("pyramid", 1, float_type)
15
+ super().__init__("pyramid", 2, float_type)
16
16
  self.min_size = min_size
17
17
  self.kernel_size = kernel_size
18
18
  self.pad_amount = (kernel_size - 1) // 2
@@ -151,11 +151,11 @@ class PyramidStack(PyramidBase):
151
151
  metadata = None
152
152
  all_laplacians = []
153
153
  levels = None
154
+ n = len(filenames)
154
155
  for i, img_path in enumerate(filenames):
155
156
  self.print_message(f": validating file {img_path.split('/')[-1]}")
156
157
 
157
158
  img, metadata, updated = self.read_image_and_update_metadata(img_path, metadata)
158
-
159
159
  if updated:
160
160
  self.dtype = metadata[1]
161
161
  self.num_pixel_values = constants.NUM_UINT8 \
@@ -163,14 +163,17 @@ class PyramidStack(PyramidBase):
163
163
  self.max_pixel_value = constants.MAX_UINT8 \
164
164
  if self.dtype == np.uint8 else constants.MAX_UINT16
165
165
  levels = int(np.log2(min(img.shape[:2]) / self.min_size))
166
-
167
166
  if self.do_step_callback:
168
167
  self.process.callback('after_step', self.process.id, self.process.name, i)
169
168
  if self.process.callback('check_running', self.process.id, self.process.name) is False:
170
169
  raise RunStopException(self.name)
171
- for img_path in filenames:
170
+ for i, img_path in enumerate(filenames):
172
171
  self.print_message(f": processing file {img_path.split('/')[-1]}")
173
172
  img = read_img(img_path)
174
173
  all_laplacians.append(self.process_single_image(img, levels))
174
+ if self.do_step_callback:
175
+ self.process.callback('after_step', self.process.id, self.process.name, i + n)
176
+ if self.process.callback('check_running', self.process.id, self.process.name) is False:
177
+ raise RunStopException(self.name)
175
178
  stacked_image = self.collapse(self.fuse_pyramids(all_laplacians))
176
179
  return stacked_image.astype(self.dtype)
@@ -0,0 +1,22 @@
1
+ # pylint: disable=C0114, C0115, C0116, C0103, W0201
2
+ from .. config.config import _ConfigBase
3
+
4
+
5
+ class _AppConfig(_ConfigBase):
6
+ def __new__(cls):
7
+ return _ConfigBase.__new__(cls)
8
+
9
+ def _init_defaults(self):
10
+ self._DONT_USE_NATIVE_MENU = True
11
+ self._COMBINED_APP = False
12
+
13
+ @property
14
+ def DONT_USE_NATIVE_MENU(self):
15
+ return self._DONT_USE_NATIVE_MENU
16
+
17
+ @property
18
+ def COMBINED_APP(self):
19
+ return self._COMBINED_APP
20
+
21
+
22
+ app_config = _AppConfig()
@@ -1,5 +1,5 @@
1
1
  # pylint: disable=C0114, C0115, C0116, C0103, R0903, W0718, W0104, W0201, E0602
2
- class _Config:
2
+ class _ConfigBase:
3
3
  _initialized = False
4
4
  _instance = None
5
5
 
@@ -9,16 +9,6 @@ class _Config:
9
9
  cls._instance._init_defaults()
10
10
  return cls._instance
11
11
 
12
- def _init_defaults(self):
13
- self._DISABLE_TQDM = False
14
- self._COMBINED_APP = False
15
- self._DONT_USE_NATIVE_MENU = True
16
- try:
17
- __IPYTHON__ # noqa
18
- self._JUPYTER_NOTEBOOK = True
19
- except Exception:
20
- self._JUPYTER_NOTEBOOK = False
21
-
22
12
  def init(self, **kwargs):
23
13
  if self._initialized:
24
14
  raise RuntimeError("Config already initialized")
@@ -29,6 +19,26 @@ class _Config:
29
19
  raise AttributeError(f"Invalid config key: {k}")
30
20
  self._initialized = True
31
21
 
22
+ def __setattr__(self, name, value):
23
+ if self._initialized and name.startswith('_'):
24
+ raise AttributeError("Can't change config after initialization")
25
+ super().__setattr__(name, value)
26
+
27
+ class _Config(_ConfigBase):
28
+
29
+ def __new__(cls):
30
+ return _ConfigBase.__new__(cls)
31
+
32
+ def _init_defaults(self):
33
+ self._DISABLE_TQDM = False
34
+ self._COMBINED_APP = False
35
+ self._DONT_USE_NATIVE_MENU = True
36
+ try:
37
+ __IPYTHON__ # noqa
38
+ self._JUPYTER_NOTEBOOK = True
39
+ except Exception:
40
+ self._JUPYTER_NOTEBOOK = False
41
+
32
42
  @property
33
43
  def DISABLE_TQDM(self):
34
44
  return self._DISABLE_TQDM
@@ -45,10 +55,5 @@ class _Config:
45
55
  def COMBINED_APP(self):
46
56
  return self._COMBINED_APP
47
57
 
48
- def __setattr__(self, name, value):
49
- if self._initialized and name.startswith('_'):
50
- raise AttributeError("Can't change config after initialization")
51
- super().__setattr__(name, value)
52
-
53
58
 
54
59
  config = _Config()
@@ -10,8 +10,10 @@ from PySide6.QtWidgets import (QWidget, QPushButton, QHBoxLayout, QFileDialog, Q
10
10
  QAbstractItemView, QListView)
11
11
  from PySide6.QtCore import Qt, QTimer
12
12
  from .. config.constants import constants
13
- from .project_model import ActionConfig
14
13
  from .. algorithms.align import validate_align_config
14
+ from .project_model import ActionConfig
15
+ from .select_path_widget import (create_select_file_paths_widget, create_layout_widget_no_margins,
16
+ create_layout_widget_and_connect)
15
17
 
16
18
  FIELD_TEXT = 'text'
17
19
  FIELD_ABS_PATH = 'abs_path'
@@ -200,25 +202,11 @@ class FieldBuilder:
200
202
  return edit
201
203
 
202
204
  def create_abs_path_field(self, tag, **kwargs):
203
- value = self.action.params.get(tag, '')
204
- edit = QLineEdit(value)
205
- edit.setPlaceholderText(kwargs.get('placeholder', ''))
206
- button = QPushButton("Browse...")
207
-
208
- def browse():
209
- path = QFileDialog.getExistingDirectory(None, f"Select {tag.replace('_', ' ')}")
210
- if path:
211
- edit.setText(path)
212
- button.clicked.connect(browse)
213
- button.setAutoDefault(False)
214
- layout = QHBoxLayout()
215
- layout.addWidget(edit)
216
- layout.addWidget(button)
217
- layout.setContentsMargins(0, 0, 0, 0)
218
- container = QWidget()
219
- container.setLayout(layout)
220
- container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
221
- return container
205
+ return create_select_file_paths_widget(
206
+ self.action.params.get(tag, ''),
207
+ kwargs.get('placeholder', ''),
208
+ tag.replace('_', ' ')
209
+ )
222
210
 
223
211
  def create_rel_path_field(self, tag, **kwargs):
224
212
  value = self.action.params.get(tag, kwargs.get('default', ''))
@@ -326,17 +314,8 @@ class FieldBuilder:
326
314
  except ValueError as e:
327
315
  traceback.print_tb(e.__traceback__)
328
316
  QMessageBox.warning(None, "Error", "Could not compute relative path")
317
+ return create_layout_widget_and_connect(button, edit, browse)
329
318
 
330
- button.clicked.connect(browse)
331
- button.setAutoDefault(False)
332
- layout = QHBoxLayout()
333
- layout.addWidget(edit)
334
- layout.addWidget(button)
335
- layout.setContentsMargins(0, 0, 0, 0)
336
- container = QWidget()
337
- container.setLayout(layout)
338
- container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
339
- return container
340
319
 
341
320
  def create_float_field(self, tag, default=0.0, min_val=0.0, max_val=1.0,
342
321
  step=0.1, decimals=2):
@@ -369,11 +348,7 @@ class FieldBuilder:
369
348
  layout.addWidget(label)
370
349
  layout.addWidget(spin)
371
350
  layout.setStretch(layout.count() - 1, 1)
372
- layout.setContentsMargins(0, 0, 0, 0)
373
- container = QWidget()
374
- container.setLayout(layout)
375
- container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
376
- return container
351
+ return create_layout_widget_no_margins(layout)
377
352
 
378
353
  def create_combo_field(self, tag, options=None, default=None, **kwargs):
379
354
  options = options or []
@@ -1,7 +1,7 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0914, R0912, R0915, W0718
2
2
  import os.path
3
3
  import os
4
- import traceback
4
+ # import traceback
5
5
  import json
6
6
  import jsonpickle
7
7
  from PySide6.QtWidgets import QMessageBox, QFileDialog, QDialog
@@ -20,8 +20,9 @@ class ActionsWindow(ProjectEditor):
20
20
 
21
21
  def update_title(self):
22
22
  title = constants.APP_TITLE
23
- if self._current_file:
24
- title += f" - {os.path.basename(self._current_file)}"
23
+ file_name = self.current_file_name()
24
+ if file_name:
25
+ title += f" - {file_name}"
25
26
  if self._modified_project:
26
27
  title += " *"
27
28
  self.window().setWindowTitle(title)
@@ -34,7 +35,7 @@ class ActionsWindow(ProjectEditor):
34
35
  def close_project(self):
35
36
  if self._check_unsaved_changes():
36
37
  self.set_project(Project())
37
- self._current_file = None
38
+ self.set_current_file_path('')
38
39
  self.update_title()
39
40
  self.job_list.clear()
40
41
  self.action_list.clear()
@@ -44,17 +45,17 @@ class ActionsWindow(ProjectEditor):
44
45
  if not self._check_unsaved_changes():
45
46
  return
46
47
  os.chdir(get_app_base_path())
47
- self._current_file = None
48
+ self.set_current_file_path('')
48
49
  self._modified_project = False
49
50
  self.update_title()
50
51
  self.job_list.clear()
51
52
  self.action_list.clear()
53
+ self.set_project(Project())
52
54
  dialog = NewProjectDialog(self)
53
55
  if dialog.exec() == QDialog.Accepted:
54
56
  input_folder = dialog.get_input_folder().split('/')
55
57
  working_path = '/'.join(input_folder[:-1])
56
58
  input_path = input_folder[-1]
57
- project = Project()
58
59
  if dialog.get_noise_detection():
59
60
  job_noise = ActionConfig(constants.ACTION_JOB,
60
61
  {'name': 'detect-noise', 'working_path': working_path,
@@ -62,7 +63,7 @@ class ActionsWindow(ProjectEditor):
62
63
  noise_detection = ActionConfig(constants.ACTION_NOISEDETECTION,
63
64
  {'name': 'detect-noise'})
64
65
  job_noise.add_sub_action(noise_detection)
65
- project.jobs.append(job_noise)
66
+ self.project.jobs.append(job_noise)
66
67
  job = ActionConfig(constants.ACTION_JOB,
67
68
  {'name': 'focus-stack', 'working_path': working_path,
68
69
  'input_path': input_path})
@@ -111,8 +112,7 @@ class ActionsWindow(ProjectEditor):
111
112
  {'name': 'multi-layer',
112
113
  'input_path': ','.join(input_path)})
113
114
  job.add_sub_action(multi_layer)
114
- project.jobs.append(job)
115
- self.set_project(project)
115
+ self.project.jobs.append(job)
116
116
  self._modified_project = True
117
117
  self.refresh_ui(0, -1)
118
118
 
@@ -124,17 +124,9 @@ class ActionsWindow(ProjectEditor):
124
124
  self, "Open Project", "", "Project Files (*.fsp);;All Files (*)")
125
125
  if file_path:
126
126
  try:
127
- self._current_file = file_path
128
- self._current_file_wd = '' if os.path.isabs(file_path) \
129
- else os.path.dirname(file_path)
130
- if not os.path.isabs(self._current_file_wd):
131
- self._current_file_wd = os.path.abspath(self._current_file_wd)
132
- self._current_file = os.path.basename(self._current_file)
133
- with open(file_path, 'r', encoding="utf-8") as file:
127
+ self.set_current_file_path(file_path)
128
+ with open(self.current_file_path(), 'r', encoding="utf-8") as file:
134
129
  json_obj = json.load(file)
135
- pp = file_path.split('/')
136
- if len(pp) > 1:
137
- os.chdir('/'.join(pp[:-1]))
138
130
  project = Project.from_dict(json_obj['project'])
139
131
  if project is None:
140
132
  raise RuntimeError(f"Project from file {file_path} produced a null project.")
@@ -145,7 +137,7 @@ class ActionsWindow(ProjectEditor):
145
137
  if self.job_list.count() > 0:
146
138
  self.job_list.setCurrentRow(0)
147
139
  except Exception as e:
148
- traceback.print_tb(e.__traceback__)
140
+ # traceback.print_tb(e.__traceback__)
149
141
  QMessageBox.critical(self, "Error", f"Cannot open file {file_path}:\n{str(e)}")
150
142
  if len(self.project.jobs) > 0:
151
143
  self.job_list.setCurrentRow(0)
@@ -173,12 +165,10 @@ class ActionsWindow(ProjectEditor):
173
165
  Please, select a valid working path.''')
174
166
  self.edit_action(action)
175
167
 
176
- def current_file_name(self):
177
- return os.path.basename(self._current_file) if self._current_file else ''
178
-
179
168
  def save_project(self):
180
- if self._current_file:
181
- self.do_save(self._current_file)
169
+ path = self.current_file_path()
170
+ if path:
171
+ self.do_save(path)
182
172
  else:
183
173
  self.save_project_as()
184
174
 
@@ -188,9 +178,8 @@ class ActionsWindow(ProjectEditor):
188
178
  if file_path:
189
179
  if not file_path.endswith('.fsp'):
190
180
  file_path += '.fsp'
191
- self._current_file_wd = ''
192
181
  self.do_save(file_path)
193
- self._current_file = file_path
182
+ self.set_current_file_path(file_path)
194
183
  self._modified_project = False
195
184
  self.update_title()
196
185
  os.chdir(os.path.dirname(file_path))
@@ -201,9 +190,7 @@ class ActionsWindow(ProjectEditor):
201
190
  'project': self.project.to_dict(),
202
191
  'version': 1
203
192
  })
204
- path = f"{self._current_file_wd}/{file_path}" \
205
- if self._current_file_wd != '' else file_path
206
- with open(path, 'w', encoding="utf-8") as f:
193
+ with open(file_path, 'w', encoding="utf-8") as f:
207
194
  f.write(json_obj)
208
195
  self._modified_project = False
209
196
  except Exception as e:
@@ -238,23 +225,7 @@ class ActionsWindow(ProjectEditor):
238
225
  if 0 <= job_index < len(self.project.jobs):
239
226
  job = self.project.jobs[job_index]
240
227
  action_index = self.action_list.row(item)
241
- action_counter = -1
242
- current_action = None
243
- is_sub_action = False
244
- for action in job.sub_actions:
245
- action_counter += 1
246
- if action_counter == action_index:
247
- current_action = action
248
- break
249
- if len(action.type_name) > 0:
250
- for sub_action in action.sub_actions:
251
- action_counter += 1
252
- if action_counter == action_index:
253
- current_action = sub_action
254
- is_sub_action = True
255
- break
256
- if current_action:
257
- break
228
+ current_action, is_sub_action = self.get_current_action_at(job, action_index)
258
229
  if current_action:
259
230
  if not is_sub_action:
260
231
  self.set_enabled_sub_actions_gui(
@@ -1,13 +1,13 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0915, R0902
2
2
  import os
3
- from PySide6.QtWidgets import (QWidget, QLineEdit, QFormLayout, QHBoxLayout, QPushButton,
4
- QDialog, QSizePolicy, QFileDialog, QLabel, QCheckBox,
5
- QSpinBox, QMessageBox)
3
+ from PySide6.QtWidgets import (QFormLayout, QHBoxLayout, QPushButton,
4
+ QDialog, QLabel, QCheckBox, QSpinBox, QMessageBox)
6
5
  from PySide6.QtGui import QIcon
7
6
  from PySide6.QtCore import Qt
8
7
  from .. config.gui_constants import gui_constants
9
8
  from .. config.constants import constants
10
9
  from .. algorithms.stack import get_bunches
10
+ from .select_path_widget import create_select_file_paths_widget
11
11
 
12
12
 
13
13
  class NewProjectDialog(QDialog):
@@ -50,25 +50,8 @@ class NewProjectDialog(QDialog):
50
50
  spacer = QLabel("")
51
51
  spacer.setFixedHeight(10)
52
52
  self.layout.addRow(spacer)
53
- self.input_folder = QLineEdit()
54
- self.input_folder .setPlaceholderText('input files folder')
55
- self.input_folder.textChanged.connect(self.update_bunches_label)
56
- button = QPushButton("Browse...")
57
-
58
- def browse():
59
- path = QFileDialog.getExistingDirectory(None, "Select input files folder")
60
- if path:
61
- self.input_folder.setText(path)
62
-
63
- button.clicked.connect(browse)
64
- button.setAutoDefault(False)
65
- layout = QHBoxLayout()
66
- layout.addWidget(self.input_folder)
67
- layout.addWidget(button)
68
- layout.setContentsMargins(0, 0, 0, 0)
69
- container = QWidget()
70
- container.setLayout(layout)
71
- container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
53
+
54
+ container = create_select_file_paths_widget('', 'input files folder', 'input files folder')
72
55
 
73
56
  self.noise_detection = QCheckBox()
74
57
  self.noise_detection.setChecked(gui_constants.NEW_PROJECT_NOISE_DETECTION)
@@ -89,10 +89,28 @@ class ProjectEditor(QMainWindow):
89
89
  self.expert_options = False
90
90
  self.script_dir = os.path.dirname(__file__)
91
91
  self.dialog = None
92
- self._current_file = None
93
- self._current_file_wd = ''
92
+ self._current_file_path = ''
94
93
  self._modified_project = False
95
94
 
95
+ def current_file_path(self):
96
+ return self._current_file_path
97
+
98
+ def current_file_directory(self):
99
+ if os.path.isdir(self._current_file_path):
100
+ return self._current_file_path
101
+ return os.path.dirname(self._current_file_path)
102
+
103
+ def current_file_name(self):
104
+ if os.path.isfile(self._current_file_path):
105
+ return os.path.basename(self._current_file_path)
106
+ return ''
107
+
108
+ def set_current_file_path(self, path):
109
+ if path and not os.path.exists(path):
110
+ raise RuntimeError(f"Path: {path} does not exist.")
111
+ self._current_file_path = os.path.abspath(path)
112
+ os.chdir(self.current_file_directory())
113
+
96
114
  def set_project(self, project):
97
115
  self.project = project
98
116
 
@@ -304,7 +322,7 @@ class ProjectEditor(QMainWindow):
304
322
  return element
305
323
 
306
324
  def action_config_dialog(self, action):
307
- return ActionConfigDialog(action, self._current_file_wd, self)
325
+ return ActionConfigDialog(action, self.current_file_directory(), self)
308
326
 
309
327
  def add_job(self):
310
328
  job_action = ActionConfig("Job")
@@ -499,6 +517,27 @@ class ProjectEditor(QMainWindow):
499
517
  self.add_list_item(self.action_list, sub_action, True)
500
518
  self.update_delete_action_state()
501
519
 
520
+ def get_current_action_at(self, job, action_index):
521
+ action_counter = -1
522
+ current_action = None
523
+ is_sub_action = False
524
+ for action in job.sub_actions:
525
+ action_counter += 1
526
+ if action_counter == action_index:
527
+ current_action = action
528
+ break
529
+ if len(action.sub_actions) > 0:
530
+ for sub_action in action.sub_actions:
531
+ action_counter += 1
532
+ if action_counter == action_index:
533
+ current_action = sub_action
534
+ is_sub_action = True
535
+ break
536
+ if current_action:
537
+ break
538
+
539
+ return current_action, is_sub_action
540
+
502
541
  def update_delete_action_state(self):
503
542
  has_job_selected = len(self.job_list.selectedItems()) > 0
504
543
  has_action_selected = len(self.action_list.selectedItems()) > 0
@@ -510,23 +549,7 @@ class ProjectEditor(QMainWindow):
510
549
  action_index = self.action_list.currentRow()
511
550
  if job_index >= 0:
512
551
  job = self.project.jobs[job_index]
513
- action_counter = -1
514
- current_action = None
515
- is_sub_action = False
516
- for action in job.sub_actions:
517
- action_counter += 1
518
- if action_counter == action_index:
519
- current_action = action
520
- break
521
- if len(action.sub_actions) > 0:
522
- for sub_action in action.sub_actions:
523
- action_counter += 1
524
- if action_counter == action_index:
525
- current_action = sub_action
526
- is_sub_action = True
527
- break
528
- if current_action:
529
- break
552
+ current_action, is_sub_action = self.get_current_action_at(job, action_index)
530
553
  enable_sub_actions = current_action is not None and \
531
554
  not is_sub_action and current_action.type_name == constants.ACTION_COMBO
532
555
  self.set_enabled_sub_actions_gui(enable_sub_actions)
@@ -0,0 +1,30 @@
1
+ # pylint: disable=C0114, C0116, E0611
2
+ from PySide6.QtWidgets import QWidget, QPushButton, QHBoxLayout, QFileDialog, QSizePolicy, QLineEdit
3
+
4
+
5
+ def create_layout_widget_no_margins(layout):
6
+ layout.setContentsMargins(0, 0, 0, 0)
7
+ container = QWidget()
8
+ container.setLayout(layout)
9
+ container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
10
+ return container
11
+
12
+ def create_layout_widget_and_connect(button, edit, browse):
13
+ button.clicked.connect(browse)
14
+ button.setAutoDefault(False)
15
+ layout = QHBoxLayout()
16
+ layout.addWidget(edit)
17
+ layout.addWidget(button)
18
+ return create_layout_widget_no_margins(layout)
19
+
20
+ def create_select_file_paths_widget(value, placeholder, tag):
21
+ edit = QLineEdit(value)
22
+ edit.setPlaceholderText(placeholder)
23
+ button = QPushButton("Browse...")
24
+
25
+ def browse():
26
+ path = QFileDialog.getExistingDirectory(None, f"Select {tag}")
27
+ if path:
28
+ edit.setText(path)
29
+
30
+ return create_layout_widget_and_connect(button, edit, browse)