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.

Files changed (68) hide show
  1. setiastro/images/abeicon.svg +16 -0
  2. setiastro/images/colorwheel.svg +97 -0
  3. setiastro/images/cosmic.svg +40 -0
  4. setiastro/images/cosmicsat.svg +24 -0
  5. setiastro/images/graxpert.svg +19 -0
  6. setiastro/images/linearfit.svg +32 -0
  7. setiastro/images/narrowbandnormalization.png +0 -0
  8. setiastro/images/pixelmath.svg +42 -0
  9. setiastro/images/planetarystacker.png +0 -0
  10. setiastro/saspro/__main__.py +1 -1
  11. setiastro/saspro/_generated/build_info.py +2 -2
  12. setiastro/saspro/aberration_ai.py +49 -11
  13. setiastro/saspro/aberration_ai_preset.py +29 -3
  14. setiastro/saspro/add_stars.py +29 -5
  15. setiastro/saspro/backgroundneutral.py +73 -33
  16. setiastro/saspro/blink_comparator_pro.py +150 -55
  17. setiastro/saspro/convo.py +9 -6
  18. setiastro/saspro/cosmicclarity.py +125 -18
  19. setiastro/saspro/crop_dialog_pro.py +96 -2
  20. setiastro/saspro/curve_editor_pro.py +132 -61
  21. setiastro/saspro/curves_preset.py +249 -47
  22. setiastro/saspro/doc_manager.py +178 -11
  23. setiastro/saspro/frequency_separation.py +1159 -208
  24. setiastro/saspro/gui/main_window.py +340 -88
  25. setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
  26. setiastro/saspro/gui/mixins/file_mixin.py +35 -16
  27. setiastro/saspro/gui/mixins/menu_mixin.py +31 -1
  28. setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
  29. setiastro/saspro/gui/mixins/toolbar_mixin.py +132 -10
  30. setiastro/saspro/gui/mixins/update_mixin.py +121 -33
  31. setiastro/saspro/histogram.py +179 -7
  32. setiastro/saspro/imageops/narrowband_normalization.py +816 -0
  33. setiastro/saspro/imageops/serloader.py +769 -0
  34. setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
  35. setiastro/saspro/imageops/stretch.py +582 -62
  36. setiastro/saspro/layers.py +13 -9
  37. setiastro/saspro/layers_dock.py +183 -3
  38. setiastro/saspro/legacy/numba_utils.py +68 -48
  39. setiastro/saspro/live_stacking.py +181 -73
  40. setiastro/saspro/multiscale_decomp.py +77 -29
  41. setiastro/saspro/narrowband_normalization.py +1618 -0
  42. setiastro/saspro/numba_utils.py +72 -57
  43. setiastro/saspro/ops/commands.py +18 -18
  44. setiastro/saspro/ops/script_editor.py +5 -0
  45. setiastro/saspro/ops/scripts.py +119 -0
  46. setiastro/saspro/remove_green.py +1 -1
  47. setiastro/saspro/resources.py +4 -0
  48. setiastro/saspro/ser_stack_config.py +68 -0
  49. setiastro/saspro/ser_stacker.py +2245 -0
  50. setiastro/saspro/ser_stacker_dialog.py +1481 -0
  51. setiastro/saspro/ser_tracking.py +206 -0
  52. setiastro/saspro/serviewer.py +1242 -0
  53. setiastro/saspro/sfcc.py +602 -214
  54. setiastro/saspro/shortcuts.py +154 -25
  55. setiastro/saspro/signature_insert.py +688 -33
  56. setiastro/saspro/stacking_suite.py +853 -401
  57. setiastro/saspro/star_alignment.py +243 -122
  58. setiastro/saspro/stat_stretch.py +878 -131
  59. setiastro/saspro/subwindow.py +303 -74
  60. setiastro/saspro/whitebalance.py +24 -0
  61. setiastro/saspro/widgets/common_utilities.py +28 -21
  62. setiastro/saspro/widgets/resource_monitor.py +128 -80
  63. {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/METADATA +2 -2
  64. {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/RECORD +68 -51
  65. {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/WHEEL +0 -0
  66. {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/entry_points.txt +0 -0
  67. {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/licenses/LICENSE +0 -0
  68. {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/licenses/license.txt +0 -0
@@ -317,61 +317,6 @@ def invert_image_numba(image):
317
317
  output[y, x, c] = 1.0 - image[y, x, c]
318
318
  return output
319
319
 
320
-
321
-
322
- @njit(parallel=True, fastmath=True, cache=True)
323
- def apply_flat_division_numba_2d(image, master_flat, master_bias=None):
324
- """
325
- Mono version: image.shape == (H,W)
326
- """
327
- if master_bias is not None:
328
- master_flat = master_flat - master_bias
329
- image = image - master_bias
330
-
331
- median_flat = np.mean(master_flat)
332
- height, width = image.shape
333
-
334
- for y in prange(height):
335
- for x in range(width):
336
- image[y, x] /= (master_flat[y, x] / median_flat)
337
-
338
- return image
339
-
340
-
341
- @njit(parallel=True, fastmath=True, cache=True)
342
- def apply_flat_division_numba_3d(image, master_flat, master_bias=None):
343
- """
344
- Color version: image.shape == (H,W,C)
345
- """
346
- if master_bias is not None:
347
- master_flat = master_flat - master_bias
348
- image = image - master_bias
349
-
350
- median_flat = np.mean(master_flat)
351
- height, width, channels = image.shape
352
-
353
- for y in prange(height):
354
- for x in range(width):
355
- for c in range(channels):
356
- image[y, x, c] /= (master_flat[y, x, c] / median_flat)
357
-
358
- return image
359
-
360
- def apply_flat_division_numba(image, master_flat, master_bias=None):
361
- """
362
- Dispatcher that calls the correct Numba function
363
- depending on whether 'image' is 2D or 3D.
364
- """
365
- if image.ndim == 2:
366
- # Mono
367
- return apply_flat_division_numba_2d(image, master_flat, master_bias)
368
- elif image.ndim == 3:
369
- # Color
370
- return apply_flat_division_numba_3d(image, master_flat, master_bias)
371
- else:
372
- raise ValueError(f"apply_flat_division_numba: expected 2D or 3D, got shape {image.shape}")
373
-
374
-
375
320
  @njit(parallel=True, cache=True)
376
321
  def subtract_dark_3d(frames, dark_frame):
377
322
  """
@@ -2495,7 +2440,77 @@ def drizzle_deposit_color_naive(image_data, affine_2x3, drizzle_buffer, coverage
2495
2440
 
2496
2441
  return drizzle_buffer, coverage_buffer
2497
2442
 
2498
- @njit(parallel=True, fastmath=True, cache=True)
2443
+ @njit(parallel=True, fastmath=True)
2444
+ def numba_mono_from_img(img, bp, denom, median_rescaled, target_median):
2445
+ H, W = img.shape
2446
+ out = np.empty_like(img)
2447
+ for y in prange(H):
2448
+ for x in range(W):
2449
+ r = (img[y, x] - bp) / denom
2450
+ numer = (median_rescaled - 1.0) * target_median * r
2451
+ denom2 = median_rescaled * (target_median + r - 1.0) - target_median * r
2452
+ if abs(denom2) < 1e-12:
2453
+ denom2 = 1e-12
2454
+ out[y, x] = numer / denom2
2455
+ return out
2456
+
2457
+ @njit(parallel=True, fastmath=True)
2458
+ def numba_color_linked_from_img(img, bp, denom, median_rescaled, target_median):
2459
+ H, W, C = img.shape
2460
+ out = np.empty_like(img)
2461
+ for y in prange(H):
2462
+ for x in range(W):
2463
+ for c in range(C):
2464
+ r = (img[y, x, c] - bp) / denom
2465
+ numer = (median_rescaled - 1.0) * target_median * r
2466
+ denom2 = median_rescaled * (target_median + r - 1.0) - target_median * r
2467
+ if abs(denom2) < 1e-12:
2468
+ denom2 = 1e-12
2469
+ out[y, x, c] = numer / denom2
2470
+ return out
2471
+
2472
+ @njit(parallel=True, fastmath=True)
2473
+ def numba_color_unlinked_from_img(img, bp3, denom3, meds_rescaled3, target_median):
2474
+ H, W, C = img.shape
2475
+ out = np.empty_like(img)
2476
+ for y in prange(H):
2477
+ for x in range(W):
2478
+ for c in range(C):
2479
+ r = (img[y, x, c] - bp3[c]) / denom3[c]
2480
+ med = meds_rescaled3[c]
2481
+ numer = (med - 1.0) * target_median * r
2482
+ denom2 = med * (target_median + r - 1.0) - target_median * r
2483
+ if abs(denom2) < 1e-12:
2484
+ denom2 = 1e-12
2485
+ out[y, x, c] = numer / denom2
2486
+ return out
2487
+
2488
+ @njit(parallel=True, fastmath=True)
2489
+ def numba_mono_final_formula(rescaled, median_rescaled, target_median):
2490
+ """
2491
+ Applies the final formula *after* we already have the rescaled values.
2492
+
2493
+ rescaled[y,x] = (original[y,x] - black_point) / (1 - black_point)
2494
+ median_rescaled = median(rescaled)
2495
+
2496
+ out_val = ((median_rescaled - 1) * target_median * r) /
2497
+ ( median_rescaled*(target_median + r -1) - target_median*r )
2498
+ """
2499
+ H, W = rescaled.shape
2500
+ out = np.empty_like(rescaled)
2501
+
2502
+ for y in prange(H):
2503
+ for x in range(W):
2504
+ r = rescaled[y, x]
2505
+ numer = (median_rescaled - 1.0) * target_median * r
2506
+ denom = median_rescaled * (target_median + r - 1.0) - target_median * r
2507
+ if np.abs(denom) < 1e-12:
2508
+ denom = 1e-12
2509
+ out[y, x] = numer / denom
2510
+
2511
+ return out
2512
+
2513
+ @njit(parallel=True, fastmath=True)
2499
2514
  def numba_color_final_formula_linked(rescaled, median_rescaled, target_median):
2500
2515
  """
2501
2516
  Linked color transform: we use one median_rescaled for all channels.
@@ -2517,7 +2532,7 @@ def numba_color_final_formula_linked(rescaled, median_rescaled, target_median):
2517
2532
 
2518
2533
  return out
2519
2534
 
2520
- @njit(parallel=True, fastmath=True, cache=True)
2535
+ @njit(parallel=True, fastmath=True)
2521
2536
  def numba_color_final_formula_unlinked(rescaled, medians_rescaled, target_median):
2522
2537
  """
2523
2538
  Unlinked color transform: a separate median_rescaled per channel.
@@ -202,7 +202,7 @@ register(CommandSpec(
202
202
  "optional targets, inherit_target."
203
203
  ),
204
204
  call_style="ctx.run_command",
205
- import_path="pro.function_bundle", # <── important
205
+ import_path="setiastro.saspro.function_bundle", # <── important
206
206
  callable_name="run_function_bundle_command",# <── important
207
207
  notes=(
208
208
  "Use this command from scripts to run a saved Function Bundle or an "
@@ -274,7 +274,7 @@ register(CommandSpec(
274
274
  group="Bundles",
275
275
  summary="Internal bundle runner. steps=[...], targets='all_open'|[doc_ptrs], stop_on_error.",
276
276
  call_style="ctx.run_command",
277
- import_path="pro.function_bundle",
277
+ import_path="setiastro.saspro.function_bundle",
278
278
  callable_name="run_function_bundle_command",
279
279
  ))
280
280
 
@@ -388,7 +388,7 @@ register(CommandSpec(
388
388
  id="ghs",
389
389
  name="Generalized Hyperbolic Stretch",
390
390
  group="Stretch",
391
- import_path="pro.ghs_preset",
391
+ import_path="setiastro.saspro.ghs_preset",
392
392
  callable_name="apply_ghs_via_preset",
393
393
  ui_method="open_ghs_with_preset",
394
394
  summary=(
@@ -499,7 +499,7 @@ register(CommandSpec(
499
499
  id="curves",
500
500
  title="Curves",
501
501
  group="Stretch",
502
- import_path="pro.curves_preset",
502
+ import_path="setiastro.saspro.curves_preset",
503
503
  callable_name="apply_curves_via_preset",
504
504
  ui_method="open_curves_with_preset",
505
505
  summary=(
@@ -597,7 +597,7 @@ register(CommandSpec(
597
597
  id="abe",
598
598
  title="Automatic Background Extraction",
599
599
  group="Background",
600
- import_path="pro.abe_preset",
600
+ import_path="setiastro.saspro.abe_preset",
601
601
  callable_name="apply_abe_via_preset",
602
602
  ui_method="open_abe_with_preset", # ✅ matches your pro/abe_preset.py
603
603
  summary=(
@@ -683,7 +683,7 @@ register(CommandSpec(
683
683
  id="graxpert",
684
684
  title="GraXpert Gradient / Denoise",
685
685
  group="Background",
686
- import_path="pro.graxpert_preset",
686
+ import_path="setiastro.saspro.graxpert_preset",
687
687
  callable_name="run_graxpert_via_preset",
688
688
  # no ui_method here unless you want to open your optional preset dialog from drops
689
689
  # ui_method="open_graxpert_with_preset", # (only if/when you add one)
@@ -807,7 +807,7 @@ register(CommandSpec(
807
807
  id="background_neutral",
808
808
  name="Background Neutralization",
809
809
  group="Background",
810
- import_path="pro.backgroundneutral",
810
+ import_path="setiastro.saspro.backgroundneutral",
811
811
  callable_name="run_background_neutral_via_preset",
812
812
  summary=(
813
813
  "Neutralizes RGB background either automatically or using a user-specified "
@@ -865,7 +865,7 @@ register(CommandSpec(
865
865
  id="remove_green",
866
866
  name="Remove Green (SCNR)",
867
867
  group="Color",
868
- import_path="pro.remove_green",
868
+ import_path="setiastro.saspro.remove_green",
869
869
  callable_name="apply_remove_green_preset_to_doc",
870
870
  ui_method="open_remove_green_dialog",
871
871
  summary=(
@@ -1044,7 +1044,7 @@ register(CommandSpec(
1044
1044
  id="recombine_luminance",
1045
1045
  name="Recombine Luminance",
1046
1046
  group="Luminance",
1047
- import_path="pro.luminancerecombine",
1047
+ import_path="setiastro.saspro.luminancerecombine",
1048
1048
  callable_name="run_recombine_luminance_via_preset",
1049
1049
  ui_method="_recombine_luminance_ui",
1050
1050
  notes=(
@@ -1144,7 +1144,7 @@ register(CommandSpec(
1144
1144
  id="wavescale_hdr",
1145
1145
  name="WaveScale HDR",
1146
1146
  group="Contrast",
1147
- import_path="pro.wavescale_hdr_preset",
1147
+ import_path="setiastro.saspro.wavescale_hdr_preset",
1148
1148
  callable_name="run_wavescale_hdr_via_preset",
1149
1149
  ui_method="_open_wavescale_hdr", # or whatever your main window uses
1150
1150
  summary=(
@@ -1188,7 +1188,7 @@ register(CommandSpec(
1188
1188
  id="wavescale_dark_enhance",
1189
1189
  name="WaveScale Dark Enhance",
1190
1190
  group="Contrast",
1191
- import_path="pro.wavescalede_preset",
1191
+ import_path="setiastro.saspro.wavescalede_preset",
1192
1192
  callable_name="run_wavescalede_via_preset",
1193
1193
  ui_method="_open_wavescale_dark_enhance", # adjust if your main window uses a different name
1194
1194
  summary=(
@@ -1295,7 +1295,7 @@ register(CommandSpec(
1295
1295
  id="aberration_ai",
1296
1296
  title="Aberration AI",
1297
1297
  group="Optics",
1298
- import_path="pro.aberration_ai_preset",
1298
+ import_path="setiastro.saspro.aberration_ai_preset",
1299
1299
  callable_name="run_aberration_ai_via_preset",
1300
1300
  # ui_method="open_aberration_ai_dialog", # if you have one; otherwise omit
1301
1301
  presets=[
@@ -1338,7 +1338,7 @@ register(CommandSpec(
1338
1338
  id="convo",
1339
1339
  title="Convolution / Deconvolution",
1340
1340
  group="Blur & Sharpen",
1341
- import_path="pro.convo_preset",
1341
+ import_path="setiastro.saspro.convo_preset",
1342
1342
  callable_name="run_convo_via_preset",
1343
1343
  aliases=[
1344
1344
  "convolution",
@@ -1433,7 +1433,7 @@ register(CommandSpec(
1433
1433
  id="cosmic_clarity",
1434
1434
  title="Cosmic Clarity",
1435
1435
  group="AI",
1436
- import_path="pro.cosmicclarity_preset",
1436
+ import_path="setiastro.saspro.cosmicclarity_preset",
1437
1437
  callable_name="run_cosmicclarity_via_preset",
1438
1438
  presets=[
1439
1439
  PresetSpec("mode", "enum", default="sharpen",
@@ -1484,7 +1484,7 @@ register(CommandSpec(
1484
1484
  id="debayer",
1485
1485
  title="Debayer",
1486
1486
  group="Color / CFA",
1487
- import_path="pro.debayer",
1487
+ import_path="setiastro.saspro.debayer",
1488
1488
  callable_name="run_debayer_via_preset",
1489
1489
  presets=[
1490
1490
  PresetSpec(
@@ -1506,7 +1506,7 @@ register(CommandSpec(
1506
1506
  id="linear_fit",
1507
1507
  title="Linear Fit",
1508
1508
  group="Calibration",
1509
- import_path="pro.linear_fit",
1509
+ import_path="setiastro.saspro.linear_fit",
1510
1510
  callable_name="run_linear_fit_via_preset",
1511
1511
  presets=[
1512
1512
  PresetSpec(
@@ -1527,7 +1527,7 @@ register(CommandSpec(
1527
1527
  id="morphology",
1528
1528
  title="Morphology",
1529
1529
  group="Masks & Morphology",
1530
- import_path="pro.morphology",
1530
+ import_path="setiastro.saspro.morphology",
1531
1531
  callable_name="apply_morphology_to_doc",
1532
1532
  presets=[
1533
1533
  PresetSpec(
@@ -1556,7 +1556,7 @@ register(CommandSpec(
1556
1556
  id="remove_stars",
1557
1557
  title="Remove Stars",
1558
1558
  group="Star Tools",
1559
- import_path="pro.remove_stars_preset",
1559
+ import_path="setiastro.saspro.remove_stars_preset",
1560
1560
  callable_name="run_remove_stars_via_preset",
1561
1561
  replay_apply_name="apply_remove_stars_to_doc",
1562
1562
  presets=[
@@ -15,6 +15,11 @@ from PyQt6.QtWidgets import (
15
15
  QLineEdit, QToolButton, QCheckBox, QTextEdit
16
16
  )
17
17
 
18
+ from typing import TYPE_CHECKING
19
+
20
+ if TYPE_CHECKING:
21
+ from setiastro.saspro.ops.scripts import ScriptEntry
22
+
18
23
  # -----------------------------------------------------------------------------
19
24
  # Code editor with line numbers (QPlainTextEdit subclass)
20
25
  # -----------------------------------------------------------------------------
@@ -294,6 +294,125 @@ class ScriptContext:
294
294
  # ✅ Normal run: let DocManager decide (ROI preview vs full)
295
295
  dm.update_active_document(img, metadata={}, step_name=step_name)
296
296
 
297
+ def _find_subwindow_for_doc(self, base_doc):
298
+ """Return (sw, widget) for the first subwindow showing base_doc."""
299
+ for sw, w in self._iter_open_subwindows():
300
+ d = self._base_doc_for_widget(w)
301
+ if d is base_doc:
302
+ return sw, w
303
+ return None, None
304
+
305
+ def rename_active_view(self, new_title: str) -> bool:
306
+ """Rename only the active MDI view title (this window)."""
307
+ w = self.active_view()
308
+ if w is None:
309
+ return False
310
+
311
+ t = (new_title or "").strip()
312
+ if not t:
313
+ return False
314
+
315
+ try:
316
+ # ImageSubWindow convention
317
+ setattr(w, "_view_title_override", t)
318
+ if hasattr(w, "_sync_host_title"):
319
+ w._sync_host_title()
320
+ return True
321
+ except Exception:
322
+ return False
323
+
324
+ def rename_active_document(self, new_name: str) -> bool:
325
+ """Rename the underlying document display name (affects explorer + other views)."""
326
+ doc = self.active_document()
327
+ if doc is None:
328
+ return False
329
+
330
+ n = (new_name or "").strip()
331
+ if not n:
332
+ return False
333
+
334
+ try:
335
+ old = ""
336
+ try:
337
+ old = str(doc.display_name() or "")
338
+ except Exception:
339
+ old = str(getattr(doc, "metadata", {}).get("display_name", "") or "")
340
+
341
+ doc.metadata["display_name"] = n
342
+ try:
343
+ doc.changed.emit()
344
+ except Exception:
345
+ pass
346
+
347
+ # If the active view had an override equal to the old doc name, drop it (matches your UI behavior)
348
+ w = self.active_view()
349
+ if w is not None:
350
+ try:
351
+ if getattr(w, "_view_title_override", None) == old:
352
+ setattr(w, "_view_title_override", None)
353
+ except Exception:
354
+ pass
355
+ try:
356
+ if hasattr(w, "_sync_host_title"):
357
+ w._sync_host_title()
358
+ except Exception:
359
+ pass
360
+
361
+ return True
362
+ except Exception:
363
+ return False
364
+
365
+ def rename_view(self, view_name_or_uid: str, new_title: str) -> bool:
366
+ """Rename a specific *view/window* by name/title/uid (first match)."""
367
+ doc = self.get_document(view_name_or_uid)
368
+ if doc is None:
369
+ return False
370
+
371
+ sw, w = self._find_subwindow_for_doc(doc)
372
+ if w is None:
373
+ return False
374
+
375
+ t = (new_title or "").strip()
376
+ if not t:
377
+ return False
378
+
379
+ try:
380
+ setattr(w, "_view_title_override", t)
381
+ if hasattr(w, "_sync_host_title"):
382
+ w._sync_host_title()
383
+ return True
384
+ except Exception:
385
+ return False
386
+
387
+ def rename_document(self, view_name_or_uid: str, new_name: str) -> bool:
388
+ """Rename a specific *document* by view name/title/uid."""
389
+ doc = self.get_document(view_name_or_uid)
390
+ if doc is None:
391
+ return False
392
+ n = (new_name or "").strip()
393
+ if not n:
394
+ return False
395
+
396
+ try:
397
+ doc.metadata["display_name"] = n
398
+ try:
399
+ doc.changed.emit()
400
+ except Exception:
401
+ pass
402
+
403
+ # resync any window currently showing it
404
+ sw, w = self._find_subwindow_for_doc(doc)
405
+ if w is not None and hasattr(w, "_sync_host_title"):
406
+ try:
407
+ w._sync_host_title()
408
+ except Exception:
409
+ pass
410
+
411
+ return True
412
+ except Exception:
413
+ return False
414
+
415
+
297
416
  # ---- convenience wrappers into main window ----
298
417
  def run_command(self, command_id: str, preset=None, **kwargs):
299
418
  return _run_command(self, command_id, preset, **kwargs)
@@ -164,7 +164,7 @@ class RemoveGreenDialog(QDialog):
164
164
 
165
165
  def _build_ui(self):
166
166
  lay = QVBoxLayout(self)
167
- lay.addWidget(QLabel(self.tr("Select the amount to remove green noise:")))
167
+ lay.addWidget(QLabel(self.tr("Select the amount to remove green:")))
168
168
 
169
169
  # amount
170
170
  self.slider = QSlider(Qt.Orientation.Horizontal)
@@ -244,6 +244,7 @@ class Icons:
244
244
  STACKING = property(lambda self: _resource_path('stacking.png'))
245
245
  LIVE_STACKING = property(lambda self: _resource_path('livestacking.png'))
246
246
  IMAGE_COMBINE = property(lambda self: _resource_path('imagecombine.png'))
247
+ PLANETARY_STACKER = property(lambda self: _resource_path('planetarystacker.png'))
247
248
 
248
249
  # Moon phase (WIMS)
249
250
  MOON_NEW = property(lambda self: _resource_path('new_moon.png'))
@@ -301,6 +302,7 @@ class Icons:
301
302
  COLOR_WHEEL = property(lambda self: _resource_path('colorwheel.png'))
302
303
  SELECTIVE_COLOR = property(lambda self: _resource_path('selectivecolor.png'))
303
304
  NB_TO_RGB = property(lambda self: _resource_path('nbtorgb.png'))
305
+ NARROWBANDNORMALIZATION = property(lambda self: _resource_path('narrowbandnormalization.png'))
304
306
 
305
307
  # Stretching
306
308
  STAT_STRETCH = property(lambda self: _resource_path('statstretch.png'))
@@ -531,6 +533,7 @@ def _init_legacy_paths():
531
533
  'collage_path': get_icon_path('collage.png'),
532
534
  'annotated_path': get_icon_path('annotated.png'),
533
535
  'colorwheel_path': get_icon_path('colorwheel.png'),
536
+ 'narrowbandnormalization_path': get_icon_path('narrowbandnormalization.png'),
534
537
  'font_path': get_icon_path('font.png'),
535
538
  'csv_icon_path': get_icon_path('cvs.png'),
536
539
  'spinner_path': get_data_path('spinner.gif'),
@@ -540,6 +543,7 @@ def _init_legacy_paths():
540
543
  'debayer_path': get_icon_path('debayer.png'),
541
544
  'aberration_path': get_icon_path('aberration.png'),
542
545
  'functionbundles_path': get_icon_path('functionbundle.png'),
546
+ 'planetarystacker_path': get_icon_path('planetarystacker.png'),
543
547
  'viewbundles_path': get_icon_path('viewbundle.png'),
544
548
  'selectivecolor_path': get_icon_path('selectivecolor.png'),
545
549
  'rgbalign_path': get_icon_path('rgbalign.png'),
@@ -0,0 +1,68 @@
1
+ # src/setiastro/saspro/ser_stack_config.py
2
+ from __future__ import annotations
3
+ from dataclasses import dataclass
4
+ from typing import Optional, Tuple, Literal, Union, Sequence
5
+
6
+ from setiastro.saspro.imageops.serloader import PlanetaryFrameSource
7
+
8
+ TrackMode = Literal["off", "planetary", "surface"]
9
+ PlanetarySource = Union[str, Sequence[str], PlanetaryFrameSource]
10
+
11
+ @dataclass
12
+ class SERStackConfig:
13
+ source: PlanetarySource
14
+ roi: Optional[Tuple[int, int, int, int]] = None
15
+ track_mode: TrackMode = "planetary"
16
+ surface_anchor: Optional[Tuple[int, int, int, int]] = None
17
+ keep_percent: float = 20.0
18
+
19
+ # AP / alignment
20
+ ap_size: int = 64
21
+ ap_spacing: int = 48
22
+ ap_min_mean: float = 0.03
23
+ ap_multiscale: bool = False
24
+ ssd_refine_bruteforce: bool = False
25
+
26
+ # ✅ Drizzle
27
+ drizzle_scale: float = 1.0 # 1.0 = off, 1.5, 2.0
28
+ drizzle_pixfrac: float = 0.80 # "drop shrink" in output pixels (roughly)
29
+ drizzle_kernel: str = "gaussian" # "square" | "circle" | "gaussian"
30
+ drizzle_sigma: float = 0.0 # only used for gaussian; 0 => auto from pixfrac
31
+
32
+ def __init__(self, source: PlanetarySource, **kwargs):
33
+ # Allow deprecated/ignored kwargs without crashing
34
+ kwargs.pop("multipoint", None) # accept but ignore
35
+
36
+ self.source = source
37
+ self.roi = kwargs.pop("roi", None)
38
+ self.track_mode = kwargs.pop("track_mode", "planetary")
39
+ self.surface_anchor = kwargs.pop("surface_anchor", None)
40
+ self.keep_percent = float(kwargs.pop("keep_percent", 20.0))
41
+
42
+ self.ap_size = int(kwargs.pop("ap_size", 64))
43
+ self.ap_spacing = int(kwargs.pop("ap_spacing", 48))
44
+ self.ap_min_mean = float(kwargs.pop("ap_min_mean", 0.03))
45
+ self.ap_multiscale = bool(kwargs.pop("ap_multiscale", False))
46
+ self.ssd_refine_bruteforce = bool(kwargs.pop("ssd_refine_bruteforce", False))
47
+
48
+ # ✅ NEW: Drizzle params
49
+ self.drizzle_scale = float(kwargs.pop("drizzle_scale", 1.0))
50
+ if self.drizzle_scale not in (1.0, 1.5, 2.0):
51
+ self.drizzle_scale = 1.0
52
+
53
+ self.drizzle_pixfrac = float(kwargs.pop("drizzle_pixfrac", 0.80))
54
+ self.drizzle_kernel = str(kwargs.pop("drizzle_kernel", "gaussian")).strip().lower()
55
+ self.drizzle_sigma = float(kwargs.pop("drizzle_sigma", 0.0))
56
+
57
+ # sanitize a bit
58
+ if self.drizzle_scale < 1.0:
59
+ self.drizzle_scale = 1.0
60
+ if self.drizzle_pixfrac <= 0.0:
61
+ self.drizzle_pixfrac = 0.01
62
+ if self.drizzle_kernel not in ("square", "circle", "gaussian"):
63
+ self.drizzle_kernel = "gaussian"
64
+ if self.drizzle_sigma < 0.0:
65
+ self.drizzle_sigma = 0.0
66
+
67
+ if kwargs:
68
+ raise TypeError(f"Unexpected config keys: {sorted(kwargs.keys())}")