shinestacker 0.2.0.post1.dev1__py3-none-any.whl

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 (67) hide show
  1. shinestacker/__init__.py +3 -0
  2. shinestacker/_version.py +1 -0
  3. shinestacker/algorithms/__init__.py +14 -0
  4. shinestacker/algorithms/align.py +307 -0
  5. shinestacker/algorithms/balance.py +367 -0
  6. shinestacker/algorithms/core_utils.py +22 -0
  7. shinestacker/algorithms/depth_map.py +164 -0
  8. shinestacker/algorithms/exif.py +238 -0
  9. shinestacker/algorithms/multilayer.py +187 -0
  10. shinestacker/algorithms/noise_detection.py +182 -0
  11. shinestacker/algorithms/pyramid.py +176 -0
  12. shinestacker/algorithms/stack.py +112 -0
  13. shinestacker/algorithms/stack_framework.py +248 -0
  14. shinestacker/algorithms/utils.py +71 -0
  15. shinestacker/algorithms/vignetting.py +137 -0
  16. shinestacker/app/__init__.py +0 -0
  17. shinestacker/app/about_dialog.py +24 -0
  18. shinestacker/app/app_config.py +39 -0
  19. shinestacker/app/gui_utils.py +35 -0
  20. shinestacker/app/help_menu.py +16 -0
  21. shinestacker/app/main.py +176 -0
  22. shinestacker/app/open_frames.py +39 -0
  23. shinestacker/app/project.py +91 -0
  24. shinestacker/app/retouch.py +82 -0
  25. shinestacker/config/__init__.py +4 -0
  26. shinestacker/config/config.py +53 -0
  27. shinestacker/config/constants.py +174 -0
  28. shinestacker/config/gui_constants.py +85 -0
  29. shinestacker/core/__init__.py +5 -0
  30. shinestacker/core/colors.py +60 -0
  31. shinestacker/core/core_utils.py +52 -0
  32. shinestacker/core/exceptions.py +50 -0
  33. shinestacker/core/framework.py +210 -0
  34. shinestacker/core/logging.py +89 -0
  35. shinestacker/gui/__init__.py +0 -0
  36. shinestacker/gui/action_config.py +879 -0
  37. shinestacker/gui/actions_window.py +283 -0
  38. shinestacker/gui/colors.py +57 -0
  39. shinestacker/gui/gui_images.py +152 -0
  40. shinestacker/gui/gui_logging.py +213 -0
  41. shinestacker/gui/gui_run.py +393 -0
  42. shinestacker/gui/img/close-round-line-icon.png +0 -0
  43. shinestacker/gui/img/forward-button-icon.png +0 -0
  44. shinestacker/gui/img/play-button-round-icon.png +0 -0
  45. shinestacker/gui/img/plus-round-line-icon.png +0 -0
  46. shinestacker/gui/main_window.py +599 -0
  47. shinestacker/gui/new_project.py +170 -0
  48. shinestacker/gui/project_converter.py +148 -0
  49. shinestacker/gui/project_editor.py +539 -0
  50. shinestacker/gui/project_model.py +138 -0
  51. shinestacker/retouch/__init__.py +0 -0
  52. shinestacker/retouch/brush.py +9 -0
  53. shinestacker/retouch/brush_controller.py +57 -0
  54. shinestacker/retouch/brush_preview.py +126 -0
  55. shinestacker/retouch/exif_data.py +65 -0
  56. shinestacker/retouch/file_loader.py +104 -0
  57. shinestacker/retouch/image_editor.py +651 -0
  58. shinestacker/retouch/image_editor_ui.py +380 -0
  59. shinestacker/retouch/image_viewer.py +356 -0
  60. shinestacker/retouch/shortcuts_help.py +98 -0
  61. shinestacker/retouch/undo_manager.py +38 -0
  62. shinestacker-0.2.0.post1.dev1.dist-info/METADATA +55 -0
  63. shinestacker-0.2.0.post1.dev1.dist-info/RECORD +67 -0
  64. shinestacker-0.2.0.post1.dev1.dist-info/WHEEL +5 -0
  65. shinestacker-0.2.0.post1.dev1.dist-info/entry_points.txt +4 -0
  66. shinestacker-0.2.0.post1.dev1.dist-info/licenses/LICENSE +1 -0
  67. shinestacker-0.2.0.post1.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,53 @@
