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.
- shinestacker/__init__.py +3 -0
- shinestacker/_version.py +1 -0
- shinestacker/algorithms/__init__.py +14 -0
- shinestacker/algorithms/align.py +307 -0
- shinestacker/algorithms/balance.py +367 -0
- shinestacker/algorithms/core_utils.py +22 -0
- shinestacker/algorithms/depth_map.py +164 -0
- shinestacker/algorithms/exif.py +238 -0
- shinestacker/algorithms/multilayer.py +187 -0
- shinestacker/algorithms/noise_detection.py +182 -0
- shinestacker/algorithms/pyramid.py +176 -0
- shinestacker/algorithms/stack.py +112 -0
- shinestacker/algorithms/stack_framework.py +248 -0
- shinestacker/algorithms/utils.py +71 -0
- shinestacker/algorithms/vignetting.py +137 -0
- shinestacker/app/__init__.py +0 -0
- shinestacker/app/about_dialog.py +24 -0
- shinestacker/app/app_config.py +39 -0
- shinestacker/app/gui_utils.py +35 -0
- shinestacker/app/help_menu.py +16 -0
- shinestacker/app/main.py +176 -0
- shinestacker/app/open_frames.py +39 -0
- shinestacker/app/project.py +91 -0
- shinestacker/app/retouch.py +82 -0
- shinestacker/config/__init__.py +4 -0
- shinestacker/config/config.py +53 -0
- shinestacker/config/constants.py +174 -0
- shinestacker/config/gui_constants.py +85 -0
- shinestacker/core/__init__.py +5 -0
- shinestacker/core/colors.py +60 -0
- shinestacker/core/core_utils.py +52 -0
- shinestacker/core/exceptions.py +50 -0
- shinestacker/core/framework.py +210 -0
- shinestacker/core/logging.py +89 -0
- shinestacker/gui/__init__.py +0 -0
- shinestacker/gui/action_config.py +879 -0
- shinestacker/gui/actions_window.py +283 -0
- shinestacker/gui/colors.py +57 -0
- shinestacker/gui/gui_images.py +152 -0
- shinestacker/gui/gui_logging.py +213 -0
- shinestacker/gui/gui_run.py +393 -0
- shinestacker/gui/img/close-round-line-icon.png +0 -0
- shinestacker/gui/img/forward-button-icon.png +0 -0
- shinestacker/gui/img/play-button-round-icon.png +0 -0
- shinestacker/gui/img/plus-round-line-icon.png +0 -0
- shinestacker/gui/main_window.py +599 -0
- shinestacker/gui/new_project.py +170 -0
- shinestacker/gui/project_converter.py +148 -0
- shinestacker/gui/project_editor.py +539 -0
- shinestacker/gui/project_model.py +138 -0
- shinestacker/retouch/__init__.py +0 -0
- shinestacker/retouch/brush.py +9 -0
- shinestacker/retouch/brush_controller.py +57 -0
- shinestacker/retouch/brush_preview.py +126 -0
- shinestacker/retouch/exif_data.py +65 -0
- shinestacker/retouch/file_loader.py +104 -0
- shinestacker/retouch/image_editor.py +651 -0
- shinestacker/retouch/image_editor_ui.py +380 -0
- shinestacker/retouch/image_viewer.py +356 -0
- shinestacker/retouch/shortcuts_help.py +98 -0
- shinestacker/retouch/undo_manager.py +38 -0
- shinestacker-0.2.0.post1.dev1.dist-info/METADATA +55 -0
- shinestacker-0.2.0.post1.dev1.dist-info/RECORD +67 -0
- shinestacker-0.2.0.post1.dev1.dist-info/WHEEL +5 -0
- shinestacker-0.2.0.post1.dev1.dist-info/entry_points.txt +4 -0
- shinestacker-0.2.0.post1.dev1.dist-info/licenses/LICENSE +1 -0
- 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()
|