DIY Livestacker....

linuxer

Aktives Mitglied
Hallo Leute,
und Sorry falls das hier das falsche Unterforum ist. Falls falsch, dann bitte vom Admin an den richtigen Ort verschieben.
War mir nicht ganz klar wo es hin sollte :unsure:

Ich habe mir die letzten 2 Wochen Zeit genommen mit Hilfe ein paar KI's einen Livestacker zu Programmieren.
Nach etlichen Fehlschlägen und Neuanfängen, wechseln der KI' zwischen Gemini, ChatGPT und Deepseek kam
endlich etwas brauchbares heraus. :)

Ich bin ein Gelegenheitsprogrammierer und komme gerade mal so zurecht.
Also habe ich o.g. KI's zur Programmierung verwendet. Wenn der eine versagte kam der andere zum Zug.
Es war immer ein hin und her. Leider kam immer der Moment wo es 1 Schritt nach vorne ging und 2 wieder zurück.
Keine der Ki's war auf längere Zeit stabil und hat fehlerfrei gearbeitet.

Der eine oder andere wird der Meinung sein das es Unnötig ist weil, es ja einige Livestacker gibt.
Ja gibt es, aber ich habe kein Geld um mehrere 100€ in APP (Astro Pixel Prozessor) oder SharpCap zu investieren.

Der Livestacker von Siril funktioniert zwar aber nicht sehr stabil (zumindest nicht bei mir).
Der Livestacker in Kstars ist noch neu und noch etwas Buggy (auf mehreren meinen Rechner jedenfalls).

Außerdem ist Sharpcap nur unter Windows lauffähig, was ich in diesem Leben aber nicht mehr nutze. ;)

Also was lag näher als selber eine SW mit KI-Unterstützung zu schreiben.
Was jetzt etwas kompliziert klingt war eigentlich ziemlich einfach. Ich habe die Oberfläche in QT-Designer
erstellt und der KI dann vorgaben gemacht wie ich mir die Funktion vorstelle. Hat immer etwas gedauert aber am Ende kam dabei das heraus.

Bildschirmfoto zu 2025-09-23 14-48-50.png


Das Programm ist Relativ einfach gehalten. Es wird der Pfad zu den Fits angegeben.
Das erste Bild wird geladen.
Auf den Button Auto-Start klicken und das Stacken beginnt.
Wenn das Programm alle Bilder gestackt hat dann wird auf das nächste Bild gewartet.
Alle 2 Sekunden wird das Verzeichnis auf neuen Bilder überprüft.
Das Fenster kann vergrößert und verkleinert werden. Kann also auch auf einem kleinen Laptop dargestellt werden.
Ich/Wir haben Multicore implementiert. Das heißt es werden immer die hälfte der Kerne des System benutzt. In meinem Fall verwendet das Programm zum Stacken
2 von 4 Kerne. Das macht die ganze Sache schneller.
Ich habe das Programm zwar für meine Umgebung entwickelt (Linux, Kstars/Ekos etc.) aber wer will kann selber daran herum basteln.
Gravierende Fehler konnte ich keine mehr finden. Ist noch etwas Kosmetik zu machen, aber die Funktion ist gegeben.

Es kann HIER herunter geladen werden.
Das Programm besteht aus 3 Dateien.

Das eigentlich Programm. > astrostacker_1_4.py
Die Oberfläche. > astrostacker_deepseek_3_1.ui
Das Icon. > livestacking.png

Es müssen noch ein paar Pakete herunter geladen.
Am besten macht man das in einer Konsole mit dem Befehl.

sudo pip install PyQt5 astropy numpy scipy scikit-image watchdog matplotlib photutils

Einige der Pakete werden vermutlich bei der Installation schon installiert sein.
Ich verwende Ubuntu-Mate und musste keines der Pakete nachinstallieren.
Alle zusätzlichen Pakete können über den normalen Paket-Manager oder in der Konsole mit sudo apt install xxx installiert werden.

Ich versuche auch pip zu meiden weil pip das in einer Art Sandbox installiert.

Also wie immer an dieser Stelle. Ich übernehme keine Verantwortung falls etwas schief geht !
Aber mal ehrlich was soll schon schief gehen außer das es nicht funktioniert.

Unter Linux läuft das ohne Probleme. Unter Windows kann ich das nicht beurteilen. Mit dem OS kenne ich mich nicht so aus.
 
Aber mal ehrlich was soll schon schief gehen außer das es nicht funktioniert.
<Paranoiamodus> Ich soll eine Software installieren, die von letztlich von Unternehmen erstellt wurde, die sich nicht um Copyright scheren, und bei denen der Programmierer wahrscheinlich keine Ahnung hat, was sie neben Livestacking vielleicht noch so alles macht? </Paranoia>

Sorry, als KI verkaufte LLMs sind für mich mittlerweile ein rotes Tuch. Der Vorteil von Linux ist ja, dass es eigentlich tief in der Open-Source-Welt verankert ist und die Chance gut sind, dass sich noch irgendwer den Quellcode angeschaut hat, oder die Lizenzen der verwendeten Quellcode-Bausteine. Nichts gegen neue Software, aber wer sagt mir, dass sie nicht doch mehr macht als du planst?

Da habe ich gerne einmalig die 100€ für Sharpcap gezahlt (ja, ich habe eine Lifetime-Lizenz statt der 14 Pfund Abogebühr jährlich), an einen Menschen, der die Software aktiv weiterentwickelt. Auch wenn ich deshalb einen Windows-PC am Teleskop habe und auch nur das Bauchgefühl habe, dass ich Robin Glover vertrauen kann.

Clear Skies,
Alex
 
Hi Alex, das tolle an opensource ist das es opensource ist ;)
Du kannst Dir ja den Sourcecode ansehen. Was bei deinem Sharpcap inclusive Windows ja nicht möglich ist.
Außerdem verwendest Du eine geschlossene SW inclusive OS und machst Dir Sorgen über eine SW in die Du hineinsehen kannst ?
Nur weil ich nicht ganz verstehe wie das Programm aufgebaut ist bedeutet das noch lange nicht das ich nicht verstehe was im Programm vor sich geht.

Wer will kann es nutzen, wer nicht will, soll es bleiben lassen. Ich arbeite nicht für eine Firma und verlange nichts für die SW.
Ob er sie benutzen möchte überlasse ich jedem selbst. Ich habe das Programm "geschrieben" damit ich meine .fits während der Aufnahme Livestacken kann und stelle es jedem zur Verfügung der es nutzen möchte :)

Meine Oma sagte immer "Wer nicht will hat schon gehabt !"
In diesem Sinne ... ;)
 
Moin.
Ich finds cool. Meiner Meinung nach lernt man auch ne Menge dabei, auch wenn der Code nicht direkt aus erster Hand stammt. Man ist beim Programmieren ja doch dazu gezwungen, sich mit dem Geschreibsel zu befassen und letztendlich macht einen das nicht dümmer :)
Habe gerade mal in den Code reingeguckt und kann auf den ersten Blick keine bösartigen Funktionen erkennen.
@linuxer Tipp: Schau Dir mal an, wie man virtual environments macht. So ein requirements file wäre auch praktisch.
Gruß
André
 
Hi Andre, (sorry für das Fehlende Apostroph)
Ja man Lernt einiges dabei. Ich bin jetzt seit über 20 Jahren mit Linux unterwegs und habe es mit Pascal, C++ und Assembler und noch
einige andere Sprachen versucht und bin letztendlich bei Python hängen geblieben. Da ich eigentlich meistens (wenn ich es brauche) mir Steuerungen für Pi/Arduino/Linux schreibe ist das hier etwas ganz neues. Hat ja eigentlich nur mit Bildverarbeitung zu tun und ist interessant.
Ja ein Requirement File wäre in manchen Fällen hilfreich da hast Du recht. Meistens schreibe ich aber die SW für meine Projekte und was an "Abhängigkeiten" benötigt werden würde habe ich meistens schon auf meinen Rechnern.
Es ist aber immer schön wenn jemand der Erfahrung hat meine SW ansehen kann und Verbesserungsvorschläge macht, oder noch besser sie weiter entwickelt. Es ist für ich auch das erste mal das ich für SW eine KI hinzuziehe. Aber wieso einen Stein nehmen um einen Nagel in ein Brett zu schlagen wenn ich eine Nagelmaschine nehmen kann ;)
 
N'abend zusammen,

"selbst" programmieren mit Hilfe von KI habe ich auch schon gemacht.
Erst so wie Thoams @linuxer mit Gemini, ChatGPT und Deepseek, aber das ist recht abenteuerlich und irgendwann ging mir die Hupferei zwischen den Chatbots auf den Keks.
Bin dann umgestiegen auf cursor, da es ein LLM rein für Programmierung ist. OK, hat auch ein Zeitlimit und Inputlimit, war letztendlich aber wesentlich effektiver.

Habe auch nichts verdächtiges gefunden. OK, a weng auskennen sollte man sich schon. Bin aber nie Programmierer gewesen, komme aber aus der IT.

Tipp: Schau Dir mal an, wie man virtual environments macht.
ist bei mir auch eingerichtet. Was nicht wirklich schwer ist. Da kann man auch die KI fragen ;).

Ich kann Alex Skepsis schon verstehen. KIs sind weder intelligent noch allwissend, auch wenn es so scheinen mag. Es sind große Wahrscheinlichkeitenrechenmaschinen (tolles Wort ;)).
KIs verstehen weder den Input denen man ihnen gibt, noch den Output.
Sie rechnen Wahrscheinlichkeiten aus.
Somit muss die Ausgabe, egal zu welchem Thema, gegen gecheckt werden. Besonders wenn es um rechtliche, medizinische und verwandte Themen geht.
Themen wie Abendessen, Ausflugsziel, halt das 0815 alltägliche kann man m.E. recht gut nutzen.
Wenn es komplexer wird, prüfen, prüfen, prüfen ... ... ...

@Thomas / @linuxer, kleiner Tipp, Du kannst hier auch Code einstellen, sogar mit korrekter Einrückung.
Bildschirmfoto_2025-09-23_21-34-38.png

Wäre interessant zu lesen was Programmierspezialisten hier im Forum dazu schreiben.

