Unistellar Evscope : exploitation des RAW

Si on est intéressé dans l’activité de “montrer” le ciel a un public, il est inutile d’aller chercher ce type d’image… Le “live stacking” du Evscope suffit largement à la tâche.

Par contre, si on est intéressé par plus dans les images, sans sursaturation systématique et une dynamique plus large, alors il faudra aller vers les images RAW.

La récupération, une fois le truc connu, est parfaitement assuré par le support de Unistellar. Comme discuté dans la page intro , il suffit d’envoyer un ticket au support pour qu’on vous envoie quelques heures plus tard un lien vers un fichier contenant vos images.

A ce stade, je n’ai pas encore utilisé la fonction “science” (les conditions météos ou de visibilité n’ont pas été favorables pour les évènements concernés) et si le contenu découvert change…

Ce chapitre va traiter spécialement de ce format d’image…

Réception des RAW’s

Le lien envoyé par Unistellar (durée limitée) va vous permettre de décharger un fichier zip d’environ 5GB
Rappel : attention à la limite ! On ne vous déchargera que 10% de la mémoire de votre télescope… Donc : veillez a faire “upload” + appel au “download” de manière fréquente si vous voulez récupérer toutes vos données !

J’y reviendrai en finale…

Le fichier zip reçu a le nom standardisé suivant :
<serial evscope>-export-<date export>.zip

Contenu du fichier zip

Et il contient une structure assez simple…

<serial>
       <serial>_<date>T<time>_EnhancedVision
       <serial>_<date>T<time>_EnhancedVision
       .... 
       <serial>_<date>T<time>_EnhancedVision
       <serial>_<date>T<time>_EnhancedVision
       <serial>_DarkframeMean

Chaque sub-dir “EnhancedVision” correspond à une image générée par la fin de l’activation de la fonction “Vision améliorée” dans l’APP.

Et dans chaque sub-dir, on va trouver :
– une sub-dir “stackinput” qui contiendra toutes les images individuelles
– deux fichiers “stacksum” qui seront une vue empilées des images individuelles.
Cette “sum” est-elle créée par le “live stacking” ou générée lors du “download”, je l’ignore? On y reviendra…

Donc, dans chaque sub-dir de capture :

<serial>_<date>T<time>_EnhancedVision
  <StackInput>
     <serial>_<date>T<time>_EnhancedVision_StackInput_<nnn>.fits
     <serial>_<date>T<time>_EnhancedVision_StackInput_<nnn>.json
  <serial>_<date>T<time>_EnhancedVision_StackSum_<nnn>.fits
  <serial>_<date>T<time>_EnhancedVision_StackSum_<nnn>.json

Chaque image fits fournies est accompagné d’un fichier json de description
Ex:
xxxxxx_2021-12-31T05-19-41.337_EnhancedVision_StackInput_0.fits
xxxxxx_2021-12-31T05-19-41.337_EnhancedVision_StackInput_0.json

Ou “xxxxxx” est le numéro de série (ils ont visiblement tout basé dessus, même la sécurité… 🙁 ) Faudra l’éliminer… 🙂

Le json d'une "StackInput"
{
"cameragain": "250cb",
"expotime": "3.971754s",
"focallength": "450mm",
"fovradecdeg": "211.3637, 28.5344",
"fovradecdms": "14h05m27.288s, +28d32m03.84s",
"framePurpose": "StackInput",
"frameseqid": 0,
"imagedepth": 16,
"pipelinemode": "EnhancedVision",
"sensormodel": "IMX224",
"serialnumber": "XXXXXX",
"size": "1304x976",
"timestamp": "2021-12-31T05-19-41.337",
"version": 1.4
}
En regroupant autrement... 
Bloc identification
"focallength": "450mm" 
"sensormodel": "IMX224"
"serialnumber": "XXXXXX"
"size": "1304x976"

Bloc conditions de capture
"cameragain": "250cb" (=> 25.0 db)
"expotime": "3.971754s"
"timestamp": "2021-12-31T05-19-41.337"
"frameseqid": 0 (=> n° au sein de la sub-dir) 
"imagedepth": 16

Bloc coordonnées
"fovradecdeg": "211.3637, 28.5344" 
"fovradecdms": "14h05m27.288s, +28d32m03.84s"

=> Pas standard, mais intéressant pour gérer ses images 

Bloc processus
"framePurpose": "StackInput"
"pipelinemode": "EnhancedVision"
Le json d'une "StackSum"

Exemple :
xxxxxx_2021-12-31T05-38-24.958_EnhancedVision_StackSum_275.fits
xxxxxx_2021-12-31T05-38-24.958_EnhancedVision_StackSum_275.json

{
"cameragain": "250cb",
"expotime": "3.971754s",(=> ne reprend que la durée individuelle, pas la somme)
"focallength": "450mm",
"fovradecdeg": "211.3637, 28.5344", (=> coord de l'objet)
"fovradecdms": "14h05m27.288s, +28d32m03.84s", 
"framePurpose": "StackSum",
"frameseqid": 275,(=> Pas de lien apparent avec StackInput)
"imagedepth": 16,(=> donc, l'image est bien en 16bits) 
"pipelinemode": "EnhancedVision",(=> apparemment, ils en prévoient d'autres)
"sensormodel": "IMX224",
"serialnumber": "xxxxxx",
"size": "1296x976", (=> on passe de 1304x976 à 1296x976) 
"timestamp": "2021-12-31T05-38-24.958",
"version": 1.4(=> je suis en 1.5 depuis, rien de changé)
}

Les json décrivent donc les conditions de capture.
Mais dans chaque fichier .fit se trouveront également des informations de capture complète :
SIMPLE  =                    T / conforms to FITS standard                      BITPIX  =                   16 / array data type                                NAXIS   =                    2 / number of array dimensions                     NAXIS1  =                 1296                                                  NAXIS2  =                  976                                                  BUNIT   = 'ADU     '                                                            ORIGIN  = 'Unistellar'         / institution responsible for creating this file 
DATE    = '2022-01-12T09:28:14.430' / date of file creation                     TIMSYER =                  0.2 / systematic error on time, in TIMEUNIT          TELESCOP= 'eVscope v1.0'       / name of telescope                              INSTRUME= 'IMX224  '           / name of instrument                             SERIALNB= 'xxxxxx  '           / Serial number of the telescope                 DATE-OBS= '2021-12-31T05:38:20.986' / date of the start of the obs              DATE-AVG= '2021-12-31T05:38:22.972' / date of the mid of the obs                DATE-END= '2021-12-31T05:38:24.958' / date of the end of the obs                MJD-OBS =    59579.23496511858 / modified Julian date of the start obs          MJD-MID =    59579.23498810316 / modified Julian date of the mid obs            MJD-END =    59579.23501108773 / modified Julian date of the end obs            EXPTIME =             3.971754 / exposure time, in TIMEUNIT                     TIMEUNIT= 's       '           / time unit                                      GAIN    =   0.1287111991587539 / gain in e-/ADU                                 GAINDB  =                 25.0 / gain in decibel used in the eVscope            OBSMODE = 'EnhancedVision'     / observation mode of the frame                  PURPOSE = 'StackSum'           / usage of the frame                             FOVRA   =             211.3637 / Field of view Right Ascension in deg (J2000.0) 
FOVDEC  =              28.5344 / Field of view Declination in deg (J2000.0)     FOVXREF =                  648 / X reference pixel for FOVRA, FOVDEC            FOVYREF =                  488 / Y reference pixel for FOVRA, FOVDEC            BSCALE  =                    1                                                  BZERO   =                32768                                                  END

Dans le cas d’une “Stackinput” :

SIMPLE = T / conforms to FITS standard
BITPIX = 16 / array data type
NAXIS = 2 / number of array dimensions
NAXIS1 = 1304
NAXIS2 = 976
BUNIT = 'ADU '
ORIGIN = 'Unistellar' / institution responsible for creating this file
DATE = '2022-01-26T13:03:53.944' / date of file creation
TIMSYER = 0.2 / systematic error on time, in TIMEUNIT
TELESCOP= 'eVscope v1.0' / name of telescope
INSTRUME= 'IMX224 ' / name of instrument
SERIALNB= 'tjhfpb ' / Serial number of the telescope
DATE-OBS= '2022-01-24T18:45:15.714' / date of the start of the obs
DATE-AVG= '2022-01-24T18:45:17.700' / date of the mid of the obs
DATE-END= '2022-01-24T18:45:19.686' / date of the end of the obs
MJD-OBS = 59603.78143187799 / modified Julian date of the start obs
MJD-MID = 59603.78145486256 / modified Julian date of the mid obs
MJD-END = 59603.78147784714 / modified Julian date of the end obs
EXPTIME = 3.971754 / exposure time, in TIMEUNIT
TIMEUNIT= 's ' / time unit
GAIN = 0.1287111991587539 / gain in e-/ADU
GAINDB = 25.0 / gain in decibel used in the eVscope
OBSMODE = 'EnhancedVision' / observation mode of the frame
PURPOSE = 'StackInput' / usage of the frame
FOVRA = 23.4621 / Field of view Right Ascension in deg (J2000.0)
FOVDEC = 30.6599 / Field of view Declination in deg (J2000.0)
FOVXREF = 652 / X reference pixel for FOVRA, FOVDEC
FOVYREF = 488 / Y reference pixel for FOVRA, FOVDEC
BSCALE = 1
BZERO = 32768

Remarquons spécialement :
TELESCOP= ‘eVscope v1.0’ => pas de différence entre Equinox et V1

BZERO = 32768 et BSCALE = 1
=> “This keyword shall be used, along with the BSCALE keyword,
when the array pixel values are not the true physical values, to
transform the primary data array values to the true values using the
equation: physical_value = BZERO + BSCALE * array_value. The value field
shall contain a floating point number representing the physical value
corresponding to an array value of zero. The default value for this
keyword is 0.0.”

PURPOSE = 'StackInput' ou 'StackSum'
=> facile pour identifier le type d'image, si même on perd le "nom" du fichier... 
FOVRA = 23.4621 / Field of view Right Ascension in deg (J2000.0)
FOVDEC = 30.6599 / Field of view Declination in deg (J2000.0)
FOVXREF = 652 / X reference pixel for FOVRA, FOVDEC
FOVYREF = 488 / Y reference pixel for FOVRA, FOVDEC
=> 1304 / 2 = 642, 976 /2 = 488... C'est donc bien le "centre" de l'image.

Tout y est, sauf : le mode de dématricage de la grille Bayer (ce qui est utile pour les logiciels de stacking). Les spécifications du IMX224 LRQ indiquent “RGGB”, mais quelques tests m’ont indiqué que cette valeur n’était pas forcément celle désirée au sein de certaines librairies Python (dont OpenCV).

Ici, en SIRIL, il semble bien que cela soit bien RGGB

Donc, le rajout des valeurs BAYERPAT et COLORTYP serait une bonne chose pour éviter d’avoir à toujours modifier ses “configurations” d’outils (ex : paramètres SIRIL)

BAYERPAT= ‘RGGB ‘ / Debayer pattern,such as RGGB,BGGR,GRBG,GBRG
COLORTYP= ‘RAW16 ‘ / Color space, such as RAW8,RAW16,RGB24

D’un autre côté, on utilise des champs “non standard” nommés FOVRA ET FOVDEC pour les coordonnées. Une conversion vers WCS pourrait être envisagée (et offrir le support des coordonnées standards).

Mais plus simplement, “traduire” en clair les coordonnées en un nom d’objet pouvant être mis dans le nom des fichiers.

Un aspect peut aussi être systématiquement édité/supprimé : le n° de série de l’Evscope. On le remplacera par une valeur moins gênante pour distribuer librement les données (le jour où il ne sera plus une faiblesse, on peut revoir le sujet…).

Darkframe ?

<serial>_DarkframeMean

Contient les images issues de la commande “Dark” et qui seront, je suppose à partir du moment où elle est stockée. Comme il y n’y en a qu’une, mieux vaut la refaire à chaque nouvelle session, comme dans l’astrophoto standard.

Rem : l’analyse des “dark” (température basse, dans mon cas) montrent d’ailleurs un “bruit électronique” très important, ressemblant à du bruit d’amplification classique, mais fortement marqué ici pour des poses de 4sec…

Si à 0° de moyenne, cela donne cela, mieux vaut le refaire souvent quand il fera plus chaud ! En plein été, je m’interroge sur l’ampleur du bruit…

Flat ?

Pas de flat… Ok, la configuration est “fixée” et donc, ne peut normalement pas se modifier…
Mais les poussières n’obéissent pas aux lois des ingénieurs !
Donc : comment fait-on pour les éliminer, au fur et à mesure de l’utilisation ? Le nettoyage du capteur (pas simple, vu la construction) risque de poser des problèmes pratiques…

=> il faut donc pouvoir prendre des “flats” et les rajouter dans le stockage, on verra cela plus tard…

Traitement des images RAW

En effectuant les tâches habituelles d’alignement et d’empilage (SIRIL), on peut enfin accéder à des approches différentes sur l’image. Un exemple :

Au-dessus : image sauvée via APP, en bas : même image retraitée sous SIRIL (traitement auto)

Mais… L’image “StackSum

Image issue du “Stacksum” après “demosaic”

On peut donc légitimement s’interroger… L’image “stacksum” parait déjà d’une bonne qualité pour contrebalancer l’image “PNG” systématiquement saturée…

Pourquoi, alors que c’est visiblement si simple…
Ne pas avoir prévu une possibilité “sortie RAW+PNG” dans les options ?
A chaque fin de mode “enhanced”, on pourrait sortie deux images : une pour la visualisation sur smartphone et l’autre : pour améliorer ensuite ???

Quand à ceux qui font des études plus poussées : ok, il faut de toute manière TOUTES les images… Mais simplement avec cette option (qui doit pas être compliquée), on résout facilement 80% des demandes !

Programmes de pré-traitement

Ce programme va donc aider au futur pré-traitement dans d’autres outils…

A faire :

  • Remplacer le n° de série par quelques chose de moins critique (à ce stade la solution)
  • Rajouter les “tags” manquants dans les header des fits
  • Renommer les images et sub-dir avec un “nom” plus parlant (extraction astrométrique sur coordonnées)
  • Rajouter dans chaque sub_dir de capture le “bon” dark en fonction de la période de capture
  • Prévoir un “flat” (même philosophie que Dark) et sa place dans les sub-dirs
  • Extra (pour éventuellement d’autres traitements) : prévoir un “debayer” optimal (avec plusieurs stratégies, vers tiff ou fits) avant post-processing.

Cela fait beaucoup… Allons-y étape par étape…

Version 1 : Extraction et rangement

Ce programme se base sur la structure de fichiers décrites au-dessus.
Il va exécuter trois actions :

  • Supprimer les n° de série de tous les fichiers… Et le remplacer par une notion “privée” pouvant éventuellement servir à les catégoriser…
  • Pour chaque sub-dir principale = un objet observé (fin de la vision augmentée si mis en automatique), on va se servir du fichier json pour aller chercher la description de l’objet via l’astrométrie.
  • En finale, on va renommer les sub_dir et fichiers avec soit le nom de l’objet trouvé, soit les coordonnées utilisées.
Astrométrie via coordonnées

Pour transformer les coordonnées fournies dans le fichier json en quelque chose de “utile”, on va simplement faire un appel à un “wrapper” disponible dans le package “astroquery”.

from astroquery.simbad import Simbad
import astropy.coordinates as coord
import astropy.units as u
import json
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
warnings.filterwarnings("ignore", category=Warning)

def QueryCoord(ra, dec,Debug=False):
    Simbad.add_votable_fields('typed_id')
    try:
        result_table = Simbad.query_region(coord.SkyCoord(ra, dec,unit=(u.deg, u.deg), frame='fk5'),radius='0d0m3s')
    except:
        result_table = None
    else:
        if Debug:
            print("query result : ", result_table)

    if result_table is None:
        r = str(ra)+"D-"+str(dec)+"D"
        return r.replace(" ","").replace(",","-").replace(".","-")
    else:
        return result_table[0][0].strip().replace("  "," ").replace(" ","-").replace("*","")

Quelques remarques sur l’interface :
– le logiciel appelé génère de nombreux “deprecation warning” qui ralentissent franchement l’exécution (ou même peuvent bloquer parfois l’exécution). Mais d’un autre côté, il faut vérifier si la résolution s’est bien passée ou pas.

Donc, pour déjà éviter tout problème, j’utilise warnings.filterwarnings(“ignore”, category=DeprecationWarning)
warnings.filterwarnings(“ignore”, category=Warning)

qui va totalement “ignorer” les messages.

– Outre la conversion des coordonnées en modèle compatible “FK5”.

Pour rappel : J2000 est la valeur “epoch / equinox” dans lequel un catalogue est basé. Mais il faut un “point de référence” pour les valeurs fournies en RA : Right Ascension et DEC : Declination. “FK5” est la référence utilisée pour les observations optiques, “ICRS” est utilisée pour les observations radio. Les observations radio sont plus précises que les optiques (surtout en interférométrie), mais pour des besoins plus simples (ce qui est le cas ici), cela n’a pas d’importance…

Il faut aussi préciser le “rayon” de recherche. Dans ce cas-ci, je démarre avec 3 arcsec.

A mon sens, il faudrait “boucler” sur plusieurs rayons pour être certain de trouver quelque chose… (vu l’incertitude de pointage du Goto)

Ci-dessous, un exemple plus parlant d’incohérence de détection…

L’image capturée est celle-ci (vue “StackSum”) :

Vue “brute” du fichier fits “Sum” avec le décalage du “live staking”,
avec un “gros” objet stellaire en suivi…

Les astroamateurs aguerris reconnaitront sans peine … NGC 891

NGC 891, à une résolution et capteur proche de celui de l’Evscope

Difficile de se tromper… Même visuellement.
Et le fichier json de description de l’image indique :

“cameragain”: “250cb”,
“expotime”: “3.971754s”,
“focallength”: “450mm”,
“fovradecdeg”: “35.53704, 42.27814“,
“fovradecdms”: “02h22m08.8896s, +42d16m41.304s“,

Le “Fits Header” de l’image le suit…
FOVRA = 35.53704 / Field of view Right Ascension in deg (J2000.0)
FOVDEC = 42.27814 / Field of view Declination in deg (J2000.0)
FOVXREF = 648 / X reference pixel for FOVRA, FOVDEC
FOVYREF = 488 / Y reference pixel for FOVRA, FOVDEC

Ok, si j’appelle Simbad “manuellement” via la page Web avec les coordonnées en degrés, j’obtiens :
No astronomical object found :
coord 02 22 08.88677115368 +42 16 41.3081614253 (ICRS, J2000, 2000.0), radius: 3 arcsec


Bizarre… Et d’ailleurs, si je recherche avec 3 arcmin, le “preview” n’est pas plus convaincant…

Query Preview sur les mêmes coordonnées…

Elle est où NGC 891, pour finir ? La réponse est vite fournie…

Recherche via “NGC 891”, mode preview

Comparons les réponses…

NGC 891 (Simbad)
02 22 32.907+42 20 53.95
NGC 891 (Evscope)
02 22 08.8896+42 16 41.304

Donc, 28″ de différence RA, et 4’42” en DEC ????
Question pointage GOTO, j’ai vu mieux… 🙂
Mais pourtant : on a bien l’objet demandé !

Puis la réflexion mène sur une autre hypothèse…
La première image du stack (0) :
“fovradecdeg”: “35.53704, 42.27814”,
“fovradecdms”: “02h22m08.8896s, +42d16m41.304s”,

La dernière image du stack (202)
“fovradecdeg”: “35.53704, 42.27814”,
“fovradecdms”: “02h22m08.8896s, +42d16m41.304s”,

Aucune différence, ce qui est logique, puisqu’on pointe, en théorie le même objet… Mais pourtant : la capture dévie ! (il suffit de regarder l’image “sum”), donc… Ne serait-ce pas l’effet du “suivi” pendant le stacking ????

Si on compare les positions entre les deux images (0 et 202), en replaçant l’objet dans son contexte…

Donc, les coordonnées inscrites dans le RAW seraient-elles celles issues de la “fin du stack”, avec l’enregistrement des dernières coordonnées du suivi ???

Et ensuite, ces coordonnées seraient simplement “copiées” vers la définition de TOUTES les images, sans vérification ?

A vérifier dans d’autres images… (J’y reviendrai) Mais en tout cas, cela ne simplifie pas ! Si le suivi est “précis” = on reste dans la région de l’objet visé, et les coordonnées sont utilisables, sinon : inexploitable sauf “élargir” la zone de recherche de 5 arcmin ! (ce coup-çi…)

Et conclusion : on ne peut donc pas directement ni croire, ni utiliser aveuglement les coordonnées fournies ! Il va falloir améliorer 🙁

eVscope_RawPreProcess V1.0

On va donc améliorer en plusieurs versions. Pour identifier les images qui ont déjà été traitées, le programme va les renommer à chaque étape. On part de “EnhancedVision” vers “Resolved” dans le nom de fichier. Si on n’est pas satisfait du processus, il suffit de renommer pour pouvoir le relancer…

"""
This program analyzes eVscope raw images files :
- Adapt naming conversion
- retrieves common name from coordinate
- rename full image files to incorporate info in filename
@author: TTFonWeb
"""
import glob
import skimage
import skimage.io
import skimage.feature
import os
import datetime
import numpy as np
from astroquery.simbad import Simbad
import astropy.coordinates as coord
import astropy.units as u
import json
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
warnings.filterwarnings("ignore", category=Warning)

#running parameters
#Debug switch, set it to False to avoid messages
Debug=True

#Set your "private" serial number replacement value
MySerial = "ttf001"

#Set work drive and path which receives extracted RAW subdir
drive = "D:"
path = drive+"\\Unistellar-export\\"

#****************************************
#Functions
def QueryCoord(ra, dec,Debug=False):
    Simbad.add_votable_fields('typed_id')
    try:
        result_table = Simbad.query_region(coord.SkyCoord(ra, dec,unit=(u.deg, u.deg), frame='fk5'),radius='0d0m3s')
    except:
        result_table = None
    else:
        if Debug:
            print("query result : ", result_table)

    if result_table is None:
        r = str(ra)+"D-"+str(dec)+"D"
        return r.replace(" ","").replace(",","-").replace(".","-")
    else:
        return result_table[0][0].strip().replace("  "," ").replace(" ","-").replace("*","")

#****************************************
#Start of program

if path=="":
    path = input('Enter input subdir : ')

#open log file
log=open(path+"\\TTFonWeb_eVscope_RawPreProcess_runlog.txt","w")
now = datetime.datetime.now()

print ("> Current date and time : ",now.strftime("%Y-%m-%d %H:%M:%S"))
log.write("#TTF_eVscope_RawPreProcess V1.1, run : " + now.strftime("%Y-%m-%d %H:%M:%S")+"\n")

listsubdir=[]
listfiles =[]

#Global scan of subdirs to get informations
for file in glob.glob(path + "\\*_EnhancedVision"):
    listsubdir.append(file)

#Darkframe scan
for file in glob.glob(path + "\\*_DarkframeMean"):
    listsubdir.append(file)

count_subdir=len(listsubdir)
count_files=len(listfiles)
print("#"+str(count_subdir) + " enhanced image subdir(s) to process"+"\n")
log.write("#"+str(count_subdir) + " enhanced image subdir(s) to process"+"\n")

print("#"+str(count_files) + " image file(s) to process"+"\n")
log.write("#"+str(count_files) + " image file(s) to process"+"\n")

listglobal = listsubdir + listfiles 

print("#total of "+str(len(listglobal)) + " image file(s) to rename"+"\n")
log.write("#total of "+ str(len(listglobal)) + " image file(s) to rename"+"\n")

#Global scan of subdirs to rename sub-dir
for file in listsubdir:
    elem = file.split('\\')
    m = len(elem) 
    file_name = elem[m-1]
    if file_name[:6] != MySerial:
        new_file_name=""
        for i in range(0,m-1):
            new_file_name = new_file_name + elem[i] + "\\"   

        new_file_name = new_file_name + MySerial + file_name[6:]
                
        if Debug:
            print("\nold file name : ", file)
            print("new file name : ", new_file_name)
        
        try:
            os.rename(file, new_file_name)
        except:
            print("error in rename : ",file,"\n by : ",new_file_name)
            log.write("> error in rename : " +file+"\n>     by : "+new_file_name+"<\n")
        else:
            print("rename : ",file,"\nby : ",new_file_name)
            log.write("> rename : " +file+"\n>     by : "+ new_file_name +"<\n")
                 
log.write("# After Rename, sub-dir to process "+"\n")

listsubdir=[]
listfiles =[]

#Global scan of subdirs to get informations
for file in glob.glob(path + "\\*_EnhancedVision"):
    listsubdir.append(file)

#Darkframe scan
for file in glob.glob(path + "\\*_DarkframeMean"):
    listsubdir.append(file)

#File scan
for subdir in listsubdir:
    for file in glob.glob(subdir+"\\*.*"):
        listfiles.append(file)

    for file in glob.glob(subdir+"\\StackInput\\*.*"):
        listfiles.append(file)

#Global scan of subdirs to rename files
for file in listfiles:
    elem = file.split('\\')
    m = len(elem) 
    file_name = elem[m-1]
    if file_name[:6] != MySerial:
        new_file_name=""
        for i in range(0,m-1):
            new_file_name = new_file_name + elem[i] + "\\"   

        new_file_name = new_file_name + MySerial + file_name[6:]
                
        if Debug:
            print("\nold file name : ", file)
            print("new file name : ", new_file_name)
        
        try:
            os.rename(file, new_file_name)
        except:
            print("error in rename : ",file,"\n by : ",new_file_name)
            log.write("> error in rename : " +file+"\n>     by : "+new_file_name+"<\n")
        else:
            print("rename : ",file,"\nby : ",new_file_name)
            log.write("> rename : " +file+"\n>     by : "+ new_file_name +"<\n")

#End of rename for Serial, go for astrometry add

listsubdir=[]
listfiles =[]

#Global scan of subdirs to get informations
for file in glob.glob(path + "\\*_EnhancedVision"):
    listsubdir.append(file)

for subdir in listsubdir:
    for file in glob.glob(subdir+"\\*.*"):
        listfiles.append(file)

    for file in glob.glob(subdir+"\\StackInput\\*.*"):
        listfiles.append(file)

count_subdir=len(listsubdir)
count_files=len(listfiles)
print("#"+str(count_subdir) + " enhanced image subdir(s) to resolve"+"\n")
log.write("#"+str(count_subdir) + " enhanced image subdir(s) to resolve"+"\n")

print("#"+str(count_files) + " image file(s) to resolve"+"\n")
log.write("#"+str(count_files) + " image file(s) to resolve"+"\n")

print("\n*** Coordinates resolution")

#Process Json extraction and rename
for file in listsubdir:
    #Try to open Json information file
    #xxxxxx_2022-01-14T02-23-50.926_EnhancedVision(_StackSum_6.json)
    jsonfile = glob.glob(file + "\\*EnhancedVision_StackSum*.json")

    if len(jsonfile)>0:
        jsonfile = jsonfile[0]
        with open(jsonfile,"r") as json_file:
            jsondata = json.load(json_file)
            print ("\nJson coord : ",jsondata['fovradecdeg'])
            result=QueryCoord(jsondata['fovradecdeg'].split(",")[0],jsondata['fovradecdeg'].split(",")[1],Debug)
            print ("Result : ",result)

        if result != "":
            new_jsonfile = jsonfile.split(".json")[0]+"_"+result.strip()+".json"
            if new_jsonfile[:6] != MySerial: 
                new_jsonfile = MySerial + new_jsonfile[6:]

            new_jsonfile = new_jsonfile.replace("EnhancedVision_StackSum","Resolved_StackSum")           
    
            try:
                if Debug:
                    print("\nnew json :",new_jsonfile)
                os.rename(jsonfile, new_jsonfile)
            except:
                print("error in rename : ",jsonfile,"\n by : ",new_jsonfile)
                log.write("> error in rename : " +jsonfile+"\n>     by : "+jsonfile+"<\n")
            else:
                print(">rename : ",jsonfile,"\nby : ",new_jsonfile)
                log.write("> rename : " +jsonfile.split(path)[1].split('\\')[1]+"\n>     by : "+new_jsonfile.split(path)[1].split('\\')[1]+"<\n")
    
            sumfile = glob.glob(file + "\\*EnhancedVision_StackSum*.fits")
            
            if len(sumfile) > 0:
                sumfile = sumfile[0] 
                new_sumfile = sumfile.split(".fits")[0]+"_"+result.strip()+".fits"
                if new_sumfile[:6] != MySerial: 
                    new_sumfile = MySerial + new_sumfile[6:]

                new_sumfile = new_sumfile.replace("EnhancedVision_StackSum","Resolved_StackSum")           
     
                try:
                    if Debug:
                        print("\nnew sum :",new_sumfile)
                    os.rename(sumfile, new_sumfile)
                except:
                    print("error in rename : ",sumfile,"\n by : ",new_sumfile)
                    log.write("> error in rename : " +sumfile+"\n>     by : "+sumfile+"<\n")
                else:
                    print(">rename : ",sumfile,"\nby : ",new_sumfile)
                    log.write("> rename : " +sumfile.split(path)[1].split('\\')[1]+"\n>     by : "+new_sumfile.split(path)[1].split('\\')[1]+"<\n")
    
            new_subdir = file+"_"+result.strip()
            new_subdir = new_subdir.replace("EnhancedVision","Resolved")           
            
            try:
                if Debug:
                    print("\nnew subdir :",new_subdir)
                os.rename(file, new_subdir)
            except:
                print("error in rename : ",file,"\n by : ",new_subdir)
                log.write("> error in rename : " +file+"\n>     by : "+subdir+"<\n")
            else:
                print(">rename : ",file,"\nby : ",new_subdir)
                log.write("> rename : " +file.split(path)[1].split('\\')[1]+"\n>     by : "+new_subdir.split(path)[1].split('\\')[1]+"<\n")
    
now = datetime.datetime.now()
print ("> Current date and time : ",now.strftime("%Y-%m-%d %H:%M:%S"))
log.write("#TTF_eVscope_RawPreProcess V1.0, end : " + now.strftime("%Y-%m-%d %H:%M:%S")+"\n")

log.close()

Cette version va à l’essentiel… Elle traite des centaines de fichiers en quelques minutes dans le but de les renommer et permettre de les retrouver plus facilement…

Un “extrait” du log…

total of 8 image file(s) to rename
rename : D:\Unistellar-export\xxxxxx_2022-01-27T19-08-35.447_EnhancedVision
by : D:\Unistellar-export\ttf001_2022-01-27T19-08-35.447_EnhancedVision<
rename : D:\Unistellar-export\xxxxxx_2022-01-27T19-09-43.308_EnhancedVision
by : D:\Unistellar-export\ttf001_2022-01-27T19-09-43.308_EnhancedVision<
…
rename : D:\Unistellar-export\xxxxxx_DarkframeMean
by : D:\Unistellar-export\ttf001_DarkframeMean<
After Rename, sub-dir to process
rename : D:\Unistellar-export\ttf001_2022-01-27T19-08-35.447_EnhancedVision\xxxxxx_2022-01-27T19-09-10.482_EnhancedVision_StackSum_5.fits
by : D:\Unistellar-export\ttf001_2022-01-27T19-08-35.447_EnhancedVision\ttf001_2022-01-27T19-09-10.482_EnhancedVision_StackSum_5.fits<
rename : D:\Unistellar-export\ttf001_2022-01-27T19-08-35.447_EnhancedVision\xxxxxx_2022-01-27T19-09-10.482_EnhancedVision_StackSum_5.json
by : D:\Unistellar-export\ttf001_2022-01-27T19-08-35.447_EnhancedVision\ttf001_2022-01-27T19-09-10.482_EnhancedVision_StackSum_5.json<
rename : D:\Unistellar-export\ttf001_2022-01-27T19-08-35.447_EnhancedVision\StackInput\xxxxxx_2022-01-27T19-08-35.447_EnhancedVision_StackInput_0.fits
by : D:\Unistellar-export\ttf001_2022-01-27T19-08-35.447_EnhancedVision\StackInput\ttf001_2022-01-27T19-08-35.447_EnhancedVision_StackInput_0.fits<
rename : D:\Unistellar-export\ttf001_2022-01-27T19-08-35.447_EnhancedVision\StackInput\xxxxxx_2022-01-27T19-08-35.447_EnhancedVision_StackInput_0.json
by : D:\Unistellar-export\ttf001_2022-01-27T19-08-35.447_EnhancedVision\StackInput\ttf001_2022-01-27T19-08-35.447_EnhancedVision_StackInput_0.json<
….
rename : ttf001_2022-01-28T04-50-19.656_EnhancedVision
by : ttf001_2022-01-28T04-50-19.656_Resolved_M-63<
TTF_eVscope_MatchImages V1.1, end : 2022-01-29 05:48:06
eVscope_RawPreProcess V1.4

Dans cette nouvelle version adresse deux besoins :

  1. “Dé-Bayeriser” l’image contenue dans le fits pour obtenir un format plus aisément visible (.tif)
  2. Préparer pour une étape d’astrométrie indépendante, il faudra faire mieux pour résoudre la position réelle.

L’astrométrie “locale” sera traitée plus tard, mais on va “préparer” une image pour cette tâche (grayscale, tiff, non compressée). Ces deux objectifs vont être résolus via une même routine :

def WriteFitsToTiff(imgfile,Debug=False):
    hdulist = fits.open(imgfile)
    image = hdulist['PRIMARY'].data
    if Debug:
        print("Image type : ",image.dtype)
        print("Image size :",image.shape)
    hdulist.close                
    
    img_tif     = imgfile.replace(".fits",".tif")
    img_tifgray = imgfile.replace(".fits","_gray.tif")
    
    #BGGR debayering for original image to BGR format
    rgbRGGB     = cv.cvtColor(image, cv.COLOR_BayerBG2BGR_EA)
    #converted to grayscale for astrometry
    img_gray    = cv.cvtColor(image, cv.COLOR_BAYER_BG2GRAY)  
    #Write standard tif image from BGR with compression
    cv.imwrite(img_tif,rgbRGGB)
    #Write to an uncompressed version
    cv.imwrite(img_tifgray,img_gray,(int(cv.IMWRITE_TIFF_COMPRESSION), 1))
    if Debug:
        print(" Save color version : ",img_tif)
        print(" Save grayscale version : ",img_tifgray)

    return True

Cette routine est très “positive”, car les cas d’erreurs ne sont quasi pas pris en compte… Mais comme la matière est complexe : on verra au fur et à mesure des tests…

Un chapitre spécial de la page “Charger/Sauver les images” décrit les fonctions utilisées… L’objectif est d’obtenir 3 images par “subdir” de type StackSum
– l’originale (.fits)
– une version (.tif) en couleur, résultat du “Bayer demosaic”, couleur 16bits
– une version (.tif) en nuance de gris, résultat du “Bayer demosaic”, grayscale, mais sauvegardée au format 8bits et non compressée

De bas en haut : fits, tif (color), tif (grayscale)

Une première anomalie : l’inversion de l’image, une deuxième : bien que j’ai indiqué plus haut que la “grille” a utiliser dans ce cas est “RGGB”, mais je dois utiliser COLOR_BayerBG2BGR (soit, vers BGGR).
L’analyse des cas est faite en testant toutes les combinaisons disponibles en OpenCV :

from astropy.io import fits 
import cv2 as cv
imgfile = "m57.fits"

print('\nDemosaic of : ' + str(imgfile))

hdulist = fits.open(imgfile)
#print(hdulist.info())
image = hdulist['PRIMARY'].data
print("Image type : ",image.dtype)
print("Image size :",image.shape)
hdulist.close

cv.imwrite(imgfile.replace(".fits","_orig_cv.tif"),image)

bgrBG2BGR = cv.cvtColor(image, cv.COLOR_BAYER_BG2BGR)
bgrGB2BGR = cv.cvtColor(image, cv.COLOR_BAYER_GB2BGR)
bgrGR2BGR = cv.cvtColor(image, cv.COLOR_BAYER_GR2BGR)
bgrRG2BGR = cv.cvtColor(image, cv.COLOR_BAYER_RG2BGR)

bgrBG2BGR_EA = cv.cvtColor(image, cv.COLOR_BAYER_BG2BGR_EA)
bgrGB2BGR_EA = cv.cvtColor(image, cv.COLOR_BAYER_GB2BGR_EA)
bgrGR2BGR_EA = cv.cvtColor(image, cv.COLOR_BAYER_GR2BGR_EA)
bgrRG2BGR_EA = cv.cvtColor(image, cv.COLOR_BAYER_RG2BGR_EA)

image_8bit = cv.convertScaleAbs(image, alpha=(255/65535)*10)

bgrBG2BGR_VNG = cv.cvtColor(image_8bit, cv.COLOR_BAYER_BG2BGR_VNG)
bgrGB2BGR_VNG = cv.cvtColor(image_8bit, cv.COLOR_BAYER_GB2BGR_VNG)
bgrGR2BGR_VNG = cv.cvtColor(image_8bit, cv.COLOR_BAYER_GR2BGR_VNG)
bgrRG2BGR_VNG = cv.cvtColor(image_8bit, cv.COLOR_BAYER_RG2BGR_VNG)

cv.imwrite(imgfile.replace(".fits","_BGGR_cv.tif"),bgrBG2BGR)
cv.imwrite(imgfile.replace(".fits","_GBRG_cv.tif"),bgrGB2BGR)
cv.imwrite(imgfile.replace(".fits","_GRGB_cv.tif"),bgrGR2BGR)
cv.imwrite(imgfile.replace(".fits","_RGGB_cv.tif"),bgrRG2BGR)

cv.imwrite(imgfile.replace(".fits","_BGGR_EA_cv.tif"),bgrBG2BGR_EA)
cv.imwrite(imgfile.replace(".fits","_GBRG_EA_cv.tif"),bgrGB2BGR_EA)
cv.imwrite(imgfile.replace(".fits","_GRGB_EA_cv.tif"),bgrGR2BGR_EA)
cv.imwrite(imgfile.replace(".fits","_RGGB_EA_cv.tif"),bgrRG2BGR_EA)

cv.imwrite(imgfile.replace(".fits","_BGGR_VNG_cv.tif"),bgrBG2BGR_VNG)
cv.imwrite(imgfile.replace(".fits","_GBRG_VNG_cv.tif"),bgrGB2BGR_VNG)
cv.imwrite(imgfile.replace(".fits","_GRGB_VNG_cv.tif"),bgrGR2BGR_VNG)
cv.imwrite(imgfile.replace(".fits","_RGGB_VNG_cv.tif"),bgrRG2BGR_VNG)

img_gray    = cv.cvtColor(bgrBG2BGR_VNG, cv.COLOR_BGR2GRAY)   
bgrBG2Gray  = cv.cvtColor(image, cv.COLOR_BAYER_BG2GRAY)

cv.imwrite(imgfile.replace(".fits","_gray_cv.tif"),img_gray)
cv.imwrite(imgfile.replace(".fits","_BGGR_gray_cv.tif"),bgrBG2Gray)

Premier point : l’inversion…

Si on garde bien les dimensions, l’image est cependant inversée (2x) après l’extraction du contenu de l’image via “hdulist”.

Second point : c’est bien le mode cv.COLOR_BAYER_BG2BGR (donc, “B” blue pixel first) ou “BGGR” qui donnera la bonne couleur finale. Les trois modes de démosaiçages (Bilinear, EA ou VNG) fournissant :

Mode EA, VNG, standard

Le mode VNG (de OpenCV) ne supportant que le 8 bits, et donc, il faut “descendre” de 16 à 8 (ici via cv.convertScaleAbs, dont l’usage est délicat…). A ce stade, ce sera le mode “EA” (Edge Adaptative) qui sera conservé.
Mais on peut également directement convertir l’image vers une version “grayscale” qui sera utile plus tard… (deux voies : depuis la couleur interpolée ou directement depuis la version RAW)

A gauche : Bayer > Color > Gray, à droite : Bayer > Gray

On intègre donc ces notions dans une nouvelle version, qui

# -*- coding: utf-8 -*-
"""
Created on Fri Jul  6 11:30:25 2018

This program analyzes eVscope raw images files :
- Adapt naming conversion
- Adapt Fits headers  
- retrieves common name from coordinate
- rename full image files to incorporate info in filename
- add correct dark frame at correct place
- add flat image

@author: TTFonWeb
"""

import glob
#import skimage
#import skimage.io
#import skimage.feature
#import skimage.color
import os
import datetime
import cv2 as cv
#import numpy as np
from astropy.io import fits
from astroquery.simbad import Simbad
import astropy.coordinates as coord
import astropy.units as u
import json
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
warnings.filterwarnings("ignore", category=Warning)

def QueryCoord(ra, dec,Debug=False):
    Simbad.add_votable_fields('typed_id')
    try:
        result_table = Simbad.query_region(coord.SkyCoord(ra, dec,unit=(u.deg, u.deg), frame='fk5'),radius='0d0m3s')
    except:
        result_table = None
    else:
        if Debug:
            print("query result : ", result_table)

    if result_table is None:
        r = str(ra)+"D-"+str(dec)+"D"
        return r.replace(" ","").replace(",","-").replace(".","-")
    else:
        return result_table[0][0].strip().replace("  "," ").replace(" ","-").replace("*","")

def WriteFitsToTiff(imgfile,Debug=False):
    hdulist = fits.open(imgfile)
    image = hdulist['PRIMARY'].data
    if Debug:
        print("Image type : ",image.dtype)
        print("Image size :",image.shape)
    hdulist.close                
    
    img_tif     = imgfile.replace(".fits",".tif")
    img_tifgray = imgfile.replace(".fits","_gray.tif")
    
    #BGGR debayering for original image to BGR format
    rgbRGGB     = cv.cvtColor(image, cv.COLOR_BayerBG2BGR_EA)
    #converted to grayscale for astrometry
    img_gray    = cv.cvtColor(image, cv.COLOR_BAYER_BG2GRAY)  
    #Write standard tif image from BGR with compression
    cv.imwrite(img_tif,rgbRGGB)
    #Write to an uncompressed version
    cv.imwrite(img_tifgray,img_gray,(int(cv.IMWRITE_TIFF_COMPRESSION), 1))
    if Debug:
        print(" Save color version : ",img_tif)
        print(" Save grayscale version : ",img_tifgray)

    return True

#Start of program

#****************************************
#running parameters
Debug=True

MySerial = "ttf001"

#path=""
#path_out=""
drive = "E:"
path = drive+"\\$Evscope_Export\\Evscope_work"

#all subdir : receives all images for capture night
#manual subdir : receives all manually requested image (captor mode)
#observator subdir : receives all enhanced vision in annotation (circle) mode
#operator subdir : receives all enhanced vision in full (2x upscaled) mode

if path=="":
    path = input('Enter input subdir : ')

#open log file
log=open(path+"\\TTFonWeb_eVscope_RawPreProcess_runlog.txt","w")
now = datetime.datetime.now()

print ("> Current date and time : ",now.strftime("%Y-%m-%d %H:%M:%S"))
log.write("#TTF_eVscope_RawPreProcess V1.1, run : " + now.strftime("%Y-%m-%d %H:%M:%S")+"\n")

listsubdir=[]
listfiles =[]

#Global scan of subdirs to get informations
for file in glob.glob(path + "\\*_EnhancedVision"):
    listsubdir.append(file)

#Darkframe scan
for file in glob.glob(path + "\\*_DarkframeMean"):
    listsubdir.append(file)

count_subdir=len(listsubdir)
count_files=len(listfiles)
print("#"+str(count_subdir) + " enhanced image subdir(s) to process"+"\n")
log.write("#"+str(count_subdir) + " enhanced image subdir(s) to process"+"\n")

print("#"+str(count_files) + " image file(s) to process"+"\n")
log.write("#"+str(count_files) + " image file(s) to process"+"\n")

listglobal = listsubdir + listfiles 

print("#total of "+str(len(listglobal)) + " image file(s) to rename"+"\n")
log.write("#total of "+ str(len(listglobal)) + " image file(s) to rename"+"\n")

#Global scan of subdirs to rename sub-dir
for file in listsubdir:
    elem = file.split('\\')
    m = len(elem) 
    file_name = elem[m-1]
    if file_name[:6] != MySerial:
        new_file_name=""
        for i in range(0,m-1):
            new_file_name = new_file_name + elem[i] + "\\"   

        new_file_name = new_file_name + MySerial + file_name[6:]
                
        if Debug:
            print("\nold file name : ", file)
            print("new file name : ", new_file_name)
        
        try:
            os.rename(file, new_file_name)
        except:
            print("error in rename : ",file,"\n by : ",new_file_name)
            log.write("> error in rename : " +file+"\n>     by : "+new_file_name+"<\n")
        else:
            print("rename : ",file,"\nby : ",new_file_name)
            log.write("> rename : " +file+"\n>     by : "+ new_file_name +"<\n")
                 
log.write("# After Rename, sub-dir to process "+"\n")

listsubdir=[]
listfiles =[]

#Global scan of subdirs to get informations
for file in glob.glob(path + "\\*_EnhancedVision"):
    listsubdir.append(file)

#Darkframe scan
for file in glob.glob(path + "\\*_DarkframeMean"):
    listsubdir.append(file)

#File scan
for subdir in listsubdir:
    for file in glob.glob(subdir+"\\*.*"):
        listfiles.append(file)

    for file in glob.glob(subdir+"\\StackInput\\*.*"):
        listfiles.append(file)

#Global scan of subdirs to rename files
for file in listfiles:
    elem = file.split('\\')
    m = len(elem) 
    file_name = elem[m-1]
    if file_name[:6] != MySerial:
        new_file_name=""
        for i in range(0,m-1):
            new_file_name = new_file_name + elem[i] + "\\"   

        new_file_name = new_file_name + MySerial + file_name[6:]
                
        if Debug:
            print("\nold file name : ", file)
            print("new file name : ", new_file_name)
        
        try:
            os.rename(file, new_file_name)
        except:
            print("error in rename : ",file,"\n by : ",new_file_name)
            log.write("> error in rename : " +file+"\n>     by : "+new_file_name+"<\n")
        else:
            print("rename : ",file,"\nby : ",new_file_name)
            log.write("> rename : " +file+"\n>     by : "+ new_file_name +"<\n")

#End of rename for Serial, go for astrometry add

listsubdir=[]
listfiles =[]

#Global scan of subdirs to get informations
for file in glob.glob(path + "\\*_EnhancedVision"):
    listsubdir.append(file)

for subdir in listsubdir:
    for file in glob.glob(subdir+"\\*.*"):
        listfiles.append(file)

    for file in glob.glob(subdir+"\\StackInput\\*.*"):
        listfiles.append(file)

count_subdir=len(listsubdir)
count_files=len(listfiles)
print("#"+str(count_subdir) + " enhanced image subdir(s) to resolve"+"\n")
log.write("#"+str(count_subdir) + " enhanced image subdir(s) to resolve"+"\n")

print("#"+str(count_files) + " image file(s) to resolve"+"\n")
log.write("#"+str(count_files) + " image file(s) to resolve"+"\n")

print("\n*** Coordinates resolution")

#Process Json extraction and rename
for file in listsubdir:
    #Try to open Json information file
    #xxxxxx_2022-01-14T02-23-50.926_EnhancedVision(_StackSum_6.json)
    jsonfile = glob.glob(file + "\\*EnhancedVision_StackSum*.json")

    if len(jsonfile)>0:
        jsonfile = jsonfile[0]
        with open(jsonfile,"r") as json_file:
            jsondata = json.load(json_file)
            print ("\nJson coord : ",jsondata['fovradecdeg'])
            result=QueryCoord(jsondata['fovradecdeg'].split(",")[0],jsondata['fovradecdeg'].split(",")[1],Debug)
            print ("Result : ",result)

        if result != "":
            new_jsonfile = jsonfile.split(".json")[0]+"_"+result.strip()+".json"
            #if new_jsonfile[:6] != MySerial: 
            #    new_jsonfile = MySerial + new_jsonfile[6:]

            new_jsonfile = new_jsonfile.replace("EnhancedVision_StackSum","Resolved_StackSum")           
    
            try:
                if Debug:
                    print("\nnew json :",new_jsonfile)
                os.rename(jsonfile, new_jsonfile)
            except:
                print("error in rename : ",jsonfile,"\n by : ",new_jsonfile)
                log.write("> error in rename : " +jsonfile+"\n>     by : "+jsonfile+"<\n")
            else:
                print(">rename : ",jsonfile,"\nby : ",new_jsonfile)
                log.write("> rename : " +jsonfile.split(path)[1].split('\\')[1]+"\n>     by : "+new_jsonfile.split(path)[1].split('\\')[1]+"<\n")
    
            sumfile = glob.glob(file + "\\*EnhancedVision_StackSum*.fits")
            
            if len(sumfile) > 0:
                sumfile = sumfile[0] 
                new_sumfile = sumfile.split(".fits")[0]+"_"+result.strip()+".fits"
                #if new_sumfile[:6] != MySerial: 
                #    new_sumfile = MySerial + new_sumfile[6:]

                new_sumfile = new_sumfile.replace("EnhancedVision_StackSum","Resolved_StackSum")           
     
                try:
                    if Debug:
                        print("\nnew sum :",new_sumfile)
                    os.rename(sumfile, new_sumfile)
                except:
                    print("error in rename : ",sumfile,"\n by : ",new_sumfile)
                    log.write("> error in rename : " +sumfile+"\n>     by : "+sumfile+"<\n")
                else:
                    print(">rename : ",sumfile,"\nby : ",new_sumfile)
                    log.write("> rename : " +sumfile.split(path)[1].split('\\')[1]+"\n>     by : "+new_sumfile.split(path)[1].split('\\')[1]+"<\n")
    
            sumfile = glob.glob(file + "\\*Resolved_StackSum*.fits")
            
            if len(sumfile) > 0:
                sumfile = sumfile[0]
                WriteFitsToTiff(sumfile,Debug)
    
            new_subdir = file+"_"+result.strip()
            new_subdir = new_subdir.replace("EnhancedVision","Resolved")           
                       
            try:
                if Debug:
                    print("\nnew subdir :",new_subdir)
                os.rename(file, new_subdir)
            except:
                print("error in rename : ",file,"\n by : ",new_subdir)
                log.write("> error in rename : " +file+"\n>     by : "+subdir+"<\n")
            else:
                print(">rename : ",file,"\nby : ",new_subdir)
                log.write("> rename : " +file.split(path)[1].split('\\')[1]+"\n>     by : "+new_subdir.split(path)[1].split('\\')[1]+"<\n")
    
now = datetime.datetime.now()
print ("> Current date and time : ",now.strftime("%Y-%m-%d %H:%M:%S"))
log.write("#TTF_eVscope_MatchImages V1.1, end : " + now.strftime("%Y-%m-%d %H:%M:%S")+"\n")

log.close()

A ce stade, on dispose déjà d’un outil utilisable sur les objets capturés pendant une période “courte” (donc, avec peu de déviation)