shinestacker 1.0.4__tar.gz → 1.1.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.

Files changed (138) hide show
  1. {shinestacker-1.0.4 → shinestacker-1.1.0}/.github/workflows/release.yml +6 -6
  2. {shinestacker-1.0.4 → shinestacker-1.1.0}/CHANGELOG.md +23 -1
  3. {shinestacker-1.0.4/src/shinestacker.egg-info → shinestacker-1.1.0}/PKG-INFO +20 -1
  4. {shinestacker-1.0.4 → shinestacker-1.1.0}/README.md +19 -0
  5. {shinestacker-1.0.4 → shinestacker-1.1.0}/docs/focus_stacking.md +5 -0
  6. {shinestacker-1.0.4 → shinestacker-1.1.0}/scripts/build_release.py +17 -6
  7. shinestacker-1.1.0/src/shinestacker/_version.py +1 -0
  8. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/algorithms/align.py +11 -11
  9. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/algorithms/base_stack_algo.py +27 -4
  10. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/algorithms/depth_map.py +8 -11
  11. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/algorithms/noise_detection.py +7 -2
  12. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/algorithms/pyramid.py +55 -41
  13. shinestacker-1.1.0/src/shinestacker/algorithms/pyramid_tiles.py +109 -0
  14. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/algorithms/stack.py +11 -8
  15. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/algorithms/stack_framework.py +5 -8
  16. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/algorithms/utils.py +5 -0
  17. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/config/constants.py +5 -2
  18. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/action_config_dialog.py +26 -4
  19. shinestacker-1.1.0/src/shinestacker/gui/flow_layout.py +105 -0
  20. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/gui_run.py +24 -19
  21. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/main_window.py +2 -2
  22. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/new_project.py +1 -0
  23. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/project_controller.py +1 -0
  24. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/project_converter.py +10 -4
  25. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/time_progress_bar.py +2 -2
  26. {shinestacker-1.0.4 → shinestacker-1.1.0/src/shinestacker.egg-info}/PKG-INFO +20 -1
  27. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker.egg-info/SOURCES.txt +2 -0
  28. shinestacker-1.0.4/src/shinestacker/_version.py +0 -1
  29. {shinestacker-1.0.4 → shinestacker-1.1.0}/.coveragerc +0 -0
  30. {shinestacker-1.0.4 → shinestacker-1.1.0}/.flake8 +0 -0
  31. {shinestacker-1.0.4 → shinestacker-1.1.0}/.github/workflows/ci-multiplatform.yml +0 -0
  32. {shinestacker-1.0.4 → shinestacker-1.1.0}/.github/workflows/pylint.yml +0 -0
  33. {shinestacker-1.0.4 → shinestacker-1.1.0}/.github/workflows/pypi-publish.yml +0 -0
  34. {shinestacker-1.0.4 → shinestacker-1.1.0}/.gitignore +0 -0
  35. {shinestacker-1.0.4 → shinestacker-1.1.0}/.pylintrc +0 -0
  36. {shinestacker-1.0.4 → shinestacker-1.1.0}/.readthedocs.yaml +0 -0
  37. {shinestacker-1.0.4 → shinestacker-1.1.0}/LICENSE +0 -0
  38. {shinestacker-1.0.4 → shinestacker-1.1.0}/MANIFEST.in +0 -0
  39. {shinestacker-1.0.4 → shinestacker-1.1.0}/THIRD_PARTY_LICENSES.txt +0 -0
  40. {shinestacker-1.0.4 → shinestacker-1.1.0}/docs/alignment.md +0 -0
  41. {shinestacker-1.0.4 → shinestacker-1.1.0}/docs/api.md +0 -0
  42. {shinestacker-1.0.4 → shinestacker-1.1.0}/docs/balancing.md +0 -0
  43. {shinestacker-1.0.4 → shinestacker-1.1.0}/docs/conf.py +0 -0
  44. {shinestacker-1.0.4 → shinestacker-1.1.0}/docs/gui.md +0 -0
  45. {shinestacker-1.0.4 → shinestacker-1.1.0}/docs/index.md +0 -0
  46. {shinestacker-1.0.4 → shinestacker-1.1.0}/docs/job.md +0 -0
  47. {shinestacker-1.0.4 → shinestacker-1.1.0}/docs/main.md +0 -0
  48. {shinestacker-1.0.4 → shinestacker-1.1.0}/docs/multilayer.md +0 -0
  49. {shinestacker-1.0.4 → shinestacker-1.1.0}/docs/noise.md +0 -0
  50. {shinestacker-1.0.4 → shinestacker-1.1.0}/docs/requirements.txt +0 -0
  51. {shinestacker-1.0.4 → shinestacker-1.1.0}/docs/vignetting.md +0 -0
  52. {shinestacker-1.0.4 → shinestacker-1.1.0}/img/coffee.gif +0 -0
  53. {shinestacker-1.0.4 → shinestacker-1.1.0}/img/coffee_stack.jpg +0 -0
  54. {shinestacker-1.0.4 → shinestacker-1.1.0}/img/extreme-vignetting.jpg +0 -0
  55. {shinestacker-1.0.4 → shinestacker-1.1.0}/img/flies.gif +0 -0
  56. {shinestacker-1.0.4 → shinestacker-1.1.0}/img/flies_stack.jpg +0 -0
  57. {shinestacker-1.0.4 → shinestacker-1.1.0}/img/flow-diagram.png +0 -0
  58. {shinestacker-1.0.4 → shinestacker-1.1.0}/img/gui-finder.png +0 -0
  59. {shinestacker-1.0.4 → shinestacker-1.1.0}/img/gui-project-new.png +0 -0
  60. {shinestacker-1.0.4 → shinestacker-1.1.0}/img/gui-project-run.png +0 -0
  61. {shinestacker-1.0.4 → shinestacker-1.1.0}/img/gui-retouch.png +0 -0
  62. {shinestacker-1.0.4 → shinestacker-1.1.0}/pyproject.toml +0 -0
  63. {shinestacker-1.0.4 → shinestacker-1.1.0}/requirements.txt +0 -0
  64. {shinestacker-1.0.4 → shinestacker-1.1.0}/scripts/git-rev-list.sh +0 -0
  65. {shinestacker-1.0.4 → shinestacker-1.1.0}/scripts/validate-tomli.py +0 -0
  66. {shinestacker-1.0.4 → shinestacker-1.1.0}/setup.cfg +0 -0
  67. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/__init__.py +0 -0
  68. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/algorithms/__init__.py +0 -0
  69. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/algorithms/balance.py +0 -0
  70. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/algorithms/denoise.py +0 -0
  71. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/algorithms/exif.py +0 -0
  72. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/algorithms/multilayer.py +0 -0
  73. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/algorithms/sharpen.py +0 -0
  74. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/algorithms/vignetting.py +0 -0
  75. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/algorithms/white_balance.py +0 -0
  76. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/app/__init__.py +0 -0
  77. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/app/about_dialog.py +0 -0
  78. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/app/gui_utils.py +0 -0
  79. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/app/help_menu.py +0 -0
  80. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/app/main.py +0 -0
  81. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/app/open_frames.py +0 -0
  82. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/app/project.py +0 -0
  83. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/app/retouch.py +0 -0
  84. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/config/__init__.py +0 -0
  85. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/config/config.py +0 -0
  86. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/config/gui_constants.py +0 -0
  87. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/core/__init__.py +0 -0
  88. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/core/colors.py +0 -0
  89. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/core/core_utils.py +0 -0
  90. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/core/exceptions.py +0 -0
  91. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/core/framework.py +0 -0
  92. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/core/logging.py +0 -0
  93. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/__init__.py +0 -0
  94. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/action_config.py +0 -0
  95. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/base_form_dialog.py +0 -0
  96. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/colors.py +0 -0
  97. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/gui_images.py +0 -0
  98. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/gui_logging.py +0 -0
  99. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
  100. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
  101. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
  102. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/ico/shinestacker.png +0 -0
  103. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/ico/shinestacker.svg +0 -0
  104. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
  105. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
  106. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
  107. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
  108. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/menu_manager.py +0 -0
  109. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/project_editor.py +0 -0
  110. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/project_model.py +0 -0
  111. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/select_path_widget.py +0 -0
  112. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/gui/tab_widget.py +0 -0
  113. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/__init__.py +0 -0
  114. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/base_filter.py +0 -0
  115. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/brush.py +0 -0
  116. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/brush_gradient.py +0 -0
  117. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/brush_preview.py +0 -0
  118. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/brush_tool.py +0 -0
  119. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/denoise_filter.py +0 -0
  120. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/display_manager.py +0 -0
  121. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/exif_data.py +0 -0
  122. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/file_loader.py +0 -0
  123. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/filter_manager.py +0 -0
  124. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/icon_container.py +0 -0
  125. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/image_editor_ui.py +0 -0
  126. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/image_viewer.py +0 -0
  127. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/io_gui_handler.py +0 -0
  128. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/io_manager.py +0 -0
  129. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/layer_collection.py +0 -0
  130. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/shortcuts_help.py +0 -0
  131. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/undo_manager.py +0 -0
  132. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/unsharp_mask_filter.py +0 -0
  133. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/vignetting_filter.py +0 -0
  134. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker/retouch/white_balance_filter.py +0 -0
  135. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker.egg-info/dependency_links.txt +0 -0
  136. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker.egg-info/entry_points.txt +0 -0
  137. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker.egg-info/requires.txt +0 -0
  138. {shinestacker-1.0.4 → shinestacker-1.1.0}/src/shinestacker.egg-info/top_level.txt +0 -0
