shinestacker 1.0.2__tar.gz → 1.0.4__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 (136) hide show
  1. {shinestacker-1.0.2 → shinestacker-1.0.4}/CHANGELOG.md +21 -0
  2. {shinestacker-1.0.2/src/shinestacker.egg-info → shinestacker-1.0.4}/PKG-INFO +2 -1
  3. {shinestacker-1.0.2 → shinestacker-1.0.4}/README.md +1 -0
  4. shinestacker-1.0.4/src/shinestacker/_version.py +1 -0
  5. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/algorithms/exif.py +10 -13
  6. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/algorithms/multilayer.py +7 -6
  7. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/algorithms/stack_framework.py +1 -1
  8. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/algorithms/utils.py +47 -7
  9. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/app/main.py +99 -2
  10. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/gui_run.py +5 -4
  11. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/main_window.py +2 -2
  12. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/new_project.py +6 -11
  13. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/project_controller.py +6 -5
  14. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/file_loader.py +3 -4
  15. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/image_editor_ui.py +1 -2
  16. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/io_gui_handler.py +17 -22
  17. {shinestacker-1.0.2 → shinestacker-1.0.4/src/shinestacker.egg-info}/PKG-INFO +2 -1
  18. shinestacker-1.0.2/src/shinestacker/_version.py +0 -1
  19. {shinestacker-1.0.2 → shinestacker-1.0.4}/.coveragerc +0 -0
  20. {shinestacker-1.0.2 → shinestacker-1.0.4}/.flake8 +0 -0
  21. {shinestacker-1.0.2 → shinestacker-1.0.4}/.github/workflows/ci-multiplatform.yml +0 -0
  22. {shinestacker-1.0.2 → shinestacker-1.0.4}/.github/workflows/pylint.yml +0 -0
  23. {shinestacker-1.0.2 → shinestacker-1.0.4}/.github/workflows/pypi-publish.yml +0 -0
  24. {shinestacker-1.0.2 → shinestacker-1.0.4}/.github/workflows/release.yml +0 -0
  25. {shinestacker-1.0.2 → shinestacker-1.0.4}/.gitignore +0 -0
  26. {shinestacker-1.0.2 → shinestacker-1.0.4}/.pylintrc +0 -0
  27. {shinestacker-1.0.2 → shinestacker-1.0.4}/.readthedocs.yaml +0 -0
  28. {shinestacker-1.0.2 → shinestacker-1.0.4}/LICENSE +0 -0
  29. {shinestacker-1.0.2 → shinestacker-1.0.4}/MANIFEST.in +0 -0
  30. {shinestacker-1.0.2 → shinestacker-1.0.4}/THIRD_PARTY_LICENSES.txt +0 -0
  31. {shinestacker-1.0.2 → shinestacker-1.0.4}/docs/alignment.md +0 -0
  32. {shinestacker-1.0.2 → shinestacker-1.0.4}/docs/api.md +0 -0
  33. {shinestacker-1.0.2 → shinestacker-1.0.4}/docs/balancing.md +0 -0
  34. {shinestacker-1.0.2 → shinestacker-1.0.4}/docs/conf.py +0 -0
  35. {shinestacker-1.0.2 → shinestacker-1.0.4}/docs/focus_stacking.md +0 -0
  36. {shinestacker-1.0.2 → shinestacker-1.0.4}/docs/gui.md +0 -0
  37. {shinestacker-1.0.2 → shinestacker-1.0.4}/docs/index.md +0 -0
  38. {shinestacker-1.0.2 → shinestacker-1.0.4}/docs/job.md +0 -0
  39. {shinestacker-1.0.2 → shinestacker-1.0.4}/docs/main.md +0 -0
  40. {shinestacker-1.0.2 → shinestacker-1.0.4}/docs/multilayer.md +0 -0
  41. {shinestacker-1.0.2 → shinestacker-1.0.4}/docs/noise.md +0 -0
  42. {shinestacker-1.0.2 → shinestacker-1.0.4}/docs/requirements.txt +0 -0
  43. {shinestacker-1.0.2 → shinestacker-1.0.4}/docs/vignetting.md +0 -0
  44. {shinestacker-1.0.2 → shinestacker-1.0.4}/img/coffee.gif +0 -0
  45. {shinestacker-1.0.2 → shinestacker-1.0.4}/img/coffee_stack.jpg +0 -0
  46. {shinestacker-1.0.2 → shinestacker-1.0.4}/img/extreme-vignetting.jpg +0 -0
  47. {shinestacker-1.0.2 → shinestacker-1.0.4}/img/flies.gif +0 -0
  48. {shinestacker-1.0.2 → shinestacker-1.0.4}/img/flies_stack.jpg +0 -0
  49. {shinestacker-1.0.2 → shinestacker-1.0.4}/img/flow-diagram.png +0 -0
  50. {shinestacker-1.0.2 → shinestacker-1.0.4}/img/gui-finder.png +0 -0
  51. {shinestacker-1.0.2 → shinestacker-1.0.4}/img/gui-project-new.png +0 -0
  52. {shinestacker-1.0.2 → shinestacker-1.0.4}/img/gui-project-run.png +0 -0
  53. {shinestacker-1.0.2 → shinestacker-1.0.4}/img/gui-retouch.png +0 -0
  54. {shinestacker-1.0.2 → shinestacker-1.0.4}/pyproject.toml +0 -0
  55. {shinestacker-1.0.2 → shinestacker-1.0.4}/requirements.txt +0 -0
  56. {shinestacker-1.0.2 → shinestacker-1.0.4}/scripts/build_release.py +0 -0
  57. {shinestacker-1.0.2 → shinestacker-1.0.4}/scripts/git-rev-list.sh +0 -0
  58. {shinestacker-1.0.2 → shinestacker-1.0.4}/scripts/validate-tomli.py +0 -0
  59. {shinestacker-1.0.2 → shinestacker-1.0.4}/setup.cfg +0 -0
  60. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/__init__.py +0 -0
  61. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/algorithms/__init__.py +0 -0
  62. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/algorithms/align.py +0 -0
  63. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/algorithms/balance.py +0 -0
  64. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/algorithms/base_stack_algo.py +0 -0
  65. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/algorithms/denoise.py +0 -0
  66. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/algorithms/depth_map.py +0 -0
  67. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/algorithms/noise_detection.py +0 -0
  68. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/algorithms/pyramid.py +0 -0
  69. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/algorithms/sharpen.py +0 -0
  70. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/algorithms/stack.py +0 -0
  71. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/algorithms/vignetting.py +0 -0
  72. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/algorithms/white_balance.py +0 -0
  73. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/app/__init__.py +0 -0
  74. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/app/about_dialog.py +0 -0
  75. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/app/gui_utils.py +0 -0
  76. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/app/help_menu.py +0 -0
  77. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/app/open_frames.py +0 -0
  78. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/app/project.py +0 -0
  79. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/app/retouch.py +0 -0
  80. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/config/__init__.py +0 -0
  81. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/config/config.py +0 -0
  82. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/config/constants.py +0 -0
  83. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/config/gui_constants.py +0 -0
  84. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/core/__init__.py +0 -0
  85. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/core/colors.py +0 -0
  86. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/core/core_utils.py +0 -0
  87. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/core/exceptions.py +0 -0
  88. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/core/framework.py +0 -0
  89. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/core/logging.py +0 -0
  90. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/__init__.py +0 -0
  91. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/action_config.py +0 -0
  92. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/action_config_dialog.py +0 -0
  93. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/base_form_dialog.py +0 -0
  94. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/colors.py +0 -0
  95. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/gui_images.py +0 -0
  96. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/gui_logging.py +0 -0
  97. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
  98. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
  99. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
  100. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/ico/shinestacker.png +0 -0
  101. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/ico/shinestacker.svg +0 -0
  102. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
  103. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
  104. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
  105. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
  106. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/menu_manager.py +0 -0
  107. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/project_converter.py +0 -0
  108. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/project_editor.py +0 -0
  109. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/project_model.py +0 -0
  110. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/select_path_widget.py +0 -0
  111. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/tab_widget.py +0 -0
  112. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/gui/time_progress_bar.py +0 -0
  113. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/__init__.py +0 -0
  114. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/base_filter.py +0 -0
  115. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/brush.py +0 -0
  116. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/brush_gradient.py +0 -0
  117. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/brush_preview.py +0 -0
  118. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/brush_tool.py +0 -0
  119. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/denoise_filter.py +0 -0
  120. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/display_manager.py +0 -0
  121. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/exif_data.py +0 -0
  122. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/filter_manager.py +0 -0
  123. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/icon_container.py +0 -0
  124. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/image_viewer.py +0 -0
  125. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/io_manager.py +0 -0
  126. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/layer_collection.py +0 -0
  127. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/shortcuts_help.py +0 -0
  128. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/undo_manager.py +0 -0
  129. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/unsharp_mask_filter.py +0 -0
  130. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/vignetting_filter.py +0 -0
  131. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker/retouch/white_balance_filter.py +0 -0
  132. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker.egg-info/SOURCES.txt +0 -0
  133. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker.egg-info/dependency_links.txt +0 -0
  134. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker.egg-info/entry_points.txt +0 -0
  135. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker.egg-info/requires.txt +0 -0
  136. {shinestacker-1.0.2 → shinestacker-1.0.4}/src/shinestacker.egg-info/top_level.txt +0 -0
