openfocus 1.28__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.
- constants.py +43 -0
- core/__init__.py +30 -0
- core/app.py +162 -0
- core/cli.py +306 -0
- core/image_loader.py +500 -0
- core/models/__init__.py +18 -0
- core/models/stackmffv4_network.py +630 -0
- core/multi_focus_fusion.py +955 -0
- core/registration.py +1152 -0
- core/workers.py +894 -0
- fusion_methods/__init__.py +14 -0
- fusion_methods/dct.py +220 -0
- fusion_methods/dtcwt.py +211 -0
- fusion_methods/gff.py +264 -0
- fusion_methods/gfg_fgf.py +362 -0
- fusion_methods/stackmffv4.py +288 -0
- openfocus-1.28.dist-info/METADATA +210 -0
- openfocus-1.28.dist-info/RECORD +35 -0
- openfocus-1.28.dist-info/WHEEL +5 -0
- openfocus-1.28.dist-info/entry_points.txt +2 -0
- openfocus-1.28.dist-info/licenses/LICENSE +21 -0
- openfocus-1.28.dist-info/top_level.txt +5 -0
- utils/__init__.py +81 -0
- utils/align_cache.py +102 -0
- utils/image_utils.py +176 -0
- utils/logging_setup.py +63 -0
- utils/platform_utils.py +108 -0
- utils/project_file.py +220 -0
- utils/self_update.py +290 -0
- utils/settings_store.py +246 -0
- utils/ui_utils.py +151 -0
- utils/updater.py +177 -0
- utils/validators.py +224 -0
- weights/__init__.py +1 -0
- weights/stackmffv4.pth +0 -0
core/image_loader.py
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
"""
|
|
2
|
+
图像栈加载器模块
|
|
3
|
+
负责从文件夹中加载图像栈并生成缩略图
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import cv2
|
|
8
|
+
import numpy as np
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import List, Tuple, Optional, Dict, Any
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from PIL import Image as PILImage, ImageOps
|
|
14
|
+
PIL_AVAILABLE = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
PIL_AVAILABLE = False
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from pillow_heif import register_heif_opener
|
|
20
|
+
register_heif_opener()
|
|
21
|
+
HEIF_AVAILABLE = True
|
|
22
|
+
except ImportError:
|
|
23
|
+
HEIF_AVAILABLE = False
|
|
24
|
+
|
|
25
|
+
from utils.image_utils import to_display_uint8
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _read_exif_bytes(path: str) -> bytes:
|
|
29
|
+
"""Raw EXIF block of an image (b"") when absent or unreadable."""
|
|
30
|
+
if not PIL_AVAILABLE:
|
|
31
|
+
return b""
|
|
32
|
+
try:
|
|
33
|
+
ext = os.path.splitext(path)[1].lower()
|
|
34
|
+
if ext not in ('.jpg', '.jpeg', '.tif', '.tiff', '.png'):
|
|
35
|
+
return b""
|
|
36
|
+
with PILImage.open(path) as pil_img:
|
|
37
|
+
return pil_img.info.get('exif', b"") or b""
|
|
38
|
+
except Exception:
|
|
39
|
+
return b""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# EXIF Orientation tag values that need a pixel transform (1 = already OK).
|
|
43
|
+
_EXIF_TRANSFORMS = {
|
|
44
|
+
2: lambda a: a[:, ::-1], # mirror horizontal
|
|
45
|
+
3: lambda a: np.rot90(a, 2), # rotate 180
|
|
46
|
+
4: lambda a: a[::-1, :], # mirror vertical
|
|
47
|
+
5: lambda a: np.rot90(a[:, ::-1], 1), # transpose
|
|
48
|
+
6: lambda a: np.rot90(a, -1), # rotate 90 CW
|
|
49
|
+
7: lambda a: np.rot90(a[::-1, :], 1), # anti-transpose
|
|
50
|
+
8: lambda a: np.rot90(a, 1), # rotate 90 CCW
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def apply_exif_orientation(path: str, img: np.ndarray) -> np.ndarray:
|
|
55
|
+
"""Rotate/flip a decoded image according to its EXIF Orientation tag.
|
|
56
|
+
|
|
57
|
+
Cameras store sensor-native orientation and record how to display it in
|
|
58
|
+
EXIF; OpenCV ignores that tag, so portrait shots load sideways without
|
|
59
|
+
this correction. Returns the image unchanged when Pillow is missing or
|
|
60
|
+
the tag is absent/neutral.
|
|
61
|
+
"""
|
|
62
|
+
if not PIL_AVAILABLE:
|
|
63
|
+
return img
|
|
64
|
+
try:
|
|
65
|
+
ext = os.path.splitext(path)[1].lower()
|
|
66
|
+
if ext not in ('.jpg', '.jpeg', '.tif', '.tiff'):
|
|
67
|
+
return img
|
|
68
|
+
with PILImage.open(path) as pil_img:
|
|
69
|
+
orientation = pil_img.getexif().get(274, 1)
|
|
70
|
+
transform = _EXIF_TRANSFORMS.get(int(orientation))
|
|
71
|
+
if transform is not None:
|
|
72
|
+
return np.ascontiguousarray(transform(img))
|
|
73
|
+
except Exception as e:
|
|
74
|
+
print(f"EXIF orientation check failed for {path}: {e}")
|
|
75
|
+
return img
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _read_via_pil(path: str) -> Optional[np.ndarray]:
|
|
79
|
+
"""Decode a file OpenCV cannot (HEIC/HEIF) through Pillow.
|
|
80
|
+
|
|
81
|
+
Returns a 3-channel BGR array like cv2.imdecode(IMREAD_COLOR), or None when
|
|
82
|
+
Pillow is missing or the file is unreadable, so callers count it as a
|
|
83
|
+
failed file instead of raising.
|
|
84
|
+
"""
|
|
85
|
+
if not PIL_AVAILABLE:
|
|
86
|
+
return None
|
|
87
|
+
try:
|
|
88
|
+
with PILImage.open(path) as pil_img:
|
|
89
|
+
if pil_img.mode in ('I', 'I;16', 'I;16B'):
|
|
90
|
+
arr = np.array(pil_img).astype(np.uint16)
|
|
91
|
+
else:
|
|
92
|
+
arr = np.array(pil_img.convert('RGB'))
|
|
93
|
+
if arr.ndim == 2:
|
|
94
|
+
return cv2.cvtColor(np.ascontiguousarray(arr), cv2.COLOR_GRAY2BGR)
|
|
95
|
+
if arr.shape[2] == 4:
|
|
96
|
+
return cv2.cvtColor(arr, cv2.COLOR_RGBA2BGR)
|
|
97
|
+
return cv2.cvtColor(arr, cv2.COLOR_RGB2BGR)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
print(f"Unsupported image for PIL fallback {path}: {e}")
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ImageStackLoader:
|
|
104
|
+
"""图像栈加载器"""
|
|
105
|
+
|
|
106
|
+
SUPPORTED_FORMATS = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif'}
|
|
107
|
+
if HEIF_AVAILABLE:
|
|
108
|
+
SUPPORTED_FORMATS |= {'.heic', '.heif'}
|
|
109
|
+
SUPPORTED_VIDEO_FORMATS = {'.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm'}
|
|
110
|
+
|
|
111
|
+
def __init__(self):
|
|
112
|
+
self.image_paths = []
|
|
113
|
+
self.images = []
|
|
114
|
+
self.thumbnail_size = (600, 400)
|
|
115
|
+
|
|
116
|
+
def load_from_folder(self, folder_path: str, scale_factor: float = 1.0) -> Tuple[bool, str, List[np.ndarray], List[str]]:
|
|
117
|
+
if not os.path.isdir(folder_path):
|
|
118
|
+
return False, "Selected path is not a valid directory", [], []
|
|
119
|
+
|
|
120
|
+
image_files = []
|
|
121
|
+
for filename in os.listdir(folder_path):
|
|
122
|
+
ext = os.path.splitext(filename)[1].lower()
|
|
123
|
+
if ext in self.SUPPORTED_FORMATS:
|
|
124
|
+
full_path = os.path.join(folder_path, filename)
|
|
125
|
+
image_files.append((filename, full_path))
|
|
126
|
+
|
|
127
|
+
if not image_files:
|
|
128
|
+
return False, "No supported image files found in the folder", [], []
|
|
129
|
+
|
|
130
|
+
image_files.sort(key=lambda x: x[0])
|
|
131
|
+
|
|
132
|
+
loaded_images = []
|
|
133
|
+
filenames = []
|
|
134
|
+
failed_count = 0
|
|
135
|
+
self.source_exif = b""
|
|
136
|
+
|
|
137
|
+
for filename, full_path in image_files:
|
|
138
|
+
try:
|
|
139
|
+
img = cv2.imdecode(np.fromfile(full_path, dtype=np.uint8),
|
|
140
|
+
cv2.IMREAD_ANYDEPTH | cv2.IMREAD_COLOR)
|
|
141
|
+
if img is None:
|
|
142
|
+
# OpenCV ships no HEIC/HEIF decoder: fall back to Pillow
|
|
143
|
+
img = _read_via_pil(full_path)
|
|
144
|
+
if img is not None:
|
|
145
|
+
if not self.source_exif:
|
|
146
|
+
self.source_exif = _read_exif_bytes(full_path)
|
|
147
|
+
img = apply_exif_orientation(full_path, img)
|
|
148
|
+
if scale_factor != 1.0 and 0 < scale_factor < 1.0:
|
|
149
|
+
width = int(img.shape[1] * scale_factor)
|
|
150
|
+
height = int(img.shape[0] * scale_factor)
|
|
151
|
+
img = cv2.resize(img, (width, height), interpolation=cv2.INTER_AREA)
|
|
152
|
+
loaded_images.append(img)
|
|
153
|
+
filenames.append(filename)
|
|
154
|
+
else:
|
|
155
|
+
failed_count += 1
|
|
156
|
+
except Exception as e:
|
|
157
|
+
failed_count += 1
|
|
158
|
+
print(f"Failed to load image {filename}: {e}")
|
|
159
|
+
|
|
160
|
+
if not loaded_images:
|
|
161
|
+
return False, "Could not load any image files", [], []
|
|
162
|
+
|
|
163
|
+
self.images = loaded_images
|
|
164
|
+
self.image_paths = [f[1] for f in image_files[:len(loaded_images)]]
|
|
165
|
+
|
|
166
|
+
message = f"Loaded {len(loaded_images)} image(s)"
|
|
167
|
+
if failed_count > 0:
|
|
168
|
+
message += f" (failed: {failed_count})"
|
|
169
|
+
|
|
170
|
+
return True, message, loaded_images, filenames
|
|
171
|
+
|
|
172
|
+
def load_from_video(self, video_path: str, scale_factor: float = 1.0) -> Tuple[bool, str, List[np.ndarray], List[str]]:
|
|
173
|
+
if not os.path.isfile(video_path):
|
|
174
|
+
return False, "Video file does not exist", [], []
|
|
175
|
+
|
|
176
|
+
ext = os.path.splitext(video_path)[1].lower()
|
|
177
|
+
if ext not in self.SUPPORTED_VIDEO_FORMATS:
|
|
178
|
+
return False, f"Unsupported video format: {ext}", [], []
|
|
179
|
+
|
|
180
|
+
cap = cv2.VideoCapture(video_path)
|
|
181
|
+
if not cap.isOpened():
|
|
182
|
+
return False, "Failed to open video file", [], []
|
|
183
|
+
|
|
184
|
+
loaded_images = []
|
|
185
|
+
filenames = []
|
|
186
|
+
frame_index = 0
|
|
187
|
+
video_name = os.path.splitext(os.path.basename(video_path))[0]
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
191
|
+
|
|
192
|
+
while True:
|
|
193
|
+
ret, frame = cap.read()
|
|
194
|
+
if not ret:
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
if scale_factor != 1.0 and 0 < scale_factor < 1.0:
|
|
198
|
+
width = int(frame.shape[1] * scale_factor)
|
|
199
|
+
height = int(frame.shape[0] * scale_factor)
|
|
200
|
+
frame = cv2.resize(frame, (width, height), interpolation=cv2.INTER_AREA)
|
|
201
|
+
|
|
202
|
+
loaded_images.append(frame)
|
|
203
|
+
filenames.append(f"{video_name}_frame_{frame_index:04d}.png")
|
|
204
|
+
frame_index += 1
|
|
205
|
+
finally:
|
|
206
|
+
cap.release()
|
|
207
|
+
|
|
208
|
+
if not loaded_images:
|
|
209
|
+
return False, "No frames could be extracted from the video", [], []
|
|
210
|
+
|
|
211
|
+
self.images = loaded_images
|
|
212
|
+
self.image_paths = [video_path] * len(loaded_images)
|
|
213
|
+
|
|
214
|
+
message = f"Loaded {len(loaded_images)} frame(s) from video"
|
|
215
|
+
if total_frames > 0 and len(loaded_images) < total_frames:
|
|
216
|
+
message += f" (expected: {total_frames})"
|
|
217
|
+
|
|
218
|
+
return True, message, loaded_images, filenames
|
|
219
|
+
|
|
220
|
+
def is_video_file(self, filepath: str) -> bool:
|
|
221
|
+
ext = os.path.splitext(filepath)[1].lower()
|
|
222
|
+
return ext in self.SUPPORTED_VIDEO_FORMATS
|
|
223
|
+
|
|
224
|
+
def load_from_filepaths(self, filepaths: list[str], scale_factor: float = 1.0) -> Tuple[bool, str, List[np.ndarray], List[str]]:
|
|
225
|
+
if not filepaths:
|
|
226
|
+
return False, "No file paths provided", [], []
|
|
227
|
+
|
|
228
|
+
loaded_images = []
|
|
229
|
+
filenames = []
|
|
230
|
+
failed_count = 0
|
|
231
|
+
|
|
232
|
+
for full_path in filepaths:
|
|
233
|
+
try:
|
|
234
|
+
if not os.path.exists(full_path):
|
|
235
|
+
failed_count += 1
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
img = cv2.imdecode(np.fromfile(full_path, dtype=np.uint8),
|
|
239
|
+
cv2.IMREAD_ANYDEPTH | cv2.IMREAD_COLOR)
|
|
240
|
+
if img is None:
|
|
241
|
+
# OpenCV ships no HEIC/HEIF decoder: fall back to Pillow
|
|
242
|
+
img = _read_via_pil(full_path)
|
|
243
|
+
if img is not None:
|
|
244
|
+
img = apply_exif_orientation(full_path, img)
|
|
245
|
+
if img is None:
|
|
246
|
+
failed_count += 1
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
if scale_factor != 1.0 and 0 < scale_factor < 1.0:
|
|
250
|
+
width = int(img.shape[1] * scale_factor)
|
|
251
|
+
height = int(img.shape[0] * scale_factor)
|
|
252
|
+
img = cv2.resize(img, (width, height), interpolation=cv2.INTER_AREA)
|
|
253
|
+
|
|
254
|
+
loaded_images.append(img)
|
|
255
|
+
filenames.append(os.path.basename(full_path))
|
|
256
|
+
except Exception as e:
|
|
257
|
+
failed_count += 1
|
|
258
|
+
print(f"Failed to load image {full_path}: {e}")
|
|
259
|
+
|
|
260
|
+
if not loaded_images:
|
|
261
|
+
return False, "Could not load any image files", [], []
|
|
262
|
+
|
|
263
|
+
self.images = loaded_images
|
|
264
|
+
self.image_paths = list(filepaths[:len(loaded_images)])
|
|
265
|
+
|
|
266
|
+
message = f"Loaded {len(loaded_images)} image(s)"
|
|
267
|
+
if failed_count > 0:
|
|
268
|
+
message += f" (failed: {failed_count})"
|
|
269
|
+
|
|
270
|
+
return True, message, loaded_images, filenames
|
|
271
|
+
|
|
272
|
+
def create_pixmaps(self, images: List[np.ndarray], max_size: Tuple[int, int] = (800, 600)) -> list:
|
|
273
|
+
pixmaps = []
|
|
274
|
+
for img in images:
|
|
275
|
+
pixmap = self._cv_to_pixmap(img, max_size)
|
|
276
|
+
pixmaps.append(pixmap)
|
|
277
|
+
return pixmaps
|
|
278
|
+
|
|
279
|
+
def create_thumbnails(self, images: List[np.ndarray], thumb_size: int = 40) -> list:
|
|
280
|
+
thumbnails = []
|
|
281
|
+
for img in images:
|
|
282
|
+
h, w = img.shape[:2]
|
|
283
|
+
if h <= 0 or w <= 0:
|
|
284
|
+
raise ValueError(f"Invalid image dimensions: {w}x{h}")
|
|
285
|
+
scale = thumb_size / max(h, w)
|
|
286
|
+
new_w = int(w * scale)
|
|
287
|
+
new_h = int(h * scale)
|
|
288
|
+
|
|
289
|
+
resized = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_AREA)
|
|
290
|
+
pixmap = self._cv_to_pixmap(to_display_uint8(resized), (thumb_size, thumb_size))
|
|
291
|
+
thumbnails.append(pixmap)
|
|
292
|
+
return thumbnails
|
|
293
|
+
|
|
294
|
+
def _cv_to_pixmap(self, cv_img: np.ndarray, max_size: Optional[Tuple[int, int]] = None):
|
|
295
|
+
# Imported lazily: the headless PyPI package uses this module for
|
|
296
|
+
# decoding without PyQt6 installed.
|
|
297
|
+
from PyQt6.QtGui import QPixmap, QImage
|
|
298
|
+
rgb_img = cv2.cvtColor(to_display_uint8(cv_img), cv2.COLOR_BGR2RGB)
|
|
299
|
+
|
|
300
|
+
if max_size is not None:
|
|
301
|
+
h, w = rgb_img.shape[:2]
|
|
302
|
+
max_w, max_h = max_size
|
|
303
|
+
|
|
304
|
+
scale = min(max_w / w, max_h / h)
|
|
305
|
+
if scale < 1.0:
|
|
306
|
+
new_w = int(w * scale)
|
|
307
|
+
new_h = int(h * scale)
|
|
308
|
+
rgb_img = cv2.resize(rgb_img, (new_w, new_h), interpolation=cv2.INTER_AREA)
|
|
309
|
+
|
|
310
|
+
h, w, ch = rgb_img.shape
|
|
311
|
+
bytes_per_line = ch * w
|
|
312
|
+
|
|
313
|
+
q_img = QImage(rgb_img.data, w, h, bytes_per_line, QImage.Format.Format_RGB888)
|
|
314
|
+
|
|
315
|
+
return QPixmap.fromImage(q_img)
|
|
316
|
+
|
|
317
|
+
def get_image_info(self, index: int) -> dict:
|
|
318
|
+
if not self.images or index < 0 or index >= len(self.images):
|
|
319
|
+
return {}
|
|
320
|
+
|
|
321
|
+
img = self.images[index]
|
|
322
|
+
path = self.image_paths[index] if index < len(self.image_paths) else ""
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
'index': index,
|
|
326
|
+
'path': path,
|
|
327
|
+
'filename': os.path.basename(path) if path else "",
|
|
328
|
+
'shape': img.shape,
|
|
329
|
+
'size': f"{img.shape[1]}x{img.shape[0]}",
|
|
330
|
+
'channels': img.shape[2] if len(img.shape) > 2 else 1,
|
|
331
|
+
'dtype': img.dtype
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
def get_image_timestamp(self, filepath: str) -> Optional[float]:
|
|
335
|
+
if not PIL_AVAILABLE:
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
with PILImage.open(filepath) as img:
|
|
340
|
+
exif_data = img._getexif()
|
|
341
|
+
if exif_data is None:
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
date_time_original = None
|
|
345
|
+
for tag_id, value in exif_data.items():
|
|
346
|
+
tag_name = PILImage.ExifTags.TAGS.get(tag_id, str(tag_id))
|
|
347
|
+
if tag_name == 'DateTimeOriginal':
|
|
348
|
+
date_time_original = value
|
|
349
|
+
break
|
|
350
|
+
|
|
351
|
+
if date_time_original:
|
|
352
|
+
dt = datetime.strptime(date_time_original, '%Y:%m:%d %H:%M:%S')
|
|
353
|
+
return dt.timestamp()
|
|
354
|
+
except Exception as e:
|
|
355
|
+
pass
|
|
356
|
+
|
|
357
|
+
return None
|
|
358
|
+
|
|
359
|
+
def load_images_with_timestamps(
|
|
360
|
+
self,
|
|
361
|
+
folder_path: str,
|
|
362
|
+
sort_by: str = 'timestamp'
|
|
363
|
+
) -> Tuple[bool, str, List[Tuple[str, np.ndarray, Optional[float]]], List[str]]:
|
|
364
|
+
if not os.path.isdir(folder_path):
|
|
365
|
+
return False, "Selected path is not a valid directory", [], []
|
|
366
|
+
|
|
367
|
+
image_files = []
|
|
368
|
+
for filename in os.listdir(folder_path):
|
|
369
|
+
ext = os.path.splitext(filename)[1].lower()
|
|
370
|
+
if ext in self.SUPPORTED_FORMATS:
|
|
371
|
+
full_path = os.path.join(folder_path, filename)
|
|
372
|
+
image_files.append((filename, full_path))
|
|
373
|
+
|
|
374
|
+
if not image_files:
|
|
375
|
+
return False, "No supported image files found in the folder", [], []
|
|
376
|
+
|
|
377
|
+
loaded_data = []
|
|
378
|
+
failed_count = 0
|
|
379
|
+
|
|
380
|
+
for filename, full_path in image_files:
|
|
381
|
+
try:
|
|
382
|
+
img = cv2.imdecode(np.fromfile(full_path, dtype=np.uint8),
|
|
383
|
+
cv2.IMREAD_ANYDEPTH | cv2.IMREAD_COLOR)
|
|
384
|
+
if img is None:
|
|
385
|
+
# OpenCV ships no HEIC/HEIF decoder: fall back to Pillow
|
|
386
|
+
img = _read_via_pil(full_path)
|
|
387
|
+
if img is not None:
|
|
388
|
+
img = apply_exif_orientation(full_path, img)
|
|
389
|
+
timestamp = self.get_image_timestamp(full_path)
|
|
390
|
+
loaded_data.append((filename, full_path, img, timestamp))
|
|
391
|
+
else:
|
|
392
|
+
failed_count += 1
|
|
393
|
+
except Exception as e:
|
|
394
|
+
failed_count += 1
|
|
395
|
+
print(f"Failed to load image {filename}: {e}")
|
|
396
|
+
|
|
397
|
+
if not loaded_data:
|
|
398
|
+
return False, "Could not load any image files", [], []
|
|
399
|
+
|
|
400
|
+
def get_sort_key(item):
|
|
401
|
+
if sort_by == 'timestamp':
|
|
402
|
+
ts = item[3]
|
|
403
|
+
if ts is not None:
|
|
404
|
+
return ts
|
|
405
|
+
return float('inf')
|
|
406
|
+
return item[0]
|
|
407
|
+
|
|
408
|
+
loaded_data.sort(key=get_sort_key)
|
|
409
|
+
|
|
410
|
+
successful_count = len(loaded_data)
|
|
411
|
+
message = f"Loaded {successful_count} image(s)"
|
|
412
|
+
if failed_count > 0:
|
|
413
|
+
message += f" (failed: {failed_count})"
|
|
414
|
+
|
|
415
|
+
result = [(item[1], item[2], item[3]) for item in loaded_data]
|
|
416
|
+
filenames = [item[0] for item in loaded_data]
|
|
417
|
+
|
|
418
|
+
return True, message, result, filenames
|
|
419
|
+
|
|
420
|
+
def split_by_count(
|
|
421
|
+
self,
|
|
422
|
+
images_with_times: List[Tuple[str, np.ndarray, Optional[float]]],
|
|
423
|
+
count_per_stack: int
|
|
424
|
+
) -> List[List[Tuple[str, np.ndarray, Optional[float]]]]:
|
|
425
|
+
if count_per_stack <= 0:
|
|
426
|
+
count_per_stack = 1
|
|
427
|
+
|
|
428
|
+
stacks = []
|
|
429
|
+
for i in range(0, len(images_with_times), count_per_stack):
|
|
430
|
+
stack = images_with_times[i:i + count_per_stack]
|
|
431
|
+
if stack:
|
|
432
|
+
stacks.append(stack)
|
|
433
|
+
|
|
434
|
+
return stacks
|
|
435
|
+
|
|
436
|
+
def split_by_time_threshold(
|
|
437
|
+
self,
|
|
438
|
+
images_with_times: list,
|
|
439
|
+
threshold_seconds: float
|
|
440
|
+
) -> list:
|
|
441
|
+
if threshold_seconds <= 0:
|
|
442
|
+
threshold_seconds = 1.0
|
|
443
|
+
|
|
444
|
+
stacks = []
|
|
445
|
+
current_stack = []
|
|
446
|
+
|
|
447
|
+
for i, (path, img, timestamp) in enumerate(images_with_times):
|
|
448
|
+
if i == 0:
|
|
449
|
+
current_stack.append((path, img, timestamp))
|
|
450
|
+
continue
|
|
451
|
+
|
|
452
|
+
if timestamp is None:
|
|
453
|
+
current_stack.append((path, img, timestamp))
|
|
454
|
+
continue
|
|
455
|
+
|
|
456
|
+
prev_timestamp = images_with_times[i - 1][2]
|
|
457
|
+
if prev_timestamp is None:
|
|
458
|
+
current_stack.append((path, img, timestamp))
|
|
459
|
+
continue
|
|
460
|
+
|
|
461
|
+
time_diff = timestamp - prev_timestamp
|
|
462
|
+
if time_diff > threshold_seconds:
|
|
463
|
+
if current_stack:
|
|
464
|
+
stacks.append(current_stack)
|
|
465
|
+
current_stack = [(path, img, timestamp)]
|
|
466
|
+
else:
|
|
467
|
+
current_stack.append((path, img, timestamp))
|
|
468
|
+
|
|
469
|
+
if current_stack:
|
|
470
|
+
stacks.append(current_stack)
|
|
471
|
+
|
|
472
|
+
return stacks
|
|
473
|
+
|
|
474
|
+
def get_stack_info(
|
|
475
|
+
self,
|
|
476
|
+
stacks: List[List[Tuple[str, np.ndarray, Optional[float]]]]
|
|
477
|
+
) -> List[Dict[str, Any]]:
|
|
478
|
+
info_list = []
|
|
479
|
+
for i, stack in enumerate(stacks):
|
|
480
|
+
count = len(stack)
|
|
481
|
+
first_img = stack[0][1]
|
|
482
|
+
height, width = first_img.shape[:2]
|
|
483
|
+
|
|
484
|
+
timestamps = [item[2] for item in stack if item[2] is not None]
|
|
485
|
+
if timestamps:
|
|
486
|
+
first_time = datetime.fromtimestamp(min(timestamps)).strftime('%H:%M:%S')
|
|
487
|
+
last_time = datetime.fromtimestamp(max(timestamps)).strftime('%H:%M:%S')
|
|
488
|
+
time_range = f"{first_time}-{last_time}"
|
|
489
|
+
else:
|
|
490
|
+
time_range = "Unknown"
|
|
491
|
+
|
|
492
|
+
info_list.append({
|
|
493
|
+
'stack_index': i,
|
|
494
|
+
'name': f"Stack {i + 1}",
|
|
495
|
+
'count': count,
|
|
496
|
+
'resolution': f"{width}x{height}",
|
|
497
|
+
'time_range': time_range
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
return info_list
|
core/models/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Neural network models for OpenFocus.
|
|
3
|
+
|
|
4
|
+
Modules:
|
|
5
|
+
- stackmffv4_network: StackMFF-V4 multi-focus fusion neural network
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from core.models.stackmffv4_network import (
|
|
9
|
+
StackMFF_V4,
|
|
10
|
+
LV_UNet,
|
|
11
|
+
lv_unet,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'StackMFF_V4',
|
|
16
|
+
'LV_UNet',
|
|
17
|
+
'lv_unet',
|
|
18
|
+
]
|