shinestacker 1.3.0__tar.gz → 1.3.1__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.3.1}/CHANGELOG.md +15 -1
- {shinestacker-1.3.0/src/shinestacker.egg-info → shinestacker-1.3.1}/PKG-INFO +1 -1
- {shinestacker-1.3.0 → shinestacker-1.3.1}/THIRD_PARTY_LICENSES.txt +6 -13
- {shinestacker-1.3.0 → shinestacker-1.3.1}/docs/alignment.md +7 -0
- shinestacker-1.3.1/index.html +47 -0
- shinestacker-1.3.1/src/shinestacker/_version.py +1 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/algorithms/align.py +35 -27
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/algorithms/align_auto.py +15 -3
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/algorithms/align_parallel.py +65 -25
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/algorithms/base_stack_algo.py +14 -20
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/algorithms/depth_map.py +9 -14
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/algorithms/pyramid.py +8 -22
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/algorithms/pyramid_auto.py +5 -14
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/algorithms/pyramid_tiles.py +18 -20
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/algorithms/stack_framework.py +1 -1
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/algorithms/utils.py +16 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/app/gui_utils.py +10 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/app/main.py +3 -1
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/app/project.py +3 -1
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/app/retouch.py +3 -1
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/action_config_dialog.py +302 -272
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/colors.py +1 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/folder_file_selection.py +5 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/main_window.py +4 -4
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/new_project.py +5 -5
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/project_editor.py +6 -4
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/sys_mon.py +24 -23
- {shinestacker-1.3.0 → shinestacker-1.3.1/src/shinestacker.egg-info}/PKG-INFO +1 -1
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker.egg-info/SOURCES.txt +1 -0
- shinestacker-1.3.0/src/shinestacker/_version.py +0 -1
- {shinestacker-1.3.0 → shinestacker-1.3.1}/.coveragerc +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/.flake8 +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/.github/workflows/ci-multiplatform.yml +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/.github/workflows/pylint.yml +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/.github/workflows/pypi-publish.yml +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/.github/workflows/release.yml +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/.gitignore +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/.pylintrc +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/.readthedocs.yaml +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/LICENSE +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/MANIFEST.in +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/README.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/docs/api.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/docs/balancing.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/docs/conf.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/docs/focus_stacking.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/docs/gui.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/docs/index.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/docs/job.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/docs/main.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/docs/multilayer.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/docs/noise.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/docs/requirements.txt +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/docs/vignetting.md +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/img/coffee.gif +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/img/coffee_stack.jpg +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/img/extreme-vignetting.jpg +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/img/flies.gif +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/img/flies_stack.jpg +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/img/flow-diagram.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/img/gui-finder.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/img/gui-project-new.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/img/gui-project-run.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/img/gui-retouch.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/pyproject.toml +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/requirements.txt +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/scripts/build_release.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/scripts/git-rev-list.sh +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/scripts/validate-tomli.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/setup.cfg +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/__init__.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/algorithms/__init__.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/algorithms/balance.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/algorithms/denoise.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/algorithms/exif.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/algorithms/multilayer.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/algorithms/noise_detection.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/algorithms/sharpen.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/algorithms/stack.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/algorithms/vignetting.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/algorithms/white_balance.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/app/__init__.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/app/about_dialog.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/app/help_menu.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/app/open_frames.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/config/__init__.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/config/config.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/config/constants.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/config/gui_constants.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/core/__init__.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/core/colors.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/core/core_utils.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/core/exceptions.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/core/framework.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/core/logging.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/__init__.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/action_config.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/base_form_dialog.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/flow_layout.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/gui_images.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/gui_logging.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/gui_run.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/ico/shinestacker.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/ico/shinestacker.svg +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/menu_manager.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/project_controller.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/project_converter.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/project_model.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/select_path_widget.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/tab_widget.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/gui/time_progress_bar.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/__init__.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/base_filter.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/brush.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/brush_gradient.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/brush_preview.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/brush_tool.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/denoise_filter.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/display_manager.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/exif_data.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/file_loader.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/filter_manager.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/icon_container.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/image_editor_ui.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/image_viewer.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/io_gui_handler.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/io_manager.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/layer_collection.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/shortcuts_help.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/undo_manager.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/unsharp_mask_filter.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/vignetting_filter.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker/retouch/white_balance_filter.py +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker.egg-info/dependency_links.txt +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker.egg-info/entry_points.txt +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker.egg-info/requires.txt +0 -0
- {shinestacker-1.3.0 → shinestacker-1.3.1}/src/shinestacker.egg-info/top_level.txt +0 -0
|
@@ -2,8 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
This page reports the main releases only and the main changes therein.
|
|
4
4
|
|
|
5
|
+
## [v1.3.1] - 2025-09-08
|
|
5
6
|
|
|
6
|
-
##
|
|
7
|
+
## Fixed
|
|
8
|
+
- fixed input folder widget in job configuration
|
|
9
|
+
- better management of patological alignments
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- improved automatic parameters for parallel alignment
|
|
13
|
+
- improved pyramid performances by combining two input steps
|
|
14
|
+
- improved performances of ORB and SURF feature extraction with key points caching
|
|
15
|
+
- improved configuration GUI using tabs and other minor GUI improvements
|
|
16
|
+
- code clean up and some fixes
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## [v1.3.0] - 2025-09-06
|
|
7
21
|
**Parallel processing and input flexibility**
|
|
8
22
|
|
|
9
23
|
### 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.
|
|
@@ -127,3 +127,10 @@ give raise to an exception.
|
|
|
127
127
|
| 0.2887 | AKAZE | BRISK | NORM_HAMMING |
|
|
128
128
|
| 0.4075 | AKAZE | SIFT | KNN |
|
|
129
129
|
| 0.4397 | SIFT | SIFT | KNN |
|
|
130
|
+
|
|
131
|
+
## References
|
|
132
|
+
|
|
133
|
+
For a detailed review of the various image registration methods, see the publication below:
|
|
134
|
+
* [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
|
|
135
|
+
|
|
136
|
+
|
|
@@ -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.3.1'
|
|
@@ -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
|
+
import matplotlib
|
|
8
9
|
from .. config.constants import constants
|
|
9
10
|
from .. core.exceptions import InvalidOptionError
|
|
10
11
|
from .. core.colors import color_str
|
|
11
12
|
from .utils import img_8bit, img_bw_8bit, save_plot, img_subsample
|
|
12
13
|
from .stack_framework import SubAction
|
|
14
|
+
matplotlib.use('Agg')
|
|
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,8 +130,9 @@ 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
138
|
def check_transform(m, img_0, transform_type,
|
|
@@ -139,7 +143,7 @@ def check_transform(m, img_0, transform_type,
|
|
|
139
143
|
if transform_type == constants.ALIGN_HOMOGRAPHY:
|
|
140
144
|
return check_homography_distortion(
|
|
141
145
|
m, img_0.shape, homography_thresholds)
|
|
142
|
-
return False, f'invalid transfrom option {transform_type}'
|
|
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):
|
|
@@ -270,6 +274,18 @@ def rescale_trasnsform(m, w0, h0, w_sub, h_sub, subsample, transform):
|
|
|
270
274
|
return m
|
|
271
275
|
|
|
272
276
|
|
|
277
|
+
def plot_matches(msk, img_ref_sub, img_0_sub, kp_ref, kp_0, good_matches, plot_path):
|
|
278
|
+
matches_mask = msk.ravel().tolist()
|
|
279
|
+
img_match = cv2.cvtColor(cv2.drawMatches(
|
|
280
|
+
img_8bit(img_0_sub), kp_0, img_8bit(img_ref_sub),
|
|
281
|
+
kp_ref, good_matches, None, matchColor=(0, 255, 0),
|
|
282
|
+
singlePointColor=None, matchesMask=matches_mask,
|
|
283
|
+
flags=2), cv2.COLOR_BGR2RGB)
|
|
284
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
285
|
+
plt.imshow(img_match, 'gray')
|
|
286
|
+
save_plot(plot_path)
|
|
287
|
+
|
|
288
|
+
|
|
273
289
|
def align_images(img_ref, img_0, feature_config=None, matching_config=None, alignment_config=None,
|
|
274
290
|
plot_path=None, callbacks=None,
|
|
275
291
|
affine_thresholds=_AFFINE_THRESHOLDS,
|
|
@@ -315,22 +331,16 @@ def align_images(img_ref, img_0, feature_config=None, matching_config=None, alig
|
|
|
315
331
|
m = None
|
|
316
332
|
if n_good_matches >= min_matches:
|
|
317
333
|
transform = alignment_config['transform']
|
|
318
|
-
src_pts = np.float32(
|
|
319
|
-
|
|
334
|
+
src_pts = np.float32(
|
|
335
|
+
[kp_0[match.queryIdx].pt for match in good_matches]).reshape(-1, 1, 2)
|
|
336
|
+
dst_pts = np.float32(
|
|
337
|
+
[kp_ref[match.trainIdx].pt for match in good_matches]).reshape(-1, 1, 2)
|
|
320
338
|
m, msk = find_transform(src_pts, dst_pts, transform, alignment_config['align_method'],
|
|
321
339
|
*(alignment_config[k]
|
|
322
340
|
for k in ['rans_threshold', 'max_iters',
|
|
323
341
|
'align_confidence', 'refine_iters']))
|
|
324
342
|
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)
|
|
343
|
+
plot_matches(msk, img_ref_sub, img_0_sub, kp_ref, kp_0, good_matches, plot_path)
|
|
334
344
|
if callbacks and 'save_plot' in callbacks:
|
|
335
345
|
callbacks['save_plot'](plot_path)
|
|
336
346
|
h_sub, w_sub = img_0_sub.shape[:2]
|
|
@@ -339,7 +349,7 @@ def align_images(img_ref, img_0, feature_config=None, matching_config=None, alig
|
|
|
339
349
|
if m is None:
|
|
340
350
|
raise InvalidOptionError("transform", transform)
|
|
341
351
|
transform_type = alignment_config['transform']
|
|
342
|
-
is_valid, reason = check_transform(
|
|
352
|
+
is_valid, reason, _result = check_transform(
|
|
343
353
|
m, img_0, transform_type,
|
|
344
354
|
affine_thresholds, homography_thresholds)
|
|
345
355
|
if not is_valid:
|
|
@@ -427,7 +437,7 @@ class AlignFramesBase(SubAction):
|
|
|
427
437
|
x = np.arange(1, len(self._n_good_matches) + 1, dtype=int)
|
|
428
438
|
no_ref = x != self.process.ref_idx + 1
|
|
429
439
|
x = x[no_ref]
|
|
430
|
-
y = self._n_good_matches[no_ref]
|
|
440
|
+
y = np.array(self._n_good_matches)[no_ref]
|
|
431
441
|
if self.process.ref_idx == 0:
|
|
432
442
|
y_max = y[1]
|
|
433
443
|
elif self.process.ref_idx >= len(y):
|
|
@@ -454,10 +464,6 @@ class AlignFramesBase(SubAction):
|
|
|
454
464
|
|
|
455
465
|
|
|
456
466
|
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
467
|
def align_images(self, idx, img_ref, img_0):
|
|
462
468
|
idx_str = f"{idx:04d}"
|
|
463
469
|
idx_tot_str = self.process.idx_tot_str(idx)
|
|
@@ -473,8 +479,10 @@ class AlignFrames(AlignFramesBase):
|
|
|
473
479
|
f"{self.process.name}: matches\nframe {idx_str}", plot_path)
|
|
474
480
|
}
|
|
475
481
|
if self.plot_matches:
|
|
476
|
-
plot_path =
|
|
477
|
-
|
|
482
|
+
plot_path = os.path.join(
|
|
483
|
+
self.process.working_path,
|
|
484
|
+
self.process.plot_path,
|
|
485
|
+
f"{self.process.name}-matches-{idx_str}.pdf")
|
|
478
486
|
else:
|
|
479
487
|
plot_path = None
|
|
480
488
|
affine_thresholds, homography_thresholds = self.get_transform_thresholds()
|
|
@@ -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
|
|
@@ -12,9 +12,11 @@ from ..config.constants import constants
|
|
|
12
12
|
from .. core.exceptions import InvalidOptionError, RunStopException
|
|
13
13
|
from .. core.colors import color_str
|
|
14
14
|
from .. core.core_utils import make_chunks
|
|
15
|
-
from .utils import read_img, img_subsample, img_bw
|
|
16
|
-
from .align import (AlignFramesBase,
|
|
17
|
-
check_transform, _cv2_border_mode_map, rescale_trasnsform
|
|
15
|
+
from .utils import read_img, img_subsample, img_bw, img_bw_8bit
|
|
16
|
+
from .align import (AlignFramesBase, find_transform,
|
|
17
|
+
check_transform, _cv2_border_mode_map, rescale_trasnsform,
|
|
18
|
+
validate_align_config, detector_map, descriptor_map,
|
|
19
|
+
get_good_matches)
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
def compose_transforms(t1, t2, transform_type):
|
|
@@ -31,8 +33,8 @@ def compose_transforms(t1, t2, transform_type):
|
|
|
31
33
|
class AlignFramesParallel(AlignFramesBase):
|
|
32
34
|
def __init__(self, enabled=True, feature_config=None, matching_config=None,
|
|
33
35
|
alignment_config=None, **kwargs):
|
|
34
|
-
super().__init__(enabled
|
|
35
|
-
alignment_config
|
|
36
|
+
super().__init__(enabled, feature_config, matching_config,
|
|
37
|
+
alignment_config, **kwargs)
|
|
36
38
|
self.max_threads = kwargs.get('max_threads', constants.DEFAULT_ALIGN_MAX_THREADS)
|
|
37
39
|
self.chunk_submit = kwargs.get('chunk_submit', constants.DEFAULT_ALIGN_CHUNK_SUBMIT)
|
|
38
40
|
self.bw_matching = kwargs.get('bw_matching', constants.DEFAULT_ALIGN_BW_MATCHING)
|
|
@@ -43,6 +45,8 @@ class AlignFramesParallel(AlignFramesBase):
|
|
|
43
45
|
self._transforms = None
|
|
44
46
|
self._cumulative_transforms = None
|
|
45
47
|
self.step_counter = 0
|
|
48
|
+
self._kp = None
|
|
49
|
+
self._des = None
|
|
46
50
|
|
|
47
51
|
def cache_img(self, idx):
|
|
48
52
|
with self._cache_locks[idx]:
|
|
@@ -98,10 +102,14 @@ class AlignFramesParallel(AlignFramesBase):
|
|
|
98
102
|
|
|
99
103
|
def begin(self, process):
|
|
100
104
|
super().begin(process)
|
|
105
|
+
if self.plot_matches:
|
|
106
|
+
self.print_message(
|
|
107
|
+
"requested plot matches is not supported with parallel processing",
|
|
108
|
+
color=constants.LOG_COLOR_WARNING, level=logging.WARNING)
|
|
101
109
|
n_frames = self.process.num_input_filepaths()
|
|
110
|
+
self.print_message(f"preprocess {n_frames} images in parallel, cores: {self.max_threads}")
|
|
102
111
|
self.process.callback(constants.CALLBACK_STEP_COUNTS,
|
|
103
112
|
self.process.id, self.process.name, 2 * n_frames)
|
|
104
|
-
self.print_message(f"preprocess {n_frames} images in parallel, cores: {self.max_threads}")
|
|
105
113
|
input_filepaths = self.process.input_filepaths()
|
|
106
114
|
self._img_cache = [None] * n_frames
|
|
107
115
|
self._img_locks = [0] * n_frames
|
|
@@ -110,6 +118,8 @@ class AlignFramesParallel(AlignFramesBase):
|
|
|
110
118
|
self._n_good_matches = [0] * n_frames
|
|
111
119
|
self._transforms = [None] * n_frames
|
|
112
120
|
self._cumulative_transforms = [None] * n_frames
|
|
121
|
+
self._kp = [None] * n_frames
|
|
122
|
+
self._des = [None] * n_frames
|
|
113
123
|
max_chunck_size = self.max_threads
|
|
114
124
|
ref_idx = self.process.ref_idx
|
|
115
125
|
self.print_message(f"reference: {self.image_str(ref_idx)}")
|
|
@@ -125,9 +135,11 @@ class AlignFramesParallel(AlignFramesBase):
|
|
|
125
135
|
self.submit_threads(idxs, imgs)
|
|
126
136
|
else:
|
|
127
137
|
self.submit_threads(sub_indices, sub_img_filepaths)
|
|
128
|
-
for
|
|
129
|
-
if self._img_cache[
|
|
130
|
-
self._img_cache[
|
|
138
|
+
for idx in range(n_frames):
|
|
139
|
+
if self._img_cache[idx] is not None:
|
|
140
|
+
self._img_cache[idx] = None
|
|
141
|
+
self._kp[idx] = None
|
|
142
|
+
self._des[idx] = None
|
|
131
143
|
gc.collect()
|
|
132
144
|
self.print_message("combining transformations")
|
|
133
145
|
transform_type = self.alignment_config['transform']
|
|
@@ -152,6 +164,9 @@ class AlignFramesParallel(AlignFramesBase):
|
|
|
152
164
|
self.print_message(
|
|
153
165
|
f"warning: no cumulative transform for {self.image_str(i)}",
|
|
154
166
|
color=constants.LOG_COLOR_WARNING, level=logging.WARNING)
|
|
167
|
+
for idx in range(n_frames):
|
|
168
|
+
self._transforms[idx] = None
|
|
169
|
+
gc.collect()
|
|
155
170
|
missing_transforms = 0
|
|
156
171
|
for i in range(n_frames):
|
|
157
172
|
if self._cumulative_transforms[i] is not None:
|
|
@@ -165,6 +180,32 @@ class AlignFramesParallel(AlignFramesBase):
|
|
|
165
180
|
self.print_message(msg)
|
|
166
181
|
self.process.add_begin_steps(n_frames)
|
|
167
182
|
|
|
183
|
+
def detect_and_compute_matches(self, img_ref, ref_idx, img_0, idx):
|
|
184
|
+
feature_config, matching_config = self.feature_config, self.matching_config
|
|
185
|
+
feature_config_detector = feature_config['detector']
|
|
186
|
+
feature_config_descriptor = feature_config['descriptor']
|
|
187
|
+
match_method = matching_config['match_method']
|
|
188
|
+
validate_align_config(feature_config_detector, feature_config_descriptor, match_method)
|
|
189
|
+
img_bw_0, img_bw_ref = img_bw_8bit(img_0), img_bw_8bit(img_ref)
|
|
190
|
+
detector = detector_map[feature_config_detector]()
|
|
191
|
+
if feature_config_detector == feature_config_descriptor and \
|
|
192
|
+
feature_config_detector in (constants.DETECTOR_SIFT,
|
|
193
|
+
constants.DETECTOR_AKAZE,
|
|
194
|
+
constants.DETECTOR_BRISK):
|
|
195
|
+
if self._kp[idx] is None or self._des[idx] is None:
|
|
196
|
+
kp_0, des_0 = detector.detectAndCompute(img_bw_0, None)
|
|
197
|
+
else:
|
|
198
|
+
kp_0, des_0 = self._kp[idx], self._des[idx]
|
|
199
|
+
if self._kp[ref_idx] is None or self._des[ref_idx] is None:
|
|
200
|
+
kp_ref, des_ref = detector.detectAndCompute(img_bw_ref, None)
|
|
201
|
+
else:
|
|
202
|
+
kp_ref, des_ref = self._kp[ref_idx], self._des[ref_idx]
|
|
203
|
+
else:
|
|
204
|
+
descriptor = descriptor_map[feature_config_descriptor]()
|
|
205
|
+
kp_0, des_0 = descriptor.compute(img_bw_0, detector.detect(img_bw_0, None))
|
|
206
|
+
kp_ref, des_ref = descriptor.compute(img_bw_ref, detector.detect(img_bw_ref, None))
|
|
207
|
+
return kp_0, kp_ref, get_good_matches(des_0, des_ref, matching_config)
|
|
208
|
+
|
|
168
209
|
def extract_features(self, idx, delta=1):
|
|
169
210
|
ref_idx = self.process.ref_idx
|
|
170
211
|
pass_ref_err_msg = "cannot find path to reference frame"
|
|
@@ -202,8 +243,8 @@ class AlignFramesParallel(AlignFramesBase):
|
|
|
202
243
|
img_ref_sub = img_subsample(img_ref, subsample, fast_subsampling)
|
|
203
244
|
else:
|
|
204
245
|
img_0_sub, img_ref_sub = img_0, img_ref
|
|
205
|
-
kp_0, kp_ref, good_matches = detect_and_compute_matches(
|
|
206
|
-
img_ref_sub,
|
|
246
|
+
kp_0, kp_ref, good_matches = self.detect_and_compute_matches(
|
|
247
|
+
img_ref_sub, ref_idx, img_0_sub, idx)
|
|
207
248
|
n_good_matches = len(good_matches)
|
|
208
249
|
if n_good_matches > min_good_matches or subsample == 1:
|
|
209
250
|
break
|
|
@@ -217,8 +258,6 @@ class AlignFramesParallel(AlignFramesBase):
|
|
|
217
258
|
f"warning: only {n_good_matches} found for "
|
|
218
259
|
f"{self.image_str(idx)}, trying next frame",
|
|
219
260
|
color=constants.LOG_COLOR_WARNING, level=logging.WARNING)
|
|
220
|
-
self._target_indices[idx] = None
|
|
221
|
-
self._transforms[idx] = None
|
|
222
261
|
return self.extract_features(idx, delta + 1)
|
|
223
262
|
transform = self.alignment_config['transform']
|
|
224
263
|
src_pts = np.float32([kp_0[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
|
|
@@ -231,23 +270,24 @@ class AlignFramesParallel(AlignFramesBase):
|
|
|
231
270
|
if subsample > 1:
|
|
232
271
|
m = rescale_trasnsform(m, w0, h0, w_sub, h_sub, subsample, transform)
|
|
233
272
|
if m is None:
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
273
|
+
self.print_message(
|
|
274
|
+
f"invalid option {transform} "
|
|
275
|
+
f"for {self.image_str(idx)}, trying next frame",
|
|
276
|
+
color=constants.LOG_COLOR_WARNING, level=logging.WARNING)
|
|
277
|
+
return self.extract_features(idx, delta + 1)
|
|
238
278
|
transform_type = self.alignment_config['transform']
|
|
239
279
|
thresholds = self.get_transform_thresholds()
|
|
240
|
-
is_valid,
|
|
280
|
+
is_valid, _reason, _result = check_transform(m, img_0, transform_type, *thresholds)
|
|
241
281
|
if not is_valid:
|
|
282
|
+
msg = f"invalid transformation for {self.image_str(idx)}"
|
|
283
|
+
do_abort = self.alignment_config['abort_abnormal']
|
|
284
|
+
if not do_abort:
|
|
285
|
+
msg += ", trying next frame"
|
|
242
286
|
self.print_message(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if self.alignment_config['abort_abnormal']:
|
|
287
|
+
msg, color=constants.LOG_COLOR_WARNING, level=logging.WARNING)
|
|
288
|
+
if do_abort:
|
|
246
289
|
raise RuntimeError("invalid transformation: {reason}")
|
|
247
|
-
|
|
248
|
-
self._target_indices[idx] = None
|
|
249
|
-
self._transforms[idx] = None
|
|
250
|
-
return info_messages, warning_messages
|
|
290
|
+
return self.extract_features(idx, delta + 1)
|
|
251
291
|
self._transforms[idx] = m
|
|
252
292
|
self._target_indices[idx] = target_idx
|
|
253
293
|
return info_messages, warning_messages
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0602, R0903
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0602, R0903, R0902
|
|
2
2
|
import os
|
|
3
3
|
import numpy as np
|
|
4
|
-
from .. core.exceptions import InvalidOptionError,
|
|
4
|
+
from .. core.exceptions import InvalidOptionError, RunStopException
|
|
5
5
|
from .. config.constants import constants
|
|
6
6
|
from .. core.colors import color_str
|
|
7
|
-
from .utils import read_img, get_img_metadata,
|
|
7
|
+
from .utils import read_img, get_img_metadata, get_first_image_file
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class BaseStackAlgo:
|
|
@@ -14,6 +14,9 @@ class BaseStackAlgo:
|
|
|
14
14
|
self.process = None
|
|
15
15
|
self.filenames = None
|
|
16
16
|
self.shape = None
|
|
17
|
+
self.dtype = None
|
|
18
|
+
self.num_pixel_values = None
|
|
19
|
+
self.max_pixel_value = None
|
|
17
20
|
self.do_step_callback = False
|
|
18
21
|
if float_type == constants.FLOAT_32:
|
|
19
22
|
self.float_type = np.float32
|
|
@@ -41,14 +44,16 @@ class BaseStackAlgo:
|
|
|
41
44
|
return f"image: {self.idx_tot_str(idx)}, " \
|
|
42
45
|
f"{os.path.basename(self.filenames[idx])}"
|
|
43
46
|
|
|
47
|
+
def num_images(self):
|
|
48
|
+
return len(self.filenames)
|
|
49
|
+
|
|
44
50
|
def init(self, filenames):
|
|
45
51
|
self.filenames = filenames
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
self.shape = get_img_file_shape(first_img_file)
|
|
52
|
+
self.shape, self.dtype = get_img_metadata(read_img(get_first_image_file(filenames)))
|
|
53
|
+
self.num_pixel_values = constants.NUM_UINT8 \
|
|
54
|
+
if self.dtype == np.uint8 else constants.NUM_UINT16
|
|
55
|
+
self.max_pixel_value = constants.MAX_UINT8 \
|
|
56
|
+
if self.dtype == np.uint8 else constants.MAX_UINT16
|
|
52
57
|
|
|
53
58
|
def total_steps(self, n_frames):
|
|
54
59
|
return self._steps_per_frame * n_frames
|
|
@@ -56,17 +61,6 @@ class BaseStackAlgo:
|
|
|
56
61
|
def print_message(self, msg):
|
|
57
62
|
self.process.sub_message_r(color_str(msg, constants.LOG_COLOR_LEVEL_3))
|
|
58
63
|
|
|
59
|
-
def read_image_and_update_metadata(self, img_path, metadata):
|
|
60
|
-
img = read_img(img_path)
|
|
61
|
-
if img is None:
|
|
62
|
-
raise ImageLoadError(img_path)
|
|
63
|
-
updated = metadata is None
|
|
64
|
-
if updated:
|
|
65
|
-
metadata = get_img_metadata(img)
|
|
66
|
-
else:
|
|
67
|
-
validate_image(img, *metadata)
|
|
68
|
-
return img, metadata, updated
|
|
69
|
-
|
|
70
64
|
def check_running(self, cleanup_callback=None):
|
|
71
65
|
if self.process.callback(constants.CALLBACK_CHECK_RUNNING,
|
|
72
66
|
self.process.id, self.process.name) is False:
|
|
@@ -3,7 +3,7 @@ import numpy as np
|
|
|
3
3
|
import cv2
|
|
4
4
|
from .. config.constants import constants
|
|
5
5
|
from .. core.exceptions import InvalidOptionError
|
|
6
|
-
from .utils import read_img, img_bw
|
|
6
|
+
from .utils import read_img, read_and_validate_img, img_bw
|
|
7
7
|
from .base_stack_algo import BaseStackAlgo
|
|
8
8
|
|
|
9
9
|
|
|
@@ -62,19 +62,15 @@ class DepthMapStack(BaseStackAlgo):
|
|
|
62
62
|
f"{constants.DM_MAP_AVERAGE} and {constants.DM_MAP_MAX}.")
|
|
63
63
|
|
|
64
64
|
def focus_stack(self):
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
n_images = len(self.filenames)
|
|
66
|
+
gray_images = np.empty((n_images, *self.shape), dtype=self.float_type)
|
|
67
67
|
for i, img_path in enumerate(self.filenames):
|
|
68
|
-
self.print_message(f": reading
|
|
69
|
-
|
|
70
|
-
img, metadata, _updated = self.read_image_and_update_metadata(img_path, metadata)
|
|
71
|
-
|
|
68
|
+
self.print_message(f": reading and validating {self.image_str(i)}")
|
|
69
|
+
img = read_and_validate_img(img_path, self.shape, self.dtype)
|
|
72
70
|
gray = img_bw(img)
|
|
73
|
-
gray_images.
|
|
71
|
+
gray_images[i] = gray.astype(self.float_type)
|
|
74
72
|
self.after_step(i)
|
|
75
73
|
self.check_running()
|
|
76
|
-
dtype = metadata[1]
|
|
77
|
-
gray_images = np.array(gray_images, dtype=self.float_type)
|
|
78
74
|
if self.energy == constants.DM_ENERGY_SOBEL:
|
|
79
75
|
energies = self.get_sobel_map(gray_images)
|
|
80
76
|
elif self.energy == constants.DM_ENERGY_LAPLACIAN:
|
|
@@ -92,7 +88,7 @@ class DepthMapStack(BaseStackAlgo):
|
|
|
92
88
|
weights = self.get_focus_map(energies)
|
|
93
89
|
blended_pyramid = None
|
|
94
90
|
for i, img_path in enumerate(self.filenames):
|
|
95
|
-
self.print_message(f": reading
|
|
91
|
+
self.print_message(f": reading {self.image_str(i)}")
|
|
96
92
|
img = read_img(img_path).astype(self.float_type)
|
|
97
93
|
weight = weights[i]
|
|
98
94
|
gp_img = [img]
|
|
@@ -109,12 +105,11 @@ class DepthMapStack(BaseStackAlgo):
|
|
|
109
105
|
for j in range(self.levels)]
|
|
110
106
|
blended_pyramid = current_blend if blended_pyramid is None \
|
|
111
107
|
else [np.add(bp, cb) for bp, cb in zip(blended_pyramid, current_blend)]
|
|
112
|
-
self.after_step(i +
|
|
108
|
+
self.after_step(i + n_images)
|
|
113
109
|
self.check_running()
|
|
114
110
|
result = blended_pyramid[0]
|
|
115
111
|
self.print_message(': blend levels')
|
|
116
112
|
for j in range(1, self.levels):
|
|
117
113
|
size = (blended_pyramid[j].shape[1], blended_pyramid[j].shape[0])
|
|
118
114
|
result = cv2.pyrUp(result, dstsize=size) + blended_pyramid[j]
|
|
119
|
-
|
|
120
|
-
return np.clip(np.absolute(result), 0, n_values).astype(dtype)
|
|
115
|
+
return np.clip(np.absolute(result), 0, self.num_pixel_values).astype(self.dtype)
|