@@ -2,6 +2,27 @@
2
2
 
3
3
  This page reports the main releases only and the main changes therein.
4
4
 
5
+ ## [v1.0.4] - 2025-08-26
6
+ **Bug fixes**
7
+
8
+ ### Changes
9
+
10
+ * extensions are treated in lower case (e.g.: both jpg and JPG)
11
+ * added retouch menu action: import frames from current project
12
+
13
+ ---
14
+ ## [v1.0.3] - 2025-08-26
15
+ **Bug fixes**
16
+
17
+ ### Changes
18
+
19
+ * fixed menu text
20
+ * fixed crash multilayer module
21
+ * fixed multilayer module
22
+ * code cleanup
23
+
24
+ ---
25
+
5
26
  ## [v1.0.2] - 2025-08-25
6
27
  **Bug fixes**
7
28
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -87,6 +87,7 @@ Pyramid methods in image processing
87
87
  # License
88
88
 
89
89
  <img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
90
+
90
91
  - **Code**: The software is provided as is under the [GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0.en.html). See [LICENSE](https://github.com/lucalista/shinestacker/blob/main/LICENSE) for details.
91
92
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/src/shinestacker/gui/ico/shinestacker.png' width="150" referrerpolicy="no-referrer" alt="Shine Stacker Logo">
92
93
 
@@ -56,6 +56,7 @@ Pyramid methods in image processing
56
56
  # License
57
57
 
58
58
  <img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
59
+
59
60
  - **Code**: The software is provided as is under the [GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0.en.html). See [LICENSE](https://github.com/lucalista/shinestacker/blob/main/LICENSE) for details.
60
61
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/src/shinestacker/gui/ico/shinestacker.png' width="150" referrerpolicy="no-referrer" alt="Shine Stacker Logo">
61
62
 
@@ -0,0 +1 @@
1
+ __version__ = '1.0.4'
@@ -10,7 +10,7 @@ from PIL.TiffImagePlugin import IFDRational
10
10
  from PIL.ExifTags import TAGS
11
11
  import tifffile
12
12
  from .. config.constants import constants
13
- from .utils import write_img
13
+ from .utils import write_img, extension_jpg, extension_tif, extension_png
14
14
 
15
15
  IMAGEWIDTH = 256
16
16
  IMAGELENGTH = 257
@@ -48,11 +48,10 @@ def extract_enclosed_data_for_jpg(data, head, foot):
48
48
  def get_exif(exif_filename):
49
49
  if not os.path.isfile(exif_filename):
50
50
  raise RuntimeError(f"File does not exist: {exif_filename}")
51
- ext = exif_filename.split(".")[-1]
52
51
  image = Image.open(exif_filename)
53
- if ext in ('tif', 'tiff'):
52
+ if extension_tif(exif_filename):
54
53
  return image.tag_v2 if hasattr(image, 'tag_v2') else image.getexif()
55
- if ext in ('jpeg', 'jpg'):
54
+ if extension_jpg(exif_filename):
56
55
  exif_data = image.getexif()
57
56
  with open(exif_filename, 'rb') as f:
58
57
  data = extract_enclosed_data_for_jpg(f.read(), b'<?xpacket', b'<?xpacket end="w"?>')
@@ -158,42 +157,40 @@ def write_image_with_exif_data(exif, image, out_filename, verbose=False):
158
157
  if exif is None:
159
158
  write_img(out_filename, image)
160
159
  return None
161
- ext = out_filename.split(".")[-1]
162
160
  if verbose:
163
161
  print_exif(exif)
164
- if ext in ('jpeg', 'jpg'):
162
+ if extension_jpg(out_filename):
165
163
  cv2.imwrite(out_filename, image, [int(cv2.IMWRITE_JPEG_QUALITY), 100])
166
164
  add_exif_data_to_jpg_file(exif, out_filename, out_filename, verbose)
167
- elif ext in ('tiff', 'tif'):
165
+ elif extension_tif(out_filename):
168
166
  metadata = {"description": f"image generated with {constants.APP_STRING} package"}
169
167
  extra_tags, exif_tags = exif_extra_tags_for_tif(exif)
170
168
  tifffile.imwrite(out_filename, image, metadata=metadata, compression='adobe_deflate',
171
169
  extratags=extra_tags, **exif_tags)
172
- elif ext == 'png':
170
+ elif extension_png(out_filename):
173
171
  image.save(out_filename, 'PNG', exif=exif, quality=100)
174
172
  return exif
175
173
 
176
174
 
177
175
  def save_exif_data(exif, in_filename, out_filename=None, verbose=False):
178
- ext = in_filename.split(".")[-1]
179
176
  if out_filename is None:
180
177
  out_filename = in_filename
181
178
  if exif is None:
182
179
  raise RuntimeError('No exif data provided.')
183
180
  if verbose:
184
181
  print_exif(exif)
185
- if ext in ('tiff', 'tif'):
182
+ if extension_tif(in_filename):
186
183
  image_new = tifffile.imread(in_filename)
187
184
  else:
188
185
  image_new = Image.open(in_filename)
189
- if ext in ('jpeg', 'jpg'):
186
+ if extension_jpg(in_filename):
190
187
  add_exif_data_to_jpg_file(exif, in_filename, out_filename, verbose)
191
- elif ext in ('tiff', 'tif'):
188
+ elif extension_tif(in_filename):
192
189
  metadata = {"description": f"image generated with {constants.APP_STRING} package"}
193
190
  extra_tags, exif_tags = exif_extra_tags_for_tif(exif)
194
191
  tifffile.imwrite(out_filename, image_new, metadata=metadata, compression='adobe_deflate',
195
192
  extratags=extra_tags, **exif_tags)
196
- elif ext == 'png':
193
+ elif extension_png(in_filename):
197
194
  image_new.save(out_filename, 'PNG', exif=exif, quality=100)
198
195
  return exif
199
196
 
@@ -13,6 +13,7 @@ from .. config.constants import constants
13
13
  from .. config.config import config
14
14
  from .. core.colors import color_str
15
15
  from .. core.framework import JobBase
16
+ from .utils import EXTENSIONS_TIF, EXTENSIONS_JPG, EXTENSIONS_PNG
16
17
  from .stack_framework import FrameMultiDirectory
17
18
  from .exif import exif_extra_tags_for_tif, get_exif
18
19
 
@@ -27,13 +28,13 @@ def write_multilayer_tiff(input_files, output_file, labels=None, exif_path='', c
27
28
  msg = ", ".join(extensions)
28
29
  raise RuntimeError("All input files must have the same extension. "
29
30
  f"Input list has the following extensions: {msg}.")
30
- extension = extensions[0]
31
- if extension in ('tif', 'tiff'):
31
+ extension = extensions[0].lower()
32
+ if extension in EXTENSIONS_TIF:
32
33
  images = [tifffile.imread(p) for p in input_files]
33
- elif extension in ('jpg', 'jpeg'):
34
+ elif extension in EXTENSIONS_JPG:
34
35
  images = [cv2.imread(p) for p in input_files]
35
36
  images = [cv2.cvtColor(i, cv2.COLOR_BGR2RGB) for i in images]
36
- elif extension == 'png':
37
+ elif extension in EXTENSIONS_PNG:
37
38
  images = [cv2.imread(p, cv2.IMREAD_UNCHANGED) for p in input_files]
38
39
  images = [cv2.cvtColor(i, cv2.COLOR_BGR2RGB) for i in images]
39
40
  if labels is None:
@@ -177,7 +178,7 @@ class MultiLayer(JobBase, FrameMultiDirectory):
177
178
  raise RuntimeError("input_path option must contain a path or an array of paths")
178
179
  if len(paths) == 0:
179
180
  self.print_message(color_str("no input paths specified",
180
- constants.LOG_COLOR_LEVEL_ALERT),
181
+ constants.LOG_COLOR_ALERT),
181
182
  level=logging.WARNING)
182
183
  return
183
184
  files = self.folder_filelist()
@@ -186,7 +187,7 @@ class MultiLayer(JobBase, FrameMultiDirectory):
186
187
  color_str(f"no input in {len(paths)} specified path" +
187
188
  ('s' if len(paths) > 1 else '') + ": "
188
189
  ", ".join([f"'{p}'" for p in paths]),
189
- constants.LOG_COLOR_LEVEL_ALERT),
190
+ constants.LOG_COLOR_ALERT),
190
191
  level=logging.WARNING)
