setiastrosuitepro 1.6.7__py3-none-any.whl → 1.7.0__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 setiastrosuitepro might be problematic. Click here for more details.
- setiastro/images/abeicon.svg +16 -0
- setiastro/images/colorwheel.svg +97 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/narrowbandnormalization.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/planetarystacker.png +0 -0
- setiastro/saspro/__main__.py +1 -1
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/aberration_ai.py +49 -11
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/add_stars.py +29 -5
- setiastro/saspro/backgroundneutral.py +73 -33
- setiastro/saspro/blink_comparator_pro.py +150 -55
- setiastro/saspro/convo.py +9 -6
- setiastro/saspro/cosmicclarity.py +125 -18
- setiastro/saspro/crop_dialog_pro.py +96 -2
- setiastro/saspro/curve_editor_pro.py +132 -61
- setiastro/saspro/curves_preset.py +249 -47
- setiastro/saspro/doc_manager.py +178 -11
- setiastro/saspro/frequency_separation.py +1159 -208
- setiastro/saspro/gui/main_window.py +340 -88
- setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
- setiastro/saspro/gui/mixins/file_mixin.py +35 -16
- setiastro/saspro/gui/mixins/menu_mixin.py +31 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +132 -10
- setiastro/saspro/gui/mixins/update_mixin.py +121 -33
- setiastro/saspro/histogram.py +179 -7
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +769 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
- setiastro/saspro/imageops/stretch.py +582 -62
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/numba_utils.py +68 -48
- setiastro/saspro/live_stacking.py +181 -73
- setiastro/saspro/multiscale_decomp.py +77 -29
- setiastro/saspro/narrowband_normalization.py +1618 -0
- setiastro/saspro/numba_utils.py +72 -57
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/ops/script_editor.py +5 -0
- setiastro/saspro/ops/scripts.py +119 -0
- setiastro/saspro/remove_green.py +1 -1
- setiastro/saspro/resources.py +4 -0
- setiastro/saspro/ser_stack_config.py +68 -0
- setiastro/saspro/ser_stacker.py +2245 -0
- setiastro/saspro/ser_stacker_dialog.py +1481 -0
- setiastro/saspro/ser_tracking.py +206 -0
- setiastro/saspro/serviewer.py +1242 -0
- setiastro/saspro/sfcc.py +602 -214
- setiastro/saspro/shortcuts.py +154 -25
- setiastro/saspro/signature_insert.py +688 -33
- setiastro/saspro/stacking_suite.py +853 -401
- setiastro/saspro/star_alignment.py +243 -122
- setiastro/saspro/stat_stretch.py +878 -131
- setiastro/saspro/subwindow.py +303 -74
- setiastro/saspro/whitebalance.py +24 -0
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/resource_monitor.py +128 -80
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/METADATA +2 -2
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/RECORD +68 -51
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/licenses/license.txt +0 -0
setiastro/saspro/layers.py
CHANGED
|
@@ -23,18 +23,19 @@ BLEND_MODES = [
|
|
|
23
23
|
@dataclass
|
|
24
24
|
class ImageLayer:
|
|
25
25
|
name: str
|
|
26
|
-
src_doc: object
|
|
26
|
+
src_doc: Optional[object] = None # ImageDocument (can be None for baked raster)
|
|
27
|
+
pixels: Optional[np.ndarray] = None # NEW: baked raster pixels in float32 or any dtype
|
|
28
|
+
|
|
27
29
|
visible: bool = True
|
|
28
|
-
opacity: float = 1.0
|
|
29
|
-
mode: str = "Normal"
|
|
30
|
-
mask_doc: Optional[object] = None
|
|
30
|
+
opacity: float = 1.0
|
|
31
|
+
mode: str = "Normal"
|
|
32
|
+
mask_doc: Optional[object] = None
|
|
31
33
|
mask_invert: bool = False
|
|
32
|
-
mask_feather: float = 0.0
|
|
34
|
+
mask_feather: float = 0.0
|
|
33
35
|
mask_use_luma: bool = False
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
sigmoid_strength: float = 10.0 # steepness of the curve
|
|
37
|
+
sigmoid_center: float = 0.5
|
|
38
|
+
sigmoid_strength: float = 10.0
|
|
38
39
|
|
|
39
40
|
def _float01(arr: np.ndarray) -> np.ndarray:
|
|
40
41
|
a = np.asarray(arr)
|
|
@@ -182,7 +183,10 @@ def composite_stack(base_img: np.ndarray, layers: List[ImageLayer]) -> np.ndarra
|
|
|
182
183
|
for L in reversed(layers or []):
|
|
183
184
|
if not L.visible:
|
|
184
185
|
continue
|
|
185
|
-
src = getattr(L
|
|
186
|
+
src = getattr(L, "pixels", None)
|
|
187
|
+
if src is None:
|
|
188
|
+
src_doc = getattr(L, "src_doc", None)
|
|
189
|
+
src = getattr(src_doc, "image", None) if src_doc is not None else None
|
|
186
190
|
if src is None:
|
|
187
191
|
continue
|
|
188
192
|
s = _ensure_3c(_float01(src))
|
setiastro/saspro/layers_dock.py
CHANGED
|
@@ -212,19 +212,29 @@ class LayersDock(QDockWidget):
|
|
|
212
212
|
top.addWidget(self.view_combo, 1)
|
|
213
213
|
|
|
214
214
|
self.list = QListWidget()
|
|
215
|
-
self.list.setSelectionMode(QAbstractItemView.SelectionMode.
|
|
215
|
+
self.list.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
|
216
216
|
self.list.setAlternatingRowColors(True)
|
|
217
217
|
v.addWidget(self.list, 1)
|
|
218
218
|
|
|
219
219
|
# buttons
|
|
220
220
|
row = QHBoxLayout(); v.addLayout(row)
|
|
221
|
+
|
|
221
222
|
self.btn_clear = QPushButton("Clear All Layers")
|
|
222
|
-
|
|
223
|
+
|
|
224
|
+
self.btn_merge = QPushButton("Merge → Push to View")
|
|
223
225
|
self.btn_merge.setToolTip("Flatten the visible layers into the current view and add an undo step.")
|
|
226
|
+
|
|
227
|
+
self.btn_merge_new = QPushButton("Merge → New Document")
|
|
228
|
+
self.btn_merge_new.setToolTip("Flatten the visible layers into a new document (does not modify the base view).")
|
|
229
|
+
|
|
230
|
+
self.btn_merge_sel = QPushButton("Merge Selected → Single Layer")
|
|
231
|
+
self.btn_merge_sel.setToolTip("Merge the selected layers into one raster layer (Photoshop-style).")
|
|
232
|
+
|
|
224
233
|
row.addWidget(self.btn_merge)
|
|
234
|
+
row.addWidget(self.btn_merge_new)
|
|
235
|
+
row.addWidget(self.btn_merge_sel)
|
|
225
236
|
row.addStretch(1)
|
|
226
237
|
row.addWidget(self.btn_clear)
|
|
227
|
-
|
|
228
238
|
self.setWidget(w)
|
|
229
239
|
|
|
230
240
|
# dnd (accept drops from views)
|
|
@@ -240,6 +250,8 @@ class LayersDock(QDockWidget):
|
|
|
240
250
|
self.docman.documentRemoved.connect(lambda _d: self._refresh_views())
|
|
241
251
|
|
|
242
252
|
self.btn_merge.clicked.connect(self._merge_and_push)
|
|
253
|
+
self.btn_merge_new.clicked.connect(self._merge_to_new_doc)
|
|
254
|
+
self.btn_merge_sel.clicked.connect(self._merge_selected_to_single_layer)
|
|
243
255
|
|
|
244
256
|
# initial
|
|
245
257
|
self._refresh_views()
|
|
@@ -420,8 +432,107 @@ class LayersDock(QDockWidget):
|
|
|
420
432
|
has_layers = bool(getattr(vw, "_layers", []))
|
|
421
433
|
self.btn_merge.setEnabled(has_layers)
|
|
422
434
|
self.btn_clear.setEnabled(has_layers)
|
|
435
|
+
if hasattr(self, "btn_merge_new"):
|
|
436
|
+
self.btn_merge_new.setEnabled(has_layers)
|
|
437
|
+
has_layers = bool(getattr(vw, "_layers", []))
|
|
438
|
+
self.btn_merge_sel.setEnabled(has_layers)
|
|
423
439
|
self._refresh_row_heights()
|
|
424
440
|
|
|
441
|
+
def _selected_layer_indices(self) -> list[int]:
|
|
442
|
+
vw = self.current_view()
|
|
443
|
+
if not vw:
|
|
444
|
+
return []
|
|
445
|
+
n = len(getattr(vw, "_layers", []) or [])
|
|
446
|
+
idxs = []
|
|
447
|
+
for it in self.list.selectedItems():
|
|
448
|
+
r = self.list.row(it)
|
|
449
|
+
if 0 <= r < n:
|
|
450
|
+
idxs.append(r)
|
|
451
|
+
idxs = sorted(set(idxs))
|
|
452
|
+
return idxs
|
|
453
|
+
|
|
454
|
+
def _render_stack(self, base_img: np.ndarray, layers: list[ImageLayer]) -> np.ndarray:
|
|
455
|
+
# composite_stack already respects visibility/opacity/modes/masks
|
|
456
|
+
out = composite_stack(base_img, layers)
|
|
457
|
+
return out if out is not None else base_img
|
|
458
|
+
|
|
459
|
+
def _open_baked_layer_doc(self, base_doc, arr: np.ndarray, title: str):
|
|
460
|
+
dm = getattr(self.mw, "docman", None)
|
|
461
|
+
if not dm or not hasattr(dm, "open_array"):
|
|
462
|
+
return None
|
|
463
|
+
meta = dict(getattr(base_doc, "metadata", {}) or {})
|
|
464
|
+
meta.update({
|
|
465
|
+
"bit_depth": "32-bit floating point",
|
|
466
|
+
"is_mono": (arr.ndim == 2 or (arr.ndim == 3 and arr.shape[-1] == 1)),
|
|
467
|
+
"source": "Layers Merge Selected",
|
|
468
|
+
})
|
|
469
|
+
return dm.open_array(arr.astype(np.float32, copy=False), metadata=meta, title=title)
|
|
470
|
+
|
|
471
|
+
def _merge_selected_to_single_layer(self):
|
|
472
|
+
vw = self.current_view()
|
|
473
|
+
if not vw:
|
|
474
|
+
return
|
|
475
|
+
|
|
476
|
+
layers = list(getattr(vw, "_layers", []) or [])
|
|
477
|
+
if not layers:
|
|
478
|
+
QMessageBox.information(self, "Layers", "There are no layers to merge.")
|
|
479
|
+
return
|
|
480
|
+
|
|
481
|
+
sel = self._selected_layer_indices()
|
|
482
|
+
if len(sel) < 2:
|
|
483
|
+
QMessageBox.information(self, "Layers", "Select two or more layers to merge.")
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
try:
|
|
487
|
+
base_doc = getattr(vw, "document", None)
|
|
488
|
+
if base_doc is None or getattr(base_doc, "image", None) is None:
|
|
489
|
+
QMessageBox.warning(self, "Layers", "No base image available for this view.")
|
|
490
|
+
return
|
|
491
|
+
base_img = base_doc.image
|
|
492
|
+
|
|
493
|
+
i0, i1 = sel[0], sel[-1]
|
|
494
|
+
|
|
495
|
+
# IMPORTANT ASSUMPTION (matches your UI order):
|
|
496
|
+
# vw._layers is top-to-bottom in the list, and "below" means larger index.
|
|
497
|
+
layers_above = layers[:i0]
|
|
498
|
+
layers_sel = layers[i0:i1+1]
|
|
499
|
+
layers_below = layers[i1+1:]
|
|
500
|
+
|
|
501
|
+
# 1) Render what exists directly under the selected range
|
|
502
|
+
under = self._render_stack(base_img, layers_below)
|
|
503
|
+
|
|
504
|
+
# 2) Render selected layers on top of that "under" image
|
|
505
|
+
baked = self._render_stack(under, layers_sel)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
# 3) Create a baked raster layer (NO new document)
|
|
509
|
+
merged_layer = ImageLayer(
|
|
510
|
+
name=f"Merged ({len(layers_sel)})",
|
|
511
|
+
src_doc=None,
|
|
512
|
+
pixels=baked.astype(np.float32, copy=False),
|
|
513
|
+
visible=True,
|
|
514
|
+
opacity=1.0,
|
|
515
|
+
mode="Normal",
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
# Keep masks off by default; you can also decide to inherit the topmost mask
|
|
519
|
+
merged_layer.mask_doc = None
|
|
520
|
+
merged_layer.mask_use_luma = True
|
|
521
|
+
merged_layer.mask_invert = False
|
|
522
|
+
|
|
523
|
+
new_layers = layers_above + [merged_layer] + layers_below
|
|
524
|
+
vw._layers = new_layers
|
|
525
|
+
|
|
526
|
+
vw._reinstall_layer_watchers()
|
|
527
|
+
self._rebuild_list()
|
|
528
|
+
vw.apply_layer_stack(vw._layers)
|
|
529
|
+
|
|
530
|
+
QMessageBox.information(self, "Layers", f"Merged {len(layers_sel)} layers into a single layer.")
|
|
531
|
+
except Exception as ex:
|
|
532
|
+
print("[LayersDock] merge_selected error:", ex)
|
|
533
|
+
QMessageBox.critical(self, "Layers", f"Merge Selected failed:\n{ex}")
|
|
534
|
+
|
|
535
|
+
|
|
425
536
|
def _layer_count(self) -> int:
|
|
426
537
|
vw = self.current_view()
|
|
427
538
|
return len(getattr(vw, "_layers", [])) if vw else 0
|
|
@@ -712,3 +823,72 @@ class LayersDock(QDockWidget):
|
|
|
712
823
|
print("[LayersDock] merge error:", ex)
|
|
713
824
|
QMessageBox.critical(self, "Layers", f"Merge failed:\n{ex}")
|
|
714
825
|
|
|
826
|
+
def _merge_to_new_doc(self):
|
|
827
|
+
vw = self.current_view()
|
|
828
|
+
if not vw:
|
|
829
|
+
return
|
|
830
|
+
|
|
831
|
+
layers = list(getattr(vw, "_layers", []) or [])
|
|
832
|
+
if not layers:
|
|
833
|
+
QMessageBox.information(self, "Layers", "There are no layers to merge.")
|
|
834
|
+
return
|
|
835
|
+
|
|
836
|
+
try:
|
|
837
|
+
base_doc = getattr(vw, "document", None)
|
|
838
|
+
if base_doc is None or getattr(base_doc, "image", None) is None:
|
|
839
|
+
QMessageBox.warning(self, "Layers", "No base image available for this view.")
|
|
840
|
+
return
|
|
841
|
+
|
|
842
|
+
base_img = base_doc.image
|
|
843
|
+
merged = composite_stack(base_img, layers)
|
|
844
|
+
if merged is None:
|
|
845
|
+
QMessageBox.warning(self, "Layers", "Composite failed (empty result).")
|
|
846
|
+
return
|
|
847
|
+
|
|
848
|
+
# Push as a new document (same pattern as stars-only)
|
|
849
|
+
self._push_merged_as_new_doc(base_doc, merged)
|
|
850
|
+
|
|
851
|
+
QMessageBox.information(self, "Layers",
|
|
852
|
+
"Merged visible layers and created a new document.")
|
|
853
|
+
except Exception as ex:
|
|
854
|
+
print("[LayersDock] merge_to_new_doc error:", ex)
|
|
855
|
+
QMessageBox.critical(self, "Layers", f"Merge failed:\n{ex}")
|
|
856
|
+
|
|
857
|
+
def _push_merged_as_new_doc(self, base_doc, arr: np.ndarray):
|
|
858
|
+
dm = getattr(self.mw, "docman", None)
|
|
859
|
+
if not dm or not hasattr(dm, "open_array"):
|
|
860
|
+
return
|
|
861
|
+
|
|
862
|
+
# Derive a friendly title based on the *view title* if possible
|
|
863
|
+
title = None
|
|
864
|
+
try:
|
|
865
|
+
# Use current view title (respects per-view rename)
|
|
866
|
+
vw = self.current_view()
|
|
867
|
+
if vw and hasattr(vw, "_effective_title"):
|
|
868
|
+
base = (vw._effective_title() or "").strip()
|
|
869
|
+
else:
|
|
870
|
+
base = ""
|
|
871
|
+
|
|
872
|
+
if not base:
|
|
873
|
+
dn = getattr(base_doc, "display_name", None)
|
|
874
|
+
base = dn() if callable(dn) else (dn or "Untitled")
|
|
875
|
+
|
|
876
|
+
suffix = "_merged"
|
|
877
|
+
title = base if base.endswith(suffix) else f"{base}{suffix}"
|
|
878
|
+
except Exception:
|
|
879
|
+
title = "Merged Layers"
|
|
880
|
+
|
|
881
|
+
try:
|
|
882
|
+
meta = dict(getattr(base_doc, "metadata", {}) or {})
|
|
883
|
+
meta.update({
|
|
884
|
+
"bit_depth": "32-bit floating point",
|
|
885
|
+
"is_mono": (arr.ndim == 2 or (arr.ndim == 3 and arr.shape[-1] == 1)),
|
|
886
|
+
"source": "Layers Merge",
|
|
887
|
+
"step_name": "Layers Merge",
|
|
888
|
+
})
|
|
889
|
+
|
|
890
|
+
newdoc = dm.open_array(arr.astype(np.float32, copy=False), metadata=meta, title=title)
|
|
891
|
+
if hasattr(self.mw, "_spawn_subwindow_for"):
|
|
892
|
+
self.mw._spawn_subwindow_for(newdoc)
|
|
893
|
+
except Exception as ex:
|
|
894
|
+
print("[LayersDock] _push_merged_as_new_doc error:", ex)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#legacy.numba_utils.py
|
|
1
|
+
#src.setiastro.saspro.legacy.numba_utils.py
|
|
2
2
|
import numpy as np
|
|
3
3
|
from numba import njit, prange
|
|
4
4
|
from numba.typed import List
|
|
@@ -407,43 +407,7 @@ def normalize_flat_cfa_inplace(flat2d: np.ndarray, pattern: str, *, combine_gree
|
|
|
407
407
|
flat2d[flat2d == 0] = 1.0
|
|
408
408
|
return flat2d
|
|
409
409
|
|
|
410
|
-
@njit(parallel=True, fastmath=True)
|
|
411
|
-
def apply_flat_division_numba_2d(image, master_flat, master_bias=None):
|
|
412
|
-
"""
|
|
413
|
-
Mono version: image.shape == (H,W)
|
|
414
|
-
"""
|
|
415
|
-
if master_bias is not None:
|
|
416
|
-
master_flat = master_flat - master_bias
|
|
417
|
-
image = image - master_bias
|
|
418
|
-
|
|
419
|
-
median_flat = np.mean(master_flat)
|
|
420
|
-
height, width = image.shape
|
|
421
|
-
|
|
422
|
-
for y in prange(height):
|
|
423
|
-
for x in range(width):
|
|
424
|
-
image[y, x] /= (master_flat[y, x] / median_flat)
|
|
425
|
-
|
|
426
|
-
return image
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
@njit(parallel=True, fastmath=True)
|
|
430
|
-
def apply_flat_division_numba_3d(image, master_flat, master_bias=None):
|
|
431
|
-
"""
|
|
432
|
-
Color version: image.shape == (H,W,C)
|
|
433
|
-
"""
|
|
434
|
-
if master_bias is not None:
|
|
435
|
-
master_flat = master_flat - master_bias
|
|
436
|
-
image = image - master_bias
|
|
437
410
|
|
|
438
|
-
median_flat = np.mean(master_flat)
|
|
439
|
-
height, width, channels = image.shape
|
|
440
|
-
|
|
441
|
-
for y in prange(height):
|
|
442
|
-
for x in range(width):
|
|
443
|
-
for c in range(channels):
|
|
444
|
-
image[y, x, c] /= (master_flat[y, x, c] / median_flat)
|
|
445
|
-
|
|
446
|
-
return image
|
|
447
411
|
|
|
448
412
|
@njit(parallel=True, fastmath=True)
|
|
449
413
|
def _flat_div_2d(img, flat):
|
|
@@ -563,24 +527,37 @@ def apply_flat_division_numba_bayer_2d(image, master_flat, med4, pat_id):
|
|
|
563
527
|
Bayer-aware mono division. image/master_flat are (H,W).
|
|
564
528
|
med4 is [R,G1,G2,B] for that master_flat, pat_id in {0..3}.
|
|
565
529
|
"""
|
|
530
|
+
# parity index = (row&1)*2 + (col&1)
|
|
531
|
+
# med4 index order: 0=R, 1=G1, 2=G2, 3=B
|
|
532
|
+
|
|
533
|
+
# tables map parity_index -> med4 index
|
|
534
|
+
# parity_index: 0:(0,0) 1:(0,1) 2:(1,0) 3:(1,1)
|
|
535
|
+
if pat_id == 0: # RGGB: (0,0)R (0,1)G1 (1,0)G2 (1,1)B
|
|
536
|
+
t0, t1, t2, t3 = 0, 1, 2, 3
|
|
537
|
+
elif pat_id == 1: # BGGR: (0,0)B (0,1)G1 (1,0)G2 (1,1)R
|
|
538
|
+
t0, t1, t2, t3 = 3, 1, 2, 0
|
|
539
|
+
elif pat_id == 2: # GRBG: (0,0)G1 (0,1)R (1,0)B (1,1)G2
|
|
540
|
+
t0, t1, t2, t3 = 1, 0, 3, 2
|
|
541
|
+
else: # GBRG: (0,0)G1 (0,1)B (1,0)R (1,1)G2
|
|
542
|
+
t0, t1, t2, t3 = 1, 3, 0, 2
|
|
543
|
+
|
|
566
544
|
H, W = image.shape
|
|
567
545
|
for y in prange(H):
|
|
568
546
|
y1 = y & 1
|
|
569
547
|
for x in range(W):
|
|
570
548
|
x1 = x & 1
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
pi = 1 if (y1==0 and x1==0) else 3 if (y1==0 and x1==1) else 0 if (y1==1 and x1==0) else 2
|
|
549
|
+
p = (y1 << 1) | x1 # 0..3
|
|
550
|
+
if p == 0:
|
|
551
|
+
pi = t0
|
|
552
|
+
elif p == 1:
|
|
553
|
+
pi = t1
|
|
554
|
+
elif p == 2:
|
|
555
|
+
pi = t2
|
|
556
|
+
else:
|
|
557
|
+
pi = t3
|
|
581
558
|
|
|
582
559
|
denom = master_flat[y, x] / med4[pi]
|
|
583
|
-
if denom == 0.0 or not np.isfinite(denom):
|
|
560
|
+
if denom == 0.0 or (not np.isfinite(denom)):
|
|
584
561
|
denom = 1.0
|
|
585
562
|
image[y, x] /= denom
|
|
586
563
|
return image
|
|
@@ -2790,7 +2767,50 @@ def evaluate_polynomial(H: int, W: int, coeffs: np.ndarray, degree: int) -> np.n
|
|
|
2790
2767
|
A_full = build_poly_terms(xx.ravel(), yy.ravel(), degree)
|
|
2791
2768
|
return (A_full @ coeffs).reshape(H, W)
|
|
2792
2769
|
|
|
2770
|
+
@njit(parallel=True, fastmath=True)
|
|
2771
|
+
def numba_mono_from_img(img, bp, denom, median_rescaled, target_median):
|
|
2772
|
+
H, W = img.shape
|
|
2773
|
+
out = np.empty_like(img)
|
|
2774
|
+
for y in prange(H):
|
|
2775
|
+
for x in range(W):
|
|
2776
|
+
r = (img[y, x] - bp) / denom
|
|
2777
|
+
numer = (median_rescaled - 1.0) * target_median * r
|
|
2778
|
+
denom2 = median_rescaled * (target_median + r - 1.0) - target_median * r
|
|
2779
|
+
if abs(denom2) < 1e-12:
|
|
2780
|
+
denom2 = 1e-12
|
|
2781
|
+
out[y, x] = numer / denom2
|
|
2782
|
+
return out
|
|
2793
2783
|
|
|
2784
|
+
@njit(parallel=True, fastmath=True)
|
|
2785
|
+
def numba_color_linked_from_img(img, bp, denom, median_rescaled, target_median):
|
|
2786
|
+
H, W, C = img.shape
|
|
2787
|
+
out = np.empty_like(img)
|
|
2788
|
+
for y in prange(H):
|
|
2789
|
+
for x in range(W):
|
|
2790
|
+
for c in range(C):
|
|
2791
|
+
r = (img[y, x, c] - bp) / denom
|
|
2792
|
+
numer = (median_rescaled - 1.0) * target_median * r
|
|
2793
|
+
denom2 = median_rescaled * (target_median + r - 1.0) - target_median * r
|
|
2794
|
+
if abs(denom2) < 1e-12:
|
|
2795
|
+
denom2 = 1e-12
|
|
2796
|
+
out[y, x, c] = numer / denom2
|
|
2797
|
+
return out
|
|
2798
|
+
|
|
2799
|
+
@njit(parallel=True, fastmath=True)
|
|
2800
|
+
def numba_color_unlinked_from_img(img, bp3, denom3, meds_rescaled3, target_median):
|
|
2801
|
+
H, W, C = img.shape
|
|
2802
|
+
out = np.empty_like(img)
|
|
2803
|
+
for y in prange(H):
|
|
2804
|
+
for x in range(W):
|
|
2805
|
+
for c in range(C):
|
|
2806
|
+
r = (img[y, x, c] - bp3[c]) / denom3[c]
|
|
2807
|
+
med = meds_rescaled3[c]
|
|
2808
|
+
numer = (med - 1.0) * target_median * r
|
|
2809
|
+
denom2 = med * (target_median + r - 1.0) - target_median * r
|
|
2810
|
+
if abs(denom2) < 1e-12:
|
|
2811
|
+
denom2 = 1e-12
|
|
2812
|
+
out[y, x, c] = numer / denom2
|
|
2813
|
+
return out
|
|
2794
2814
|
|
|
2795
2815
|
@njit(parallel=True, fastmath=True)
|
|
2796
2816
|
def numba_mono_final_formula(rescaled, median_rescaled, target_median):
|