setiastrosuitepro 1.6.2__py3-none-any.whl → 1.6.12__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/acv_icon.png +0 -0
- setiastro/images/colorwheel.svg +97 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/first_quarter.png +0 -0
- setiastro/images/full_moon.png +0 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/last_quarter.png +0 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/new_moon.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/rotatearbitrary.png +0 -0
- setiastro/images/waning_crescent_1.png +0 -0
- setiastro/images/waning_crescent_2.png +0 -0
- setiastro/images/waning_crescent_3.png +0 -0
- setiastro/images/waning_crescent_4.png +0 -0
- setiastro/images/waning_crescent_5.png +0 -0
- setiastro/images/waning_gibbous_1.png +0 -0
- setiastro/images/waning_gibbous_2.png +0 -0
- setiastro/images/waning_gibbous_3.png +0 -0
- setiastro/images/waning_gibbous_4.png +0 -0
- setiastro/images/waning_gibbous_5.png +0 -0
- setiastro/images/waxing_crescent_1.png +0 -0
- setiastro/images/waxing_crescent_2.png +0 -0
- setiastro/images/waxing_crescent_3.png +0 -0
- setiastro/images/waxing_crescent_4.png +0 -0
- setiastro/images/waxing_crescent_5.png +0 -0
- setiastro/images/waxing_gibbous_1.png +0 -0
- setiastro/images/waxing_gibbous_2.png +0 -0
- setiastro/images/waxing_gibbous_3.png +0 -0
- setiastro/images/waxing_gibbous_4.png +0 -0
- setiastro/images/waxing_gibbous_5.png +0 -0
- setiastro/qml/ResourceMonitor.qml +84 -82
- setiastro/saspro/__main__.py +20 -1
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/abe.py +37 -4
- setiastro/saspro/aberration_ai.py +237 -21
- setiastro/saspro/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +33 -6
- setiastro/saspro/backgroundneutral.py +114 -37
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +548 -275
- setiastro/saspro/clahe.py +4 -1
- setiastro/saspro/continuum_subtract.py +4 -1
- setiastro/saspro/convo.py +13 -7
- setiastro/saspro/cosmicclarity.py +129 -18
- setiastro/saspro/crop_dialog_pro.py +134 -8
- setiastro/saspro/curve_editor_pro.py +109 -42
- setiastro/saspro/doc_manager.py +246 -16
- setiastro/saspro/exoplanet_detector.py +120 -28
- setiastro/saspro/frequency_separation.py +1158 -204
- setiastro/saspro/function_bundle.py +16 -16
- setiastro/saspro/ghs_dialog_pro.py +81 -16
- setiastro/saspro/graxpert.py +1 -0
- setiastro/saspro/gui/main_window.py +519 -289
- setiastro/saspro/gui/mixins/dock_mixin.py +276 -42
- setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
- setiastro/saspro/gui/mixins/menu_mixin.py +28 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +416 -27
- setiastro/saspro/gui/mixins/update_mixin.py +138 -36
- setiastro/saspro/gui/mixins/view_mixin.py +42 -0
- setiastro/saspro/halobgon.py +4 -0
- setiastro/saspro/histogram.py +5 -1
- setiastro/saspro/image_combine.py +4 -0
- setiastro/saspro/image_peeker_pro.py +4 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
- setiastro/saspro/imageops/stretch.py +582 -62
- setiastro/saspro/isophote.py +4 -0
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/image_manager.py +154 -20
- setiastro/saspro/legacy/numba_utils.py +67 -47
- setiastro/saspro/legacy/xisf.py +240 -98
- setiastro/saspro/live_stacking.py +180 -79
- setiastro/saspro/luminancerecombine.py +228 -27
- setiastro/saspro/mask_creation.py +174 -15
- setiastro/saspro/mfdeconv.py +113 -35
- setiastro/saspro/mfdeconvcudnn.py +119 -70
- setiastro/saspro/mfdeconvsport.py +112 -35
- setiastro/saspro/morphology.py +4 -0
- setiastro/saspro/multiscale_decomp.py +748 -255
- setiastro/saspro/numba_utils.py +72 -57
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/ops/script_editor.py +10 -2
- setiastro/saspro/ops/scripts.py +122 -0
- setiastro/saspro/perfect_palette_picker.py +37 -3
- setiastro/saspro/plate_solver.py +84 -49
- setiastro/saspro/psf_viewer.py +119 -37
- setiastro/saspro/remove_stars_preset.py +55 -13
- setiastro/saspro/resources.py +97 -11
- setiastro/saspro/rgbalign.py +4 -0
- setiastro/saspro/selective_color.py +83 -21
- setiastro/saspro/sfcc.py +364 -152
- setiastro/saspro/shortcuts.py +253 -49
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1610 -574
- setiastro/saspro/star_alignment.py +522 -453
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +743 -128
- setiastro/saspro/status_log_dock.py +1 -1
- setiastro/saspro/subwindow.py +786 -360
- setiastro/saspro/supernovaasteroidhunter.py +1 -1
- setiastro/saspro/swap_manager.py +77 -42
- setiastro/saspro/translations/all_source_strings.json +1588 -516
- setiastro/saspro/translations/ar_translations.py +915 -684
- setiastro/saspro/translations/de_translations.py +442 -463
- setiastro/saspro/translations/es_translations.py +277 -47
- setiastro/saspro/translations/fr_translations.py +279 -47
- setiastro/saspro/translations/hi_translations.py +253 -21
- setiastro/saspro/translations/integrate_translations.py +3 -2
- setiastro/saspro/translations/it_translations.py +1211 -161
- setiastro/saspro/translations/ja_translations.py +3340 -3107
- setiastro/saspro/translations/pt_translations.py +3315 -3337
- setiastro/saspro/translations/ru_translations.py +351 -117
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +15902 -138
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +14428 -133
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +11503 -7821
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +11168 -7812
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +14733 -135
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +14347 -7821
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +14860 -137
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +14904 -137
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +11766 -168
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +15115 -135
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +11206 -6729
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +10581 -7812
- setiastro/saspro/translations/sw_translations.py +282 -56
- setiastro/saspro/translations/uk_translations.py +264 -35
- setiastro/saspro/translations/zh_translations.py +282 -47
- setiastro/saspro/view_bundle.py +17 -17
- setiastro/saspro/wavescale_hdr.py +4 -1
- setiastro/saspro/wavescalede.py +4 -1
- setiastro/saspro/whitebalance.py +84 -12
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/minigame/game.js +11 -6
- setiastro/saspro/widgets/resource_monitor.py +133 -57
- setiastro/saspro/widgets/spinboxes.py +28 -13
- setiastro/saspro/wimi.py +92 -721
- setiastro/saspro/wims.py +46 -36
- setiastro/saspro/window_shelf.py +2 -2
- setiastro/saspro/xisf.py +101 -11
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/METADATA +8 -7
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/RECORD +162 -128
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/license.txt +0 -0
setiastro/saspro/wimi.py
CHANGED
|
@@ -2596,12 +2596,12 @@ class WIMIDialog(QDialog):
|
|
|
2596
2596
|
button_layout = QHBoxLayout()
|
|
2597
2597
|
|
|
2598
2598
|
# Load button
|
|
2599
|
-
self.load_button = QPushButton("Load Image File")
|
|
2599
|
+
self.load_button = QPushButton(self.tr("Load Image File"))
|
|
2600
2600
|
self.load_button.setIcon(QApplication.style().standardIcon(QStyle.StandardPixmap.SP_FileDialogStart))
|
|
2601
2601
|
self.load_button.clicked.connect(self.open_image)
|
|
2602
2602
|
|
|
2603
2603
|
self.load_from_view_btn = QToolButton()
|
|
2604
|
-
self.load_from_view_btn.setText("Load from View")
|
|
2604
|
+
self.load_from_view_btn.setText(self.tr("Load from View"))
|
|
2605
2605
|
self.load_from_view_btn.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
|
|
2606
2606
|
self.load_from_view_btn.setIcon(QApplication.style().standardIcon(QStyle.StandardPixmap.SP_DirOpenIcon))
|
|
2607
2607
|
self.load_from_view_menu = QMenu(self)
|
|
@@ -2611,7 +2611,7 @@ class WIMIDialog(QDialog):
|
|
|
2611
2611
|
button_layout.addWidget(self.load_from_view_btn)
|
|
2612
2612
|
|
|
2613
2613
|
# AutoStretch button
|
|
2614
|
-
self.auto_stretch_button = QPushButton("AutoStretch")
|
|
2614
|
+
self.auto_stretch_button = QPushButton(self.tr("AutoStretch"))
|
|
2615
2615
|
self.auto_stretch_button.clicked.connect(self.toggle_autostretch)
|
|
2616
2616
|
|
|
2617
2617
|
# Add both buttons to the horizontal layout
|
|
@@ -2622,7 +2622,7 @@ class WIMIDialog(QDialog):
|
|
|
2622
2622
|
left_panel.addLayout(button_layout)
|
|
2623
2623
|
|
|
2624
2624
|
# Create the instruction QLabel for search region
|
|
2625
|
-
search_region_instruction_label = QLabel("Shift+Click to define a search region")
|
|
2625
|
+
search_region_instruction_label = QLabel(self.tr("Shift+Click to define a search region"))
|
|
2626
2626
|
search_region_instruction_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
2627
2627
|
search_region_instruction_label.setStyleSheet("font-size: 15px; color: gray;")
|
|
2628
2628
|
|
|
@@ -2632,12 +2632,12 @@ class WIMIDialog(QDialog):
|
|
|
2632
2632
|
|
|
2633
2633
|
|
|
2634
2634
|
# Query Simbad button
|
|
2635
|
-
self.query_button = QPushButton("Query Simbad")
|
|
2635
|
+
self.query_button = QPushButton(self.tr("Query Simbad"))
|
|
2636
2636
|
self.query_button.setIcon(QApplication.style().standardIcon(QStyle.StandardPixmap.SP_DialogApplyButton))
|
|
2637
2637
|
left_panel.addWidget(self.query_button)
|
|
2638
2638
|
self.query_button.clicked.connect(lambda: self.query_simbad(self.get_defined_radius()))
|
|
2639
2639
|
|
|
2640
|
-
self.legend_button = QPushButton("Legend")
|
|
2640
|
+
self.legend_button = QPushButton(self.tr("Legend"))
|
|
2641
2641
|
self.legend_button.clicked.connect(self.show_legend)
|
|
2642
2642
|
left_panel.addWidget(self.legend_button)
|
|
2643
2643
|
|
|
@@ -2645,12 +2645,12 @@ class WIMIDialog(QDialog):
|
|
|
2645
2645
|
show_clear_layout = QHBoxLayout()
|
|
2646
2646
|
|
|
2647
2647
|
# Create the Show Object Names checkbox
|
|
2648
|
-
self.show_names_checkbox = QCheckBox("Show Object Names")
|
|
2648
|
+
self.show_names_checkbox = QCheckBox(self.tr("Show Object Names"))
|
|
2649
2649
|
self.show_names_checkbox.stateChanged.connect(self.toggle_object_names) # Connect to a function to toggle names
|
|
2650
2650
|
show_clear_layout.addWidget(self.show_names_checkbox)
|
|
2651
2651
|
|
|
2652
2652
|
# Create the Clear Results button
|
|
2653
|
-
self.clear_results_button = QPushButton("Clear Results")
|
|
2653
|
+
self.clear_results_button = QPushButton(self.tr("Clear Results"))
|
|
2654
2654
|
self.clear_results_button.setIcon(QApplication.style().standardIcon(QStyle.StandardPixmap.SP_DialogCloseButton))
|
|
2655
2655
|
self.clear_results_button.clicked.connect(self.clear_search_results) # Connect to a function to clear results
|
|
2656
2656
|
show_clear_layout.addWidget(self.clear_results_button)
|
|
@@ -2662,7 +2662,7 @@ class WIMIDialog(QDialog):
|
|
|
2662
2662
|
button_layout = QHBoxLayout()
|
|
2663
2663
|
|
|
2664
2664
|
# Show Visible Objects Only button
|
|
2665
|
-
self.toggle_visible_objects_button = QPushButton("Show Visible Objects Only")
|
|
2665
|
+
self.toggle_visible_objects_button = QPushButton(self.tr("Show Visible Objects Only"))
|
|
2666
2666
|
self.toggle_visible_objects_button.setCheckable(True) # Toggle button state
|
|
2667
2667
|
self.toggle_visible_objects_button.setIcon(QIcon(eye_icon_path))
|
|
2668
2668
|
self.toggle_visible_objects_button.clicked.connect(self.filter_visible_objects)
|
|
@@ -2670,7 +2670,7 @@ class WIMIDialog(QDialog):
|
|
|
2670
2670
|
button_layout.addWidget(self.toggle_visible_objects_button)
|
|
2671
2671
|
|
|
2672
2672
|
# Save CSV button
|
|
2673
|
-
self.save_csv_button = QPushButton("Save CSV")
|
|
2673
|
+
self.save_csv_button = QPushButton(self.tr("Save CSV"))
|
|
2674
2674
|
self.save_csv_button.setIcon(QIcon(csv_icon_path))
|
|
2675
2675
|
self.save_csv_button.clicked.connect(self.save_results_as_csv)
|
|
2676
2676
|
button_layout.addWidget(self.save_csv_button)
|
|
@@ -2679,7 +2679,7 @@ class WIMIDialog(QDialog):
|
|
|
2679
2679
|
left_panel.addLayout(button_layout)
|
|
2680
2680
|
|
|
2681
2681
|
# Advanced Search Button
|
|
2682
|
-
self.advanced_search_button = QPushButton("Advanced Search")
|
|
2682
|
+
self.advanced_search_button = QPushButton(self.tr("Advanced Search"))
|
|
2683
2683
|
self.advanced_search_button.setIcon(QApplication.style().standardIcon(QStyle.StandardPixmap.SP_FileDialogDetailedView))
|
|
2684
2684
|
self.advanced_search_button.setCheckable(True)
|
|
2685
2685
|
self.advanced_search_button.clicked.connect(self.toggle_advanced_search)
|
|
@@ -2693,14 +2693,14 @@ class WIMIDialog(QDialog):
|
|
|
2693
2693
|
self.advanced_search_panel_widget.setVisible(False) # Hide initially
|
|
2694
2694
|
|
|
2695
2695
|
# Status label
|
|
2696
|
-
self.status_label = QLabel("Status: Ready")
|
|
2696
|
+
self.status_label = QLabel(self.tr("Status: Ready"))
|
|
2697
2697
|
left_panel.addWidget(self.status_label)
|
|
2698
2698
|
|
|
2699
2699
|
# Create a horizontal layout
|
|
2700
2700
|
button_layout = QHBoxLayout()
|
|
2701
2701
|
|
|
2702
2702
|
# Copy RA/Dec to Clipboard button
|
|
2703
|
-
self.copy_button = QPushButton("Copy RA/Dec to Clipboard", self)
|
|
2703
|
+
self.copy_button = QPushButton(self.tr("Copy RA/Dec to Clipboard"), self)
|
|
2704
2704
|
self.copy_button.setIcon(QApplication.style().standardIcon(QStyle.StandardPixmap.SP_CommandLink))
|
|
2705
2705
|
self.copy_button.clicked.connect(self.copy_ra_dec_to_clipboard)
|
|
2706
2706
|
button_layout.addWidget(self.copy_button)
|
|
@@ -2715,23 +2715,23 @@ class WIMIDialog(QDialog):
|
|
|
2715
2715
|
left_panel.addLayout(button_layout)
|
|
2716
2716
|
|
|
2717
2717
|
# Save Plate Solved Fits Button
|
|
2718
|
-
self.save_plate_solved_button = QPushButton("Save Plate Solved Fits")
|
|
2718
|
+
self.save_plate_solved_button = QPushButton(self.tr("Save Plate Solved Fits"))
|
|
2719
2719
|
self.save_plate_solved_button.setIcon(QIcon(disk_icon_path))
|
|
2720
2720
|
self.save_plate_solved_button.clicked.connect(self.save_plate_solved_fits)
|
|
2721
2721
|
left_panel.addWidget(self.save_plate_solved_button)
|
|
2722
2722
|
|
|
2723
2723
|
# RA/Dec Labels
|
|
2724
2724
|
ra_dec_layout = QHBoxLayout()
|
|
2725
|
-
self.ra_label = QLabel("RA: N/A")
|
|
2726
|
-
self.dec_label = QLabel("Dec: N/A")
|
|
2727
|
-
self.orientation_label = QLabel("Orientation: N/A°")
|
|
2725
|
+
self.ra_label = QLabel(self.tr("RA: N/A"))
|
|
2726
|
+
self.dec_label = QLabel(self.tr("Dec: N/A"))
|
|
2727
|
+
self.orientation_label = QLabel(self.tr("Orientation: N/A°"))
|
|
2728
2728
|
ra_dec_layout.addWidget(self.ra_label)
|
|
2729
2729
|
ra_dec_layout.addWidget(self.dec_label)
|
|
2730
2730
|
ra_dec_layout.addWidget(self.orientation_label)
|
|
2731
2731
|
left_panel.addLayout(ra_dec_layout)
|
|
2732
2732
|
|
|
2733
2733
|
# Mini Preview
|
|
2734
|
-
self.mini_preview = QLabel("Mini Preview")
|
|
2734
|
+
self.mini_preview = QLabel(self.tr("Mini Preview"))
|
|
2735
2735
|
self.mini_preview.setMaximumSize(300, 300)
|
|
2736
2736
|
self.mini_preview.mousePressEvent = self.on_mini_preview_press
|
|
2737
2737
|
self.mini_preview.mouseMoveEvent = self.on_mini_preview_drag
|
|
@@ -2746,9 +2746,9 @@ class WIMIDialog(QDialog):
|
|
|
2746
2746
|
|
|
2747
2747
|
# Zoom buttons above the main preview
|
|
2748
2748
|
zoom_controls_layout = QHBoxLayout()
|
|
2749
|
-
self.zoom_in_button = QPushButton("Zoom In")
|
|
2749
|
+
self.zoom_in_button = QPushButton(self.tr("Zoom In"))
|
|
2750
2750
|
self.zoom_in_button.clicked.connect(self.zoom_in)
|
|
2751
|
-
self.zoom_out_button = QPushButton("Zoom Out")
|
|
2751
|
+
self.zoom_out_button = QPushButton(self.tr("Zoom Out"))
|
|
2752
2752
|
self.zoom_out_button.clicked.connect(self.zoom_out)
|
|
2753
2753
|
zoom_controls_layout.addWidget(self.zoom_in_button)
|
|
2754
2754
|
zoom_controls_layout.addWidget(self.zoom_out_button)
|
|
@@ -2783,28 +2783,28 @@ class WIMIDialog(QDialog):
|
|
|
2783
2783
|
save_buttons_layout = QHBoxLayout()
|
|
2784
2784
|
|
|
2785
2785
|
# Button to toggle annotation tools section
|
|
2786
|
-
self.show_annotations_button = QPushButton("Show Annotation Tools")
|
|
2786
|
+
self.show_annotations_button = QPushButton(self.tr("Show Annotation Tools"))
|
|
2787
2787
|
self.show_annotations_button.setIcon(QApplication.style().standardIcon(QStyle.StandardPixmap.SP_DialogResetButton))
|
|
2788
2788
|
self.show_annotations_button.clicked.connect(self.toggle_annotation_tools)
|
|
2789
2789
|
save_buttons_layout.addWidget(self.show_annotations_button)
|
|
2790
2790
|
|
|
2791
|
-
self.save_annotated_button = QPushButton("Save Annotated Image")
|
|
2791
|
+
self.save_annotated_button = QPushButton(self.tr("Save Annotated Image"))
|
|
2792
2792
|
self.save_annotated_button.setIcon(QIcon(annotated_path))
|
|
2793
2793
|
self.save_annotated_button.clicked.connect(self.save_annotated_image)
|
|
2794
2794
|
save_buttons_layout.addWidget(self.save_annotated_button)
|
|
2795
2795
|
|
|
2796
|
-
self.save_collage_button = QPushButton("Save Collage of Objects")
|
|
2796
|
+
self.save_collage_button = QPushButton(self.tr("Save Collage of Objects"))
|
|
2797
2797
|
self.save_collage_button.setIcon(QIcon(collage_path))
|
|
2798
2798
|
self.save_collage_button.clicked.connect(self.save_collage_of_objects)
|
|
2799
2799
|
save_buttons_layout.addWidget(self.save_collage_button)
|
|
2800
2800
|
|
|
2801
2801
|
# New 3D View Button
|
|
2802
|
-
self.show_3d_view_button = QPushButton("3D Distance Model")
|
|
2802
|
+
self.show_3d_view_button = QPushButton(self.tr("3D Distance Model"))
|
|
2803
2803
|
self.show_3d_view_button.clicked.connect(self.show_3d_model_view)
|
|
2804
2804
|
self.show_3d_view_button.setIcon( QApplication.style().standardIcon(QStyle.StandardPixmap.SP_TitleBarNormalButton))
|
|
2805
2805
|
save_buttons_layout.addWidget(self.show_3d_view_button)
|
|
2806
2806
|
|
|
2807
|
-
self.show_hr_button = QPushButton("H-R Diagram")
|
|
2807
|
+
self.show_hr_button = QPushButton(self.tr("H-R Diagram"))
|
|
2808
2808
|
# Optionally give it an icon:
|
|
2809
2809
|
self.show_hr_button.setIcon(QApplication.style().standardIcon(
|
|
2810
2810
|
QStyle.StandardPixmap.SP_DesktopIcon))
|
|
@@ -2821,10 +2821,10 @@ class WIMIDialog(QDialog):
|
|
|
2821
2821
|
label_layout = QHBoxLayout()
|
|
2822
2822
|
|
|
2823
2823
|
# Create the label to display the count of objects
|
|
2824
|
-
self.object_count_label = QLabel("Objects Found: 0")
|
|
2824
|
+
self.object_count_label = QLabel(self.tr("Objects Found: 0"))
|
|
2825
2825
|
|
|
2826
2826
|
# Create the label with instructions
|
|
2827
|
-
self.instructions_label = QLabel("Right Click a Row for More Options")
|
|
2827
|
+
self.instructions_label = QLabel(self.tr("Right Click a Row for More Options"))
|
|
2828
2828
|
|
|
2829
2829
|
# Add both labels to the horizontal layout
|
|
2830
2830
|
label_layout.addWidget(self.object_count_label)
|
|
@@ -2850,61 +2850,61 @@ class WIMIDialog(QDialog):
|
|
|
2850
2850
|
self.annotation_tools_section = QWidget()
|
|
2851
2851
|
annotation_tools_layout = QGridLayout(self.annotation_tools_section)
|
|
2852
2852
|
|
|
2853
|
-
annotation_instruction_label = QLabel("Ctrl+Click to add items, Alt+Click to measure distance")
|
|
2853
|
+
annotation_instruction_label = QLabel(self.tr("Ctrl+Click to add items, Alt+Click to measure distance"))
|
|
2854
2854
|
annotation_instruction_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
2855
2855
|
annotation_instruction_label.setStyleSheet("font-size: 10px; color: gray;")
|
|
2856
2856
|
|
|
2857
|
-
self.draw_ellipse_button = QPushButton("Draw Ellipse")
|
|
2857
|
+
self.draw_ellipse_button = QPushButton(self.tr("Draw Ellipse"))
|
|
2858
2858
|
self.draw_ellipse_button.tool_name = "Ellipse"
|
|
2859
2859
|
self.draw_ellipse_button.clicked.connect(lambda: self.set_tool("Ellipse"))
|
|
2860
2860
|
self.annotation_buttons.append(self.draw_ellipse_button)
|
|
2861
2861
|
|
|
2862
|
-
self.freehand_button = QPushButton("Freehand (Lasso)")
|
|
2862
|
+
self.freehand_button = QPushButton(self.tr("Freehand (Lasso)"))
|
|
2863
2863
|
self.freehand_button.tool_name = "Freehand"
|
|
2864
2864
|
self.freehand_button.clicked.connect(lambda: self.set_tool("Freehand"))
|
|
2865
2865
|
self.annotation_buttons.append(self.freehand_button)
|
|
2866
2866
|
|
|
2867
|
-
self.draw_rectangle_button = QPushButton("Draw Rectangle")
|
|
2867
|
+
self.draw_rectangle_button = QPushButton(self.tr("Draw Rectangle"))
|
|
2868
2868
|
self.draw_rectangle_button.tool_name = "Rectangle"
|
|
2869
2869
|
self.draw_rectangle_button.clicked.connect(lambda: self.set_tool("Rectangle"))
|
|
2870
2870
|
self.annotation_buttons.append(self.draw_rectangle_button)
|
|
2871
2871
|
|
|
2872
|
-
self.draw_arrow_button = QPushButton("Draw Arrow")
|
|
2872
|
+
self.draw_arrow_button = QPushButton(self.tr("Draw Arrow"))
|
|
2873
2873
|
self.draw_arrow_button.tool_name = "Arrow"
|
|
2874
2874
|
self.draw_arrow_button.clicked.connect(lambda: self.set_tool("Arrow"))
|
|
2875
2875
|
self.annotation_buttons.append(self.draw_arrow_button)
|
|
2876
2876
|
|
|
2877
|
-
self.place_compass_button = QPushButton("Place Celestial Compass")
|
|
2877
|
+
self.place_compass_button = QPushButton(self.tr("Place Celestial Compass"))
|
|
2878
2878
|
self.place_compass_button.tool_name = "Compass"
|
|
2879
2879
|
self.place_compass_button.clicked.connect(lambda: self.set_tool("Compass"))
|
|
2880
2880
|
self.annotation_buttons.append(self.place_compass_button)
|
|
2881
2881
|
|
|
2882
|
-
self.add_text_button = QPushButton("Add Text")
|
|
2882
|
+
self.add_text_button = QPushButton(self.tr("Add Text"))
|
|
2883
2883
|
self.add_text_button.tool_name = "Text"
|
|
2884
2884
|
self.add_text_button.clicked.connect(lambda: self.set_tool("Text"))
|
|
2885
2885
|
self.annotation_buttons.append(self.add_text_button)
|
|
2886
2886
|
|
|
2887
2887
|
# Add Color and Font buttons
|
|
2888
|
-
self.color_button = QPushButton("Select Color")
|
|
2888
|
+
self.color_button = QPushButton(self.tr("Select Color"))
|
|
2889
2889
|
self.color_button.setIcon(QIcon(colorwheel_path))
|
|
2890
2890
|
self.color_button.clicked.connect(self.select_color)
|
|
2891
2891
|
|
|
2892
|
-
self.font_button = QPushButton("Select Font")
|
|
2892
|
+
self.font_button = QPushButton(self.tr("Select Font"))
|
|
2893
2893
|
self.font_button.setIcon(QIcon(font_path))
|
|
2894
2894
|
self.font_button.clicked.connect(self.select_font)
|
|
2895
2895
|
|
|
2896
2896
|
# Undo button
|
|
2897
|
-
self.undo_button = QPushButton("Undo")
|
|
2897
|
+
self.undo_button = QPushButton(self.tr("Undo"))
|
|
2898
2898
|
self.undo_button.setIcon(QApplication.style().standardIcon(QStyle.StandardPixmap.SP_ArrowLeft)) # Left arrow icon for undo
|
|
2899
2899
|
self.undo_button.clicked.connect(self.main_preview.undo_annotation) # Connect to undo_annotation in CustomGraphicsView
|
|
2900
2900
|
|
|
2901
2901
|
# Clear Annotations button
|
|
2902
|
-
self.clear_annotations_button = QPushButton("Clear Annotations")
|
|
2902
|
+
self.clear_annotations_button = QPushButton(self.tr("Clear Annotations"))
|
|
2903
2903
|
self.clear_annotations_button.setIcon(QApplication.style().standardIcon(QStyle.StandardPixmap.SP_TrashIcon)) # Trash icon
|
|
2904
2904
|
self.clear_annotations_button.clicked.connect(self.main_preview.clear_annotations) # Connect to clear_annotations in CustomGraphicsView
|
|
2905
2905
|
|
|
2906
2906
|
# Delete Selected Object button
|
|
2907
|
-
self.delete_selected_object_button = QPushButton("Delete Selected
|
|
2907
|
+
self.delete_selected_object_button = QPushButton(self.tr("Delete Selected Items"))
|
|
2908
2908
|
self.delete_selected_object_button.setIcon(QApplication.style().standardIcon(QStyle.StandardPixmap.SP_DialogCloseButton)) # Trash icon
|
|
2909
2909
|
self.delete_selected_object_button.clicked.connect(self.main_preview.delete_selected_objects)
|
|
2910
2910
|
|
|
@@ -2928,12 +2928,12 @@ class WIMIDialog(QDialog):
|
|
|
2928
2928
|
right_panel.addWidget(self.annotation_tools_section)
|
|
2929
2929
|
|
|
2930
2930
|
# Advanced Search Panel
|
|
2931
|
-
self.advanced_param_label = QLabel("Advanced Search Parameters")
|
|
2931
|
+
self.advanced_param_label = QLabel(self.tr("Advanced Search Parameters"))
|
|
2932
2932
|
self.advanced_search_panel.addWidget(self.advanced_param_label)
|
|
2933
2933
|
|
|
2934
2934
|
# TreeWidget for object types
|
|
2935
2935
|
self.object_tree = QTreeWidget()
|
|
2936
|
-
self.object_tree.setHeaderLabels(["Object Type", "Description"])
|
|
2936
|
+
self.object_tree.setHeaderLabels([self.tr("Object Type"), self.tr("Description")])
|
|
2937
2937
|
self.object_tree.setColumnWidth(0, 150)
|
|
2938
2938
|
self.object_tree.setSortingEnabled(True)
|
|
2939
2939
|
|
|
@@ -2949,17 +2949,17 @@ class WIMIDialog(QDialog):
|
|
|
2949
2949
|
toggle_buttons_layout = QHBoxLayout()
|
|
2950
2950
|
|
|
2951
2951
|
# Toggle All
|
|
2952
|
-
self.toggle_all_button = QPushButton("Toggle All")
|
|
2952
|
+
self.toggle_all_button = QPushButton(self.tr("Toggle All"))
|
|
2953
2953
|
self.toggle_all_button.clicked.connect(self.toggle_all_items)
|
|
2954
2954
|
toggle_buttons_layout.addWidget(self.toggle_all_button)
|
|
2955
2955
|
|
|
2956
2956
|
# Save Custom List
|
|
2957
|
-
self.save_list_button = QPushButton("Save List…")
|
|
2957
|
+
self.save_list_button = QPushButton(self.tr("Save List…"))
|
|
2958
2958
|
self.save_list_button.clicked.connect(self.save_custom_list)
|
|
2959
2959
|
toggle_buttons_layout.addWidget(self.save_list_button)
|
|
2960
2960
|
|
|
2961
2961
|
# Load Custom List
|
|
2962
|
-
self.load_list_button = QPushButton("Load List…")
|
|
2962
|
+
self.load_list_button = QPushButton(self.tr("Load List…"))
|
|
2963
2963
|
self.load_list_button.clicked.connect(self.load_custom_list)
|
|
2964
2964
|
toggle_buttons_layout.addWidget(self.load_list_button)
|
|
2965
2965
|
|
|
@@ -2968,11 +2968,11 @@ class WIMIDialog(QDialog):
|
|
|
2968
2968
|
# Add Simbad Search buttons below the toggle buttons
|
|
2969
2969
|
search_button_layout = QHBoxLayout()
|
|
2970
2970
|
|
|
2971
|
-
self.simbad_defined_region_button = QPushButton("Search Defined Region")
|
|
2971
|
+
self.simbad_defined_region_button = QPushButton(self.tr("Search Defined Region"))
|
|
2972
2972
|
self.simbad_defined_region_button.clicked.connect(self.search_defined_region)
|
|
2973
2973
|
search_button_layout.addWidget(self.simbad_defined_region_button)
|
|
2974
2974
|
|
|
2975
|
-
self.simbad_entire_image_button = QPushButton("Search Entire Image")
|
|
2975
|
+
self.simbad_entire_image_button = QPushButton(self.tr("Search Entire Image"))
|
|
2976
2976
|
self.simbad_entire_image_button.clicked.connect(self.search_entire_image)
|
|
2977
2977
|
search_button_layout.addWidget(self.simbad_entire_image_button)
|
|
2978
2978
|
|
|
@@ -2981,17 +2981,17 @@ class WIMIDialog(QDialog):
|
|
|
2981
2981
|
# ─────────────────────────────
|
|
2982
2982
|
# Minor Planets / Comets block
|
|
2983
2983
|
# ─────────────────────────────
|
|
2984
|
-
self.minor_group = QGroupBox("Minor Planets / Comets")
|
|
2984
|
+
self.minor_group = QGroupBox(self.tr("Minor Planets / Comets"))
|
|
2985
2985
|
minor_layout = QGridLayout(self.minor_group)
|
|
2986
2986
|
|
|
2987
2987
|
# --- DB info + buttons ---
|
|
2988
|
-
self.minor_db_label = QLabel("Database: not downloaded")
|
|
2988
|
+
self.minor_db_label = QLabel(self.tr("Database: not downloaded"))
|
|
2989
2989
|
self.minor_db_label.setStyleSheet("font-size: 10px; color: gray;")
|
|
2990
2990
|
|
|
2991
|
-
self.btn_minor_download = QPushButton("Download Catalog")
|
|
2991
|
+
self.btn_minor_download = QPushButton(self.tr("Download Catalog"))
|
|
2992
2992
|
self.btn_minor_download.clicked.connect(self.download_minor_body_catalog)
|
|
2993
2993
|
|
|
2994
|
-
self.btn_minor_search = QPushButton("Search Minor Bodies")
|
|
2994
|
+
self.btn_minor_search = QPushButton(self.tr("Search Minor Bodies"))
|
|
2995
2995
|
self.btn_minor_search.clicked.connect(self.perform_minor_body_search)
|
|
2996
2996
|
|
|
2997
2997
|
# Row 0: status label across full width
|
|
@@ -3002,18 +3002,18 @@ class WIMIDialog(QDialog):
|
|
|
3002
3002
|
minor_layout.addWidget(self.btn_minor_search, 1, 2, 1, 2)
|
|
3003
3003
|
|
|
3004
3004
|
# --- Search scope (Defined Circle vs Entire Image) ---
|
|
3005
|
-
scope_label = QLabel("Search scope:")
|
|
3005
|
+
scope_label = QLabel(self.tr("Search scope:"))
|
|
3006
3006
|
self.minor_scope_combo = QComboBox()
|
|
3007
3007
|
self.minor_scope_combo.addItems([
|
|
3008
|
-
"Defined Region",
|
|
3009
|
-
"Entire Image",
|
|
3008
|
+
self.tr("Defined Region"),
|
|
3009
|
+
self.tr("Entire Image"),
|
|
3010
3010
|
])
|
|
3011
3011
|
|
|
3012
3012
|
minor_layout.addWidget(scope_label, 2, 0)
|
|
3013
3013
|
minor_layout.addWidget(self.minor_scope_combo, 2, 1, 1, 3)
|
|
3014
3014
|
|
|
3015
3015
|
# --- Limits row 1: asteroid H_max + max count ---
|
|
3016
|
-
ast_H_label = QLabel("Asteroid H \u2264")
|
|
3016
|
+
ast_H_label = QLabel(self.tr("Asteroid H \u2264"))
|
|
3017
3017
|
self.minor_ast_H_spin = QDoubleSpinBox()
|
|
3018
3018
|
self.minor_ast_H_spin.setRange(0.0, 40.0)
|
|
3019
3019
|
self.minor_ast_H_spin.setDecimals(1)
|
|
@@ -3022,7 +3022,7 @@ class WIMIDialog(QDialog):
|
|
|
3022
3022
|
float(self.settings.value("wimi/minor/asteroid_H_max", 20.0))
|
|
3023
3023
|
)
|
|
3024
3024
|
|
|
3025
|
-
ast_max_label = QLabel("Max asteroids:")
|
|
3025
|
+
ast_max_label = QLabel(self.tr("Max asteroids:"))
|
|
3026
3026
|
self.minor_ast_max_spin = QSpinBox()
|
|
3027
3027
|
self.minor_ast_max_spin.setRange(100, 2000000)
|
|
3028
3028
|
self.minor_ast_max_spin.setSingleStep(1000)
|
|
@@ -3036,7 +3036,7 @@ class WIMIDialog(QDialog):
|
|
|
3036
3036
|
minor_layout.addWidget(self.minor_ast_max_spin, 3, 3)
|
|
3037
3037
|
|
|
3038
3038
|
# --- Limits row 2: comet H_max + max count ---
|
|
3039
|
-
com_H_label = QLabel("Comet H \u2264")
|
|
3039
|
+
com_H_label = QLabel(self.tr("Comet H \u2264"))
|
|
3040
3040
|
self.minor_com_H_spin = QDoubleSpinBox()
|
|
3041
3041
|
self.minor_com_H_spin.setRange(0.0, 40.0)
|
|
3042
3042
|
self.minor_com_H_spin.setDecimals(1)
|
|
@@ -3045,7 +3045,7 @@ class WIMIDialog(QDialog):
|
|
|
3045
3045
|
float(self.settings.value("wimi/minor/comet_H_max", 15.0))
|
|
3046
3046
|
)
|
|
3047
3047
|
|
|
3048
|
-
com_max_label = QLabel("Max comets:")
|
|
3048
|
+
com_max_label = QLabel(self.tr("Max comets:"))
|
|
3049
3049
|
self.minor_com_max_spin = QSpinBox()
|
|
3050
3050
|
self.minor_com_max_spin.setRange(100, 100000)
|
|
3051
3051
|
self.minor_com_max_spin.setSingleStep(500)
|
|
@@ -3059,14 +3059,14 @@ class WIMIDialog(QDialog):
|
|
|
3059
3059
|
minor_layout.addWidget(self.minor_com_max_spin, 4, 3)
|
|
3060
3060
|
|
|
3061
3061
|
# --- Optional specific target (designation / name) ---
|
|
3062
|
-
target_label = QLabel("Target (optional):")
|
|
3062
|
+
target_label = QLabel(self.tr("Target (optional):"))
|
|
3063
3063
|
self.minor_target_edit = QLineEdit()
|
|
3064
3064
|
self.minor_target_edit.setPlaceholderText("e.g. 584, Semiramis, C/2023 A3...")
|
|
3065
3065
|
|
|
3066
3066
|
minor_layout.addWidget(target_label, 5, 0)
|
|
3067
3067
|
minor_layout.addWidget(self.minor_target_edit, 5, 1, 1, 3)
|
|
3068
3068
|
|
|
3069
|
-
self.minor_count_button = QPushButton("Count Objects Brighter Than Limits")
|
|
3069
|
+
self.minor_count_button = QPushButton(self.tr("Count Objects Brighter Than Limits"))
|
|
3070
3070
|
self.minor_count_button.setToolTip(
|
|
3071
3071
|
"Show how many catalog objects are brighter than the selected H limits."
|
|
3072
3072
|
)
|
|
@@ -3075,7 +3075,7 @@ class WIMIDialog(QDialog):
|
|
|
3075
3075
|
minor_layout.addWidget(self.minor_count_button, 6, 0, 1, 4)
|
|
3076
3076
|
|
|
3077
3077
|
# --- Time offset (hours) for ephemerides ---
|
|
3078
|
-
time_offset_label = QLabel("Time offset (hours):")
|
|
3078
|
+
time_offset_label = QLabel(self.tr("Time offset (hours):"))
|
|
3079
3079
|
self.minor_time_offset_spin = QDoubleSpinBox()
|
|
3080
3080
|
self.minor_time_offset_spin.setRange(-24.0, 24.0)
|
|
3081
3081
|
self.minor_time_offset_spin.setDecimals(2)
|
|
@@ -3099,7 +3099,7 @@ class WIMIDialog(QDialog):
|
|
|
3099
3099
|
self._load_minor_db_path()
|
|
3100
3100
|
|
|
3101
3101
|
# Adding the "Deep Vizier Search" button below the other search buttons
|
|
3102
|
-
self.deep_vizier_button = QPushButton("Caution - Deep Vizier Search")
|
|
3102
|
+
self.deep_vizier_button = QPushButton(self.tr("Caution - Deep Vizier Search"))
|
|
3103
3103
|
self.deep_vizier_button.setIcon(QIcon(nuke_path)) # Assuming `nuke_path` is the correct path for the icon
|
|
3104
3104
|
self.deep_vizier_button.setToolTip("Perform a deep search with Vizier. Caution: May return large datasets.")
|
|
3105
3105
|
|
|
@@ -3109,7 +3109,7 @@ class WIMIDialog(QDialog):
|
|
|
3109
3109
|
# Add the Deep Vizier button to the advanced search layout
|
|
3110
3110
|
self.advanced_search_panel.addWidget(self.deep_vizier_button)
|
|
3111
3111
|
|
|
3112
|
-
self.mast_search_button = QPushButton("Search M.A.S.T Database")
|
|
3112
|
+
self.mast_search_button = QPushButton(self.tr("Search M.A.S.T Database"))
|
|
3113
3113
|
self.mast_search_button.setIcon(QIcon(hubble_path))
|
|
3114
3114
|
self.mast_search_button.clicked.connect(self.perform_mast_search)
|
|
3115
3115
|
self.mast_search_button.setToolTip("Search Hubble, JWST, Spitzer, TESS and More.")
|
|
@@ -3604,15 +3604,15 @@ class WIMIDialog(QDialog):
|
|
|
3604
3604
|
menu = QMenu(self)
|
|
3605
3605
|
|
|
3606
3606
|
# Define actions
|
|
3607
|
-
open_website_action = QAction("Open Website", self)
|
|
3607
|
+
open_website_action = QAction(self.tr("Open Website"), self)
|
|
3608
3608
|
open_website_action.triggered.connect(lambda: self.results_tree.itemDoubleClicked.emit(item, 0))
|
|
3609
3609
|
menu.addAction(open_website_action)
|
|
3610
3610
|
|
|
3611
|
-
zoom_to_object_action = QAction("Zoom to Object", self)
|
|
3611
|
+
zoom_to_object_action = QAction(self.tr("Zoom to Object"), self)
|
|
3612
3612
|
zoom_to_object_action.triggered.connect(lambda: self.zoom_to_object(item))
|
|
3613
3613
|
menu.addAction(zoom_to_object_action)
|
|
3614
3614
|
|
|
3615
|
-
copy_info_action = QAction("Copy Object Information", self)
|
|
3615
|
+
copy_info_action = QAction(self.tr("Copy Object Information"), self)
|
|
3616
3616
|
copy_info_action.triggered.connect(lambda: self.copy_object_information(item))
|
|
3617
3617
|
menu.addAction(copy_info_action)
|
|
3618
3618
|
|
|
@@ -6524,509 +6524,6 @@ class WIMIDialog(QDialog):
|
|
|
6524
6524
|
print(f"[MinorBodies] objects inside cone: {kept}")
|
|
6525
6525
|
return results
|
|
6526
6526
|
|
|
6527
|
-
def _get_astap_exe(self) -> str:
|
|
6528
|
-
s = self._settings()
|
|
6529
|
-
# preferred key (what SettingsDialog writes)
|
|
6530
|
-
p = s.value("paths/astap", "", type=str)
|
|
6531
|
-
if p:
|
|
6532
|
-
return p
|
|
6533
|
-
# migrate legacy key if present
|
|
6534
|
-
legacy = s.value("astap/exe_path", "", type=str)
|
|
6535
|
-
if legacy:
|
|
6536
|
-
s.setValue("paths/astap", legacy)
|
|
6537
|
-
s.remove("astap/exe_path")
|
|
6538
|
-
s.sync()
|
|
6539
|
-
return legacy
|
|
6540
|
-
return ""
|
|
6541
|
-
|
|
6542
|
-
def _set_astap_exe(self, path: str) -> None:
|
|
6543
|
-
s = self._settings()
|
|
6544
|
-
s.setValue("paths/astap", path)
|
|
6545
|
-
s.sync()
|
|
6546
|
-
|
|
6547
|
-
def plate_solve_image(self):
|
|
6548
|
-
"""
|
|
6549
|
-
Attempts to plate-solve the loaded image using ASTAP,
|
|
6550
|
-
first trying a seeded solve (RA, SPD, scale, binning),
|
|
6551
|
-
then falling back to a blind solve if anything is missing.
|
|
6552
|
-
On success, updates self.header and self.wcs.
|
|
6553
|
-
"""
|
|
6554
|
-
if not hasattr(self, 'image_path') or not self.image_path:
|
|
6555
|
-
return
|
|
6556
|
-
|
|
6557
|
-
# 1) Ensure ASTAP path
|
|
6558
|
-
astap_exe = self._get_astap_exe()
|
|
6559
|
-
if not astap_exe or not os.path.exists(astap_exe):
|
|
6560
|
-
# last-resort browse if nothing in settings (keeps existing behavior)
|
|
6561
|
-
filt = "Executables (*.exe);;All Files (*)" if sys.platform.startswith("win") else "Executables (*)"
|
|
6562
|
-
new_path, _ = QFileDialog.getOpenFileName(self, "Select ASTAP Executable", "", filt)
|
|
6563
|
-
if not new_path:
|
|
6564
|
-
return
|
|
6565
|
-
astap_exe = new_path
|
|
6566
|
-
self._set_astap_exe(astap_exe)
|
|
6567
|
-
|
|
6568
|
-
# 2) Write out the normalized FITS for ASTAP
|
|
6569
|
-
normalized = self.stretch_image(self.image_data.astype(np.float32))
|
|
6570
|
-
try:
|
|
6571
|
-
tmp_path = self.save_temp_fits_image(normalized, self.image_path)
|
|
6572
|
-
except Exception as e:
|
|
6573
|
-
QMessageBox.critical(self, "Plate Solve", f"Error saving temp FITS: {e}")
|
|
6574
|
-
return
|
|
6575
|
-
|
|
6576
|
-
# 3) Seed arguments from header
|
|
6577
|
-
raw_hdr = None
|
|
6578
|
-
if isinstance(self.original_header, fits.Header):
|
|
6579
|
-
raw_hdr = self.original_header
|
|
6580
|
-
elif self.image_path.lower().endswith(('.fits','.fit')):
|
|
6581
|
-
with fits.open(self.image_path, memmap=False) as hdul:
|
|
6582
|
-
raw_hdr = hdul[0].header
|
|
6583
|
-
|
|
6584
|
-
seed_args = []
|
|
6585
|
-
if isinstance(raw_hdr, fits.Header):
|
|
6586
|
-
# debug-dump
|
|
6587
|
-
print("🔍 Raw header contents:")
|
|
6588
|
-
for k,v in raw_hdr.items():
|
|
6589
|
-
print(f" {k} = {v}")
|
|
6590
|
-
|
|
6591
|
-
try:
|
|
6592
|
-
# RA→hours, SPD
|
|
6593
|
-
ra_deg = float(raw_hdr["CRVAL1"])
|
|
6594
|
-
dec_deg= float(raw_hdr["CRVAL2"])
|
|
6595
|
-
ra_h = ra_deg / 15.0
|
|
6596
|
-
spd = dec_deg + 90.0
|
|
6597
|
-
|
|
6598
|
-
# plate scale from CD matrix (°/px→″/px)
|
|
6599
|
-
cd1 = float(raw_hdr.get("CD1_1", raw_hdr.get("CDELT1",0)))
|
|
6600
|
-
cd2 = float(raw_hdr.get("CD2_1", raw_hdr.get("CDELT2",0)))
|
|
6601
|
-
scale = np.hypot(cd1, cd2) * 3600.0
|
|
6602
|
-
|
|
6603
|
-
# apply XBINNING/YBINNING
|
|
6604
|
-
bx = int(raw_hdr.get("XBINNING", 1))
|
|
6605
|
-
by = int(raw_hdr.get("YBINNING", bx))
|
|
6606
|
-
if bx != by:
|
|
6607
|
-
print(f"⚠️ Unequal binning: {bx}×{by}, averaging.")
|
|
6608
|
-
binf = (bx+by)/2.0
|
|
6609
|
-
scale *= binf
|
|
6610
|
-
|
|
6611
|
-
seed_args = [
|
|
6612
|
-
"-ra", f"{ra_h:.6f}",
|
|
6613
|
-
"-spd", f"{spd:.6f}",
|
|
6614
|
-
"-scale", f"{scale:.3f}"
|
|
6615
|
-
]
|
|
6616
|
-
print(f"🔸 Seeding ASTAP: RA={ra_h:.6f}h, SPD={spd:.6f}°, scale={scale:.3f}\"/px (×{binf} bin)")
|
|
6617
|
-
except Exception as e:
|
|
6618
|
-
print("⚠️ Failed to build seed args, will do blind solve:", e)
|
|
6619
|
-
|
|
6620
|
-
# 4) Build ASTAP args
|
|
6621
|
-
if seed_args:
|
|
6622
|
-
args = ["-f", tmp_path] + seed_args + ["-wcs", "-sip"]
|
|
6623
|
-
else:
|
|
6624
|
-
args = ["-f", tmp_path, "-r", "179", "-fov", "0", "-z", "0", "-wcs", "-sip"]
|
|
6625
|
-
|
|
6626
|
-
print("▶️ Running ASTAP with arguments:", args)
|
|
6627
|
-
|
|
6628
|
-
# create and launch the process
|
|
6629
|
-
process = QProcess(self)
|
|
6630
|
-
process.start(astap_exe, args)
|
|
6631
|
-
if not process.waitForStarted(5000):
|
|
6632
|
-
#QMessageBox.critical(self, "Plate Solve", "Failed to start ASTAP process.")
|
|
6633
|
-
os.remove(tmp_path)
|
|
6634
|
-
|
|
6635
|
-
return None
|
|
6636
|
-
if not process.waitForFinished(300000):
|
|
6637
|
-
#QMessageBox.critical(self, "Plate Solve", "ASTAP process timed out.")
|
|
6638
|
-
os.remove(tmp_path)
|
|
6639
|
-
return None
|
|
6640
|
-
|
|
6641
|
-
exit_code = process.exitCode()
|
|
6642
|
-
stdout = process.readAllStandardOutput().data().decode()
|
|
6643
|
-
stderr = process.readAllStandardError().data().decode()
|
|
6644
|
-
print("ASTAP exit code:", exit_code)
|
|
6645
|
-
print("ASTAP STDOUT:\n", stdout)
|
|
6646
|
-
print("ASTAP STDERR:\n", stderr)
|
|
6647
|
-
|
|
6648
|
-
if exit_code != 0:
|
|
6649
|
-
os.remove(tmp_path)
|
|
6650
|
-
#QMessageBox.warning(self, "Plate Solve", "ASTAP failed. Falling back to blind solve.")
|
|
6651
|
-
|
|
6652
|
-
return None
|
|
6653
|
-
|
|
6654
|
-
# --- Retrieve the initial solved header from the temporary FITS file ---
|
|
6655
|
-
try:
|
|
6656
|
-
with fits.open(tmp_path, memmap=False) as hdul:
|
|
6657
|
-
solved_header = dict(hdul[0].header)
|
|
6658
|
-
for key in ["COMMENT", "HISTORY", "END"]:
|
|
6659
|
-
solved_header.pop(key, None)
|
|
6660
|
-
print("Initial solved header retrieved from temporary FITS file:")
|
|
6661
|
-
for key, value in solved_header.items():
|
|
6662
|
-
print(f"{key} = {value}")
|
|
6663
|
-
except Exception as e:
|
|
6664
|
-
QMessageBox.critical(self, "Plate Solve", f"Error reading solved header: {e}")
|
|
6665
|
-
os.remove(tmp_path)
|
|
6666
|
-
|
|
6667
|
-
return None
|
|
6668
|
-
|
|
6669
|
-
# --- Check for a .wcs file and merge its header if present ---
|
|
6670
|
-
wcs_path = os.path.splitext(tmp_path)[0] + ".wcs"
|
|
6671
|
-
if os.path.exists(wcs_path):
|
|
6672
|
-
try:
|
|
6673
|
-
wcs_header = {}
|
|
6674
|
-
with open(wcs_path, "r") as f:
|
|
6675
|
-
text = f.read()
|
|
6676
|
-
# Matches a FITS header keyword and its value (with an optional comment).
|
|
6677
|
-
pattern = r"(\w+)\s*=\s*('?[^/']*'?)[\s/]"
|
|
6678
|
-
for match in re.finditer(pattern, text):
|
|
6679
|
-
key = match.group(1).strip().upper()
|
|
6680
|
-
val = match.group(2).strip()
|
|
6681
|
-
if val.startswith("'") and val.endswith("'"):
|
|
6682
|
-
val = val[1:-1].strip()
|
|
6683
|
-
wcs_header[key] = val
|
|
6684
|
-
wcs_header.pop("END", None)
|
|
6685
|
-
print("WCS header retrieved from .wcs file:")
|
|
6686
|
-
for key, value in wcs_header.items():
|
|
6687
|
-
print(f"{key} = {value}")
|
|
6688
|
-
# Merge the parsed WCS header into the solved header.
|
|
6689
|
-
solved_header.update(wcs_header)
|
|
6690
|
-
except Exception as e:
|
|
6691
|
-
print("Error reading .wcs file:", e)
|
|
6692
|
-
else:
|
|
6693
|
-
print("No .wcs file found; using header from temporary FITS.")
|
|
6694
|
-
|
|
6695
|
-
# --- If loaded from a slot, merge the original file path from slot metadata ---
|
|
6696
|
-
if getattr(self, "_from_slot", False) and hasattr(self, "_slot_meta"):
|
|
6697
|
-
if "file_path" not in solved_header and "file_path" in self._slot_meta:
|
|
6698
|
-
solved_header["file_path"] = self._slot_meta["file_path"]
|
|
6699
|
-
print("Merged file_path from slot metadata into solved header.")
|
|
6700
|
-
|
|
6701
|
-
# --- Add any missing required WCS keywords ---
|
|
6702
|
-
required_keys = {
|
|
6703
|
-
"CTYPE1": "RA---TAN",
|
|
6704
|
-
"CTYPE2": "DEC--TAN",
|
|
6705
|
-
"RADECSYS": "ICRS",
|
|
6706
|
-
"WCSAXES": 2,
|
|
6707
|
-
# CRVAL1, CRVAL2, CRPIX1, CRPIX2 are ideally provided by ASTAP.
|
|
6708
|
-
}
|
|
6709
|
-
for key, default in required_keys.items():
|
|
6710
|
-
if key not in solved_header:
|
|
6711
|
-
solved_header[key] = default
|
|
6712
|
-
print(f"Added missing key {key} with default value {default}.")
|
|
6713
|
-
|
|
6714
|
-
# --- Convert keys that are expected to be numeric from strings to numbers ---
|
|
6715
|
-
expected_numeric_keys = {
|
|
6716
|
-
"CRPIX1", "CRPIX2", "CRVAL1", "CRVAL2", "CROTA1", "CROTA2",
|
|
6717
|
-
"CDELT1", "CDELT2", "CD1_1", "CD1_2", "CD2_1", "CD2_2", "WCSAXES"
|
|
6718
|
-
}
|
|
6719
|
-
for key in expected_numeric_keys:
|
|
6720
|
-
if key in solved_header:
|
|
6721
|
-
try:
|
|
6722
|
-
# For keys that should be integers, you can use int(float(...)) if necessary.
|
|
6723
|
-
solved_header[key] = float(solved_header[key])
|
|
6724
|
-
except ValueError:
|
|
6725
|
-
print(f"Warning: Could not convert {key} value '{solved_header[key]}' to float.")
|
|
6726
|
-
|
|
6727
|
-
# --- Ensure integer keywords are stored as integers ---
|
|
6728
|
-
for key in ["WCSAXES", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3"]:
|
|
6729
|
-
if key in solved_header:
|
|
6730
|
-
try:
|
|
6731
|
-
solved_header[key] = int(float(solved_header[key]))
|
|
6732
|
-
except ValueError:
|
|
6733
|
-
print(f"Warning: Could not convert {key} value '{solved_header[key]}' to int.")
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
os.remove(tmp_path)
|
|
6737
|
-
print("ASTAP plate solving successful. Final solved header:")
|
|
6738
|
-
for key, value in solved_header.items():
|
|
6739
|
-
print(f"{key} = {value}")
|
|
6740
|
-
|
|
6741
|
-
# --------------------------------------------------------------------
|
|
6742
|
-
# 1) Make sure A_ORDER/B_ORDER exist in pairs:
|
|
6743
|
-
if "B_ORDER" in solved_header and "A_ORDER" not in solved_header:
|
|
6744
|
-
solved_header["A_ORDER"] = solved_header["B_ORDER"]
|
|
6745
|
-
if "A_ORDER" in solved_header and "B_ORDER" not in solved_header:
|
|
6746
|
-
solved_header["B_ORDER"] = solved_header["A_ORDER"]
|
|
6747
|
-
|
|
6748
|
-
# 2) Convert SIP‐order keywords to ints:
|
|
6749
|
-
for key in ("A_ORDER","B_ORDER","AP_ORDER","BP_ORDER"):
|
|
6750
|
-
if key in solved_header:
|
|
6751
|
-
solved_header[key] = int(float(solved_header[key]))
|
|
6752
|
-
|
|
6753
|
-
# 3) Convert every SIP coefficient to float:
|
|
6754
|
-
for k in list(solved_header):
|
|
6755
|
-
if re.match(r"^(?:A|B|AP|BP)_[0-9]+_[0-9]+$", k):
|
|
6756
|
-
solved_header[k] = float(solved_header[k])
|
|
6757
|
-
|
|
6758
|
-
# --------------------------------------------------------------------
|
|
6759
|
-
# 4) Now rebuild your FITS header from the dict, preserving ordering:
|
|
6760
|
-
new_hdr = fits.Header()
|
|
6761
|
-
for key, val in solved_header.items():
|
|
6762
|
-
# skip any stray non‑FITS metadata
|
|
6763
|
-
if key == "file_path":
|
|
6764
|
-
continue
|
|
6765
|
-
new_hdr[key] = val
|
|
6766
|
-
|
|
6767
|
-
# 5) Finally swap in the new header and re-init WCS (with SIP!)
|
|
6768
|
-
self.header = new_hdr
|
|
6769
|
-
try:
|
|
6770
|
-
self.apply_wcs_header(self.header)
|
|
6771
|
-
self.status_label.setText("Status: ASTAP solve succeeded.")
|
|
6772
|
-
except Exception as e:
|
|
6773
|
-
QMessageBox.critical(self, "Plate Solve", f"Error initializing WCS from solved header:\n{e}")
|
|
6774
|
-
return
|
|
6775
|
-
|
|
6776
|
-
return solved_header
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
def save_temp_fits_image(self, normalized_image, image_path: str):
|
|
6780
|
-
"""
|
|
6781
|
-
Save the normalized_image as a FITS file to a temporary file.
|
|
6782
|
-
|
|
6783
|
-
If the original image is FITS, this method retrieves the stored metadata
|
|
6784
|
-
from the ImageManager and passes it directly to save_image().
|
|
6785
|
-
If not, it generates a minimal header.
|
|
6786
|
-
|
|
6787
|
-
Returns the path to the temporary FITS file.
|
|
6788
|
-
"""
|
|
6789
|
-
# Always save as FITS.
|
|
6790
|
-
selected_format = "fits"
|
|
6791
|
-
bit_depth = "32-bit floating point"
|
|
6792
|
-
is_mono = (normalized_image.ndim == 2 or
|
|
6793
|
-
(normalized_image.ndim == 3 and normalized_image.shape[2] == 1))
|
|
6794
|
-
|
|
6795
|
-
# If the original image is FITS, try to get its stored metadata.
|
|
6796
|
-
original_header = None
|
|
6797
|
-
if image_path.lower().endswith((".fits", ".fit")):
|
|
6798
|
-
if self.parent() and hasattr(self.parent(), "image_manager"):
|
|
6799
|
-
# Use the metadata from the current slot.
|
|
6800
|
-
_, meta = self.parent().image_manager.get_current_image_and_metadata()
|
|
6801
|
-
# Assume that meta already contains a proper 'original_header'
|
|
6802
|
-
# (or the entire meta is the header).
|
|
6803
|
-
original_header = meta.get("original_header", None)
|
|
6804
|
-
# If nothing is stored, fall back to creating a minimal header.
|
|
6805
|
-
if original_header is None:
|
|
6806
|
-
print("No stored FITS header found; creating a minimal header.")
|
|
6807
|
-
original_header = self.create_minimal_fits_header(normalized_image, is_mono)
|
|
6808
|
-
else:
|
|
6809
|
-
# For non-FITS images, generate a minimal header.
|
|
6810
|
-
original_header = self.create_minimal_fits_header(normalized_image, is_mono)
|
|
6811
|
-
|
|
6812
|
-
# Create a temporary filename.
|
|
6813
|
-
tmp_file = tempfile.NamedTemporaryFile(suffix=".fits", delete=False)
|
|
6814
|
-
tmp_path = tmp_file.name
|
|
6815
|
-
tmp_file.close()
|
|
6816
|
-
|
|
6817
|
-
try:
|
|
6818
|
-
# Call your global save_image() exactly as in AstroEditingSuite.
|
|
6819
|
-
save_image(
|
|
6820
|
-
img_array=normalized_image,
|
|
6821
|
-
filename=tmp_path,
|
|
6822
|
-
original_format=selected_format,
|
|
6823
|
-
bit_depth=bit_depth,
|
|
6824
|
-
original_header=original_header,
|
|
6825
|
-
is_mono=is_mono
|
|
6826
|
-
# (image_meta and file_meta can be omitted if not needed)
|
|
6827
|
-
)
|
|
6828
|
-
print(f"Temporary normalized FITS saved to: {tmp_path}")
|
|
6829
|
-
except Exception as e:
|
|
6830
|
-
print("Error saving temporary FITS file using save_image():", e)
|
|
6831
|
-
raise e
|
|
6832
|
-
return tmp_path
|
|
6833
|
-
|
|
6834
|
-
def create_minimal_fits_header(self, img_array, is_mono=False):
|
|
6835
|
-
"""
|
|
6836
|
-
Creates a minimal FITS header when the original header is missing.
|
|
6837
|
-
"""
|
|
6838
|
-
|
|
6839
|
-
header = Header()
|
|
6840
|
-
header['SIMPLE'] = (True, 'Standard FITS file')
|
|
6841
|
-
header['BITPIX'] = -32 # 32-bit floating-point data
|
|
6842
|
-
header['NAXIS'] = 2 if is_mono else 3
|
|
6843
|
-
header['NAXIS1'] = img_array.shape[2] if img_array.ndim == 3 and not is_mono else img_array.shape[1] # Image width
|
|
6844
|
-
header['NAXIS2'] = img_array.shape[1] if img_array.ndim == 3 and not is_mono else img_array.shape[0] # Image height
|
|
6845
|
-
if not is_mono:
|
|
6846
|
-
header['NAXIS3'] = img_array.shape[0] if img_array.ndim == 3 else 1 # Number of color channels
|
|
6847
|
-
header['BZERO'] = 0.0 # No offset
|
|
6848
|
-
header['BSCALE'] = 1.0 # No scaling
|
|
6849
|
-
header.add_comment("Minimal FITS header generated by AstroEditingSuite.")
|
|
6850
|
-
|
|
6851
|
-
return header
|
|
6852
|
-
|
|
6853
|
-
def stretch_image(self, image):
|
|
6854
|
-
"""
|
|
6855
|
-
Perform an unlinked linear stretch on the image.
|
|
6856
|
-
Each channel is stretched independently by subtracting its own minimum,
|
|
6857
|
-
recording its own median, and applying the stretch formula.
|
|
6858
|
-
Returns the stretched image in [0,1].
|
|
6859
|
-
"""
|
|
6860
|
-
was_single_channel = False # Flag to check if image was single-channel
|
|
6861
|
-
|
|
6862
|
-
# If the image is 2D or has one channel, convert to 3-channel
|
|
6863
|
-
if image.ndim == 2 or (image.ndim == 3 and image.shape[2] == 1):
|
|
6864
|
-
was_single_channel = True
|
|
6865
|
-
image = np.stack([image] * 3, axis=-1)
|
|
6866
|
-
|
|
6867
|
-
image = np.asarray(image, dtype=np.float32)
|
|
6868
|
-
stretched_image = image.copy() # Need copy for in-place modification
|
|
6869
|
-
self.stretch_original_mins = []
|
|
6870
|
-
self.stretch_original_medians = []
|
|
6871
|
-
target_median = 0.02
|
|
6872
|
-
|
|
6873
|
-
for c in range(3):
|
|
6874
|
-
channel_min = np.min(stretched_image[..., c])
|
|
6875
|
-
self.stretch_original_mins.append(channel_min)
|
|
6876
|
-
stretched_image[..., c] -= channel_min
|
|
6877
|
-
channel_median = np.median(stretched_image[..., c])
|
|
6878
|
-
self.stretch_original_medians.append(channel_median)
|
|
6879
|
-
if channel_median != 0:
|
|
6880
|
-
numerator = (channel_median - 1) * target_median * stretched_image[..., c]
|
|
6881
|
-
denominator = (
|
|
6882
|
-
channel_median * (target_median + stretched_image[..., c] - 1)
|
|
6883
|
-
- target_median * stretched_image[..., c]
|
|
6884
|
-
)
|
|
6885
|
-
denominator = np.where(denominator == 0, 1e-6, denominator)
|
|
6886
|
-
stretched_image[..., c] = numerator / denominator
|
|
6887
|
-
else:
|
|
6888
|
-
print(f"Channel {c} - Median is zero. Skipping stretch.")
|
|
6889
|
-
|
|
6890
|
-
stretched_image = np.clip(stretched_image, 0.0, 1.0)
|
|
6891
|
-
self.was_single_channel = was_single_channel
|
|
6892
|
-
return stretched_image
|
|
6893
|
-
|
|
6894
|
-
def unstretch_image(self, image):
|
|
6895
|
-
"""
|
|
6896
|
-
Undo the unlinked linear stretch using stored parameters.
|
|
6897
|
-
Returns the unstretched image.
|
|
6898
|
-
"""
|
|
6899
|
-
original_mins = self.stretch_original_mins
|
|
6900
|
-
original_medians = self.stretch_original_medians
|
|
6901
|
-
was_single_channel = self.was_single_channel
|
|
6902
|
-
|
|
6903
|
-
image = np.asarray(image, dtype=np.float32)
|
|
6904
|
-
|
|
6905
|
-
if image.ndim == 2:
|
|
6906
|
-
channel_median = np.median(image)
|
|
6907
|
-
original_median = original_medians[0]
|
|
6908
|
-
original_min = original_mins[0]
|
|
6909
|
-
if channel_median != 0 and original_median != 0:
|
|
6910
|
-
numerator = (channel_median - 1) * original_median * image
|
|
6911
|
-
denominator = channel_median * (original_median + image - 1) - original_median * image
|
|
6912
|
-
denominator = np.where(denominator == 0, 1e-6, denominator)
|
|
6913
|
-
image = numerator / denominator
|
|
6914
|
-
else:
|
|
6915
|
-
print("Channel median or original median is zero. Skipping unstretch.")
|
|
6916
|
-
image += original_min
|
|
6917
|
-
image = np.clip(image, 0, 1)
|
|
6918
|
-
return image
|
|
6919
|
-
|
|
6920
|
-
for c in range(3):
|
|
6921
|
-
channel_median = np.median(image[..., c])
|
|
6922
|
-
original_median = original_medians[c]
|
|
6923
|
-
original_min = original_mins[c]
|
|
6924
|
-
if channel_median != 0 and original_median != 0:
|
|
6925
|
-
numerator = (channel_median - 1) * original_median * image[..., c]
|
|
6926
|
-
denominator = (
|
|
6927
|
-
channel_median * (original_median + image[..., c] - 1)
|
|
6928
|
-
- original_median * image[..., c]
|
|
6929
|
-
)
|
|
6930
|
-
denominator = np.where(denominator == 0, 1e-6, denominator)
|
|
6931
|
-
image[..., c] = numerator / denominator
|
|
6932
|
-
else:
|
|
6933
|
-
print(f"Channel {c} - Median or original median is zero. Skipping unstretch.")
|
|
6934
|
-
image[..., c] += original_min
|
|
6935
|
-
|
|
6936
|
-
image = np.clip(image, 0, 1)
|
|
6937
|
-
if was_single_channel and image.ndim == 3:
|
|
6938
|
-
image = np.mean(image, axis=2, keepdims=True)
|
|
6939
|
-
return image
|
|
6940
|
-
|
|
6941
|
-
def retrieve_and_apply_wcs(self, job_id):
|
|
6942
|
-
"""Download the wcs.fits file from Astrometry.net, extract WCS header data, and apply it."""
|
|
6943
|
-
try:
|
|
6944
|
-
wcs_url = f"https://nova.astrometry.net/wcs_file/{job_id}"
|
|
6945
|
-
wcs_filepath = "wcs.fits"
|
|
6946
|
-
max_retries = 10
|
|
6947
|
-
delay = 10 # seconds
|
|
6948
|
-
|
|
6949
|
-
for attempt in range(max_retries):
|
|
6950
|
-
response = requests.get(wcs_url, stream=True)
|
|
6951
|
-
response.raise_for_status()
|
|
6952
|
-
|
|
6953
|
-
with open(wcs_filepath, 'wb') as f:
|
|
6954
|
-
for chunk in response.iter_content(chunk_size=8192):
|
|
6955
|
-
f.write(chunk)
|
|
6956
|
-
|
|
6957
|
-
try:
|
|
6958
|
-
with fits.open(wcs_filepath, ignore_missing_simple=True, ignore_missing_end=True) as hdul:
|
|
6959
|
-
wcs_header = hdul[0].header
|
|
6960
|
-
print("WCS header successfully retrieved.")
|
|
6961
|
-
# 🔁 use common path
|
|
6962
|
-
self.apply_wcs_header(wcs_header)
|
|
6963
|
-
return wcs_header
|
|
6964
|
-
except Exception as e:
|
|
6965
|
-
print(f"Attempt {attempt + 1}: Failed to process WCS file - possibly HTML instead of FITS. Retrying in {delay} seconds...")
|
|
6966
|
-
print(f"Error: {e}")
|
|
6967
|
-
time.sleep(delay)
|
|
6968
|
-
|
|
6969
|
-
print("Failed to download a valid WCS FITS file after multiple attempts.")
|
|
6970
|
-
return None
|
|
6971
|
-
|
|
6972
|
-
except requests.exceptions.RequestException as e:
|
|
6973
|
-
print(f"Error downloading WCS file: {e}")
|
|
6974
|
-
except Exception as e:
|
|
6975
|
-
print(f"Error processing WCS file: {e}")
|
|
6976
|
-
|
|
6977
|
-
return None
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
def apply_wcs_header(self, wcs_header):
|
|
6982
|
-
"""
|
|
6983
|
-
Apply a solved WCS header. Sets self.wcs, self.pixscale (arcsec/pix),
|
|
6984
|
-
self.orientation, and updates the orientation label.
|
|
6985
|
-
"""
|
|
6986
|
-
# 1) Initialize the WCS object
|
|
6987
|
-
self.wcs = WCS(wcs_header, naxis=2, relax=True)
|
|
6988
|
-
|
|
6989
|
-
# 2) Derive pixel scale (arcsec/pixel)
|
|
6990
|
-
if 'CDELT1' in wcs_header:
|
|
6991
|
-
# CDELT1 is degrees/pixel
|
|
6992
|
-
self.pixscale = abs(float(wcs_header['CDELT1'])) * 3600.0
|
|
6993
|
-
elif 'CD1_1' in wcs_header and 'CD2_2' in wcs_header:
|
|
6994
|
-
# approximate from CD matrix determinant
|
|
6995
|
-
det = (wcs_header['CD1_1'] * wcs_header['CD2_2']
|
|
6996
|
-
- wcs_header['CD1_2'] * wcs_header['CD2_1'])
|
|
6997
|
-
pixscale_deg = math.sqrt(abs(det))
|
|
6998
|
-
self.pixscale = pixscale_deg * 3600.0
|
|
6999
|
-
else:
|
|
7000
|
-
self.pixscale = None
|
|
7001
|
-
print("Warning: could not derive pixscale from header.")
|
|
7002
|
-
|
|
7003
|
-
# 3) Extract orientation (CROTA2 if present)
|
|
7004
|
-
if 'CROTA2' in wcs_header:
|
|
7005
|
-
self.orientation = float(wcs_header['CROTA2'])
|
|
7006
|
-
else:
|
|
7007
|
-
# fallback to your custom function
|
|
7008
|
-
self.orientation = calculate_orientation(wcs_header)
|
|
7009
|
-
|
|
7010
|
-
# 4) Update the GUI label
|
|
7011
|
-
if self.orientation is not None:
|
|
7012
|
-
self.orientation_label.setText(f"Orientation: {self.orientation:.2f}°")
|
|
7013
|
-
else:
|
|
7014
|
-
self.orientation_label.setText("Orientation: N/A")
|
|
7015
|
-
|
|
7016
|
-
print(f" -> pixscale = {self.pixscale} arcsec/pixel")
|
|
7017
|
-
print(f" -> orientation = {self.orientation}°")
|
|
7018
|
-
try:
|
|
7019
|
-
cr1 = wcs_header.get('CRVAL1')
|
|
7020
|
-
cr2 = wcs_header.get('CRVAL2')
|
|
7021
|
-
if cr1 is not None and cr2 is not None:
|
|
7022
|
-
self.center_ra = float(cr1)
|
|
7023
|
-
self.center_dec = float(cr2)
|
|
7024
|
-
print(f" -> center RA/Dec = {self.center_ra:.6f}, {self.center_dec:.6f}")
|
|
7025
|
-
except Exception:
|
|
7026
|
-
print("Warning: could not extract CRVAL1/CRVAL2")
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
6527
|
def calculate_pixel_from_ra_dec(self, ra, dec):
|
|
7031
6528
|
"""Convert RA/Dec to pixel coordinates using the WCS data."""
|
|
7032
6529
|
if not hasattr(self, "wcs") or self.wcs is None:
|
|
@@ -7059,117 +6556,6 @@ class WIMIDialog(QDialog):
|
|
|
7059
6556
|
|
|
7060
6557
|
return int(round(x_val)), int(round(y_val))
|
|
7061
6558
|
|
|
7062
|
-
def login_to_astrometry(self, api_key):
|
|
7063
|
-
try:
|
|
7064
|
-
response = requests.post(
|
|
7065
|
-
ASTROMETRY_API_URL + "login",
|
|
7066
|
-
data={'request-json': json.dumps({"apikey": api_key})}
|
|
7067
|
-
)
|
|
7068
|
-
response_data = response.json()
|
|
7069
|
-
if response_data.get("status") == "success":
|
|
7070
|
-
return response_data["session"]
|
|
7071
|
-
else:
|
|
7072
|
-
raise ValueError("Login failed: " + response_data.get("error", "Unknown error"))
|
|
7073
|
-
except Exception as e:
|
|
7074
|
-
raise Exception("Login to Astrometry.net failed: " + str(e))
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
def upload_image_to_astrometry(self, image_path, session_key):
|
|
7078
|
-
try:
|
|
7079
|
-
# Check if the file is XISF format
|
|
7080
|
-
file_extension = os.path.splitext(image_path)[-1].lower()
|
|
7081
|
-
if file_extension == ".xisf":
|
|
7082
|
-
# Load the XISF image
|
|
7083
|
-
xisf = XISF(image_path)
|
|
7084
|
-
im_data = xisf.read_image(0)
|
|
7085
|
-
|
|
7086
|
-
# Convert to a temporary TIFF file for upload
|
|
7087
|
-
temp_image_path = os.path.splitext(image_path)[0] + "_converted.tif"
|
|
7088
|
-
if im_data.dtype == np.float32 or im_data.dtype == np.float64:
|
|
7089
|
-
im_data = np.clip(im_data, 0, 1) * 65535
|
|
7090
|
-
im_data = im_data.astype(np.uint16)
|
|
7091
|
-
|
|
7092
|
-
# Save as TIFF
|
|
7093
|
-
if im_data.shape[-1] == 1: # Grayscale
|
|
7094
|
-
tiff.imwrite(temp_image_path, np.squeeze(im_data, axis=-1))
|
|
7095
|
-
else: # RGB
|
|
7096
|
-
tiff.imwrite(temp_image_path, im_data)
|
|
7097
|
-
|
|
7098
|
-
print(f"Converted XISF file to TIFF at {temp_image_path} for upload.")
|
|
7099
|
-
image_path = temp_image_path # Use the converted file for upload
|
|
7100
|
-
|
|
7101
|
-
# Upload the image file
|
|
7102
|
-
with open(image_path, 'rb') as image_file:
|
|
7103
|
-
files = {'file': image_file}
|
|
7104
|
-
data = {
|
|
7105
|
-
'request-json': json.dumps({
|
|
7106
|
-
"publicly_visible": "y",
|
|
7107
|
-
"allow_modifications": "d",
|
|
7108
|
-
"session": session_key,
|
|
7109
|
-
"allow_commercial_use": "d"
|
|
7110
|
-
})
|
|
7111
|
-
}
|
|
7112
|
-
response = requests.post(ASTROMETRY_API_URL + "upload", files=files, data=data)
|
|
7113
|
-
response_data = response.json()
|
|
7114
|
-
if response_data.get("status") == "success":
|
|
7115
|
-
return response_data["subid"]
|
|
7116
|
-
else:
|
|
7117
|
-
raise ValueError("Image upload failed: " + response_data.get("error", "Unknown error"))
|
|
7118
|
-
|
|
7119
|
-
except Exception as e:
|
|
7120
|
-
raise Exception("Image upload to Astrometry.net failed: " + str(e))
|
|
7121
|
-
|
|
7122
|
-
finally:
|
|
7123
|
-
# Clean up temporary file if created
|
|
7124
|
-
if file_extension == ".xisf" and os.path.exists(temp_image_path):
|
|
7125
|
-
os.remove(temp_image_path)
|
|
7126
|
-
print(f"Temporary TIFF file {temp_image_path} deleted after upload.")
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
def poll_submission_status(self, subid):
|
|
7131
|
-
"""Poll Astrometry.net to retrieve the job ID once the submission is processed."""
|
|
7132
|
-
max_retries = 90 # Adjust as necessary
|
|
7133
|
-
retries = 0
|
|
7134
|
-
while retries < max_retries:
|
|
7135
|
-
try:
|
|
7136
|
-
response = requests.get(ASTROMETRY_API_URL + f"submissions/{subid}")
|
|
7137
|
-
response_data = response.json()
|
|
7138
|
-
jobs = response_data.get("jobs", [])
|
|
7139
|
-
if jobs and jobs[0] is not None:
|
|
7140
|
-
return jobs[0]
|
|
7141
|
-
else:
|
|
7142
|
-
print(f"Polling attempt {retries + 1}: Job not ready yet.")
|
|
7143
|
-
except Exception as e:
|
|
7144
|
-
print(f"Error while polling submission status: {e}")
|
|
7145
|
-
|
|
7146
|
-
retries += 1
|
|
7147
|
-
time.sleep(10) # Wait 10 seconds between retries
|
|
7148
|
-
|
|
7149
|
-
return None
|
|
7150
|
-
|
|
7151
|
-
def poll_calibration_data(self, job_id):
|
|
7152
|
-
"""Poll Astrometry.net to retrieve the calibration data once it's available."""
|
|
7153
|
-
max_retries = 90 # Retry for up to 15 minutes (90 * 10 seconds)
|
|
7154
|
-
retries = 0
|
|
7155
|
-
while retries < max_retries:
|
|
7156
|
-
try:
|
|
7157
|
-
response = requests.get(ASTROMETRY_API_URL + f"jobs/{job_id}/calibration/")
|
|
7158
|
-
response_data = response.json()
|
|
7159
|
-
if response_data and 'ra' in response_data and 'dec' in response_data:
|
|
7160
|
-
print("Calibration data retrieved:", response_data)
|
|
7161
|
-
return response_data # Calibration data is complete
|
|
7162
|
-
else:
|
|
7163
|
-
print(f"Calibration data not available yet (Attempt {retries + 1})")
|
|
7164
|
-
except Exception as e:
|
|
7165
|
-
print(f"Error retrieving calibration data: {e}")
|
|
7166
|
-
|
|
7167
|
-
retries += 1
|
|
7168
|
-
time.sleep(10) # Wait 10 seconds between retries
|
|
7169
|
-
|
|
7170
|
-
return None
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
6559
|
#If originally a fits file update the header
|
|
7174
6560
|
def update_fits_with_wcs(self, filepath, calibration_data):
|
|
7175
6561
|
if not filepath.lower().endswith(('.fits', '.fit')):
|
|
@@ -7391,23 +6777,43 @@ class WIMIDialog(QDialog):
|
|
|
7391
6777
|
CIRCLE('ICRS', {ra_center}, {dec_center}, {radius_deg})
|
|
7392
6778
|
) = 1
|
|
7393
6779
|
"""
|
|
6780
|
+
|
|
6781
|
+
result = None
|
|
6782
|
+
last_err = None
|
|
6783
|
+
|
|
7394
6784
|
for attempt in range(5):
|
|
7395
6785
|
try:
|
|
7396
6786
|
result = Simbad.query_tap(query)
|
|
6787
|
+
last_err = None
|
|
7397
6788
|
break
|
|
7398
6789
|
except Exception as e:
|
|
6790
|
+
last_err = e
|
|
7399
6791
|
if attempt < 4:
|
|
7400
|
-
time.sleep(1)
|
|
6792
|
+
time.sleep(1) # or QThread.msleep(1000) if you want less UI freeze
|
|
7401
6793
|
else:
|
|
7402
|
-
|
|
6794
|
+
# After 5 attempts total, stop with a helpful message
|
|
6795
|
+
err_txt = str(last_err) if last_err is not None else "Unknown error"
|
|
6796
|
+
QMessageBox.warning(
|
|
7403
6797
|
self,
|
|
7404
|
-
"
|
|
7405
|
-
|
|
7406
|
-
|
|
6798
|
+
"SIMBAD network error",
|
|
6799
|
+
(
|
|
6800
|
+
"We couldn't reach SIMBAD due to a network or service error.\n\n"
|
|
6801
|
+
"We tried 5 times and then stopped.\n\n"
|
|
6802
|
+
"What to try:\n"
|
|
6803
|
+
" • Check your internet connection / VPN / firewall\n"
|
|
6804
|
+
" • Verify SIMBAD is reachable: https://simbad.cds.unistra.fr/simbad/\n"
|
|
6805
|
+
" • Then try again\n\n"
|
|
6806
|
+
f"Last error:\n{err_txt}"
|
|
6807
|
+
)
|
|
6808
|
+
)
|
|
6809
|
+
return
|
|
7407
6810
|
|
|
7408
6811
|
if result is None or len(result) == 0:
|
|
7409
|
-
QMessageBox.information(
|
|
7410
|
-
|
|
6812
|
+
QMessageBox.information(
|
|
6813
|
+
self,
|
|
6814
|
+
"No Results",
|
|
6815
|
+
"No objects found in the specified area."
|
|
6816
|
+
)
|
|
7411
6817
|
return
|
|
7412
6818
|
|
|
7413
6819
|
# ——— 3a) list of all “star” & binary/variable OTYPE codes ———
|
|
@@ -7912,41 +7318,6 @@ class WIMIDialog(QDialog):
|
|
|
7912
7318
|
self.status_label.setText(f"Status: Blind solve failed — {res}")
|
|
7913
7319
|
|
|
7914
7320
|
|
|
7915
|
-
def extract_wcs_data(file_path):
|
|
7916
|
-
try:
|
|
7917
|
-
# Open the FITS file with minimal validation to ignore potential errors in non-essential parts
|
|
7918
|
-
with fits.open(file_path, ignore_missing_simple=True, ignore_missing_end=True) as hdul:
|
|
7919
|
-
header = hdul[0].header
|
|
7920
|
-
|
|
7921
|
-
# Extract essential WCS parameters
|
|
7922
|
-
wcs_params = {}
|
|
7923
|
-
keys_to_extract = [
|
|
7924
|
-
'WCSAXES', 'CTYPE1', 'CTYPE2', 'EQUINOX', 'LONPOLE', 'LATPOLE',
|
|
7925
|
-
'CRVAL1', 'CRVAL2', 'CRPIX1', 'CRPIX2', 'CUNIT1', 'CUNIT2',
|
|
7926
|
-
'CD1_1', 'CD1_2', 'CD2_1', 'CD2_2', 'A_ORDER', 'A_0_0', 'A_0_1',
|
|
7927
|
-
'A_0_2', 'A_1_0', 'A_1_1', 'A_2_0', 'B_ORDER', 'B_0_0', 'B_0_1',
|
|
7928
|
-
'B_0_2', 'B_1_0', 'B_1_1', 'B_2_0', 'AP_ORDER', 'AP_0_0', 'AP_0_1',
|
|
7929
|
-
'AP_0_2', 'AP_1_0', 'AP_1_1', 'AP_2_0', 'BP_ORDER', 'BP_0_0',
|
|
7930
|
-
'BP_0_1', 'BP_0_2', 'BP_1_0', 'BP_1_1', 'BP_2_0'
|
|
7931
|
-
]
|
|
7932
|
-
for key in keys_to_extract:
|
|
7933
|
-
if key in header:
|
|
7934
|
-
wcs_params[key] = header[key]
|
|
7935
|
-
|
|
7936
|
-
# Manually create a minimal header with WCS information
|
|
7937
|
-
wcs_header = fits.Header()
|
|
7938
|
-
for key, value in wcs_params.items():
|
|
7939
|
-
wcs_header[key] = value
|
|
7940
|
-
|
|
7941
|
-
# Initialize WCS with this custom header
|
|
7942
|
-
wcs = WCS(wcs_header)
|
|
7943
|
-
print("WCS successfully initialized with minimal header.")
|
|
7944
|
-
return wcs
|
|
7945
|
-
|
|
7946
|
-
except Exception as e:
|
|
7947
|
-
print(f"Error processing WCS file: {e}")
|
|
7948
|
-
return None
|
|
7949
|
-
|
|
7950
7321
|
# Function to calculate comoving radial distance (in Gly)
|
|
7951
7322
|
def calculate_comoving_distance(z):
|
|
7952
7323
|
z = abs(z)
|