Script Sharpcap pour faire sa bibliothèque de Dark
Publié : 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
), 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
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.
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
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()