@@ -44,8 +44,8 @@ jobs:
44
44
  uses: actions/upload-artifact@v4
45
45
  with:
46
46
  name: shinestacker-${{ matrix.os }}
47
- path: dist/shinestacker-release.zip
48
-
47
+ path: |
48
+ ${{ matrix.os == 'windows-latest' && 'dist/shinestacker-release.zip' || 'dist/shinestacker-release.tar.gz' }}
49
49
  create-release:
50
50
  needs: publish-release
51
51
  runs-on: ubuntu-latest
@@ -58,9 +58,9 @@ jobs:
58
58
  - name: Prepare release assets
59
59
  run: |
60
60
  mkdir -p release_assets
61
- cp artifacts/shinestacker-ubuntu-latest/shinestacker-release.zip release_assets/shinestacker-ubuntu.zip
61
+ cp artifacts/shinestacker-ubuntu-latest/shinestacker-release.tar.gz release_assets/shinestacker-ubuntu.tar.gz
62
+ cp artifacts/shinestacker-macos-latest/shinestacker-release.tar.gz release_assets/shinestacker-macos.tar.gz
62
63
  cp artifacts/shinestacker-windows-latest/shinestacker-release.zip release_assets/shinestacker-windows.zip
63
- cp artifacts/shinestacker-macos-latest/shinestacker-release.zip release_assets/shinestacker-macos.zip
64
64
  ls -la release_assets/
