Marcin Modest Trzaska

Piszę, co mi przyjdzie do głowy. Na tematy różne.

No to tegoroczną jesień uważam za otwartą. Co prawda w aplikacji IMGW są jeszcze dni z temperaturą powyżej 25 °C, ale dziś na rower poszedłem w bluzie z długim rękawem. I nie żałowałem.

Na szczęście, wbrem przepowiedniom aplikacji pogodowych, nie spadła kropla deszczu.

#rower

— Marcin “czach” Trzaska reply-to: @czach@mastodon.argilus.online

Zainspirowany tootkiem sprawdziłem, jak się mają sprawy z nocami tropikalnymi we Wrocławiu.

Noc tropikalna to umownie noc, podczas której temperatura nie spada poniżej 20 °C.

Mam stację pogodową Netatmo wraz z modułami do pomiaru opadów oraz wiatru. Stacja znajduje się na wysokości 119 metrów nad poziomem morza i jakieś 10 metrów od gruntu.

Na potrzeby tego wpisu wklepałem prosty skrypt w pythonie zakładając, że noc jest między 18:00 a 06:00.

Ściągnąłem surowe dane ze stacji pogodowej (plik cvs) i… oto co stacja raportowała w 2026 roku.

maj

Pierwsza tropikalna noc w tym roku miała miejsce w nocy z 26 na 27 maja. Minimalna temperatura to 21.7 °C (05:30)

Noc Temp Status
26–27.05 21.7 °C 🥵 tropikalna
27–28.05 13.8 °C —
28–29.05 12.8 °C —
29–30.05 16.7 °C —
30–31.05 18.7 °C —
31–1.06 17.0 °C —

czerwiec

Pierwsza tropikalna noc w czerwcu miała miejsce w nocy z 18 na 19 czerwca. Minimalna temperatura to 21.1 °C.

W czerwcu miała miejsce także noc z rekordowo wysoką temperaturą. Z 28 na 29 czerwca minimalna temperatura to 27.2 °C. Warto dodać, że 28 czerwca to rekordzista temperatury w ciągu dnia – 41.0 °C (13:30)

Noc Temp Status
1–2.06 15.0 °C —
2–3.06 17.0 °C —
3–4.06 15.6 °C —
4–5.06 18.1 °C —
5–6.06 12.9 °C —
6–7.06 16.8 °C —
7–8.06 16.1 °C —
8–9.06 18.4 °C —
9–10.06 15.3 °C —
10–11.06 11.7 °C —
11–12.06 10.1 °C —
12–13.06 15.4 °C —
13–14.06 13.3 °C —
14–15.06 12.4 °C —
15–16.06 12.6 °C —
16–17.06 17.0 °C —
17–18.06 19.5 °C ⚠️ o włos
18–19.06 21.1 °C 🥵 tropikalna
19–20.06 19.8 °C ⚠️ o włos
20–21.06 21.7 °C 🥵 tropikalna
21–22.06 21.7 °C 🥵 tropikalna
22–23.06 19.5 °C ⚠️ o włos
23–24.06 17.4 °C —
24–25.06 21.1 °C 🥵 tropikalna
25–26.06 24.5 °C 🥵 tropikalna
26–27.06 22.0 °C 🥵 tropikalna
27–28.06 26.7 °C 🥵 tropikalna
28–29.06 27.2 °C 🥵 tropikalna 🥵
29–30.06 25.1 °C 🥵 tropikalna
30–1.07 22.7 °C 🥵 tropikalna

lipiec

14 tropikalnych nocy