Hier mal ein Beispiel-Code:
PHP:
<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <title>3D-Planeten Mittel</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
    <style>
        body { margin: 0; background: #000; overflow: hidden; }
        #container { width: 100vw; height: 100vh; position: relative; }
        #controls { position: absolute; top: 10px; left: 10px; z-index: 10; background: rgba(0,0,0,0.7); color: #fff; padding: 10px; border-radius: 8px; }
        .planet-label { position: absolute; color: #fff; background: rgba(0,0,0,0.7); padding: 2px 6px; border-radius: 4px; font-size: 12px; pointer-events: none; z-index: 5; white-space: nowrap; }
        #info-panel {
            position: absolute;
            top: 80px;
            left: 30px;
            background: rgba(0,0,0,0.92);
            color: #fff;
            padding: 18px 22px;
            border-radius: 12px;
            z-index: 2000;
            min-width: 220px;
            font-size: 15px;
            box-shadow: 0 2px 12px #0008;
            display: none;
        }
        .tooltip {
            position: absolute;
            background: rgba(30,30,30,0.98);
            color: #ffd700;
            padding: 5px 10px;
            border-radius: 6px;
            font-size: 13px;
            pointer-events: none;
            z-index: 3000;
            white-space: nowrap;
            box-shadow: 0 2px 8px #0006;
            display: none;
        }
    </style>
</head>
<body>
<div id="container">
    <div id="controls">
        <button onclick="toggleLabels()">Beschriftungen ein/aus</button>
        <button onclick="window.location.href='planet3d_web_aussen_datum.php'">Zu den äußeren Planeten/Datum</button>
        <button onclick="window.location.href='/astro/index.php'">Zurück zum Hauptmenü</button>
    </div>
    <div id="date-controls" style="position: absolute; top: 20px; right: 20px; background: rgba(0,0,0,0.8); color: #fff; padding: 16px 20px; border-radius: 10px; z-index: 1000;">
        <button id="animBtn" type="button" style="margin-bottom:12px; background: linear-gradient(45deg, #00c3ff, #005bea); color: white; padding: 8px 16px; border: none; border-radius: 5px; cursor: pointer; width:100%;" onclick="toggleAnimation()">Animation stoppen</button><br>
        <div style="margin-bottom:12px;">
            <label for="simSpeed" style="color:#ffd700;">Simulationsgeschwindigkeit:</label><br>
            <input type="range" id="simSpeed" min="0.1" max="100" step="0.1" value="1" style="width:120px; vertical-align:middle;" oninput="updateSimSpeed(this.value)">
            <input type="number" id="simSpeedNum" min="0.1" max="100" step="0.1" value="1" style="width:60px; margin-left:6px; vertical-align:middle;" oninput="updateSimSpeed(this.value)">
            <span style="font-size:12px; color:#ffd700;">Tag/Sek</span>
        </div>
        <form onsubmit="event.preventDefault(); showPositionsForDate();">
            <label for="dateInput" style="color:#ffd700;">Datum wählen:</label><br>
            <input type="date" id="dateInput" name="dateInput" style="margin: 8px 0 12px 0; padding: 6px; border-radius: 5px; border: none;">
            <button type="submit" style="background: linear-gradient(45deg, #ff6b6b, #ee5a24); color: white; padding: 8px 16px; border: none; border-radius: 5px; cursor: pointer;">Positionen anzeigen</button>
        </form>
        <div id="date-info" style="margin-top: 8px; font-size: 13px; color: #ffd700;"></div>
        <div id="sim-date-info" style="margin-top: 4px; font-size: 13px; color: #ffd700;"></div>
        <div id="explain-info" style="margin-top: 10px; font-size: 12px; color: #fff; background: rgba(0,0,0,0.7); border-radius: 8px; padding: 10px 14px; max-width: 380px;">
            <b>Begriffe:</b><br>
            <b>Aktuelle Umlaufposition:</b> Winkel des Planeten auf seiner Bahn (in Grad, 0° = Frühlingspunkt).<br>
            <b>Umlaufzeit:</b> Zeit für einen vollständigen Umlauf um die Sonne (in Tagen).<br>
            <b>Abstand zur Sonne (aktuell):</b> Tatsächlicher Abstand des Planeten zur Sonne am gewählten Datum (in Astronomischen Einheiten, 1 AU = mittlerer Abstand Erde-Sonne).<br>
            <b>Exzentrizität:</b> Maß für die Abweichung der Bahn von der Kreisform (0 = Kreis, nahe 1 = sehr langgestreckt).<br>
            <b>Perihel:</b> Kleinster Abstand des Planeten zur Sonne (in AU).<br>
            <b>Aphel:</b> Größter Abstand des Planeten zur Sonne (in AU).
        </div>
    </div>
    <div id="legend" style="position: absolute; left: 20px; bottom: 20px; background: rgba(0,0,0,0.8); color: #fff; padding: 14px 18px; border-radius: 10px; font-size: 13px; z-index: 100; min-width: 180px;">
        <b>Legende</b><br>
        <span style="color:#ffff00;">●</span> Sonne<br>
        <span style="color:#8c7853;">●</span> Merkur<br>
        <span style="color:#ffa500;">●</span> Venus<br>
        <span style="color:#0066ff;">●</span> Erde<br>
        <span style="color:#cccccc;">●</span> Mond (Erde)<br>
        <span style="color:#ff4400;">●</span> Mars<br>
        <span style="color:#aaaaaa;">●</span> Phobos (Mars)<br>
        <span style="color:#bbbbbb;">●</span> Deimos (Mars)<br>
        <span style="color:#ffffff;">●</span> Ceres (Asteroid)
    </div>
    <div id="info-panel"></div>
</div>
<div class="tooltip" id="planet-tooltip"></div>
<script>
let scene, camera, renderer, controls;
let planets = [];
let labels = [];
let labelsVisible = true;
let animationRunning = true;

const planetData = [
    { name: 'Sonne', color: 0xffff00, radius: 2, distance: 0, realAU: 0, ecc: 0, peri: 0, aphe: 0, speed: 0 },
    { name: 'Merkur', color: 0x8c7853, radius: 0.3, distance: 5, realAU: 0.387, ecc: 0.2056, peri: 0.307, aphe: 0.467, speed: 0 },
    { name: 'Venus', color: 0xffa500, radius: 0.5, distance: 7.5, realAU: 0.723, ecc: 0.0067, peri: 0.718, aphe: 0.728, speed: 0 },
    { name: 'Erde', color: 0x0066ff, radius: 0.7, distance: 10, realAU: 1.0, ecc: 0.0167, peri: 0.983, aphe: 1.017, speed: 0 },
    { name: 'Mars', color: 0xff4400, radius: 0.5, distance: 12.5, realAU: 1.524, ecc: 0.0934, peri: 1.381, aphe: 1.666, speed: 0 },
    { name: 'Ceres', color: 0xffffff, radius: 0.13, distance: 16, realAU: 2.77, ecc: 0.079, peri: 2.557, aphe: 2.983, speed: 0 },
    { name: 'Vesta', color: 0xdddddd, radius: 0.11, distance: 16.7, realAU: 2.36, ecc: 0.089, peri: 2.151, aphe: 2.572, speed: 0 },
    { name: 'Pallas', color: 0x99ccff, radius: 0.10, distance: 17.3, realAU: 2.77, ecc: 0.231, peri: 2.131, aphe: 3.412, speed: 0 },
    { name: 'Hygiea', color: 0xffeebb, radius: 0.09, distance: 18, realAU: 3.14, ecc: 0.117, peri: 2.776, aphe: 3.504, speed: 0 }
];
let moon = null;
let moonAngle = Math.random() * Math.PI * 2;
const moonDistance = 1.2;
let marsMoons = [];
let marsMoonAngles = [Math.random() * Math.PI * 2, Math.random() * Math.PI * 2];
const marsMoonData = [
    { name: 'Phobos', color: 0xaaaaaa, radius: 0.08, distance: 0.7, speed: 0.09 },
    { name: 'Deimos', color: 0xbbbbbb, radius: 0.06, distance: 1.2, speed: 0.05 }
];

// Umlaufzeiten in Tagen (NASA/JPL)
const orbitalPeriods = {
    'Merkur': 87.969,
    'Venus': 224.701,
    'Erde': 365.256,
    'Mars': 686.980,
    'Ceres': 1680.0,
    'Vesta': 1325.0,
    'Pallas': 1687.0,
    'Hygiea': 2034.0,
    'Mond': 27.321661,
    'Phobos': 0.31891,
    'Deimos': 1.263,
};
// Simulationsfaktor: wie viele Tage pro Sekunde?
let simDaysPerSecond = 1;
let lastFrameTime = Date.now();

// Add global variable to store last planet positions from backend
let lastPlanetPositions = null;
let angleTargets = [];
let requestInProgress = false;
let backendError = false;

// Add global variables for simulation date
let simDate = null;
let simDateFrac = 0; // Fractional day
let lastSimDateString = null;

// Add global cache for seasons per year
let seasonsCache = {};
let seasonsLoading = {};

// Helper: fetch seasons for a year from backend
function fetchSeasonsForYear(year, cb) {
    if (seasonsCache[year]) {
        cb(seasonsCache[year]);
        return;
    }
    if (seasonsLoading[year]) {
        // Already loading, wait and retry
        setTimeout(() => fetchSeasonsForYear(year, cb), 200);
        return;
    }
    seasonsLoading[year] = true;
    fetch('get_seasons.php?year=' + year)
        .then(r => r.json())
        .then(data => {
            seasonsCache[year] = data;
            cb(data);
        })
        .catch(() => {
            cb(null);
        });
}

// Helper: determine season from ISO dates
function getSeasonFromAstronomical(dateObj, seasons) {
    // dateObj: JS Date (UTC), seasons: {spring_equinox, summer_solstice, autumn_equinox, winter_solstice}
    if (!seasons || !seasons.spring_equinox) return null;
    const d = dateObj;
    const spring = new Date(seasons.spring_equinox);
    const summer = new Date(seasons.summer_solstice);
    const autumn = new Date(seasons.autumn_equinox);
    const winter = new Date(seasons.winter_solstice);
    if (d >= spring && d < summer) return 'Frühling';
    if (d >= summer && d < autumn) return 'Sommer';
    if (d >= autumn && d < winter) return 'Herbst';
    // Winter: von Winteranfang bis Frühlingsanfang (Jahreswechsel beachten)
    return 'Winter';
}

function getAngularSpeed(period) {
    return 2 * Math.PI / period; // rad/Tag
}

// Ergänze Bahnparameter für jeden Planeten (Werte: NASA/JPL, Einheiten: a in AU, i/Omega/omega in Grad)
const orbitalElements = {
    'Merkur':  { a: 0.387, e: 0.2056, i: 7.005, Omega: 48.331, omega: 29.124 },
    'Venus':   { a: 0.723, e: 0.0067, i: 3.394, Omega: 76.680, omega: 54.884 },
    'Erde':    { a: 1.000, e: 0.0167, i: 0.000, Omega: -11.260, omega: 114.207 },
    'Mars':    { a: 1.524, e: 0.0934, i: 1.850, Omega: 49.558, omega: 286.502 },
    'Ceres':   { a: 2.77,  e: 0.079,  i: 10.593, Omega: 80.305, omega: 73.597 },
    'Vesta':   { a: 2.36,  e: 0.089,  i: 7.140, Omega: 103.851, omega: 150.647 },
    'Pallas':  { a: 2.77,  e: 0.231,  i: 34.837, Omega: 173.087, omega: 310.048 },
    'Hygiea':  { a: 3.14,  e: 0.117,  i: 3.842, Omega: 283.646, omega: 312.346 }
};

// Hilfsfunktion: Grad zu Radiant
function deg2rad(deg) { return deg * Math.PI / 180; }

// Hilfsfunktion: Kepler-Gleichung lösen (numerisch, für Exzentrische Anomalie E)
function solveKepler(M, e, tol=1e-6) {
    let E = M;
    let dE;
    do {
        dE = (E - e * Math.sin(E) - M) / (1 - e * Math.cos(E));
        E -= dE;
    } while (Math.abs(dE) > tol);
    return E;
}

// Hilfsfunktion: 3D-Position aus Bahnparametern und wahrer Anomalie
function orbitalToEcliptic(a, e, i, Omega, omega, nu) {
    // a: große Halbachse (AU), e: Exzentrizität, i/Omega/omega/nu: Radiant
    // 1. Abstand r
    let r = a * (1 - e * e) / (1 + e * Math.cos(nu));
    // 2. Position in Bahnebene
    let x_orb = r * Math.cos(nu);
    let y_orb = r * Math.sin(nu);
    let z_orb = 0;
    // 3. Rotationen: erst um omega (z), dann i (x), dann Omega (z)
    // Rotationsmatrix (z-omega, x-i, z-Omega)
    let x1 = x_orb * Math.cos(omega) - y_orb * Math.sin(omega);
    let y1 = x_orb * Math.sin(omega) + y_orb * Math.cos(omega);
    let z1 = z_orb;
    let x2 = x1;
    let y2 = y1 * Math.cos(i) - z1 * Math.sin(i);
    let z2 = y1 * Math.sin(i) + z1 * Math.cos(i);
    let x3 = x2 * Math.cos(Omega) - y2 * Math.sin(Omega);
    let y3 = x2 * Math.sin(Omega) + y2 * Math.cos(Omega);
    let z3 = z2;
    return { x: x3, y: z3, z: y3 }; // y3 als z, z3 als y (Three.js: y=up)
}

// Funktion: Ellipsenbahn als Linie zeichnen
function createOrbitEllipse(planetName, color=0xbbbbbb) {
    const el = orbitalElements[planetName];
    if (!el) return null;
    const points = [];
    for (let f = 0; f <= 360; f += 2) {
        let nu = deg2rad(f);
        let pos = orbitalToEcliptic(el.a * 10, el.e, deg2rad(el.i), deg2rad(el.Omega), deg2rad(el.omega), nu); // *10 für Skalierung
        points.push(new THREE.Vector3(pos.x, pos.y, pos.z));
    }
    const geometry = new THREE.BufferGeometry().setFromPoints(points);
    const material = new THREE.LineBasicMaterial({ color: color, opacity: 0.5, transparent: true });
    return new THREE.Line(geometry, material);
}

function init() {
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0x000011);
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.set(0, 10, 30);
    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.getElementById('container').appendChild(renderer.domElement);
    controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    // Licht
    scene.add(new THREE.AmbientLight(0xffffff, 0.7));
    let sunLight = new THREE.PointLight(0xffffff, 2, 100);
    sunLight.position.set(0,0,0);
    scene.add(sunLight);
    // Planeten
    planetData.forEach((data, idx) => {
        let geometry = new THREE.SphereGeometry(data.radius, 32, 32);
        let material = new THREE.MeshPhongMaterial({ color: data.color });
        let mesh = new THREE.Mesh(geometry, material);
        mesh.position.x = data.distance; // wird später überschrieben
        scene.add(mesh);
        planets.push({ mesh, data, angle: Math.random() * Math.PI * 2 });
        // Orbit als Ellipse
        if (data.name !== 'Sonne' && orbitalElements[data.name]) {
            let orbit = createOrbitEllipse(data.name);
            if (orbit) scene.add(orbit);
        }
    });
    // Mond zur Erde
    let moonGeometry = new THREE.SphereGeometry(0.15, 16, 16);
    let moonMaterial = new THREE.MeshPhongMaterial({ color: 0xcccccc });
    moon = new THREE.Mesh(moonGeometry, moonMaterial);
    scene.add(moon);
    // Asteroidengürtel als Punktwolke
    const asteroidCount = 200;
    const astMin = 15;
    const astMax = 17;
    const asteroidGeometry = new THREE.BufferGeometry();
    const asteroidPositions = [];
    for (let i = 0; i < asteroidCount; i++) {
        const angle = Math.random() * Math.PI * 2;
        const radius = astMin + Math.random() * (astMax - astMin);
        const x = Math.cos(angle) * radius;
        const z = Math.sin(angle) * radius;
        const y = (Math.random() - 0.5) * 0.2;
        asteroidPositions.push(x, y, z);
    }
    asteroidGeometry.setAttribute('position', new THREE.Float32BufferAttribute(asteroidPositions, 3));
    const asteroidMaterial = new THREE.PointsMaterial({ color: 0xcccccc, size: 0.12 });
    const asteroids = new THREE.Points(asteroidGeometry, asteroidMaterial);
    scene.add(asteroids);
    // Marsmonde
    let mars = planets[4];
    marsMoons = marsMoonData.map((moon, i) => {
        let geo = new THREE.SphereGeometry(moon.radius, 12, 12);
        let mat = new THREE.MeshPhongMaterial({ color: moon.color });
        let mesh = new THREE.Mesh(geo, mat);
        scene.add(mesh);
        return { mesh, data: moon };
    });
    createLabels();
    animate();
    window.addEventListener('resize', onWindowResize);
}
function createLabels() {
    const container = document.getElementById('container');
    // Planeten-Labels
    Object.keys(planets).forEach((planetName, i) => {
        const planet = planets[planetName];
        let label = document.createElement('div');
        label.className = 'planet-label';
        label.textContent = planet.data.name;
        label.style.display = labelsVisible ? 'block' : 'none';
        container.appendChild(label);
        labels[i] = label;
    });
    // Mond-Label Erde
    let moonLabel = document.createElement('div');
    moonLabel.className = 'planet-label';
    moonLabel.textContent = 'Mond';
    moonLabel.style.display = labelsVisible ? 'block' : 'none';
    container.appendChild(moonLabel);
    labels.push(moonLabel);
    // Marsmonde-Labels
    marsMoons.forEach((m, i) => {
        let moonLabel = document.createElement('div');
        moonLabel.className = 'planet-label';
        moonLabel.textContent = marsMoonData[i].name;
        moonLabel.style.display = labelsVisible ? 'block' : 'none';
        container.appendChild(moonLabel);
        labels.push(moonLabel);
    });
}
function updateLabels() {
    if (!labelsVisible) {
        labels.forEach(label => label.style.display = 'none');
        return;
    }
    // Planeten
    Object.keys(planets).forEach((planetName, i) => {
        const planet = planets[planetName];
        let label = labels[i];
        let vector = planet.mesh.position.clone();
        vector.project(camera);
        const x = (vector.x * 0.5 + 0.5) * window.innerWidth;
        const y = (-(vector.y * 0.5) + 0.5) * window.innerHeight;
        if (vector.z < 1) {
            label.style.display = 'block';
            label.style.left = x + 'px';
            label.style.top = y + 'px';
            label.style.transform = 'translate(-50%, -50%)';
        } else {
            label.style.display = 'none';
        }
    });
    // Mond Erde
    let moonLabel = labels[Object.keys(planets).length];
    let moonVector = moon.position.clone();
    moonVector.project(camera);
    const mx = (moonVector.x * 0.5 + 0.5) * window.innerWidth;
    const my = (-(moonVector.y * 0.5) + 0.5) * window.innerHeight;
    if (moonVector.z < 1) {
        moonLabel.style.display = 'block';
        moonLabel.style.left = mx + 'px';
        moonLabel.style.top = my + 'px';
        moonLabel.style.transform = 'translate(-50%, -50%)';
    } else {
        moonLabel.style.display = 'none';
    }
    // Marsmonde
    marsMoons.forEach((m, i) => {
        let moonLabel = labels[Object.keys(planets).length + 1 + i];
        let moonVector = m.mesh.position.clone();
        moonVector.project(camera);
        const mx = (moonVector.x * 0.5 + 0.5) * window.innerWidth;
        const my = (-(moonVector.y * 0.5) + 0.5) * window.innerHeight;
        if (moonVector.z < 1) {
            moonLabel.style.display = 'block';
            moonLabel.style.left = mx + 'px';
            moonLabel.style.top = my + 'px';
            moonLabel.style.transform = 'translate(-50%, -50%)';
        } else {
            moonLabel.style.display = 'none';
        }
    });
}
function toggleLabels() {
    labelsVisible = !labelsVisible;
    updateLabels();
}
function toggleAnimation() {
    animationRunning = !animationRunning;
    document.getElementById('animBtn').textContent = animationRunning ? 'Animation stoppen' : 'Animation starten';
}
function animate() {
    requestAnimationFrame(animate);
    let now = Date.now();
    let deltaSec = (now - lastFrameTime) / 1000;
    lastFrameTime = now;
    if (animationRunning && simDate) {
        // Advance simulation date
        let daysToAdd = deltaSec * simDaysPerSecond;
        simDateFrac += daysToAdd;
        if (simDateFrac >= 1 || simDateFrac <= -1) {
            let wholeDays = Math.trunc(simDateFrac);
            simDate.setUTCDate(simDate.getUTCDate() + wholeDays);
            simDateFrac -= wholeDays;
        }
        updateSimDateDisplay();
        // Query backend if date changed by at least 1 day
        let d = new Date(simDate.getTime() + simDateFrac * 86400000);
        let yyyy = d.getUTCFullYear();
        let mm = String(d.getUTCMonth() + 1).padStart(2, '0');
        let dd = String(d.getUTCDate()).padStart(2, '0');
        let dateStr = `${yyyy}-${mm}-${dd}`;
        if (dateStr !== lastSimDateString && !requestInProgress && /^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
            requestInProgress = true;
            fetch('/astro/astronomy/planets/get_planet_positions.php?date=' + encodeURIComponent(dateStr))
                .then(r => r.json())
                .then(data => {
                    if (data.error) {
                        backendError = true;
                        requestInProgress = false;
                        return;
                    }
                    lastPlanetPositions = data.planets;
                    // Backend-Daten werden in der Hauptschleife verwendet, nicht hier direkt
                    backendError = false;
                    document.getElementById('date-info').textContent = '';
                    lastSimDateString = dateStr;
                    requestInProgress = false;
                })
                .catch(err => {
                    backendError = true;
                    document.getElementById('date-info').textContent = 'Fehler beim Laden der Planetenpositionen.';
                    requestInProgress = false;
                });
        }
        planets.forEach((p, i) => {
            if (p.data.name === 'Sonne') return;
            const el = orbitalElements[p.data.name];
            if (!el) return;
            
            // Hole aktuellen Winkel (Backend: ekliptische Länge lambda, in Grad)
            let lambda = null;
            if (lastPlanetPositions) {
                const backendMap = { 'Merkur': 'mercury', 'Venus': 'venus', 'Erde': 'earth', 'Mars': 'mars', 'Ceres': 'ceres', 'Vesta': 'vesta', 'Pallas': 'pallas', 'Hygiea': 'hygiea' };
                let key = backendMap[p.data.name];
                if (key && lastPlanetPositions[key] && typeof lastPlanetPositions[key].angle === 'number') {
                    lambda = deg2rad(-lastPlanetPositions[key].angle); // MINUS für korrekte Richtung!
                }
            }
            
            // Wenn kein Backend: lokale Simulation (KORRIGIERT: gegen Uhrzeigersinn)
            if (lambda === null) {
                // Aktualisiere Winkel basierend auf Umlaufzeit (gegen Uhrzeigersinn)
                let period = orbitalPeriods[p.data.name];
                if (period) {
                    let omega = getAngularSpeed(period);
                    let deltaAngle = omega * deltaSec * simDaysPerSecond;
                    p.angle -= deltaAngle; // MINUS für gegen Uhrzeigersinn!
                }
                lambda = p.angle;
            }
            
            // Berechne wahre Anomalie nu aus lambda, Omega, omega
            // lambda = Omega + omega + nu  =>  nu = lambda - Omega - omega
            let nu = lambda - deg2rad(el.Omega) - deg2rad(el.omega);
            // 3D-Position
            let pos = orbitalToEcliptic(el.a * 10, el.e, deg2rad(el.i), deg2rad(el.Omega), deg2rad(el.omega), nu); // *10 für Skalierung
            p.mesh.position.set(pos.x, pos.y, pos.z);
        });
        // Mond umkreist Erde (realistisch)
        let earth = planets[3];
        if (earth) {
            let moonPeriod = orbitalPeriods['Mond'];
            let moonOmega = getAngularSpeed(moonPeriod);
            let moonDelta = moonOmega * deltaSec * simDaysPerSecond;
            moonAngle -= moonDelta;
            moon.position.x = earth.mesh.position.x + Math.cos(moonAngle) * moonDistance;
            moon.position.z = earth.mesh.position.z + Math.sin(moonAngle) * moonDistance;
            moon.position.y = earth.mesh.position.y;
        }
        // Marsmonde umkreisen Mars (realistisch)
        let mars = planets[4];
        if (mars) {
            // Phobos
            let phobosPeriod = orbitalPeriods['Phobos'];
            let phobosOmega = getAngularSpeed(phobosPeriod);
            let phobosDelta = phobosOmega * deltaSec * simDaysPerSecond;
            marsMoonAngles[0] -= phobosDelta;
            marsMoons[0].mesh.position.x = mars.mesh.position.x + Math.cos(marsMoonAngles[0]) * marsMoonData[0].distance;
            marsMoons[0].mesh.position.z = mars.mesh.position.z + Math.sin(marsMoonAngles[0]) * marsMoonData[0].distance;
            marsMoons[0].mesh.position.y = mars.mesh.position.y;
            // Deimos (Korrektur: gleiche Richtung wie Phobos)
            let deimosPeriod = orbitalPeriods['Deimos'];
            let deimosOmega = getAngularSpeed(deimosPeriod);
            let deimosDelta = deimosOmega * deltaSec * simDaysPerSecond;
            marsMoonAngles[1] -= deimosDelta;
            marsMoons[1].mesh.position.x = mars.mesh.position.x + Math.cos(marsMoonAngles[1]) * marsMoonData[1].distance;
            marsMoons[1].mesh.position.z = mars.mesh.position.z + Math.sin(marsMoonAngles[1]) * marsMoonData[1].distance;
            marsMoons[1].mesh.position.y = mars.mesh.position.y;
        }
    } else {
        updateSimDateDisplay();
    }
    updateLabels();
    controls.update();
    renderer.render(scene, camera);
}
function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
}
function showPositionsForDate() {
    const date = document.getElementById('dateInput').value;
    document.getElementById('date-info').textContent = date ? `Gewähltes Datum: ${date}` : '';
    document.getElementById('sim-date-info').textContent = '';
    if (!date) return;
    // Set simulation date to selected date
    simDate = new Date(date + 'T00:00:00Z');
    simDateFrac = 0;
    lastSimDateString = date; // Setze auf das neue Datum, damit der Animations-Loop nicht sofort einen weiteren Request absetzt
    updateSimDateDisplay();
    fetch('/astro/astronomy/planets/get_planet_positions.php?date=' + encodeURIComponent(date))
        .then(r => r.json())
        .then(data => {
            if (data.error) {
                document.getElementById('date-info').textContent = 'Fehler: ' + data.error;
                return;
            }
            lastPlanetPositions = data.planets;
            // Mapping: planetData[1]=Merkur, [2]=Venus, [3]=Erde, [4]=Mars
            const planetOrder = ['mercury','venus','earth','mars'];
            planetOrder.forEach((name, i) => {
                if (data.planets && data.planets[name] && typeof data.planets[name].angle === 'number') {
                    // Umrechnung Grad -> Bogenmaß
                    const angleRad = -data.planets[name].angle * Math.PI / 180;
                    angleTargets[i+1] = angleRad; // Setze Zielwinkel
                    planets[i+1].angle = angleRad; // Sofort setzen
                    planets[i+1].mesh.position.x = Math.cos(angleRad) * planets[i+1].data.distance;
                    planets[i+1].mesh.position.z = Math.sin(angleRad) * planets[i+1].data.distance;
                }
            });
            // Info anzeigen
            document.getElementById('date-info').textContent = `Gewähltes Datum: ${data.date}`;
            updateSimDateDisplay();
            // Visueller Hinweis
            let info = document.createElement('div');
            info.id = 'position-update-info';
            info.style.position = 'absolute';
            info.style.top = '60px';
            info.style.right = '40px';
            info.style.background = 'rgba(0,180,0,0.85)';
            info.style.color = '#fff';
            info.style.padding = '10px 18px';
            info.style.borderRadius = '8px';
            info.style.fontSize = '16px';
            info.style.zIndex = 3000;
            info.textContent = 'Positionen aktualisiert!';
            document.body.appendChild(info);
            setTimeout(() => {
                if (info.parentNode) info.parentNode.removeChild(info);
            }, 2000);
        })
        .catch(err => {
            document.getElementById('date-info').textContent = 'Fehler beim Laden der Planetenpositionen.';
        });
}
function updateSimSpeed(val) {
    simDaysPerSecond = parseFloat(val);
    document.getElementById('simSpeed').value = simDaysPerSecond;
    document.getElementById('simSpeedNum').value = simDaysPerSecond;
}
document.addEventListener('DOMContentLoaded', function() {
    updateSimSpeed(1);
});
init();

let raycaster = new THREE.Raycaster();
let mouse = new THREE.Vector2();
let hoveredObj = null;
let selectedObj = null;
const infoPanel = document.getElementById('info-panel');
const tooltip = document.getElementById('planet-tooltip');

function getSeasonForDate(dateObj) {
    // dateObj: JS Date (UTC)
    const year = dateObj.getUTCFullYear();
    // Astronomical seasons (northern hemisphere, UTC)
    // Approximate dates (can be refined with ephemeris if needed)
    const spring = new Date(Date.UTC(year, 2, 20)); // 20. März
    const summer = new Date(Date.UTC(year, 5, 21)); // 21. Juni
    const autumn = new Date(Date.UTC(year, 8, 23)); // 23. Sept
    const winter = new Date(Date.UTC(year, 11, 21)); // 21. Dez
    if (dateObj >= spring && dateObj < summer) return 'Frühling';
    if (dateObj >= summer && dateObj < autumn) return 'Sommer';
    if (dateObj >= autumn && dateObj < winter) return 'Herbst';
    // Winter: von Winteranfang bis Frühlingsanfang (Jahreswechsel beachten)
    return 'Winter';
}

function getObjectInfo(obj) {
    let name = obj.data?.name || obj.data?.label || 'Objekt';
    let period = orbitalPeriods[name] || null;
    let angleDeg = obj.angle !== undefined ? (obj.angle * 180 / Math.PI) % 360 : null;
    if (angleDeg < 0) angleDeg += 360;
    let realAU = obj.data?.realAU;
    let ecc = obj.data?.ecc;
    let peri = obj.data?.peri;
    let aphe = obj.data?.aphe;
    // Lookup true distance from lastPlanetPositions if available
    let backendDist = null;
    if (lastPlanetPositions && name !== 'Sonne') {
        // Map JS name to backend key
        const backendMap = { 'Merkur': 'mercury', 'Venus': 'venus', 'Erde': 'earth', 'Mars': 'mars', 'Ceres': 'ceres', 'Vesta': 'vesta', 'Pallas': 'pallas', 'Hygiea': 'hygiea' };
        let key = backendMap[name];
        if (key && lastPlanetPositions[key] && typeof lastPlanetPositions[key].distance_au === 'number') {
            backendDist = lastPlanetPositions[key].distance_au;
        }
    }
    let info = `<b>${name}</b><br>`;
    if (angleDeg !== null) info += `Aktuelle Umlaufposition: ${angleDeg.toFixed(2)}°<br>`;
    if (period) info += `Umlaufzeit: ${period} Tage<br>`;
    if (backendDist) info += `Abstand zur Sonne (aktuell): ${backendDist} AU<br>`;
    else if (realAU && name !== 'Sonne') info += `Abstand zur Sonne: ${realAU} AU<br>`;
    if (ecc && name !== 'Sonne') info += `Exzentrizität: ${ecc}<br>`;
    if (peri && name !== 'Sonne') info += `Perihel: ${peri} AU<br>`;
    if (aphe && name !== 'Sonne') info += `Aphel: ${aphe} AU<br>`;
    // Jahreszeit für Erde anzeigen (exakt)
    if (name === 'Erde' && simDate) {
        let d = new Date(simDate.getTime() + simDateFrac * 86400000);
        let year = d.getUTCFullYear();
        let fallbackSeason = getSeasonForDate(d); // fallback
        let seasonText = '';
        fetchSeasonsForYear(year, function(seasons) {
            let season = getSeasonFromAstronomical(d, seasons);
            let show = season || fallbackSeason;
            infoPanel.innerHTML = info + `<b>Jahreszeit (Nordhalbkugel):</b> ${show}<br>`;
        });
        // Platzhalter, wird nachgeladen
        info += `<b>Jahreszeit (Nordhalbkugel):</b> ...<br>`;
    }
    if (name === 'Mond') info += `Abstand zur Erde: ${moonDistance} AU (skaliert)`;
    if (name === 'Phobos' || name === 'Deimos') info += `Abstand zu Mars: ${marsMoonData[name === 'Phobos' ? 0 : 1].distance} AU (skaliert)`;
    return info;
}

