shinestacker 1.3.0__tar.gz → 1.4.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-1.3.0 → shinestacker-1.4.0}/CHANGELOG.md +39 -1
- {shinestacker-1.3.0/src/shinestacker.egg-info → shinestacker-1.4.0}/PKG-INFO +1 -1
- {shinestacker-1.3.0 → shinestacker-1.4.0}/THIRD_PARTY_LICENSES.txt +6 -13
- {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/alignment.md +18 -2
- shinestacker-1.4.0/index.html +47 -0
- shinestacker-1.4.0/src/shinestacker/_version.py +1 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/align.py +229 -41
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/align_auto.py +15 -3
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/align_parallel.py +81 -25
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/balance.py +23 -13
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/base_stack_algo.py +14 -20
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/depth_map.py +9 -14
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/noise_detection.py +3 -1
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/pyramid.py +8 -22
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/pyramid_auto.py +5 -14
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/pyramid_tiles.py +18 -20
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/stack_framework.py +1 -1
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/utils.py +37 -10
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/vignetting.py +2 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/app/gui_utils.py +10 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/app/main.py +3 -1
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/app/project.py +3 -1
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/app/retouch.py +3 -1
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/config/gui_constants.py +2 -2
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/core/core_utils.py +10 -1
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/action_config.py +172 -7
- shinestacker-1.4.0/src/shinestacker/gui/action_config_dialog.py +864 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/colors.py +1 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/folder_file_selection.py +5 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/gui_run.py +2 -2
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/main_window.py +18 -9
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/menu_manager.py +26 -2
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/new_project.py +5 -5
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/project_controller.py +4 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/project_editor.py +6 -4
- shinestacker-1.4.0/src/shinestacker/gui/recent_file_manager.py +93 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/sys_mon.py +24 -23
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/base_filter.py +5 -5
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/brush_preview.py +3 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/brush_tool.py +11 -11
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/display_manager.py +21 -37
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/image_editor_ui.py +129 -71
- shinestacker-1.4.0/src/shinestacker/retouch/image_view_status.py +61 -0
- shinestacker-1.4.0/src/shinestacker/retouch/image_viewer.py +123 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/io_gui_handler.py +12 -2
- shinestacker-1.4.0/src/shinestacker/retouch/overlaid_view.py +212 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/shortcuts_help.py +13 -3
- shinestacker-1.4.0/src/shinestacker/retouch/sidebyside_view.py +479 -0
- shinestacker-1.4.0/src/shinestacker/retouch/view_strategy.py +466 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0/src/shinestacker.egg-info}/PKG-INFO +1 -1
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker.egg-info/SOURCES.txt +6 -0
- shinestacker-1.3.0/src/shinestacker/_version.py +0 -1
- shinestacker-1.3.0/src/shinestacker/gui/action_config_dialog.py +0 -873
- shinestacker-1.3.0/src/shinestacker/retouch/image_viewer.py +0 -465
- {shinestacker-1.3.0 → shinestacker-1.4.0}/.coveragerc +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/.flake8 +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/.github/workflows/ci-multiplatform.yml +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/.github/workflows/pylint.yml +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/.github/workflows/pypi-publish.yml +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/.github/workflows/release.yml +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/.gitignore +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/.pylintrc +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/.readthedocs.yaml +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/LICENSE +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/MANIFEST.in +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/README.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/api.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/balancing.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/conf.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/focus_stacking.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/gui.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/index.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/job.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/main.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/multilayer.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/noise.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/requirements.txt +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/vignetting.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/img/coffee.gif +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/img/coffee_stack.jpg +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/img/extreme-vignetting.jpg +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/img/flies.gif +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/img/flies_stack.jpg +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/img/flow-diagram.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/img/gui-finder.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/img/gui-project-new.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/img/gui-project-run.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/img/gui-retouch.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/pyproject.toml +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/requirements.txt +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/scripts/build_release.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/scripts/git-rev-list.sh +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/scripts/validate-tomli.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/setup.cfg +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/__init__.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/__init__.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/denoise.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/exif.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/multilayer.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/sharpen.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/stack.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/white_balance.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/app/__init__.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/app/about_dialog.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/app/help_menu.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/app/open_frames.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/config/__init__.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/config/config.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/config/constants.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/core/__init__.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/core/colors.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/core/exceptions.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/core/framework.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/core/logging.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/__init__.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/base_form_dialog.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/flow_layout.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/gui_images.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/gui_logging.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/ico/shinestacker.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/ico/shinestacker.svg +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/project_converter.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/project_model.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/select_path_widget.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/tab_widget.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/time_progress_bar.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/__init__.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/brush.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/brush_gradient.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/denoise_filter.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/exif_data.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/file_loader.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/filter_manager.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/icon_container.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/io_manager.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/layer_collection.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/undo_manager.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/unsharp_mask_filter.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/vignetting_filter.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/white_balance_filter.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker.egg-info/dependency_links.txt +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker.egg-info/entry_points.txt +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker.egg-info/requires.txt +0 -0
- {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker.egg-info/top_level.txt +0 -0
|
@@ -2,8 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
This page reports the main releases only and the main changes therein.
|
|
4
4
|
|
|
5
|
+
## [v1.4.0] - 2025-09-14
|
|
6
|
+
**GUI improvements**
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
### Added
|
|
9
|
+
- added retouch view mode with master and frame side by side and top-bottom
|
|
10
|
+
- implemented "Open Recent" menu entry for both projects and retouch images
|
|
11
|
+
- expert options can be shown with a checkbox in each dialog
|
|
12
|
+
- optional summary plots for alignment transformation parameters
|
|
13
|
+
|
|
14
|
+
## Fixed
|
|
15
|
+
- fixed bug in plot generation
|
|
16
|
+
- fixes warning due to missing glyph in PDF generation on macOS
|
|
17
|
+
- safer parallel plot generation using a thread locks
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- code refactoring in various areas
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- code cleanup
|
|
26
|
+
|
|
27
|
+
## [v1.3.1] - 2025-09-08
|
|
28
|
+
**Fixes and optimizations**
|
|
29
|
+
|
|
30
|
+
## Fixed
|
|
31
|
+
- fixed input folder widget in job configuration
|
|
32
|
+
- better management of patological alignments
|
|
33
|
+
- restored alignment match plots
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
- improved automatic parameters for parallel alignment
|
|
37
|
+
- improved pyramid performances by combining two input steps
|
|
38
|
+
- improved performances of ORB and SURF feature extraction with key points caching
|
|
39
|
+
- improved configuration GUI using tabs and other minor GUI improvements
|
|
40
|
+
- code clean up and some fixes
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## [v1.3.0] - 2025-09-06
|
|
7
45
|
**Parallel processing and input flexibility**
|
|
8
46
|
|
|
9
47
|
### Added
|
|
@@ -40,18 +40,17 @@ psdtags
|
|
|
40
40
|
License: MIT
|
|
41
41
|
https://pypi.org/project/psdtags/
|
|
42
42
|
|
|
43
|
+
-------------------------------------------------------------------------------
|
|
44
|
+
psutil
|
|
45
|
+
License: BSD-3-Clause
|
|
46
|
+
https://pypi.org/project/psutil/
|
|
47
|
+
|
|
43
48
|
-------------------------------------------------------------------------------
|
|
44
49
|
PySide6
|
|
45
50
|
License: GNU Lesser General Public License v3.0 (LGPL-3.0)
|
|
46
51
|
or commercial license from The Qt Company
|
|
47
52
|
https://doc.qt.io/qtforpython/
|
|
48
53
|
|
|
49
|
-
Full license text follows:
|
|
50
|
-
|
|
51
|
-
--- BEGIN LGPL-3.0 LICENSE ---
|
|
52
|
-
<qui incolla il testo completo della LGPL-3.0>
|
|
53
|
-
--- END LGPL-3.0 LICENSE ---
|
|
54
|
-
|
|
55
54
|
-------------------------------------------------------------------------------
|
|
56
55
|
scipy
|
|
57
56
|
License: BSD-3-Clause
|
|
@@ -67,14 +66,8 @@ tqdm
|
|
|
67
66
|
License: Mozilla Public License 2.0 (MPL-2.0)
|
|
68
67
|
https://github.com/tqdm/tqdm
|
|
69
68
|
|
|
70
|
-
Full license text follows:
|
|
71
|
-
|
|
72
|
-
--- BEGIN MPL-2.0 LICENSE ---
|
|
73
|
-
<qui incolla il testo completo della MPL-2.0>
|
|
74
|
-
--- END MPL-2.0 LICENSE ---
|
|
75
|
-
|
|
76
69
|
-------------------------------------------------------------------------------
|
|
77
70
|
|
|
78
71
|
NOTE:
|
|
79
72
|
This file is provided for license compliance and attribution purposes.
|
|
80
|
-
|
|
73
|
+
Shine Stacker code is licensed separately under the GNU Lesser General Public License v3.0.
|
|
@@ -100,12 +100,21 @@ This class has extra parameters, in addition to the above ones:
|
|
|
100
100
|
|
|
101
101
|
* ```max_threads``` (optional, default: ```2```): number of parallel processes allowed. The number of actual threads will not be greater than the number of available CPU cores.
|
|
102
102
|
* ```chunk_submit``` (optional, default: ```True```): submit at most ```max_threads``` parallel processes. If ```chunk_submit``` is greater than ```max_threads``` a moderate performance gain is achieved at the cost of a possibly large memory occupancy.
|
|
103
|
-
* ```bw_matching``` (optional, default: ```False```): perform matches on black and white version of the images in order to save memory. Preliminary tests indicate that the gain with this option is marginal, and this option may be dropped in the future.
|
|
103
|
+
* ```bw_matching``` (optional, default: ```False```): perform matches on black and white version of the images in order to save memory. Preliminary tests indicate that the gain with this option is marginal, and this option may be dropped in the future.
|
|
104
|
+
|
|
105
|
+
## Automatic selection of processing strategy
|
|
106
|
+
|
|
107
|
+
A class ```AlignFramesAuto``` implements alignment with either sequential or parallel processing, and automatically tunes parallel processing parameters.
|
|
108
|
+
This class has extra parameters, in addition to the above ones:
|
|
109
|
+
|
|
110
|
+
* ```mode``` (optional, default: ```auto```): can be ```auto```, ```sequential``` or ```parallel```.
|
|
111
|
+
* ```memory_limit``` (optional, default: 8×1024<sup>3</sup>sup>): memory limit to determine optimal running parameters
|
|
112
|
+
|
|
104
113
|
|
|
105
114
|
## Allowed configurations
|
|
106
115
|
|
|
107
116
|
⚠️ Not all combinations of detector, descriptor and match methods are allowed. Combinations that are not allowed
|
|
108
|
-
give raise to an exception.
|
|
117
|
+
give raise to an exception. This is automatically prevented if one works with the GUI, but may occur when using python scripting. Below the table of the allowed combination with a comparison of CPU performances.
|
|
109
118
|
|
|
110
119
|
## CPU performances
|
|
111
120
|
|
|
@@ -127,3 +136,10 @@ give raise to an exception.
|
|
|
127
136
|
| 0.2887 | AKAZE | BRISK | NORM_HAMMING |
|
|
128
137
|
| 0.4075 | AKAZE | SIFT | KNN |
|
|
129
138
|
| 0.4397 | SIFT | SIFT | KNN |
|
|
139
|
+
|
|
140
|
+
## References
|
|
141
|
+
|
|
142
|
+
For a detailed review of the various image registration methods, see the publication below:
|
|
143
|
+
* [A Review of Keypoints’ Detection and Feature Description in Image Registration](https://onlinelibrary.wiley.com/doi/10.1155/2021/8509164), Scientific Programming 2021, 8509164, doi:10.1155/2021/8509164
|
|
144
|
+
|
|
145
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Shine Stacker</title>
|
|
7
|
+
<meta name="description" content="Shine Stacker – Open source focus stacking framework with GUI for macro and micro photography.">
|
|
8
|
+
|
|
9
|
+
<!-- Open Graph / Facebook -->
|
|
10
|
+
<meta property="og:type" content="website">
|
|
11
|
+
<meta property="og:title" content="Shine Stacker">
|
|
12
|
+
<meta property="og:description" content="Open source focus stacking framework with GUI for macro and micro photography.">
|
|
13
|
+
<meta property="og:image" content="https://raw.githubusercontent.com/lucalista/shinestacker/main/src/shinestacker/gui/ico/shinestacker.png">
|
|
14
|
+
<meta property="og:url" content="https://lucalista.github.io/shinestacker/">
|
|
15
|
+
|
|
16
|
+
<!-- Twitter Card -->
|
|
17
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
18
|
+
<meta name="twitter:title" content="Shine Stacker">
|
|
19
|
+
<meta name="twitter:description" content="Open source focus stacking framework with GUI for macro and micro photography.">
|
|
20
|
+
<meta name="twitter:image" content="https://raw.githubusercontent.com/lucalista/shinestacker/main/src/shinestacker/gui/ico/shinestacker.png">
|
|
21
|
+
<style>
|
|
22
|
+
body { font-family: sans-serif; margin: 40px; line-height: 1.6; background: #f7f7f7; color: #333; }
|
|
23
|
+
h1 { color: #2c3e50; font-size: 48px}
|
|
24
|
+
a { color: #2980b9; text-decoration: none; }
|
|
25
|
+
a:hover { text-decoration: underline; }
|
|
26
|
+
.container { max-width: 800px; margin: auto; background: #fff; padding: 30px; border-radius: 10px; box-shadow: 0 4px 10px rgba(0,0,0,0.1);}
|
|
27
|
+
.logo { width: 150px; }
|
|
28
|
+
ul { text-align: left; }
|
|
29
|
+
.footer { color: #A0A0A0; }
|
|
30
|
+
</style>
|
|
31
|
+
</head>
|
|
32
|
+
<body>
|
|
33
|
+
<div class="container" style="text-align:center;">
|
|
34
|
+
<h1>Shine Stacker</h1>
|
|
35
|
+
<img src="https://raw.githubusercontent.com/lucalista/shinestacker/main/src/shinestacker/gui/ico/shinestacker.png" alt="Shine Stacker Logo" class="logo"> <p>Open source framework and GUI for focus stacking macro and micro photography.</p>
|
|
36
|
+
<h2><a href="https://lucalista.github.io/shinestacker/docs/main.html">Go to GitHub website</h2>
|
|
37
|
+
<ul>
|
|
38
|
+
<li><a href="https://shinestacker.wordpress.com/">Shine Stacker on Wordpress</a></li>
|
|
39
|
+
<li><a href="https://shinestacker.readthedocs.io/">Complete documentation</a></li>
|
|
40
|
+
<li><a href="https://github.com/lucalista/shinestacker">Source code and releases on GitHub</a></li>
|
|
41
|
+
</ul>
|
|
42
|
+
<hr style="color: #f0f0ff;">
|
|
43
|
+
<p class="footer">Shine Stacker, © 2025, <a href="https://github.com/lucalista" alt="Luca Lista">Luca Lista</a> (LGPL-3.0).<br>
|
|
44
|
+
Logo © <a href="https://linktr.ee/alelista" alt="Alessandro Lista">Alessandro Lista</a> — All rights reserved.
|
|
45
|
+
</div>
|
|
46
|
+
</body>
|
|
47
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.4.0'
|
|
@@ -3,13 +3,15 @@ import os
|
|
|
3
3
|
import math
|
|
4
4
|
import logging
|
|
5
5
|
import numpy as np
|
|
6
|
-
import matplotlib.pyplot as plt
|
|
7
6
|
import cv2
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
8
|
from .. config.constants import constants
|
|
9
9
|
from .. core.exceptions import InvalidOptionError
|
|
10
10
|
from .. core.colors import color_str
|
|
11
|
+
from .. core.core_utils import setup_matplotlib_mode
|
|
11
12
|
from .utils import img_8bit, img_bw_8bit, save_plot, img_subsample
|
|
12
13
|
from .stack_framework import SubAction
|
|
14
|
+
setup_matplotlib_mode()
|
|
13
15
|
|
|
14
16
|
_DEFAULT_FEATURE_CONFIG = {
|
|
15
17
|
'detector': constants.DEFAULT_DETECTOR,
|
|
@@ -75,7 +77,7 @@ def decompose_affine_matrix(m):
|
|
|
75
77
|
|
|
76
78
|
def check_affine_matrix(m, img_shape, affine_thresholds=_AFFINE_THRESHOLDS):
|
|
77
79
|
if affine_thresholds is None:
|
|
78
|
-
return True, "No thresholds provided"
|
|
80
|
+
return True, "No thresholds provided", None
|
|
79
81
|
(scale_x, scale_y), rotation, shear, (tx, ty) = decompose_affine_matrix(m)
|
|
80
82
|
h, w = img_shape[:2]
|
|
81
83
|
reasons = []
|
|
@@ -94,13 +96,14 @@ def check_affine_matrix(m, img_shape, affine_thresholds=_AFFINE_THRESHOLDS):
|
|
|
94
96
|
if abs(ty) > max_ty:
|
|
95
97
|
reasons.append(f"y-translation too large (|{ty:.1f}| > {max_ty:.1f})")
|
|
96
98
|
if reasons:
|
|
97
|
-
return False, "; ".join(reasons)
|
|
98
|
-
return True, "Transformation within acceptable limits"
|
|
99
|
+
return False, "; ".join(reasons), None
|
|
100
|
+
return True, "Transformation within acceptable limits", \
|
|
101
|
+
(scale_x, scale_y, tx, ty, rotation, shear)
|
|
99
102
|
|
|
100
103
|
|
|
101
104
|
def check_homography_distortion(m, img_shape, homography_thresholds=_HOMOGRAPHY_THRESHOLDS):
|
|
102
105
|
if homography_thresholds is None:
|
|
103
|
-
return True, "No thresholds provided"
|
|
106
|
+
return True, "No thresholds provided", None
|
|
104
107
|
h, w = img_shape[:2]
|
|
105
108
|
corners = np.array([[0, 0], [w, 0], [w, h], [0, h]], dtype=np.float32)
|
|
106
109
|
transformed = cv2.perspectiveTransform(corners.reshape(1, -1, 2), m).reshape(-1, 2)
|
|
@@ -127,19 +130,20 @@ def check_homography_distortion(m, img_shape, homography_thresholds=_HOMOGRAPHY_
|
|
|
127
130
|
if max_angle_dev > homography_thresholds['max_skew']:
|
|
128
131
|
reasons.append(f"angle distortion too large ({max_angle_dev:.1f}°)")
|
|
129
132
|
if reasons:
|
|
130
|
-
return False, "; ".join(reasons)
|
|
131
|
-
return True, "Transformation within acceptable limits"
|
|
133
|
+
return False, "; ".join(reasons), None
|
|
134
|
+
return True, "Transformation within acceptable limits", \
|
|
135
|
+
(area_ratio, aspect_ratio, max_angle_dev)
|
|
132
136
|
|
|
133
137
|
|
|
134
|
-
def check_transform(m,
|
|
138
|
+
def check_transform(m, img_shape, transform_type,
|
|
135
139
|
affine_thresholds, homography_thresholds):
|
|
136
140
|
if transform_type == constants.ALIGN_RIGID:
|
|
137
141
|
return check_affine_matrix(
|
|
138
|
-
m,
|
|
142
|
+
m, img_shape, affine_thresholds)
|
|
139
143
|
if transform_type == constants.ALIGN_HOMOGRAPHY:
|
|
140
144
|
return check_homography_distortion(
|
|
141
|
-
m,
|
|
142
|
-
return False, f'invalid transfrom option {transform_type}'
|
|
145
|
+
m, img_shape, homography_thresholds)
|
|
146
|
+
return False, f'invalid transfrom option {transform_type}', None
|
|
143
147
|
|
|
144
148
|
|
|
145
149
|
def get_good_matches(des_0, des_ref, matching_config=None):
|
|
@@ -247,7 +251,10 @@ def find_transform(src_pts, dst_pts, transform=constants.DEFAULT_TRANSFORM,
|
|
|
247
251
|
confidence=align_confidence / 100.0,
|
|
248
252
|
refineIters=refine_iters)
|
|
249
253
|
else:
|
|
250
|
-
raise InvalidOptionError(
|
|
254
|
+
raise InvalidOptionError(
|
|
255
|
+
'transform', method,
|
|
256
|
+
f". Valid options are: {constants.ALIGN_HOMOGRAPHY}, {constants.ALIGN_RIGID}"
|
|
257
|
+
)
|
|
251
258
|
return result
|
|
252
259
|
|
|
253
260
|
|
|
@@ -270,6 +277,18 @@ def rescale_trasnsform(m, w0, h0, w_sub, h_sub, subsample, transform):
|
|
|
270
277
|
return m
|
|
271
278
|
|
|
272
279
|
|
|
280
|
+
def plot_matches(msk, img_ref_sub, img_0_sub, kp_ref, kp_0, good_matches, plot_path):
|
|
281
|
+
matches_mask = msk.ravel().tolist()
|
|
282
|
+
img_match = cv2.cvtColor(cv2.drawMatches(
|
|
283
|
+
img_8bit(img_0_sub), kp_0, img_8bit(img_ref_sub),
|
|
284
|
+
kp_ref, good_matches, None, matchColor=(0, 255, 0),
|
|
285
|
+
singlePointColor=None, matchesMask=matches_mask,
|
|
286
|
+
flags=2), cv2.COLOR_BGR2RGB)
|
|
287
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
288
|
+
plt.imshow(img_match, 'gray')
|
|
289
|
+
save_plot(plot_path)
|
|
290
|
+
|
|
291
|
+
|
|
273
292
|
def align_images(img_ref, img_0, feature_config=None, matching_config=None, alignment_config=None,
|
|
274
293
|
plot_path=None, callbacks=None,
|
|
275
294
|
affine_thresholds=_AFFINE_THRESHOLDS,
|
|
@@ -315,22 +334,16 @@ def align_images(img_ref, img_0, feature_config=None, matching_config=None, alig
|
|
|
315
334
|
m = None
|
|
316
335
|
if n_good_matches >= min_matches:
|
|
317
336
|
transform = alignment_config['transform']
|
|
318
|
-
src_pts = np.float32(
|
|
319
|
-
|
|
337
|
+
src_pts = np.float32(
|
|
338
|
+
[kp_0[match.queryIdx].pt for match in good_matches]).reshape(-1, 1, 2)
|
|
339
|
+
dst_pts = np.float32(
|
|
340
|
+
[kp_ref[match.trainIdx].pt for match in good_matches]).reshape(-1, 1, 2)
|
|
320
341
|
m, msk = find_transform(src_pts, dst_pts, transform, alignment_config['align_method'],
|
|
321
342
|
*(alignment_config[k]
|
|
322
343
|
for k in ['rans_threshold', 'max_iters',
|
|
323
344
|
'align_confidence', 'refine_iters']))
|
|
324
345
|
if plot_path is not None:
|
|
325
|
-
|
|
326
|
-
img_match = cv2.cvtColor(cv2.drawMatches(
|
|
327
|
-
img_8bit(img_0_sub), kp_0, img_8bit(img_ref_sub),
|
|
328
|
-
kp_ref, good_matches, None, matchColor=(0, 255, 0),
|
|
329
|
-
singlePointColor=None, matchesMask=matches_mask,
|
|
330
|
-
flags=2), cv2.COLOR_BGR2RGB)
|
|
331
|
-
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
332
|
-
plt.imshow(img_match, 'gray')
|
|
333
|
-
save_plot(plot_path)
|
|
346
|
+
plot_matches(msk, img_ref_sub, img_0_sub, kp_ref, kp_0, good_matches, plot_path)
|
|
334
347
|
if callbacks and 'save_plot' in callbacks:
|
|
335
348
|
callbacks['save_plot'](plot_path)
|
|
336
349
|
h_sub, w_sub = img_0_sub.shape[:2]
|
|
@@ -339,9 +352,11 @@ def align_images(img_ref, img_0, feature_config=None, matching_config=None, alig
|
|
|
339
352
|
if m is None:
|
|
340
353
|
raise InvalidOptionError("transform", transform)
|
|
341
354
|
transform_type = alignment_config['transform']
|
|
342
|
-
is_valid, reason = check_transform(
|
|
343
|
-
m, img_0, transform_type,
|
|
355
|
+
is_valid, reason, result = check_transform(
|
|
356
|
+
m, img_0.shape, transform_type,
|
|
344
357
|
affine_thresholds, homography_thresholds)
|
|
358
|
+
if callbacks and 'save_transform_result' in callbacks:
|
|
359
|
+
callbacks['save_transform_result'](result)
|
|
345
360
|
if not is_valid:
|
|
346
361
|
if callbacks and 'warning' in callbacks:
|
|
347
362
|
callbacks['warning'](f"invalid transformation: {reason}")
|
|
@@ -397,6 +412,18 @@ class AlignFramesBase(SubAction):
|
|
|
397
412
|
for k in self.alignment_config:
|
|
398
413
|
if k in kwargs:
|
|
399
414
|
self.alignment_config[k] = kwargs[k]
|
|
415
|
+
self._area_ratio = None
|
|
416
|
+
self._aspect_ratio = None
|
|
417
|
+
self._max_angle_dev = None
|
|
418
|
+
self._scale_x = None
|
|
419
|
+
self._scale_y = None
|
|
420
|
+
self._translation_x = None
|
|
421
|
+
self._translation_y = None
|
|
422
|
+
self._rotation = None
|
|
423
|
+
self._shear = None
|
|
424
|
+
|
|
425
|
+
def relative_transformation(self):
|
|
426
|
+
return None
|
|
400
427
|
|
|
401
428
|
def align_images(self, idx, img_ref, img_0):
|
|
402
429
|
pass
|
|
@@ -407,6 +434,15 @@ class AlignFramesBase(SubAction):
|
|
|
407
434
|
def begin(self, process):
|
|
408
435
|
self.process = process
|
|
409
436
|
self._n_good_matches = np.zeros(process.total_action_counts)
|
|
437
|
+
self._area_ratio = np.ones(process.total_action_counts)
|
|
438
|
+
self._aspect_ratio = np.ones(process.total_action_counts)
|
|
439
|
+
self._max_angle_dev = np.zeros(process.total_action_counts)
|
|
440
|
+
self._scale_x = np.ones(process.total_action_counts)
|
|
441
|
+
self._scale_y = np.ones(process.total_action_counts)
|
|
442
|
+
self._translation_x = np.zeros(process.total_action_counts)
|
|
443
|
+
self._translation_y = np.zeros(process.total_action_counts)
|
|
444
|
+
self._rotation = np.zeros(process.total_action_counts)
|
|
445
|
+
self._shear = np.zeros(process.total_action_counts)
|
|
410
446
|
|
|
411
447
|
def run_frame(self, idx, ref_idx, img_0):
|
|
412
448
|
if idx == self.process.ref_idx:
|
|
@@ -422,24 +458,29 @@ class AlignFramesBase(SubAction):
|
|
|
422
458
|
f"{os.path.basename(self.process.input_filepath(idx))}"
|
|
423
459
|
|
|
424
460
|
def end(self):
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
x = np.arange(1, len(
|
|
461
|
+
|
|
462
|
+
def get_coordinates(items):
|
|
463
|
+
x = np.arange(1, len(items) + 1, dtype=int)
|
|
428
464
|
no_ref = x != self.process.ref_idx + 1
|
|
429
465
|
x = x[no_ref]
|
|
430
|
-
y =
|
|
466
|
+
y = np.array(items)[no_ref]
|
|
431
467
|
if self.process.ref_idx == 0:
|
|
432
|
-
|
|
468
|
+
y_ref = y[1]
|
|
433
469
|
elif self.process.ref_idx >= len(y):
|
|
434
|
-
|
|
470
|
+
y_ref = y[-1]
|
|
435
471
|
else:
|
|
436
|
-
|
|
472
|
+
y_ref = (y[self.process.ref_idx - 1] + y[self.process.ref_idx]) / 2
|
|
473
|
+
return x, y, y_ref
|
|
437
474
|
|
|
475
|
+
if self.plot_summary:
|
|
476
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
477
|
+
x, y, y_ref = get_coordinates(self._n_good_matches)
|
|
438
478
|
plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
|
|
439
|
-
[0,
|
|
479
|
+
[0, y_ref], color='cornflowerblue', linestyle='--', label='reference frame')
|
|
440
480
|
plt.plot([x[0], x[-1]], [self.min_matches, self.min_matches], color='lightgray',
|
|
441
481
|
linestyle='--', label='min. matches')
|
|
442
482
|
plt.plot(x, y, color='navy', label='matches')
|
|
483
|
+
plt.title("Number of matches")
|
|
443
484
|
plt.xlabel('frame')
|
|
444
485
|
plt.ylabel('# of matches')
|
|
445
486
|
plt.legend()
|
|
@@ -448,19 +489,160 @@ class AlignFramesBase(SubAction):
|
|
|
448
489
|
plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
|
|
449
490
|
f"{self.process.name}-matches.pdf"
|
|
450
491
|
save_plot(plot_path)
|
|
451
|
-
plt.close('all')
|
|
452
492
|
self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
|
|
453
493
|
f"{self.process.name}: matches", plot_path)
|
|
494
|
+
transform = self.alignment_config['transform']
|
|
495
|
+
title = "Transformation parameters rel. to reference frame"
|
|
496
|
+
if transform == constants.ALIGN_RIGID:
|
|
497
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
498
|
+
x, y, y_ref = get_coordinates(self._rotation)
|
|
499
|
+
plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
|
|
500
|
+
[0, y_ref], color='cornflowerblue',
|
|
501
|
+
linestyle='--', label='reference frame')
|
|
502
|
+
plt.plot([x[0], x[-1]], [0, 0], color='cornflowerblue', linestyle='--')
|
|
503
|
+
plt.plot(x, y, color='navy', label='rotation (°)')
|
|
504
|
+
y_lim = max(abs(y.min()), abs(y.max())) * 1.1
|
|
505
|
+
plt.ylim(-y_lim, y_lim)
|
|
506
|
+
plt.title(title)
|
|
507
|
+
plt.xlabel('frame')
|
|
508
|
+
plt.ylabel('rotation angle (degrees)')
|
|
509
|
+
plt.legend()
|
|
510
|
+
plt.xlim(x[0], x[-1])
|
|
511
|
+
plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
|
|
512
|
+
f"{self.process.name}-rotation.pdf"
|
|
513
|
+
save_plot(plot_path)
|
|
514
|
+
self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
|
|
515
|
+
f"{self.process.name}: rotation", plot_path)
|
|
516
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
517
|
+
x, y_x, y_x_ref = get_coordinates(self._translation_x)
|
|
518
|
+
x, y_y, y_y_ref = get_coordinates(self._translation_y)
|
|
519
|
+
plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
|
|
520
|
+
[y_x_ref, y_y_ref], color='cornflowerblue',
|
|
521
|
+
linestyle='--', label='reference frame')
|
|
522
|
+
plt.plot([x[0], x[-1]], [0, 0], color='cornflowerblue', linestyle='--')
|
|
523
|
+
plt.plot(x, y_x, color='blue', label='translation, x (px)')
|
|
524
|
+
plt.plot(x, y_y, color='red', label='translation, y (px)')
|
|
525
|
+
y_lim = max(abs(y_x.min()), abs(y_x.max()), abs(y_y.min()), abs(y_y.max())) * 1.1
|
|
526
|
+
plt.ylim(-y_lim, y_lim)
|
|
527
|
+
plt.title(title)
|
|
528
|
+
plt.xlabel('frame')
|
|
529
|
+
plt.ylabel('translation (pixels)')
|
|
530
|
+
plt.legend()
|
|
531
|
+
plt.xlim(x[0], x[-1])
|
|
532
|
+
plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
|
|
533
|
+
f"{self.process.name}-translation.pdf"
|
|
534
|
+
save_plot(plot_path)
|
|
535
|
+
self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
|
|
536
|
+
f"{self.process.name}: translation", plot_path)
|
|
537
|
+
|
|
538
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
539
|
+
x, y, y_ref = get_coordinates(self._scale_x)
|
|
540
|
+
plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
|
|
541
|
+
[1, y_ref], color='cornflowerblue',
|
|
542
|
+
linestyle='--', label='reference frame')
|
|
543
|
+
plt.plot([x[0], x[-1]], [1, 1], color='cornflowerblue', linestyle='--')
|
|
544
|
+
plt.plot(x, y, color='blue', label='scale factor')
|
|
545
|
+
d_max = max(abs(y.min() - 1), abs(y.max() - 1)) * 1.1
|
|
546
|
+
plt.ylim(1.0 - d_max, 1.0 + d_max)
|
|
547
|
+
plt.title(title)
|
|
548
|
+
plt.xlabel('frame')
|
|
549
|
+
plt.ylabel('scale factor')
|
|
550
|
+
plt.legend()
|
|
551
|
+
plt.xlim(x[0], x[-1])
|
|
552
|
+
plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
|
|
553
|
+
f"{self.process.name}-scale.pdf"
|
|
554
|
+
save_plot(plot_path)
|
|
555
|
+
self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
|
|
556
|
+
f"{self.process.name}: scale", plot_path)
|
|
557
|
+
elif transform == constants.ALIGN_HOMOGRAPHY:
|
|
558
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
559
|
+
x, y, y_ref = get_coordinates(self._area_ratio)
|
|
560
|
+
plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
|
|
561
|
+
[0, y_ref], color='cornflowerblue',
|
|
562
|
+
linestyle='--', label='reference frame')
|
|
563
|
+
plt.plot([x[0], x[-1]], [0, 0], color='cornflowerblue', linestyle='--')
|
|
564
|
+
plt.plot(x, y, color='navy', label='area ratio')
|
|
565
|
+
d_max = max(abs(y.min() - 1), abs(y.max() - 1)) * 1.1
|
|
566
|
+
plt.ylim(1.0 - d_max, 1.0 + d_max)
|
|
567
|
+
plt.title(title)
|
|
568
|
+
plt.xlabel('frame')
|
|
569
|
+
plt.ylabel('warped area ratio')
|
|
570
|
+
plt.legend()
|
|
571
|
+
plt.xlim(x[0], x[-1])
|
|
572
|
+
plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
|
|
573
|
+
f"{self.process.name}-area-ratio.pdf"
|
|
574
|
+
save_plot(plot_path)
|
|
575
|
+
self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
|
|
576
|
+
f"{self.process.name}: area ratio", plot_path)
|
|
577
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
578
|
+
x, y, y_ref = get_coordinates(self._aspect_ratio)
|
|
579
|
+
plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
|
|
580
|
+
[0, y_ref], color='cornflowerblue',
|
|
581
|
+
linestyle='--', label='reference frame')
|
|
582
|
+
plt.plot([x[0], x[-1]], [0, 0], color='cornflowerblue', linestyle='--')
|
|
583
|
+
plt.plot(x, y, color='navy', label='aspect ratio')
|
|
584
|
+
y_min, y_max = y.min(), y.max()
|
|
585
|
+
delta = y_max - y_min
|
|
586
|
+
plt.ylim(y_min - 0.05 * delta, y_max + 0.05 * delta)
|
|
587
|
+
plt.title(title)
|
|
588
|
+
plt.xlabel('frame')
|
|
589
|
+
plt.ylabel('aspect ratio')
|
|
590
|
+
plt.legend()
|
|
591
|
+
plt.xlim(x[0], x[-1])
|
|
592
|
+
plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
|
|
593
|
+
f"{self.process.name}-aspect-ratio.pdf"
|
|
594
|
+
save_plot(plot_path)
|
|
595
|
+
self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
|
|
596
|
+
f"{self.process.name}: aspect ratio", plot_path)
|
|
597
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
598
|
+
x, y, y_ref = get_coordinates(self._max_angle_dev)
|
|
599
|
+
plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
|
|
600
|
+
[0, y_ref], color='cornflowerblue',
|
|
601
|
+
linestyle='--', label='reference frame')
|
|
602
|
+
plt.plot([x[0], x[-1]], [0, 0], color='cornflowerblue', linestyle='--')
|
|
603
|
+
plt.plot(x, y, color='navy', label='max. dev. ang. (°)')
|
|
604
|
+
y_lim = max(abs(y.min()), abs(y.max())) * 1.1
|
|
605
|
+
plt.ylim(-y_lim, y_lim)
|
|
606
|
+
plt.title(title)
|
|
607
|
+
plt.xlabel('frame')
|
|
608
|
+
plt.ylabel('max deviation angle (degrees)')
|
|
609
|
+
plt.legend()
|
|
610
|
+
plt.xlim(x[0], x[-1])
|
|
611
|
+
plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
|
|
612
|
+
f"{self.process.name}-rotation.pdf"
|
|
613
|
+
save_plot(plot_path)
|
|
614
|
+
self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
|
|
615
|
+
f"{self.process.name}: rotation", plot_path)
|
|
616
|
+
|
|
617
|
+
def save_transform_result(self, idx, result):
|
|
618
|
+
if result is None:
|
|
619
|
+
return
|
|
620
|
+
transform = self.alignment_config['transform']
|
|
621
|
+
if transform == constants.ALIGN_HOMOGRAPHY:
|
|
622
|
+
area_ratio, aspect_ratio, max_angle_dev = result
|
|
623
|
+
self._area_ratio[idx] = area_ratio
|
|
624
|
+
self._aspect_ratio[idx] = aspect_ratio
|
|
625
|
+
self._max_angle_dev[idx] = max_angle_dev
|
|
626
|
+
elif transform == constants.ALIGN_RIGID:
|
|
627
|
+
scale_x, scale_y, translation_x, translation_y, rotation, shear = result
|
|
628
|
+
self._scale_x[idx] = scale_x
|
|
629
|
+
self._scale_y[idx] = scale_y
|
|
630
|
+
self._translation_x[idx] = translation_x
|
|
631
|
+
self._translation_y[idx] = translation_y
|
|
632
|
+
self._rotation[idx] = rotation
|
|
633
|
+
self._shear[idx] = shear
|
|
634
|
+
else:
|
|
635
|
+
raise InvalidOptionError(
|
|
636
|
+
'transform', transform,
|
|
637
|
+
f". Valid options are: {constants.ALIGN_HOMOGRAPHY}, {constants.ALIGN_RIGID}"
|
|
638
|
+
)
|
|
454
639
|
|
|
455
640
|
|
|
456
641
|
class AlignFrames(AlignFramesBase):
|
|
457
|
-
def __init__(self, enabled=True, feature_config=None, matching_config=None,
|
|
458
|
-
alignment_config=None, **kwargs):
|
|
459
|
-
super().__init__(enabled)
|
|
460
|
-
|
|
461
642
|
def align_images(self, idx, img_ref, img_0):
|
|
462
643
|
idx_str = f"{idx:04d}"
|
|
463
644
|
idx_tot_str = self.process.idx_tot_str(idx)
|
|
645
|
+
|
|
464
646
|
callbacks = {
|
|
465
647
|
'message': lambda: self.print_message(f'{idx_tot_str}: find matches'),
|
|
466
648
|
'matches_message': lambda n: self.print_message(f'{idx_tot_str}: good matches: {n}'),
|
|
@@ -470,11 +652,14 @@ class AlignFrames(AlignFramesBase):
|
|
|
470
652
|
f': {msg}', constants.LOG_COLOR_WARNING),
|
|
471
653
|
'save_plot': lambda plot_path: self.process.callback(
|
|
472
654
|
constants.CALLBACK_SAVE_PLOT, self.process.id,
|
|
473
|
-
f"{self.process.name}: matches\nframe {idx_str}", plot_path)
|
|
655
|
+
f"{self.process.name}: matches\nframe {idx_str}", plot_path),
|
|
656
|
+
'save_transform_result': lambda result: self.save_transform_result(idx, result)
|
|
474
657
|
}
|
|
475
658
|
if self.plot_matches:
|
|
476
|
-
plot_path =
|
|
477
|
-
|
|
659
|
+
plot_path = os.path.join(
|
|
660
|
+
self.process.working_path,
|
|
661
|
+
self.process.plot_path,
|
|
662
|
+
f"{self.process.name}-matches-{idx_str}.pdf")
|
|
478
663
|
else:
|
|
479
664
|
plot_path = None
|
|
480
665
|
affine_thresholds, homography_thresholds = self.get_transform_thresholds()
|
|
@@ -496,5 +681,8 @@ class AlignFrames(AlignFramesBase):
|
|
|
496
681
|
return None
|
|
497
682
|
return img
|
|
498
683
|
|
|
684
|
+
def relative_transformation(self):
|
|
685
|
+
return False
|
|
686
|
+
|
|
499
687
|
def sequential_processing(self):
|
|
500
688
|
return True
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, W0718, R0912, R0915, E1101, R0914, R0911, E0606, R0801, R0902
|
|
2
2
|
import os
|
|
3
|
+
import numpy as np
|
|
3
4
|
from ..config.constants import constants
|
|
4
5
|
from .align import AlignFramesBase, AlignFrames
|
|
5
6
|
from .align_parallel import AlignFramesParallel
|
|
7
|
+
from .utils import get_first_image_file, get_img_metadata, read_img
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
class AlignFramesAuto(AlignFramesBase):
|
|
9
11
|
def __init__(self, enabled=True, feature_config=None, matching_config=None,
|
|
10
12
|
alignment_config=None, **kwargs):
|
|
11
|
-
super().__init__(enabled=True, feature_config=None, matching_config=None,
|
|
12
|
-
alignment_config=None, **kwargs)
|
|
13
13
|
self.mode = kwargs.pop('mode', constants.DEFAULT_ALIGN_MODE)
|
|
14
|
+
self.memory_limit = kwargs.pop('memory_limit', constants.DEFAULT_ALIGN_MEMORY_LIMIT_GB)
|
|
14
15
|
self.max_threads = kwargs.pop('max_threads', constants.DEFAULT_ALIGN_MAX_THREADS)
|
|
15
16
|
self.chunk_submit = kwargs.pop('chunk_submit', constants.DEFAULT_ALIGN_CHUNK_SUBMIT)
|
|
16
17
|
self.bw_matching = kwargs.pop('bw_matching', constants.DEFAULT_ALIGN_BW_MATCHING)
|
|
17
18
|
self.kwargs = kwargs
|
|
19
|
+
super().__init__(enabled=True, feature_config=None, matching_config=None,
|
|
20
|
+
alignment_config=None, **kwargs)
|
|
18
21
|
available_cores = os.cpu_count() or 1
|
|
19
22
|
self.num_threads = min(self.max_threads, available_cores)
|
|
20
23
|
self._implementation = None
|
|
24
|
+
self.overhead = 30.0
|
|
21
25
|
|
|
22
26
|
def begin(self, process):
|
|
23
27
|
if self.mode == 'sequential' or self.num_threads == 1:
|
|
@@ -39,7 +43,15 @@ class AlignFramesAuto(AlignFramesBase):
|
|
|
39
43
|
descriptor = constants.DEFAULT_DESCRIPTOR
|
|
40
44
|
if detector in (constants.DETECTOR_SIFT, constants.DETECTOR_AKAZE) or \
|
|
41
45
|
descriptor in (constants.DESCRIPTOR_SIFT, constants.DESCRIPTOR_AKAZE):
|
|
42
|
-
|
|
46
|
+
shape, dtype = get_img_metadata(
|
|
47
|
+
read_img(get_first_image_file(process.input_filepaths())))
|
|
48
|
+
bytes_per_pixel = 3 * np.dtype(dtype).itemsize
|
|
49
|
+
img_memory = bytes_per_pixel * float(shape[0]) * float(shape[1]) * \
|
|
50
|
+
self.overhead / constants.ONE_GIGA
|
|
51
|
+
num_threads = max(
|
|
52
|
+
1,
|
|
53
|
+
int(round(self.memory_limit) / img_memory))
|
|
54
|
+
num_threads = min(num_threads, self.num_threads)
|
|
43
55
|
chunk_submit = True
|
|
44
56
|
else:
|
|
45
57
|
num_threads = self.num_threads
|