Noc Temp Status
1–2.07 18.1 °C —
2–3.07 20.1 °C 🥵tropikalna
3–4.07 17.0 °C —
4–5.07 17.9 °C —
5–6.07 17.1 °C —
6–7.07 19.2 °C —
7–8.07 14.3 °C —
8–9.07 16.2 °C —
9–10.07 22.4 °C 🥵 tropikalna
10–11.07 21.8 °C 🥵 tropikalna
11–12.07 22.6 °C 🥵 tropikalna
12–13.07 23.8 °C 🥵 tropikalna
13–14.07 23.1 °C 🥵 tropikalna
14–15.07 18.5 °C —
15–16.07 22.9 °C 🥵 tropikalna
16–17.07 22.8 °C 🥵 tropikalna
17–18.07 21.6 °C 🥵 tropikalna
18–19.07 15.7 °C —
19–20.07 17.1 °C —
20–21.07 15.7 °C —
21–22.07 18.0 °C —
22–23.07 14.2 °C —
23–24.07 16.2 °C —
24–25.07 19.4 °C —
25–26.07 23.1 °C 🥵 tropikalna
26–27.07 19.4 °C —
27–28.07 18.0 °C —
28–29.07 21.4 °C 🥵 tropikalna
29–30.07 26.1 °C 🥵 tropikalna
30–31.07 26.1 °C 🥵 tropikalna
31–1.08 22.5 °C 🥵 tropikalna

sierpień

9 tropikalnych nocy

Noc Temp Status
1–2.08 20.4 °C 🥵 tropikalna
2–3.08 19.2 °C —
3–4.08 23.5 °C 🥵 tropikalna
4–5.08 25.0 °C 🥵 tropikalna
5–6.08 26.6 °C 🥵 tropikalna
6–7.08 22.8 °C 🥵 tropikalna
7–8.08 18.6 °C —
8–9.08 15.8 °C —
9–10.08 20.3 °C 🥵 tropikalna
10–11.08 21.1 °C 🥵 tropikalna
11–12.08 15.0 °C —
12–13.08 15.4 °C —
13–14.08 16.7 °C —
14–15.08 18.6 °C —
15–16.08 21.7 °C 🥵 tropikalna
16–17.08 18.7 °C —
17–18.08 15.6 °C —
18–19.08 16.7 °C —
19–20.08 18.2 °C —
20–21.08 21.1 °C 🥵 tropikalna
21–22.08 15.0 °C —
22–23.08 14.0 °C —
23–24.08 13.3 °C —
24–25.08 13.0 °C —
25–26.08 12.8 °C —
26–27.08 14.4 °C —
27–28.08 17.0 °C —
28–29.08 18.8 °C —
29–30.08 16.0 °C —
30–31.08 18.3 °C —
31–1.09 17.1 °C —

wrzesień

jak na razie ;–) jedna

Noc Temp Status
1–2.09 16.9 °C —
2–3.09 17.0 °C —
3–4.09 19.9 °C ⚠️ o włos
4–5.09 17.0 °C —
5–6.09 14.6 °C —
6–7.09 11.6 °C —
7–8.09 14.2 °C —
8–9.09 20.3 °C 🥵 tropikalna
9–10.09 13.8 °C —

Podsumowanie okresu

  • Przeanalizowanych nocy: 133
  • Liczba nocy tropikalnych (≥ 20 °C): 35
  • Noce tuż pod progiem (19.5 °C – 19.9 °C): 4
  • Najchłodniejsza noc: 10.1 °C
  • Najcieplejsza noc (najwyższe minimum): 27.2 °C
  • Najdłuższa seria z rzędu: 7 nocy tropikalnych
    • Okres: 2026-06-24 do 2026-07-01

Dla porównania — w całym 2025 roku takie noce były trzy.

Po sugestii @jacekstaszczuk@101010.pl sprawdziłem ręcznie dane za rok 2025 zamiast w aplikacji, i dane są takie:

Podsumowanie okresu

  • Przeanalizowanych nocy: 154
  • Liczba nocy tropikalnych (≥ 20°C): 7
  • Noce tuż pod progiem (19.5°C – 19.9°C): 2
  • Najchłodniejsza noc: 4.0°C
  • Najcieplejsza noc (najwyższe minimum): 22.1°C

#pogoda #klimat #noctropikalna

— Marcin “czach” Trzaska reply-to: @czach@mastodon.argilus.online

Od kilku dni temu świat astronomii żyje nową supernową.

Supernowa – termin określający kilka rodzajów kosmicznych eksplozji powodujących powstanie na niebie niezwykle jasnego obiektu, który już po kilku tygodniach lub miesiącach staje się niemal niewidoczny.

SN 2026aaiv została odkryta 1 września 2026 roku przez ATLAS

Ponieważ, według prognoz, od północy (00:00) miały być idealne warunki do smażenia odpaliłem po 21:00 Seestara, żeby złapać coś przed i o północy zacząć smażyć NGC 7331. Niestety, ale znów prognozy mogłem sobie włożyć (.), bo co chwilę obiekty znikały za chmurami. Po północy było już trochę lepiej, ale i tak z_palca wywaliłem ponad 10% materiału.

Ostatecznie, udało się złożyć obraz ze 189 klatek po 30 sekund każda, czyli zaledwie 94.5 minuty. Noo, mało, mało. Za mało, aby uzyskać więcej szczegółów galaktyki NGC 7331. Będę jednak, o ile pogoda pozwoli, smażyć dalej.

Wracając do tematu. Spróbowałem to złożyć na dwa sposoby i uzyskałem takie dwa obrazy.

NGC 7331 wraz z SN 2026aaiv, wrzesień 2026, obraz uzyskany za pomocą skryptów Vectra NGC 7331 wraz z SN 2026aaiv, wrzesień 2026, obraz uzyskany metodą ręcznego dziergania w Sirilu ;-)

NGC 7331 wraz z SN 2026aaiv, wrzesień 2026, obraz uzyskany za pomocą skryptów Vectra

NGC 7331 wraz z SN 2026aaiv, wrzesień 2026, obraz uzyskany metodą ręcznego dziergania w Sirilu ;-) Dla porównania obraz z sierpnia 2025 roku.

NGC 7331, sierpień 2025

W prawym, dolnym rogu widoczny jest Kwintet Stephana

A będąc już w temacie supernowej warto przypomnieć opowiadanie A. C. Clarka, pt. Gwiazda.

Tutaj link do opowiadania w języku angielskim.

A tutaj A. C. Clark czyta swoje opowiadanie. Można włączyć napisy i ich automatyczne tłumaczenie na język polski

— Marcin “czach” Trzaska reply-to: @czach@mastodon.argilus.online

Zaskoczył mnie dziś smart ring. Znaczy aplikacja. A konkretnie powiadomienie z aplikacji.

Otóż zostałem powiadomiony o nadmiernej ekspozycji na światło słoneczne i abym zadbał o nawodnienie. Oooo

#ciekawostki

— Marcin “czach” Trzaska reply-to: @czach@mastodon.argilus.online

Nie mieszam się do polityki, gardzę nią (i politykami) i plwam na nią.

Ale!

Kliknij, aby przenieść się do świata polityki Z czasów, gdy polityka była, z przyczyn służbowych, w moim zainteresowaniu, pamiętam takie stwierdzenie pana prezesa, obchodzącego urodziny w tym samym dniu co ja. Otóż:

W każdym społeczeństwie jest taki element najbardziej zdemoralizowany, podły, animalny

Nie dawało mi to stwierdzenie spokoju.

Aż! Niedawno przeczytałem nagłówek artykułu

Sporo kontrowersji wzbudziło zachowanie posłanki PiS Dominiki Chorosińskiej, która zaszczekała podczas piątkowego wystąpienia w Sejmie wicepremiera, szefa MON Władysława Kosiniaka-Kamysza.

I wszystko jasne.

#polityka

— Marcin “czach” Trzaska reply-to: @czach@mastodon.argilus.online

Pokusiłem się zbratać z AI, aby sprawdzać pokrycie materiału w mozaice.

☝️
Powiązane wpisy

Seestar Mosaic Coverage, gdzie pojawił się pomysł.

Rekin po raz drugi, gdzie pomysł się rozwinął.

I wpadłem na pomysł, żeby to wykorzystać bardziej praktycznie i zapewnić sobie w miarę równomierne pokrycie.

Mam jakiś stary materiał z IC 1848 — 2617 klatek i zapuściłem skrypt. Oto efekt. wyrównanie mozaiki

Usunął 500 klatek, zmniejszając pokrycie środka i wyrównując tym samym nieco całość.

Teraz przekładając to praktykę:

  1. zgrywam pliki
  2. traktuję je ASTAPem (wywalam najgorsze)
  3. zapuszczam skrypt, aby nieco wyrównać pokrycie
  4. Siril [...]

Nazbieram czegoś nowego i przetestuję.

#Seestar #astrofotografia

— Marcin “czach” Trzaska reply-to: @czach@mastodon.argilus.online

Próbuję po raz drugi ogarnąć Dark Shark Nebula, tym razem na szerzej, to znaczy w trybie mozaiki x2 (czyli pole widzenia Seestara S50 x 4).

Pokrycie fragmentu nieba

Ciekawi mnie, jak to wyjdzie. Na razie za malo materiału, żeby cokolwiek zobaczyć.

Tak przy okazji — na dysku pojawiła się nowa wersja skryptu a tak wygląda wynik jego pracy: Pokrycie fragmentu nieba

#!/usr/bin/env python3
"""
seestar_mosaic_coverage.py

Wczytuje pliki FIT (Seestar, tryb mozaiki) z podanego folderu i rysuje
mape pokrycia nieba: kolorem pokazuje, ile klatek pokrywa dany fragment.
"""

import argparse
import glob
import os
import sys

import numpy as np
from astropy.io import fits
from astropy.wcs import WCS
import matplotlib.pyplot as plt
from matplotlib.path import Path as MplPath
from matplotlib.ticker import MaxNLocator

# ---------- odczyt naglowkow ----------

RA_KEYS = ["RA", "OBJCTRA", "CRVAL1"]
DEC_KEYS = ["DEC", "OBJCTDEC", "CRVAL2"]
ROT_KEYS = ["ROTATION", "PA", "ROTATANG", "ORIENTAT", "ROT"]


def _first_present(header, keys):
    for k in keys:
        if k in header:
            return header[k]
    return None


def _to_decimal_ra(val):
    if isinstance(val, (int, float)):
        return float(val)
    val = str(val).strip()
    if ":" in val:
        h, m, s = [float(x) for x in val.split(":")]
        return (h + m / 60 + s / 3600) * 15.0
    return float(val)


def _to_decimal_dec(val):
    if isinstance(val, (int, float)):
        return float(val)
    val = str(val).strip()
    sign = -1.0 if val.startswith("-") else 1.0
    val = val.lstrip("+-")
    if ":" in val:
        d, m, s = [float(x) for x in val.split(":")]
        return sign * (d + m / 60 + s / 3600)
    return float(val) * sign


def read_frame_pointing(path):
    header = fits.getheader(path)
    if "CTYPE1" in header and "CRVAL1" in header:
        try:
            wcs = WCS(header)
            footprint = wcs.calc_footprint(header=header)
            ra_c, dec_c = footprint.mean(axis=0)
            return ra_c, dec_c, 0.0, footprint
        except Exception:
            pass

    ra_raw = _first_present(header, RA_KEYS)
    dec_raw = _first_present(header, DEC_KEYS)
    if ra_raw is None or dec_raw is None:
        raise ValueError("brak RA/DEC w naglowku")

    ra_deg = _to_decimal_ra(ra_raw)
    dec_deg = _to_decimal_dec(dec_raw)
    rot_raw = _first_present(header, ROT_KEYS)
    rot_deg = float(rot_raw) if rot_raw is not None else 0.0

    return ra_deg, dec_deg, rot_deg, None


# ---------- projekcja i footprinty ----------

def gnomonic(ra_deg, dec_deg, ra0_deg, dec0_deg):
    ra, dec = np.radians(ra_deg), np.radians(dec_deg)
    ra0, dec0 = np.radians(ra0_deg), np.radians(dec0_deg)
    dra = ra - ra0
    denom = np.sin(dec0) * np.sin(dec) + np.cos(dec0) * np.cos(dec) * np.cos(dra)
    x = np.cos(dec) * np.sin(dra) / denom
    y = (np.cos(dec0) * np.sin(dec) - np.sin(dec0) * np.cos(dec) * np.cos(dra)) / denom
    return np.degrees(x), np.degrees(y)


def frame_corners_tangent(ra_c, dec_c, rot_deg, fov_w, fov_h, ra0, dec0):
    xc, yc = gnomonic(ra_c, dec_c, ra0, dec0)
    hw, hh = fov_w / 2, fov_h / 2
    local = np.array([(-hw, -hh), (hw, -hh), (hw, hh), (-hw, hh)])
    theta = np.radians(rot_deg)
    rot = np.array([[np.cos(theta), -np.sin(theta)],
                    [np.sin(theta), np.cos(theta)]])
    rotated = local @ rot.T
    return rotated + np.array([xc, yc])


def wcs_corners_tangent(footprint_radec, ra0, dec0):
    x, y = gnomonic(footprint_radec[:, 0], footprint_radec[:, 1], ra0, dec0)
    return np.column_stack([x, y])


# ---------- glowna logika ----------

def build_coverage(folder, fov_w, fov_h, grid_n):
    files = sorted(glob.glob(os.path.join(folder, "*.fit")) +
                   glob.glob(os.path.join(folder, "*.fits")))
    if not files:
        sys.exit(f"Nie znaleziono plikow .fit/.fits w: {folder}")

    raw = []
    for f in files:
        try:
            ra_c, dec_c, rot, footprint = read_frame_pointing(f)
            raw.append((f, ra_c, dec_c, rot, footprint))
        except Exception as e:
            print(f"[pomijam] {os.path.basename(f)}: {e}")

    if not raw:
        sys.exit("Nie udalo sie odczytac zadnej klatki.")

    ra0 = float(np.mean([r[1] for r in raw]))
    dec0 = float(np.mean([r[2] for r in raw]))

    polygons = []
    for f, ra_c, dec_c, rot, footprint in raw:
        if footprint is not None:
            corners = wcs_corners_tangent(footprint, ra0, dec0)
        else:
            corners = frame_corners_tangent(ra_c, dec_c, rot, fov_w, fov_h, ra0, dec0)
        polygons.append(corners)

    all_pts = np.vstack(polygons)
    pad = 0.15 * max(fov_w, fov_h)
    xmin, ymin = all_pts.min(axis=0) - pad
    xmax, ymax = all_pts.max(axis=0) + pad

    xs = np.linspace(xmin, xmax, grid_n)
    ys = np.linspace(ymin, ymax, grid_n)
    XX, YY = np.meshgrid(xs, ys)
    pts = np.column_stack([XX.ravel(), YY.ravel()])

    count = np.zeros(pts.shape[0], dtype=int)
    for corners in polygons:
        path = MplPath(corners)
        count += path.contains_points(pts)

    count = count.reshape(XX.shape)
    return XX, YY, count, polygons, len(raw), ra0, dec0


def plot_coverage(XX, YY, count, polygons, n_frames, ra0, dec0, out_path):
    # Ciemny motyw pasuje do astronomii i poprawia kontrast
    plt.style.use('dark_background')
    fig, ax = plt.subplots(figsize=(10, 9))

    # Maskowanie obszarów gdzie count == 0, żeby tło było czyste
    count_masked = np.ma.masked_equal(count, 0)

    max_count = max(1, int(count.max()))
    
    # Użycie palety 'plasma' lub 'magma' – są bardziej czytelne niż viridis na czarnym tle
    mesh = ax.pcolormesh(XX, YY, count_masked, shading="auto",
                         cmap="plasma", vmin=1, vmax=max_count)
    
    cbar = fig.colorbar(mesh, ax=ax, boundaries=np.arange(0.5, max_count + 1.5, 1))
    cbar.set_label("Liczba nakładających się klatek", color='white')
    cbar.locator = MaxNLocator(integer=True)
    cbar.update_ticks()

    # Rysowanie klatek tylko wtedy, gdy jest ich rozsądna liczba, 
    # żeby nie zrobić z wykresu białej plamy
    if len(polygons) <= 150:
        for corners in polygons:
            closed = np.vstack([corners, corners[0]])
            ax.plot(closed[:, 0], closed[:, 1], color="cyan", linewidth=0.3, alpha=0.4)

    ax.set_xlabel(f"Odsunięcie ΔRA [deg] względem RA={ra0:.3f}°", fontsize=11)
    ax.set_ylabel(f"Odsunięcie ΔDec [deg] względem Dec={dec0:.3f}°", fontsize=11)
    ax.set_title(f"Mapa pokrycia mozaiki ({n_frames} klatek)", fontsize=13, pad=12)
    
    ax.invert_xaxis()  # Konwencja astronomiczna: RA rośnie w lewo
    ax.set_aspect("equal")
    ax.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.3)

    fig.tight_layout()
    fig.savefig(out_path, dpi=200, facecolor=fig.get_facecolor(), edgecolor='none')
    print(f"Zapisano ulepszoną mapę: {out_path}")
    plt.show()


def main():
    ap = argparse.ArgumentParser(description="Mapa pokrycia mozaiki Seestar z plikow FIT.")
    ap.add_argument("folder", help="Folder z plikami .fit/.fits")
    ap.add_argument("--fov-w", type=float, default=0.75, help="Szerokość FOV w stopniach")
    ap.add_argument("--fov-h", type=float, default=1.33, help="Wysokość FOV w stopniach")
    ap.add_argument("--grid", type=int, default=500, help="Rozdzielczość siatki (domyślnie 500 dla wyższej gładkości)")
    ap.add_argument("--out", default="coverage_improved.png", help="Plik wyjściowy PNG")
    args = ap.parse_args()

    XX, YY, count, polygons, n, ra0, dec0 = build_coverage(
        args.folder, args.fov_w, args.fov_h, args.grid
    )
    plot_coverage(XX, YY, count, polygons, n, ra0, dec0, args.out)


if __name__ == "__main__":
    main()

#Seestar #S50 #astrofotografia

— Marcin “czach” Trzaska reply-to: @czach@mastodon.argilus.online

Poszliśmy na grzyby, bo znajomi znajomych wczoraj nazbierali w cholerę prawdziwków.

No to idziemy. Blisko mamy. Nooo… Przeszliśmy prawie 5 km i znaleźliśmy jedną kanię. Nawet grzybów niejadalnych nie było za wiele. Więcej chyba było butelek i puszek. :–(

Dobra, skoro tak las z nami pogrywa, to jedziemy tam gdzie wczoraj znajomi znajomych… Wchodzimy i… pierwsze co, to 4 dorodne prawdziwki. I to by było na tyle w temacie prawdziwki. Za to znaleźliśmy kilkanaście kań.

Tak w sam raz, aby zapełnić suszarkę.

Suszarka do grzybów - 100% ;-) #grzyby

— Marcin “czach” Trzaska reply-to: @czach@mastodon.argilus.online

Tak coś mnie tknęło… dawno nie robiłem żadnej mozaiki Seestarem. Nie robiłem, bo się zniechęciłem — jakoś nie wychodziło to najlepiej…

No i coś mnie tknęło, żeby sprawdzić pokrycie na mapie/obrazku…

I takie coś mam od Klaudiusza[^1]. ;–)

#!/usr/bin/env python3
"""
seestar_mosaic_coverage.py

Wczytuje pliki FIT (Seestar, tryb mozaiki) z podanego folderu i rysuje
mape pokrycia nieba: kolorem pokazuje, ile klatek pokrywa dany fragment.

Wymagania:
    pip install astropy numpy matplotlib

Uzycie:
    python seestar_mosaic_coverage.py /sciezka/do/folderu_z_fit
    python seestar_mosaic_coverage.py /sciezka --fov-w 0.75 --fov-h 1.33 --grid 400
"""

import argparse
import glob
import os
import sys

import numpy as np
from astropy.io import fits
from astropy.wcs import WCS
import matplotlib.pyplot as plt
from matplotlib.path import Path as MplPath

# ---------- odczyt naglowkow ----------

RA_KEYS = ["RA", "OBJCTRA", "CRVAL1"]
DEC_KEYS = ["DEC", "OBJCTDEC", "CRVAL2"]
ROT_KEYS = ["ROTATION", "PA", "ROTATANG", "ORIENTAT", "ROT"]


def _first_present(header, keys):
    for k in keys:
        if k in header:
            return header[k]
    return None


def _to_decimal_ra(val):
    """RA moze byc liczba (stopnie) albo string HH:MM:SS."""
    if isinstance(val, (int, float)):
        return float(val)
    val = str(val).strip()
    if ":" in val:
        h, m, s = [float(x) for x in val.split(":")]
        return (h + m / 60 + s / 3600) * 15.0
    return float(val)


def _to_decimal_dec(val):
    if isinstance(val, (int, float)):
        return float(val)
    val = str(val).strip()
    sign = -1.0 if val.startswith("-") else 1.0
    val = val.lstrip("+-")
    if ":" in val:
        d, m, s = [float(x) for x in val.split(":")]
        return sign * (d + m / 60 + s / 3600)
    return float(val) * sign


def read_frame_pointing(path):
    """Zwraca (ra_deg, dec_deg, rotation_deg, footprint_lub_None) dla jednej klatki FIT."""
    header = fits.getheader(path)

    # Sciezka 1: pelne WCS (jesli plik byl plate-solved, np. przez ASTAP)
    if "CTYPE1" in header and "CRVAL1" in header:
        try:
            wcs = WCS(header)
            footprint = wcs.calc_footprint(header=header)  # 4x2 [ra,dec] w stopniach
            ra_c, dec_c = footprint.mean(axis=0)
            return ra_c, dec_c, 0.0, footprint
        except Exception:
            pass

    # Sciezka 2: naglowek Seestara (RA/DEC srodka klatki + ewentualna rotacja)
    ra_raw = _first_present(header, RA_KEYS)
    dec_raw = _first_present(header, DEC_KEYS)
    if ra_raw is None or dec_raw is None:
        raise ValueError("brak RA/DEC w naglowku")

    ra_deg = _to_decimal_ra(ra_raw)
    dec_deg = _to_decimal_dec(dec_raw)
    rot_raw = _first_present(header, ROT_KEYS)
    rot_deg = float(rot_raw) if rot_raw is not None else 0.0

    return ra_deg, dec_deg, rot_deg, None


# ---------- projekcja i footprinty ----------

def gnomonic(ra_deg, dec_deg, ra0_deg, dec0_deg):
    """Rzutuje RA/Dec na plaszczyzne styczna wzgledem (ra0, dec0). Zwraca stopnie."""
    ra, dec = np.radians(ra_deg), np.radians(dec_deg)
    ra0, dec0 = np.radians(ra0_deg), np.radians(dec0_deg)
    dra = ra - ra0
    denom = np.sin(dec0) * np.sin(dec) + np.cos(dec0) * np.cos(dec) * np.cos(dra)
    x = np.cos(dec) * np.sin(dra) / denom
    y = (np.cos(dec0) * np.sin(dec) - np.sin(dec0) * np.cos(dec) * np.cos(dra)) / denom
    return np.degrees(x), np.degrees(y)


def frame_corners_tangent(ra_c, dec_c, rot_deg, fov_w, fov_h, ra0, dec0):
    """Rogi klatki (prostokat obrocony o rot_deg) w ukladzie stycznym."""
    xc, yc = gnomonic(ra_c, dec_c, ra0, dec0)
    hw, hh = fov_w / 2, fov_h / 2
    local = np.array([(-hw, -hh), (hw, -hh), (hw, hh), (-hw, hh)])
    theta = np.radians(rot_deg)
    rot = np.array([[np.cos(theta), -np.sin(theta)],
                    [np.sin(theta), np.cos(theta)]])
    rotated = local @ rot.T
    return rotated + np.array([xc, yc])


def wcs_corners_tangent(footprint_radec, ra0, dec0):
    x, y = gnomonic(footprint_radec[:, 0], footprint_radec[:, 1], ra0, dec0)
    return np.column_stack([x, y])


# ---------- glowna logika ----------

def build_coverage(folder, fov_w, fov_h, grid_n):
    files = sorted(glob.glob(os.path.join(folder, "*.fit")) +
                    glob.glob(os.path.join(folder, "*.fits")))
    if not files:
        sys.exit(f"Nie znaleziono plikow .fit/.fits w: {folder}")

    raw = []
    for f in files:
        try:
            ra_c, dec_c, rot, footprint = read_frame_pointing(f)
            raw.append((f, ra_c, dec_c, rot, footprint))
        except Exception as e:
            print(f"[pomijam] {os.path.basename(f)}: {e}")

    if not raw:
        sys.exit("Nie udalo sie odczytac zadnej klatki - sprawdz nazwy kluczy w naglowku (patrz ponizej).")

    ra0 = float(np.mean([r[1] for r in raw]))
    dec0 = float(np.mean([r[2] for r in raw]))

    polygons = []
    for f, ra_c, dec_c, rot, footprint in raw:
        if footprint is not None:
            corners = wcs_corners_tangent(footprint, ra0, dec0)
        else:
            corners = frame_corners_tangent(ra_c, dec_c, rot, fov_w, fov_h, ra0, dec0)
        polygons.append(corners)

    all_pts = np.vstack(polygons)
    pad = 0.1 * max(fov_w, fov_h)
    xmin, ymin = all_pts.min(axis=0) - pad
    xmax, ymax = all_pts.max(axis=0) + pad

    xs = np.linspace(xmin, xmax, grid_n)
    ys = np.linspace(ymin, ymax, grid_n)
    XX, YY = np.meshgrid(xs, ys)
    pts = np.column_stack([XX.ravel(), YY.ravel()])

    count = np.zeros(pts.shape[0], dtype=int)
    for corners in polygons:
        path = MplPath(corners)
        count += path.contains_points(pts)

    count = count.reshape(XX.shape)
    return XX, YY, count, polygons, len(raw), ra0, dec0


def plot_coverage(XX, YY, count, polygons, n_frames, ra0, dec0, out_path):
    fig, ax = plt.subplots(figsize=(9, 8))

    max_count = max(1, int(count.max()))
    mesh = ax.pcolormesh(XX, YY, count, shading="auto",
                          cmap="viridis", vmin=0, vmax=max_count)
    cbar = fig.colorbar(mesh, ax=ax)
    cbar.set_label("Liczba nakladajacych sie klatek")

    for corners in polygons:
        closed = np.vstack([corners, corners[0]])
        ax.plot(closed[:, 0], closed[:, 1], color="white", linewidth=0.4, alpha=0.6)

    ax.set_xlabel(f"Odsuniecie RA [deg] wzgledem {ra0:.3f}")
    ax.set_ylabel(f"Odsuniecie Dec [deg] wzgledem {dec0:.3f}")
    ax.set_title(f"Pokrycie mozaiki - {n_frames} klatek")
    ax.invert_xaxis()  # konwencja: RA rosnie w lewo
    ax.set_aspect("equal")

    fig.tight_layout()
    fig.savefig(out_path, dpi=150)
    print(f"Zapisano: {out_path}")
    plt.show()


def main():
    ap = argparse.ArgumentParser(description="Mapa pokrycia mozaiki Seestar z plikow FIT.")
    ap.add_argument("folder", help="Folder z plikami .fit/.fits")
    ap.add_argument("--fov-w", type=float, default=0.75,
                    help="Szerokosc FOV w stopniach (domyslnie 0.75 - Seestar S50)")
    ap.add_argument("--fov-h", type=float, default=1.33,
                    help="Wysokosc FOV w stopniach (domyslnie 1.33 - Seestar S50)")
    ap.add_argument("--grid", type=int, default=300,
                    help="Rozdzielczosc siatki liczacej nakladki (domyslnie 300)")
    ap.add_argument("--out", default="coverage.png", help="Plik wyjsciowy PNG")
    args = ap.parse_args()

    XX, YY, count, polygons, n, ra0, dec0 = build_coverage(
        args.folder, args.fov_w, args.fov_h, args.grid
    )
    plot_coverage(XX, YY, count, polygons, n, ra0, dec0, args.out)


if __name__ == "__main__":
    main()

Najpierw

$ python3 -m venv astro
$ source astro/bin/activate

a później, na przykład:

$ python seestar_mosaic_coverage.py SeestarS50/Messier/M\ 31_manual_mosaic
pokrycie mozaiki pokrycie mozaiki
pokrycie mozaiki pokrycie mozaiki

pokrycie M31 pokrycie NGC700

No i te dwa ostatnie obrazki pokazują mi dobitnie, dlaczego mozaiki były fallusowe.

[^1]: Claude AI

#seestar #mosaic #mozaika #astrophotography #astrofotografia

— Marcin “czach” Trzaska reply-to: @czach@mastodon.argilus.online