65
65
 
66
66
  - name: Create release
@@ -68,6 +68,6 @@ jobs:
68
68
  with:
69
69
  draft: true
70
70
  files: |
71
- release_assets/shinestacker-ubuntu.zip
71
+ release_assets/shinestacker-ubuntu.tar.gz
72
+ release_assets/shinestacker-macos.tar.gz
72
73
  release_assets/shinestacker-windows.zip
73
- release_assets/shinestacker-macos.zip
@@ -2,13 +2,35 @@
2
2
 
3
3
  This page reports the main releases only and the main changes therein.
4
4
 
5
+ ## [v1.1.0] - 2025-08-28
6
+ **New Pyramids algorith, some improvements and more fixes**
7
+
8
+ ### Changes
9
+
10
+ * added Pyramids Tiles, that requires less RAM by fusing the image in tiles
11
+ * the alignment module now tolerates images of different shapes
12
+ * noisy pixel mask verifies that the mask has the same shape as input images
13
+ * minor changes to default alignment parameters
14
+ * some improvements to the GUI
15
+ * some bug fixes
16
+ ---
17
+
18
+ ## [v1.0.4.pre2] - 2025-08-26
19
+ **Bug fixes**
20
+
21
+ ### Changes
22
+
23
+ * fixed release build script changing format from zip to tar.gz for macOs and Linux
24
+
25
+ ---
26
+
5
27
  ## [v1.0.4] - 2025-08-26
6
28
  **Bug fixes**
7
29
 
8
30
  ### Changes
9
31
 
10
32
  * extensions are treated in lower case (e.g.: both jpg and JPG)
11
- * added retouch menu action: import frames from current project
33
+ * added missing retouch menu action: import frames from current project
12
34
 
13
35
  ---
14
36
  ## [v1.0.3] - 2025-08-26
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.0.4
3
+ Version: 1.1.0
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -69,6 +69,25 @@ The GUI has two main working areas:
69
69
 
70
70
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/gui-retouch.png' width="600" referrerpolicy="no-referrer">
71
71
 