191
192
  return
192
193
  self.print_message(color_str("merging frames in " + self.folder_list_str(),
@@ -92,7 +92,7 @@ class FramePaths:
92
92
  ('' if self.working_path[-1] == '/' else '/') + self.plot_path
93
93
  if not os.path.exists(self.plot_path):
94
94
  os.makedirs(self.plot_path)
95
- if self.input_path == '':
95
+ if self.input_path in ['', []]:
96
96
  if len(job.paths) == 0:
97
97
  raise RuntimeError(f"Job {job.name} does not have any configured path")
98
98
  self.input_path = job.paths[-1]
@@ -8,25 +8,65 @@ from .. config.config import config
8
8
  from .. core.exceptions import ShapeError, BitDepthError
9
9
 
10
10
 
11
+ def get_path_extension(path):
12
+ return os.path.splitext(path)[1].lstrip('.')
13
+
14
+
15
+ EXTENSIONS_TIF = ['tif', 'tiff']
16
+ EXTENSIONS_JPG = ['jpg', 'jpeg']
17
+ EXTENSIONS_PNG = ['png']
18
+ EXTENSIONS_PDF = ['pdf']
19
+
20
+
21
+ def extension_in(path, exts):
22
+ return get_path_extension(path).lower() in exts
23
+
24
+
25
+ def extension_tif(path):
26
+ return extension_in(path, EXTENSIONS_TIF)
27
+
28
+
29
+ def extension_jpg(path):
30
+ return extension_in(path, EXTENSIONS_JPG)
31
+
32
+
33
+ def extension_png(path):
34
+ return extension_in(path, EXTENSIONS_PNG)
35
+
36
+
37
+ def extension_pdf(path):
38
+ return extension_in(path, EXTENSIONS_PDF)
39
+
40
+
41
+ def extension_tif_jpg(path):
42
+ return extension_in(path, EXTENSIONS_TIF + EXTENSIONS_JPG)
43
+
44
+
45
+ def extension_tif_png(path):
46
+ return extension_in(path, EXTENSIONS_TIF + EXTENSIONS_PNG)
47
+
48
+
49
+ def extension_jpg_png(path):
50
+ return extension_in(path, EXTENSIONS_JPG + EXTENSIONS_PNG)
51
+
52
+
11
53
  def read_img(file_path):
12
54
  if not os.path.isfile(file_path):
13
55
  raise RuntimeError("File does not exist: " + file_path)
14
- ext = file_path.split(".")[-1]
15
56
  img = None
16
- if ext in ['jpeg', 'jpg']:
57
+ if extension_jpg(file_path):
17
58
  img = cv2.imread(file_path)
18
- elif ext in ['tiff', 'tif', 'png']:
59
+ elif extension_tif_png(file_path):
19
60
  img = cv2.imread(file_path, cv2.IMREAD_UNCHANGED)
20
61
  return img
21
62
 
22
63
 
23
64
  def write_img(file_path, img):
24
- ext = file_path.split(".")[-1]
25
- if ext in ['jpeg', 'jpg']:
65
+ if extension_jpg(file_path):
26
66
  cv2.imwrite(file_path, img, [int(cv2.IMWRITE_JPEG_QUALITY), 100])
27
- elif ext in ['tiff', 'tif']:
67
+ elif extension_tif(file_path):
28
68
  cv2.imwrite(file_path, img, [int(cv2.IMWRITE_TIFF_COMPRESSION), 1])
29
- elif ext == 'png':
69
+ elif extension_png(file_path):
30
70
  cv2.imwrite(file_path, img)
31
71
 
32
72
 
@@ -6,9 +6,10 @@ import argparse
6
6
  import matplotlib
7
7
  import matplotlib.backends.backend_pdf
8
8
  matplotlib.use('agg')
9
- from PySide6.QtWidgets import QApplication, QMainWindow, QStackedWidget, QMenu
9
+ from PySide6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QStackedWidget,
10
+ QMenu, QMessageBox, QDialog, QLabel, QListWidget, QPushButton)
10
11
  from PySide6.QtGui import QAction, QIcon, QGuiApplication
11
- from PySide6.QtCore import Qt, QEvent, QTimer
12
+ from PySide6.QtCore import Qt, QEvent, QTimer, Signal
12
13
  from shinestacker.config.config import config
13
14
  config.init(DISABLE_TQDM=True, COMBINED_APP=True, DONT_USE_NATIVE_MENU=True)
14
15
  from shinestacker.config.constants import constants
@@ -20,6 +21,59 @@ from shinestacker.app.help_menu import add_help_action
20
21
  from shinestacker.app.open_frames import open_frames
21
22
 
22
23
 
24
+ class SelectionDialog(QDialog):
25
+ selection_made = Signal(str, bool)
26
+
27
+ def __init__(self, title, message, items, parent=None):
28
+ super().__init__(parent)
29
+ self.setWindowTitle(title)
30
+ self.setModal(True)
31
+ self.selected_item = ""
32
+ self.setup_ui(message, items)
33
+ self.setMinimumSize(300, 300)
34
+
35
+ def setup_ui(self, message, items):
36
+ layout = QVBoxLayout(self)
37
+ if message:
38
+ label = QLabel(message)
39
+ layout.addWidget(label)
40
+ self.list_widget = QListWidget()
41
+ self.list_widget.addItems(items)
42
+ self.list_widget.itemSelectionChanged.connect(self.on_selection_changed)
43
+ layout.addWidget(self.list_widget)
44
+ button_layout = QHBoxLayout()
45
+ self.ok_button = QPushButton("OK")
46
+ self.ok_button.clicked.connect(self.accept)
47
+ self.ok_button.setEnabled(False)
48
+ button_layout.addWidget(self.ok_button)
49
+ cancel_button = QPushButton("Cancel")
50
+ cancel_button.clicked.connect(self.reject)
51
+ button_layout.addWidget(cancel_button)
52
+ layout.addLayout(button_layout)
53
+
54
+ def on_selection_changed(self):
55
+ selected_items = self.list_widget.selectedItems()
56
+ self.ok_button.setEnabled(len(selected_items) > 0)
57
+
58
+ def accept(self):
59
+ selected_items = self.list_widget.selectedItems()
60
+ if selected_items:
61
+ self.selected_item = selected_items[0].text()
62
+ self.selection_made.emit(self.selected_item, True)
63
+ super().accept()
64
+
65
+ def reject(self):
66
+ self.selected_item = ""
67
+ self.selection_made.emit("", False)
68
+ super().reject()
69
+
70
+ @staticmethod
71
+ def get_selection(title, message, items, parent=None):
72
+ dialog = SelectionDialog(title, message, items, parent)
73
+ result = dialog.exec()
74
+ return dialog.selected_item if result == QDialog.Accepted else ""
75
+
76
+
23
77
  class MainApp(QMainWindow):
24
78
  def __init__(self):
25
79
  super().__init__()
@@ -41,6 +95,17 @@ class MainApp(QMainWindow):
41
95
  self.retouch_window.menuBar().actions()[0], self.app_menu)