function showTooltip(obj, x, y) {
    tooltip.innerHTML = obj.data?.name || obj.data?.label || 'Objekt';
    tooltip.style.left = (x + 12) + 'px';
    tooltip.style.top = (y - 8) + 'px';
    tooltip.style.display = 'block';
}
function hideTooltip() {
    tooltip.style.display = 'none';
}
function showInfoPanel(obj) {
    infoPanel.innerHTML = getObjectInfo(obj);
    infoPanel.style.display = 'block';
}
function hideInfoPanel() {
    infoPanel.style.display = 'none';
}

// Mouse move: Tooltip
renderer.domElement.addEventListener('mousemove', function(event) {
    const rect = renderer.domElement.getBoundingClientRect();
    mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
    mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
    raycaster.setFromCamera(mouse, camera);
    let objects = planets.map(p => p.mesh).concat([moon], marsMoons.map(m => m.mesh));
    let intersects = raycaster.intersectObjects(objects);
    if (intersects.length > 0) {
        let mesh = intersects[0].object;
        let obj = planets.find(p => p.mesh === mesh) ||
                  (moon === mesh ? {mesh: moon, data: {name: 'Mond'}, angle: moonAngle} : null) ||
                  marsMoons.find((m, i) => m.mesh === mesh ? {mesh: m.mesh, data: {name: marsMoonData[i].name}, angle: marsMoonAngles[i]} : null);
        hoveredObj = obj;
        showTooltip(obj, event.clientX, event.clientY);
    } else {
        hoveredObj = null;
        hideTooltip();
    }
});
// Mouse out: Tooltip
renderer.domElement.addEventListener('mouseleave', function() {
    hoveredObj = null;
    hideTooltip();
});
// Mouse click: Info-Panel
renderer.domElement.addEventListener('click', function(event) {
    if (hoveredObj) {
        selectedObj = hoveredObj;
        showInfoPanel(selectedObj);
    } else {
        selectedObj = null;
        hideInfoPanel();
    }
});

// Info-Panel beim Start ausblenden
hideInfoPanel();

function updateSimDateDisplay() {
    if (!simDate) {
        document.getElementById('sim-date-info').textContent = '';
        return;
    }
    // Format YYYY-MM-DD
    let d = new Date(simDate.getTime() + simDateFrac * 86400000);
    let yyyy = d.getUTCFullYear();
    let mm = String(d.getUTCMonth() + 1).padStart(2, '0');
    let dd = String(d.getUTCDate()).padStart(2, '0');
    let dateStr = `${yyyy}-${mm}-${dd}`;
    document.getElementById('sim-date-info').textContent = `Simuliertes Datum: ${dateStr}`;
}
</script>
</body>
</html>

Dazu ein Scrennshot wofür:
Bildschirmfoto_2025-09-23_21-18-06.png


Gruß
Mathias
 
Hi Matthias,

das ich hier Code einfügen kann wusste ich, wusste nur nicht ob es OK ist wenn ich hier den ganzen Programmcode einspiele.
Aber kein Problem mache ich gerne.

Code:
import os
import sys
import numpy as np
import multiprocessing as mp
from astropy.io import fits
from astropy.stats import sigma_clipped_stats
from astropy.table import Table
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from PyQt5 import QtWidgets, uic
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QObject
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from scipy import ndimage
from scipy.ndimage import shift
from scipy.optimize import curve_fit
from scipy.spatial import KDTree
from skimage.registration import phase_cross_correlation

try:
    from photutils.detection import DAOStarFinder
    PHOTUTILS_AVAILABLE = True
except ImportError:
    PHOTUTILS_AVAILABLE = False

def get_ui_path(ui_filename):
    script_dir = os.path.dirname(os.path.abspath(__file__))
    ui_path = os.path.join(script_dir, ui_filename)
    if os.path.exists(ui_path):
        return ui_path
   
    # Fallback, falls die Datei im aktuellen Verzeichnis liegt
    if os.path.exists(ui_filename):
        return ui_filename
   
    raise FileNotFoundError(f"UI-Datei '{ui_filename}' nicht gefunden. Stelle sicher, dass sie im gleichen Verzeichnis wie das Skript liegt.")

class FitsWatcher(QObject, FileSystemEventHandler):
    new_file = pyqtSignal(str)
   
    def __init__(self, watch_path):
        super().__init__()
        self.watch_path = watch_path
        self.observer = Observer()
        self.observer.schedule(self, watch_path, recursive=False)
       
    def start(self): self.observer.start()
    def stop(self):
        if self.observer.is_alive():
            self.observer.stop()
            self.observer.join()
       
    def on_created(self, event):
        if not event.is_directory and event.src_path.lower().endswith('.fits'):
            self.new_file.emit(event.src_path)

class MplCanvas(FigureCanvas):
    def __init__(self, parent=None, width=5, height=4, dpi=100):
        self.fig = Figure(figsize=(width, height), dpi=dpi, facecolor='#2b2b2b')
        self.axes = self.fig.add_subplot(111)
        super(MplCanvas, self).__init__(self.fig)
        self.fig.tight_layout()
        self.set_dark_style()
        self.mpl_connect('scroll_event', self.on_scroll)
        self.mpl_connect('button_press_event', self.on_click)
        self.original_xlim = None
        self.original_ylim = None
        self.current_xlim = None
        self.current_ylim = None

    def set_dark_style(self):
        self.fig.patch.set_facecolor('#2b2b2b')
        self.axes.set_facecolor('#1e1e1e')
        for spine in self.axes.spines.values():
            spine.set_color('#555555')
        self.axes.tick_params(colors='#cccccc', which='both')

    def on_scroll(self, event):
        if event.inaxes != self.axes: return
        if self.original_xlim is None:
            self.original_xlim = self.axes.get_xlim()
            self.original_ylim = self.axes.get_ylim()
            self.current_xlim = self.original_xlim
            self.current_ylim = self.original_ylim
       
        base_scale = 1.1
        if event.button == 'up': scale_factor = 1 / base_scale
        elif event.button == 'down': scale_factor = base_scale
        else: return
       
        xlim = self.axes.get_xlim()
        ylim = self.axes.get_ylim()
        xdata, ydata = event.xdata, event.ydata
       
        new_width = (xlim[1] - xlim[0]) * scale_factor
        new_height = (ylim[1] - ylim[0]) * scale_factor
        new_xlim = [xdata - new_width/2, xdata + new_width/2]
        new_ylim = [ydata - new_height/2, ydata + new_height/2]
       
        self.axes.set_xlim(new_xlim)
        self.axes.set_ylim(new_ylim)
        self.current_xlim = new_xlim
        self.current_ylim = new_ylim
        self.draw()

    def on_click(self, event):
        if event.button == 3 and event.inaxes == self.axes:
            if self.original_xlim is not None:
                self.axes.set_xlim(self.original_xlim)
                self.axes.set_ylim(self.original_ylim)
                self.current_xlim = self.original_xlim
                self.current_ylim = self.original_ylim
                self.draw()
            else:
                self.axes.relim()
                self.axes.autoscale_view()
                self.draw()

    def apply_zoom(self):
        if self.current_xlim is not None and self.current_ylim is not None:
            self.axes.set_xlim(self.current_xlim)
            self.axes.set_ylim(self.current_ylim)

def load_fits_image(file_path):
    try:
        with fits.open(file_path) as hdul:
            image_data = hdul[0].data
            if len(image_data.shape) == 3: image_data = image_data[0]
            return image_data.astype(np.float64)
    except Exception as e:
        return None

def find_stars(image_data):
    if not PHOTUTILS_AVAILABLE:
        mean, median, std = sigma_clipped_stats(image_data, sigma=3.0)
        threshold = median + 3.0 * std
        return simple_star_finder(image_data, threshold)
    try:
        mean, median, std = sigma_clipped_stats(image_data, sigma=3.0)
        daofind = DAOStarFinder(fwhm=5.0, threshold=3.0*std)
        sources = daofind(image_data - median)
        return sources if sources is not None else []
    except Exception as e:
        return simple_star_finder(image_data, np.median(image_data) + 2.0 * np.std(image_data))

def simple_star_finder(image_data, threshold):
    try:
        labeled, num_features = ndimage.label(image_data > threshold)
        centers = ndimage.center_of_mass(image_data, labeled, range(1, num_features + 1))
        stars = Table()
        stars['xcentroid'] = [c[1] for c in centers]
        stars['ycentroid'] = [c[0] for c in centers]
        return stars
    except:
        return Table()
       
def filter_stars_by_fwhm(stars):
    if len(stars) == 0: return stars
    return stars

def calculate_precise_shift(current_image, reference_image):
    try:
        h, w = reference_image.shape
        ref_norm = (reference_image - np.mean(reference_image)) / np.std(reference_image)
        curr_norm = (current_image - np.mean(current_image)) / np.std(current_image)
        shift_result, error, phasediff = phase_cross_correlation(
            ref_norm, curr_norm, upsample_factor=10, normalization=None
        )
        return shift_result[1], shift_result[0], error
    except Exception as e:
        return 0, 0, 1.0

def high_precision_alignment(current_image, reference_image):
    try:
        shift_x, shift_y, error = calculate_precise_shift(current_image, reference_image)
        if abs(shift_x) < 0.1 and abs(shift_y) < 0.1:
            return current_image, 0.01, shift_x, shift_y, error
        if abs(shift_x) > 200 or abs(shift_y) > 200:
            return current_image, 1.0, shift_x, shift_y, error
        aligned_image = shift(current_image, (shift_y, shift_x), mode='reflect', order=1)
        return aligned_image, error, shift_x, shift_y, error
    except Exception as e:
        return current_image, 1.0, 0, 0, 1.0

def calculate_snr(image_data):
    if image_data is None:
        return 0.0
   
    try:
        height, width = image_data.shape
        border_size = min(50, height//10, width//10)
       
        top_border = image_data[:border_size, :]
        bottom_border = image_data[-border_size:, :]
        left_border = image_data[:, :border_size]
        right_border = image_data[:, -border_size:]
       
        noise_region = np.concatenate([
            top_border.flatten(),
            bottom_border.flatten(),
            left_border.flatten(),
            right_border.flatten()
        ])
       
        signal = np.median(image_data)
        noise = np.std(noise_region)
       
        if noise > 0:
            snr = signal / noise
        else:
            snr = 0.0
           
        return snr
       
    except Exception as e:
        try:
            mean, median, std = sigma_clipped_stats(image_data, sigma=3.0)
            if std > 0:
                return median / std
            return 0.0
        except:
            return 0.0

def calculate_fwhm(image_data, stars):
    if image_data is None or len(stars) == 0:
        return 0.0
   
    try:
        fwhm_values = []
        for star in stars:
            try:
                x, y = int(star['xcentroid']), int(star['ycentroid'])
                size = 15
                y_min = max(0, y - size)
                y_max = min(image_data.shape[0], y + size + 1)
                x_min = max(0, x - size)
                x_max = min(image_data.shape[1], x + size + 1)
                star_region = image_data[y_min:y_max, x_min:x_max]
                if star_region.size == 0: continue
                max_val = np.max(star_region)
                half_max = max_val / 2.0
                above_half_max = star_region >= half_max
                if np.any(above_half_max):
                    area = np.sum(above_half_max)
                    fwhm = 2 * np.sqrt(area / np.pi)
                    fwhm_values.append(fwhm)
            except Exception as e:
                continue
       
        if fwhm_values:
            median_fwhm = np.median(fwhm_values)
            return median_fwhm
        else:
            return 0.0
    except Exception as e:
        return 0.0

def process_image_in_worker(file_path, reference_image, is_first_image):
    try:
        raw_image_data = load_fits_image(file_path)
        if raw_image_data is None:
            return (None, f"Fehler beim Laden von {os.path.basename(file_path)}", 0, os.path.basename(file_path), 0.0, None)
       
        stars = find_stars(raw_image_data)
        filtered_stars = filter_stars_by_fwhm(stars)
       
        if len(filtered_stars) < 3:
            return (None, f"Ignoriert: {os.path.basename(file_path)} (zu wenige Sterne)", 0, os.path.basename(file_path), 0.0, None)
           
        current_fwhm = calculate_fwhm(raw_image_data, filtered_stars)
       
        if is_first_image:
            aligned_image = raw_image_data
            alignment_error = 0.0
            shift_x = 0
            shift_y = 0
        else:
            aligned_image, alignment_error, shift_x, shift_y, _ = high_precision_alignment(raw_image_data, reference_image)
            if alignment_error > 0.99:
                return (None, f"Ignoriert: {os.path.basename(file_path)} (Ausrichtungsfehler)", 0, os.path.basename(file_path), 0.0, None)
       
        debug_msg = f"Verschiebung: x={shift_x:.2f}, y={shift_y:.2f}, Fehler={alignment_error:.6f}"
       
        return (aligned_image, f"Verarbeitet: {os.path.basename(file_path)}", len(filtered_stars), os.path.basename(file_path), current_fwhm, debug_msg)
    except Exception as e:
        return (None, f"Fehler im Worker-Prozess für {os.path.basename(file_path)}: {str(e)}", 0, os.path.basename(file_path), 0.0, None)


class LiveStacker(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        try:
            ui_path = get_ui_path("astrostacker_deepseek_3_1.ui")
            uic.loadUi(ui_path, self)
        except FileNotFoundError as e:
            print(f"Fehler: {e}")
            sys.exit(1)
       
        self.initialize_ui_elements()
        self.watch_path = ""
        self.fits_files = []
        self.processed_files = set()
        self.processing_files = set()
        self.current_raw_image = None
        self.stacked_image = None
        self.stacked_count = 0
        self.ignored_files = set()
        self.star_count = 0
        self.is_paused = self.is_stopped = self.is_autostart = False
        self.current_file_index = 0
        self.reference_image = None
        self.alignment_quality = []
        self.brightness = 0
        self.contrast = 0
        self.first_image_snr = None
        self.current_fwhm = 0.0
       
        self.autostack_timer = QTimer()
        self.autostack_timer.timeout.connect(self.process_next_image)
        self.display_timer = QTimer()
        self.display_timer.timeout.connect(self.update_displays)
        self.display_timer.start(1000)
       
        self.result_queue = mp.Queue()
        self.num_processes = max(1, mp.cpu_count() // 2)
        self.worker_pool = mp.Pool(processes=self.num_processes)
       
        self.check_result_timer = QTimer()
        self.check_result_timer.timeout.connect(self.check_result_queue)
        self.check_result_timer.start(100)
       
        self.connect_signals()
        self.setup_canvas()
        self.enable_autostretch()
        self.statusBar().showMessage("Bereit - Wähle ein Verzeichnis aus")
   
    def initialize_ui_elements(self):
        self.pfad_btn = self.findChild(QtWidgets.QPushButton, "pfad_btn") or self.findChild(QtWidgets.QPushButton)
        self.start_btn = self.findChild(QtWidgets.QPushButton, "start_btn") or self.findChild(QtWidgets.QPushButton, "pushButton_2")
        self.autostart_btn = self.findChild(QtWidgets.QPushButton, "autostart_btn") or self.findChild(QtWidgets.QPushButton, "pushButton_7")
        self.pause_btn = self.findChild(QtWidgets.QPushButton, "pause_btn") or self.findChild(QtWidgets.QPushButton, "pushButton_3")
        self.stop_btn = self.findChild(QtWidgets.QPushButton, "stop_btn") or self.findChild(QtWidgets.QPushButton, "pushButton_4")
        self.reset_btn = self.findChild(QtWidgets.QPushButton, "reset_btn") or self.findChild(QtWidgets.QPushButton, "pushButton_5")
        self.beenden_btn = self.findChild(QtWidgets.QPushButton, "beenden_btn") or self.findChild(QtWidgets.QPushButton, "pushButton_6")
       
        self.image_frm = self.findChild(QtWidgets.QFrame, "image_frm") or self.findChild(QtWidgets.QFrame, "frame")
        self.stack_frm = self.findChild(QtWidgets.QFrame, "stack_frm") or self.findChild(QtWidgets.QFrame, "frame_2")
       
        self.projekt_lbl = self.findChild(QtWidgets.QLabel, "projekt_lbl")
        self.fortschritt_lbl = self.findChild(QtWidgets.QLabel, "fortschritt_lbl")
        self.ignoriert_lbl = self.findChild(QtWidgets.QLabel, "ignoriert_lbl")
        self.sterneanzahl_lbl = self.findChild(QtWidgets.QLabel, "sterneanzahl_lbl")
        self.pfad_lbl = self.findChild(QtWidgets.QLabel, "pfad_lbl")
        self.snr_lbl = self.findChild(QtWidgets.QLabel, "snr_lbl")
        self.fwhm_lbl = self.findChild(QtWidgets.QLabel, "fwhm_lbl")
       
        self.autostretch_cb = self.findChild(QtWidgets.QCheckBox, "autostretch_chkbox")
        if self.autostretch_cb is None:
            self.autostretch_cb = self.findChild(QtWidgets.QCheckBox, "checkBox")
            if self.autostretch_cb is None:
                self.autostretch_cb = QtWidgets.QCheckBox()
                self.autostretch_cb.setVisible(False)
       
        self.helligkeit_sldr = self.findChild(QtWidgets.QSlider, "helligkeit_sldr")
        self.kontrast_sldr = self.findChild(QtWidgets.QSlider, "kontrast_sldr")
       
        self.helligkeit_lbl = self.findChild(QtWidgets.QLabel, "helligkeit_lbl")
        self.kontrast_lbl = self.findChild(QtWidgets.QLabel, "kontrast_lbl")
   
    def enable_autostretch(self):
        if hasattr(self, 'autostretch_cb') and self.autostretch_cb:
            self.autostretch_cb.setChecked(True)
            self.statusBar().showMessage("Auto-Stretch aktiviert")
   
    def connect_signals(self):
        if self.pfad_btn: self.pfad_btn.clicked.connect(self.select_directory)
        if self.start_btn: self.start_btn.clicked.connect(self.start_stacking)
        if self.autostart_btn: self.autostart_btn.clicked.connect(self.toggle_autostart)
        if self.pause_btn: self.pause_btn.clicked.connect(self.toggle_pause)
        if self.stop_btn: self.stop_btn.clicked.connect(self.stop_stacking)
        if self.reset_btn: self.reset_btn.clicked.connect(self.reset_stacking)
        if self.beenden_btn: self.beenden_btn.clicked.connect(self.close)
       
        if hasattr(self, 'autostretch_cb') and self.autostretch_cb and self.autostretch_cb.isVisible():
            self.autostretch_cb.stateChanged.connect(self.on_autostretch_changed)
       
        if self.helligkeit_sldr: self.helligkeit_sldr.valueChanged.connect(self.on_brightness_changed)
        if self.kontrast_sldr: self.kontrast_sldr.valueChanged.connect(self.on_contrast_changed)
   
    def setup_canvas(self):
        self.stack_canvas = MplCanvas(self.stack_frm, width=5, height=4, dpi=100)
        self.image_canvas = MplCanvas(self.image_frm, width=5, height=4, dpi=100)
        self.stack_frm.setStyleSheet("background-color: #2b2b2b; border: 1px solid #555555;")
        self.image_frm.setStyleSheet("background-color: #2b2b2b; border: 1px solid #555555;")
        image_layout = QtWidgets.QVBoxLayout()
        image_layout.setContentsMargins(0, 0, 0, 0)
        image_layout.addWidget(self.image_canvas)
        self.image_frm.setLayout(image_layout)
        stack_layout = QtWidgets.QVBoxLayout()
        stack_layout.setContentsMargins(0, 0, 0, 0)
        stack_layout.addWidget(self.stack_canvas)
        self.stack_frm.setLayout(stack_layout)
        parent_layout = self.image_frm.parentWidget().layout()
        if parent_layout is None:
            print("FEHLER: Konnte das Eltern-Layout nicht finden!")
            return
        image_index = parent_layout.indexOf(self.image_frm)
        stack_index = parent_layout.indexOf(self.stack_frm)
        parent_layout.removeWidget(self.image_frm)
        parent_layout.removeWidget(self.stack_frm)
        splitter = QtWidgets.QSplitter(Qt.Vertical)
        splitter.addWidget(self.stack_frm)
        splitter.addWidget(self.image_frm)
        splitter.setSizes([300, 300])
        insert_index = min(stack_index, image_index)
        parent_layout.insertWidget(insert_index, splitter)
        self.update_displays()

    def on_brightness_changed(self, value):
        self.brightness = value
        if self.helligkeit_lbl:
            self.helligkeit_lbl.setText(f"Helligkeit: {value}")
        self.update_displays()
   
    def on_contrast_changed(self, value):
        self.contrast = value
        if self.kontrast_lbl:
            self.kontrast_lbl.setText(f"Kontrast: {value}")
        self.update_displays()
   
    def adjust_image(self, image_data):
        if image_data is None: return image_data
        brightness_factor = self.brightness / 100.0 * np.ptp(image_data) if np.ptp(image_data) > 0 else 0
        adjusted = image_data + brightness_factor
        contrast_factor = 1.0 + (self.contrast / 100.0)
        mean_val = np.mean(adjusted)
        adjusted = mean_val + contrast_factor * (adjusted - mean_val)
        adjusted = np.clip(adjusted, np.min(image_data), np.max(image_data))
        return adjusted
   
    def toggle_autostart(self):
        self.is_autostart = not self.is_autostart
        if self.is_autostart:
            self.statusBar().showMessage("Auto-Stack gestartet")
            if self.autostart_btn: self.autostart_btn.setText("Auto-Stop")
            self.autostack_timer.start(2000)
            self.process_next_image()
        else:
            self.statusBar().showMessage("Auto-Stack gestoppt")
            if self.autostart_btn: self.autostart_btn.setText("Auto-Start")
            self.autostack_timer.stop()
   
    def start_stacking(self):
        self.is_stopped = False
        self.statusBar().showMessage("Stacking gestartet - warte auf neue Bilder")
        if self.start_btn: self.start_btn.setEnabled(False)
        self.autostack_timer.start(2000)
        self.process_next_image()
   
    def select_directory(self):
        directory = QtWidgets.QFileDialog.getExistingDirectory(self, "Verzeichnis auswählen")
        if directory:
            self.watch_path = directory
            if self.pfad_lbl: self.pfad_lbl.setText(f"Überwachter Pfad: {directory}")
            self.start_watching()
   
    def start_watching(self):
        if hasattr(self, 'fits_watcher') and self.fits_watcher:
            self.fits_watcher.stop()
       
        if not os.path.exists(self.watch_path):
            print(f"FEHLER: Überwachungspfad '{self.watch_path}' existiert nicht!")
            self.statusBar().showMessage(f"FEHLER: Pfad existiert nicht!")
            self.watch_path = ""
            if self.pfad_lbl:
                self.pfad_lbl.setText("Überwachter Pfad: Kein Pfad ausgewählt")
            return
       
        try:
            self.fits_watcher = FitsWatcher(self.watch_path)
            self.fits_watcher.new_file.connect(self.on_new_file)
            self.fits_watcher.start()
            self.load_existing_files()
            self.statusBar().showMessage(f"Überwache: {self.watch_path}")
        except Exception as e:
            print(f"Fehler beim Starten des Watchers: {e}")
            self.statusBar().showMessage(f"Fehler: {str(e)}")
   
    def load_existing_files(self):
        if not self.watch_path:
            return
       
        try:
            if not os.path.exists(self.watch_path):
                print(f"FEHLER: Überwachungspfad '{self.watch_path}' wurde gelöscht!")
                self.statusBar().showMessage(f"FEHLER: Pfad wurde gelöscht!")
                self.watch_path = ""
                if self.pfad_lbl:
                    self.pfad_lbl.setText("Überwachter Pfad: Kein Pfad ausgewählt")
                return
               
            self.fits_files = sorted([os.path.join(self.watch_path, f) for f in os.listdir(self.watch_path)
                              if f.lower().endswith('.fits')], key=lambda x: os.path.getctime(x))
            self.processed_files = set()
            self.processing_files = set()
            self.ignored_files = set()
            self.current_file_index = 0
           
            if self.fits_files:
                first_file_path = self.fits_files[0]
                image_data = load_fits_image(first_file_path)
                if image_data is not None:
                    self.current_raw_image = image_data
                    self.stacked_image = image_data
                    self.reference_image = image_data
                    self.stacked_count = 1
                    self.processed_files.add(first_file_path)
                    print(f"Erstes Bild geladen und als Referenzbild gesetzt: {os.path.basename(first_file_path)}")
                    self.update_displays()
                else:
                    self.statusBar().showMessage("Fehler: Konnte das erste Bild nicht laden.")
       
        except FileNotFoundError:
            print(f"FEHLER: Überwachungspfad '{self.watch_path}' wurde gelöscht!")
            self.statusBar().showMessage(f"FEHLER: Pfad wurde gelöscht!")
            self.watch_path = ""
            if self.pfad_lbl:
                self.pfad_lbl.setText("Überwachter Pfad: Kein Pfad ausgewählt")
        except Exception as e:
            print(f"Fehler beim Laden der Dateien: {e}")
   
    def on_new_file(self, file_path):
        if file_path not in self.fits_files:
            self.fits_files.append(file_path)
            self.fits_files.sort(key=lambda x: os.path.getctime(x))
            print(f"Neue Datei erkannt: {os.path.basename(file_path)}")
            if self.is_autostart and not self.is_paused:
                self.process_next_image()
   
    def process_next_image(self):
        if self.is_stopped or self.is_paused:
            return
       
        # Entfernt: print("Warte, bis die aktuelle Verarbeitung abgeschlossen ist.")
        if self.processing_files:
            return

        unprocessed_files = sorted([f for f in self.fits_files
                           if f not in self.processed_files and f not in self.ignored_files and f not in self.processing_files], key=lambda x: os.path.getctime(x))
       
        if not unprocessed_files:
            self.statusBar().showMessage("Warte auf neue Bilder...")
            return
       
        next_file = unprocessed_files[0]
        self.processing_files.add(next_file)
       
        is_first_image = (self.stacked_count == 0)
       
        print(f"Sende {os.path.basename(next_file)} zur Verarbeitung...")
        self.worker_pool.apply_async(process_image_in_worker, args=(next_file, self.reference_image, is_first_image), callback=self.on_worker_complete)
   
    def check_result_queue(self):
        if self.result_queue.empty():
            return
       
        while not self.result_queue.empty():
            try:
                result = self.result_queue.get(timeout=0.1)
                self.process_worker_result(result)
            except:
                pass

    def on_worker_complete(self, result):
        self.result_queue.put(result)
       
    def process_worker_result(self, result):
        aligned_image, message, star_count, filename, current_fwhm, debug_msg = result
        print(message)
        if debug_msg:
            print(debug_msg)

        file_path = os.path.join(self.watch_path, filename)
       
        if file_path in self.processing_files:
            self.processing_files.remove(file_path)
           
        if aligned_image is None:
            if file_path not in self.ignored_files:
                self.ignored_files.add(file_path)
            if self.ignoriert_lbl: self.ignoriert_lbl.setText(f"Ignoriert: {len(self.ignored_files)}")
            return
       
        if self.stacked_count == 0:
            self.reference_image = aligned_image
            self.first_image_snr = calculate_snr(aligned_image)
            self.stacked_image = aligned_image.astype(np.float64)
        else:
            self.stacked_image = (self.stacked_image * self.stacked_count + aligned_image) / (self.stacked_count + 1)
       
        self.stacked_count += 1
        if file_path not in self.processed_files:
            self.processed_files.add(file_path)

        self.current_raw_image = aligned_image
        self.star_count = star_count
        self.current_fwhm = current_fwhm

        if self.fortschritt_lbl: self.fortschritt_lbl.setText(f"Gestapelt: {self.stacked_count}")
        if self.sterneanzahl_lbl: self.sterneanzahl_lbl.setText(f"Sterne: {self.star_count}")
        if self.fwhm_lbl: self.fwhm_lbl.setText(f"FWHM: {self.current_fwhm:.2f} px")
       
        if self.stacked_image is not None:
            stacked_snr = calculate_snr(self.stacked_image)
            if self.snr_lbl: self.snr_lbl.setText(f"SNR: {stacked_snr:.1f}")

        if self.projekt_lbl: self.projekt_lbl.setText(f"Aktuell: {filename}")
       
        self.update_displays()

        if self.is_autostart and not self.is_paused:
            self.process_next_image()
           
    def load_and_display_image(self, file_path):
        image_data = load_fits_image(file_path)
        if image_data is not None:
            self.current_raw_image = image_data
            if self.projekt_lbl: self.projekt_lbl.setText(f"Aktuell: {os.path.basename(file_path)}")
            self.display_image(self.current_raw_image, self.image_canvas, reset_zoom=True)
            if self.stacked_image is None:
                self.display_image(self.current_raw_image, self.stack_canvas, reset_zoom=True)
            else:
                self.display_image(self.stacked_image, self.stack_canvas, reset_zoom=False)
   
    def display_image(self, image_data, canvas, reset_zoom=False):
        if image_data is None: return
        canvas.axes.clear()
        canvas.axes.set_facecolor('#1e1e1e')
        for spine in canvas.axes.spines.values(): spine.set_color('#555555')
        canvas.axes.tick_params(colors='#cccccc')
       
        adjusted_image = self.adjust_image(image_data)
        use_autostretch = (hasattr(self, 'autostretch_cb') and self.autostretch_cb and self.autostretch_cb.isChecked())
       
        if use_autostretch:
            vmin, vmax = np.percentile(adjusted_image, 5), np.percentile(adjusted_image, 95)
        else:
            vmin, vmax = np.min(adjusted_image), np.max(adjusted_image)
       
        canvas.axes.imshow(adjusted_image, cmap='gray', origin='lower', vmin=vmin, vmax=vmax)
        canvas.axes.set_title('', color='#cccccc')
        canvas.axes.set_xticks([])
        canvas.axes.set_yticks([])
       
        if reset_zoom:
            canvas.original_xlim = None
            canvas.original_ylim = None
            canvas.current_xlim = None
            canvas.current_ylim = None
        else:
            canvas.apply_zoom()
        canvas.draw()
   
    def update_displays(self):
        if self.current_raw_image is not None:
            self.display_image(self.current_raw_image, self.image_canvas, reset_zoom=False)
            if self.stacked_image is not None:
                self.display_image(self.stacked_image, self.stack_canvas, reset_zoom=False)
            else:
                self.display_image(self.current_raw_image, self.stack_canvas, reset_zoom=False)
   
    def on_autostretch_changed(self, state):
        if hasattr(self, 'autostretch_cb') and self.autostretch_cb:
            if state == Qt.Checked:
                self.statusBar().showMessage("Auto-Stretch aktiviert")
            else:
                self.statusBar().showMessage("Auto-Stretch deaktiviert")
            self.update_displays()
   
    def toggle_pause(self):
        self.is_paused = not self.is_paused
        if self.is_paused:
            self.statusBar().showMessage("Stacking pausiert")
            if self.pause_btn: self.pause_btn.setText("Fortsetzen")
            self.autostack_timer.stop()
        else:
            self.statusBar().showMessage("Stacking fortgesetzt")
            if self.pause_btn: self.pause_btn.setText("Pause")
            if self.is_autostart:
                self.autostack_timer.start(2000)
   
    def stop_stacking(self):
        self.is_stopped = True
        self.is_autostart = False
        self.autostack_timer.stop()
        if self.autostart_btn: self.autostart_btn.setText("Auto-Start")
        if self.start_btn: self.start_btn.setEnabled(True)
        self.statusBar().showMessage("Stacking gestoppt")
   
    def reset_stacking(self):
        self.stacked_image = None
        self.stacked_count = 0
        self.ignored_files = set()
        self.processing_files = set()
        self.processed_files = set()
        self.star_count = 0
        self.is_paused = self.is_stopped = self.is_autostart = False
        self.current_file_index = 0
        self.reference_image = None
        self.alignment_quality = []
        self.first_image_snr = None
        self.current_fwhm = 0.0
        self.autostack_timer.stop()
       
        self.worker_pool.terminate()
        self.worker_pool.join()
        self.worker_pool = mp.Pool(processes=self.num_processes)
       
        if self.fortschritt_lbl: self.fortschritt_lbl.setText("Gestapelt: 0")
        if self.ignoriert_lbl: self.ignoriert_lbl.setText("Ignoriert: 0")
        if self.sterneanzahl_lbl: self.sterneanzahl_lbl.setText("Sterne: 0")
        if self.pfad_lbl: self.pfad_lbl.setText("Überwachter Pfad: Kein Pfad ausgewählt")
        if self.snr_lbl: self.snr_lbl.setText("SNR: -")
        if self.fwhm_lbl: self.fwhm_lbl.setText("FWHM: -")
        if self.autostart_btn: self.autostart_btn.setText("Auto-Start")
        if self.start_btn: self.start_btn.setEnabled(True)
       
        self.enable_autostretch()
        self.statusBar().showMessage("Stacking zurückgesetzt")
        self.load_existing_files()
   
    def closeEvent(self, event):
        if hasattr(self, 'fits_watcher') and self.fits_watcher is not None:
            print("Beende Fits-Dateibeobachter...")
            self.fits_watcher.stop()
        if hasattr(self, 'check_result_timer') and self.check_result_timer.isActive():
            self.check_result_timer.stop()
        if hasattr(self, 'autostack_timer') and self.autostack_timer.isActive():
            self.autostack_timer.stop()
       
        if hasattr(self, 'worker_pool') and self.worker_pool:
            print("Beende Worker-Prozesse...")
            self.worker_pool.terminate()
            self.worker_pool.join()  
            print("Worker-Prozesse beendet.")
        event.accept()

def main():
    mp.freeze_support()
    app = QtWidgets.QApplication(sys.argv)
    window = LiveStacker()
    window.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()
Ich habe das Programm mit 3 Aufnahme-Sessions getestet und es lief bei mir ohne Fehler durch.
Eine ältere Version habe ich am 19.ten Verwendet bei einer Aufnahme, die hatte noch ein paar Macken. Bei der hier habe ich nichts mehr gefunden.
Das sollte fehlerfrei laufen.
 
Ähm, ja is meiner, nix geklautes :)
Wie gesagt, wer will der kann, wer nicht will, soll es bleiben lassen.
 
<Paranoiamodus> Ich soll eine Software installieren, die von letztlich von Unternehmen erstellt wurde, die sich nicht um Copyright scheren, und bei denen der Programmierer wahrscheinlich keine Ahnung hat, was sie neben Livestacking vielleicht noch so alles macht? </Paranoia>

Sorry, als KI verkaufte LLMs sind für mich mittlerweile ein rotes Tuch. Der Vorteil von Linux ist ja, dass es eigentlich tief in der Open-Source-Welt verankert ist und die Chance gut sind, dass sich noch irgendwer den Quellcode angeschaut hat, oder die Lizenzen der verwendeten Quellcode-Bausteine. Nichts gegen neue Software, aber wer sagt mir, dass sie nicht doch mehr macht als du planst?

Da habe ich gerne einmalig die 100€ für Sharpcap gezahlt (ja, ich habe eine Lifetime-Lizenz statt der 14 Pfund Abogebühr jährlich), an einen Menschen, der die Software aktiv weiterentwickelt. Auch wenn ich deshalb einen Windows-PC am Teleskop habe und auch nur das Bauchgefühl habe, dass ich Robin Glover vertrauen kann.

Clear Skies,
Alex

Hi Alex,

ich kann deine Skepsis nachvollziehen. Deine Bemerkung "als KI verkaufte LLM": ja da ist was dran, KI ist momentan ein noch viel zu großer Hype, aber es zeigen sich schon die ersten Wölkchen am noch fast blauen KI-Himmel.

Bin da etwas zwiegespalten. Sorge macht mir, daß in die KI, und wie Du richtigt sagst, ist damit for allem das LLM gemeint, viel zu viel Hoffnung hineingesteckt wird, und leider auch von Entscheidern die von Tuten und Blasen recht wenig verstehen.

Auf der anderen Seite spriessen gerade die Programmierer wie Pilze aus dem Boden. Erinnert mich an die 80er Homecomputer-Ära, wo die Kids anstelle der Hausaufgaben erst mal gezockt haben, dann Listings abgetippt und schliesslich angefangen haben zu hacken.

Das hat doch auch etwas Positives: es entstehen neue Dinge, die sonst nicht entstanden wären. Wir in Europa sind da immer zu zögerlich, und lassen andere das Rennen machen, und erst wenn man erkennt, daß man muß, dann fängt man auch hier an - meistens zu spät, die Claims sind schon abgesteckt.

Wegen der rechtlichen Hintergründe: das entwickelt sich gerade. Das Internet war in den Anfangsjahren ja auch gefühlt eine rechtsfreie Räuberbude - diese Zeiten sind vorbei, und die KI werden sie auch irgendwann einfangen, hoffen wir mal.

Was die konkreten Sicherheitsbedenken in diesem Fall betrifft: der Code hier ist doch überschaubar.

Was mir allerdings ein wörtlich rotes Tuch wäre, wären irgendwelche Skripte gerade von jsdelivr nachzuladen und ausführen zu lassen - gerade von jsdelivr, wo eine einzelne Person Zugriff auf alle Services und Servers hat.

Ansonsten: Daumen hoch für den selbt gebastelten Livestacker unter Linux: dieser Markt kann noch etwas Sättigung vertragen ;-)

Gruß
Andreas
 
Hallo Andreas,

guter Text, bin mit dem meisten d'accord.
Soweit ich gesehen habe ist das Script des TO "linuxer" eh alles von Hand und relativ überschaubar. Über die imports kann ich nix sagen, aber das sind Standardpakete die Millionen andere auch laden und von denen ich mal ausgehe, das die nix Schlimmes machen, da wäre die Community schon dahinter gestiegen, oder etwa nicht?

<exkurs>
Was das Zuladen von externen Scripts betrifft: Wenn ich sowas mache bei meinen bescheidenen JavaScript-Apps, dann lade ich niemals via http vom Fremdserver. Ich lade mir die Script-Seite, kopiere es in ein JS File und lade es dann von meinem Server. Alles was ich verwende ist Open Source, wenn ich's überhaupt brauche. Nichts gewonnen? Das Problem ist, das diese Scripte oft minifiziert sind, um KB zu sparen, was ja gut ist. Natürlich sollte man sich zugeladene Scripte schon mal durchsehen, man ent-minifiziert es und sieht dann drüber, leider sind dann Functions und Variablen aber oft zu einem (oder ein paar) Buchstaben "verkommen", sodass man die konkreten Tätigkeiten der Funktionen nicht mehr sofort erkennen kann. Zugegeben mühsam. Was man aber sehrwohl erkennen könnte sind wiederum Aufrufe, die wieder Weiteres zuladen möchten oder irgendwelche Cookies setzen, oder httpRequests usw.
Außerdem gibt's ja auch noch die Browser Entwicklertools, mit denen man herausfinden kann, wohin da telefoniert wird.
</exkurs>

Wie das bei den Python-Zusatzpaketen genau läuft, weiß ich nicht genau. Ich denke mal die allermeisten Sachen werden allein dadurch, dass sie von vielen o.s. entwickelt werden so geschrieben, dass sie auf der sicheren Seite sind. Und die Umgebungen, in denen das dann läuft muss man ja auch haben bzw. herunterladen. Also wenn man da zu paranoid wäre, darf man überhaupt nix mehr installieren. Ich finde es super, wenn sich heute noch Leute hinsetzen und sich selbst was ausdenken, das ausformulieren und dann anderen gratis zur Verfügung stellen.

cs,
harald

--
 
Vielleicht sollte ich noch erwähnen das nichts von irgendwelchen Server heruntergeladen werden muss das nicht sowieso schon in den Standard-Repositories des Systems vorhanden ist.
Was also zusätzlich an Bibliotheken und Programmen installiert werden "muss/müsste (für diesen Pythonscript)" ist meistens schon auf den Jeweiligen Recher vorhanden.

Für Windows-Nutzer :
Ein Repository wird vom "Hersteller" der Linuxversion bereitgestellt. Alle Programme die nachinstalliert, upgedatet etc. werden kommen
in der Regel aus diesem Repository, es sei den sie werden irgendwo anders aus dem Internet bewusst heruntergeladen.
Der Standardbefehl "sudo apt install xxx" zieht aber nur aus diesem Sytem-Repository seine Programme. Der Hersteller, in meinem Fall Ubuntu, sorgt auch dafür das die Daten Fehler und Viren frei sind.

Soweit ich mich noch erinnern kann ist das bei Windows anders.
Da gibt es kein M$-Repsitory und jeder kann im INet von überall Programme herunterladen und installieren.

Wie schon gesagt das Programm ist Opensource und einsehbar. Auch das zugehörige User Interface-File ist nicht kompiliert sondern in Klartext gehalten.

Ich kann Skepsis verstehen, gerade unter Nutzer des OS von Microsoft, aber ich hätte nie gedacht das dieses kleine Programm so wellen schlägt. ;)
 
Hallo Thomas,

Ich kann Skepsis verstehen, gerade unter Nutzer des OS von Microsoft, aber ich hätte nie gedacht das dieses kleine Programm so wellen schlägt. ;)

Wellen schlägt es ja noch nicht. Im Post Nr. 2 steht ja vorne am Text <Paranoiamodus>, soll wahrscheinlich heißen vom Standpunkt eines Advocatus Diaboli aus gesehen. Die Diskussion über solche external packages ist ja durchaus berechtigt. Wer Software macht und hier im Forum bereitstellt musst halt auch damit rechnen, dass der eine oder andere seinen Senf dazugeben muss. Kritik kann man halt auf viele Arten anbringen, muss man aber nicht. Mir z.B. sind schon mal Leute mit dem Urheberrechtsgesetzt gekommen....

cs,
harald

--
 
<exkurs>
Was das Zuladen von externen Scripts betrifft: Wenn ich sowas mache bei meinen bescheidenen JavaScript-Apps, dann lade ich niemals via http vom Fremdserver. Ich lade mir die Script-Seite, kopiere es in ein JS File und lade es dann von meinem Server. Alles was ich verwende ist Open Source, wenn ich's überhaupt brauche. Nichts gewonnen? Das Problem ist, das diese Scripte oft minifiziert sind, um KB zu sparen, was ja gut ist. Natürlich sollte man sich zugeladene Scripte schon mal durchsehen, man ent-minifiziert es und sieht dann drüber, leider sind dann Functions und Variablen aber oft zu einem (oder ein paar) Buchstaben "verkommen", sodass man die konkreten Tätigkeiten der Funktionen nicht mehr sofort erkennen kann. Zugegeben mühsam. Was man aber sehrwohl erkennen könnte sind wiederum Aufrufe, die wieder Weiteres zuladen möchten oder irgendwelche Cookies setzen, oder httpRequests usw.
Außerdem gibt's ja auch noch die Browser Entwicklertools, mit denen man herausfinden kann, wohin da telefoniert wird.
</exkurs>

Hi Harald,

vorbildlich, genau so, und das Gleiche auch mit den Fonts, die hostet man auch besser lokal. Den Aufwand, die Skripte zu entminifizieren mache ich nicht, die bekommt man eigentlich immer auch entminifiziert. Wenn ich dem Herausgeber nicht traue, schaue ich da schon mal rein, aber genauer sehe ich's im Browser und in der Firewall, ob ein Skript versucht, nach hause zu telefonieren.

Wer genauer hinsieht hat, der sieht auch, daß die Kritik mit den externen Skripten nicht den linuxer getroffen hat, sondern das Beispiel von Mathias, aber der ist aussen vor, weil er sein Projekt nicht rausgibt :cool:

Gruß
Andreas
 
Tach zusammen,

sondern das Beispiel von Mathias, aber der ist aussen vor, weil er sein Projekt nicht rausgibt :cool:
das Projekt hat etwas mehr als 18 GB. OK, zieh Backups, Daten, Tests u.ä. ab, dann sind es vielleicht noch 12 GB.
Aufräumen wäre angesagt ;).
Dazu kommt noch MySQL Datenbank(en).
Alles läuft lokal auf meinem Notebook unter Apache und der Firlefanz drumherum. Externen (V)Server habe ich aus Kostengründen abgeschafft.
Dazu kommen dann noch php's die Benutzernamen und PWs für APIs (z.B. Wetter) oder Zugriff auf Datenbank(en) einlesen. Das rausfriemeln ... ... ... :unsure:.
Und bei mancher Grafik/Bild bin ich mir nicht ganz so sicher :whistle: :affemund:.

Gruß
Mathias
 
Zurück
Oben