72
+ # Note for macOS users
73
+
74
+ **The following note is only relevant if you download the application as compressed archive from the [release page](https://github.com/lucalista/shinestacker/releases).**
75
+
76
+ The macOS system security protection prevent to run applications downloaded from the web that come from developers that don't hold an Apple Developer Certificate.
77
+
78
+ In order to prevent this, follow the instructions below:
79
+
80
+ 1. Download the compressed archive ```shinestacker-macos.tar.gz``` in your ```Download``` folder.
81
+ 2. Double-click the archive to uncompress it. You will find a new folder ```shinestacker```.
82
+ 3. Open a terminal (*Applications > Utilities > Terminal*)
83
+ 4. Type the folliwng command on the terminal:
84
+ ```bash
85
+ xattr -cr ~/Downloads/shinestacker/shinestacker.app
86
+ ```
87
+ 5. Now you can double-click the Sine Stacker icon app in the ```shiestacker``` folder and it should run.
88
+
89
+ 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
+
72
91
  # Resources
73
92
 
74
93
  🌍 [Website on WordPress](https://shinestacker.wordpress.com) • 📖 [Main documentation](https://shinestacker.readthedocs.io) • 📝 [Changelog](https://github.com/lucalista/shinestacker/blob/main/CHANGELOG.md)
@@ -38,6 +38,25 @@ 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
+ # Note for macOS users
42
+
43
+ **The following note is only relevant if you download the application as compressed archive from the [release page](https://github.com/lucalista/shinestacker/releases).**
44
+
45
+ The macOS system security protection prevent to run applications downloaded from the web that come from developers that don't hold an Apple Developer Certificate.
46
+
47
+ In order to prevent this, follow the instructions below:
48
+
49
+ 1. Download the compressed archive ```shinestacker-macos.tar.gz``` in your ```Download``` folder.
50
+ 2. Double-click the archive to uncompress it. You will find a new folder ```shinestacker```.
51
+ 3. Open a terminal (*Applications > Utilities > Terminal*)
52
+ 4. Type the folliwng command on the terminal:
53
+ ```bash
54
+ xattr -cr ~/Downloads/shinestacker/shinestacker.app
55
+ ```
56
+ 5. Now you can double-click the Sine Stacker icon app in the ```shiestacker``` folder and it should run.
57
+
58
+ 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
+
41
60
  # Resources
42
61
 
43
62
  🌍 [Website on WordPress](https://shinestacker.wordpress.com) • 📖 [Main documentation](https://shinestacker.readthedocs.io) • 📝 [Changelog](https://github.com/lucalista/shinestacker/blob/main/CHANGELOG.md)
@@ -42,6 +42,11 @@ Arguments for the constructor are:
42
42
  * ```gen_kernel``` (optional, default: 0.4)
43
43
  * ```float_type``` (optional, default: ```FLOAT_32```, possible values: ```FLOAT_32```, ```FLOAT_64```): precision for internal image representation
44
44
 
45
+ ```PyramidTilesStack```, Laplacian pyramid with tile pyramid merging to optimize memory usage for large files
46
+
47
+ Arguments for the constructor are the same ad for ```PyramidStack``` plus:
48
+ * ```tile_size``` (optional, default: 512): size of the tile used for partial image merging
49
+
45
50
  ```DepthMapStack```, Depth map focus stacking algorithm
46
51
 
47
52
  Arguments for the constructor are:
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  import shutil
3
+ import tarfile
3
4
  import subprocess
4
5
  from pathlib import Path
5
6
  import platform
@@ -35,9 +36,19 @@ for project_file in ["complete-project.fsp", "stack-from-frames.fsp"]:
35
36
  shutil.copy(examples_dir / project_file, target_examples)
36
37
  shutil.copytree(examples_dir / 'input', target_examples / 'input', dirs_exist_ok=True)
37
38
 
38
- shutil.make_archive(
39
- base_name=str(dist_dir / "shinestacker-release"),
40
- format="zip",
41
- root_dir=dist_dir,
42
- base_dir=package_dir
43
- )
39
+ if sys_name == 'windows':
40
+ shutil.make_archive(
41
+ base_name=str(dist_dir / "shinestacker-release"),
42
+ format="zip",
43
+ root_dir=dist_dir,
44
+ base_dir=package_dir
45
+ )
46
+ else:
47
+ archive_path = dist_dir / "shinestacker-release.tar.gz"
48
+ with tarfile.open(archive_path, "w:gz") as tar:
49
+ tar.add(
50
+ dist_dir / package_dir,
51
+ arcname=package_dir,
52
+ recursive=True,
53
+ filter=lambda info: info
54
+ )
@@ -0,0 +1 @@
1
+ __version__ = '1.1.0'
@@ -6,7 +6,7 @@ import cv2
6
6
  from .. config.constants import constants
7
7
  from .. core.exceptions import AlignmentError, InvalidOptionError
8
8
  from .. core.colors import color_str
9
- from .utils import img_8bit, img_bw_8bit, save_plot, get_img_metadata, validate_image, img_subsample
9
+ from .utils import img_8bit, img_bw_8bit, save_plot, img_subsample
10
10
  from .stack_framework import SubAction
11
11
 
12
12
  _DEFAULT_FEATURE_CONFIG = {
@@ -161,9 +161,10 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
161
161
  except KeyError as e:
162
162
  raise InvalidOptionError("border_mode", alignment_config['border_mode']) from e
163
163
  min_matches = 4 if alignment_config['transform'] == constants.ALIGN_HOMOGRAPHY else 3
164
- validate_image(img_0, *get_img_metadata(img_1))
165
164
  if callbacks and 'message' in callbacks:
166
165
  callbacks['message']()
166
+ h_ref, w_ref = img_1.shape[:2]
167
+ h0, w0 = img_0.shape[:2]
167
168
  subsample = alignment_config['subsample']
168
169
  fast_subsampling = alignment_config['fast_subsampling']
169
170
  min_good_matches = alignment_config['min_good_matches']
@@ -204,15 +205,14 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
204
205
  flags=2), cv2.COLOR_BGR2RGB)
205
206
  plt.figure(figsize=(10, 5))
206
207
  plt.imshow(img_match, 'gray')
207
- plt.savefig(plot_path)
208
+ save_plot(plot_path)
208
209
  if callbacks and 'save_plot' in callbacks:
209
210
  callbacks['save_plot'](plot_path)
210
- h, w = img_0.shape[:2]
211
211
  h_sub, w_sub = img_0_sub.shape[:2]
212
212
  if subsample > 1:
213
213
  if transform == constants.ALIGN_HOMOGRAPHY:
214
214
  low_size = np.float32([[0, 0], [0, h_sub], [w_sub, h_sub], [w_sub, 0]])
215
- high_size = np.float32([[0, 0], [0, h], [w, h], [w, 0]])
215
+ high_size = np.float32([[0, 0], [0, h0], [w0, h0], [w0, 0]])
216
216
  scale_up = cv2.getPerspectiveTransform(low_size, high_size)
217
217
  scale_down = cv2.getPerspectiveTransform(high_size, low_size)
218
218
  m = scale_up @ m @ scale_down
@@ -230,17 +230,17 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
230
230
  img_mask = np.ones_like(img_0, dtype=np.uint8)
231
231
  if alignment_config['transform'] == constants.ALIGN_HOMOGRAPHY:
232
232
  img_warp = cv2.warpPerspective(
233
- img_0, m, (w, h),
233
+ img_0, m, (w_ref, h_ref),
234
234
  borderMode=cv2_border_mode, borderValue=alignment_config['border_value'])
235
235
  if alignment_config['border_mode'] == constants.BORDER_REPLICATE_BLUR:
236
- mask = cv2.warpPerspective(img_mask, m, (w, h),
236
+ mask = cv2.warpPerspective(img_mask, m, (w_ref, h_ref),
237
237
  borderMode=cv2.BORDER_CONSTANT, borderValue=0)
238
238
  elif alignment_config['transform'] == constants.ALIGN_RIGID:
239
239
  img_warp = cv2.warpAffine(
240
- img_0, m, (w, h),
240
+ img_0, m, (w_ref, h_ref),
241
241
  borderMode=cv2_border_mode, borderValue=alignment_config['border_value'])
242
242
  if alignment_config['border_mode'] == constants.BORDER_REPLICATE_BLUR:
243
- mask = cv2.warpAffine(img_mask, m, (w, h),
243
+ mask = cv2.warpAffine(img_mask, m, (w_ref, h_ref),
244
244
  borderMode=cv2.BORDER_CONSTANT, borderValue=0)
245
245
  if alignment_config['border_mode'] == constants.BORDER_REPLICATE_BLUR:
246
246
  if callbacks and 'blur_message' in callbacks:
@@ -293,7 +293,7 @@ class AlignFrames(SubAction):
293
293
  'ecc_message': lambda: self.sub_msg(": ecc refinement"),
294
294
  'blur_message': lambda: self.sub_msg(': blur borders'),
295
295
  'warning': lambda msg: self.sub_msg(
296
- f': {msg}', constants.LOG_COLOR_ALERT),
296
+ f': {msg}', constants.LOG_COLOR_WARNING),
297
297
  'save_plot': lambda plot_path: self.process.callback(
298
298
  'save_plot', self.process.id,
299
299
  f"{self.process.name}: matches\nframe {idx_str}", plot_path)
@@ -315,7 +315,7 @@ class AlignFrames(SubAction):
315
315
  if n_good_matches < self.min_matches:
316
316
  self.process.sub_message(f": image not aligned, too few matches found: "
317
317
  f"{n_good_matches}", level=logging.CRITICAL)
318
- raise AlignmentError(idx, f"too few matches found: "
318
+ raise AlignmentError(idx, f"Image not aligned, too few matches found: "
319
319
  f"{n_good_matches} < {self.min_matches}")
320
320
  return img
321
321
 
@@ -1,9 +1,10 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0602, R0903
2
+ import os
2
3
  import numpy as np
3
- from .. core.exceptions import InvalidOptionError, ImageLoadError
4
+ from .. core.exceptions import InvalidOptionError, ImageLoadError, RunStopException
4
5
  from .. config.constants import constants
5
6
  from .. core.colors import color_str
6
- from .utils import read_img, get_img_metadata, validate_image
7
+ from .utils import read_img, get_img_metadata, validate_image, get_img_file_shape, extension_tif_jpg
7
8
 
8
9
 
9
10
  class BaseStackAlgo:
@@ -11,6 +12,9 @@ class BaseStackAlgo:
11
12
  self._name = name
12
13
  self._steps_per_frame = steps_per_frame
13
14
  self.process = None
15
+ self.filenames = None
16
+ self.shape = None
17
+ self.do_step_callback = False
14
18
  if float_type == constants.FLOAT_32:
15
19
  self.float_type = np.float32
16
20
  elif float_type == constants.FLOAT_64:
@@ -24,8 +28,17 @@ class BaseStackAlgo:
24
28
  def name(self):
25
29
  return self._name
26
30
 
27
- def steps_per_frame(self):
28
- return self._steps_per_frame
31
+ def init(self, filenames):
32
+ self.filenames = filenames
33
+ first_img_file = ''
34
+ for filename in filenames:
35
+ if os.path.isfile(filename) and extension_tif_jpg(filename):
36
+ first_img_file = filename
37
+ break
38
+ self.shape = get_img_file_shape(first_img_file)
39
+
40
+ def total_steps(self, n_frames):
41
+ return self._steps_per_frame * n_frames
29
42
 
30
43
  def print_message(self, msg):
31
44
  self.process.sub_message_r(color_str(msg, constants.LOG_COLOR_LEVEL_3))
@@ -40,3 +53,13 @@ class BaseStackAlgo:
40
53
  else:
41
54
  validate_image(img, *metadata)
42
55
  return img, metadata, updated
56
+
57
+ def check_running(self, cleanup_callback=None):
58
+ if self.process.callback('check_running', self.process.id, self.process.name) is False:
59
+ if cleanup_callback is not None:
60
+ cleanup_callback()
61
+ raise RunStopException(self.name)
62
+
63
+ def after_step(self, step):
64
+ if self.do_step_callback:
65
+ self.process.callback('after_step', self.process.id, self.process.name, step)
@@ -2,7 +2,7 @@
2
2
  import numpy as np
3
3
  import cv2
4
4
  from .. config.constants import constants
5
- from .. core.exceptions import InvalidOptionError, RunStopException
5
+ from .. core.exceptions import InvalidOptionError
6
6
  from .utils import read_img, img_bw
7
7
  from .base_stack_algo import BaseStackAlgo
8
8
 
@@ -61,19 +61,18 @@ class DepthMapStack(BaseStackAlgo):
61
61
  raise InvalidOptionError("map_type", self.map_type, details=f" valid values are "
62
62
  f"{constants.DM_MAP_AVERAGE} and {constants.DM_MAP_MAX}.")
63
63
 
64
- def focus_stack(self, filenames):
64
+ def focus_stack(self):
65
65
  gray_images = []
66
66
  metadata = None
67
- for i, img_path in enumerate(filenames):
67
+ for i, img_path in enumerate(self.filenames):
68
68
  self.print_message(f": reading file (1/2) {img_path.split('/')[-1]}")
69
69
 
70
70
  img, metadata, _updated = self.read_image_and_update_metadata(img_path, metadata)
71
71
 
72
72
  gray = img_bw(img)
73
73
  gray_images.append(gray)
74
- self.process.callback('after_step', self.process.id, self.process.name, i)
75
- if self.process.callback('check_running', self.process.id, self.process.name) is False:
76
- raise RunStopException(self.name)
74
+ self.after_step(i)
75
+ self.check_running()
77
76
  dtype = metadata[1]
78
77
  gray_images = np.array(gray_images, dtype=self.float_type)
79
78
  if self.energy == constants.DM_ENERGY_SOBEL:
@@ -92,7 +91,7 @@ class DepthMapStack(BaseStackAlgo):
92
91
  energies = self.smooth_energy(energies)
93
92
  weights = self.get_focus_map(energies)
94
93
  blended_pyramid = None
95
- for i, img_path in enumerate(filenames):
94
+ for i, img_path in enumerate(self.filenames):
96
95
  self.print_message(f": reading file (2/2) {img_path.split('/')[-1]}")
97
96
  img = read_img(img_path).astype(self.float_type)
98
97
  weight = weights[i]
@@ -110,10 +109,8 @@ class DepthMapStack(BaseStackAlgo):
110
109
  for j in range(self.levels)]
111
110
  blended_pyramid = current_blend if blended_pyramid is None \
112
111
  else [np.add(bp, cb) for bp, cb in zip(blended_pyramid, current_blend)]
113
- self.process.callback('after_step', self.process.id,
114
- self.process.name, i + len(filenames))
115
- if self.process.callback('check_running', self.process.id, self.process.name) is False:
116
- raise RunStopException(self.name)
112
+ self.after_step(i + len(self.filenames))
113
+ self.check_running()
117
114
  result = blended_pyramid[0]
118
115
  self.print_message(': blend levels')
119
116
  for j in range(1, self.levels):
@@ -1,4 +1,4 @@
1
- # pylint: disable=C0114, C0115, C0116, E1101, W0718, R0914, R0915
1
+ # pylint: disable=C0114, C0115, C0116, E1101, W0718, R0914, R0915, R0902
2
2
  import os
3
3
  import errno
4
4
  import logging
@@ -11,7 +11,7 @@ from .. core.colors import color_str
11
11
  from .. core.exceptions import ImageLoadError
12
12
  from .. core.framework import JobBase
13
13
  from .. core.core_utils import make_tqdm_bar
14
- from .. core.exceptions import RunStopException
14
+ from .. core.exceptions import RunStopException, ShapeError
15
15
  from .stack_framework import FrameMultiDirectory, SubAction
16
16
  from .utils import read_img, save_plot, get_img_metadata, validate_image
17
17
 
@@ -154,6 +154,7 @@ class MaskNoise(SubAction):
154
154
  self.method = method
155
155
  self.process = None
156
156
  self.noise_mask_img = None
157
+ self.expected_shape = None
157
158
 
158
159
  def begin(self, process):
159
160
  self.process = process
@@ -163,6 +164,7 @@ class MaskNoise(SubAction):
163
164
  f': reading noisy pixel mask file: {self.noise_mask}',
164
165
  constants.LOG_COLOR_LEVEL_3))
165
166
  self.noise_mask_img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
167
+ self.expected_shape = self.noise_mask_img.shape[:2]
166
168
  if self.noise_mask_img is None:
167
169
  raise ImageLoadError(path, f"failed to load image file {self.noise_mask}.")
168
170
  else:
@@ -170,6 +172,9 @@ class MaskNoise(SubAction):
170
172
 
171
173
  def run_frame(self, _idx, _ref_idx, image):
172
174
  self.process.sub_message_r(color_str(': mask noisy pixels', constants.LOG_COLOR_LEVEL_3))
175
+ shape = image.shape[:2]
176
+ if shape != self.expected_shape:
177
+ raise ShapeError(self.expected_shape, shape)
173
178
  if len(image.shape) == 3:
174
179
  corrected = image.copy()
175
180
  for c in range(3):
@@ -1,25 +1,36 @@
1
- # pylint: disable=C0114, C0115, C0116, E1101
1
+ # pylint: disable=C0114, C0115, C0116, E1101, R0913, R0917, R0902
2
2
  import numpy as np
3
3
  import cv2
4
4
  from .. config.constants import constants
5
- from .. core.exceptions import RunStopException
6
5
  from .utils import read_img
7
6
  from .base_stack_algo import BaseStackAlgo
8
7
 
9
8
 
10
9
  class PyramidBase(BaseStackAlgo):
11
- def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE,
10
+ def __init__(self, name, min_size=constants.DEFAULT_PY_MIN_SIZE,
12
11
  kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
13
12
  gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
14
13
  float_type=constants.DEFAULT_PY_FLOAT):
15
- super().__init__("pyramid", 2, float_type)
14
+ super().__init__(name, 2, float_type)
16
15
  self.min_size = min_size
17
16
  self.kernel_size = kernel_size
18
17
  self.pad_amount = (kernel_size - 1) // 2
19
- self.do_step_callback = False
20
18
  kernel = np.array([0.25 - gen_kernel / 2.0, 0.25,
21
19
  gen_kernel, 0.25, 0.25 - gen_kernel / 2.0])
22
20
  self.gen_kernel = np.outer(kernel, kernel)
21
+ self.dtype = None
22
+ self.num_pixel_values = None
23
+ self.max_pixel_value = None
24
+ self.n_levels = 0
25
+ self.n_frames = 0
26
+
27
+ def init(self, filenames):
28
+ super().init(filenames)
29
+ self.n_levels = int(np.log2(min(self.shape) / self.min_size))
30
+
31
+ def total_steps(self, n_frames):
32
+ self.n_frames = n_frames
33
+ return self._steps_per_frame * n_frames + self.n_levels
23
34
 
24
35
  def convolve(self, image):
25
36
  return cv2.filter2D(image, -1, self.gen_kernel, borderType=cv2.BORDER_REFLECT101)
@@ -55,6 +66,7 @@ class PyramidBase(BaseStackAlgo):
55
66
  return fused
56
67
 
57
68
  def collapse(self, pyramid):
69
+ self.print_message(': collapsing pyramid')
58
70
  img = pyramid[-1]
59
71
  for layer in pyramid[-2::-1]:
60
72
  expanded = self.expand_layer(img)
@@ -110,19 +122,22 @@ class PyramidBase(BaseStackAlgo):
110
122
  fused += np.where(best_d[:, :, np.newaxis] == layer, img, 0)
111
123
  return (fused / 2).astype(images.dtype)
112
124
 
125
+ def focus_stack_validate(self, cleanup_callback=None):
126
+ metadata = None
127
+ for i, img_path in enumerate(self.filenames):
128
+ self.print_message(f": validating file {img_path.split('/')[-1]}")
113
129
 
114
- class PyramidStack(PyramidBase):
115
- def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE,
116
- kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
117
- gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
118
- float_type=constants.DEFAULT_PY_FLOAT):
119
- super().__init__(min_size, kernel_size, gen_kernel, float_type)
120
- self.offset = np.arange(-self.pad_amount, self.pad_amount + 1)
121
- self.dtype = None
122
- self.num_pixel_values = None
123
- self.max_pixel_value = None
130
+ _img, metadata, updated = self.read_image_and_update_metadata(img_path, metadata)
131
+ if updated:
132
+ self.dtype = metadata[1]
133
+ self.num_pixel_values = constants.NUM_UINT8 \
134
+ if self.dtype == np.uint8 else constants.NUM_UINT16
135
+ self.max_pixel_value = constants.MAX_UINT8 \
136
+ if self.dtype == np.uint8 else constants.MAX_UINT16
137
+ self.after_step(i + 1)
138
+ self.check_running(cleanup_callback)
124
139
 
125
- def process_single_image(self, img, levels):
140
+ def single_image_laplacian(self, img, levels):
126
141
  pyramid = [img.astype(self.float_type)]
127
142
  for _ in range(levels):
128
143
  next_layer = self.reduce_layer(pyramid[-1])
@@ -136,44 +151,43 @@ class PyramidStack(PyramidBase):
136
151
  h, w = pyr.shape[:2]
137
152
  expanded = expanded[:h, :w]
138
153
  laplacian.append(pyr - expanded)
154
+ return laplacian
155
+
156
+
157
+ class PyramidStack(PyramidBase):
158
+ def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE,
159
+ kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
160
+ gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
161
+ float_type=constants.DEFAULT_PY_FLOAT):
162
+ super().__init__("pyramid", min_size, kernel_size, gen_kernel, float_type)
163
+ self.offset = np.arange(-self.pad_amount, self.pad_amount + 1)
164
+
165
+ def process_single_image(self, img, levels):
166
+ laplacian = self.single_image_laplacian(img, levels)
139
167
  return laplacian[::-1]
