Kleinigkeiten

This commit is contained in:
2026-06-29 15:04:02 +02:00
parent a8b6fa2fa6
commit 21d9936138
5 changed files with 356 additions and 15 deletions
+6
View File
@@ -0,0 +1,6 @@
{
"host": "10.42.42.102",
"port": 22,
"user": "tm",
"password": "heute1234"
}
+248
View File
@@ -0,0 +1,248 @@
# Solve-Field-GUI
Grafische Windows-Oberfläche für `solve-field` aus Astrometry.net mit Ausführung auf einem Linux-Server per SSH.
Die Software richtet sich an Anwender, die Astrometrie komfortabel über eine GUI steuern möchten, ohne die `solve-field`-Befehle jedes Mal von Hand zusammenzubauen.
## Was die Software macht
Die Anwendung läuft lokal unter Windows und verbindet sich per SSH mit einem entfernten Linux-Server.
Dort kann sie:
- Bilddateien auf den Server hochladen
- `solve-field` mit vielen Parametern starten
- die Ausgabe live im Log anzeigen
- Ergebnisdateien im Remote-Verzeichnis anzeigen
- einzelne oder mehrere Ergebnisdateien wieder lokal herunterladen
- Serverfunktionen wie SExtractor-Prüfung und Diagnose ausführen
## Hauptfunktionen
- SSH-Verbindung zu einem Linux-Server mit Benutzername, Passwort und Port
- Upload lokaler Bilddateien per SFTP
- grafische Konfiguration fast aller wichtigen `solve-field`-Parameter
- automatische Befehlsvorschau des erzeugten `solve-field`-Aufrufs
- Unterstützung für Maßstab, RA/Dek, Suchradius, Downsampling, CPU-Limit, Tweak-Order und viele weitere Optionen
- Unterstützung für Source Extractor per `--use-source-extractor`
- automatische Berechnung von `--plot-scale` aus der lokalen Bildgröße
- serverseitige Diagnose für `solve-field`, `astrometry.cfg`, Indexdateien, SExtractor und Speicherplatz
- Remote-Ergebnis-Tab mit Dateiliste
- Mehrfachdownload ausgewählter Ergebnisdateien
- Profilspeicherung und Profilladen für komplette GUI-Einstellungen
## Unterstützte Dateitypen
Als Eingabedatei lokal:
- FITS
- FIT
- FTS
- JPG / JPEG
- PNG
- TIFF / TIF
Für die automatische Plot-Skalierung kann die Anwendung Bildgrößen direkt aus mehreren Formaten lesen, darunter FITS, PNG, JPG, GIF und BMP.
## Voraussetzungen
## Lokal unter Windows
- Python 3
- `asyncssh`
- Netzwerkzugriff auf den Linux-Server per SSH
Installation von `asyncssh`:
```bash
pip install asyncssh
```
## Auf dem Linux-Server
- Astrometry.net mit `solve-field`
- passende Indexdateien
- SSH-Zugang
Optional:
- `source-extractor`, wenn SExtractor zur Quellenerkennung genutzt werden soll
## Starten
Direkt mit Python:
```bash
python astrometry_gui.py
```
Oder unter Windows über:
```bash
astrometry_gui.bat
```
## Bedienung
## 1. SSH eintragen
Oben in der GUI:
- Host
- Port
- Benutzer
- Passwort
Mit `Verbinden & testen` kann geprüft werden, ob der Server erreichbar ist und `solve-field` vorhanden ist.
## 2. Bild auswählen und hochladen
Im Tab `Input / Output`:
- lokale Datei auswählen
- Remote-Verzeichnis festlegen
- Datei per `Hochladen` auf den Server übertragen
Nach dem Upload wird der Remote-Dateipfad automatisch als Eingabedatei gesetzt.
## 3. Solver-Parameter einstellen
Je nach Bedarf können unter anderem gesetzt werden:
- Maßstabsbereich
- RA / Dek / Radius
- CPU-Limit
- Downsampling
- Plot-Skalierung
- Tweak-Order
- Verifikation
- Invertierung
- SExtractor-Nutzung
## 4. Befehl prüfen und starten
Im Tab `Befehlsvorschau` sieht man den tatsächlich erzeugten `solve-field`-Befehl.
Mit `solve-field starten` wird der Befehl auf dem Server ausgeführt.
## Tabs im Überblick
## Input / Output
- lokale Bilddatei
- Remote-Verzeichnis
- Upload
- Eingabedatei auf dem Server
- Ausgabeoptionen wie `--wcs`, `--corr`, `--rdls`, `--match`, `--new-fits`
## Maßstab & Position
- `--scale-low`
- `--scale-high`
- `--scale`
- `--scale-err`
- `--ra`
- `--dec`
- `--radius`
## Solver-Parameter
- `--depth`
- `--cpulimit`
- `--plot-scale`
- `--downsample`
- `--tweak-order`
- `--odds-*`
- `--objs`
- `--pixel-error`
- zahlreiche Flags wie `--no-plots`, `--invert`, `--no-verify`
Zusätzlich gibt es einen Plot-Helfer:
- automatische Berechnung von `plot-scale`
- Zielkantenlänge einstellbar
## Erweitert
- manuelle Indexdateien
- zusätzliche freie Kommandozeilenargumente
## Befehlsvorschau
- zeigt den generierten Befehl
- kann aktualisiert werden
- kann in die Zwischenablage kopiert werden
## Server Setup
- SExtractor prüfen
- SExtractor installieren
- Server-Diagnose ausführen
## Remote-Ergebnisse
- zeigt Dateien im aktuellen Remote-Verzeichnis
- lädt den Inhalt beim Öffnen des Tabs automatisch neu
- erlaubt Auswahl und Download von einer oder mehreren Dateien
## Remote-Ergebnisse herunterladen
Im Tab `Remote-Ergebnisse`:
- eine oder mehrere Dateien markieren
- `Herunterladen` klicken
- Zielordner auswählen
Standardmäßig wird als Zielordner der Ordner der ursprünglich lokal ausgewählten Bilddatei vorgeschlagen.
## Remote-Verzeichnis leeren
Im Tab `Input / Output` gibt es die Funktion:
- `Remote-Inhalt löschen`
Dabei wird nur der Inhalt des Remote-Verzeichnisses gelöscht, nicht das Verzeichnis selbst.
## Profile speichern und laden
Über die Menühleiste:
- `Profil -> Profil speichern…`
- `Profil -> Profil laden…`
Gespeichert werden:
- SSH-Daten
- Solver-Parameter
- Ein-/Ausgabe-Einstellungen
- Zusatzargumente
- Indexliste
Das Passwort wird nicht im Klartext gespeichert, sondern mit einer einfachen internen Verschlüsselung abgelegt.
## Typischer Workflow
1. Serververbindung eintragen
2. Lokales Bild auswählen
3. Datei hochladen
4. Maßstab und Solver-Parameter setzen
5. Optional SExtractor aktivieren
6. `solve-field` starten
7. Ergebnisse im Tab `Remote-Ergebnisse` prüfen
8. Gewünschte Dateien lokal herunterladen
## Hinweise
- Die GUI baut nur den Befehl und führt ihn per SSH aus. Die eigentliche Astrometrie läuft vollständig auf dem Server.
- Die installierte Astrometry-Version auf dem Server bestimmt, welche Optionen tatsächlich verfügbar sind.
- Für SExtractor muss auf dem Server in der Regel `source-extractor` installiert sein.
- Für gute Ergebnisse müssen die passenden Indexdateien serverseitig vorhanden und in `astrometry.cfg` korrekt eingebunden sein.
## Dateien im Projekt
- [astrometry_gui.py](C:/Users/ich/source/repos/Solve-Field-GUI/astrometry_gui.py)
- [astrometry_gui.bat](C:/Users/ich/source/repos/Solve-Field-GUI/astrometry_gui.bat)
## Lizenz / Nutzung
Dieses Repository enthält eine GUI zur Bedienung von Astrometry.net. Die Lizenz und Bedingungen von Astrometry.net sowie von eventuell installierten Serverpaketen gelten zusätzlich zu diesem Projekt.
Binary file not shown.
+92 -11
View File
@@ -11,6 +11,7 @@ import asyncio
import base64 import base64
import json import json
import tkinter as tk import tkinter as tk
import tkinter.font as tkfont
from tkinter import ttk, filedialog, messagebox, scrolledtext from tkinter import ttk, filedialog, messagebox, scrolledtext
import threading import threading
import os import os
@@ -26,6 +27,9 @@ except ImportError:
HAS_ASYNCSSH = False HAS_ASYNCSSH = False
SSH_CREDENTIALS_JSON = "ssh_zugangsdaten.json"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Tooltip # Tooltip
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -215,6 +219,7 @@ class AstrometryGUI:
self.v_port = tk.IntVar(value=22) self.v_port = tk.IntVar(value=22)
self.v_user = tk.StringVar(value="eskimue") self.v_user = tk.StringVar(value="eskimue")
self.v_pass = tk.StringVar(value="Preside") self.v_pass = tk.StringVar(value="Preside")
self._load_startup_credentials()
self.v_local_file = tk.StringVar() self.v_local_file = tk.StringVar()
self.v_local_file.trace_add("write", self._on_local_file_changed) self.v_local_file.trace_add("write", self._on_local_file_changed)
@@ -275,6 +280,27 @@ class AstrometryGUI:
self.v_extra = tk.StringVar() self.v_extra = tk.StringVar()
self.results_files: dict[str, dict] = {} self.results_files: dict[str, dict] = {}
def _load_startup_credentials(self):
path = os.path.join(os.getcwd(), SSH_CREDENTIALS_JSON)
if not os.path.isfile(path):
return
try:
self._load_credentials_from_file(path)
except Exception:
pass
def _load_credentials_from_file(self, path: str):
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
if "host" in data:
self.v_host.set(str(data["host"]))
if "port" in data:
self.v_port.set(int(data["port"]))
if "user" in data:
self.v_user.set(str(data["user"]))
if "password" in data:
self.v_pass.set(str(data["password"]))
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# UI aufbauen # UI aufbauen
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
@@ -345,6 +371,8 @@ class AstrometryGUI:
menubar = tk.Menu(self.root) menubar = tk.Menu(self.root)
profile_menu = tk.Menu(menubar, tearoff=False) profile_menu = tk.Menu(menubar, tearoff=False)
profile_menu.add_command(label="Verbindung laden…", command=self._menu_load_credentials)
profile_menu.add_separator()
profile_menu.add_command(label="Profil speichern…", command=self._save_profile) profile_menu.add_command(label="Profil speichern…", command=self._save_profile)
profile_menu.add_command(label="Profil laden…", command=self._load_profile) profile_menu.add_command(label="Profil laden…", command=self._load_profile)
menubar.add_cascade(label="Profil", menu=profile_menu) menubar.add_cascade(label="Profil", menu=profile_menu)
@@ -399,6 +427,9 @@ class AstrometryGUI:
"Öffnet einen Datei-Dialog um die lokale Astro-Bild-Datei auszuwählen." "Öffnet einen Datei-Dialog um die lokale Astro-Bild-Datei auszuwählen."
).grid(row=0, column=2, padx=4) ).grid(row=0, column=2, padx=4)
self.local_image_size_label = ttk.Label(uf, text="Größe: —", foreground="gray")
self.local_image_size_label.grid(row=0, column=3, sticky=tk.W, padx=4, pady=3)
ttk.Label(uf, text="Remote-Verzeichnis:").grid(row=1, column=0, sticky=tk.W, padx=4, pady=3) ttk.Label(uf, text="Remote-Verzeichnis:").grid(row=1, column=0, sticky=tk.W, padx=4, pady=3)
tip(ttk.Entry(uf, textvariable=self.v_remote_dir), tip(ttk.Entry(uf, textvariable=self.v_remote_dir),
"Verzeichnis auf dem Server wohin die Datei hochgeladen wird.\n" "Verzeichnis auf dem Server wohin die Datei hochgeladen wird.\n"
@@ -772,6 +803,9 @@ class AstrometryGUI:
tip(ttk.Button(head, text="Aktualisieren", command=self._refresh_results_tab), tip(ttk.Button(head, text="Aktualisieren", command=self._refresh_results_tab),
"Liest den Inhalt des aktuell eingestellten Remote-Verzeichnisses erneut vom Server ein." "Liest den Inhalt des aktuell eingestellten Remote-Verzeichnisses erneut vom Server ein."
).pack(side=tk.RIGHT, padx=4) ).pack(side=tk.RIGHT, padx=4)
tip(ttk.Button(head, text="Remote-Inhalt löschen", command=self._delete_remote_dir),
"Löscht den Inhalt des aktuell eingestellten Remote-Verzeichnisses auf dem Server."
).pack(side=tk.RIGHT, padx=4)
tip(ttk.Button(head, text="Herunterladen", command=self._download_selected_result), tip(ttk.Button(head, text="Herunterladen", command=self._download_selected_result),
"Lädt die ausgewählte Remote-Datei per SFTP herunter." "Lädt die ausgewählte Remote-Datei per SFTP herunter."
).pack(side=tk.RIGHT, padx=4) ).pack(side=tk.RIGHT, padx=4)
@@ -780,7 +814,7 @@ class AstrometryGUI:
self.results_info.pack(anchor=tk.W, pady=(0, 4)) self.results_info.pack(anchor=tk.W, pady=(0, 4))
cols = ("name", "size", "remote") cols = ("name", "size", "remote")
self.results_tree = ttk.Treeview(parent, columns=cols, show="headings", height=16) self.results_tree = ttk.Treeview(parent, columns=cols, show="headings", height=16, selectmode="extended")
self.results_tree.heading("name", text="Datei") self.results_tree.heading("name", text="Datei")
self.results_tree.heading("size", text="Größe") self.results_tree.heading("size", text="Größe")
self.results_tree.heading("remote", text="Remote-Pfad") self.results_tree.heading("remote", text="Remote-Pfad")
@@ -1060,6 +1094,20 @@ class AstrometryGUI:
entry["remote"], entry["remote"],
), ),
) )
self._autosize_results_columns()
def _autosize_results_columns(self):
body_font = tkfont.nametofont("TkDefaultFont")
head_font = tkfont.nametofont("TkHeadingFont")
padding = 24
for col in ("name", "size", "remote"):
header = self.results_tree.heading(col, "text") or col
width = head_font.measure(header) + padding
for item_id in self.results_tree.get_children():
value = self.results_tree.set(item_id, col)
width = max(width, body_font.measure(str(value)) + padding)
self.results_tree.column(col, width=width)
def _clear_wait_cursor(self): def _clear_wait_cursor(self):
self.root.config(cursor="") self.root.config(cursor="")
@@ -1084,13 +1132,16 @@ class AstrometryGUI:
def _download_selected_result(self): def _download_selected_result(self):
selection = self.results_tree.selection() selection = self.results_tree.selection()
if not selection: if not selection:
messagebox.showwarning("Keine Datei ausgewählt", "Bitte zuerst eine Datei im Ergebnis-Tab auswählen.") messagebox.showwarning("Keine Datei ausgewählt", "Bitte zuerst eine oder mehrere Dateien im Ergebnis-Tab auswählen.")
return return
entry = self.results_files.get(selection[0]) entries = []
if not entry: for item_id in selection:
messagebox.showwarning("Datei fehlt", "Die ausgewählte Datei konnte nicht gefunden werden.") entry = self.results_files.get(item_id)
if entry:
entries.append(entry)
if not entries:
messagebox.showwarning("Datei fehlt", "Die ausgewählten Dateien konnten nicht gefunden werden.")
return return
suggested = os.path.basename(entry["remote"])
source_dir = os.path.dirname(self.v_local_file.get().strip()) if self.v_local_file.get().strip() else "" source_dir = os.path.dirname(self.v_local_file.get().strip()) if self.v_local_file.get().strip() else ""
initial_dir = source_dir if source_dir and os.path.isdir(source_dir) else os.getcwd() initial_dir = source_dir if source_dir and os.path.isdir(source_dir) else os.getcwd()
local_dir = filedialog.askdirectory( local_dir = filedialog.askdirectory(
@@ -1099,20 +1150,25 @@ class AstrometryGUI:
) )
if not local_dir: if not local_dir:
return return
downloads = []
for entry in entries:
suggested = os.path.basename(entry["remote"])
local_path = os.path.normpath(os.path.join(local_dir, suggested)) local_path = os.path.normpath(os.path.join(local_dir, suggested))
self._submit(self._async_download_result(entry["remote"], local_path)) downloads.append((entry["remote"], local_path))
self._submit(self._async_download_results(downloads))
async def _async_download_result(self, remote_path: str, local_path: str): async def _async_download_results(self, downloads: list[tuple[str, str]]):
self._log(f"[INFO] Lade Ergebnis herunter: {remote_path} → {local_path}\n", "info")
try: try:
os.makedirs(os.path.dirname(local_path), exist_ok=True)
ssh = self._make_ssh() ssh = self._make_ssh()
await ssh.connect() await ssh.connect()
for remote_path, local_path in downloads:
self._log(f"[INFO] Lade Ergebnis herunter: {remote_path} → {local_path}\n", "info")
os.makedirs(os.path.dirname(local_path), exist_ok=True)
await ssh.download(remote_path, local_path) await ssh.download(remote_path, local_path)
await ssh.close()
if not os.path.isfile(local_path): if not os.path.isfile(local_path):
raise FileNotFoundError(f"Datei nach Download nicht gefunden: {local_path}") raise FileNotFoundError(f"Datei nach Download nicht gefunden: {local_path}")
self._log(f"[OK] Download abgeschlossen: {local_path}\n", "ok") self._log(f"[OK] Download abgeschlossen: {local_path}\n", "ok")
await ssh.close()
except Exception as exc: except Exception as exc:
self._log(f"[FEHLER] Download: {exc}\n", "error") self._log(f"[FEHLER] Download: {exc}\n", "error")
@@ -1159,6 +1215,15 @@ class AstrometryGUI:
self.v_new_fits.set(auto_path) self.v_new_fits.set(auto_path)
def _on_local_file_changed(self, *_): def _on_local_file_changed(self, *_):
path = self.v_local_file.get().strip()
if not path or not os.path.isfile(path):
self.local_image_size_label.config(text="Größe: —")
else:
try:
width, height = self._read_image_size(path)
self.local_image_size_label.config(text=f"Größe: {width} × {height}")
except Exception:
self.local_image_size_label.config(text="Größe: unbekannt")
if self.v_plot_scale_auto.get(): if self.v_plot_scale_auto.get():
self._recalculate_plot_scale() self._recalculate_plot_scale()
@@ -1285,6 +1350,8 @@ class AstrometryGUI:
) )
if path: if path:
self.v_local_file.set(path) self.v_local_file.set(path)
if messagebox.askyesno("Datei hochladen", "Soll die ausgewählte Datei jetzt direkt auf den Server hochgeladen werden?"):
self._upload()
def _refresh_cmd(self): def _refresh_cmd(self):
cmd = self._build_command() cmd = self._build_command()
@@ -1385,6 +1452,20 @@ class AstrometryGUI:
except Exception as exc: except Exception as exc:
messagebox.showerror("Profil laden", f"Laden fehlgeschlagen:\n{exc}") messagebox.showerror("Profil laden", f"Laden fehlgeschlagen:\n{exc}")
def _menu_load_credentials(self):
path = filedialog.askopenfilename(
title="Zugangsdaten laden",
initialdir=os.getcwd(),
filetypes=[("JSON-Dateien", "*.json"), ("Alle Dateien", "*.*")],
)
if not path:
return
try:
self._load_credentials_from_file(path)
self._log(f"[OK] Zugangsdaten geladen: {path}\n", "ok")
except Exception as exc:
messagebox.showerror("Zugangsdaten laden", f"Laden fehlgeschlagen:\n{exc}")
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Log (thread-sicher via Queue) # Log (thread-sicher via Queue)
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
+6
View File
@@ -0,0 +1,6 @@
{
"host": "10.42.44.25",
"port": 22,
"user": "eskimue",
"password": "Preside"
}