1
+ class _Config:
2
+ _initialized = False
3
+ _instance = None
4
+
5
+ def __new__(cls):
6
+ if cls._instance is None:
7
+ cls._instance = super().__new__(cls)
8
+ cls._instance._init_defaults()
9
+ return cls._instance
10
+
11
+ def _init_defaults(self):
12
+ self._DISABLE_TQDM = False
13
+ self._COMBINED_APP = False
14
+ self._DONT_USE_NATIVE_MENU = True
15
+ try:
16
+ __IPYTHON__ # noqa
17
+ self._JUPYTER_NOTEBOOK = True
18
+ except Exception:
19
+ self._JUPYTER_NOTEBOOK = False
20
+
21
+ def init(self, **kwargs):
22
+ if self._initialized:
23
+ raise RuntimeError("Config already initialized")
24
+ for k, v in kwargs.items():
25
+ if hasattr(self, f"_{k}"):
26
+ setattr(self, f"_{k}", v)
27
+ else:
28
+ raise AttributeError(f"Invalid config key: {k}")
29
+ self._initialized = True
30
+
31
+ @property
32
+ def DISABLE_TQDM(self):
33
+ return self._DISABLE_TQDM
34
+
35
+ @property
36
+ def JUPYTER_NOTEBOOK(self):
37
+ return self._JUPYTER_NOTEBOOK
38
+
39
+ @property
40
+ def DONT_USE_NATIVE_MENU(self):
41
+ return self._DONT_USE_NATIVE_MENU
42
+
43
+ @property
44
+ def COMBINED_APP(self):
45
+ return self._COMBINED_APP
46
+
47
+ def __setattr__(self, name, value):
48
+ if self._initialized and name.startswith('_'):
49
+ raise AttributeError("Can't change config after initialization")
50
+ super().__setattr__(name, value)
51
+
52
+
53
+ config = _Config()
@@ -0,0 +1,174 @@
1
+ import sys
2
+ import re
3
+
4
+
5
+ class _Constants:
6
+ APP_TITLE = "Shine Stacker"
7
+ APP_STRING = "ShineStacker"
8
+ EXTENSIONS = set(["jpeg", "jpg", "png", "tif", "tiff"])
9
+
10
+ NUM_UINT8 = 256
11
+ NUM_UINT16 = 65536
12
+ MAX_UINT8 = 255
13
+ MAX_UINT16 = 65535
14
+
15
+ LOG_FONTS = ['Monaco', 'Menlo', ' Lucida Console', 'Courier New', 'Courier', 'monospace']
16
+ LOG_FONTS_STR = ", ".join(LOG_FONTS)
17
+
18
+ ANSI_ESCAPE = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
19
+
20
+ ACTION_JOB = "Job"
21
+ ACTION_COMBO = "CombinedActions"
22
+ ACTION_NOISEDETECTION = "NoiseDetection"
23
+ ACTION_FOCUSSTACK = "FocusStack"
24
+ ACTION_FOCUSSTACKBUNCH = "FocusStackBunch"
25
+ ACTION_MULTILAYER = "MultiLayer"
26
+ ACTION_TYPES = [ACTION_COMBO, ACTION_FOCUSSTACKBUNCH, ACTION_FOCUSSTACK,
27
+ ACTION_MULTILAYER, ACTION_NOISEDETECTION]
28
+ COMPOSITE_TYPES = [ACTION_COMBO]
29
+ ACTION_MASKNOISE = "MaskNoise"
30
+ ACTION_VIGNETTING = "Vignetting"
31
+ ACTION_ALIGNFRAMES = "AlignFrames"
32
+ ACTION_BALANCEFRAMES = "BalanceFrames"
33
+ SUB_ACTION_TYPES = [ACTION_MASKNOISE, ACTION_VIGNETTING, ACTION_ALIGNFRAMES,
34
+ ACTION_BALANCEFRAMES]
35
+ STACK_ALGO_PYRAMID = 'Pyramid'
36
+ STACK_ALGO_DEPTH_MAP = 'Depth map'
37
+ STACK_ALGO_OPTIONS = [STACK_ALGO_PYRAMID, STACK_ALGO_DEPTH_MAP]
38
+ STACK_ALGO_DEFAULT = STACK_ALGO_PYRAMID
39
+ DEFAULT_PLOTS_PATH = 'plots'
40
+
41
+ PATH_SEPARATOR = ';'
42
+
43
+ DEFAULT_FILE_REVERSE_ORDER = False
44
+ DEFAULT_MULTILAYER_FILE_REVERSE_ORDER = True
45
+
46
+ DEFAULT_NOISE_MAP_FILENAME = "noise-map/hot_pixels.png"
47
+ DEFAULT_MN_KERNEL_SIZE = 3
48
+ INTERPOLATE_MEAN = 'MEAN'
49
+ INTERPOLATE_MEDIAN = 'MEDIAN'
50
+ RGB_LABELS = ['r', 'g', 'b']
51
+ RGBA_LABELS = ['r', 'g', 'b', 'a']
52
+ DEFAULT_CHANNEL_THRESHOLDS = [13, 13, 13]
53
+ DEFAULT_BLUR_SIZE = 5
54
+ DEFAULT_NOISE_PLOT_RANGE = [5, 30]
55
+ VALID_INTERPOLATE = {INTERPOLATE_MEAN, INTERPOLATE_MEDIAN}
56
+
57
+ ALIGN_HOMOGRAPHY = "ALIGN_HOMOGRAPHY"
58
+ ALIGN_RIGID = "ALIGN_RIGID"
59
+ BORDER_CONSTANT = "BORDER_CONSTANT"
60
+ BORDER_REPLICATE = "BORDER_REPLICATE"
61
+ BORDER_REPLICATE_BLUR = "BORDER_REPLICATE_BLUR"
62
+ DETECTOR_SIFT = "SIFT"
63
+ DETECTOR_ORB = "ORB"
64
+ DETECTOR_SURF = "SURF"
65
+ DETECTOR_AKAZE = "AKAZE"
66
+ DETECTOR_BRISK = "BRISK"
67
+ DESCRIPTOR_SIFT = "SIFT"
68
+ DESCRIPTOR_ORB = "ORB"
69
+ DESCRIPTOR_AKAZE = "AKAZE"
70
+ DESCRIPTOR_BRISK = "BRISK"
71
+ MATCHING_KNN = "KNN"
72
+ MATCHING_NORM_HAMMING = "NORM_HAMMING"
73
+ ALIGN_RANSAC = "RANSAC"
74
+ ALIGN_LMEDS = "LMEDS"
75
+
76
+ VALID_DETECTORS = [DETECTOR_SIFT, DETECTOR_ORB, DETECTOR_SURF, DETECTOR_AKAZE, DETECTOR_BRISK]
77
+ VALID_DESCRIPTORS = [DESCRIPTOR_SIFT, DESCRIPTOR_ORB, DESCRIPTOR_AKAZE, DESCRIPTOR_BRISK]
78
+ VALID_MATCHING_METHODS = [MATCHING_KNN, MATCHING_NORM_HAMMING]
79
+ VALID_TRANSFORMS = [ALIGN_RIGID, ALIGN_HOMOGRAPHY]
80
+ VALID_BORDER_MODES = [BORDER_CONSTANT, BORDER_REPLICATE, BORDER_REPLICATE_BLUR]
81
+ VALID_ALIGN_METHODS = [ALIGN_RANSAC, ALIGN_LMEDS]
82
+ NOKNN_METHODS = {'detectors': [DETECTOR_ORB, DETECTOR_SURF, DETECTOR_AKAZE, DETECTOR_BRISK],
83
+ 'descriptors': [DESCRIPTOR_ORB, DESCRIPTOR_AKAZE, DESCRIPTOR_BRISK]}
84
+
85
+ DEFAULT_DETECTOR = DETECTOR_SIFT
86
+ DEFAULT_DESCRIPTOR = DESCRIPTOR_SIFT
87
+ DEFAULT_MATCHING_METHOD = MATCHING_KNN
88
+ DEFAULT_FLANN_IDX_KDTREE = 2
89
+ DEFAULT_FLANN_TREES = 5
90
+ DEFAULT_FLANN_CHECKS = 50
91
+ DEFAULT_ALIGN_THRESHOLD = 0.75
92
+ DEFAULT_TRANSFORM = ALIGN_RIGID
93
+ DEFAULT_BORDER_MODE = BORDER_REPLICATE_BLUR
94
+ DEFAULT_ALIGN_METHOD = 'RANSAC'
95
+ DEFAULT_RANS_THRESHOLD = 3.0 # px
96
+ DEFAULT_REFINE_ITERS = 100
97
+ DEFAULT_ALIGN_CONFIDENCE = 99.9
98
+ DEFAULT_ALIGN_MAX_ITERS = 2000
99
+ DEFAULT_BORDER_VALUE = [0] * 4
100
+ DEFAULT_BORDER_BLUR = 50
101
+ DEFAULT_ALIGN_SUBSAMPLE = 1
102
+ DEFAULT_ALIGN_FAST_SUBSAMPLING = False
103
+
104
+ BALANCE_LINEAR = "LINEAR"
105
+ BALANCE_GAMMA = "GAMMA"
106
+ BALANCE_MATCH_HIST = "MATCH_HIST"
107
+ VALID_BALANCE = [BALANCE_LINEAR, BALANCE_GAMMA, BALANCE_MATCH_HIST]
108
+
109
+ BALANCE_LUMI = "LUMI"
110
+ BALANCE_RGB = "RGB"
111
+ BALANCE_HSV = "HSV"
112
+ BALANCE_HLS = "HLS"
113
+ VALID_BALANCE_CHANNELS = [BALANCE_LUMI, BALANCE_RGB, BALANCE_HSV, BALANCE_HLS]
114
+
115
+ DEFAULT_BALANCE_SUBSAMPLE = 8
116
+ DEFAULT_CORR_MAP = BALANCE_LINEAR
117
+ DEFAULT_CHANNEL = BALANCE_LUMI
118
+ DEFAULT_INTENSITY_INTERVAL = {'min': 0, 'max': -1}
119
+
120
+ DEFAULT_R_STEPS = 100
121
+ DEFAULT_BLACK_THRESHOLD = 1
122
+ DEFAULT_MAX_CORRECTION = 1
123
+
124
+ FLOAT_32 = 'float-32'
125
+ FLOAT_64 = 'float-64'
126
+ VALID_FLOATS = [FLOAT_32, FLOAT_64]
127
+
128
+ DEFAULT_FRAMES = 10
129
+ DEFAULT_OVERLAP = 2
130
+ DEFAULT_STACK_PREFIX = "stack_"
131
+ DEFAULT_BUNCH_PREFIX = "bunch_"
132
+
133
+ DEFAULT_DM_FLOAT = FLOAT_32
134
+ DM_ENERGY_LAPLACIAN = "laplacian"
135
+ DM_ENERGY_SOBEL = "sobel"
136
+ DM_MAP_AVERAGE = "average"
137
+ DM_MAP_MAX = "max"
138
+ VALID_DM_MAP = [DM_MAP_AVERAGE, DM_MAP_MAX]
139
+ VALID_DM_ENERGY = [DM_ENERGY_LAPLACIAN, DM_ENERGY_SOBEL]
140
+ DEFAULT_DM_MAP = DM_MAP_AVERAGE
141
+ DEFAULT_DM_ENERGY = DM_ENERGY_LAPLACIAN
142
+ DEFAULT_DM_KERNEL_SIZE = 5
143
+ DEFAULT_DM_BLUR_SIZE = 5
144
+ DEFAULT_DM_SMOOTH_SIZE = 15
145
+ DEFAULT_DM_TEMPERATURE = 0.1
146
+ DEFAULT_DM_LEVELS = 3
147
+
148
+ DEFAULT_PY_FLOAT = FLOAT_32
149
+ DEFAULT_PY_MIN_SIZE = 32
150
+ DEFAULT_PY_KERNEL_SIZE = 5
151
+ DEFAULT_PY_GEN_KERNEL = 0.4
152
+
153
+ DEFAULT_PLOT_STACK_BUNCH = False
154
+ DEFAULT_PLOT_STACK = True
155
+
156
+ STATUS_RUNNING = 1
157
+ STATUS_PAUSED = 2
158
+ STATUS_STOPPED = 3
159
+
160
+ RUN_COMPLETED = 0
161
+ RUN_ONGOING = 1
162
+ RUN_FAILED = 2
163
+ RUN_STOPPED = 3
164
+
165
+ def __setattr__aux(self, name, value):
166
+ raise AttributeError(f"Can't reassign constant '{name}'")
167
+
168
+ def __init__(self):
169
+ self.PYTHON_APP = sys.executable
170
+ self.RETOUCH_APP = "shinestacker-retouch"
171
+ _Constants.__setattr__ = _Constants.__setattr__aux
172
+
173
+
174
+ constants = _Constants()
@@ -0,0 +1,85 @@
1
+ import math
2
+
3
+
4
+ class _GuiConstants:
5
+ GUI_IMG_WIDTH = 250 # px
6
+ DISABLED_TAG = "" # " <disabled>"
7
+
8
+ MIN_ZOOMED_IMG_WIDTH = 400
9
+ MAX_ZOOMED_IMG_PX_SIZE = 50
10
+ MAX_UNDO_SIZE = 65535
11
+
12
+ NEW_PROJECT_NOISE_DETECTION = False
13
+ NEW_PROJECT_VIGNETTING_CORRECTION = False
14
+ NEW_PROJECT_ALIGN_FRAMES = True
15
+ NEW_PROJECT_BALANCE_FRAMES = True
16
+ NEW_PROJECT_BUNCH_STACK = False
17
+ NEW_PROJECT_BUNCH_FRAMES = {'min': 2, 'max': 20}
18
+ NEW_PROJECT_BUNCH_OVERLAP = {'min': 0, 'max': 10}
19
+ NEW_PROJECT_FOCUS_STACK_PYRAMID = True
20
+ NEW_PROJECT_FOCUS_STACK_DEPTH_MAP = False
21
+ NEW_PROJECT_MULTI_LAYER = False
22
+
23
+ BRUSH_COLORS = {
24
+ 'outer': (255, 0, 0, 200),
25
+ 'inner': (255, 0, 0, 150),
26
+ 'gradient_end': (255, 0, 0, 0),
27
+ 'pen': (255, 0, 0, 150),
28
+ 'preview': (255, 180, 180),
29
+ 'cursor_inner': (255, 0, 0, 120),
30
+ 'preview_inner': (255, 255, 255, 150)
31
+ }
32
+
33
+ MIN_MOUSE_STEP_BRUSH_FRACTION = 0.25
34
+ PAINT_REFRESH_TIMER = 50 # milliseconds
35
+
36
+ THUMB_WIDTH = 120 # px
37
+ THUMB_HEIGHT = 80 # px
38
+ IMG_WIDTH = 100 # px
39
+ IMG_HEIGHT = 80 # px
40
+ LABEL_HEIGHT = 20 # px
41
+
42
+ MAX_UNDO_STEPS = 50
43
+
44
+ BRUSH_SIZE_SLIDER_MAX = 1000
45
+
46
+ UI_SIZES = {
47
+ 'brush_preview': (100, 80),
48
+ 'thumbnail': (IMG_WIDTH, IMG_HEIGHT),
49
+ 'master_thumb': (THUMB_WIDTH, THUMB_HEIGHT)
50
+ }
51
+
52
+ DEFAULT_BRUSH_HARDNESS = 50
53
+ DEFAULT_BRUSH_OPACITY = 100
54
+ DEFAULT_BRUSH_FLOW = 100
55
+ BRUSH_SIZES = {
56
+ 'default': 50,
57
+ 'min': 5,
58
+ 'mid': 50,
59
+ 'max': 1000
60
+ }
61
+ DEFAULT_CURSOR_STYLE = 'preview'
62
+ BRUSH_LINE_WIDTH = 2
63
+ BRUSH_PREVIEW_LINE_WIDTH = 1.5
64
+ ZOOM_IN_FACTOR = 1.25
65
+ ZOOM_OUT_FACTOR = 0.80
66
+
67
+ def calculate_gamma(self):
68
+ if self.BRUSH_SIZES['mid'] <= self.BRUSH_SIZES['min'] or self.BRUSH_SIZES['max'] <= 0:
69
+ return 1.0
70
+ ratio = (self.BRUSH_SIZES['mid'] - self.BRUSH_SIZES['min']) / self.BRUSH_SIZES['max']
71
+ half_point = self.BRUSH_SIZE_SLIDER_MAX / 2
72
+ if ratio <= 0:
73
+ return 1.0
74
+ gamma = math.log(ratio) / math.log(half_point / self.BRUSH_SIZE_SLIDER_MAX)
75
+ return gamma
76
+
77
+ def __setattr__aux(self, name, value):
78
+ raise AttributeError(f"Can't reassign constant '{name}'")
79
+
80
+ def __init__(self):
81
+ self.BRUSH_GAMMA = self.calculate_gamma()
82
+ _GuiConstants.__setattr__ = _GuiConstants.__setattr__aux
83
+
84
+
85
+ gui_constants = _GuiConstants()
@@ -0,0 +1,5 @@
1
+ # flake8: noqa F401
2
+ from .logging import setup_logging, console_logging_overwrite, console_logging_newline
3
+ from .exceptions import (FocusStackError, InvalidOptionError, ImageLoadError, AlignmentError,
4
+ BitDepthError, ShapeError)
5
+ from .framework import TqdmCallbacks
@@ -0,0 +1,60 @@
1
+ COLORS = {
2
+ "black": 30,
3
+ "red": 31,
4
+ "green": 32,
5
+ "yellow": 33,
6
+ "blue": 34,
7
+ "magenta": 35,
8
+ "cyan": 36,
9
+ "light_grey": 37,
10
+ "dark_grey": 90,
11
+ "light_red": 91,
12
+ "light_green": 92,
13
+ "light_yellow": 93,
14
+ "light_blue": 94,
15
+ "light_magenta": 95,
16
+ "light_cyan": 96,
17
+ "white": 97,
18
+ }
19
+
20
+ BG_COLORS = {
21
+ "bg_black": 40,
22
+ "bg_red": 41,
23
+ "bg_green": 42,
24
+ "bg_yellow": 43,
25
+ "bg_blue": 44,
26
+ "bg_magenta": 45,
27
+ "bg_cyan": 46,
28
+ "bg_light_grey": 47,
29
+ "bg_dark_grey": 100,
30
+ "bg_light_red": 101,
31
+ "bg_light_green": 102,
32
+ "bg_light_yellow": 103,
33
+ "bg_light_blue": 104,
34
+ "bg_light_magenta": 105,
35
+ "bg_light_cyan": 106,
36
+ "bg_white": 107,
37
+ }
38
+
39
+ EFFECTS = {
40
+ "bold": 1,
41
+ "dark": 2,
42
+ "italic": 3,
43
+ "underline": 4,
44
+ "blink": 5,
45
+ "reverse": 7,
46
+ }
47
+
48
+
49
+ def color_str(text, *args):
50
+ text_colored = text
51
+ for arg in args:
52
+ if arg in COLORS.keys():
53
+ text_colored = f"\033[{COLORS[arg]}m{text_colored}"
54
+ elif arg in BG_COLORS.keys():
55
+ text_colored = f"\033[{BG_COLORS[arg]}m{text_colored}"
56
+ elif arg in EFFECTS.keys():
57
+ text_colored = f"\033[{EFFECTS[arg]}m{text_colored}"
58
+ else:
59
+ raise ValueError(f"Color or effect not supported: {arg}")
60
+ return text_colored + "\033[0m"
@@ -0,0 +1,52 @@
1
+ import os
2
+ import sys
3
+ import platform
4
+ from .. config.config import config
5
+
6
+ if not config.DISABLE_TQDM:
7
+ from tqdm import tqdm
8
+ from tqdm.notebook import tqdm_notebook
9
+
10
+
11
+ def check_path_exists(path):
12
+ if not os.path.exists(path):
13
+ raise Exception('Path does not exist: ' + path)
14
+
15
+
16
+ def make_tqdm_bar(name, size, ncols=80):
17
+ if not config.DISABLE_TQDM:
18
+ if config.JUPYTER_NOTEBOOK:
19
+ bar = tqdm_notebook(desc=name, total=size)
20
+ else:
21
+ bar = tqdm(desc=name, total=size, ncols=ncols)
22
+ return bar
23
+ else:
24
+ return None
25
+
26
+
27
+ def get_app_base_path():
28
+ sep = '\\' if (platform.system() == 'Windows') else '/'
29
+ if getattr(sys, 'frozen', False):
30
+ path = os.path.dirname(os.path.realpath(sys.executable))
31
+ dirs = path.split(sep)
32
+ last = -1
33
+ for i in range(len(dirs) - 1, -1, -1):
34
+ if dirs[i] == 'shinestacker':
35
+ last = i
36
+ break
37
+ path = sep.join(dirs if last == 1 else dirs[:last + 1])
38
+ elif __file__:
39
+ path = sep.join(os.path.dirname(os.path.abspath(__file__)).split(sep)[:-3])
40
+ return path
41
+
42
+
43
+ def running_under_windows() -> bool:
44
+ return platform.system().lower() == 'windows'
45
+
46
+
47
+ def running_under_macos() -> bool:
48
+ return platform.system().lower() == "darwin"
49
+
50
+
51
+ def running_under_linux() -> bool:
52
+ return platform.system().lower() == 'linux'
@@ -0,0 +1,50 @@
1
+ class FocusStackError(Exception):
2
+ pass
3
+
4
+
5
+ class InvalidOptionError(FocusStackError):
6
+ def __init__(self, option, value, details=""):
7
+ self.option = option
8
+ self.value = value
9
+ self.details = details
10
+ super().__init__(f"Invalid option {option} = {value}" + ("" if details == "" else f": {details}"))
11
+
12
+
13
+ class ImageLoadError(FocusStackError):
14
+ def __init__(self, path, details=""):
15
+ self.path = path
16
+ self.details = details
17
+ super().__init__(f"Failed to load {path}" + ("" if details == "" else f": {details}"))
18
+
19
+
20
+ class ImageSaveError(FocusStackError):
21
+ def __init__(self, path, details=""):
22
+ self.path = path
23
+ self.details = details
24
+ super().__init__(f"Failed to save {path}" + ("" if details == "" else f": {details}"))
25
+
26
+
27
+ class AlignmentError(FocusStackError):
28
+ def __init__(self, index, details):
29
+ self.index = index
30
+ self.details = details
31
+ super().__init__(f"Alignment failed for image {index}: {details}")
32
+
33
+
34
+ class BitDepthError(FocusStackError):
35
+ def __init__(self, dtype_ref, dtype):
36
+ super().__init__(f"Image has type {dtype}, expected {dtype_ref}.")
37
+
38
+
39
+ class ShapeError(FocusStackError):
40
+ def __init__(self, shape_ref, shape):
41
+ super().__init__(f'''
42
+ Image has shape ({shape[1]}x{shape[0]}), while it was expected ({shape_ref[1]}x{shape_ref[0]}).
43
+ ''')
44
+
45
+
46
+ class RunStopException(FocusStackError):
47
+ def __init__(self, name):
48
+ if name != "":
49
+ name = f"{name} "
50
+ super().__init__(f"Job {name}stopped")
@@ -0,0 +1,210 @@
1
+ import time
2
+ import logging
3
+ from .. config.config import config
4
+ from .colors import color_str
5
+ from .logging import setup_logging
6
+ from .core_utils import make_tqdm_bar
7
+ from .exceptions import RunStopException
8
+
9
+ LINE_UP = "\r\033[A"
10
+ trailing_spaces = " " * 30
11
+
12
+
13
+ class TqdmCallbacks:
14
+ _instance = None
15
+
16
+ callbacks = {
17
+ 'step_counts': lambda id, name, counts: TqdmCallbacks.instance().step_counts(name, counts),
18
+ 'begin_steps': lambda id, name: TqdmCallbacks.instance().begin_steps(name),
19
+ 'end_steps': lambda id, name: TqdmCallbacks.instance().end_steps(name),
20
+ 'after_step': lambda id, name, steps: TqdmCallbacks.instance().after_step(name)
21
+ }
22
+
23
+ def __init__(self):
24
+ self.bar = None
25
+ self.counts = -1
26
+
27
+ @classmethod
28
+ def instance(cls):
29
+ if cls._instance is None:
30
+ cls._instance = TqdmCallbacks()
31
+ return cls._instance
32
+
33
+ def step_counts(self, name, counts):
34
+ self.counts = counts
35
+ self.bar = make_tqdm_bar(name, self.counts)
36
+
37
+ def begin_steps(self, name):
38
+ pass
39
+
40
+ def end_steps(self, name):
41
+ if self.bar is None:
42
+ raise RuntimeError("tqdm bar not initialized")
43
+ self.bar.close()
44
+ self.bar = None
45
+
46
+ def after_step(self, name):
47
+ self.bar.write("")
48
+ self.bar.update(1)
49
+
50
+
51
+ tqdm_callbacks = TqdmCallbacks()
52
+
53
+
54
+ def elapsed_time_str(start):
55
+ dt = time.time() - start
56
+ mm = int(dt // 60)
57
+ ss = dt - mm * 60
58
+ hh = mm // 60
59
+ mm -= hh * 60
60
+ return ("{:02d}:{:02d}:{:05.2f}s".format(hh, mm, ss))
61
+
62
+
63
+ class JobBase:
64
+ def __init__(self, name, enabled=True):
65
+ self.id = -1
66
+ self.name = name
67
+ self.enabled = enabled
68
+ self.base_message = ''
69
+ if config.JUPYTER_NOTEBOOK:
70
+ self.begin_r, self.end_r = "", "\r",
71
+ else:
72
+ self.begin_r, self.end_r = LINE_UP, None
73
+
74
+ def callback(self, key, *args):
75
+ has_callbacks = hasattr(self, 'callbacks')
76
+ if has_callbacks and self.callbacks is not None:
77
+ callback = self.callbacks.get(key, None)
78
+ if callback:
79
+ return callback(*args)
80
+ return None
81
+
82
+ def run(self):
83
+ self.__t0 = time.time()
84
+ if not self.enabled:
85
+ self.get_logger().warning(color_str(self.name + ": entire job disabled", 'red'))
86
+ self.callback('before_action', self.id, self.name)
87
+ self.run_core()
88
+ self.callback('after_action', self.id, self.name)
89
+ self.get_logger().info(
90
+ color_str(self.name + ": ",
91
+ "green", "bold") + color_str("elapsed "
92
+ "time: {}".format(elapsed_time_str(self.__t0)),
93
+ "green") + trailing_spaces)
94
+ self.get_logger().info(
95
+ color_str(self.name + ": ",
96
+ "green", "bold") + color_str("completed", "green") + trailing_spaces)
97
+
98
+ def get_logger(self, tqdm=False):
99
+ if config.DISABLE_TQDM:
100
+ tqdm = False
101
+ if self.logger is None:
102
+ return logging.getLogger("tqdm" if tqdm else __name__)
103
+ else:
104
+ return self.logger
105
+
106
+ def set_terminator(self, tqdm=False, end='\n'):
107
+ if config.DISABLE_TQDM:
108
+ tqdm = False
109
+ if end is not None:
110
+ logging.getLogger("tqdm" if tqdm else None).handlers[0].terminator = end
111
+
112
+ def print_message(self, msg='', level=logging.INFO, end=None, begin='', tqdm=False):
113
+ if config.DISABLE_TQDM:
114
+ tqdm = False
115
+ self.base_message = color_str(self.name, "blue", "bold")
116
+ if msg != '':
117
+ self.base_message += (': ' + msg)
118
+ self.set_terminator(tqdm, end)
119
+ self.get_logger(tqdm).log(level, begin + color_str(self.base_message, 'blue', 'bold') + trailing_spaces)
120
+ self.set_terminator(tqdm)
121
+
122
+ def sub_message(self, msg, level=logging.INFO, end=None, begin='', tqdm=False):
123
+ if config.DISABLE_TQDM:
124
+ tqdm = False
125
+ self.set_terminator(tqdm, end)
126
+ self.get_logger(tqdm).log(level, begin + self.base_message + msg + trailing_spaces)
127
+ self.set_terminator(tqdm)
128
+
129
+ def print_message_r(self, msg='', level=logging.INFO):
130
+ self.print_message(msg, level, self.end_r, self.begin_r, False)
131
+
132
+ def sub_message_r(self, msg='', level=logging.INFO):
133
+ self.sub_message(msg, level, self.end_r, self.begin_r, False)
134
+
135
+
136
+ class Job(JobBase):
137
+ def __init__(self, name, logger_name=None, log_file='', callbacks=None, **kwargs):
138
+ JobBase.__init__(self, name, **kwargs)
139
+ self.action_counter = 0
140
+ self.__actions = []
141
+ if logger_name is None:
142
+ setup_logging(log_file=log_file)
143
+ self.logger = None if logger_name is None else logging.getLogger(logger_name)
144
+ self.callbacks = TqdmCallbacks.callbacks if callbacks == 'tqdm' else callbacks
145
+
146
+ def time(self):
147
+ return time.time() - self.__t0
148
+
149
+ def init(self, a):
150
+ pass
151
+
152
+ def add_action(self, a: JobBase):
153
+ a.id = self.action_counter
154
+ self.action_counter += 1
155
+ a.logger = self.logger
156
+ a.callbacks = self.callbacks
157
+ self.init(a)
158
+ self.__actions.append(a)
159
+
160
+ def run_core(self):
161
+ for a in self.__actions:
162
+ if not (a.enabled and self.enabled):
163
+ z = []
164
+ if not a.enabled:
165
+ z.append("action")
166
+ if not self.enabled:
167
+ z.append("job")
168
+ msg = " and ".join(z)
169
+ self.get_logger().warning(color_str(a.name + f": {msg} disabled", 'red'))
170
+ else:
171
+ if self.callback('check_running', self.id, self.name) is False:
172
+ raise RunStopException(self.name)
173
+ a.run()
174
+
175
+
176
+ class ActionList(JobBase):
177
+ def __init__(self, name, enabled=True, **kwargs):
178
+ JobBase.__init__(self, name, enabled, **kwargs)
179
+
180
+ def set_counts(self, counts):
181
+ self.counts = counts
182
+ self.callback('step_counts', self.id, self.name, self.counts)
183
+
184
+ def begin(self):
185
+ self.callback('begin_steps', self.id, self.name)
186
+
187
+ def end(self):
188
+ self.callback('end_steps', self.id, self.name)
189
+
190
+ def __iter__(self):
191
+ self.count = 1
192
+ return self
193
+
194
+ def __next__(self):
195
+ if self.count <= self.counts:
196
+ self.run_step()
197
+ x = self.count
198
+ self.count += 1
199
+ return x
200
+ else:
201
+ raise StopIteration
202
+
203
+ def run_core(self):
204
+ self.print_message('begin run', end='\n')
205
+ self.begin()
206
+ for x in iter(self):
207
+ self.callback('after_step', self.id, self.name, self.count)
208
+ if self.callback('check_running', self.id, self.name) is False:
209
+ raise RunStopException(self.name)
210
+ self.end()