140
168
 
141
169
  def fuse_pyramids(self, all_laplacians):
142
170
  fused = [self.get_fused_base(np.stack([p[-1] for p in all_laplacians], axis=0))]
171
+ count = 0
143
172
  for layer in range(len(all_laplacians[0]) - 2, -1, -1):
144
173
  self.print_message(f': fusing pyramids, layer: {layer + 1}')
145
174
  laplacians = np.stack([p[layer] for p in all_laplacians], axis=0)
146
175
  fused.append(self.fuse_laplacian(laplacians))
176
+ count += 1
177
+ self.after_step(self._steps_per_frame * self.n_frames + count)
178
+ self.check_running()
147
179
  self.print_message(': pyramids fusion completed')
148
180
  return fused[::-1]
149
181
 
150
- def focus_stack(self, filenames):
151
- metadata = None
182
+ def focus_stack(self):
183
+ n = len(self.filenames)
184
+ self.focus_stack_validate()
152
185
  all_laplacians = []
153
- levels = None
154
- n = len(filenames)
155
- for i, img_path in enumerate(filenames):
156
- self.print_message(f": validating file {img_path.split('/')[-1]}")
157
-
158
- img, metadata, updated = self.read_image_and_update_metadata(img_path, metadata)
159
- if updated:
160
- self.dtype = metadata[1]
161
- self.num_pixel_values = constants.NUM_UINT8 \
162
- if self.dtype == np.uint8 else constants.NUM_UINT16
163
- self.max_pixel_value = constants.MAX_UINT8 \
164
- if self.dtype == np.uint8 else constants.MAX_UINT16
165
- levels = int(np.log2(min(img.shape[:2]) / self.min_size))
166
- if self.do_step_callback:
167
- self.process.callback('after_step', self.process.id, self.process.name, i)
168
- if self.process.callback('check_running', self.process.id, self.process.name) is False:
169
- raise RunStopException(self.name)
170
- for i, img_path in enumerate(filenames):
186
+ for i, img_path in enumerate(self.filenames):
171
187
  self.print_message(f": processing file {img_path.split('/')[-1]}")
172
188
  img = read_img(img_path)
173
- all_laplacians.append(self.process_single_image(img, levels))
174
- if self.do_step_callback:
175
- self.process.callback('after_step', self.process.id, self.process.name, i + n)
176
- if self.process.callback('check_running', self.process.id, self.process.name) is False:
177
- raise RunStopException(self.name)
189
+ all_laplacians.append(self.process_single_image(img, self.n_levels))
190
+ self.after_step(i + n + 1)
191
+ self.check_running()
178
192
  stacked_image = self.collapse(self.fuse_pyramids(all_laplacians))
179
193
  return stacked_image.astype(self.dtype)