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()
