shinestacker 1.2.0__tar.gz → 1.3.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.2.0 → shinestacker-1.3.0}/CHANGELOG.md +45 -8
- {shinestacker-1.2.0/src/shinestacker.egg-info → shinestacker-1.3.0}/PKG-INFO +6 -6
- {shinestacker-1.2.0 → shinestacker-1.3.0}/README.md +4 -5
- {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/alignment.md +8 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/focus_stacking.md +1 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/job.md +3 -1
- {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/main.md +1 -1
- {shinestacker-1.2.0 → shinestacker-1.3.0}/pyproject.toml +1 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/requirements.txt +1 -0
- shinestacker-1.3.0/src/shinestacker/_version.py +1 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/align.py +148 -115
- shinestacker-1.3.0/src/shinestacker/algorithms/align_auto.py +64 -0
- shinestacker-1.3.0/src/shinestacker/algorithms/align_parallel.py +296 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/balance.py +14 -13
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/base_stack_algo.py +11 -2
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/multilayer.py +14 -15
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/noise_detection.py +13 -14
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/pyramid.py +4 -4
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/pyramid_auto.py +16 -10
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/pyramid_tiles.py +19 -11
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/stack.py +30 -26
- shinestacker-1.3.0/src/shinestacker/algorithms/stack_framework.py +330 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/vignetting.py +16 -13
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/app/main.py +7 -3
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/config/constants.py +63 -26
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/config/gui_constants.py +1 -1
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/core/core_utils.py +4 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/core/framework.py +114 -33
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/action_config.py +57 -5
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/action_config_dialog.py +156 -17
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/base_form_dialog.py +2 -2
- shinestacker-1.3.0/src/shinestacker/gui/folder_file_selection.py +101 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/gui_images.py +10 -10
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/gui_run.py +13 -11
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/main_window.py +10 -5
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/menu_manager.py +4 -0
- shinestacker-1.3.0/src/shinestacker/gui/new_project.py +348 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/project_controller.py +13 -9
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/project_converter.py +4 -2
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/project_editor.py +72 -53
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/select_path_widget.py +1 -1
- shinestacker-1.3.0/src/shinestacker/gui/sys_mon.py +96 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/tab_widget.py +3 -3
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/time_progress_bar.py +4 -3
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/exif_data.py +1 -1
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/image_editor_ui.py +2 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0/src/shinestacker.egg-info}/PKG-INFO +6 -6
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker.egg-info/SOURCES.txt +4 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker.egg-info/requires.txt +1 -0
- shinestacker-1.2.0/src/shinestacker/_version.py +0 -1
- shinestacker-1.2.0/src/shinestacker/algorithms/stack_framework.py +0 -308
- shinestacker-1.2.0/src/shinestacker/gui/new_project.py +0 -251
- {shinestacker-1.2.0 → shinestacker-1.3.0}/.coveragerc +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/.flake8 +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/.github/workflows/ci-multiplatform.yml +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/.github/workflows/pylint.yml +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/.github/workflows/pypi-publish.yml +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/.github/workflows/release.yml +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/.gitignore +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/.pylintrc +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/.readthedocs.yaml +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/LICENSE +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/MANIFEST.in +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/THIRD_PARTY_LICENSES.txt +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/api.md +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/balancing.md +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/conf.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/gui.md +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/index.md +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/multilayer.md +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/noise.md +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/requirements.txt +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/vignetting.md +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/img/coffee.gif +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/img/coffee_stack.jpg +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/img/extreme-vignetting.jpg +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/img/flies.gif +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/img/flies_stack.jpg +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/img/flow-diagram.png +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/img/gui-finder.png +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/img/gui-project-new.png +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/img/gui-project-run.png +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/img/gui-retouch.png +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/scripts/build_release.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/scripts/git-rev-list.sh +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/scripts/validate-tomli.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/setup.cfg +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/__init__.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/__init__.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/denoise.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/depth_map.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/exif.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/sharpen.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/utils.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/white_balance.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/app/__init__.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/app/about_dialog.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/app/gui_utils.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/app/help_menu.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/app/open_frames.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/app/project.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/app/retouch.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/config/__init__.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/config/config.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/core/__init__.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/core/colors.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/core/exceptions.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/core/logging.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/__init__.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/colors.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/flow_layout.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/gui_logging.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/ico/shinestacker.png +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/ico/shinestacker.svg +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/project_model.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/__init__.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/base_filter.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/brush.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/brush_gradient.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/brush_preview.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/brush_tool.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/denoise_filter.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/display_manager.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/file_loader.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/filter_manager.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/icon_container.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/image_viewer.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/io_gui_handler.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/io_manager.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/layer_collection.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/shortcuts_help.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/undo_manager.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/unsharp_mask_filter.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/vignetting_filter.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/white_balance_filter.py +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker.egg-info/dependency_links.txt +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker.egg-info/entry_points.txt +0 -0
- {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker.egg-info/top_level.txt +0 -0
|
@@ -2,20 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
This page reports the main releases only and the main changes therein.
|
|
4
4
|
|
|
5
|
+
|
|
6
|
+
## [v1.3.0] - 2025-09-05
|
|
7
|
+
**Parallel processing and input flexibility**
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Parallel processing in alignment feature extraction
|
|
11
|
+
- Parallel processing of combined actions
|
|
12
|
+
- Job input can now specify a list of files (not only a folder)
|
|
13
|
+
- CPU and memory usage monitor widget for running jobs
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Path in example project
|
|
17
|
+
- Bug fix in config dialog
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Changes some default parameters for better performances
|
|
21
|
+
- Some GUI restyling
|
|
22
|
+
- Code cleanup
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## [v1.2.1] - 2025-09-02
|
|
27
|
+
**Bug fixes and minor improvements**
|
|
28
|
+
|
|
29
|
+
### Changes
|
|
30
|
+
|
|
31
|
+
* alignment is more tolerant in case of failures: frames are skipped and the running job is not stopped
|
|
32
|
+
* fixed the -x (--expert) option
|
|
33
|
+
* more safety checks prevent crashes for abnormal conditions
|
|
34
|
+
* reference frame index improved with a more consistent treatment, a better numbering scheme and GUI widget
|
|
35
|
+
* improved project undo action description text
|
|
36
|
+
* some bug fixes and code cleanup
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
5
40
|
## [v1.2.0] - 2025-08-31
|
|
6
|
-
**
|
|
41
|
+
**Parallel processing and more improvements**
|
|
42
|
+
|
|
43
|
+
### Changes
|
|
7
44
|
|
|
8
45
|
* Implemented parallel processing for pyramid stacking algorithm
|
|
9
|
-
* optimized
|
|
10
|
-
* Implemented automatic subsample option for alignment, balancing and vignetting, now default
|
|
46
|
+
* optimized pyramid algorithm: selects automatically the best within the given memory budget to avoid memory issues in case many pictures are selected. Explicit configuration is also possible for specific needs.
|
|
47
|
+
* Implemented automatic subsample option for alignment, balancing and vignetting, now default
|
|
11
48
|
* HLS and HSV corrections now supported for 16 bit images
|
|
12
49
|
* Added luminosity correction in the LAB color space
|
|
13
50
|
* Alignment module skips frames if transformation parameters are out of a reasonable ranges
|
|
14
|
-
* Multilayer modules sends a warning if the estimated file size is > 1GB
|
|
15
|
-
* "Run all jobs" action is enabled only if more than one job are present
|
|
16
|
-
* Updated default module names
|
|
17
|
-
* Code refactoring
|
|
18
|
-
* Some GUI fixes
|
|
51
|
+
* Multilayer modules sends a warning if the estimated output file size is > 1GB
|
|
52
|
+
* "Run all jobs" action is enabled only if more than one job are present
|
|
53
|
+
* Updated default module names in project genereated by "new project" dialog
|
|
54
|
+
* Code refactoring
|
|
55
|
+
* Some GUI fixes
|
|
19
56
|
|
|
20
57
|
---
|
|
21
58
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shinestacker
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: ShineStacker
|
|
5
5
|
Author-email: Luca Lista <luka.lista@gmail.com>
|
|
6
6
|
License-Expression: LGPL-3.0
|
|
@@ -20,6 +20,7 @@ Requires-Dist: numpy
|
|
|
20
20
|
Requires-Dist: opencv_python
|
|
21
21
|
Requires-Dist: pillow
|
|
22
22
|
Requires-Dist: psdtags
|
|
23
|
+
Requires-Dist: psutil
|
|
23
24
|
Requires-Dist: PySide6
|
|
24
25
|
Requires-Dist: scipy
|
|
25
26
|
Requires-Dist: tifffile
|
|
@@ -69,6 +70,10 @@ The GUI has two main working areas:
|
|
|
69
70
|
|
|
70
71
|
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/gui-retouch.png' width="600" referrerpolicy="no-referrer">
|
|
71
72
|
|
|
73
|
+
# Resources
|
|
74
|
+
|
|
75
|
+
🌍 [Website on WordPress](https://shinestacker.wordpress.com) • 📖 [Main documentation](https://shinestacker.readthedocs.io) • 📝 [Changelog](https://github.com/lucalista/shinestacker/blob/main/CHANGELOG.md)
|
|
76
|
+
|
|
72
77
|
# Note for macOS users
|
|
73
78
|
|
|
74
79
|
**The following note is only relevant if you download the application as compressed archive from the [release page](https://github.com/lucalista/shinestacker/releases).**
|
|
@@ -88,11 +93,6 @@ xattr -cr ~/Downloads/shinestacker/shinestacker.app
|
|
|
88
93
|
|
|
89
94
|
macOS adds a quarantine flag to all files downloaded from the internet. The above command removes that flag while preserving all other application functionality.
|
|
90
95
|
|
|
91
|
-
# Resources
|
|
92
|
-
|
|
93
|
-
🌍 [Website on WordPress](https://shinestacker.wordpress.com) • 📖 [Main documentation](https://shinestacker.readthedocs.io) • 📝 [Changelog](https://github.com/lucalista/shinestacker/blob/main/CHANGELOG.md)
|
|
94
|
-
|
|
95
|
-
|
|
96
96
|
# Credits
|
|
97
97
|
|
|
98
98
|
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.
|
|
@@ -38,6 +38,10 @@ The GUI has two main working areas:
|
|
|
38
38
|
|
|
39
39
|
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/gui-retouch.png' width="600" referrerpolicy="no-referrer">
|
|
40
40
|
|
|
41
|
+
# Resources
|
|
42
|
+
|
|
43
|
+
🌍 [Website on WordPress](https://shinestacker.wordpress.com) • 📖 [Main documentation](https://shinestacker.readthedocs.io) • 📝 [Changelog](https://github.com/lucalista/shinestacker/blob/main/CHANGELOG.md)
|
|
44
|
+
|
|
41
45
|
# Note for macOS users
|
|
42
46
|
|
|
43
47
|
**The following note is only relevant if you download the application as compressed archive from the [release page](https://github.com/lucalista/shinestacker/releases).**
|
|
@@ -57,11 +61,6 @@ xattr -cr ~/Downloads/shinestacker/shinestacker.app
|
|
|
57
61
|
|
|
58
62
|
macOS adds a quarantine flag to all files downloaded from the internet. The above command removes that flag while preserving all other application functionality.
|
|
59
63
|
|
|
60
|
-
# Resources
|
|
61
|
-
|
|
62
|
-
🌍 [Website on WordPress](https://shinestacker.wordpress.com) • 📖 [Main documentation](https://shinestacker.readthedocs.io) • 📝 [Changelog](https://github.com/lucalista/shinestacker/blob/main/CHANGELOG.md)
|
|
63
|
-
|
|
64
|
-
|
|
65
64
|
# Credits
|
|
66
65
|
|
|
67
66
|
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.
|
|
@@ -93,6 +93,14 @@ alignment_config = {
|
|
|
93
93
|
* ```plot_matches``` (optional, default: ```False```): if ```True```, for each image matches with reference frame are drawn. May be useful for inspection and debugging.
|
|
94
94
|
* ```enabled``` (optional, default: ```True```): allows to switch on and off this module.
|
|
95
95
|
|
|
96
|
+
## Parallel processing
|
|
97
|
+
|
|
98
|
+
A class ```AlignFramesParallel``` implements alignment using parallel processing.
|
|
99
|
+
This class has extra parameters, in addition to the above ones:
|
|
100
|
+
|
|
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
|
+
* ```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.
|
|
96
104
|
|
|
97
105
|
## Allowed configurations
|
|
98
106
|
|
|
@@ -48,6 +48,7 @@ Arguments for the constructor are, in addition to the ones for ```PyramidStack``
|
|
|
48
48
|
* ```tile_size``` (optional, default: 512): size of a time
|
|
49
49
|
* ```n_tiled_layers``` (optional, default: 2): number of layers that are tiled. Usually the last one or two are the ones that take more memory.
|
|
50
50
|
* ```max_threads``` (optional, default: number of cores, up to a maximum of 8): maximum number of thread used for parallel processing. The actual number of threads does not exceed the number of available cores.
|
|
51
|
+
* ```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.
|
|
51
52
|
|
|
52
53
|
|
|
53
54
|
```PyramidAutoStack```, pyramid algorithn with capability to automatically switch from all-in-memory to I/O buffered tiled.
|
|
@@ -41,6 +41,8 @@ Arguments for the constructor of ```CombinedActions``` are for the :
|
|
|
41
41
|
* ```working_path``` (optional): the directory that contains input and output image subdirectories. If not specified, it is the same as ```job.working_path```.
|
|
42
42
|
* ```plot_path``` (optional, default: ```plots```): the directory within ```working_path``` that contains plots produced by the different actions.
|
|
43
43
|
* ```resample``` (optional, default: 1): take every *n*<sup>th</sup> frame in the selected directory. Default: take all frames.
|
|
44
|
-
* ```
|
|
44
|
+
* ```reference_index``` (optional, default: 0): the index of the image used as reference. Images are numbered starting from one to N. -1 is interpreted as the last image, 0 as the median index.
|
|
45
45
|
* ```step_process``` (optional): if equal to ```True``` (default), each image is processed with respect to the previous or next image, depending if its file is placed in alphabetic order after or befor the reference image.
|
|
46
46
|
* ```enabled``` (optional, default: ```True```): allows to switch on and off this module.
|
|
47
|
+
* ```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.
|
|
48
|
+
* ```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.
|
|
@@ -87,7 +87,7 @@ job.run()
|
|
|
87
87
|
|
|
88
88
|
### Core processing
|
|
89
89
|
```bash
|
|
90
|
-
pip install imagecodecs matplotlib numpy opencv-python pillow psdtags scipy setuptools-scm tifffile tqdm
|
|
90
|
+
pip install imagecodecs matplotlib numpy opencv-python pillow psdtags psutil scipy setuptools-scm tifffile tqdm
|
|
91
91
|
```
|
|
92
92
|
## GUI support
|
|
93
93
|
```bash
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.3.0'
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E1101, R0914, R0913, R0917, R0912, R0915, R0902, E1121, W0102
|
|
2
|
-
import
|
|
2
|
+
import os
|
|
3
3
|
import math
|
|
4
|
+
import logging
|
|
4
5
|
import numpy as np
|
|
5
6
|
import matplotlib.pyplot as plt
|
|
6
7
|
import cv2
|
|
7
8
|
from .. config.constants import constants
|
|
8
|
-
from .. core.exceptions import
|
|
9
|
+
from .. core.exceptions import InvalidOptionError
|
|
9
10
|
from .. core.colors import color_str
|
|
10
11
|
from .utils import img_8bit, img_bw_8bit, save_plot, img_subsample
|
|
11
12
|
from .stack_framework import SubAction
|
|
@@ -20,7 +21,7 @@ _DEFAULT_MATCHING_CONFIG = {
|
|
|
20
21
|
'flann_idx_kdtree': constants.DEFAULT_FLANN_IDX_KDTREE,
|
|
21
22
|
'flann_trees': constants.DEFAULT_FLANN_TREES,
|
|
22
23
|
'flann_checks': constants.DEFAULT_FLANN_CHECKS,
|
|
23
|
-
'threshold': constants.DEFAULT_ALIGN_THRESHOLD
|
|
24
|
+
'threshold': constants.DEFAULT_ALIGN_THRESHOLD,
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
_DEFAULT_ALIGNMENT_CONFIG = {
|
|
@@ -130,7 +131,18 @@ def check_homography_distortion(m, img_shape, homography_thresholds=_HOMOGRAPHY_
|
|
|
130
131
|
return True, "Transformation within acceptable limits"
|
|
131
132
|
|
|
132
133
|
|
|
133
|
-
def
|
|
134
|
+
def check_transform(m, img_0, transform_type,
|
|
135
|
+
affine_thresholds, homography_thresholds):
|
|
136
|
+
if transform_type == constants.ALIGN_RIGID:
|
|
137
|
+
return check_affine_matrix(
|
|
138
|
+
m, img_0.shape, affine_thresholds)
|
|
139
|
+
if transform_type == constants.ALIGN_HOMOGRAPHY:
|
|
140
|
+
return check_homography_distortion(
|
|
141
|
+
m, img_0.shape, homography_thresholds)
|
|
142
|
+
return False, f'invalid transfrom option {transform_type}'
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_good_matches(des_0, des_ref, matching_config=None):
|
|
134
146
|
matching_config = {**_DEFAULT_MATCHING_CONFIG, **(matching_config or {})}
|
|
135
147
|
match_method = matching_config['match_method']
|
|
136
148
|
good_matches = []
|
|
@@ -139,12 +151,12 @@ def get_good_matches(des_0, des_1, matching_config=None):
|
|
|
139
151
|
{'algorithm': matching_config['flann_idx_kdtree'],
|
|
140
152
|
'trees': matching_config['flann_trees']},
|
|
141
153
|
{'checks': matching_config['flann_checks']})
|
|
142
|
-
matches = flann.knnMatch(des_0,
|
|
154
|
+
matches = flann.knnMatch(des_0, des_ref, k=2)
|
|
143
155
|
good_matches = [m for m, n in matches
|
|
144
156
|
if m.distance < matching_config['threshold'] * n.distance]
|
|
145
157
|
elif match_method == constants.MATCHING_NORM_HAMMING:
|
|
146
158
|
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
|
|
147
|
-
good_matches = sorted(bf.match(des_0,
|
|
159
|
+
good_matches = sorted(bf.match(des_0, des_ref), key=lambda x: x.distance)
|
|
148
160
|
else:
|
|
149
161
|
raise InvalidOptionError(
|
|
150
162
|
'match_method', match_method,
|
|
@@ -172,39 +184,42 @@ def validate_align_config(detector, descriptor, match_method):
|
|
|
172
184
|
" require matching method Hamming distance")
|
|
173
185
|
|
|
174
186
|
|
|
175
|
-
|
|
187
|
+
detector_map = {
|
|
188
|
+
constants.DETECTOR_SIFT: cv2.SIFT_create,
|
|
189
|
+
constants.DETECTOR_ORB: cv2.ORB_create,
|
|
190
|
+
constants.DETECTOR_SURF: cv2.FastFeatureDetector_create,
|
|
191
|
+
constants.DETECTOR_AKAZE: cv2.AKAZE_create,
|
|
192
|
+
constants.DETECTOR_BRISK: cv2.BRISK_create
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
descriptor_map = {
|
|
196
|
+
constants.DESCRIPTOR_SIFT: cv2.SIFT_create,
|
|
197
|
+
constants.DESCRIPTOR_ORB: cv2.ORB_create,
|
|
198
|
+
constants.DESCRIPTOR_AKAZE: cv2.AKAZE_create,
|
|
199
|
+
constants.DETECTOR_BRISK: cv2.BRISK_create
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def detect_and_compute_matches(img_ref, img_0, feature_config=None, matching_config=None):
|
|
176
204
|
feature_config = {**_DEFAULT_FEATURE_CONFIG, **(feature_config or {})}
|
|
177
205
|
matching_config = {**_DEFAULT_MATCHING_CONFIG, **(matching_config or {})}
|
|
178
206
|
feature_config_detector = feature_config['detector']
|
|
179
207
|
feature_config_descriptor = feature_config['descriptor']
|
|
180
208
|
match_method = matching_config['match_method']
|
|
181
209
|
validate_align_config(feature_config_detector, feature_config_descriptor, match_method)
|
|
182
|
-
img_bw_0,
|
|
183
|
-
detector_map = {
|
|
184
|
-
constants.DETECTOR_SIFT: cv2.SIFT_create,
|
|
185
|
-
constants.DETECTOR_ORB: cv2.ORB_create,
|
|
186
|
-
constants.DETECTOR_SURF: cv2.FastFeatureDetector_create,
|
|
187
|
-
constants.DETECTOR_AKAZE: cv2.AKAZE_create,
|
|
188
|
-
constants.DETECTOR_BRISK: cv2.BRISK_create
|
|
189
|
-
}
|
|
190
|
-
descriptor_map = {
|
|
191
|
-
constants.DESCRIPTOR_SIFT: cv2.SIFT_create,
|
|
192
|
-
constants.DESCRIPTOR_ORB: cv2.ORB_create,
|
|
193
|
-
constants.DESCRIPTOR_AKAZE: cv2.AKAZE_create,
|
|
194
|
-
constants.DETECTOR_BRISK: cv2.BRISK_create
|
|
195
|
-
}
|
|
210
|
+
img_bw_0, img_bw_ref = img_bw_8bit(img_0), img_bw_8bit(img_ref)
|
|
196
211
|
detector = detector_map[feature_config_detector]()
|
|
197
212
|
if feature_config_detector == feature_config_descriptor and \
|
|
198
213
|
feature_config_detector in (constants.DETECTOR_SIFT,
|
|
199
214
|
constants.DETECTOR_AKAZE,
|
|
200
215
|
constants.DETECTOR_BRISK):
|
|
201
216
|
kp_0, des_0 = detector.detectAndCompute(img_bw_0, None)
|
|
202
|
-
|
|
217
|
+
kp_ref, des_ref = detector.detectAndCompute(img_bw_ref, None)
|
|
203
218
|
else:
|
|
204
219
|
descriptor = descriptor_map[feature_config_descriptor]()
|
|
205
220
|
kp_0, des_0 = descriptor.compute(img_bw_0, detector.detect(img_bw_0, None))
|
|
206
|
-
|
|
207
|
-
return kp_0,
|
|
221
|
+
kp_ref, des_ref = descriptor.compute(img_bw_ref, detector.detect(img_bw_ref, None))
|
|
222
|
+
return kp_0, kp_ref, get_good_matches(des_0, des_ref, matching_config)
|
|
208
223
|
|
|
209
224
|
|
|
210
225
|
def find_transform(src_pts, dst_pts, transform=constants.DEFAULT_TRANSFORM,
|
|
@@ -236,7 +251,26 @@ def find_transform(src_pts, dst_pts, transform=constants.DEFAULT_TRANSFORM,
|
|
|
236
251
|
return result
|
|
237
252
|
|
|
238
253
|
|
|
239
|
-
def
|
|
254
|
+
def rescale_trasnsform(m, w0, h0, w_sub, h_sub, subsample, transform):
|
|
255
|
+
if transform == constants.ALIGN_HOMOGRAPHY:
|
|
256
|
+
low_size = np.float32([[0, 0], [0, h_sub], [w_sub, h_sub], [w_sub, 0]])
|
|
257
|
+
high_size = np.float32([[0, 0], [0, h0], [w0, h0], [w0, 0]])
|
|
258
|
+
scale_up = cv2.getPerspectiveTransform(low_size, high_size)
|
|
259
|
+
scale_down = cv2.getPerspectiveTransform(high_size, low_size)
|
|
260
|
+
m = scale_up @ m @ scale_down
|
|
261
|
+
elif transform == constants.ALIGN_RIGID:
|
|
262
|
+
rotation = m[:2, :2]
|
|
263
|
+
translation = m[:, 2]
|
|
264
|
+
translation_fullres = translation * subsample
|
|
265
|
+
m = np.empty((2, 3), dtype=np.float32)
|
|
266
|
+
m[:2, :2] = rotation
|
|
267
|
+
m[:, 2] = translation_fullres
|
|
268
|
+
else:
|
|
269
|
+
return 0
|
|
270
|
+
return m
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def align_images(img_ref, img_0, feature_config=None, matching_config=None, alignment_config=None,
|
|
240
274
|
plot_path=None, callbacks=None,
|
|
241
275
|
affine_thresholds=_AFFINE_THRESHOLDS,
|
|
242
276
|
homography_thresholds=_HOMOGRAPHY_THRESHOLDS):
|
|
@@ -250,11 +284,11 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
|
|
|
250
284
|
min_matches = 4 if alignment_config['transform'] == constants.ALIGN_HOMOGRAPHY else 3
|
|
251
285
|
if callbacks and 'message' in callbacks:
|
|
252
286
|
callbacks['message']()
|
|
253
|
-
h_ref, w_ref =
|
|
287
|
+
h_ref, w_ref = img_ref.shape[:2]
|
|
254
288
|
h0, w0 = img_0.shape[:2]
|
|
255
289
|
subsample = alignment_config['subsample']
|
|
256
290
|
if subsample == 0:
|
|
257
|
-
img_res = (float(h0) /
|
|
291
|
+
img_res = (float(h0) / constants.ONE_KILO) * (float(w0) / constants.ONE_KILO)
|
|
258
292
|
target_res = constants.DEFAULT_ALIGN_RES_TARGET_MPX
|
|
259
293
|
subsample = int(1 + math.floor(img_res / target_res))
|
|
260
294
|
fast_subsampling = alignment_config['fast_subsampling']
|
|
@@ -262,11 +296,11 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
|
|
|
262
296
|
while True:
|
|
263
297
|
if subsample > 1:
|
|
264
298
|
img_0_sub = img_subsample(img_0, subsample, fast_subsampling)
|
|
265
|
-
|
|
299
|
+
img_ref_sub = img_subsample(img_ref, subsample, fast_subsampling)
|
|
266
300
|
else:
|
|
267
|
-
img_0_sub,
|
|
268
|
-
kp_0,
|
|
269
|
-
|
|
301
|
+
img_0_sub, img_ref_sub = img_0, img_ref
|
|
302
|
+
kp_0, kp_ref, good_matches = detect_and_compute_matches(
|
|
303
|
+
img_ref_sub, img_0_sub, feature_config, matching_config)
|
|
270
304
|
n_good_matches = len(good_matches)
|
|
271
305
|
if n_good_matches > min_good_matches or subsample == 1:
|
|
272
306
|
break
|
|
@@ -282,7 +316,7 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
|
|
|
282
316
|
if n_good_matches >= min_matches:
|
|
283
317
|
transform = alignment_config['transform']
|
|
284
318
|
src_pts = np.float32([kp_0[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
|
|
285
|
-
dst_pts = np.float32([
|
|
319
|
+
dst_pts = np.float32([kp_ref[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)
|
|
286
320
|
m, msk = find_transform(src_pts, dst_pts, transform, alignment_config['align_method'],
|
|
287
321
|
*(alignment_config[k]
|
|
288
322
|
for k in ['rans_threshold', 'max_iters',
|
|
@@ -290,47 +324,30 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
|
|
|
290
324
|
if plot_path is not None:
|
|
291
325
|
matches_mask = msk.ravel().tolist()
|
|
292
326
|
img_match = cv2.cvtColor(cv2.drawMatches(
|
|
293
|
-
img_8bit(img_0_sub), kp_0, img_8bit(
|
|
294
|
-
|
|
327
|
+
img_8bit(img_0_sub), kp_0, img_8bit(img_ref_sub),
|
|
328
|
+
kp_ref, good_matches, None, matchColor=(0, 255, 0),
|
|
295
329
|
singlePointColor=None, matchesMask=matches_mask,
|
|
296
330
|
flags=2), cv2.COLOR_BGR2RGB)
|
|
297
|
-
plt.figure(figsize=
|
|
331
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
298
332
|
plt.imshow(img_match, 'gray')
|
|
299
333
|
save_plot(plot_path)
|
|
300
334
|
if callbacks and 'save_plot' in callbacks:
|
|
301
335
|
callbacks['save_plot'](plot_path)
|
|
302
336
|
h_sub, w_sub = img_0_sub.shape[:2]
|
|
303
337
|
if subsample > 1:
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
high_size = np.float32([[0, 0], [0, h0], [w0, h0], [w0, 0]])
|
|
307
|
-
scale_up = cv2.getPerspectiveTransform(low_size, high_size)
|
|
308
|
-
scale_down = cv2.getPerspectiveTransform(high_size, low_size)
|
|
309
|
-
m = scale_up @ m @ scale_down
|
|
310
|
-
elif transform == constants.ALIGN_RIGID:
|
|
311
|
-
rotation = m[:2, :2]
|
|
312
|
-
translation = m[:, 2]
|
|
313
|
-
translation_fullres = translation * subsample
|
|
314
|
-
m = np.empty((2, 3), dtype=np.float32)
|
|
315
|
-
m[:2, :2] = rotation
|
|
316
|
-
m[:, 2] = translation_fullres
|
|
317
|
-
else:
|
|
338
|
+
m = rescale_trasnsform(m, w0, h0, w_sub, h_sub, subsample, transform)
|
|
339
|
+
if m is None:
|
|
318
340
|
raise InvalidOptionError("transform", transform)
|
|
319
|
-
|
|
320
341
|
transform_type = alignment_config['transform']
|
|
321
|
-
is_valid =
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
is_valid, reason = check_affine_matrix(
|
|
325
|
-
m, img_0.shape, affine_thresholds)
|
|
326
|
-
elif transform_type == constants.ALIGN_HOMOGRAPHY:
|
|
327
|
-
is_valid, reason = check_homography_distortion(
|
|
328
|
-
m, img_0.shape, homography_thresholds)
|
|
342
|
+
is_valid, reason = check_transform(
|
|
343
|
+
m, img_0, transform_type,
|
|
344
|
+
affine_thresholds, homography_thresholds)
|
|
329
345
|
if not is_valid:
|
|
330
346
|
if callbacks and 'warning' in callbacks:
|
|
331
347
|
callbacks['warning'](f"invalid transformation: {reason}")
|
|
348
|
+
if alignment_config['abort_abnormal']:
|
|
349
|
+
raise RuntimeError("invalid transformation: {reason}")
|
|
332
350
|
return n_good_matches, None, None
|
|
333
|
-
|
|
334
351
|
if callbacks and 'align_message' in callbacks:
|
|
335
352
|
callbacks['align_message']()
|
|
336
353
|
img_mask = np.ones_like(img_0, dtype=np.uint8)
|
|
@@ -358,12 +375,12 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
|
|
|
358
375
|
return n_good_matches, m, img_warp
|
|
359
376
|
|
|
360
377
|
|
|
361
|
-
class
|
|
378
|
+
class AlignFramesBase(SubAction):
|
|
362
379
|
def __init__(self, enabled=True, feature_config=None, matching_config=None,
|
|
363
380
|
alignment_config=None, **kwargs):
|
|
364
381
|
super().__init__(enabled)
|
|
365
382
|
self.process = None
|
|
366
|
-
self.
|
|
383
|
+
self._n_good_matches = None
|
|
367
384
|
self.feature_config = {**_DEFAULT_FEATURE_CONFIG, **(feature_config or {})}
|
|
368
385
|
self.matching_config = {**_DEFAULT_MATCHING_CONFIG, **(matching_config or {})}
|
|
369
386
|
self.alignment_config = {**_DEFAULT_ALIGNMENT_CONFIG, **(alignment_config or {})}
|
|
@@ -381,27 +398,78 @@ class AlignFrames(SubAction):
|
|
|
381
398
|
if k in kwargs:
|
|
382
399
|
self.alignment_config[k] = kwargs[k]
|
|
383
400
|
|
|
401
|
+
def align_images(self, idx, img_ref, img_0):
|
|
402
|
+
pass
|
|
403
|
+
|
|
404
|
+
def print_message(self, msg, color=constants.LOG_COLOR_LEVEL_3, level=logging.INFO):
|
|
405
|
+
self.process.print_message(color_str(msg, color), level=level)
|
|
406
|
+
|
|
407
|
+
def begin(self, process):
|
|
408
|
+
self.process = process
|
|
409
|
+
self._n_good_matches = np.zeros(process.total_action_counts)
|
|
410
|
+
|
|
384
411
|
def run_frame(self, idx, ref_idx, img_0):
|
|
385
412
|
if idx == self.process.ref_idx:
|
|
386
413
|
return img_0
|
|
387
414
|
img_ref = self.process.img_ref(ref_idx)
|
|
388
415
|
return self.align_images(idx, img_ref, img_0)
|
|
389
416
|
|
|
390
|
-
def
|
|
391
|
-
|
|
417
|
+
def get_transform_thresholds(self):
|
|
418
|
+
return _AFFINE_THRESHOLDS, _HOMOGRAPHY_THRESHOLDS
|
|
419
|
+
|
|
420
|
+
def image_str(self, idx):
|
|
421
|
+
return f"image: {self.process.idx_tot_str(idx)}, " \
|
|
422
|
+
f"{os.path.basename(self.process.input_filepath(idx))}"
|
|
423
|
+
|
|
424
|
+
def end(self):
|
|
425
|
+
if self.plot_summary:
|
|
426
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
427
|
+
x = np.arange(1, len(self._n_good_matches) + 1, dtype=int)
|
|
428
|
+
no_ref = x != self.process.ref_idx + 1
|
|
429
|
+
x = x[no_ref]
|
|
430
|
+
y = self._n_good_matches[no_ref]
|
|
431
|
+
if self.process.ref_idx == 0:
|
|
432
|
+
y_max = y[1]
|
|
433
|
+
elif self.process.ref_idx >= len(y):
|
|
434
|
+
y_max = y[-1]
|
|
435
|
+
else:
|
|
436
|
+
y_max = (y[self.process.ref_idx - 1] + y[self.process.ref_idx]) / 2
|
|
437
|
+
|
|
438
|
+
plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
|
|
439
|
+
[0, y_max], color='cornflowerblue', linestyle='--', label='reference frame')
|
|
440
|
+
plt.plot([x[0], x[-1]], [self.min_matches, self.min_matches], color='lightgray',
|
|
441
|
+
linestyle='--', label='min. matches')
|
|
442
|
+
plt.plot(x, y, color='navy', label='matches')
|
|
443
|
+
plt.xlabel('frame')
|
|
444
|
+
plt.ylabel('# of matches')
|
|
445
|
+
plt.legend()
|
|
446
|
+
plt.ylim(0)
|
|
447
|
+
plt.xlim(x[0], x[-1])
|
|
448
|
+
plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
|
|
449
|
+
f"{self.process.name}-matches.pdf"
|
|
450
|
+
save_plot(plot_path)
|
|
451
|
+
plt.close('all')
|
|
452
|
+
self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
|
|
453
|
+
f"{self.process.name}: matches", plot_path)
|
|
454
|
+
|
|
392
455
|
|
|
393
|
-
|
|
456
|
+
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
|
+
def align_images(self, idx, img_ref, img_0):
|
|
394
462
|
idx_str = f"{idx:04d}"
|
|
463
|
+
idx_tot_str = self.process.idx_tot_str(idx)
|
|
395
464
|
callbacks = {
|
|
396
|
-
'message': lambda: self.
|
|
397
|
-
'matches_message': lambda n: self.
|
|
398
|
-
'align_message': lambda: self.
|
|
399
|
-
'
|
|
400
|
-
'
|
|
401
|
-
'warning': lambda msg: self.sub_msg(
|
|
465
|
+
'message': lambda: self.print_message(f'{idx_tot_str}: find matches'),
|
|
466
|
+
'matches_message': lambda n: self.print_message(f'{idx_tot_str}: good matches: {n}'),
|
|
467
|
+
'align_message': lambda: self.print_message(f'{idx_tot_str}: align images'),
|
|
468
|
+
'blur_message': lambda: self.print_message(f'{idx_tot_str}: blur borders'),
|
|
469
|
+
'warning': lambda msg: self.print_message(
|
|
402
470
|
f': {msg}', constants.LOG_COLOR_WARNING),
|
|
403
471
|
'save_plot': lambda plot_path: self.process.callback(
|
|
404
|
-
|
|
472
|
+
constants.CALLBACK_SAVE_PLOT, self.process.id,
|
|
405
473
|
f"{self.process.name}: matches\nframe {idx_str}", plot_path)
|
|
406
474
|
}
|
|
407
475
|
if self.plot_matches:
|
|
@@ -409,14 +477,9 @@ class AlignFrames(SubAction):
|
|
|
409
477
|
f"{self.process.name}-matches-{idx_str}.pdf"
|
|
410
478
|
else:
|
|
411
479
|
plot_path = None
|
|
412
|
-
|
|
413
|
-
affine_thresholds = _AFFINE_THRESHOLDS
|
|
414
|
-
homography_thresholds = _HOMOGRAPHY_THRESHOLDS
|
|
415
|
-
else:
|
|
416
|
-
affine_thresholds = None
|
|
417
|
-
homography_thresholds = None
|
|
480
|
+
affine_thresholds, homography_thresholds = self.get_transform_thresholds()
|
|
418
481
|
n_good_matches, _m, img = align_images(
|
|
419
|
-
|
|
482
|
+
img_ref, img_0,
|
|
420
483
|
feature_config=self.feature_config,
|
|
421
484
|
matching_config=self.matching_config,
|
|
422
485
|
alignment_config=self.alignment_config,
|
|
@@ -425,43 +488,13 @@ class AlignFrames(SubAction):
|
|
|
425
488
|
affine_thresholds=affine_thresholds,
|
|
426
489
|
homography_thresholds=homography_thresholds
|
|
427
490
|
)
|
|
428
|
-
self.
|
|
491
|
+
self._n_good_matches[idx] = n_good_matches
|
|
429
492
|
if n_good_matches < self.min_matches:
|
|
430
|
-
self.process.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
493
|
+
self.process.print_message(
|
|
494
|
+
f"{self.image_str(idx)} not aligned, too few matches found: "
|
|
495
|
+
f"{n_good_matches}")
|
|
496
|
+
return None
|
|
434
497
|
return img
|
|
435
498
|
|
|
436
|
-
def
|
|
437
|
-
|
|
438
|
-
self.n_matches = np.zeros(process.counts)
|
|
439
|
-
|
|
440
|
-
def end(self):
|
|
441
|
-
if self.plot_summary:
|
|
442
|
-
plt.figure(figsize=(10, 5))
|
|
443
|
-
x = np.arange(1, len(self.n_matches) + 1, dtype=int)
|
|
444
|
-
no_ref = x != self.process.ref_idx + 1
|
|
445
|
-
x = x[no_ref]
|
|
446
|
-
y = self.n_matches[no_ref]
|
|
447
|
-
y_max = y[1] \
|
|
448
|
-
if self.process.ref_idx == 0 \
|
|
449
|
-
else y[-1] if self.process.ref_idx == len(y) - 1 \
|
|
450
|
-
else (y[self.process.ref_idx - 1] + y[self.process.ref_idx]) / 2
|
|
451
|
-
|
|
452
|
-
plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
|
|
453
|
-
[0, y_max], color='cornflowerblue', linestyle='--', label='reference frame')
|
|
454
|
-
plt.plot([x[0], x[-1]], [self.min_matches, self.min_matches], color='lightgray',
|
|
455
|
-
linestyle='--', label='min. matches')
|
|
456
|
-
plt.plot(x, y, color='navy', label='matches')
|
|
457
|
-
plt.xlabel('frame')
|
|
458
|
-
plt.ylabel('# of matches')
|
|
459
|
-
plt.legend()
|
|
460
|
-
plt.ylim(0)
|
|
461
|
-
plt.xlim(x[0], x[-1])
|
|
462
|
-
plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
|
|
463
|
-
f"{self.process.name}-matches.pdf"
|
|
464
|
-
save_plot(plot_path)
|
|
465
|
-
plt.close('all')
|
|
466
|
-
self.process.callback('save_plot', self.process.id,
|
|
467
|
-
f"{self.process.name}: matches", plot_path)
|
|
499
|
+
def sequential_processing(self):
|
|
500
|
+
return True
|