42
96
  add_help_action(self.project_window)
43
97
  add_help_action(self.retouch_window)
98
+ file_menu = None
99
+ for action in self.retouch_window.menuBar().actions():
100
+ if action.text() == "&File":
101
+ file_menu = action.menu()
102
+ break
103
+ if file_menu is not None:
104
+ import_action = QAction("Import From Current Project", self)
105
+ import_action.triggered.connect(self.import_from_project)
106
+ file_menu.addAction(import_action)
107
+ else:
108
+ raise RuntimeError("File menu not found!")
44
109
 
45
110
  def switch_to_project(self):
46
111
  self.switch_app(0)
@@ -91,6 +156,38 @@ class MainApp(QMainWindow):
91
156
  else:
92
157
  self.retouch_window.io_gui_handler.open_file(filename)
93
158
 
159
+ def import_from_project(self):
160
+ project = self.project_window.project()
161
+ if project is None:
162
+ QMessageBox.warning(self.parent(),
163
+ "No Active Project", "No project has been created or opened.")
164
+ return
165
+ if len(project.jobs) == 0:
166
+ QMessageBox.warning(self.parent(),
167
+ "No Jobs In Project", "The current project has no job. "
168
+ "Create and run a job first.")
169
+ return
170
+ if len(project.jobs) > 1:
171
+ job_names = [job.params['name'] for job in project.jobs]
172
+ job_name = SelectionDialog.get_selection(
173
+ "Job Selection",
174
+ "Please select one of the active jobs:",
175
+ job_names
176
+ )
177
+ job = None
178
+ for job in project.jobs:
179
+ if job.params['name'] == job_name:
180
+ break
181
+ if job is None:
182
+ return
183
+ else:
184
+ job = project.jobs[0]
185
+ retouch_path = self.project_window.get_retouch_path(job)
186
+ if isinstance(retouch_path, list):
187
+ open_frames(self.retouch_window, None, ";".join(retouch_path))
188
+ else:
189
+ self.retouch_window.io_gui_handler.open_file(retouch_path)
190
+
94
191
 
