Script Sharpcap pour faire sa bibliothèque de Dark

Le labo avec les dernières avancées, c'est ici !
Répondre
darki
Messages : 4057
Inscription : 09 déc. 2024, 16:37
Localisation : Environs Aix en Provence

Script Sharpcap pour faire sa bibliothèque de Dark

Message par darki » 11 avr. 2026, 23:33

Bonsoir,

Devant refaire ma biblio de Darks pour Sharpcap et n'ayant pas envie de rester devant l'écran pour relancer pour chaque exposition (d'autant plus que je n'étais pas à la maison :lol:), j'ai demandé à mon nouvel ami de 30 ans, Claude AI, si il pouvait m'écrire un script pour faire ça.

Après quelques itération, on arrive à ceci que je vous partage des fois que ça puisse servir à certains

Code : Tout sélectionner

# ============================================================
# Dark Library Builder pour SharpCap Pro (IronPython)
# Sans dependance externe - natif SharpCap uniquement
# ============================================================

import time
import datetime
import os
import math
import clr

clr.AddReference("System.Windows.Forms")
clr.AddReference("System")
from System.Windows.Forms import MessageBox
# APRES
from System.IO import File, Directory, Path
import System.Environment

# ============================================================
# CONFIGURATION
# ============================================================

LATITUDE     = 45.00    # Aix-en-Provence
LONGITUDE    = 10.00
UTC_OFFSET   = 2        # Heure ete France (+1 en hiver)

GAIN         = 210
OFFSET_VAL   = 6
N_FRAMES     = 31

EXPOSITIONS  = [1, 2, 5, 10, 15, 30, 45, 60, 90, 120]

# ============================================================
# CALCUL CREPUSCULE NAUTIQUE (-12 degres)
# ============================================================

def calcul_crepuscule_nautique(lat, lon, date, utc_offset):
    a   = (14 - date.month) // 12
    y   = date.year + 4800 - a
    m   = date.month + 12 * a - 3
    jdn = date.day + (153*m+2)//5 + 365*y + y//4 - y//100 + y//400 - 32045
    jd  = jdn - 0.5

    n   = jd - 2451545.0
    L   = (280.46 + 0.9856474 * n) % 360
    g   = math.radians((357.528 + 0.9856003 * n) % 360)
    lam = math.radians(L + 1.915 * math.sin(g) + 0.020 * math.sin(2*g))
    eps = math.radians(23.439 - 0.0000004 * n)
    dec = math.asin(math.sin(eps) * math.sin(lam))

    lat_r     = math.radians(lat)
    cos_omega = (math.sin(math.radians(-12.0))
                 - math.sin(lat_r) * math.sin(dec)) \
                / (math.cos(lat_r) * math.cos(dec))

    if abs(cos_omega) > 1.0:
        return None

    omega    = math.degrees(math.acos(cos_omega))
    EqT      = (L - math.degrees(math.atan2(
                    math.cos(eps) * math.sin(lam), math.cos(lam)))) / 15.0
    midi_utc = 12.0 - lon / 15.0 - EqT / 60.0
    h_utc    = midi_utc + omega / 15.0

    hh = int(h_utc)
    mm = int((h_utc - hh) * 60)
    ss = int(((h_utc - hh) * 60 - mm) * 60)

    try:
        dt_utc = datetime.datetime(date.year, date.month, date.day,
                                   hh % 24, mm, ss)
        return dt_utc + datetime.timedelta(hours=utc_offset)
    except:
        return None

# ============================================================
# ECRITURE FITS MINIMAL SANS ASTROPY
# Ecrit un fichier FITS 16 bits valide a la main
# ============================================================

def ecrire_fits_uint16(chemin, data_uint16, metadata):
    """
    Ecrit un fichier FITS 16 bits non signe (BZERO=32768) sans astropy.
    data_uint16 : liste plate de valeurs int (ligne par ligne)
    metadata    : dict avec NAXIS1, NAXIS2, EXPTIME, GAIN, OFFSET, NCOMBINE
    """
    import struct

    def fits_card(key, value, comment=""):
        if isinstance(value, bool):
            v = "T" if value else "F"
            card = "{:<8}= {:>20} / {:<47}".format(key, v, comment)
        elif isinstance(value, int):
            card = "{:<8}= {:>20} / {:<47}".format(key, value, comment)
        elif isinstance(value, float):
            card = "{:<8}= {:>20.10G} / {:<47}".format(key, value, comment)
        elif isinstance(value, str):
            s = "'{:<8}'".format(value[:18])
            card = "{:<8}= {:<20} / {:<47}".format(key, s, comment)
        else:
            card = "{:<8}= {:<20} / {:<47}".format(key, str(value), comment)
        return card[:80].ljust(80)

    naxis1   = metadata["NAXIS1"]
    naxis2   = metadata["NAXIS2"]
    exptime  = metadata["EXPTIME"]
    gain     = metadata["GAIN"]
    offset   = metadata["OFFSET"]
    ncombine = metadata["NCOMBINE"]
    dateobs  = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")

    # Construction header FITS
    cards = []
    cards.append(fits_card("SIMPLE", True,    "FITS standard"))
    cards.append(fits_card("BITPIX", 16,       "16 bits entiers signes"))
    cards.append(fits_card("NAXIS",  2,        "Nombre d axes"))
    cards.append(fits_card("NAXIS1", naxis1,   "Largeur pixels"))
    cards.append(fits_card("NAXIS2", naxis2,   "Hauteur pixels"))
    cards.append(fits_card("BZERO",  32768,    "Decalage uint16"))
    cards.append(fits_card("BSCALE", 1,        "Echelle"))
    cards.append(fits_card("EXPTIME", float(exptime), "Exposition secondes"))
    cards.append(fits_card("GAIN",    gain,    "Gain camera"))
    cards.append(fits_card("OFFSET",  offset,  "Offset camera"))
    cards.append(fits_card("NCOMBINE",ncombine,"Nb poses empilees"))
    cards.append(fits_card("IMAGETYP","Dark Frame", "Type image"))
    cards.append(fits_card("FRAME",  "Dark",   "Type frame"))
    cards.append(fits_card("DATE-OBS", dateobs,"Date UTC capture"))
    cards.append("END" + " " * 77)

    # Padding header a multiple de 2880 octets
    header_bytes = bytearray()
    for c in cards:
        header_bytes.extend(c.encode("ascii"))
    while len(header_bytes) % 2880 != 0:
        header_bytes.extend(b" ")

    # Conversion donnees int16 signes (BZERO=32768)
    # uint16 -> int16 : soustraire 32768
    data_int16 = []
    for v in data_uint16:
        signed = int(v) - 32768
        data_int16.append(signed)

    # Pack en big-endian int16 (standard FITS)
    data_bytes = bytearray()
    for v in data_int16:
        data_bytes.extend(struct.pack(">h", max(-32768, min(32767, v))))

    # Padding donnees a multiple de 2880
    while len(data_bytes) % 2880 != 0:
        data_bytes.extend(b"\x00")

    # Ecriture fichier
    with open(chemin, "wb") as f:
        f.write(header_bytes)
        f.write(data_bytes)

# ============================================================
# CAPTURE UNE SERIE DE DARKS
# ============================================================

def capturer_serie(cam, expo_sec, n_frames, gain, offset_val, dark_lib):

    expo_ms = float(expo_sec * 1000)
    print("")
    print("=== Exposition : " + str(expo_sec) + "s | " +
          str(n_frames) + " poses ===")

    # Reglage gain
    try:
        cam.Controls.Gain.Value = gain
    except:
        print("  WARN : gain non regle")

    # Reglage offset
    try:
        ctrl = cam.Controls.Find(lambda x: x.Name == "Offset")
        if ctrl is not None:
            ctrl.Value = offset_val
    except:
        print("  WARN : offset non regle")

    # Reglage exposition
    cam.Controls.Exposure.Automatic = False
    cam.Controls.Exposure.ExposureMs = expo_ms

    # Format FITS
    try:
        cam.Controls.OutputFormat.Value = "FITS files (*.fits)"
    except:
        try:
            cam.Controls.OutputFormat.Value = "FITS Files (*.fits)"
        except:
            print("  WARN : format FITS non disponible, PNG utilise")

    # Dossier temporaire pour les brutes
    tmp_dir = Path.Combine(Path.GetTempPath(), "SharpCap_Dark_Tmp")
    if not Directory.Exists(tmp_dir):
        Directory.CreateDirectory(tmp_dir)

    # Stabilisation camera (2 poses ignorees)
    print("  Stabilisation...")
    time.sleep(float(expo_sec) * 2.0 + 2.0)

    # --- Accumulation ---
    # On utilise CaptureSingleFrameTo puis on lit
    # le fichier FITS brut avec struct (pas astropy)
    import struct

    def lire_fits_brut(chemin_fits):
        """Lit les donnees d un FITS 16 bits, retourne liste de int"""
        with open(chemin_fits, "rb") as f:
            raw = f.read()

        # Cherche fin du header (bloc END)
        data_start = 0
        for i in range(0, len(raw), 2880):
            bloc = raw[i:i+2880].decode("ascii", errors="replace")
            if "END" + " "*77 in bloc or bloc.rstrip().endswith("END"):
                data_start = i + 2880
                break

        # Lecture NAXIS1 / NAXIS2 depuis header
        header_str = raw[:data_start].decode("ascii", errors="replace")
        naxis1, naxis2 = 0, 0
        bzero = 32768
        for line in [header_str[i:i+80] for i in range(0, data_start, 80)]:
            key = line[:8].strip()
            val = line[10:30].strip().replace("'", "").strip()
            try:
                if key == "NAXIS1": naxis1 = int(val)
                if key == "NAXIS2": naxis2 = int(val)
                if key == "BZERO":  bzero  = int(float(val))
            except:
                pass

        n_pixels = naxis1 * naxis2
        data_raw = raw[data_start: data_start + n_pixels * 2]
        pixels = struct.unpack(">" + "h" * n_pixels, data_raw)
        # Reconversion uint16
        pixels_uint = [p + bzero for p in pixels]
        return pixels_uint, naxis1, naxis2

    accumulator = None
    naxis1_ref  = 0
    naxis2_ref  = 0
    n_ok        = 0

    for i in range(n_frames):
        tmp_file = Path.Combine(tmp_dir, "_dark_tmp_" + str(i) + ".fits")
        try:
            cam.CaptureSingleFrameTo(tmp_file)
            # Attente fin de pose + marge securite
            time.sleep(float(expo_sec) + 2.0)

            pixels, nx, ny = lire_fits_brut(tmp_file)

            if accumulator is None:
                accumulator = [float(p) for p in pixels]
                naxis1_ref  = nx
                naxis2_ref  = ny
            else:
                for j in range(len(pixels)):
                    accumulator[j] += float(pixels[j])

            if File.Exists(tmp_file):
                File.Delete(tmp_file)

            n_ok += 1
            print("  Pose " + str(i+1) + "/" + str(n_frames) + " OK")

        except Exception as e:
            print("  ERREUR pose " + str(i+1) + " : " + str(e))
            if File.Exists(tmp_file):
                File.Delete(tmp_file)

    if accumulator is None or n_ok == 0:
        print("  ECHEC : aucune pose reussie !")
        return False

    # Moyenne
    master = [int(round(v / n_ok)) for v in accumulator]

    # Dossier dark library structure SharpCap
    cam_name = cam.DeviceName.replace(" ", "_").replace("/", "-")
    res_str  = str(naxis1_ref) + "x" + str(naxis2_ref)
    sous_dir = Path.Combine(dark_lib, cam_name, res_str,
                            "G" + str(gain) + "_O" + str(offset_val),
                            str(expo_sec) + "s")
    if not Directory.Exists(sous_dir):
        Directory.CreateDirectory(sous_dir)

    ts       = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
    nom_fits = Path.Combine(sous_dir,
               "MasterDark_G" + str(gain) +
               "_O" + str(offset_val) +
               "_" + str(expo_sec) + "s" +
               "_" + str(n_ok) + "f_" + ts + ".fits")

    ecrire_fits_uint16(nom_fits, master, {
        "NAXIS1":   naxis1_ref,
        "NAXIS2":   naxis2_ref,
        "EXPTIME":  expo_sec,
        "GAIN":     gain,
        "OFFSET":   offset_val,
        "NCOMBINE": n_ok
    })

    print("  ==> Sauvegarde : " + nom_fits)
    return True

# ============================================================
# PROGRAMME PRINCIPAL
# ============================================================

def main():
    cam = SharpCap.SelectedCamera
    if cam is None:
        MessageBox.Show("Aucune camera selectionnee !", "Erreur")
        return

    # Recupere le dossier dark library depuis les settings SharpCap
    try:
        dark_lib = SharpCap.Settings.DarkLibraryFolder
        if not dark_lib or not Directory.Exists(dark_lib):
            raise Exception("Dossier introuvable")
    except:
        # Fallback sans System.Environment
        import os
        dark_lib = os.path.join(
            os.environ.get("LOCALAPPDATA", "C:\\Users\\Public"),
            "SharpCap", "Dark Library")
        if not Directory.Exists(dark_lib):
            Directory.CreateDirectory(dark_lib)

    print("========================================")
    print("  Dark Library Builder")
    print("  Camera   : " + cam.DeviceName)
    print("  Gain     : " + str(GAIN))
    print("  Offset   : " + str(OFFSET_VAL))
    print("  Poses    : " + str(N_FRAMES))
    print("  Dark Lib : " + dark_lib)
    print("  Lat/Lon  : " + str(LATITUDE) + " / " + str(LONGITUDE))
    print("========================================")

    # Calcul et attente crepuscule nautique
    today      = datetime.date.today()
    crepuscule = calcul_crepuscule_nautique(LATITUDE, LONGITUDE,
                                            today, UTC_OFFSET)
    maintenant = datetime.datetime.now()

    if crepuscule is None:
        print("Calcul crepuscule impossible - demarrage immediat")
    elif maintenant >= crepuscule:
        print("Crepuscule deja passe - demarrage immediat")
    else:
        print("Crepuscule nautique : " + crepuscule.strftime("%H:%M:%S"))
        print("Heure actuelle     : " + maintenant.strftime("%H:%M:%S"))
        attente_s = int((crepuscule - maintenant).total_seconds())
        print("Attente : " + str(attente_s // 60) + " min " +
              str(attente_s % 60) + " s")

        # Attente par tranches de 60s maximum
        # pour rester interruptible
        while datetime.datetime.now() < crepuscule:
            restant = (crepuscule - datetime.datetime.now()).total_seconds()
            if restant <= 0:
                break
            # Dort au maximum 60s par tranche
            tranche = min(60.0, restant)
            if restant % 300 < 61 and restant > 60:
                print("  Debut dans " + str(int(restant // 60)) + " min")
            time.sleep(tranche)

        print("==> Crepuscule atteint - debut captures !")

    # Boucle captures
    resultats = []
    for expo in EXPOSITIONS:
        ok = capturer_serie(cam, expo, N_FRAMES, GAIN, OFFSET_VAL, dark_lib)
        resultats.append((expo, "OK" if ok else "ECHEC"))
        time.sleep(3)

    # Rapport
    print("")
    print("========================================")
    print("  RAPPORT FINAL")
    for expo, statut in resultats:
        ligne = "  " + str(expo) + "s : " + statut
        print(ligne)
    print("========================================")
    MessageBox.Show("Dark Library terminee !", "Termine")

main()
Vous coller ça dans la partie du bas de la console python de sharpcap, vous sauvegardez pour pas le perdre et en cliquant sur la flèche verte, ça fait le taf pendant que vous faites autre chose.
image.png
Dernière modification par darki le 13 avr. 2026, 16:16, modifié 1 fois.

pejive
Messages : 11401
Inscription : 09 avr. 2019, 05:43
Localisation : 33

Script Sharpcap pour faire sa bibliothèque de Dark

Message par pejive » 12 avr. 2026, 08:53

je ne comprends pas l'utilité de calculer l'heure du crépuscule pour faire un dark :?:

darki
Messages : 4057
Inscription : 09 déc. 2024, 16:37
Localisation : Environs Aix en Provence

Script Sharpcap pour faire sa bibliothèque de Dark

Message par darki » 12 avr. 2026, 10:10

Parce que je voulais lancer ça en plein jour avant de me casser de la maison et éviter d'éventuelles fuites de lumière malgré la bâche. Du coup je voulais que ça démarre quand il fait sombre

Avatar de l’utilisateur
Olivier-Fantasy
Messages : 10665
Inscription : 14 oct. 2020, 23:03
Localisation : Région parisienne (92)

Script Sharpcap pour faire sa bibliothèque de Dark

Message par Olivier-Fantasy » 12 avr. 2026, 10:33

Ben dis donc, fallait y penser :clap:

Est-ce qu'on peut faire sauter la partie "Démarrer après la tombée de la nuit", si on veut justement profiter de la journée pour faire ses darks ? (à la cave, pour ma part, c'est parfaitement noir et frais : nickel !)

Faudra vérifier quand même que les darks soient bien fonctionnels (on ne sait jamais) ;)

Le cran d'après, ce serait de pouvoir lui faire enchaîner la prise de darks sur deux séries de "gain-offset" différentes (quand on fait des darks pour le RVB et pour le SHO ensuite, ce ne sont plus les mêmes gain, donc plus les mêmes offset non plus). On peut relancer une autre fois ton script... mais s'il savait faire directement les deux séries, on n'aurait plus qu'à revenir récolter 8h après ! :D

darki
Messages : 4057
Inscription : 09 déc. 2024, 16:37
Localisation : Environs Aix en Provence

Script Sharpcap pour faire sa bibliothèque de Dark

Message par darki » 12 avr. 2026, 20:32

Oui et oui.

Je vais essayer de documenter un peu, ou de demander a Claude d'implémenter des questions pour choisir ce qu'on veut faire

darki
Messages : 4057
Inscription : 09 déc. 2024, 16:37
Localisation : Environs Aix en Provence

Script Sharpcap pour faire sa bibliothèque de Dark

Message par darki » 12 avr. 2026, 20:33

Olivier-Fantasy a écrit :
12 avr. 2026, 10:33
Faudra vérifier quand même que les darks soient bien fonctionnels (on ne sait jamais
C'est prévu, Claude m'a meme proposé de me faire un script pour ça, mais je vérifierai aussi sur le ciel dès que possible

Avatar de l’utilisateur
Olivier-Fantasy
Messages : 10665
Inscription : 14 oct. 2020, 23:03
Localisation : Région parisienne (92)

Script Sharpcap pour faire sa bibliothèque de Dark

Message par Olivier-Fantasy » 12 avr. 2026, 20:40

darki a écrit :
12 avr. 2026, 20:32
implémenter des questions pour choisir ce qu'on veut faire
Chouette !! :-D

darki
Messages : 4057
Inscription : 09 déc. 2024, 16:37
Localisation : Environs Aix en Provence

Script Sharpcap pour faire sa bibliothèque de Dark

Message par darki » 12 avr. 2026, 21:01

Déja voila le résultat du script de vérification
----------------------------------------------------------------------
Fichier : MasterDark_G210_O6_5s_31f_20260411_234223.fits
Resolution : 3856x2180 | Taille : 16.0 Mo
Exposition : 5.0s | Gain : 210 | Offset : 6 | Poses : 31
Min : 0 | Max : 13936 | Mediane : 387 | Moyenne : 387.5 | Sigma : 4.4000000000000004
Pixels chauds : 2615 | Pixels zero : 13
Statut : OK
----------------------------------------------------------------------
Fichier : MasterDark_G210_O6_60s_31f_20260412_014147.fits
Resolution : 3856x2180 | Taille : 16.0 Mo
Exposition : 60.0s | Gain : 210 | Offset : 6 | Poses : 31
Min : 0 | Max : 65520 | Mediane : 388 | Moyenne : 388.39999999999998 | Sigma : 4.5
Pixels chauds : 2627 | Pixels zero : 13
Statut : OK
----------------------------------------------------------------------
Fichier : MasterDark_G210_O6_90s_31f_20260412_023722.fits
Resolution : 3856x2180 | Taille : 16.0 Mo
Exposition : 90.0s | Gain : 210 | Offset : 6 | Poses : 31
Min : 0 | Max : 65520 | Mediane : 388 | Moyenne : 388.69999999999999 | Sigma : 4.5
Pixels chauds : 2661 | Pixels zero : 13
Statut : OK
======================================================================
BILAN : 10 OK | 0 avertissements
======================================================================
Et le code de ce scripts

Code : Tout sélectionner

# ============================================================
# Verification master darks - Dark Library Builder
# A lancer dans un Python externe (astropy requis)
# ============================================================

import os
import struct
import datetime

DARK_LIB = r"C:\Users\alain\AppData\Local\SharpCap\Dark Library"

# ============================================================
# LECTURE FITS SANS ASTROPY (compatible IronPython SharpCap)
# ============================================================

def lire_fits_stats(chemin):
    """Lit un FITS 16 bits et retourne les stats essentielles"""
    with open(chemin, "rb") as f:
        raw = f.read()

    # Lecture header
    data_start = 0
    for i in range(0, len(raw), 2880):
        bloc = raw[i:i+2880].decode("ascii", errors="replace")
        if "END" in bloc:
            data_start = i + 2880
            break

    header_str = raw[:data_start].decode("ascii", errors="replace")
    naxis1, naxis2, bzero = 0, 0, 32768
    exptime, gain, offset, ncombine = 0, 0, 0, 0
    imagetyp = ""

    for line in [header_str[i:i+80] for i in range(0, data_start, 80)]:
        key = line[:8].strip()
        val = line[10:30].strip().replace("'", "").strip()
        try:
            if key == "NAXIS1":   naxis1   = int(val)
            if key == "NAXIS2":   naxis2   = int(val)
            if key == "BZERO":    bzero    = int(float(val))
            if key == "EXPTIME":  exptime  = float(val)
            if key == "GAIN":     gain     = int(float(val))
            if key == "OFFSET":   offset   = int(float(val))
            if key == "NCOMBINE": ncombine = int(float(val))
            if key == "IMAGETYP": imagetyp = val
        except:
            pass

    n_pixels = naxis1 * naxis2
    if n_pixels == 0:
        return None

    data_raw = raw[data_start: data_start + n_pixels * 2]
    if len(data_raw) < n_pixels * 2:
        return None

    pixels_signed = struct.unpack(">" + "h" * n_pixels, data_raw)
    pixels = [p + bzero for p in pixels_signed]

    # Stats manuelles sans numpy
    pixels_sorted = sorted(pixels)
    n = len(pixels_sorted)
    minimum  = pixels_sorted[0]
    maximum  = pixels_sorted[-1]
    median   = pixels_sorted[n // 2]
    moyenne  = sum(pixels) / n

    # Ecart type (sur echantillon de 10000 pixels pour la vitesse)
    step     = max(1, n // 10000)
    sample   = pixels[::step]
    mean_s   = sum(sample) / len(sample)
    variance = sum((x - mean_s)**2 for x in sample) / len(sample)
    ecart_type = variance ** 0.5

    # Pixels chauds (> moyenne + 10*ecart_type)
    seuil_chaud = mean_s + 10 * ecart_type
    px_chauds   = sum(1 for p in pixels if p > seuil_chaud)

    # Pixels a zero
    px_zero = sum(1 for p in pixels if p == 0)

    return {
        "fichier":    os.path.basename(chemin),
        "resolution": str(naxis1) + "x" + str(naxis2),
        "exptime":    exptime,
        "gain":       gain,
        "offset":     offset,
        "ncombine":   ncombine,
        "imagetyp":   imagetyp,
        "min":        minimum,
        "max":        maximum,
        "median":     median,
        "moyenne":    round(moyenne, 1),
        "ecart_type": round(ecart_type, 1),
        "px_chauds":  px_chauds,
        "px_zero":    px_zero,
        "n_pixels":   n_pixels,
        "taille_mb":  round(os.path.getsize(chemin) / 1024 / 1024, 1)
    }

def statut(stats):
    """Evalue la qualite du dark"""
    alertes = []

    # Mediane coherente avec offset camera
    if stats["median"] < 100:
        alertes.append("WARN mediane tres basse - bias peut-etre soustrait ?")
    if stats["median"] > 5000:
        alertes.append("WARN mediane elevee - fuite de lumiere possible ?")

    # Pixels chauds
    pct_chauds = 100.0 * stats["px_chauds"] / stats["n_pixels"]
    if pct_chauds > 1.0:
        alertes.append("WARN " + str(round(pct_chauds, 2)) +
                       "% pixels chauds - normal sur longues poses")

    # Pixels a zero suspects
    pct_zero = 100.0 * stats["px_zero"] / stats["n_pixels"]
    if pct_zero > 0.1:
        alertes.append("WARN " + str(round(pct_zero, 3)) +
                       "% pixels a zero - clipping possible")

    # Nombre de poses
    if stats["ncombine"] < 20:
        alertes.append("WARN seulement " + str(stats["ncombine"]) +
                       " poses empilees - qualite reduite")

    if not alertes:
        return "OK"
    return " | ".join(alertes)

# ============================================================
# SCAN ET RAPPORT
# ============================================================

print("=" * 70)
print("  Verification Dark Library")
print("  " + DARK_LIB)
print("  " + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
print("=" * 70)

fits_trouves = []
for root, dirs, files in os.walk(DARK_LIB):
    for f in files:
        if f.lower().endswith(".fits") and "MasterDark" in f:
            fits_trouves.append(os.path.join(root, f))

fits_trouves.sort()

if not fits_trouves:
    print("Aucun master dark trouve dans " + DARK_LIB)
else:
    print("Fichiers trouves : " + str(len(fits_trouves)))
    print("")

    ok_count   = 0
    warn_count = 0

    for chemin in fits_trouves:
        print("-" * 70)
        stats = lire_fits_stats(chemin)

        if stats is None:
            print("ERREUR lecture : " + os.path.basename(chemin))
            warn_count += 1
            continue

        s = statut(stats)
        if s == "OK":
            ok_count += 1
        else:
            warn_count += 1

        print("Fichier    : " + stats["fichier"])
        print("Resolution : " + stats["resolution"] +
              "  |  Taille : " + str(stats["taille_mb"]) + " Mo")
        print("Exposition : " + str(stats["exptime"]) + "s" +
              "  |  Gain : " + str(stats["gain"]) +
              "  |  Offset : " + str(stats["offset"]) +
              "  |  Poses : " + str(stats["ncombine"]))
        print("Min : " + str(stats["min"]) +
              "  |  Max : "    + str(stats["max"]) +
              "  |  Mediane : " + str(stats["median"]) +
              "  |  Moyenne : " + str(stats["moyenne"]) +
              "  |  Sigma : "  + str(stats["ecart_type"]))
        print("Pixels chauds : " + str(stats["px_chauds"]) +
              "  |  Pixels zero : " + str(stats["px_zero"]))
        print("Statut : " + s)

    print("=" * 70)
    print("BILAN : " + str(ok_count) + " OK  |  " +
          str(warn_count) + " avertissements")
    print("=" * 70)

darki
Messages : 4057
Inscription : 09 déc. 2024, 16:37
Localisation : Environs Aix en Provence

Script Sharpcap pour faire sa bibliothèque de Dark

Message par darki » 12 avr. 2026, 22:10

Voila un script qui demande gentiment si on veut démarrer tout de suite ou au crépuscule (nautique ou astronomique)

Il permet de définir des serie d'exposition pour plusieurs couples de gain / offset que vous configurez dans le script dans la partie suivante (vers le début) :
# ============================================================
# CONFIGURATION
# ============================================================

LATITUDE = 45.00 # Aix-en-Provence
LONGITUDE = 10.00
UTC_OFFSET = 2 # +1 en hiver, +2 en ete

# Mode de demarrage :
# 'immediate' -> demarre tout de suite sans dialog
# 'nautique' -> attend crepuscule nautique (-12 deg)
# 'astronomique' -> attend crepuscule astronomique (-18 deg)
# None -> affiche la boite de dialogue au lancement
START_MODE = None

N_FRAMES = 31

# Couples (gain, offset, [expositions en secondes])
SERIES = [
(210, 6, [1, 2, 5, 10, 15, 30, 45, 60, 90, 120]),
(310, 12, [1, 2, 5, 10, 15, 30]),
]


# Laisser vide pour utiliser le dossier SharpCap automatiquement
# ou forcer un chemin : r"C:\MonDossier\Dark Library"
DARK_LIB = ""
Le script :

Code : Tout sélectionner

# ============================================================
# Dark Library Builder v2 pour SharpCap Pro (IronPython)
# - Demarrage immediat / crepuscule nautique / astronomique
# - Multi couples gain/offset avec expositions independantes
# - Boucle d'attente corrigee (tranches 60s)
# ============================================================

import time
import datetime
import os
import math
import struct
import clr

clr.AddReference("System.Windows.Forms")
clr.AddReference("System.Drawing")
from System.Windows.Forms import (MessageBox, MessageBoxButtons,
                                  MessageBoxIcon, DialogResult)
from System.Drawing import Size, Point, Font, FontStyle
from System.IO import File, Directory, Path

# ============================================================
# CONFIGURATION
# ============================================================

LATITUDE     = 45.00    # Aix-en-Provence
LONGITUDE    = 10.00
UTC_OFFSET = 2       # +1 en hiver, +2 en ete

# Mode de demarrage :
# 'immediate'     -> demarre tout de suite sans dialog
# 'nautique'      -> attend crepuscule nautique (-12 deg)
# 'astronomique'  -> attend crepuscule astronomique (-18 deg)
# None            -> affiche la boite de dialogue au lancement
START_MODE = None

N_FRAMES = 31

# Couples (gain, offset, [expositions en secondes])
SERIES = [
    (210, 6,  [1, 2, 5, 10, 15, 30, 45, 60, 90, 120]),
    (310, 12, [1, 2, 5, 10, 15, 30]),
]

# Laisser vide pour utiliser le dossier SharpCap automatiquement
# ou forcer un chemin : r"C:\MonDossier\Dark Library"
DARK_LIB = ""

# ============================================================
# BOITE DE DIALOGUE MODE DEMARRAGE
# ============================================================

def choisir_mode():
    from System.Windows.Forms import (MessageBox, MessageBoxButtons,
                                      MessageBoxIcon, DialogResult, Form)
    from System.Drawing import Size

    owner = Form()
    owner.TopMost = True
    owner.Size    = Size(0, 0)
    owner.Show()
    owner.BringToFront()
    owner.Activate()
    owner.Focus()
    owner.Hide()

    rep1 = MessageBox.Show(
        owner,
        "Demarrer immediatement ?\n\n" +
        "OUI  -> demarrage immediat\n" +
        "NON  -> attente crepuscule",
        "Dark Library Builder v2",
        MessageBoxButtons.YesNo,
        MessageBoxIcon.Question)

    if rep1 == DialogResult.Yes:
        owner.Dispose()
        return "immediate"

    rep2 = MessageBox.Show(
        owner,
        "Choisir le type de crepuscule :\n\n" +
        "OUI  -> Nautique     (-12 deg)\n" +
        "NON  -> Astronomique (-18 deg)",
        "Dark Library Builder v2",
        MessageBoxButtons.YesNo,
        MessageBoxIcon.Question)

    owner.Dispose()

    if rep2 == DialogResult.Yes:
        return "nautique"
    else:
        return "astronomique"

# ============================================================
# CALCUL CREPUSCULE
# ============================================================

def calcul_crepuscule(lat, lon, date, depression_deg, utc_offset):
    """
    Calcule l'heure locale du crepuscule du soir.
    depression_deg : 12 = nautique, 18 = astronomique
    """
    a   = (14 - date.month) // 12
    y   = date.year + 4800 - a
    m   = date.month + 12 * a - 3
    jdn = date.day + (153*m+2)//5 + 365*y + y//4 - y//100 + y//400 - 32045
    jd  = jdn - 0.5

    n   = jd - 2451545.0
    L   = (280.46 + 0.9856474 * n) % 360
    g   = math.radians((357.528 + 0.9856003 * n) % 360)
    lam = math.radians(L + 1.915 * math.sin(g) + 0.020 * math.sin(2*g))
    eps = math.radians(23.439 - 0.0000004 * n)
    dec = math.asin(math.sin(eps) * math.sin(lam))

    lat_r     = math.radians(lat)
    cos_omega = (math.sin(math.radians(-depression_deg))
                 - math.sin(lat_r) * math.sin(dec)) \
                / (math.cos(lat_r) * math.cos(dec))

    if abs(cos_omega) > 1.0:
        return None

    omega    = math.degrees(math.acos(cos_omega))
    EqT      = (L - math.degrees(math.atan2(
                    math.cos(eps) * math.sin(lam),
                    math.cos(lam)))) / 15.0
    midi_utc = 12.0 - lon / 15.0 - EqT / 60.0
    h_utc    = midi_utc + omega / 15.0

    hh = int(h_utc) % 24
    mm = int((h_utc - int(h_utc)) * 60)
    ss = int(((h_utc - int(h_utc)) * 60 - mm) * 60)

    try:
        dt_utc = datetime.datetime(date.year, date.month, date.day, hh, mm, ss)
        return dt_utc + datetime.timedelta(hours=utc_offset)
    except:
        return None

def attendre_crepuscule(mode):
    depression = 12.0 if mode == "nautique" else 18.0
    label      = "nautique (-12 deg)" if mode == "nautique" \
                 else "astronomique (-18 deg)"

    today      = datetime.date.today()
    crepuscule = calcul_crepuscule(LATITUDE, LONGITUDE,
                                   today, depression, UTC_OFFSET)
    maintenant = datetime.datetime.now()

    if crepuscule is None:
        print("Calcul crepuscule impossible - demarrage immediat")
        return

    print("Crepuscule " + label + " : " + crepuscule.strftime("%H:%M:%S"))
    print("Heure actuelle              : " + maintenant.strftime("%H:%M:%S"))

    if maintenant >= crepuscule:
        print("Crepuscule deja passe - demarrage immediat")
        return

    attente_s = int((crepuscule - maintenant).total_seconds())
    print("Attente : " + str(attente_s // 60) + " min " +
          str(attente_s % 60) + " s")

    # Boucle par tranches de 60s max - reste interruptible
    while True:
        maintenant = datetime.datetime.now()
        if maintenant >= crepuscule:
            break
        restant = (crepuscule - maintenant).total_seconds()
        if restant <= 0:
            break
        tranche = min(60.0, restant)
        if restant > 60 and int(restant) % 300 < 62:
            print("  Debut dans " + str(int(restant) // 60) + " min")
        time.sleep(tranche)

    print("==> Crepuscule atteint - debut des captures !")

# ============================================================
# ECRITURE FITS UINT16 SANS ASTROPY
# ============================================================

def ecrire_fits_uint16(chemin, data_uint16, metadata):
    def card(key, value, comment=""):
        if isinstance(value, bool):
            v = "T" if value else "F"
            return ("{:<8}= {:>20} / {:<47}".format(
                    key, v, comment))[:80].ljust(80)
        elif isinstance(value, int):
            return ("{:<8}= {:>20} / {:<47}".format(
                    key, value, comment))[:80].ljust(80)
        elif isinstance(value, float):
            return ("{:<8}= {:>20.10G} / {:<47}".format(
                    key, value, comment))[:80].ljust(80)
        else:
            s = "'{}'".format(str(value)[:18])
            return ("{:<8}= {:<20} / {:<47}".format(
                    key, s, comment))[:80].ljust(80)

    cards = [
        card("SIMPLE",   True,                       "FITS standard"),
        card("BITPIX",   16,                         "16 bits signes"),
        card("NAXIS",    2,                          "Nb axes"),
        card("NAXIS1",   metadata["NAXIS1"],         "Largeur pixels"),
        card("NAXIS2",   metadata["NAXIS2"],         "Hauteur pixels"),
        card("BZERO",    32768,                      "Offset uint16"),
        card("BSCALE",   1,                          "Echelle"),
        card("EXPTIME",  float(metadata["EXPTIME"]), "Exposition secondes"),
        card("GAIN",     metadata["GAIN"],           "Gain camera"),
        card("OFFSET",   metadata["OFFSET"],         "Offset camera"),
        card("NCOMBINE", metadata["NCOMBINE"],       "Nb poses empilees"),
        card("IMAGETYP", "Dark Frame",               "Type image"),
        card("FRAME",    "Dark",                     "Type frame"),
        card("DATE-OBS",
             datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S"),
             "Date UTC"),
        "END" + " " * 77,
    ]

    header_bytes = bytearray()
    for c in cards:
        header_bytes.extend(c.encode("ascii"))
    while len(header_bytes) % 2880 != 0:
        header_bytes.extend(b" ")

    data_bytes = bytearray()
    for v in data_uint16:
        signed = int(v) - 32768
        data_bytes.extend(struct.pack(">h", max(-32768, min(32767, signed))))
    while len(data_bytes) % 2880 != 0:
        data_bytes.extend(b"\x00")

    with open(chemin, "wb") as f:
        f.write(header_bytes)
        f.write(data_bytes)

# ============================================================
# LECTURE FITS BRUT
# ============================================================

def lire_fits_brut(chemin_fits):
    with open(chemin_fits, "rb") as f:
        raw = f.read()

    data_start = 0
    for i in range(0, len(raw), 2880):
        bloc = raw[i:i+2880].decode("ascii", errors="replace")
        if "END" in bloc:
            data_start = i + 2880
            break

    header_str = raw[:data_start].decode("ascii", errors="replace")
    naxis1, naxis2, bzero = 0, 0, 32768
    for line in [header_str[i:i+80] for i in range(0, data_start, 80)]:
        key = line[:8].strip()
        val = line[10:30].strip().replace("'", "").strip()
        try:
            if key == "NAXIS1": naxis1 = int(val)
            if key == "NAXIS2": naxis2 = int(val)
            if key == "BZERO":  bzero  = int(float(val))
        except:
            pass

    n_pixels = naxis1 * naxis2
    data_raw = raw[data_start: data_start + n_pixels * 2]
    pixels   = struct.unpack(">" + "h" * n_pixels, data_raw)
    return [p + bzero for p in pixels], naxis1, naxis2

# ============================================================
# CAPTURE UNE SERIE
# ============================================================

def capturer_serie(cam, expo_sec, gain, offset_val, dark_lib):
    expo_ms = float(expo_sec * 1000)
    print("")
    print("--- Expo : " + str(expo_sec) + "s | Gain : " + str(gain) +
          " | Offset : " + str(offset_val) + " | " +
          str(N_FRAMES) + " poses ---")

    # Reglages camera
    try:
        cam.Controls.Gain.Value = gain
    except:
        print("  WARN : gain non regle")

    try:
        ctrl = cam.Controls.Find(lambda x: x.Name == "Offset")
        if ctrl is not None:
            ctrl.Value = offset_val
    except:
        print("  WARN : offset non regle")

    cam.Controls.Exposure.Automatic = False
    cam.Controls.Exposure.ExposureMs = expo_ms

    try:
        cam.Controls.OutputFormat.Value = "FITS files (*.fits)"
    except:
        try:
            cam.Controls.OutputFormat.Value = "FITS Files (*.fits)"
        except:
            print("  WARN : format FITS non disponible")

    # Dossier temporaire
    tmp_dir = os.path.join(
        os.environ.get("TEMP", "C:\\Temp"), "SC_Dark_Tmp")
    if not Directory.Exists(tmp_dir):
        Directory.CreateDirectory(tmp_dir)

    # Stabilisation
    print("  Stabilisation...")
    time.sleep(float(expo_sec) * 2.0 + 2.0)

    # Capture et accumulation
    accumulator = None
    naxis1_ref  = 0
    naxis2_ref  = 0
    n_ok        = 0

    for i in range(N_FRAMES):
        tmp_file = os.path.join(tmp_dir, "dark_tmp_" + str(i) + ".fits")
        try:
            cam.CaptureSingleFrameTo(tmp_file)
            time.sleep(float(expo_sec) + 2.0)

            pixels, nx, ny = lire_fits_brut(tmp_file)

            if accumulator is None:
                accumulator = [float(p) for p in pixels]
                naxis1_ref  = nx
                naxis2_ref  = ny
            else:
                for j in range(len(pixels)):
                    accumulator[j] += float(pixels[j])

            if File.Exists(tmp_file):
                File.Delete(tmp_file)
            n_ok += 1
            print("  Pose " + str(i+1) + "/" + str(N_FRAMES) + " OK")

        except Exception as e:
            print("  ERREUR pose " + str(i+1) + " : " + str(e))
            if File.Exists(tmp_file):
                File.Delete(tmp_file)

    if accumulator is None or n_ok == 0:
        print("  ECHEC : aucune pose reussie !")
        return False

    # Moyenne
    master = [int(round(v / n_ok)) for v in accumulator]

    # Dossier dark library structure
    cam_name = cam.DeviceName.replace(" ", "_").replace("/", "-")
    res_str  = str(naxis1_ref) + "x" + str(naxis2_ref)
    sous_dir = os.path.join(
        dark_lib, cam_name, res_str,
        "G" + str(gain) + "_O" + str(offset_val),
        str(expo_sec) + "s")
    if not Directory.Exists(sous_dir):
        Directory.CreateDirectory(sous_dir)

    ts  = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
    nom = os.path.join(
        sous_dir,
        "MasterDark_G" + str(gain) +
        "_O" + str(offset_val) +
        "_" + str(expo_sec) + "s" +
        "_" + str(n_ok) + "f_" + ts + ".fits")

    ecrire_fits_uint16(nom, master, {
        "NAXIS1":   naxis1_ref,
        "NAXIS2":   naxis2_ref,
        "EXPTIME":  expo_sec,
        "GAIN":     gain,
        "OFFSET":   offset_val,
        "NCOMBINE": n_ok
    })
    print("  ==> " + nom)
    return True

# ============================================================
# PROGRAMME PRINCIPAL
# ============================================================

def main():
    print("Dark Library Builder v3 - Initialisation...")
    cam = SharpCap.SelectedCamera
    if cam is None:
        MessageBox.Show("Aucune camera selectionnee !", "Erreur")
        return

    # Choix du mode de demarrage
    mode = START_MODE
    if mode is None:
        print("Ouverture dialog mode demarrage - verifiez la barre des taches !")
        mode = choisir_mode()
    if mode is None:
        print("Annule.")
        return

    # Resolution du dossier dark library
    dark_lib = DARK_LIB
    if not dark_lib:
        try:
            dark_lib = SharpCap.Settings.DarkLibraryFolder
            if not dark_lib or not Directory.Exists(dark_lib):
                raise Exception("Dossier introuvable")
            print("Dark Library (SharpCap settings) : " + dark_lib)
        except:
            dark_lib = os.path.join(
                os.environ.get("LOCALAPPDATA", "C:\\Users\\Public"),
                "SharpCap", "Dark Library")
            print("Dark Library (fallback) : " + dark_lib)
    else:
        print("Dark Library (manuel) : " + dark_lib)

    if not Directory.Exists(dark_lib):
        Directory.CreateDirectory(dark_lib)

    # Compte le total des series
    total_series = sum(len(expos) for _, _, expos in SERIES)
    print("=" * 60)
    print("  Dark Library Builder v2")
    print("  Camera   : " + cam.DeviceName)
    print("  Mode     : " + mode)
    print("  Couples  : " + str(len(SERIES)))
    print("  Series   : " + str(total_series))
    print("  Poses/s  : " + str(N_FRAMES))
    print("  Dark Lib : " + dark_lib)
    print("=" * 60)

    # Affiche le planning complet
    print("Planning :")
    for gain, offset_val, expos in SERIES:
        print("  Gain " + str(gain) + " / Offset " + str(offset_val) +
              " -> " + str(expos) + "s")
    print("")

    # Attente crepuscule si necessaire
    if mode in ("nautique", "astronomique"):
        attendre_crepuscule(mode)
    else:
        print("Demarrage immediat dans 10s - capuchon pose ?")
        time.sleep(10)

    # Boucle principale sur les couples gain/offset
    resultats = []
    for gain, offset_val, expos in SERIES:
        print("")
        print("*" * 60)
        print("  Gain " + str(gain) + " / Offset " + str(offset_val))
        print("*" * 60)
        for expo in expos:
            ok = capturer_serie(cam, expo, gain, offset_val, dark_lib)
            resultats.append((gain, offset_val, expo,
                              "OK" if ok else "ECHEC"))
            time.sleep(3)

    # Rapport final
    print("")
    print("=" * 60)
    print("  RAPPORT FINAL")
    print("=" * 60)
    ok_count   = sum(1 for r in resultats if r[3] == "OK")
    fail_count = sum(1 for r in resultats if r[3] == "ECHEC")
    for gain, offset_val, expo, statut in resultats:
        print("  G" + str(gain) + "/O" + str(offset_val) +
              " - " + str(expo) + "s : " + statut)
    print("")
    print("  Total : " + str(ok_count) + " OK | " +
          str(fail_count) + " ECHEC")
    print("=" * 60)
    MessageBox.Show(
        str(ok_count) + " series OK\n" +
        str(fail_count) + " echecs\nConsulte la console pour le detail.",
        "Dark Library Builder v2 - Termine",
        MessageBoxButtons.OK,
        MessageBoxIcon.Information)

main()
Si les boites de dialogues ne s'affichent pas, regardez dans la barre des taches qu'elles ne soient pas masquées à l'arrière plan.
image.png
image.png
Dark Library Builder v3 - Initialisation...
Ouverture dialog mode demarrage - verifiez la barre des taches !
Dark Library (fallback) : C:\Users\alain\AppData\Local\SharpCap\Dark Library
============================================================
Dark Library Builder v2
Camera : Uranus-C PRO (IMX585)
Mode : astronomique
Couples : 2
Series : 16
Poses/s : 31
Dark Lib : C:\Users\alain\AppData\Local\SharpCap\Dark Library
============================================================
Planning :
Gain 210 / Offset 6 -> [1, 2, 5, 10, 15, 30, 45, 60, 90, 120]s
Gain 310 / Offset 12 -> [1, 2, 5, 10, 15, 30]s

Crepuscule astronomique (-18 deg) : 21:58:34
Heure actuelle : 22:10:29
Crepuscule deja passe - demarrage immediat

************************************************************
Gain 210 / Offset 6
************************************************************

--- Expo : 1s | Gain : 210 | Offset : 6 | 31 poses ---
Stabilisation...
Dernière modification par darki le 13 avr. 2026, 16:18, modifié 2 fois.

darki
Messages : 4057
Inscription : 09 déc. 2024, 16:37
Localisation : Environs Aix en Provence

Script Sharpcap pour faire sa bibliothèque de Dark

Message par darki » 12 avr. 2026, 22:29

Je vus met la V4 dans laquelle, pour les étourdis comme moi qui oublient de refroidir la caméra, on peut définir la température et la tolérance pour débuter dans la partie configuration.

On peut dire -10°C et de démarrer quand la temp est atteinte à 0.1 ° près par exemple

Code : Tout sélectionner

# ============================================================
# Dark Library Builder v3 pour SharpCap Pro (IronPython)
# - Demarrage immediat / crepuscule nautique / astronomique
# - Multi couples gain/offset avec expositions independantes
# - Boucle d'attente corrigee (tranches 60s)
# - Gestion temperature capteur
# ============================================================

import time
import datetime
import os
import math
import struct
import clr

clr.AddReference("System.Windows.Forms")
clr.AddReference("System.Drawing")
from System.Windows.Forms import (MessageBox, MessageBoxButtons,
                                  MessageBoxIcon, DialogResult)
from System.Drawing import Size
from System.IO import File, Directory, Path

# ============================================================
# CONFIGURATION
# ============================================================

LATITUDE   = 45.00
LONGITUDE  = 10.00
UTC_OFFSET = 2       # +1 en hiver, +2 en ete

# Mode de demarrage :
# 'immediate'     -> demarre tout de suite sans dialog
# 'nautique'      -> attend crepuscule nautique (-12 deg)
# 'astronomique'  -> attend crepuscule astronomique (-18 deg)
# None            -> affiche la boite de dialogue au lancement
START_MODE = None

N_FRAMES = 31

# Couples (gain, offset, [expositions en secondes])
SERIES = [
    (210, 6,  [1, 2, 5, 10, 15, 30, 45, 60, 90, 120]),
    (310, 12, [1, 2, 5, 10, 15, 30]),
]

# Laisser vide pour utiliser le dossier SharpCap automatiquement
# ou forcer un chemin : r"C:\MonDossier\Dark Library"
DARK_LIB = ""

# Temperature cible en degres Celsius (None = pas de controle)
TARGET_TEMP = -10
TARGET_TEMP_DELTA = 0.1

# ============================================================
# BOITE DE DIALOGUE MODE DEMARRAGE
# ============================================================

def choisir_mode():
    from System.Windows.Forms import (MessageBox, MessageBoxButtons,
                                      MessageBoxIcon, DialogResult, Form)
    from System.Drawing import Size

    owner = Form()
    owner.TopMost = True
    owner.Size    = Size(1, 1)
    owner.Show()
    owner.BringToFront()
    owner.Activate()
    owner.Focus()

    rep1 = MessageBox.Show(
        owner,
        "Demarrer immediatement ?\n\n" +
        "OUI  -> demarrage immediat\n" +
        "NON  -> attente crepuscule",
        "Dark Library Builder v3",
        MessageBoxButtons.YesNo,
        MessageBoxIcon.Question)

    if rep1 == DialogResult.Yes:
        owner.Dispose()
        return "immediate"

    rep2 = MessageBox.Show(
        owner,
        "Choisir le type de crepuscule :\n\n" +
        "OUI  -> Nautique     (-12 deg)\n" +
        "NON  -> Astronomique (-18 deg)",
        "Dark Library Builder v3",
        MessageBoxButtons.YesNo,
        MessageBoxIcon.Question)

    owner.Dispose()

    if rep2 == DialogResult.Yes:
        return "nautique"
    else:
        return "astronomique"

# ============================================================
# CALCUL CREPUSCULE
# ============================================================

def calcul_crepuscule(lat, lon, date, depression_deg, utc_offset):
    """
    Calcule l'heure locale du crepuscule du soir.
    depression_deg : 12 = nautique, 18 = astronomique
    """
    a   = (14 - date.month) // 12
    y   = date.year + 4800 - a
    m   = date.month + 12 * a - 3
    jdn = date.day + (153*m+2)//5 + 365*y + y//4 - y//100 + y//400 - 32045
    jd  = jdn - 0.5

    n   = jd - 2451545.0
    L   = (280.46 + 0.9856474 * n) % 360
    g   = math.radians((357.528 + 0.9856003 * n) % 360)
    lam = math.radians(L + 1.915 * math.sin(g) + 0.020 * math.sin(2*g))
    eps = math.radians(23.439 - 0.0000004 * n)
    dec = math.asin(math.sin(eps) * math.sin(lam))

    lat_r     = math.radians(lat)
    cos_omega = (math.sin(math.radians(-depression_deg))
                 - math.sin(lat_r) * math.sin(dec)) \
                / (math.cos(lat_r) * math.cos(dec))

    if abs(cos_omega) > 1.0:
        return None

    omega    = math.degrees(math.acos(cos_omega))
    EqT      = (L - math.degrees(math.atan2(
                    math.cos(eps) * math.sin(lam),
                    math.cos(lam)))) / 15.0
    midi_utc = 12.0 - lon / 15.0 - EqT / 60.0
    h_utc    = midi_utc + omega / 15.0

    hh = int(h_utc) % 24
    mm = int((h_utc - int(h_utc)) * 60)
    ss = int(((h_utc - int(h_utc)) * 60 - mm) * 60)

    try:
        dt_utc = datetime.datetime(date.year, date.month, date.day, hh, mm, ss)
        return dt_utc + datetime.timedelta(hours=utc_offset)
    except:
        return None

def attendre_crepuscule(mode):
    depression = 12.0 if mode == "nautique" else 18.0
    label      = "nautique (-12 deg)" if mode == "nautique" \
                 else "astronomique (-18 deg)"

    today      = datetime.date.today()
    crepuscule = calcul_crepuscule(LATITUDE, LONGITUDE,
                                   today, depression, UTC_OFFSET)
    maintenant = datetime.datetime.now()

    if crepuscule is None:
        print("Calcul crepuscule impossible - demarrage immediat")
        return

    print("Crepuscule " + label + " : " + crepuscule.strftime("%H:%M:%S"))
    print("Heure actuelle              : " + maintenant.strftime("%H:%M:%S"))

    if maintenant >= crepuscule:
        print("Crepuscule deja passe - demarrage immediat")
        return

    attente_s = int((crepuscule - maintenant).total_seconds())
    print("Attente : " + str(attente_s // 60) + " min " +
          str(attente_s % 60) + " s")

    # Boucle par tranches de 60s max - reste interruptible
    while True:
        maintenant = datetime.datetime.now()
        if maintenant >= crepuscule:
            break
        restant = (crepuscule - maintenant).total_seconds()
        if restant <= 0:
            break
        tranche = min(60.0, restant)
        if restant > 60 and int(restant) % 300 < 62:
            print("  Debut dans " + str(int(restant) // 60) + " min")
        time.sleep(tranche)

    print("==> Crepuscule atteint - debut des captures !")

# ============================================================
# ECRITURE FITS UINT16 SANS ASTROPY
# ============================================================

def ecrire_fits_uint16(chemin, data_uint16, metadata):
    def card(key, value, comment=""):
        if isinstance(value, bool):
            v = "T" if value else "F"
            return ("{:<8}= {:>20} / {:<47}".format(
                    key, v, comment))[:80].ljust(80)
        elif isinstance(value, int):
            return ("{:<8}= {:>20} / {:<47}".format(
                    key, value, comment))[:80].ljust(80)
        elif isinstance(value, float):
            return ("{:<8}= {:>20.10G} / {:<47}".format(
                    key, value, comment))[:80].ljust(80)
        else:
            s = "'{}'".format(str(value)[:18])
            return ("{:<8}= {:<20} / {:<47}".format(
                    key, s, comment))[:80].ljust(80)

    cards = [
        card("SIMPLE",   True,                       "FITS standard"),
        card("BITPIX",   16,                         "16 bits signes"),
        card("NAXIS",    2,                          "Nb axes"),
        card("NAXIS1",   metadata["NAXIS1"],         "Largeur pixels"),
        card("NAXIS2",   metadata["NAXIS2"],         "Hauteur pixels"),
        card("BZERO",    32768,                      "Offset uint16"),
        card("BSCALE",   1,                          "Echelle"),
        card("EXPTIME",  float(metadata["EXPTIME"]), "Exposition secondes"),
        card("GAIN",     metadata["GAIN"],           "Gain camera"),
        card("OFFSET",   metadata["OFFSET"],         "Offset camera"),
        card("NCOMBINE", metadata["NCOMBINE"],       "Nb poses empilees"),
        card("CCD-TEMP",  metadata.get("TEMP", 0),   "Temperature capteur"),
        card("IMAGETYP", "Dark Frame",               "Type image"),
        card("FRAME",    "Dark",                     "Type frame"),
        card("DATE-OBS",
             datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S"),
             "Date UTC"),
        "END" + " " * 77,
    ]

    header_bytes = bytearray()
    for c in cards:
        header_bytes.extend(c.encode("ascii"))
    while len(header_bytes) % 2880 != 0:
        header_bytes.extend(b" ")

    data_bytes = bytearray()
    for v in data_uint16:
        signed = int(v) - 32768
        data_bytes.extend(struct.pack(">h", max(-32768, min(32767, signed))))
    while len(data_bytes) % 2880 != 0:
        data_bytes.extend(b"\x00")

    with open(chemin, "wb") as f:
        f.write(header_bytes)
        f.write(data_bytes)

# ============================================================
# LECTURE FITS BRUT
# ============================================================

def lire_fits_brut(chemin_fits):
    with open(chemin_fits, "rb") as f:
        raw = f.read()

    data_start = 0
    for i in range(0, len(raw), 2880):
        bloc = raw[i:i+2880].decode("ascii", errors="replace")
        if "END" in bloc:
            data_start = i + 2880
            break

    header_str = raw[:data_start].decode("ascii", errors="replace")
    naxis1, naxis2, bzero = 0, 0, 32768
    for line in [header_str[i:i+80] for i in range(0, data_start, 80)]:
        key = line[:8].strip()
        val = line[10:30].strip().replace("'", "").strip()
        try:
            if key == "NAXIS1": naxis1 = int(val)
            if key == "NAXIS2": naxis2 = int(val)
            if key == "BZERO":  bzero  = int(float(val))
        except:
            pass

    n_pixels = naxis1 * naxis2
    data_raw = raw[data_start: data_start + n_pixels * 2]
    pixels   = struct.unpack(">" + "h" * n_pixels, data_raw)
    return [p + bzero for p in pixels], naxis1, naxis2

# ============================================================
# CAPTURE UNE SERIE
# ============================================================

def capturer_serie(cam, expo_sec, gain, offset_val, dark_lib):
    expo_ms = float(expo_sec * 1000)
    print("")
    print("--- Expo : " + str(expo_sec) + "s | Gain : " + str(gain) +
          " | Offset : " + str(offset_val) + " | " +
          str(N_FRAMES) + " poses ---")

    # Reglages camera
    try:
        cam.Controls.Gain.Value = gain
    except:
        print("  WARN : gain non regle")

    try:
        ctrl = cam.Controls.Find(lambda x: x.Name == "Offset")
        if ctrl is not None:
            ctrl.Value = offset_val
    except:
        print("  WARN : offset non regle")

    cam.Controls.Exposure.Automatic = False
    cam.Controls.Exposure.ExposureMs = expo_ms

    try:
        cam.Controls.OutputFormat.Value = "FITS files (*.fits)"
    except:
        try:
            cam.Controls.OutputFormat.Value = "FITS Files (*.fits)"
        except:
            print("  WARN : format FITS non disponible")

    # Dossier temporaire
    tmp_dir = os.path.join(
        os.environ.get("TEMP", "C:\\Temp"), "SC_Dark_Tmp")
    if not Directory.Exists(tmp_dir):
        Directory.CreateDirectory(tmp_dir)

    # Reglage et attente temperature
    if TARGET_TEMP is not None:
        try:
            ctrl_cooler = cam.Controls.Find(lambda x: x.Name == "Cooler")
            if ctrl_cooler is not None:
                ctrl_cooler.Value = "On"
        except:
            pass
        try:
            ctrl_temp = cam.Controls.Find(
                lambda x: x.Name == "Target Temperature")
            if ctrl_temp is not None:
                ctrl_temp.Value = TARGET_TEMP
                print("  Temp cible : " + str(TARGET_TEMP) + " deg C")
        except:
            print("  WARN : temperature non reglee")

        print("  Attente temperature cible " + str(TARGET_TEMP) + " deg C...")
        timeout = 300
        elapsed = 0
        while elapsed < timeout:
            try:
                ctrl_t = cam.Controls.Find(
                    lambda x: x.Name == "Temperature")
                if ctrl_t is not None:
                    temp_actuelle = float(ctrl_t.Value)
                    print("  Temp actuelle : " +
                          str(round(temp_actuelle, 1)) + " deg C")
                    if abs(temp_actuelle - TARGET_TEMP) <= TARGET_TEMP_DELTA:
                        print("  Temperature atteinte !")
                        break
            except:
                pass
            time.sleep(10)
            elapsed += 10
        if elapsed >= timeout:
            print("  WARN : timeout temperature - capture quand meme")
    else:
        # Stabilisation standard si pas de controle temperature
        print("  Stabilisation...")
        time.sleep(float(expo_sec) * 2.0 + 2.0)

    # Capture et accumulation
    accumulator = None
    naxis1_ref  = 0
    naxis2_ref  = 0
    n_ok        = 0

    for i in range(N_FRAMES):
        tmp_file = os.path.join(tmp_dir, "dark_tmp_" + str(i) + ".fits")
        try:
            cam.CaptureSingleFrameTo(tmp_file)
            time.sleep(float(expo_sec) + 2.0)

            pixels, nx, ny = lire_fits_brut(tmp_file)

            if accumulator is None:
                accumulator = [float(p) for p in pixels]
                naxis1_ref  = nx
                naxis2_ref  = ny
            else:
                for j in range(len(pixels)):
                    accumulator[j] += float(pixels[j])

            if File.Exists(tmp_file):
                File.Delete(tmp_file)
            n_ok += 1
            print("  Pose " + str(i+1) + "/" + str(N_FRAMES) + " OK")

        except Exception as e:
            print("  ERREUR pose " + str(i+1) + " : " + str(e))
            if File.Exists(tmp_file):
                File.Delete(tmp_file)

    if accumulator is None or n_ok == 0:
        print("  ECHEC : aucune pose reussie !")
        return False

    # Moyenne
    master = [int(round(v / n_ok)) for v in accumulator]

    # Dossier dark library structure
    cam_name = cam.DeviceName.replace(" ", "_").replace("/", "-")
    res_str  = str(naxis1_ref) + "x" + str(naxis2_ref)
    sous_dir = os.path.join(
        dark_lib, cam_name, res_str,
        "G" + str(gain) + "_O" + str(offset_val),
        str(expo_sec) + "s")
    if not Directory.Exists(sous_dir):
        Directory.CreateDirectory(sous_dir)

    ts  = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
    nom = os.path.join(
        sous_dir,
        "MasterDark_G" + str(gain) +
        "_O" + str(offset_val) +
        "_" + str(expo_sec) + "s" +
        "_" + str(n_ok) + "f_" + ts + ".fits")

    # Lecture temperature reelle au moment de la sauvegarde
    temp_reelle = TARGET_TEMP if TARGET_TEMP is not None else 0
    try:
        ctrl_t = cam.Controls.Find(lambda x: x.Name == "Temperature")
        if ctrl_t is not None:
            temp_reelle = round(float(ctrl_t.Value), 1)
    except:
        pass

    ecrire_fits_uint16(nom, master, {
        "NAXIS1":   naxis1_ref,
        "NAXIS2":   naxis2_ref,
        "EXPTIME":  expo_sec,
        "GAIN":     gain,
        "OFFSET":   offset_val,
        "NCOMBINE": n_ok,
        "TEMP":     temp_reelle
    })
    print("  ==> " + nom)
    return True

# ============================================================
# PROGRAMME PRINCIPAL
# ============================================================

def main():
    print("Dark Library Builder v3 - Initialisation...")
    cam = SharpCap.SelectedCamera
    if cam is None:
        MessageBox.Show("Aucune camera selectionnee !", "Erreur")
        return

    # Choix du mode de demarrage
    mode = START_MODE
    if mode is None:
        print("Ouverture dialog mode demarrage - verifiez la barre des taches !")
        mode = choisir_mode()
    if mode is None:
        print("Annule.")
        return

    # Resolution du dossier dark library
    dark_lib = DARK_LIB
    if not dark_lib:
        try:
            dark_lib = SharpCap.Settings.DarkLibraryFolder
            if not dark_lib or not Directory.Exists(dark_lib):
                raise Exception("Dossier introuvable")
            print("Dark Library (SharpCap settings) : " + dark_lib)
        except:
            dark_lib = os.path.join(
                os.environ.get("LOCALAPPDATA", "C:\\Users\\Public"),
                "SharpCap", "Dark Library")
            print("Dark Library (fallback) : " + dark_lib)
    else:
        print("Dark Library (manuel) : " + dark_lib)

    if not Directory.Exists(dark_lib):
        Directory.CreateDirectory(dark_lib)

    # Compte le total des series
    total_series = sum(len(expos) for _, _, expos in SERIES)
    print("=" * 60)
    print("  Dark Library Builder v3")
    print("  Camera   : " + cam.DeviceName)
    print("  Mode     : " + mode)
    print("  Temp     : " + (str(TARGET_TEMP) + " deg C" if TARGET_TEMP is not None else "non geree"))
    print("  Couples  : " + str(len(SERIES)))
    print("  Series   : " + str(total_series))
    print("  Poses/s  : " + str(N_FRAMES))
    print("  Dark Lib : " + dark_lib)
    print("=" * 60)

    # Affiche le planning complet
    print("Planning :")
    for gain, offset_val, expos in SERIES:
        print("  Gain " + str(gain) + " / Offset " + str(offset_val) +
              " -> " + str(expos) + "s")
    print("")

    # Attente crepuscule si necessaire
    if mode in ("nautique", "astronomique"):
        attendre_crepuscule(mode)
    else:
        print("Demarrage immediat dans 10s - capuchon pose ?")
        time.sleep(10)

    # Boucle principale sur les couples gain/offset
    resultats = []
    for gain, offset_val, expos in SERIES:
        print("")
        print("*" * 60)
        print("  Gain " + str(gain) + " / Offset " + str(offset_val))
        print("*" * 60)
        for expo in expos:
            ok = capturer_serie(cam, expo, gain, offset_val, dark_lib)
            resultats.append((gain, offset_val, expo,
                              "OK" if ok else "ECHEC"))
            time.sleep(3)

    # Rapport final
    print("")
    print("=" * 60)
    print("  RAPPORT FINAL")
    print("=" * 60)
    ok_count   = sum(1 for r in resultats if r[3] == "OK")
    fail_count = sum(1 for r in resultats if r[3] == "ECHEC")
    for gain, offset_val, expo, statut in resultats:
        print("  G" + str(gain) + "/O" + str(offset_val) +
              " - " + str(expo) + "s : " + statut)
    print("")
    print("  Total : " + str(ok_count) + " OK | " +
          str(fail_count) + " ECHEC")
    print("=" * 60)
    MessageBox.Show(
        str(ok_count) + " series OK\n" +
        str(fail_count) + " echecs\nConsulte la console pour le detail.",
        "Dark Library Builder v3 - Termine",
        MessageBoxButtons.OK,
        MessageBoxIcon.Information)

main()
Dernière modification par darki le 13 avr. 2026, 16:15, modifié 1 fois.

Avatar de l’utilisateur
Olivier-Fantasy
Messages : 10665
Inscription : 14 oct. 2020, 23:03
Localisation : Région parisienne (92)

Script Sharpcap pour faire sa bibliothèque de Dark

Message par Olivier-Fantasy » 13 avr. 2026, 00:04

Eh bien dis donc, sacré boulot, merci ! Ça pourra être pratique (prochaine fois que j'aurais des darks à faire/refaire) ;)

darki
Messages : 4057
Inscription : 09 déc. 2024, 16:37
Localisation : Environs Aix en Provence

Script Sharpcap pour faire sa bibliothèque de Dark

Message par darki » 13 avr. 2026, 07:47

Olivier-Fantasy a écrit :
13 avr. 2026, 00:04
sacré boulot
J’ai juste demandé à une IA de faire et j’ai testé et demandé les corrections

Avatar de l’utilisateur
Opmc73
Messages : 4695
Inscription : 03 juil. 2024, 21:03
Localisation : Savoie/Lyon

Script Sharpcap pour faire sa bibliothèque de Dark

Message par Opmc73 » 13 avr. 2026, 16:09

darki a écrit :
11 avr. 2026, 23:33
Vous coller ça dans la partie du bas de la console python de sharpcap, vous sauvegardez pour pas le perdre et en cliquant sur la flèche verte, ça fait le taf pendant que vous faites autre chose.
tu as vu que le script contient ta latitude et longitude ? ;) et c'est quoi l'interet d'attendre le crépuscule nautique dans ce traitement ? Si on veut faire des darks il suffit de mettre un capuchon sur la camera non ?

darki
Messages : 4057
Inscription : 09 déc. 2024, 16:37
Localisation : Environs Aix en Provence

Script Sharpcap pour faire sa bibliothèque de Dark

Message par darki » 13 avr. 2026, 16:14

C'est une lat et long approximative (Aix en Provence) mais je vais quand même éditer.

Je l'ai dit pour le crépuscule, mes setup sont à demeure à l'extérieur le plus souvent et le capuchon sur le C8 ne suffit pas si il ne fait pas nuit, surtout que j'ai du y faire des emplacements pour les fils de l'anneau chauffant

Avatar de l’utilisateur
Opmc73
Messages : 4695
Inscription : 03 juil. 2024, 21:03
Localisation : Savoie/Lyon

Script Sharpcap pour faire sa bibliothèque de Dark

Message par Opmc73 » 13 avr. 2026, 16:32

darki a écrit :
13 avr. 2026, 16:14
mes setup sont à demeure à l'extérieur le plus souvent et le capuchon sur le C8 ne suffit pas si il ne fait pas nuit, surtout que j'ai du y faire des emplacements pour les fils de l'anneau chauffant
ah ok , j'avais pas vu. Dans mon cas les blocs optiques sont retirés et rangés dans une boite a l'abris , donc dark avec capuchons ;)

Ameleo37
Messages : 111
Inscription : 25 déc. 2020, 21:54
Localisation : Tours

Script Sharpcap pour faire sa bibliothèque de Dark

Message par Ameleo37 » 24 mai 2026, 12:44

un partage et demande d'avis sur le résultat que j'ai obtenu sur mes darks avec ce script.
j'ai lancé le script sur plusieurs soirs d'avril avec temps couvert, matos dans le jardin (pour ne pas fatiguer le refroidissement de la cam.
Caméra ARES PRO MC.
J'ai fait trois série de darks avec même offset et gains 125 250 400.
Je n'avais pas ressorti le matos depuis cette série.
Hier soir, je fais à l'arrache (ma cam de guidage ZWO sous W11 est inutilisable ... voir sur les autres forums ce pb de signature de drivers) qques poses pour vérifier ma config.
Et je me rends compte que toutes mes photos sont à moitié bizarres:
Image

J'ai vite soupçonné mes darks. Car le résultat état meilleur sans!

Cet effet est valable pour tous mes darks qui sont comme cela:
[attachment=1]Stack_4frames_8s_WithDisp ... ttachment]

vous avez une idée sur la cause de cela??

.... je vais devoir reprendre ma bibliothèque de darks!
Pièces jointes
MasterDark_G250_O35_2s_31f_20260422_214442.fits
(17.26 Mio) Téléchargé 9 fois
Stack_4frames_8s_WithDisplayStretch.png

Répondre

Revenir à « Recherches et développements »