From 58ab0d89251cb2142f6e792f2184f3dc886da1dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sun, 28 Jun 2026 19:48:42 +0200 Subject: [PATCH 1/3] Dies und das --- __pycache__/astrometry_gui.cpython-312.pyc | Bin 0 -> 88800 bytes astrometry_gui.py | 377 ++++++++++++++++++++- 2 files changed, 371 insertions(+), 6 deletions(-) create mode 100644 __pycache__/astrometry_gui.cpython-312.pyc diff --git a/__pycache__/astrometry_gui.cpython-312.pyc b/__pycache__/astrometry_gui.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..44e363e6bbd6252ebd2830484875d964f62ea538 GIT binary patch literal 88800 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", lambda _e: self._download_selected_result()) + # ----------------------------------------------------------------------- # Befehl bauen # ----------------------------------------------------------------------- @@ -754,6 +840,7 @@ class AstrometryGUI: add("--depth", self.v_depth) add("--cpulimit", self.v_cpulimit) + add("--plot-scale", self.v_plot_scale) add("--downsample", self.v_downsample) add("--odds-to-solve", self.v_odds_solve) add("--odds-to-reject", self.v_odds_reject) @@ -843,6 +930,163 @@ class AstrometryGUI: finally: self.root.after(0, lambda: self.upload_btn.config(state=tk.NORMAL)) + def _delete_remote_dir(self): + remote_dir = self.v_remote_dir.get().strip() + if not remote_dir: + messagebox.showwarning("Remote-Verzeichnis fehlt", "Bitte ein Remote-Verzeichnis angeben.") + return + if remote_dir == "/": + messagebox.showerror("Ung\u00fcltiges Verzeichnis", "Das Root-Verzeichnis '/' darf nicht gel\u00f6scht werden.") + return + if not messagebox.askyesno( + "Remote-Inhalt l\u00f6schen", + f"Soll nur der Inhalt dieses Verzeichnisses auf dem Server rekursiv gel\u00f6scht werden?\n\n{remote_dir}" + ): + return + self._submit(self._async_delete_remote_dir(remote_dir)) + + async def _async_delete_remote_dir(self, remote_dir: str): + quoted_dir = shlex.quote(remote_dir) + cmd = ( + f"dir={quoted_dir}; " + "if [ -z \"$dir\" ] || [ \"$dir\" = \"/\" ]; then " + "echo '[FEHLER] Unsicheres Remote-Verzeichnis.'; exit 1; " + "fi; " + "if [ ! -e \"$dir\" ]; then " + "echo '[INFO] Remote-Verzeichnis existiert nicht.'; exit 0; " + "fi; " + "find \"$dir\" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +; " + "echo \"[OK] Remote-Inhalt gel\u00f6scht: $dir\"" + ) + self._log(f"[INFO] L\u00f6sche Remote-Inhalt: {remote_dir}\n", "info") + try: + ssh = self._make_ssh() + await ssh.connect() + await ssh.exec_stream(cmd, self._log) + await ssh.close() + except Exception as exc: + self._log(f"[FEHLER] Remote-Inhalt l\u00f6schen: {exc}\n", "error") + + def _show_results_tab(self): + self.nb.select(self.results_tab) + + def _refresh_results_tab(self): + remote_dir = self.v_remote_dir.get().strip() + self._submit(self._async_refresh_results_tab(remote_dir)) + + async def _async_refresh_results_tab(self, remote_dir: str, returncode: int | None = None): + if not remote_dir: + self.root.after(0, lambda: self._set_results_info("Kein Remote-Verzeichnis gesetzt.")) + self.root.after(0, lambda: self._set_results_items([])) + return + + quoted_dir = shlex.quote(remote_dir) + cmd = ( + f"dir={quoted_dir}; " + "if [ ! -e \"$dir\" ]; then " + "echo 'Remote-Verzeichnis existiert nicht.'; exit 0; " + "fi; " + "find \"$dir\" -maxdepth 2 -type f -printf '%s\t%P\t%p\\n' | sort" + ) + info = f"Remote-Verzeichnis: {remote_dir}" + if returncode is not None: + info += f" | solve-field Exit-Code: {returncode}" + self.root.after(0, lambda: self._set_results_info(info)) + self.root.after(0, lambda: self._set_results_items([])) + + entries: list[dict[str, str]] = [] + + def _capture(line: str): + stripped = line.strip() + if not stripped or stripped.startswith("[Prozess beendet"): + return + if "\t" not in stripped: + return + size, rel_path, full_path = stripped.split("\t", 2) + entries.append({ + "name": rel_path or os.path.basename(full_path), + "size": size, + "remote": full_path, + }) + + try: + ssh = self._make_ssh() + await ssh.connect() + await ssh.exec_stream(cmd, _capture) + await ssh.close() + self.root.after(0, lambda: self._set_results_items(entries)) + except Exception as exc: + self.root.after(0, lambda: self._set_results_info(f"[FEHLER] {exc}")) + self.root.after(0, lambda: self._set_results_items([])) + + def _set_results_info(self, text: str): + self.results_info.config(text=text, foreground="gray") + + def _set_results_items(self, entries: list[dict[str, str]]): + self.results_files = {} + for item in self.results_tree.get_children(): + self.results_tree.delete(item) + for i, entry in enumerate(entries): + item_id = f"result_{i}" + self.results_files[item_id] = entry + self.results_tree.insert( + "", + tk.END, + iid=item_id, + values=( + entry["name"], + self._format_size(entry["size"]), + entry["remote"], + ), + ) + + def _format_size(self, size_text: str) -> str: + try: + size = int(size_text) + except Exception: + return size_text + units = ["B", "KB", "MB", "GB", "TB"] + value = float(size) + for unit in units: + if value < 1024 or unit == units[-1]: + if unit == "B": + return f"{int(value)} {unit}" + return f"{value:.1f} {unit}" + value /= 1024 + return size_text + + def _download_selected_result(self): + selection = self.results_tree.selection() + if not selection: + messagebox.showwarning("Keine Datei ausgewählt", "Bitte zuerst eine Datei im Ergebnis-Tab auswählen.") + return + entry = self.results_files.get(selection[0]) + if not entry: + messagebox.showwarning("Datei fehlt", "Die ausgewählte Datei konnte nicht gefunden werden.") + return + suggested = os.path.basename(entry["remote"]) + local_dir = filedialog.askdirectory( + title="Zielordner für Download auswählen", + ) + if not local_dir: + return + local_path = os.path.normpath(os.path.join(local_dir, suggested)) + self._submit(self._async_download_result(entry["remote"], local_path)) + + async def _async_download_result(self, remote_path: str, local_path: str): + self._log(f"[INFO] Lade Ergebnis herunter: {remote_path} → {local_path}\n", "info") + try: + os.makedirs(os.path.dirname(local_path), exist_ok=True) + ssh = self._make_ssh() + await ssh.connect() + await ssh.download(remote_path, local_path) + await ssh.close() + if not os.path.isfile(local_path): + raise FileNotFoundError(f"Datei nach Download nicht gefunden: {local_path}") + self._log(f"[OK] Download abgeschlossen: {local_path}\n", "ok") + except Exception as exc: + self._log(f"[FEHLER] Download: {exc}\n", "error") + def _run(self): if not self.v_input_file.get(): messagebox.showwarning("Keine Eingabedatei", "Bitte einen Dateipfad angeben.") @@ -855,10 +1099,14 @@ class AstrometryGUI: try: ssh = self._make_ssh() await ssh.connect() - await ssh.exec_stream(cmd, self._log) + rc = await ssh.exec_stream(cmd, self._log) await ssh.close() + self._current_task = None + self.root.after(0, self._show_results_tab) + await self._async_refresh_results_tab(self.v_remote_dir.get().strip(), rc) except Exception as exc: self._log(f"[FEHLER] {exc}\n", "error") + self._current_task = None def _kill(self): if self._current_task: @@ -882,6 +1130,123 @@ class AstrometryGUI: if not current or (current.endswith(".fits") and current.startswith(out_dir)): self.v_new_fits.set(auto_path) + def _on_local_file_changed(self, *_): + if self.v_plot_scale_auto.get(): + self._recalculate_plot_scale() + + def _on_plot_scale_settings_changed(self, *_): + if self.v_plot_scale_auto.get(): + self._recalculate_plot_scale() + + def _recalculate_plot_scale(self): + path = self.v_local_file.get().strip() + if not path or not os.path.isfile(path): + return + try: + target = float(self.v_plot_target_long_edge.get().replace(",", ".")) + if target <= 0: + return + width, height = self._read_image_size(path) + long_edge = max(width, height) + if long_edge <= 0: + return + scale = target / long_edge + self.v_plot_scale.set(f"{scale:.3f}".rstrip("0").rstrip(".")) + except Exception: + return + + def _read_image_size(self, path: str) -> tuple[int, int]: + ext = os.path.splitext(path)[1].lower() + if ext in (".fits", ".fit", ".fts"): + return self._read_fits_size(path) + if ext == ".png": + return self._read_png_size(path) + if ext in (".jpg", ".jpeg"): + return self._read_jpeg_size(path) + if ext == ".gif": + return self._read_gif_size(path) + if ext == ".bmp": + return self._read_bmp_size(path) + raise ValueError("Nicht unterstütztes Bildformat") + + def _read_fits_size(self, path: str) -> tuple[int, int]: + width = height = None + with open(path, "rb") as f: + while True: + block = f.read(2880) + if not block: + break + for i in range(0, len(block), 80): + card = block[i:i + 80].decode("ascii", errors="ignore") + key = card[:8].strip() + if key == "END": + if width and height: + return width, height + break + if "=" not in card: + continue + value = card[10:30].split("/")[0].strip() + if key == "NAXIS1": + width = int(value) + elif key == "NAXIS2": + height = int(value) + raise ValueError("FITS-Bildgröße nicht lesbar") + + def _read_png_size(self, path: str) -> tuple[int, int]: + with open(path, "rb") as f: + header = f.read(24) + if header[:8] != b"\x89PNG\r\n\x1a\n": + raise ValueError("Ungültige PNG-Datei") + return struct.unpack(">II", header[16:24]) + + def _read_gif_size(self, path: str) -> tuple[int, int]: + with open(path, "rb") as f: + header = f.read(10) + if header[:6] not in (b"GIF87a", b"GIF89a"): + raise ValueError("Ungültige GIF-Datei") + return struct.unpack(" tuple[int, int]: + with open(path, "rb") as f: + header = f.read(26) + if header[:2] != b"BM": + raise ValueError("Ungültige BMP-Datei") + width, height = struct.unpack(" tuple[int, int]: + with open(path, "rb") as f: + if f.read(2) != b"\xff\xd8": + raise ValueError("Ungültige JPEG-Datei") + while True: + marker_prefix = f.read(1) + if not marker_prefix: + break + if marker_prefix != b"\xff": + continue + marker = f.read(1) + while marker == b"\xff": + marker = f.read(1) + if marker in (b"\xd8", b"\xd9"): + continue + size_bytes = f.read(2) + if len(size_bytes) != 2: + break + segment_size = struct.unpack(">H", size_bytes)[0] + if marker in ( + b"\xc0", b"\xc1", b"\xc2", b"\xc3", + b"\xc5", b"\xc6", b"\xc7", + b"\xc9", b"\xca", b"\xcb", + b"\xcd", b"\xce", b"\xcf", + ): + data = f.read(5) + if len(data) != 5: + break + height, width = struct.unpack(">HH", data[1:5]) + return width, height + f.seek(segment_size - 2, os.SEEK_CUR) + raise ValueError("JPEG-Bildgröße nicht lesbar") + def _browse_local(self): path = filedialog.askopenfilename( title="Astro-Bild auswählen", From a8b6fa2fa661040a1be07334a4be76e56ec69095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sun, 28 Jun 2026 20:12:56 +0200 Subject: [PATCH 2/3] Finale I --- __pycache__/astrometry_gui.cpython-312.pyc | Bin 88800 -> 97192 bytes astrometry_gui.py | 132 +++++++++++++++++++-- profiles/HU 0815.json | 60 ++++++++++ profiles/Test.json | 60 ++++++++++ 4 files changed, 245 insertions(+), 7 deletions(-) create mode 100644 profiles/HU 0815.json create mode 100644 profiles/Test.json diff --git a/__pycache__/astrometry_gui.cpython-312.pyc b/__pycache__/astrometry_gui.cpython-312.pyc index 44e363e6bbd6252ebd2830484875d964f62ea538..84a327f48c0b120c7a4398e74e65f1f8185f99f9 100644 GIT binary patch delta 16902 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 delta 10413 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 diff --git a/astrometry_gui.py b/astrometry_gui.py index 5f552be..fa15cd1 100644 --- a/astrometry_gui.py +++ b/astrometry_gui.py @@ -8,6 +8,8 @@ Abhängigkeiten: """ import asyncio +import base64 +import json import tkinter as tk from tkinter import ttk, filedialog, messagebox, scrolledtext import threading @@ -180,7 +182,7 @@ class AstrometryGUI: def __init__(self, root: tk.Tk): self.root = root - self.root.title("Astrometry.net — solve-field GUI") + self.root.title("Astrometry.net — solve-field GUI vom Astronomiemuseum") self.root.geometry("1000x840") self.root.minsize(800, 620) @@ -278,12 +280,15 @@ class AstrometryGUI: # ----------------------------------------------------------------------- def _build_ui(self): + self._build_menu() + top = ttk.Frame(self.root, padding=4) top.pack(fill=tk.X) self._build_conn_bar(top) self.nb = ttk.Notebook(self.root) self.nb.pack(fill=tk.BOTH, expand=True, padx=6, pady=(0, 4)) + self.nb.bind("<>", self._on_tab_changed) for title, builder in [ ("Input / Output", self._tab_io), @@ -336,6 +341,20 @@ class AstrometryGUI: self._current_task: asyncio.Future | None = None + def _build_menu(self): + menubar = tk.Menu(self.root) + + profile_menu = tk.Menu(menubar, tearoff=False) + profile_menu.add_command(label="Profil speichern…", command=self._save_profile) + profile_menu.add_command(label="Profil laden…", command=self._load_profile) + menubar.add_cascade(label="Profil", menu=profile_menu) + + self.root.config(menu=menubar) + + def _on_tab_changed(self, _event=None): + if self.nb.select() == str(self.results_tab): + self._refresh_results_tab() + # ----------------------------------------------------------------------- # Verbindungs-Leiste # ----------------------------------------------------------------------- @@ -605,7 +624,7 @@ class AstrometryGUI: ("Weitermachen (--continue)", self.v_continue, "Überspringt bereits gelöste Bilder (prüft ob .solved-Datei existiert).\n" "Ideal für unterbrochene Batch-Verarbeitungen."), - ("SExtractor nutzen (--use-sextractor)", self.v_sextractor, + ("SExtractor nutzen (--use-source-extractor)", self.v_sextractor, "Verwendet SExtractor statt des eingebauten Quellenerkennungs-Algorithmus.\n" "Muss auf dem Server installiert sein. Oft besser bei schwierigen Bildern."), ("Als FITS-Bild behandeln (--fits-image)", self.v_fits_image, @@ -799,7 +818,7 @@ class AstrometryGUI: flag("--resort", self.v_resort) flag("--continue", self.v_continue) flag("--no-delete-temp", self.v_no_del_temp) - flag("--use-sextractor", self.v_sextractor) + flag("--use-source-extractor", self.v_sextractor) flag("--fits-image", self.v_fits_image) flag("--no-verify", self.v_no_verify) flag("--just-augment", self.v_just_augment) @@ -967,11 +986,11 @@ class AstrometryGUI: except Exception as exc: self._log(f"[FEHLER] Remote-Inhalt l\u00f6schen: {exc}\n", "error") - def _show_results_tab(self): - self.nb.select(self.results_tab) - def _refresh_results_tab(self): remote_dir = self.v_remote_dir.get().strip() + self.root.config(cursor="watch") + self.nb.config(cursor="watch") + self.results_tree.config(cursor="watch") self._submit(self._async_refresh_results_tab(remote_dir)) async def _async_refresh_results_tab(self, remote_dir: str, returncode: int | None = None): @@ -1015,9 +1034,11 @@ class AstrometryGUI: await ssh.exec_stream(cmd, _capture) await ssh.close() self.root.after(0, lambda: self._set_results_items(entries)) + self.root.after(0, self._clear_wait_cursor) except Exception as exc: self.root.after(0, lambda: self._set_results_info(f"[FEHLER] {exc}")) self.root.after(0, lambda: self._set_results_items([])) + self.root.after(0, self._clear_wait_cursor) def _set_results_info(self, text: str): self.results_info.config(text=text, foreground="gray") @@ -1040,6 +1061,11 @@ class AstrometryGUI: ), ) + def _clear_wait_cursor(self): + self.root.config(cursor="") + self.nb.config(cursor="") + self.results_tree.config(cursor="") + def _format_size(self, size_text: str) -> str: try: size = int(size_text) @@ -1065,8 +1091,11 @@ class AstrometryGUI: messagebox.showwarning("Datei fehlt", "Die ausgewählte Datei konnte nicht gefunden werden.") 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 "" + initial_dir = source_dir if source_dir and os.path.isdir(source_dir) else os.getcwd() local_dir = filedialog.askdirectory( title="Zielordner für Download auswählen", + initialdir=initial_dir, ) if not local_dir: return @@ -1102,7 +1131,6 @@ class AstrometryGUI: rc = await ssh.exec_stream(cmd, self._log) await ssh.close() self._current_task = None - self.root.after(0, self._show_results_tab) await self._async_refresh_results_tab(self.v_remote_dir.get().strip(), rc) except Exception as exc: self._log(f"[FEHLER] {exc}\n", "error") @@ -1267,6 +1295,96 @@ class AstrometryGUI: self.root.clipboard_clear() self.root.clipboard_append(self.cmd_text.get("1.0", tk.END).strip()) + def _encrypt_text(self, text: str) -> str: + if not text: + return "" + key = b"AstrometryGuiInternalKey-2026" + plain = text.encode("utf-8") + encrypted = bytes(b ^ key[i % len(key)] for i, b in enumerate(plain)) + return base64.b64encode(encrypted).decode("ascii") + + def _decrypt_text(self, token: str) -> str: + if not token: + return "" + key = b"AstrometryGuiInternalKey-2026" + encrypted = base64.b64decode(token.encode("ascii")) + plain = bytes(b ^ key[i % len(key)] for i, b in enumerate(encrypted)) + return plain.decode("utf-8") + + def _collect_profile_data(self) -> dict: + vars_data = {} + for name, value in self.__dict__.items(): + if name.startswith("v_") and hasattr(value, "get"): + vars_data[name] = value.get() + if "v_pass" in vars_data: + vars_data["v_pass"] = "" + return { + "format": 2, + "variables": vars_data, + "secrets": { + "v_pass_enc": self._encrypt_text(self.v_pass.get()), + }, + "idx_text": self.idx_text.get("1.0", tk.END).strip(), + } + + def _apply_profile_data(self, data: dict): + vars_data = data.get("variables", {}) + for name, value in vars_data.items(): + var = getattr(self, name, None) + if var is not None and hasattr(var, "set"): + var.set(value) + + secrets = data.get("secrets", {}) + encrypted_pass = secrets.get("v_pass_enc") or secrets.get("v_pass_dpapi") + if encrypted_pass: + try: + self.v_pass.set(self._decrypt_text(encrypted_pass)) + except Exception: + pass + + self.idx_text.delete("1.0", tk.END) + idx_text = data.get("idx_text", "") + if idx_text: + self.idx_text.insert("1.0", idx_text) + + self._refresh_cmd() + + def _save_profile(self): + profile_dir = os.path.join(os.getcwd(), "profiles") + os.makedirs(profile_dir, exist_ok=True) + path = filedialog.asksaveasfilename( + title="Profil speichern", + initialdir=profile_dir, + defaultextension=".json", + filetypes=[("JSON-Dateien", "*.json"), ("Alle Dateien", "*.*")], + ) + if not path: + return + data = self._collect_profile_data() + try: + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + self._log(f"[OK] Profil gespeichert: {path}\n", "ok") + except Exception as exc: + messagebox.showerror("Profil speichern", f"Speichern fehlgeschlagen:\n{exc}") + + def _load_profile(self): + profile_dir = os.path.join(os.getcwd(), "profiles") + path = filedialog.askopenfilename( + title="Profil laden", + initialdir=profile_dir if os.path.isdir(profile_dir) else os.getcwd(), + filetypes=[("JSON-Dateien", "*.json"), ("Alle Dateien", "*.*")], + ) + if not path: + return + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + self._apply_profile_data(data) + self._log(f"[OK] Profil geladen: {path}\n", "ok") + except Exception as exc: + messagebox.showerror("Profil laden", f"Laden fehlgeschlagen:\n{exc}") + # ----------------------------------------------------------------------- # Log (thread-sicher via Queue) # ----------------------------------------------------------------------- diff --git a/profiles/HU 0815.json b/profiles/HU 0815.json new file mode 100644 index 0000000..0f1da39 --- /dev/null +++ b/profiles/HU 0815.json @@ -0,0 +1,60 @@ +{ + "format": 2, + "variables": { + "v_host": "10.42.44.25", + "v_port": 22, + "v_user": "eskimue", + "v_pass": "", + "v_local_file": "", + "v_remote_dir": "/tmp/astrometry", + "v_input_file": "", + "v_out_prefix": "", + "v_out_dir": "/tmp/astrometry", + "v_new_fits": "", + "v_wcs": "", + "v_corr": "", + "v_rdls": "", + "v_axy": "", + "v_solved": "", + "v_match": "", + "v_scale_units": "degwidth", + "v_scale_low": "20", + "v_scale_high": "30", + "v_scale_est": "", + "v_scale_err": "", + "v_ra": "", + "v_dec": "", + "v_radius": "1.0", + "v_depth": "", + "v_cpulimit": "300", + "v_plot_scale": "", + "v_plot_scale_auto": true, + "v_plot_target_long_edge": "900", + "v_downsample": "6", + "v_tweak_order": "2", + "v_odds_solve": "", + "v_odds_reject": "", + "v_odds_tune": "", + "v_objs": "", + "v_pixel_error": "", + "v_parity": "both", + "v_overwrite": true, + "v_no_plots": false, + "v_no_tweak": false, + "v_resort": false, + "v_verbose": false, + "v_timestamp": true, + "v_no_del_temp": false, + "v_continue": false, + "v_sextractor": false, + "v_fits_image": false, + "v_no_verify": false, + "v_just_augment": false, + "v_invert": true, + "v_extra": "" + }, + "secrets": { + "v_pass_enc": "EQERAQYJAA==" + }, + "idx_text": "" +} \ No newline at end of file diff --git a/profiles/Test.json b/profiles/Test.json new file mode 100644 index 0000000..10b0bb5 --- /dev/null +++ b/profiles/Test.json @@ -0,0 +1,60 @@ +{ + "format": 2, + "variables": { + "v_host": "10.42.44.25", + "v_port": 22, + "v_user": "eskimue", + "v_pass": "", + "v_local_file": "C:/Users/ich/Cloud__Museum/Museumsentwicklung/Shop/Faksimile/2026/Heinlein Dieter/IMG_410328-1956_08_30-20000+2000-Te3-26-50.png", + "v_remote_dir": "/tmp/astrometry", + "v_input_file": "/tmp/astrometry/IMG_410328-1956_08_30-20000+2000-Te3-26-50.png", + "v_out_prefix": "", + "v_out_dir": "/tmp/astrometry", + "v_new_fits": "/tmp/astrometry/IMG_410328-1956_08_30-20000+2000-Te3-26-50.fits", + "v_wcs": "", + "v_corr": "", + "v_rdls": "", + "v_axy": "", + "v_solved": "", + "v_match": "", + "v_scale_units": "degwidth", + "v_scale_low": "20", + "v_scale_high": "30", + "v_scale_est": "", + "v_scale_err": "", + "v_ra": "", + "v_dec": "", + "v_radius": "1.0", + "v_depth": "", + "v_cpulimit": "300", + "v_plot_scale": "0.15", + "v_plot_scale_auto": true, + "v_plot_target_long_edge": "900", + "v_downsample": "6", + "v_tweak_order": "2", + "v_odds_solve": "", + "v_odds_reject": "", + "v_odds_tune": "", + "v_objs": "", + "v_pixel_error": "", + "v_parity": "both", + "v_overwrite": true, + "v_no_plots": false, + "v_no_tweak": false, + "v_resort": false, + "v_verbose": false, + "v_timestamp": true, + "v_no_del_temp": false, + "v_continue": false, + "v_sextractor": false, + "v_fits_image": false, + "v_no_verify": false, + "v_just_augment": false, + "v_invert": true, + "v_extra": "" + }, + "secrets": { + "v_pass_enc": "EQERAQYJAA==" + }, + "idx_text": "" +} \ No newline at end of file From 21d9936138859033c274ad538e1c08a0aca93f27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Mon, 29 Jun 2026 15:04:02 +0200 Subject: [PATCH 3/3] Kleinigkeiten --- AstroMac.json | 6 + README.md | 248 +++++++++++++++++++++ __pycache__/astrometry_gui.cpython-312.pyc | Bin 97192 -> 102575 bytes astrometry_gui.py | 111 +++++++-- ssh_zugangsdaten.json | 6 + 5 files changed, 356 insertions(+), 15 deletions(-) create mode 100644 AstroMac.json create mode 100644 README.md create mode 100644 ssh_zugangsdaten.json diff --git a/AstroMac.json b/AstroMac.json new file mode 100644 index 0000000..281147c --- /dev/null +++ b/AstroMac.json @@ -0,0 +1,6 @@ +{ + "host": "10.42.42.102", + "port": 22, + "user": "tm", + "password": "heute1234" +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..619e083 --- /dev/null +++ b/README.md @@ -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. diff --git a/__pycache__/astrometry_gui.cpython-312.pyc b/__pycache__/astrometry_gui.cpython-312.pyc index 84a327f48c0b120c7a4398e74e65f1f8185f99f9..70b2bbd97f7e1d8f4c3a0cb787c498eba0bfce8f 100644 GIT binary patch delta 14981 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 NF*8w*|1O2_{{c&l>wW+L diff --git a/astrometry_gui.py b/astrometry_gui.py index fa15cd1..43aadcf 100644 --- a/astrometry_gui.py +++ b/astrometry_gui.py @@ -11,6 +11,7 @@ import asyncio import base64 import json import tkinter as tk +import tkinter.font as tkfont from tkinter import ttk, filedialog, messagebox, scrolledtext import threading import os @@ -26,6 +27,9 @@ except ImportError: HAS_ASYNCSSH = False +SSH_CREDENTIALS_JSON = "ssh_zugangsdaten.json" + + # --------------------------------------------------------------------------- # Tooltip # --------------------------------------------------------------------------- @@ -215,6 +219,7 @@ class AstrometryGUI: self.v_port = tk.IntVar(value=22) self.v_user = tk.StringVar(value="eskimue") self.v_pass = tk.StringVar(value="Preside") + self._load_startup_credentials() self.v_local_file = tk.StringVar() self.v_local_file.trace_add("write", self._on_local_file_changed) @@ -275,6 +280,27 @@ class AstrometryGUI: self.v_extra = tk.StringVar() 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 # ----------------------------------------------------------------------- @@ -345,6 +371,8 @@ class AstrometryGUI: menubar = tk.Menu(self.root) 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 laden…", command=self._load_profile) 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." ).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) tip(ttk.Entry(uf, textvariable=self.v_remote_dir), "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), "Liest den Inhalt des aktuell eingestellten Remote-Verzeichnisses erneut vom Server ein." ).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), "Lädt die ausgewählte Remote-Datei per SFTP herunter." ).pack(side=tk.RIGHT, padx=4) @@ -780,7 +814,7 @@ class AstrometryGUI: self.results_info.pack(anchor=tk.W, pady=(0, 4)) 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("size", text="Größe") self.results_tree.heading("remote", text="Remote-Pfad") @@ -1060,6 +1094,20 @@ class AstrometryGUI: 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): self.root.config(cursor="") @@ -1084,13 +1132,16 @@ class AstrometryGUI: def _download_selected_result(self): selection = self.results_tree.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 - entry = self.results_files.get(selection[0]) - if not entry: - messagebox.showwarning("Datei fehlt", "Die ausgewählte Datei konnte nicht gefunden werden.") + entries = [] + for item_id in selection: + 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 - 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 "" initial_dir = source_dir if source_dir and os.path.isdir(source_dir) else os.getcwd() local_dir = filedialog.askdirectory( @@ -1099,20 +1150,25 @@ class AstrometryGUI: ) if not local_dir: return - local_path = os.path.normpath(os.path.join(local_dir, suggested)) - self._submit(self._async_download_result(entry["remote"], local_path)) + downloads = [] + for entry in entries: + suggested = os.path.basename(entry["remote"]) + local_path = os.path.normpath(os.path.join(local_dir, suggested)) + 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): - self._log(f"[INFO] Lade Ergebnis herunter: {remote_path} → {local_path}\n", "info") + async def _async_download_results(self, downloads: list[tuple[str, str]]): try: - os.makedirs(os.path.dirname(local_path), exist_ok=True) ssh = self._make_ssh() await ssh.connect() - await ssh.download(remote_path, local_path) + 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) + if not os.path.isfile(local_path): + raise FileNotFoundError(f"Datei nach Download nicht gefunden: {local_path}") + self._log(f"[OK] Download abgeschlossen: {local_path}\n", "ok") await ssh.close() - if not os.path.isfile(local_path): - raise FileNotFoundError(f"Datei nach Download nicht gefunden: {local_path}") - self._log(f"[OK] Download abgeschlossen: {local_path}\n", "ok") except Exception as exc: self._log(f"[FEHLER] Download: {exc}\n", "error") @@ -1159,6 +1215,15 @@ class AstrometryGUI: self.v_new_fits.set(auto_path) 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(): self._recalculate_plot_scale() @@ -1285,6 +1350,8 @@ class AstrometryGUI: ) if 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): cmd = self._build_command() @@ -1385,6 +1452,20 @@ class AstrometryGUI: except Exception as 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) # ----------------------------------------------------------------------- diff --git a/ssh_zugangsdaten.json b/ssh_zugangsdaten.json new file mode 100644 index 0000000..646df60 --- /dev/null +++ b/ssh_zugangsdaten.json @@ -0,0 +1,6 @@ +{ + "host": "10.42.44.25", + "port": 22, + "user": "eskimue", + "password": "Preside" +}