95
192
  class Application(QApplication):
96
193
  def event(self, event):
@@ -1,4 +1,5 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0903, R0915, R0914, R0917, R0913, R0902
2
+ import os
2
3
  from PySide6.QtWidgets import (QWidget, QPushButton, QVBoxLayout, QHBoxLayout,
3
4
  QMessageBox, QScrollArea, QSizePolicy, QFrame, QLabel, QComboBox)
4
5
  from PySide6.QtGui import QColor
@@ -7,6 +8,7 @@ from PySide6.QtCore import Signal, Slot
7
8
  from .. config.constants import constants
8
9
  from .. config.gui_constants import gui_constants
9
10
  from .colors import RED_BUTTON_STYLE, BLUE_BUTTON_STYLE, BLUE_COMBO_STYLE
11
+ from .. algorithms.utils import extension_tif_jpg, extension_pdf
10
12
  from .gui_logging import LogWorker, QTextEditLogger
11
13
  from .gui_images import GuiPdfView, GuiImageView, GuiOpenApp
12
14
  from .colors import (
@@ -200,13 +202,12 @@ class RunWindow(QTextEditLogger):
200
202
  label = QLabel(name, self)
201
203
  label.setStyleSheet("QLabel {margin-top: 5px; font-weight: bold;}")
202
204
  self.image_layout.addWidget(label)
203
- ext = path.split('.')[-1].lower()
204
- if ext == 'pdf':
205
+ if extension_pdf(path):
205
206
  image_view = GuiPdfView(path, self)
206
- elif ext in ['jpg', 'jpeg', 'tif', 'tiff', 'png']:
207
+ elif extension_tif_jpg(path):
207
208
  image_view = GuiImageView(path, self)
208
209
  else:
209
- raise RuntimeError("Can't visualize file type {ext}.")
210
+ raise RuntimeError(f"Can't visualize file type {os.path.splitext(path)[1]}.")
210
211
  self.image_views.append(image_view)
211
212
  self.image_layout.addWidget(image_view)
212
213
  max_width = max(pv.size().width() for pv in self.image_views) if self.image_views else 0
@@ -278,7 +278,7 @@ class MainWindow(QMainWindow, LogManager):
278
278
  current_action = None
279
279
  if item:
280
280
  index = self.job_list().row(item)
281
- current_action = self.get_job_at(index)
281
+ current_action = self.project_editor.get_job_at(index)
282
282
  self.set_current_job(index)
283
283
  item = self.action_list().itemAt(self.action_list().viewport().mapFrom(self, event.pos()))
284
284
  if item:
@@ -328,7 +328,7 @@ class MainWindow(QMainWindow, LogManager):
328
328
  self.current_action_output_path = f"{self.current_action_working_path}/{op}"
329
329
  if os.path.exists(self.current_action_output_path):
330
330
  action_name = "Browse Output Path" + (f" > {name}" if name != '' else '')
331
- n_files = len(next(os.walk(op))[2])
331
+ n_files = len(next(os.walk(self.current_action_output_path))[2])
332
332
  s = "" if n_files == 1 else "s"
333
333
  action_name += f" ({n_files} file{s})"
334
334
  self.browse_output_path_action = QAction(action_name)
@@ -7,13 +7,12 @@ from PySide6.QtGui import QIcon
7
7
  from PySide6.QtCore import Qt
8
8
  from .. config.gui_constants import gui_constants
9
9
  from .. config.constants import constants
10
- from .. algorithms.utils import read_img
10
+ from .. algorithms.utils import read_img, extension_tif_jpg
11
11
  from .. algorithms.stack import get_bunches
12
12
  from .select_path_widget import create_select_file_paths_widget
13
13
  from .base_form_dialog import BaseFormDialog
14
14
 
15
15
  DEFAULT_NO_COUNT_LABEL = " - "
16
- EXTENSIONS = ['jpg', 'jpeg', 'tif', 'tiff']
17
16
 
18
17
 
19
18
  class NewProjectDialog(BaseFormDialog):
@@ -133,10 +132,8 @@ class NewProjectDialog(BaseFormDialog):
133
132
  return 0
134
133
  count = 0
135
134
  for filename in os.listdir(path):
136
- if '.' in filename:
137
- ext = filename.lower().split('.')[-1]
138
- if ext in EXTENSIONS:
139
- count += 1
135
+ if extension_tif_jpg(filename):
136
+ count += 1
140
137
  return count
141
138
 
142
139
  self.n_image_files = count_image_files(self.input_folder.text())
@@ -173,11 +170,9 @@ class NewProjectDialog(BaseFormDialog):
173
170
  file_path = None
174
171
  for filename in files:
175
172
  full_path = os.path.join(path, filename)
176
- if os.path.isfile(full_path):
177
- ext = full_path.split(".")[-1].lower()
178
- if ext in EXTENSIONS:
179
- file_path = full_path
180
- break
173
+ if extension_tif_jpg(full_path):
174
+ file_path = full_path
175
+ break
181
176
  if file_path is None:
182
177
  QMessageBox.warning(
183
178
  self, "Invalid input", "Could not find images now in the selected path")
@@ -204,11 +204,12 @@ class ProjectController(QObject):
204
204
  input_path.append("focus-stack-depth-map")
205
205
  if dialog.get_bunch_stack():
206
206
  input_path.append("bunches")
207
- else:
208
- input_path.append(input_path)
209
- multi_layer = ActionConfig(constants.ACTION_MULTILAYER,
210
- {'name': 'multi-layer',
211
- 'input_path': ','.join(input_path)})
207
+ multi_layer = ActionConfig(
208
+ constants.ACTION_MULTILAYER,
209
+ {
210
+ 'name': 'multi-layer',
211
+ 'input_path': constants.PATH_SEPARATOR.join(input_path)
212
+ })
212
213
  job.add_sub_action(multi_layer)
213
214
  self.add_job_to_project(job)
214
215
  self.mark_as_modified(True)
@@ -5,7 +5,7 @@ import numpy as np
5
5
  import cv2
6
6
  from psdtags import PsdChannelId
7
7
  from PySide6.QtCore import QThread, Signal
8
- from .. algorithms.utils import read_img
8
+ from .. algorithms.utils import read_img, extension_tif, extension_jpg
9
9
  from .. algorithms.multilayer import read_multilayer_tiff
10
10
 
11
11
 
@@ -50,15 +50,14 @@ class FileLoader(QThread):
50
50
  raise RuntimeError(f"Path {path} does not exist.")
51
51
  if not os.path.isfile(path):
52
52
  raise RuntimeError(f"Path {path} is not a file.")
53
- extension = path.split('.')[-1]
54
- if extension in ['jpg', 'jpeg']:
53
+ if extension_jpg(path):
55
54
  try:
56
55
  stack = np.array([cv2.cvtColor(read_img(path), cv2.COLOR_BGR2RGB)])
57
56
  return stack, [path.split('/')[-1].split('.')[0]]
58
57
  except Exception as e:
59
58
  traceback.print_tb(e.__traceback__)
60
59
  return None, None
61
- elif extension in ['tif', 'tiff']:
60
+ elif extension_tif(path):
62
61
  try:
63
62
  psd_data = read_multilayer_tiff(path)
64
63
  layers = []
@@ -330,9 +330,8 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
330
330
  view_individual_action.setShortcut("L")
331
331
  view_individual_action.triggered.connect(self.set_view_individual)
332
332
  view_menu.addAction(view_individual_action)
333
- view_menu.addSeparator()
334
333
 
335
- toggle_view_master_individual_action = QAction("View Individual", self)
334
+ toggle_view_master_individual_action = QAction("Toggle Master/Individual", self)
336
335
  toggle_view_master_individual_action.setShortcut("T")
337
336
  toggle_view_master_individual_action.triggered.connect(self.toggle_view_master_individual)
338
337
  view_menu.addAction(toggle_view_master_individual_action)
@@ -56,9 +56,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
56
56
  self.set_master_layer(master_layer)
57
57
  self.undo_manager.reset()
58
58
  self.blank_layer = np.zeros(master_layer.shape[:2])
59
- self.finish_loading_setup(
60
- stack, None, master_layer, False,
61
- f"Loaded: {self.current_file_path()}")
59
+ self.finish_loading_setup(f"Loaded: {self.current_file_path()}")
62
60
 
63
61
  def on_file_error(self, error_msg):
64
62
  QApplication.restoreOverrideCursor()
@@ -137,26 +135,23 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
137
135
  msg.setText(str(e))
138
136
  msg.exec()
139
137
  return
140
- self.finish_loading_setup(
141
- stack, labels, master, True,
142
- "Selected frames imported")
143
-
144
- def finish_loading_setup(self, stack, labels, master, add_layers, message):
145
- if add_layers:
146
- if self.layer_stack() is None and len(stack) > 0:
147
- self.set_layer_stack(np.array(stack))
148
- if labels is None:
149
- labels = self.layer_labels()
150
- else:
151
- self.set_layer_labels(labels)
152
- self.set_master_layer(master)
153
- self.blank_layer = np.zeros(master.shape[:2])
138
+ if self.layer_stack() is None and len(stack) > 0:
139
+ self.set_layer_stack(np.array(stack))
140
+ if labels is None:
141
+ labels = self.layer_labels()
154
142
  else:
155
- if labels is None:
156
- labels = self.layer_labels()
157
- for img, label in zip(stack, labels):
158
- self.add_layer_label(label)
159
- self.add_layer(img)
143
+ self.set_layer_labels(labels)
144
+ self.set_master_layer(master)
145
+ self.blank_layer = np.zeros(master.shape[:2])
146
+ else:
147
+ if labels is None:
148
+ labels = self.layer_labels()
149
+ for img, label in zip(stack, labels):
150
+ self.add_layer_label(label)
151
+ self.add_layer(img)
152
+ self.finish_loading_setup("Selected frames imported")
153
+
154
+ def finish_loading_setup(self, message):
160
155
  self.display_manager.update_thumbnails()
161
156
  self.mark_as_modified_requested.emit(True)
162
157
  self.change_layer_requested.emit(0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -87,6 +87,7 @@ Pyramid methods in image processing
87
87
  # License
88
88
 
89
89
  <img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
90
+
90
91
  - **Code**: The software is provided as is under the [GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0.en.html). See [LICENSE](https://github.com/lucalista/shinestacker/blob/main/LICENSE) for details.
91
92
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/src/shinestacker/gui/ico/shinestacker.png' width="150" referrerpolicy="no-referrer" alt="Shine Stacker Logo">
92
93
 
@@ -1 +0,0 @@
1
- __version__ = '1.0.2'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes