shinestacker 0.4.0__tar.gz → 1.0.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.
Potentially problematic release.
This version of shinestacker might be problematic. Click here for more details.
- {shinestacker-0.4.0 → shinestacker-1.0.0}/CHANGELOG.md +29 -0
- {shinestacker-0.4.0/src/shinestacker.egg-info → shinestacker-1.0.0}/PKG-INFO +14 -2
- {shinestacker-0.4.0 → shinestacker-1.0.0}/README.md +13 -1
- {shinestacker-0.4.0 → shinestacker-1.0.0}/docs/balancing.md +1 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/docs/gui.md +4 -3
- {shinestacker-0.4.0 → shinestacker-1.0.0}/docs/vignetting.md +0 -1
- shinestacker-1.0.0/img/gui-project-new.png +0 -0
- shinestacker-1.0.0/scripts/git-rev-list.sh +6 -0
- shinestacker-1.0.0/src/shinestacker/_version.py +1 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/algorithms/align.py +4 -12
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/algorithms/balance.py +11 -9
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/algorithms/depth_map.py +0 -30
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/algorithms/utils.py +10 -0
- shinestacker-1.0.0/src/shinestacker/algorithms/vignetting.py +210 -0
- shinestacker-1.0.0/src/shinestacker/app/about_dialog.py +117 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/app/gui_utils.py +1 -1
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/app/help_menu.py +1 -1
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/app/main.py +2 -2
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/app/project.py +2 -2
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/config/constants.py +4 -1
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/config/gui_constants.py +10 -9
- shinestacker-1.0.0/src/shinestacker/gui/action_config.py +363 -0
- shinestacker-0.4.0/src/shinestacker/gui/action_config.py → shinestacker-1.0.0/src/shinestacker/gui/action_config_dialog.py +20 -372
- shinestacker-1.0.0/src/shinestacker/gui/base_form_dialog.py +18 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/gui/colors.py +5 -6
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/gui/gui_logging.py +0 -1
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/gui/gui_run.py +54 -106
- shinestacker-1.0.0/src/shinestacker/gui/ico/shinestacker.icns +0 -0
- shinestacker-1.0.0/src/shinestacker/gui/ico/shinestacker.ico +0 -0
- shinestacker-1.0.0/src/shinestacker/gui/ico/shinestacker.png +0 -0
- shinestacker-1.0.0/src/shinestacker/gui/ico/shinestacker.svg +60 -0
- shinestacker-1.0.0/src/shinestacker/gui/main_window.py +555 -0
- shinestacker-1.0.0/src/shinestacker/gui/menu_manager.py +236 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/gui/new_project.py +75 -20
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/gui/project_converter.py +6 -6
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/gui/project_editor.py +248 -165
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/gui/project_model.py +2 -7
- shinestacker-1.0.0/src/shinestacker/gui/tab_widget.py +81 -0
- shinestacker-1.0.0/src/shinestacker/gui/time_progress_bar.py +95 -0
- shinestacker-1.0.0/src/shinestacker/retouch/base_filter.py +261 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/retouch/brush_preview.py +0 -10
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/retouch/brush_tool.py +25 -11
- shinestacker-1.0.0/src/shinestacker/retouch/denoise_filter.py +12 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/retouch/display_manager.py +57 -20
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/retouch/exif_data.py +10 -13
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/retouch/file_loader.py +1 -1
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/retouch/filter_manager.py +1 -4
- shinestacker-1.0.0/src/shinestacker/retouch/image_editor_ui.py +691 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/retouch/image_viewer.py +34 -11
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/retouch/io_gui_handler.py +96 -43
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/retouch/io_manager.py +23 -7
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/retouch/layer_collection.py +2 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/retouch/shortcuts_help.py +12 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/retouch/unsharp_mask_filter.py +10 -10
- shinestacker-1.0.0/src/shinestacker/retouch/vignetting_filter.py +69 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/retouch/white_balance_filter.py +46 -14
- {shinestacker-0.4.0 → shinestacker-1.0.0/src/shinestacker.egg-info}/PKG-INFO +14 -2
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker.egg-info/SOURCES.txt +8 -4
- shinestacker-0.4.0/img/gui-project-new.png +0 -0
- shinestacker-0.4.0/src/shinestacker/_version.py +0 -1
- shinestacker-0.4.0/src/shinestacker/algorithms/vignetting.py +0 -164
- shinestacker-0.4.0/src/shinestacker/app/about_dialog.py +0 -28
- shinestacker-0.4.0/src/shinestacker/app/app_config.py +0 -22
- shinestacker-0.4.0/src/shinestacker/gui/actions_window.py +0 -258
- shinestacker-0.4.0/src/shinestacker/gui/ico/shinestacker.icns +0 -0
- shinestacker-0.4.0/src/shinestacker/gui/ico/shinestacker.ico +0 -0
- shinestacker-0.4.0/src/shinestacker/gui/ico/shinestacker.png +0 -0
- shinestacker-0.4.0/src/shinestacker/gui/main_window.py +0 -646
- shinestacker-0.4.0/src/shinestacker/retouch/base_filter.py +0 -128
- shinestacker-0.4.0/src/shinestacker/retouch/denoise_filter.py +0 -51
- shinestacker-0.4.0/src/shinestacker/retouch/image_editor.py +0 -201
- shinestacker-0.4.0/src/shinestacker/retouch/image_editor_ui.py +0 -375
- shinestacker-0.4.0/src/shinestacker/retouch/image_filters.py +0 -69
- {shinestacker-0.4.0 → shinestacker-1.0.0}/.coveragerc +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/.flake8 +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/.github/workflows/ci-multiplatform.yml +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/.github/workflows/pylint.yml +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/.github/workflows/pypi-publish.yml +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/.github/workflows/release.yml +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/.gitignore +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/.pylintrc +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/.readthedocs.yaml +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/LICENSE +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/MANIFEST.in +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/THIRD_PARTY_LICENSES.txt +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/docs/alignment.md +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/docs/api.md +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/docs/conf.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/docs/focus_stacking.md +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/docs/index.md +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/docs/job.md +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/docs/main.md +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/docs/multilayer.md +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/docs/noise.md +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/docs/requirements.txt +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/img/coffee.gif +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/img/coffee_stack.jpg +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/img/extreme-vignetting.jpg +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/img/flies.gif +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/img/flies_stack.jpg +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/img/flow-diagram.png +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/img/gui-finder.png +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/img/gui-project-run.png +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/img/gui-retouch.png +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/pyproject.toml +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/requirements.txt +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/scripts/build_release.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/scripts/validate-tomli.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/setup.cfg +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/__init__.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/algorithms/__init__.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/algorithms/base_stack_algo.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/algorithms/denoise.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/algorithms/exif.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/algorithms/multilayer.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/algorithms/noise_detection.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/algorithms/pyramid.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/algorithms/sharpen.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/algorithms/stack.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/algorithms/stack_framework.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/algorithms/white_balance.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/app/__init__.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/app/open_frames.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/app/retouch.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/config/__init__.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/config/config.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/core/__init__.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/core/colors.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/core/core_utils.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/core/exceptions.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/core/framework.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/core/logging.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/gui/__init__.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/gui/gui_images.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/gui/select_path_widget.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/retouch/__init__.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/retouch/brush.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/retouch/brush_gradient.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/retouch/icon_container.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker/retouch/undo_manager.py +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker.egg-info/dependency_links.txt +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker.egg-info/entry_points.txt +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker.egg-info/requires.txt +0 -0
- {shinestacker-0.4.0 → shinestacker-1.0.0}/src/shinestacker.egg-info/top_level.txt +0 -0
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
This page reports the main releases only and the main changes therein.
|
|
4
4
|
|
|
5
|
+
## [v1.0.0] - 2025-08-25
|
|
6
|
+
**First stable release**
|
|
7
|
+
|
|
8
|
+
### Changes
|
|
9
|
+
|
|
10
|
+
* implemented vignetting correction filter
|
|
11
|
+
* improved vignetting performance using subsampling
|
|
12
|
+
* implemented fast subsample option in balance algorithms
|
|
13
|
+
* implemented hex color line editin white balance filter
|
|
14
|
+
* new application logo
|
|
15
|
+
* interface improvements: implemented master/layer toggle
|
|
16
|
+
* more informative GUI messages and colors
|
|
17
|
+
* code refactoring and various cleanup
|
|
18
|
+
* bug fixes
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## [v0.5.0] - 2025-08-20
|
|
23
|
+
**GUI and robustness improvements**
|
|
24
|
+
|
|
25
|
+
### Changes
|
|
26
|
+
|
|
27
|
+
* layer selection highlightted with a blue border
|
|
28
|
+
* improved font rendering in brush preview
|
|
29
|
+
* fixed thumbnail spacing
|
|
30
|
+
* fixed and improved save strategy for retouched images
|
|
31
|
+
* added checks for updated version in about dialog
|
|
32
|
+
* disable "Save" and "Save As..." menus if do not apply to current status
|
|
33
|
+
|
|
5
34
|
---
|
|
6
35
|
|
|
7
36
|
## [v0.4.0] - 2025-08-19
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shinestacker
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: ShineStacker
|
|
5
5
|
Author-email: Luca Lista <luka.lista@gmail.com>
|
|
6
6
|
License-Expression: LGPL-3.0
|
|
@@ -41,6 +41,7 @@ Dynamic: license-file
|
|
|
41
41
|
[](https://codecov.io/github/lucalista/shinestacker)
|
|
42
42
|
[](https://shinestacker.readthedocs.io/en/latest/?badge=latest)
|
|
43
43
|
[](https://www.gnu.org/licenses/lgpl-3.0)
|
|
44
|
+
[](https://pepy.tech/projects/shinestacker)
|
|
44
45
|
|
|
45
46
|
<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">
|
|
46
47
|
|
|
@@ -75,6 +76,14 @@ The GUI has two main working areas:
|
|
|
75
76
|
|
|
76
77
|
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.
|
|
77
78
|
|
|
79
|
+
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/src/shinestacker/gui/ico/shinestacker.png' width="256" referrerpolicy="no-referrer" alt="Shine Stacker Logo">
|
|
80
|
+
|
|
81
|
+
## Logo attribution
|
|
82
|
+
|
|
83
|
+
The Shine Stacker logo was designed by [Alessandro Lista](https://linktr.ee/alelista).
|
|
84
|
+
Copyright © Alessandro Lista. All rights reserved.
|
|
85
|
+
The logo is not covered by the LGPL-3.0 license of this project.
|
|
86
|
+
|
|
78
87
|
# Resources
|
|
79
88
|
|
|
80
89
|
* [Pyramid Methods in Image Processing](https://www.researchgate.net/publication/246727904_Pyramid_Methods_in_Image_Processing), E. H. Adelson, C. H. Anderson, J. R. Bergen, P. J. Burt, J. M. Ogden, RCA Engineer, 29-6, Nov/Dec 1984
|
|
@@ -83,8 +92,11 @@ Pyramid methods in image processing
|
|
|
83
92
|
|
|
84
93
|
# License
|
|
85
94
|
|
|
86
|
-
<img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
|
|
95
|
+
- **Code**: <img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
|
|
87
96
|
The software is provided as is under the [GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0.en.html). See [LICENSE](https://github.com/lucalista/shinestacker/blob/main/LICENSE) for details.
|
|
97
|
+
- **Logo**: The Shine Stacker logo was designed by [Alessandro Lista](https://linktr.ee/alelista).
|
|
98
|
+
Copyright © Alessandro Lista. All rights reserved.
|
|
99
|
+
The logo is not covered by the LGPL-3.0 license of this project.
|
|
88
100
|
|
|
89
101
|
# Attribution request
|
|
90
102
|
📸 If you publish images created with Shine Stacker, please consider adding a note such as:
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
[](https://codecov.io/github/lucalista/shinestacker)
|
|
11
11
|
[](https://shinestacker.readthedocs.io/en/latest/?badge=latest)
|
|
12
12
|
[](https://www.gnu.org/licenses/lgpl-3.0)
|
|
13
|
+
[](https://pepy.tech/projects/shinestacker)
|
|
13
14
|
|
|
14
15
|
<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">
|
|
15
16
|
|
|
@@ -44,6 +45,14 @@ The GUI has two main working areas:
|
|
|
44
45
|
|
|
45
46
|
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.
|
|
46
47
|
|
|
48
|
+
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/src/shinestacker/gui/ico/shinestacker.png' width="256" referrerpolicy="no-referrer" alt="Shine Stacker Logo">
|
|
49
|
+
|
|
50
|
+
## Logo attribution
|
|
51
|
+
|
|
52
|
+
The Shine Stacker logo was designed by [Alessandro Lista](https://linktr.ee/alelista).
|
|
53
|
+
Copyright © Alessandro Lista. All rights reserved.
|
|
54
|
+
The logo is not covered by the LGPL-3.0 license of this project.
|
|
55
|
+
|
|
47
56
|
# Resources
|
|
48
57
|
|
|
49
58
|
* [Pyramid Methods in Image Processing](https://www.researchgate.net/publication/246727904_Pyramid_Methods_in_Image_Processing), E. H. Adelson, C. H. Anderson, J. R. Bergen, P. J. Burt, J. M. Ogden, RCA Engineer, 29-6, Nov/Dec 1984
|
|
@@ -52,8 +61,11 @@ Pyramid methods in image processing
|
|
|
52
61
|
|
|
53
62
|
# License
|
|
54
63
|
|
|
55
|
-
<img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
|
|
64
|
+
- **Code**: <img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
|
|
56
65
|
The software is provided as is under the [GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0.en.html). See [LICENSE](https://github.com/lucalista/shinestacker/blob/main/LICENSE) for details.
|
|
66
|
+
- **Logo**: The Shine Stacker logo was designed by [Alessandro Lista](https://linktr.ee/alelista).
|
|
67
|
+
Copyright © Alessandro Lista. All rights reserved.
|
|
68
|
+
The logo is not covered by the LGPL-3.0 license of this project.
|
|
57
69
|
|
|
58
70
|
# Attribution request
|
|
59
71
|
📸 If you publish images created with Shine Stacker, please consider adding a note such as:
|
|
@@ -16,6 +16,7 @@ intensity_interval = {
|
|
|
16
16
|
```
|
|
17
17
|
Note that for 8-bit images the maximum intensity is 255, while for 16-bit images the maximum intensity is 65536.
|
|
18
18
|
* ```subsample``` (optional, default: 8): extracts intensity histogram using every n-th pixel in each dimension in order to reduce processing time. By default, it takes one every 8 pixels in horizontal and vertical directions, i.e.: one every 100 pixels in total. This option is not ised if ```corr_map``` is equal to ```BALANCE_MATCH_HIST```.
|
|
19
|
+
* ```fast_subsampling``` (optional, default: ```False```): perform fast image subsampling without interpolation. Used if ```subsample``` is set to ```True```.
|
|
19
20
|
* ```corr_map``` (optional, default: ```BALANCE_LINEAR```, possible values: ```BALANCE_LINEAR```, ```BALANCE_GAMMA``` and ```MATCH_HIST```): specifies the type of intensity correction.
|
|
20
21
|
* ```BALANCE_LINEAR```: a linear correction is applied in order to balance the average intensity of the corrected images to the reference image in the specified channels.
|
|
21
22
|
* ```BALANCE_GAMMA```: a gamma correction, i.e.: a power law, is applied in order to balance the average intensity of the corrected images to reference image in the specified channels. The gamma correction avoids saturation of low or high intensity pixels which may occur for a linear coorection, but may introduce more distortion than a linear mapping.
|
|
@@ -107,13 +107,14 @@ Adjust in the top toolbar:
|
|
|
107
107
|
|
|
108
108
|
| Action | Shortcut |
|
|
109
109
|
|---------------------|---------------------------|
|
|
110
|
-
| Zoom in/out | `Ctrl` + `+`/`- or mouse wheel
|
|
110
|
+
| Zoom in/out | `Ctrl` + `+`/`- or mouse wheel or pinch on touchpad |
|
|
111
111
|
| Reset view | `Ctrl` + `0` |
|
|
112
|
-
| Pan | `Space` + mouse drag
|
|
112
|
+
| Pan | `Space` + mouse drag or two fingers on touchpad |
|
|
113
113
|
| Prev./next layer | `Up`/`Down` arrows |
|
|
114
114
|
| View master layer | `M` |
|
|
115
115
|
| View source layer | `L` |
|
|
116
|
-
| Toggle master ↔
|
|
116
|
+
| Toggle master ↔ layer | `T` |
|
|
117
|
+
| Temp. toggle master ↔ source | `X` |
|
|
117
118
|
|
|
118
119
|
See help menu for complete list of shortcuts.
|
|
119
120
|
|
|
@@ -14,7 +14,6 @@ Arguments for the constructor of ```Vignetting``` are:
|
|
|
14
14
|
* ```r_steps``` (optional, default: 100): number of radial steps to determine mean pixel luminosity.
|
|
15
15
|
* ```black_threshold``` (optional, default: 1): apply correction only on pixels with luminosity greater than.
|
|
16
16
|
* ```max_correction``` (optional, default: 1): if less than one, the correction is rescaled in order to be at most the specified valye.
|
|
17
|
-
* ```apply_correction``` (optional, default: ```True```): if ```False```, the correction is computed but not applied to the image. It may be useful in order to determine a value of the parameter ```mask_size``` for the action ```BalanceFrames``` by looking at the output curve plot.
|
|
18
17
|
* ```plot_correction``` (optional, default: ```False```): if ```True```, plot vignetting correction curve for each frame.
|
|
19
18
|
* ```plot_summary``` (optional, default: ```False```): if ```True```, plot a summary histogram with the vignetting correction levels.
|
|
20
19
|
* ```enabled``` (optional, default: ```True```): allows to switch on and off this module.
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.0.0'
|
|
@@ -6,8 +6,7 @@ import cv2
|
|
|
6
6
|
from .. config.constants import constants
|
|
7
7
|
from .. core.exceptions import AlignmentError, InvalidOptionError
|
|
8
8
|
from .. core.colors import color_str
|
|
9
|
-
from .utils import img_8bit, img_bw_8bit, save_plot
|
|
10
|
-
from .utils import get_img_metadata, validate_image
|
|
9
|
+
from .utils import img_8bit, img_bw_8bit, save_plot, get_img_metadata, validate_image, img_subsample
|
|
11
10
|
from .stack_framework import SubAction
|
|
12
11
|
|
|
13
12
|
_DEFAULT_FEATURE_CONFIG = {
|
|
@@ -166,19 +165,12 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
|
|
|
166
165
|
if callbacks and 'message' in callbacks:
|
|
167
166
|
callbacks['message']()
|
|
168
167
|
subsample = alignment_config['subsample']
|
|
168
|
+
fast_subsampling = alignment_config['fast_subsampling']
|
|
169
169
|
min_good_matches = alignment_config['min_good_matches']
|
|
170
170
|
while True:
|
|
171
171
|
if subsample > 1:
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
img_0[::subsample, ::subsample], img_1[::subsample, ::subsample]
|
|
175
|
-
else:
|
|
176
|
-
img_0_sub = cv2.resize(img_0, (0, 0),
|
|
177
|
-
fx=1 / subsample, fy=1 / subsample,
|
|
178
|
-
interpolation=cv2.INTER_AREA)
|
|
179
|
-
img_1_sub = cv2.resize(img_1, (0, 0),
|
|
180
|
-
fx=1 / subsample, fy=1 / subsample,
|
|
181
|
-
interpolation=cv2.INTER_AREA)
|
|
172
|
+
img_0_sub = img_subsample(img_0, subsample, fast_subsampling)
|
|
173
|
+
img_1_sub = img_subsample(img_1, subsample, fast_subsampling)
|
|
182
174
|
else:
|
|
183
175
|
img_0_sub, img_1_sub = img_0, img_1
|
|
184
176
|
kp_0, kp_1, good_matches = detect_and_compute(img_0_sub, img_1_sub,
|
|
@@ -7,7 +7,7 @@ from scipy.interpolate import interp1d
|
|
|
7
7
|
from .. config.constants import constants
|
|
8
8
|
from .. core.exceptions import InvalidOptionError
|
|
9
9
|
from .. core.colors import color_str
|
|
10
|
-
from .utils import read_img, save_plot
|
|
10
|
+
from .utils import read_img, save_plot, img_subsample
|
|
11
11
|
from .stack_framework import SubAction
|
|
12
12
|
|
|
13
13
|
|
|
@@ -122,13 +122,15 @@ class LinearMap(CorrectionMap):
|
|
|
122
122
|
|
|
123
123
|
class Correction:
|
|
124
124
|
def __init__(self, channels, mask_size=0, intensity_interval=None,
|
|
125
|
-
subsample=-1,
|
|
125
|
+
subsample=-1, fast_subsampling=constants.DEFAULT_BALANCE_FAST_SUBSAMPLING,
|
|
126
|
+
corr_map=constants.DEFAULT_CORR_MAP,
|
|
126
127
|
plot_histograms=False, plot_summary=False):
|
|
127
128
|
self.mask_size = mask_size
|
|
128
129
|
self.intensity_interval = intensity_interval
|
|
129
130
|
self.plot_histograms = plot_histograms
|
|
130
131
|
self.plot_summary = plot_summary
|
|
131
132
|
self.subsample = constants.DEFAULT_BALANCE_SUBSAMPLE if subsample == -1 else subsample
|
|
133
|
+
self.fast_subsampling = fast_subsampling
|
|
132
134
|
self.corr_map = corr_map
|
|
133
135
|
self.channels = channels
|
|
134
136
|
self.dtype = None
|
|
@@ -154,17 +156,18 @@ class Correction:
|
|
|
154
156
|
self.corrections = np.ones((size, self.channels))
|
|
155
157
|
|
|
156
158
|
def calc_hist_1ch(self, image):
|
|
157
|
-
|
|
159
|
+
img_sub = image if self.subsample == 1 \
|
|
160
|
+
else img_subsample(image, self.subsample, self.fast_subsampling)
|
|
158
161
|
if self.mask_size == 0:
|
|
159
|
-
image_sel =
|
|
162
|
+
image_sel = img_sub
|
|
160
163
|
else:
|
|
161
|
-
height, width =
|
|
164
|
+
height, width = img_sub.shape[:2]
|
|
162
165
|
xv, yv = np.meshgrid(
|
|
163
166
|
np.linspace(0, width - 1, width),
|
|
164
167
|
np.linspace(0, height - 1, height)
|
|
165
168
|
)
|
|
166
169
|
mask_radius = min(width, height) * self.mask_size / 2
|
|
167
|
-
image_sel =
|
|
170
|
+
image_sel = img_sub[
|
|
168
171
|
(xv - width / 2) ** 2 + (yv - height / 2) ** 2 <= mask_radius ** 2
|
|
169
172
|
]
|
|
170
173
|
hist, _bins = np.histogram(
|
|
@@ -305,9 +308,6 @@ class Ch2Correction(Correction):
|
|
|
305
308
|
def preprocess(self, image):
|
|
306
309
|
assert False, 'abstract method'
|
|
307
310
|
|
|
308
|
-
def get_labels(self):
|
|
309
|
-
assert False, 'abstract method'
|
|
310
|
-
|
|
311
311
|
def get_hist(self, image, idx):
|
|
312
312
|
hist = [self.calc_hist_1ch(chan) for chan in cv2.split(image)]
|
|
313
313
|
if self.plot_histograms:
|
|
@@ -370,6 +370,8 @@ class BalanceFrames(SubAction):
|
|
|
370
370
|
self.shape = None
|
|
371
371
|
corr_map = kwargs.get('corr_map', constants.DEFAULT_CORR_MAP)
|
|
372
372
|
subsample = kwargs.get('subsample', constants.DEFAULT_BALANCE_SUBSAMPLE)
|
|
373
|
+
self.fast_subsampling = kwargs.get(
|
|
374
|
+
'fast_subsampling', constants.DEFAULT_BALANCE_FAST_SUBSAMPLING)
|
|
373
375
|
channel = kwargs.pop('channel', constants.DEFAULT_CHANNEL)
|
|
374
376
|
kwargs['subsample'] = (
|
|
375
377
|
1 if corr_map == constants.BALANCE_MATCH_HIST
|
|
@@ -61,36 +61,6 @@ class DepthMapStack(BaseStackAlgo):
|
|
|
61
61
|
raise InvalidOptionError("map_type", self.map_type, details=f" valid values are "
|
|
62
62
|
f"{constants.DM_MAP_AVERAGE} and {constants.DM_MAP_MAX}.")
|
|
63
63
|
|
|
64
|
-
def pyramid_blend(self, images, weights):
|
|
65
|
-
blended = None
|
|
66
|
-
for i in range(images.shape[0]):
|
|
67
|
-
img = images[i].astype(self.float_type)
|
|
68
|
-
weight = weights[i]
|
|
69
|
-
gp_img = [img]
|
|
70
|
-
gp_weight = [weight]
|
|
71
|
-
for _ in range(self.levels - 1):
|
|
72
|
-
gp_img.append(cv2.pyrDown(gp_img[-1]))
|
|
73
|
-
gp_weight.append(cv2.pyrDown(gp_weight[-1]))
|
|
74
|
-
lp_img = [gp_img[-1]]
|
|
75
|
-
for j in range(self.levels - 1, 0, -1):
|
|
76
|
-
size = (gp_img[j - 1].shape[1], gp_img[j - 1].shape[0])
|
|
77
|
-
expanded = cv2.pyrUp(gp_img[j], dstsize=size)
|
|
78
|
-
lp_img.append(gp_img[j - 1] - expanded)
|
|
79
|
-
current_blend = []
|
|
80
|
-
for j in range(self.levels):
|
|
81
|
-
w = gp_weight[self.levels - 1 - j][..., np.newaxis]
|
|
82
|
-
current_blend.append(lp_img[j] * w)
|
|
83
|
-
if blended is None:
|
|
84
|
-
blended = current_blend
|
|
85
|
-
else:
|
|
86
|
-
for j in range(self.levels):
|
|
87
|
-
blended[j] += current_blend[j]
|
|
88
|
-
result = blended[0]
|
|
89
|
-
for j in range(1, self.levels):
|
|
90
|
-
size = (blended[j].shape[1], blended[j].shape[0])
|
|
91
|
-
result = cv2.pyrUp(result, dstsize=size) + blended[j]
|
|
92
|
-
return result
|
|
93
|
-
|
|
94
64
|
def focus_stack(self, filenames):
|
|
95
65
|
gray_images = []
|
|
96
66
|
metadata = None
|
|
@@ -74,3 +74,13 @@ def save_plot(filename):
|
|
|
74
74
|
if config.JUPYTER_NOTEBOOK:
|
|
75
75
|
plt.show()
|
|
76
76
|
plt.close('all')
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def img_subsample(img, subsample, fast=True):
|
|
80
|
+
if fast:
|
|
81
|
+
img_sub = img[::subsample, ::subsample]
|
|
82
|
+
else:
|
|
83
|
+
img_sub = cv2.resize(img, (0, 0),
|
|
84
|
+
fx=1 / subsample, fy=1 / subsample,
|
|
85
|
+
interpolation=cv2.INTER_AREA)
|
|
86
|
+
return img_sub
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, R0902, E1101, W0718, W0640, R0913, R0917, R0914
|
|
2
|
+
import traceback
|
|
3
|
+
import logging
|
|
4
|
+
import numpy as np
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
from scipy.optimize import curve_fit, fsolve
|
|
7
|
+
import cv2
|
|
8
|
+
from .. core.colors import color_str
|
|
9
|
+
from .. config.constants import constants
|
|
10
|
+
from .utils import img_8bit, save_plot, img_subsample
|
|
11
|
+
from .stack_framework import SubAction
|
|
12
|
+
|
|
13
|
+
CLIP_EXP = 10
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def sigmoid_model(r, i0, k, r0):
|
|
17
|
+
return i0 / (1.0 +
|
|
18
|
+
np.exp(np.minimum(CLIP_EXP,
|
|
19
|
+
np.exp(np.clip(k * (r - r0),
|
|
20
|
+
-CLIP_EXP, CLIP_EXP)))))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def radial_mean_intensity(image, r_steps):
|
|
24
|
+
if len(image.shape) > 2:
|
|
25
|
+
raise ValueError("The image must be grayscale")
|
|
26
|
+
h, w = image.shape
|
|
27
|
+
w_2, h_2 = w / 2, h / 2
|
|
28
|
+
r_max = np.sqrt((w / 2)**2 + (h / 2)**2)
|
|
29
|
+
radii = np.linspace(0, r_max, r_steps + 1)
|
|
30
|
+
mean_intensities = np.zeros(r_steps)
|
|
31
|
+
y, x = np.ogrid[:h, :w]
|
|
32
|
+
dist_from_center = np.sqrt((x - w_2)**2 + (y - h_2)**2)
|
|
33
|
+
for i in range(r_steps):
|
|
34
|
+
mask = (dist_from_center >= radii[i]) & (dist_from_center < radii[i + 1])
|
|
35
|
+
if np.any(mask):
|
|
36
|
+
mean_intensities[i] = np.mean(image[mask])
|
|
37
|
+
else:
|
|
38
|
+
mean_intensities[i] = np.nan
|
|
39
|
+
return (radii[1:] + radii[:-1]) / 2, mean_intensities
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def fit_sigmoid(radii, intensities):
|
|
43
|
+
valid_mask = ~np.isnan(intensities)
|
|
44
|
+
i_valid, r_valid = intensities[valid_mask], radii[valid_mask]
|
|
45
|
+
r_max = radii.max()
|
|
46
|
+
res = curve_fit(sigmoid_model, r_valid, i_valid,
|
|
47
|
+
p0=[2 * np.max(i_valid), 10 / r_max, 0.8 * r_max],
|
|
48
|
+
bounds=([0, 0, 0], ['inf', 'inf', 'inf']))[0]
|
|
49
|
+
return res
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def img_subsampled(image, subsample=constants.DEFAULT_VIGN_SUBSAMPLE,
|
|
53
|
+
fast_subsampling=constants.DEFAULT_VIGN_FAST_SUBSAMPLING):
|
|
54
|
+
image_bw = cv2.cvtColor(img_8bit(image), cv2.COLOR_BGR2GRAY)
|
|
55
|
+
return image_bw if subsample == 1 else img_subsample(image_bw, subsample, fast_subsampling)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def compute_fit_parameters(
|
|
59
|
+
image, r_steps, radii=None, intensities=None,
|
|
60
|
+
subsample=constants.DEFAULT_VIGN_SUBSAMPLE,
|
|
61
|
+
fast_subsampling=constants.DEFAULT_VIGN_FAST_SUBSAMPLING):
|
|
62
|
+
image_sub = img_subsampled(image, subsample, fast_subsampling)
|
|
63
|
+
if radii is None and intensities is None:
|
|
64
|
+
radii, intensities = radial_mean_intensity(image_sub, r_steps)
|
|
65
|
+
params = fit_sigmoid(radii, intensities)
|
|
66
|
+
params[1] /= subsample # k
|
|
67
|
+
params[2] *= subsample # r0
|
|
68
|
+
return params
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def correct_vignetting(
|
|
72
|
+
image, max_correction=constants.DEFAULT_MAX_CORRECTION,
|
|
73
|
+
black_threshold=constants.DEFAULT_BLACK_THRESHOLD,
|
|
74
|
+
r_steps=constants.DEFAULT_R_STEPS, params=None, v0=None,
|
|
75
|
+
subsample=constants.DEFAULT_VIGN_SUBSAMPLE,
|
|
76
|
+
fast_subsampling=constants.DEFAULT_VIGN_FAST_SUBSAMPLING):
|
|
77
|
+
if params is None:
|
|
78
|
+
if r_steps is None:
|
|
79
|
+
raise RuntimeError("Either r_steps or pars must not be None")
|
|
80
|
+
params = compute_fit_parameters(
|
|
81
|
+
image, r_steps, subsample=subsample, fast_subsampling=fast_subsampling)
|
|
82
|
+
if v0 is None:
|
|
83
|
+
v0 = sigmoid_model(0, *params)
|
|
84
|
+
h, w = image.shape[:2]
|
|
85
|
+
y, x = np.ogrid[:h, :w]
|
|
86
|
+
r = np.sqrt((x - w / 2)**2 + (y - h / 2)**2)
|
|
87
|
+
vignette = np.clip(sigmoid_model(r, *params) / v0, 1e-6, 1)
|
|
88
|
+
if max_correction < 1:
|
|
89
|
+
vignette = (1.0 - max_correction) + vignette * max_correction
|
|
90
|
+
threshold = black_threshold if image.dtype == np.uint8 else black_threshold * 256
|
|
91
|
+
if len(image.shape) == 3:
|
|
92
|
+
vignette = vignette[:, :, np.newaxis]
|
|
93
|
+
vignette[np.min(image, axis=2) < threshold, :] = 1
|
|
94
|
+
else:
|
|
95
|
+
vignette[image < black_threshold] = 1
|
|
96
|
+
return np.clip(image / vignette, 0, 255
|
|
97
|
+
if image.dtype == np.uint8 else 65535).astype(image.dtype)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Vignetting(SubAction):
|
|
101
|
+
def __init__(self, enabled=True, percentiles=(0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95), **kwargs):
|
|
102
|
+
super().__init__(enabled)
|
|
103
|
+
self.r_steps = kwargs.get('r_steps', constants.DEFAULT_R_STEPS)
|
|
104
|
+
self.black_threshold = kwargs.get('black_threshold', constants.DEFAULT_BLACK_THRESHOLD)
|
|
105
|
+
self.plot_correction = kwargs.get('plot_correction', False)
|
|
106
|
+
self.plot_summary = kwargs.get('plot_summary', False)
|
|
107
|
+
self.max_correction = kwargs.get('max_correction', constants.DEFAULT_MAX_CORRECTION)
|
|
108
|
+
self.percentiles = np.sort(percentiles)
|
|
109
|
+
self.subsample = kwargs.get('subsample', constants.DEFAULT_VIGN_SUBSAMPLE)
|
|
110
|
+
self.fast_subsampling = kwargs.get(
|
|
111
|
+
'fast_subsampling', constants.DEFAULT_VIGN_FAST_SUBSAMPLING)
|
|
112
|
+
self.w_2 = None
|
|
113
|
+
self.h_2 = None
|
|
114
|
+
self.v0 = None
|
|
115
|
+
self.r_max = None
|
|
116
|
+
self.process = None
|
|
117
|
+
self.corrections = None
|
|
118
|
+
|
|
119
|
+
def run_frame(self, idx, _ref_idx, img_0):
|
|
120
|
+
self.process.sub_message_r(color_str(": compute vignetting", "cyan"))
|
|
121
|
+
h, w = img_0.shape[:2]
|
|
122
|
+
self.w_2, self.h_2 = w / 2, h / 2
|
|
123
|
+
self.r_max = np.sqrt((w / 2)**2 + (h / 2)**2)
|
|
124
|
+
image_sub = img_subsampled(img_0, self.subsample, self.fast_subsampling)
|
|
125
|
+
radii, intensities = radial_mean_intensity(image_sub, self.r_steps)
|
|
126
|
+
try:
|
|
127
|
+
params = compute_fit_parameters(
|
|
128
|
+
img_0, self.r_steps, radii, intensities, self.subsample, self.fast_subsampling)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
traceback.print_tb(e.__traceback__)
|
|
131
|
+
self.process.sub_message(
|
|
132
|
+
color_str(": could not find vignetting model", "red"), level=logging.WARNING)
|
|
133
|
+
params = None
|
|
134
|
+
if params is None:
|
|
135
|
+
return img_0
|
|
136
|
+
self.v0 = sigmoid_model(0, *params)
|
|
137
|
+
i0_fit, k_fit, r0_fit = params
|
|
138
|
+
self.process.sub_message(color_str(": vignetting model parameters: ", "cyan") +
|
|
139
|
+
color_str(f"i0={i0_fit / 2:.4f}, "
|
|
140
|
+
f"k={k_fit * self.r_max:.4f}, "
|
|
141
|
+
f"r0={r0_fit / self.r_max:.4f}",
|
|
142
|
+
"light_blue"),
|
|
143
|
+
level=logging.DEBUG)
|
|
144
|
+
if self.plot_correction:
|
|
145
|
+
plt.figure(figsize=(10, 5))
|
|
146
|
+
plt.plot(radii, intensities, label="image mean intensity")
|
|
147
|
+
plt.plot(radii, sigmoid_model(radii * self.subsample, *params), label="sigmoid fit")
|
|
148
|
+
plt.xlabel('radius (pixels)')
|
|
149
|
+
plt.ylabel('mean intensity')
|
|
150
|
+
plt.legend()
|
|
151
|
+
plt.xlim(radii[0], radii[-1])
|
|
152
|
+
plt.ylim(0)
|
|
153
|
+
idx_str = f"{idx:04d}"
|
|
154
|
+
plot_path = f"{self.process.working_path}/" \
|
|
155
|
+
f"{self.process.plot_path}/{self.process.name}-" \
|
|
156
|
+
f"radial-intensity-{idx_str}.pdf"
|
|
157
|
+
save_plot(plot_path)
|
|
158
|
+
plt.close('all')
|
|
159
|
+
self.process.callback(
|
|
160
|
+
'save_plot', self.process.id,
|
|
161
|
+
f"{self.process.name}: intensity\nframe {idx_str}", plot_path)
|
|
162
|
+
for i, p in enumerate(self.percentiles):
|
|
163
|
+
self.corrections[i][idx] = fsolve(lambda x: sigmoid_model(x, *params) /
|
|
164
|
+
self.v0 - p, r0_fit)[0]
|
|
165
|
+
self.process.sub_message_r(color_str(": correct vignetting", "cyan"))
|
|
166
|
+
return correct_vignetting(
|
|
167
|
+
img_0, self.max_correction, self.black_threshold, None, params, self.v0,
|
|
168
|
+
self.subsample, self.fast_subsampling)
|
|
169
|
+
|
|
170
|
+
def begin(self, process):
|
|
171
|
+
self.process = process
|
|
172
|
+
self.corrections = [np.full(self.process.counts, None, dtype=float)
|
|
173
|
+
for p in self.percentiles]
|
|
174
|
+
|
|
175
|
+
def end(self):
|
|
176
|
+
if self.plot_summary:
|
|
177
|
+
plt.figure(figsize=(10, 5))
|
|
178
|
+
xs = np.arange(1, len(self.corrections[0]) + 1, dtype=int)
|
|
179
|
+
for i, p in enumerate(self.percentiles):
|
|
180
|
+
linestyle = 'solid'
|
|
181
|
+
if p == 0.5:
|
|
182
|
+
linestyle = '-.'
|
|
183
|
+
elif i in (0, len(self.percentiles) - 1):
|
|
184
|
+
linestyle = 'dotted'
|
|
185
|
+
plt.plot(xs, self.corrections[i], label=f"{p:.0%} correction",
|
|
186
|
+
linestyle=linestyle, color="blue")
|
|
187
|
+
plt.fill_between(xs, self.corrections[-1], self.corrections[0], color="#0000ff20")
|
|
188
|
+
iis = np.where(self.percentiles == 0.5)
|
|
189
|
+
if len(iis) > 0:
|
|
190
|
+
i = iis[0][0]
|
|
191
|
+
if 1 <= i < len(self.percentiles) - 1:
|
|
192
|
+
plt.fill_between(xs, self.corrections[i - 1], self.corrections[i + 1],
|
|
193
|
+
color="#0000ff20")
|
|
194
|
+
plt.plot(xs[[0, -1]], [self.r_max] * 2,
|
|
195
|
+
linestyle="--", label="max. radius", color="darkred")
|
|
196
|
+
plt.plot(xs[[0, -1]], [self.w_2] * 2,
|
|
197
|
+
linestyle="--", label="half width", color="limegreen")
|
|
198
|
+
plt.plot(xs[[0, -1]], [self.h_2] * 2,
|
|
199
|
+
linestyle="--", label="half height", color="darkgreen")
|
|
200
|
+
plt.xlabel('frame')
|
|
201
|
+
plt.ylabel('distance from center (pixels)')
|
|
202
|
+
plt.legend(ncols=2)
|
|
203
|
+
plt.xlim(xs[0], xs[-1])
|
|
204
|
+
plt.ylim(0, self.r_max * 1.05)
|
|
205
|
+
plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
|
|
206
|
+
f"{self.process.name}-r0.pdf"
|
|
207
|
+
save_plot(plot_path)
|
|
208
|
+
plt.close('all')
|
|
209
|
+
self.process.callback('save_plot', self.process.id,
|
|
210
|
+
f"{self.process.name}: vignetting", plot_path)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, W0718, R0903
|
|
2
|
+
import json
|
|
3
|
+
from urllib.request import urlopen, Request
|
|
4
|
+
from urllib.error import URLError
|
|
5
|
+
from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel
|
|
6
|
+
from PySide6.QtCore import Qt
|
|
7
|
+
from .. import __version__
|
|
8
|
+
from .. retouch.icon_container import icon_container
|
|
9
|
+
from .. config.constants import constants
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AboutDialog(QDialog):
|
|
13
|
+
def __init__(self, parent=None, about_text=""):
|
|
14
|
+
super().__init__(parent)
|
|
15
|
+
self.setWindowTitle("About")
|
|
16
|
+
self.resize(400, 300)
|
|
17
|
+
layout = QVBoxLayout(self)
|
|
18
|
+
layout.setAlignment(Qt.AlignTop)
|
|
19
|
+
icon_widget = icon_container()
|
|
20
|
+
icon_layout = QHBoxLayout()
|
|
21
|
+
icon_layout.addStretch()
|
|
22
|
+
icon_layout.addWidget(icon_widget)
|
|
23
|
+
icon_layout.addStretch()
|
|
24
|
+
layout.addLayout(icon_layout)
|
|
25
|
+
about_label = QLabel(about_text)
|
|
26
|
+
about_label.setWordWrap(True)
|
|
27
|
+
about_label.setAlignment(Qt.AlignLeft)
|
|
28
|
+
layout.addWidget(about_label)
|
|
29
|
+
button_layout = QHBoxLayout()
|
|
30
|
+
button_layout.addStretch()
|
|
31
|
+
button = QPushButton("OK")
|
|
32
|
+
button.setFixedWidth(100)
|
|
33
|
+
button.clicked.connect(self.accept)
|
|
34
|
+
button_layout.addWidget(button)
|
|
35
|
+
button_layout.addStretch()
|
|
36
|
+
layout.addLayout(button_layout)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def compare_versions(current, latest):
|
|
40
|
+
def parse_version(v):
|
|
41
|
+
v = v.lstrip('v')
|
|
42
|
+
parts = v.split('.')
|
|
43
|
+
result = []
|
|
44
|
+
for part in parts:
|
|
45
|
+
try:
|
|
46
|
+
result.append(int(part))
|
|
47
|
+
except ValueError:
|
|
48
|
+
result.append(part)
|
|
49
|
+
return result
|
|
50
|
+
current_parts = parse_version(current)
|
|
51
|
+
latest_parts = parse_version(latest)
|
|
52
|
+
for i in range(max(len(current_parts), len(latest_parts))):
|
|
53
|
+
c = current_parts[i] if i < len(current_parts) else 0
|
|
54
|
+
l = latest_parts[i] if i < len(latest_parts) else 0 # noqa: E741
|
|
55
|
+
if isinstance(c, int) and isinstance(l, int):
|
|
56
|
+
if c < l:
|
|
57
|
+
return -1
|
|
58
|
+
if c > l:
|
|
59
|
+
return 1
|
|
60
|
+
else:
|
|
61
|
+
if str(c) < str(l):
|
|
62
|
+
return -1
|
|
63
|
+
if str(c) > str(l):
|
|
64
|
+
return 1
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_latest_version():
|
|
69
|
+
try:
|
|
70
|
+
url = "https://api.github.com/repos/lucalista/shinestacker/releases/latest"
|
|
71
|
+
headers = {'User-Agent': 'ShineStacker'}
|
|
72
|
+
req = Request(url, headers=headers)
|
|
73
|
+
with urlopen(req, timeout=5) as response:
|
|
74
|
+
data = json.loads(response.read().decode())
|
|
75
|
+
return data['tag_name']
|
|
76
|
+
except (URLError, ValueError, KeyError, TimeoutError):
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def show_about_dialog(parent):
|
|
81
|
+
version_clean = __version__.split("+", maxsplit=1)[0]
|
|
82
|
+
latest_version = None
|
|
83
|
+
try:
|
|
84
|
+
latest_version = get_latest_version()
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
update_text = ""
|
|
88
|
+
if latest_version:
|
|
89
|
+
latest_clean = latest_version.lstrip('v')
|
|
90
|
+
if compare_versions(version_clean, latest_clean) < 0:
|
|
91
|
+
update_text = f"""
|
|
92
|
+
<p style="color: red; font-weight: bold;">
|
|
93
|
+
Update available! Latest version: {latest_version}
|
|
94
|
+
<br><a href="https://github.com/lucalista/shinestacker/releases/latest">Download here</a>
|
|
95
|
+
</p>
|
|
96
|
+
""" # noqa E501
|
|
97
|
+
else:
|
|
98
|
+
update_text = """
|
|
99
|
+
<p style="color: green; font-weight: bold;">
|
|
100
|
+
You are using the lastet version.
|
|
101
|
+
</p>
|
|
102
|
+
"""
|
|
103
|
+
about_text = f"""
|
|
104
|
+
<h3>{constants.APP_TITLE}</h3>
|
|
105
|
+
<h4>version: v{version_clean}</h4>
|
|
106
|
+
{update_text}
|
|
107
|
+
<p style='font-weight: normal;'>Focus stackign applications and framework.<br>
|
|
108
|
+
Combine multiple frames into a single focused image.</p>
|
|
109
|
+
<p>Author: Luca Lista<br/>
|
|
110
|
+
Email: <a href="mailto:luka.lista@gmail.com">luka.lista@gmail.com</a></p>
|
|
111
|
+
<ul>
|
|
112
|
+
<li><a href="https://shinestacker.wordpress.com/">Website on Wordpress</a></li>
|
|
113
|
+
<li><a href="https://github.com/lucalista/shinestacker">GitHub project repository</a></li>
|
|
114
|
+
</ul>
|
|
115
|
+
"""
|
|
116
|
+
dialog = AboutDialog(parent, about_text)
|
|
117
|
+
dialog.exec()
|
|
@@ -42,7 +42,7 @@ def disable_macos_special_menu_items():
|
|
|
42
42
|
|
|
43
43
|
def fill_app_menu(app, app_menu):
|
|
44
44
|
about_action = QAction(f"About {constants.APP_STRING}", app)
|
|
45
|
-
about_action.triggered.connect(show_about_dialog)
|
|
45
|
+
about_action.triggered.connect(lambda: show_about_dialog(app))
|
|
46
46
|
app_menu.addAction(about_action)
|
|
47
47
|
app_menu.addSeparator()
|
|
48
48
|
if config.DONT_USE_NATIVE_MENU:
|