Unistellar Evscope : Quelques utilitaires généraux

Voici quelques utilitaires destinés à simplifier la vie de toute utilisateur de Evscope désireux de garder/traiter ses images.

On va post-traiter les images directement issues des fonctions standard de “sauvegarde” de l’APP (voir page “protocole“, si nécessaire).

Dans une autre page, on s’occupera des images “brutes” reçues via “export” depuis le site de Unistellar.

Topic : Rangement automatique des images

La première tâche à faire, quand on a fini une soirée d’observation, c’est de ranger ses images… Et dans le cas de l’Evscope, ce n’est pas le plus simple : 4 types d’images, pas d’informations pertinentes, une seule information certaine (si on est sous Android) : l’heure de capture… Donc, même si on a pris des notes, cela prend du temps ! (et on peut facilement se tromper…)

J’exploite donc ici le protocole mis au point pour la capture d’images, qui accumule les 4 types d’images sur deux plateformes (PC et Tablette).
En finale, on obtient donc deux “tas” d’images sous le format :

eVscope-aaaammdd-hhmmss.png (rem : aaaammdd-hhmmss est fournit en UTC)
ex : eVscope-20220105-020042.png

Trois problèmes se posent :
– identifier le type de l’image
– gérer les duplicatas (deux images peuvent avoir le même nom, car “sauvées” à la même minute)
– préparer les images pour la suite

On va donc organiser cela via un petit programme qui va partir d’une sub-dir “maître” (du genre date+lieu d’observation) dans laquelle on crée
– une subdir input/operateur et on y stocke toutes les images venant du PC
– une subdir input/observateur et on y stocke toutes les images venant de la Tablette.

Ensuite, le programme suivant va trier les images par type, les convertir au format tiff et les stocker dans des sub-dir séparées. Le code est très simple et ne demande guère de commentaires.

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

This program analyzes eVscope provided images :
- determines its type
- adjusts some characteristics
- converts them to tiff format
- saves them in separate sub-dirs

@author: TTFonWeb
"""

import glob
import os
import imageio
import datetime
import skimage.io

#Define functions
#****************************************
def AnalyseImage(im,name,Debug=False):
    #Analyse image and determines main characteristics
    #Extract size, determine eVscope type, extract capture date
    h,w,c = im.shape

    #Determine eVscope type
    width={2240:"crop",2560:"full",1280:"base",1120:"cropbase",0:"undefined"}
    height={2240:"crop",1920:"full",960:"base",1120:"cropbase",0:"undefined"}

    imagetype=width[0]
    for t in width:
        if t == w :
            imagetype=width[t]

    #Extract capture date
    capture_date = name.split("eVscope-")[1].split(".")[0]

    return w,h,imagetype,capture_date

def ValidateSubdir(path,Debug=False):
    #Scan for needed sub-dirs and build them if needed...
    status=True
    msg=""
    if not os.path.exists(path):
        msq = path+"is not present"
        status=False
    if not os.path.exists(path+"\\input\\observator"):
        msq = path+"\input\observator is not present"
        status=False
    if not os.path.exists(path+"\\input\\operator"):
        msq = path+"\input\operator is not present"
        status=False
    if not os.path.isdir(path+"\\output"):
        print('The directory : output is not present. Creating a new one..')
        os.mkdir(path+"\\output")
    if not os.path.isdir(path+"\\output\\full"):
        print('The directory : full is not present. Creating a new one..')
        os.mkdir(path+"\\output\\full")
    if not os.path.isdir(path+"\\output\\crop"):
        print('The directory : crop is not present. Creating a new one..')
        os.mkdir(path+"\\output\\crop")
    if not os.path.isdir(path+"\\output\\base"):
        print('The directory : base is not present. Creating a new one..')
        os.mkdir(path+"\\output\\base")
    if not os.path.isdir(path+"\\output\\cropbase"):
        print('The directory : cropbase is not present. Creating a new one..')
        os.mkdir(path+"\\output\\cropbase")
    if not os.path.isdir(path+"\\output\\debug"):
        print('The directory : debug is not present. Creating a new one..')
        os.mkdir(path+"\\output\\debug")
    if not os.path.isdir(path+"\\output\\undefined"):
        print('The directory : undefined is not present. Creating a new one..')
        os.mkdir(path+"\\output\\undefined")
    return status,msg

#Start of program
#****************************************
#running parameters
#Debug=True
Debug=False
path=""
path_out=""

#Modified paths values
#drive name
drive    = "I:"
#main path of images data
path     = drive+"\\$Photo_Astro\\20220111-13_France"

#Mandatory subdirs
#input subdir : receives all images for capture night
path_in = path + "\\input"

#mandatory subdirs
#input/observator : receives all enhanced vision in annotation (circle) mode
#input/operator   : receives all enhanced vision in full (2x upscaled) mode

#output subdir : automaticcaly build, will receive all images after sorting
path_out = path + "\\output"

#computed subdirs for all image types
#output/full, output/base, output/crop; output/cropbase

#if not defined in program, ask it
if path_in=="":
    path_in = input('Enter input subdir pathname : ')
if path_out=="":
    path_out = input('Enter output subdir pathname : ')

#verify sub-dir information or build necessary files
status,msg = ValidateSubdir(path,Debug)
if status:
    print("All sub-dirs are ok, start processing")
else:
    print(msg+", aborting program")
    exit()

SupportedInputType =["png"]

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

print ("> Current date and time : ",now.strftime("%Y-%m-%d %H:%M:%S"))
log.write("#eVscope_SortImages V1.1, run : " + now.strftime("%Y-%m-%d %H:%M:%S")+"\n")
log.write(">Operator   input sub_dir  : "+ path_in + "\\operator"+"\n")
log.write(">Observator input sub_dir  : "+ path_in + "\\observator"+"\n")

imagefiles=[]

#Global scan of subdirs
for type in SupportedInputType:
    for file in glob.glob(path_in + "\\operator\\eVscope-*" + type):
        imagefiles.append(file)

    count_operator=len(imagefiles)
    log.write("> "+str(count_operator) + " Operator image file(s) to process"+"\n")

    for file in glob.glob(path_in + "\\observator\\eVscope-*" + type):
        imagefiles.append(file)

    count_observator=len(imagefiles)-count_operator
    log.write("> "+str(count_observator) + " Observator image file(s) to process"+"\n")

if len(imagefiles) > 0:
    print("> Total : " + str(len(imagefiles)) + " image file(s) to process"+"\n")
    max_image_count = len(imagefiles)
    count_full=0
    count_crop=0
    count_cropbase=0
    count_base=0
    count_undef=0

    #For each found image
    print("Step 1 - Analyze & sort image files")
    log.write("#Step 1 - Analyze & sort image files"+"\n")

    for file in imagefiles:
        if os.name == "posix":
            name = file.split('/')
        else:
            name = file.split('\\')
        if len(name) > 0:
            name = name[len(name)-1]

        im = skimage.io.imread(file)

        w,h,image_type,capture_date = AnalyseImage(im,name)
        print("File : " + name + " > Type : " + image_type)

        #Anyway, convert and store it at correct place with standardized name
        if image_type != "undefined":
            if   image_type == "full":
                count_full+=1
            elif image_type == "crop":
                count_crop+=1
                if im.shape[2] == 4:
                    #suppress alpha channel 32 bits => 24 bits RGB
                    im = im[:,:,0:3]
                    print("Convert alpha layer",im.shape)
            elif image_type == "cropbase":
                count_cropbase+=1
                if im.shape[2] == 4:
                    #im = skimage.color.rgba2rgb(im)
                    im = im[:,:,0:3]
                    print("Suppress alpha layer, final ",im.shape)
            elif image_type == "base":
                count_base+=1
            else:
                print("error in image type")
                exit()
            target_name = path_out + "\\"+image_type+"\\"+image_type+"_"+capture_date+"_orig.tif"
            imageio.imsave(target_name,im)
        else:
            target_name = path_out + "\\"+image_type+"\\"+image_type+"_"+capture_date+"_orig.jpg"
            imageio.imsave(target_name,im)
            count_undef+=1
        print("> save : " + target_name)

    print("Output subdir : "+path_out)
    print("Sorted images - \\full      : "+str(count_full))
    print("Sorted images - \\crop      : "+str(count_crop))
    print("Sorted images - \\base      : "+str(count_base))
    print("Sorted images - \\cropbase  : "+str(count_cropbase))
    print("Sorted images - \\undefined : "+str(count_undef))
    print("Total  images : "+str(count_full+count_crop+count_base+count_cropbase+count_undef))

    log.write("#Output subdir : "+path_out+"\n")
    log.write(">Sorted images - \\full      : "+str(count_full)+"\n")
    log.write(">Sorted images - \\crop      : "+str(count_crop)+"\n")
    log.write(">Sorted images - \\base      : "+str(count_base)+"\n")
    log.write(">Sorted images - \\cropbase  : "+str(count_cropbase)+"\n")
    log.write(">Sorted images - \\undefined : "+str(count_undef)+"\n")
    log.write(">Total  images : "+str(count_full+count_crop+count_base+count_cropbase+count_undef)+"\n")
else:
    log.write("No image file(s) to process"+"\n")
    print("No images found")

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

Le fichier log (TTFonWeb_eVscope_SortImages_runlog.txt) décrit les phases exécutées

#TTfonWeb eVscope_SortImages V1.1, run : 2022-01-17 06:10:41
>Operator   input sub_dir  : I:\$Photo_Astro\20220105-EvscopeAtHome\input\operator
>Observator input sub_dir  : I:\$Photo_Astro\20220105-EvscopeAtHome\input\observator
#77 Operator image file(s) to process
#15 Observator image file(s) to process
#Step 1 - Analyze & sort image files
#Output subdir : I:\$Photo_Astro\20220105-EvscopeAtHome\output
>Sorted images - \full      : 59
>Sorted images - \crop      : 15
>Sorted images - \base      : 18
>Sorted images - \cropbase  : 0
>Sorted images - \undefined : 0
>Total  images : 92

Voila, la première partie est faite… Cela va déjà beaucoup plus rapidement !

Topic : Extraction des indications de capture

Comment passer du contenu d’un fichier image, issu de l’action de l’utilitaire précédant, tel que : “crop_20220105-054544_orig.tif

crop_20220105-054544_orig.tif

à “crop_20220105-054544_orig_M57_Ring-Nebula_7min_JANV052022.tif” qui sera plus parlant, et évidemment sans passer du temps à le faire manuellement ?

Deux défis à relever : rendre le texte courbe “rectiligne” et le reconnaître… Mais en Python, c’est relativement facile à faire… 🙂

Rendre un texte “courbe” rectiligne : une manière simple…

Si on regarde la théorie concernant le “curved text linearization”, on trouve des méthodes assez sophistiquées pour résoudre tous les cas de figures…
Mais ici, on va faire plus simple en ne s’attaquant que à ce cas particulier.

Première étape : Convertir l’image en niveau de gris et l’inverser… On va utiliser OpenCv pour régler cela… Et au cas où, on prévoit le cas où il resterait des images avec un “canal alpha” (4 plans) qui traîne…

Cela se fait rapidement via :

        img_rgb  = cv.imread(file)
        if img_rgb.shape[2] == 4:
            img_gray = cv.bitwise_not(cv.cvtColor(skimage.color.rgba2rgb(img_rgb), cv.COLOR_BGR2GRAY))
        else:
            img_gray = cv.bitwise_not(cv.cvtColor(img_rgb, cv.COLOR_BGR2GRAY))

Ce qui nous donne :

Deuxième étape, en regardant les images, s’imaginer en train de regarder qu’une petite partie de la courbe… Ex sur une autre image ;

Considérer le centre de la courbe, au plus près de l’horizontale
Isoler UN caractère : il est “droit”

On dispose d’un “N” parfaitement droit (en tout cas, assez que pour être reconnu sans peine). Idem avec un chiffre, tel le “5” plus loin…

Parfaitement lisibles…

Donc, il suffit de faire défiler les caractères derrière cette “lucarne” pour récupérer ceux-ci quasi horizontaux et lisibles. Un exemple en recherchant le prochain “N” dans une image ayant subi une rotation :

Des caractères quasi horizontaux…

Dont acte… On commence par faire tourner l’image de -90° pour récupérer tous les caractères (on démarre dans la “ligne”) et on va ensuite extraire dans l’ordre : ligne, logo Unistellar, texte utile et ligne…
De nouveau, OpenCV nous fournit tous les outils nécessaires :

# initial rotate our image by -90 degrees around the center of the image
M = cv.getRotationMatrix2D((cX, cY), 90, 1.0
img_gray = cv.warpAffine(img_gray, M, (w, h))

#rotation allowed angle, 0 to 179 by 1
allowed_angle = np.arange(0, 180, 1, dtype=int)
angle=0
cpt_area=0
#will receive all image cropped elements 
list_cutimages=[]
#sw to manage final image creation on cut shape size
defined=False

for angle in allowed_angle:
    if angle > 0:
        (h, w) = img_gray.shape[:2]
        (cX, cY) = (w // 2, h // 2)
        M = cv.getRotationMatrix2D((cX, cY), -angle, 1.0)
        rotated = cv.warpAffine(img_gray, M, (w, h))
    else:
        rotated = img_gray

    #crop area is fixed acording image type... here for "large crop image"
    cut_image = rotated[2127:2192, 1083:1127]
    list_cutimages.append(cut_image)
    if not defined:
        final_image = np.zeros([cut_image.shape[0],cut_image.shape[1]*200],dtype=int) + 255
        final_image, nextpos = AssembleImage(final_image,0,cut_image)
        defined=True
    else:
        final_image, nextpos = AssembleImage(final_image,nextpos,cut_image)

Ce qui nous donne (en laissant tomber la ligne) ce qu’il nous faut…

Exemple de découpage et “relinéarisation” simplifiée…

La fonction “AssembleImage” va devoir mettre cela correctement ensemble…

def AssembleImage(stacked_image,start,addimg,Debug=False):
#Assemble curved "EXIF" text image elements to prepare for OCR reading
    #im must be large enough to store all added images
    #both image but me "black and white" only
    (thresh, addimg) = cv.threshold(addimg, 127, 255, cv.THRESH_BINARY)
    #Start indicate next free position to add next image, shift : shifting value
    shift=18
    #first build a white empty array identical to im
    image = np.zeros(stacked_image.shape,dtype=int) + 255
    #next move addimg values to correct target position
    for y in range(0,addimg.shape[0]):
        for x in range(0,addimg.shape[1]):
            image[y,start+x] = addimg[y,x]

    #then add to original, average method on "black" added elements
    stacked_image = np.where(image < 255, (stacked_image+image)/2, stacked_image)

    return stacked_image, start+shift

Ce qui nous donne :

Ce qui est bien ce que l’on voulait !
Parfait, maintenant, il faut “interpréter” le contenu, un step “OCR” s’impose…

Optical Character Recognition (OCR)

Est la fonction qui transformera des “images de caractères” en “caractères”, et nécessite un logiciel dédié pour cela… Ici, on va utiliser une fonction OCR basée sur Tesseract.

Tesseract est un logiciel “open source” de text recognition (OCR), disponible sous Apache 2.0 license désormais supporté par Google. Ici, on va utiliser un “engine” local (sous Windows, dans mon cas) et un “wrapper” pour Python nommé pytesseract qui se chargera de l’appel des fonctions.

Pour que le programme en Python fonctionne, il faut donc deux choses :
– que pytesseract soit installé (via le pip install pytessract standard)
– que le module exécutable soit mis disponible sur le PC, soit en incluant la sub-dir d’installation dans le PATH, soit en précisant l’endroit lors de l’exécution :

# If you don't have tesseract executable in your PATH, include the following: pytesseract.pytesseract.tesseract_cmd = r'<full_path_to_your_tesseract_executable>'
# Example tesseract_cmd = r'C:\Program Files (x86)\Tesseract-OCR\tesseract'

Pour ce qui est de l’exécutable, j’utilise un module assemblé pour Windows, (à installer comme une application normale) disponible ici.

Dans la version disponible, le “vocabulaire” reconnu est limité, mais suffit pour notre besoin (que les lettres majuscules, dans ce cas précis).

Ensuite, il suffit d’intégrer le tout… On sauve l’image précédemment assemblée et on la passe à une fonction dédiée

def OCROnImage(filename,Debug=False):
#Tesseract call for OCR on "EXIF" like assembled elements
    img = np.array(Image.open(filename))
    text = pytesseract.image_to_string(img)
    if Debug:
        print("OCR Extracted text : ",text)
    return text

Il suffit ensuite d’intégrer le tout… Et quand le “nom” final peut être déterminé (selon le cas d’image), de renommer l’image originale dans la sub-dir “output/crop” générée avec le précédant utilitaire.

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

This program analyzes eVscope "crop" provided images :
- extract all capture information
- rename image files to incorporate info in filename

@author: TTFonWeb
"""

import glob
import skimage
import skimage.io
import skimage.feature
import skimage.color
import os
#import imageio
import datetime
import cv2 as cv
import numpy as np
import pytesseract

from PIL import Image
#from matplotlib import pyplot as plt
#from math import sqrt

#from skimage import data
#from skimage.color import rgb2gray

#Define functions
#****************************************

def CorrectObjectChar(text):
    text = text.replace("/","-")
    text = text.replace(" ","-")
    text = text.replace(".","")
    text = text.replace("I","1")
    text = text.replace("S","5")
    text = text.replace("O","0")
    text = text.replace("Gh-","6h-")
    text = text.replace("\n","")
    return text

def CorrectTimeChar(text):
    text = text.replace("/","-")
    text = text.replace(" ","-")
    text = text.replace(".","")
    text = text.replace("I","1")
    text = text.replace("S","5")
    text = text.replace("O","0")
    text = text.replace("\n","")
    return text

def CorrectOCRChar(text):
    text = text.replace("\n","")
    text = text.replace("°","D")
    text = text.replace("'","M")
    text = text.replace("!","M")
    text = text.replace('"','S')
    text = text.replace(',','')
    text = text.replace(".-",". -")
    text = text.replace("-1","- 1")
    text = text.replace("-0","- 0")
    text = text.replace("-2","- 2")
    text = text.replace("-3","- 3")
    text = text.replace("-4","- 4")
    text = text.replace("-5","- 5")
    text = text.replace("-6","- 6")
    text = text.replace("-7","- 7")
    text = text.replace("-8","- 8")
    text = text.replace("-9","- 9")
    text = text.replace("-A","- A")
    text = text.replace("-B","- B")
    text = text.replace("-C","- C")
    text = text.replace("-D","- D")
    text = text.replace("-E","- E")
    text = text.replace("-F","- F")
    text = text.replace("-G","- G")
    text = text.replace("-H","- H")
    text = text.replace("-I","- I")
    text = text.replace("-K","- K")
    text = text.replace("-L","- L")
    text = text.replace("-M","- M")
    text = text.replace("-N","- N")
    text = text.replace("-O","- O")
    text = text.replace("-P","- P")
    text = text.replace("-Q","- Q")
    text = text.replace("-R","- R")
    text = text.replace("-S","- S")
    text = text.replace("-T","- T")
    text = text.replace("-U","- U")
    text = text.replace("-V","- V")
    text = text.replace("-X","- X")
    text = text.replace("-Y","- Y")
    text = text.replace("-Z","- Z")    
    return text

def ExtractImage(path_out,mode, Debug=False):
    cpt_convert=0
    
    if mode not in ["crop", "cropbase"]:
        print ("invalid image type : crop or cropbase" )
        return False
    
    listfiles=[]
    for file in glob.glob(path_out + "\\"+mode+"\\"+mode+"*_orig.tif"):
        listfiles.append(file)

    if len(listfiles)>0:
        print("Number of files : "+str(len(listfiles)))
        log.write("> Number of 'crop' files : "+str(len(listfiles))+"\n")

        log.write("> Observator files & extracted capture info\n")

        for file in listfiles:
            if os.name == "posix":
                name = file.split('/')
            else:
                name = file.split('\\')
            if len(name) > 0:
                name = name[len(name)-1]
            print("\nProcessing : "+name+"\n")

            img_rgb  = cv.imread(file)
            if img_rgb.shape[2] == 4:
                img_gray = cv.bitwise_not(cv.cvtColor(skimage.color.rgba2rgb(img_rgb), cv.COLOR_BGR2GRAY))
            else:
                img_gray = cv.bitwise_not(cv.cvtColor(img_rgb, cv.COLOR_BGR2GRAY))
            (h, w) = img_gray.shape[:2]
            (cX, cY) = (w // 2, h // 2)

            # initial rotate our image by -90 degrees around the center of the image
            M = cv.getRotationMatrix2D((cX, cY), 90, 1.0)
            img_gray = cv.warpAffine(img_gray, M, (w, h))

            #rotation allowed angle, 0 to 179 by 1
            allowed_angle = np.arange(0, 180, 1, dtype=int)
            angle=0
            #will receive all image cropped elements
            list_cutimages=[]
            #sw to manage final image creation on cut shape size
            defined=False

            for angle in allowed_angle:
                if angle > 0:
                    (h, w) = img_gray.shape[:2]
                    (cX, cY) = (w // 2, h // 2)
                    M = cv.getRotationMatrix2D((cX, cY), -angle, 1.0)
                    rotated = cv.warpAffine(img_gray, M, (w, h))
                else:
                    rotated = img_gray

                if mode == "crop":
                    cut_image = rotated[2127:2192, 1083:1127]
                if mode == "cropbase":
                    cut_image = rotated[2127:2192, 1083:1127] #values to be determined, no image yet...
                list_cutimages.append(cut_image)
                if not defined:
                    final_image = np.zeros([cut_image.shape[0],cut_image.shape[1]*200],dtype=int) + 255
                    final_image, nextpos = AssembleImage(final_image,0,cut_image)
                    defined=True
                else:
                    final_image, nextpos = AssembleImage(final_image,nextpos,cut_image)

            ocrfilename=path_out + "\\" + mode + "\\ocr_"+ name.split(".")[0]+"_capture.png"
            if Debug:
                print("ocr file : ",ocrfilename)
            cv.imwrite(ocrfilename,final_image)
            #first step : convert image to char
            text = OCROnImage(ocrfilename)
            #second step : convert " - " sequence to split text objects
            text = CorrectOCRChar(OCROnImage(ocrfilename))
            #third step : build separate objects
            elem = text.split(" - ")
            
            #fourth step : rebuild separate elements with different logic
            image_name = name.split(".")[0]
            nbr_elem = len(elem)

            if Debug:
                print("image name : ", image_name)
                print("text : ", text)
                print("elem : ", elem)
                print("nbr elem :",nbr_elem)

            #target objects
            objectname = ""
            commonname = ""
            duration = ""
            localization = ""
            date = ""

            if nbr_elem == 6:
                objectname = elem[1].strip()
                commonname = elem[2].strip()
                duration = elem[3].strip()
                localization = elem[4].strip()
                date = elem[5].strip()
            if nbr_elem == 5:
                objectname = "-"
                commonname = elem[1].strip()
                duration = elem[2].strip()
                localization = elem[3].strip()
                date = elem[4].strip()
            if nbr_elem == 4:
                objectname = "-"
                commonname = elem[1].strip()
                duration = elem[2].strip()
                localization = elem[3].strip()
                date = elem[4].strip()

            objectname  = CorrectObjectChar(objectname)
            commonname  = CorrectObjectChar(commonname)
            duration    = CorrectTimeChar(duration)
            localization = CorrectObjectChar(localization)
            date        = CorrectTimeChar(date)

            log.write("-"+name+" >"+image_name+" >"+objectname+" >"+commonname+" >"+duration+" >"+localization+" >"+date+"<\n")

            capture_date = image_name.split("_")[1] 

            new_file_name = path_out+"\\"+mode+"\\crop_"+capture_date+"_iden"
            if Debug:
                print("New file name :",new_file_name)
            new_file_name = new_file_name+"_"+objectname+"_"+commonname+"_"+duration+"_"+date+".tif"
            if Debug:
                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+" by"+new_file_name+"<\n")
            else:
                print("rename : ",file,"\nby : ",new_file_name)
                log.write("> rename : " +file+" by"+new_file_name+"<\n")
                cpt_convert+=1
        
    return cpt_convert

def AssembleImage(stacked_image,start,addimg,Debug=False):
#Assemble curved "EXIF" text image elements to prepare for OCR reading
    #im must be large enough to store all added images
    #both image but me "black and white" only
    (thresh, addimg) = cv.threshold(addimg, 127, 255, cv.THRESH_BINARY)
    #Start indicate next free position to add next image, shift : shifting value
    shift=18
    #first build an empty array identical to im
    image = np.zeros(stacked_image.shape,dtype=int) + 255
    #next move addimg values to correct target position
    for y in range(0,addimg.shape[0]):
        for x in range(0,addimg.shape[1]):
            image[y,start+x] = addimg[y,x]

    #then add to original, average method on "black" added elements
    stacked_image = np.where(image < 255, (stacked_image+image)/2, stacked_image)

    return stacked_image, start+shift

def OCROnImage(filename,Debug=False):
#Tesseract call for OCR on "EXIF" like assembled elements
    img = np.array(Image.open(filename))
    text = pytesseract.image_to_string(img)
    if Debug:
        print("OCR Extracted text : ",text)
    return text

#Start of program
#****************************************
#running parameter
Debug=False

drive    = "I:"
path     = drive+"\\$Photo_Astro\\20220111-13_France"
path_in  = path+"\\input"
path_out = path+"\\output"

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

print ("> Current date and time : ",now.strftime("%Y-%m-%d %H:%M:%S"))
log.write("#TTFonWeb eVscope_extractdata V1.1, run : " + now.strftime("%Y-%m-%d %H:%M:%S")+"\n")
log.write(">Operator   input sub_dir  : "+ path_in +"\n")
log.write(">Observator input sub_dir  : "+ path_out +"\n")

print("Extract information of observator images")
log.write("#"+"\n")
log.write("#Extract information of observator images"+"\n")

cpt = ExtractImage(path_out,"crop",Debug=True)

print("\nNumber of converted image : ",cpt)
#ExtractImage(path_out,"cropbase") Will be defined in future... 
now = datetime.datetime.now()

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

Rem : les fonctions sont actuellement adaptée au format “crop”, plus réaliste selon la procédure définie. La position dans les images “cropbase” n’est pas encore définie, à l’écriture de ces lignes… Je la mettrai à jour quand le cas se présentera.

Le programme complet devient simple :

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

This program analyzes eVscope "crop" provided images :
- extract all capture information
- rename image files to incorporate info in filename

@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
import pytesseract
from PIL import Image

#Define functions
#****************************************

def CorrectChar(text):
    text = text.replace("/","-")
    text = text.replace(" ","-")
    text = text.replace(".","")
    text = text.replace("I","1")
    text = text.replace("S","5")
    text = text.replace("O","0")
    text = text.replace("\n","")
    return text

def ExtractImage(path_out,mode, Debug=False):
    cpt_convert=0
    
    if mode not in ["crop", "cropbase"]:
        print ("invalid image type : crop or cropbase" )
        return False
    
    listfiles=[]
    for file in glob.glob(path_out + "\\"+mode+"\\"+mode+"*.tif"):
        listfiles.append(file)

    if len(listfiles)>0:
        print("Number of files : "+str(len(listfiles)))
        log.write("> Number of 'crop' files : "+str(len(listfiles))+"\n")

        log.write("> Observator files & extracted capture info\n")

        for file in listfiles:
            if os.name == "posix":
                name = file.split('/')
            else:
                name = file.split('\\')
            if len(name) > 0:
                name = name[len(name)-1]
            print("Processing : "+name+"\n")

            img_rgb  = cv.imread(file)
            if img_rgb.shape[2] == 4:
                img_gray = cv.bitwise_not(cv.cvtColor(skimage.color.rgba2rgb(img_rgb), cv.COLOR_BGR2GRAY))
            else:
                img_gray = cv.bitwise_not(cv.cvtColor(img_rgb, cv.COLOR_BGR2GRAY))
            (h, w) = img_gray.shape[:2]
            (cX, cY) = (w // 2, h // 2)

            # initial rotate our image by -90 degrees around the center of the image
            M = cv.getRotationMatrix2D((cX, cY), 90, 1.0)
            img_gray = cv.warpAffine(img_gray, M, (w, h))

            #rotation allowed angle, 0 to 179 by 1
            allowed_angle = np.arange(0, 180, 1, dtype=int)
            angle=0
            #will receive all image cropped elements
            list_cutimages=[]
            #sw to manage final image creation on cut shape size
            defined=False

            for angle in allowed_angle:
                if angle > 0:
                    (h, w) = img_gray.shape[:2]
                    (cX, cY) = (w // 2, h // 2)
                    M = cv.getRotationMatrix2D((cX, cY), -angle, 1.0)
                    rotated = cv.warpAffine(img_gray, M, (w, h))
                else:
                    rotated = img_gray

                if mode == "crop":
                    cut_image = rotated[2127:2192, 1083:1127]
                if mode == "cropbase":
                    cut_image = rotated[2127:2192, 1083:1127] #values to be determined, no image yet...
                list_cutimages.append(cut_image)
                if not defined:
                    final_image = np.zeros([cut_image.shape[0],cut_image.shape[1]*200],dtype=int) + 255
                    final_image, nextpos = AssembleImage(final_image,0,cut_image)
                    defined=True
                else:
                    final_image, nextpos = AssembleImage(final_image,nextpos,cut_image)

            ocrfilename=path_out + "\\" + mode + "\\ocr_"+ name.split(".")[0]+"_capture.png"
            if Debug:
                print("ocr file : ",ocrfilename)
            cv.imwrite(ocrfilename,final_image)
            text = OCROnImage(ocrfilename)
            text = text.replace("\n","")

            elem = text.split(" - ")
            image_name = name.split(".")[0]
            nbr_elem = len(elem)

            print("image name : ", image_name)
            print("text : ", text)
            print("elem : ", elem)
            print("nbr elem :",nbr_elem)

            objectname = ""
            commonname = ""
            duration = ""
            localization = ""
            date = ""

            if nbr_elem == 6:
                objectname = elem[1].strip()
                commonname = elem[2].strip()
                duration = elem[3].strip()
                localization = elem[4].strip()
                date = elem[5].strip()
            else:
                objectname = elem[1].strip()
                duration = elem[2].strip()
                localization = elem[3].strip()
                date = elem[4].strip()

            objectname = CorrectChar(objectname)
            commonname = CorrectChar(commonname)
            duration = CorrectChar(duration)
            localization = CorrectChar(localization)
            date = CorrectChar(date)

            log.write("-"+name+" >"+image_name+" >"+objectname+" >"+commonname+" >"+duration+" >"+localization+" >"+date+"<\n")

            new_file_name = path_out+"\\"+mode+"\\"+image_name
            if nbr_elem == 6:
                new_file_name = new_file_name+"_"+objectname+"_"+commonname+"_"+duration+"_"+date+".tif"
            else:
                new_file_name = new_file_name+"_"+objectname+"_"+duration+"_"+date+".tif"

            try:
                os.rename(file, new_file_name)
            except:
                print("error in rename : ",file,"\n by : ",new_file_name)
                log.write("> error in rename : " +file+" by"+new_file_name+"<\n")
            else:
                print("rename : ",file,"\nby : ",new_file_name)
                log.write("> rename : " +file+" by"+new_file_name+"<\n")
                cpt_convert+=1
        
    return cpt_convert

def AssembleImage(stacked_image,start,addimg,Debug=False):
#Assemble curved "EXIF" text image elements to prepare for OCR reading
    #im must be large enough to store all added images
    #both image but me "black and white" only
    (thresh, addimg) = cv.threshold(addimg, 127, 255, cv.THRESH_BINARY)
    #Start indicate next free position to add next image, shift : shifting value
    shift=18
    #first build an empty array identical to im
    image = np.zeros(stacked_image.shape,dtype=int) + 255
    #next move addimg values to correct target position
    for y in range(0,addimg.shape[0]):
        for x in range(0,addimg.shape[1]):
            image[y,start+x] = addimg[y,x]

    #then add to original, average method on "black" added elements
    stacked_image = np.where(image < 255, (stacked_image+image)/2, stacked_image)

    return stacked_image, start+shift

def OCROnImage(filename,Debug=False):
#Tesseract call for OCR on "EXIF" like assembled elements
    img = np.array(Image.open(filename))
    text = pytesseract.image_to_string(img)
    if Debug:
        print("OCR Extracted text : ",text)
    return text

#Start of program
#****************************************
#running parameter
Debug=False

drive    = "I:"
path     = drive+"\\$Photo_Astro\\20220105-EvscopeAtHome"
path_in  = path+"\\input"
path_out = path+"\\output"

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

print ("> Current date and time : ",now.strftime("%Y-%m-%d %H:%M:%S"))
log.write("#TTFonWeb eVscope_extractdata V1.1, run : " + now.strftime("%Y-%m-%d %H:%M:%S")+"\n")
log.write(">Operator   input sub_dir  : "+ path_in +"\n")
log.write(">Observator input sub_dir  : "+ path_out +"\n")

print("Extract information of observator images")
log.write("#"+"\n")
log.write("#Extract information of observator images"+"\n")

cpt = ExtractImage(path_out,"crop",Debug=True)

print("\nNumber of converted image : ",cpt)
#ExtractImage(path_out,"cropbase") Will be defined in future... 

log.close()

A l’exécution, on passe donc de :

crop_20220105-050431_orig.tif
crop_20220105-051044_orig.tif
crop_20220105-051910_orig.tif
crop_20220105-052157_orig.tif
crop_20220105-052209_orig.tif
crop_20220105-052923_orig.tif
crop_20220105-052933_orig.tif
crop_20220105-054401_orig.tif
crop_20220105-054536_orig.tif
crop_20220105-054544_orig.tif
crop_20220105-055112_orig.tif
crop_20220105-055329_orig.tif
crop_20220105-055425_orig.tif
crop_20220105-055524_orig.tif
crop_20220105-055533_orig.tif

à

crop_20220105-055533_orig_Blinking-Planetary-Nebula_116sec_JANV-05-2022.tif
crop_20220105-050431_orig_M97_0wl-Nebula_14min_JANV-05-2022.tif
crop_20220105-051044_orig_M97_0wl-Nebula_20min_JANV-05-2022.tif
crop_20220105-051910_orig_67P-Churyumov-Gerasimenko_2min_JANV-05-2022.tif
crop_20220105-052157_orig_67P-Churyumov-Gerasimenko_5min_JANV-05-2022.tif
crop_20220105-052209_orig_67P-Churyumov-Gerasimenko_5min_JANV-05-2022.tif
crop_20220105-052923_orig_C-2018-X3-(PAN5TARR5)_84sec_JANV-05-2022.tif
crop_20220105-052933_orig_C-2018-X3-(PAN5TARR5)_88sec_JANV-05-2022.tif
crop_20220105-054401_orig_M57_Ring-Nebula_5min_JANV-05-2022.tif
crop_20220105-054536_orig_M57_Ring-Nebula_7min_JANV-05-2022.tif
crop_20220105-054544_orig_M57_Ring-Nebula_7min_JANV-05-2022.tif
crop_20220105-055112_orig_Blinking-Planetary-Nebula_36sec_JANV-05-2022.tif
crop_20220105-055329_orig_Blinking-Planetary-Nebula_76sec_JANV-05-2022.tif
crop_20220105-055425_orig_Blinking-Planetary-Nebula_52sec_JANV-05-2022.tif
crop_20220105-055524_orig_Blinking-Planetary-Nebula_112sec_JANV-05-2022.tif

Ce qui est tout de même plus “parlant”…

Maintenant, on “décodé” les images de type “crop” et il faudrait utiliser ces informations pour renommer de la même manière les fichiers de type “Full”…
Mais comment retrouver les bonnes de manière certaine ? Il peut y avoir un décalage important entre les deux images ou on peut avoir sauvé plusieurs images pour la même observation… Donc, il va falloir être un brin plus subtil qu’un simple “merge”…

Topic : “Signature” d’images pour l’identification croisée

La problématique visant à reconnaître deux astrophotos est relativement banale… Trois méthodes existent :

1) la synchronisation du moment de capture : si deux télescopes capturent un objet au même moment (synchronisé par une horloge externe), on peut les traiter de manière identique.

Ici : pas possible, du moins entièrement. Disons que si on suit le protocole, les dates/heures entre captures doivent être “proches”, mais pas exactes.

2) la synchronisation sur le contenu : si deux images issues de deux télescopes sont identiques (selon un pourcentage des succès), ils pointent le même objet.

Ici : le % de différence est important (vu le “crop” exécuté et le délai entre captures), mais qui sait, en réduisant le champ pris en considération, pourquoi pas ?

3) synchronisation sur coordonnées : si l’astrométrie des étoiles présentes sur chaque astrophoto concordent, les télescopes les ayant prises pointent vers le même objet.

Ici, le but est d’aller rapidement au résultat. Donc, on va tenter de combiner
– comparaison entre dates/heures
– comparaison entre une “signature” calculée sur chaque image “proche dans le temps”

Mais est-ce possible ? Prenons des exemples avec des captures du même jour :

Comparaison sur base de la même minute…

Tester sur les “dates” : rappelons que, en théorie, le protocole propose de
1) sauver l’image au format “crop” (sur tablette)
2) stopper la vision augmentée, ce qui déclenche la sauvegarde de la “full” sur le PC.
Donc : les “dates” des images “crop” devraient systématiquement précéder celles des “full”. Mais si on regarde le tableau ci-dessus, cela ne parait pas forcément évident. Il faut donc examiner en détail quelles images correspondent réellement.

A l’examen (visuel) :
– pour les “vertes”, cela correspond… Mais évidemment en comparant sur la différence entre dates, pas sur la “minute” active, ce qui semble logique.
– il y a des “manques” ou incohérence… Qui doivent trouver leurs origines dans des erreurs de manipulations.
Mais en gros, la méthode peut fonctionner, mais peut-on garantir mieux ?

Si on veut comparer les images, il faudra d’abord les “réduire” à des composants compatible… En effet, si on compare un “crop” et une “full”, on distingue rapidement que la “crop” est une image qui aura subit des modifications pour son “embellissement” avant sauvegarde…

A gauche : full, à droite : crop.

Les intensités et “bruit” ont été adaptés… Si on veut comparer, il va falloir traiter l’image.

Conclusion : l’astrométrie, à ce stade, ne parait pas indispensable (et lourd), donc on va se focaliser sur la comparaison via dates et une “signature” caractéristique d’une image. Si les deux sont suffisamment proches : on associe les informations !

On va procéder en deux étapes :
– en premier lieu, pour chaque image crop de base, on va examiner la différence entre la date de l’image de base et de toutes les images full disponibles. On détermine ainsi la plus “proche” dans le temps.
– en second lieu, on va découper chaque image sous une format commun et fixer une valeur qui la caractérise. Par exemple : le nombre de pixels saturés (puisque c’est une caractéristique systématique des captures) présentes dans la zone.
– si la “date” est proche et que le nombre de pixels est inférieur à une limite (déterminée expérimentalement) => on considère que l’on parle du même objet…

Pour la zone, on va sélectionner un cercle de 600 pixels de rayon à partir du centre de chaque image, en ayant évidemment pris la précaution de la convertir en niveaux de gris et l’inverser avant… Il suffira ensuite de “recadrer” les deux images sur le cercle découpé (et un fond uniforme) pour simplement faire la soustraction…

Tout est dit, “y a ka”…

# -*- coding: utf-8 -*-
"""
This program analyzes eVscope "crop" and "full" provided images :
- determines if both are matching on the same observed object 
- rename full image files to incorporate info in filename
 
@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 skimage import data
#from skimage.color import rgb2gray

#Define functions
#****************************************
def ExtractImage(im,mode,debug=False):
    r=int(1200/2)
    w=0
    h=0
    imBW=[]
    if mode== "crop":
        w=2240
        h=2240
    if mode== "full":
        w=2560
        h=1920
    if mode== "cropbase" or mode== "base" :
        print("not supported for base image type")
        return False,im

    if debug:
        print("Extract Image mode :",mode)
        print("Extract Image size,w,h :",im.shape,w,h)

    if w > 0:
        x=int(w/2)
        y=int(h/2)
        #build masks : 1 = black exterior, white circle, 2= white exteriour, black circle
        #zeros_like => problem...
        mask1 = np.zeros(im.shape,dtype=int)
        mask1 = cv.circle(mask1, (x, y), r, (255, 255, 255), -1)
        mask2 = np.zeros(im.shape,dtype=int)+255
        mask2 = cv.circle(mask2, (x, y), r, (0, 0, 0), -1)
        #extract cropped area
        out = np.where(mask1 == 255, im, 255)
        #refill outside of area in black
        out = np.where(mask2 == 0, out, 0)
        #invert image, then convert it to gray and final to BW
        imGray = cv.bitwise_not(cv.cvtColor(out, cv.COLOR_BGR2GRAY))

        (thresh, imBW) = cv.threshold(imGray, 127, 255, cv.THRESH_BINARY)

        if debug:
            print("Gray converted size :",imGray.shape)
            print("BW converted size :",imBW.shape)

        #trim to 2240 on 1920 for both images
        if imBW.shape[0] > 1920:
            h = (imBW.shape[0]-1920)/2
            imBW = imBW[int(h):imBW.shape[0]-int(h),:]
            
        if imBW.shape[1] > 2240:
            w = (imBW.shape[1]-2240)/2
            imBW = imBW[:,int(w):imBW.shape[1]-int(w)]
        
        if debug:
            print("BW final size :",imBW.shape,"\n")
                      
    return True, imBW

def CompareImage(cropname,fullname,debug=False):
    #Compute a key based on star counting on a standard eVscope "cropped" area
    
    #trim to standardized size of 1200 pixels 
    #first, crop image
    im = skimage.io.imread(cropname)
    status,imcrop = ExtractImage(im,"crop",debug=debug)
    #second, full image
    im = skimage.io.imread(fullname)
    status,imfull = ExtractImage(im,"full",debug=debug)

    npfull = np.sum(imfull == 0)
    npcrop = np.sum(imcrop == 0)
    result = npfull - npcrop

    if debug:
        path = cropname.split("\\output\\")[0]
        print("Debug retrieved path : "+path)
        name = cropname.split("\\output\\crop\\")[1].split(".")[0]
        print("Debug crop path : "+path+"\\output\\debug\\"+name+"_out.tif")
        cv.imwrite(path+"\\output\\debug\\"+name+"_out.tif",imcrop)
        name = fullname.split("\\output\\full\\")[1].split(".")[0]
        print("Debug full path : "+path+"\\output\\debug\\"+name+"_out.tif")
        cv.imwrite(path+"\\output\\debug\\"+name+"_out.tif",imfull)
        print("Size : ",imcrop.shape,imfull.shape)
        print("Result, sum imfull, sum imcrop : ",result, npfull,npcrop)

    return result,npfull,npcrop

def sortFunc(e):
    f=e.split("\\output\\full\\")[1]
    print(f.split("_")[1])
    return f.split("_")[1]

#Start of program
#****************************************
#running parameters
Debug=True

#No effective rename, test matching first 
Replace=True

#path=""
#path_out=""
drive = "E:"
path = drive+"\\$Evscope_Capture\\20220205_home"
path_out = path+"\\output"

#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 : ')
if path_out=="":
    path = input('Enter output subdir : ')
    if path_out =="":
        path_out = path

SupportedInputType =["tif"]

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

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

listcrop=[]
listcropday=[]
listcroptime=[]
listcropsec=[]

listfull=[]
listfullday=[]
listfulltime=[]
listfullsec=[]

#Global scan of subdirs to get informations
for type in SupportedInputType:
    for file in glob.glob(path_out + "\\crop\\crop_*_iden_*." + type):
        listcrop.append(file)
        image_day = file.split(path)[1].split("_")[1].split("-")[0]
        image_time = file.split(path)[1].split("_")[1].split("-")[1]
        listcropday.append(image_day) 
        listcroptime.append(image_time)
        listcropsec.append(int(image_time[0:2])*3600+int(image_time[2:4])*60+int(image_time[4:6]))

    count_crop=len(listcrop)
    print("#"+str(count_crop) + " crop image file(s) to process"+"\n")
    log.write("#"+str(count_crop) + " crop image file(s) to process"+"\n")

    for file in glob.glob(path_out + "\\full\\full_*_orig." + type):
        listfull.append(file)
        image_day = file.split(path)[1].split("_")[1].split("-")[0]
        image_time = file.split(path)[1].split("_")[1].split("-")[1]
        listfullday.append(image_day) 
        listfulltime.append(image_time)
        listfullsec.append(int(image_time[0:2])*3600+int(image_time[2:4])*60+int(image_time[4:6]))

    count_full=len(listfull)
    print("#"+str(count_full) + " full image file(s) to process"+"\n")
    log.write("#"+str(count_full) + " full image file(s) to process"+"\n")

    log.write("\n# Original Crop files list"+"\n")
    for imagecrop in listcrop: 
        log.write(">"+imagecrop.split("\\output\\crop\\")[1]+" "+str(listcropday[listcrop.index(imagecrop)])+" "+str(listcropsec[listcrop.index(imagecrop)])+"\n")

    log.write("\n# Original full files list"+"\n")
    for imagefull in listfull: 
        log.write(">"+imagefull.split("\\output\\full\\")[1]+" "+str(listfullday[listfull.index(imagefull)])+" "+str(listfullsec[listfull.index(imagefull)])+"\n")

    log.write("\nStart scanning"+"\n")


count=0
for imagecrop in listcrop: 
    count+=1
    min = 999999
    target = ""
    print("\nScan for ",imagecrop)
    log.write("\nScan for crop : "+imagecrop+"\n")
    
    #For 1 Crop image, test all Full around timing difference
    for imagefull in listfull:
        log.write("versus Full : "+imagefull+"\n")      
        if listcropday[listcrop.index(imagecrop)] == listfullday[listfull.index(imagefull)]: 
            if Debug:
                print("Same day : ", listcropday[listcrop.index(imagecrop)],listfullday[listfull.index(imagefull)])
                log.write("Same day : "+listcropday[listcrop.index(imagecrop)]+" = "+listfullday[listfull.index(imagefull)]+"\n")

            delta = listfullsec[listfull.index(imagefull)] - listcropsec[listcrop.index(imagecrop)]
            if Debug:
                print("Full sec : "+str(listfullsec[listfull.index(imagefull)]))
                print("Crop sec : "+str(listcropsec[listcrop.index(imagecrop)]))
                print("Delta time, abs : ", delta,abs(delta))
                log.write("Full sec : "+str(listfullsec[listfull.index(imagefull)])+"\n")
                log.write("Crop sec : "+str(listcropsec[listcrop.index(imagecrop)])+"\n")
                log.write("Delta time, abs : " +str(delta)+" "+str(abs(delta))+"\n")

            if Debug:
                print("Actual min : ", min)
                log.write("Actuel min : "+str(min)+"\n")

            if abs(delta) <= min and abs(delta) < 60:
                min = abs(delta)
                if Debug:
                    print("New min : ", min)
                    log.write("New min : "+str(min)+"\n")
                target = imagefull
    
    compare_status = False
    #If one found, compare image contents
    if min != 999999:
        compare_delta,npfull,npcrop = CompareImage(imagecrop,target,debug=Debug)
    else:
        compare_delta = 999999
        npfull=999999
        npcrop=999999
        
    #if less than 1000 differences found : high probability of identical target
    if Debug:
        log.write("Compare Result : "+str(compare_delta)+" "+str(npfull)+" "+str(npcrop)+"\n")      

    if compare_delta < 1000 :
        print ("best match :",min," compare delta = ",compare_delta,target)
        log.write("<{:3d}> {:06d} {:06d}".format(count, min, compare_delta)) 
        log.write(" * best match for "+imagecrop.split("\\output\\crop\\")[1]+" is "+target.split("\\output\\full\\")[1]+"\n")

        old_file_name = target

        #image name sample : full_20220111-231049_orig.tif
        date = target.split("\\output\\full\\")[1].split("_")[1]
        new_file_name = imagecrop.split("\\output\\crop\\")[1] #get complete image name
        elem = new_file_name.split("_")
        new_file_name = path_out + "\\full\\full_"+date+"_"+elem[2]+"_"+elem[3]+"_"+elem[4]+"_"+elem[5]+"_"+elem[6]

        print("\nold file name : ", old_file_name)
        print("new file name : ", new_file_name)

        if Replace:
            try:
                os.rename(old_file_name, new_file_name)
            except:
                print("error in rename : ",old_file_name,"\n by : ",new_file_name)
                log.write("> error in rename : " +old_file_name+" by "+new_file_name+"<\n")
            else:
                print("rename : ",old_file_name,"\nby : ",new_file_name)
                log.write("> rename : " +old_file_name.split("\\output\\full\\")[1]+" by "+new_file_name.split("\\output\\full\\")[1]+"<\n")
        else:
            print("No rename of : ",old_file_name,"\nby : ",new_file_name)
            log.write("> No rename : " +old_file_name.split("\\output\\full\\")[1]+" by "+new_file_name.split("\\output\\full\\")[1]+"<\n")
        
        #after rename, rebuild full image list        
        listfull=[]
        listfullday=[]
        listfulltime=[]
        listfullsec=[]
        
        for file in glob.glob(path_out + "\\full\\full_*_orig." + type):
            listfull.append(file)
            image_day = file.split(path)[1].split("_")[1].split("-")[0]
            image_time = file.split(path)[1].split("_")[1].split("-")[1]
            listfullday.append(image_day) 
            listfulltime.append(image_time)
            listfullsec.append(int(image_time[0:2])*3600+int(image_time[2:4])*60+int(image_time[4:6]))

        count_full=len(listfull)
        print("#"+str(count_full) + " Regen : full image file(s) to process"+"\n")
        log.write("#"+str(count_full) + " full image file(s) to process after list regen "+"\n")    
        
    else:
        print ("not found : delta =",min, "compare delta = ",compare_delta)
        log.write("<{:3d}> {:06d} {:06d}".format(count, min, compare_delta)) 
        log.write(" - no match for "+imagecrop.split("\\output\\crop\\")[1]+"\n")

for type in SupportedInputType:
    for file in glob.glob(path_out + "\\full\\full_*." + type):
        listfull.append(file)
        image_day = file.split(path)[1].split("_")[1].split("-")[0]
        image_time = file.split(path)[1].split("_")[1].split("-")[1]
        listfullday.append(image_day) 
        listfulltime.append(image_time)
        listfullsec.append(int(image_time[0:2])*3600+int(image_time[2:4])*60+int(image_time[4:6]))

    listfull.sort(key=sortFunc)

    log.write("\n# Final full files list"+"\n")
    for imagefull in listfull: 
        log.write(">"+imagefull.split("\\output\\full\\")[1]+" "+str(listfullday[listfull.index(imagefull)])+"\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()

Et voilà, après exécution sur les sub-dirs concernées, on finira par avoir une série de “full” parfaitement documentées. Par exemple (extrait du rapport d’exécution) :

Final full files list
full_20220111-175207_orig.tif 20220111
full_20220111-175207_orig.tif 20220111
full_20220111-175727_iden_-2h-32m-3724s-+15D-9M-165_52sec_JANV-11-2022.tif 20220111 
full_20220111-180438_iden-Hamal_24sec_JANV-11-2022.tif 20220111 full_20220111-181114_iden-Uranus_3min_JANV-11-2022.tif 20220111 full_20220111-181945_orig.tif 20220111 
full_20220111-181945_orig.tif 20220111 
full_20220111-182855_iden-5heratan_72sec_JANV-11-2022.tif 20220112 
etc...

Il apparait clairement que une image a été renommée (avec succès ou pas, faudra voir à la longue…) et que certaines sont restées inchangées. Il suffira d’examiner si celles-ci concernent le même objet “proche” (avant ou après) et de les renommer manuellement si nécessaire.

Si le facteur de corrélation (ici 1000 pixels de différence) est trop “bas”, on peut le rehausser pour que la reconnaissance se fasse même si l’image diverge plus (à cause du suivi par exemple).

Mais là, je crois que ce facteur devra se déterminer télescope par télescope…

Topic : Un exemple d’exécution

Lors d’un séjour en France, j’ai réalisé plusieurs nuits d’observation.
Au total, des images de tous les types sauvées dans une sub-dir par dates et origine (PC ou tablette).

Charité bien ordonnée… etc…, c’est le moment de faire “tourner” les utilitaires pour voir comment cela se passe !

Etape 1 : “ranger” rapidement les images…

Je créée une sub-dir “maître” : /20220111-13_France
Une subdir “/input/observator”
Une subdir “/input/operator”

Et je copie TOUTES les images capturées en mode opérateur et observateur dans leurs sub-dir respectives. En final : 136 images “observator” et 92 images “operator”, soit 228 images au total.

Etape 2 : Programme de “tri/rangement”

A ce stade, je laisse le mode “Debug” actif et remplace simplement le “path” avec la sub-dir maître créée. On regarde le rapport d’éxécution :

#eVscope_SortImages V1.1, run : 2022-01-19 06:32:00
>Operator   input sub_dir  : I:\$Photo_Astro\20220111-13_France\input\operator
>Observator input sub_dir  : I:\$Photo_Astro\20220111-13_France\input\observator
> 92 Operator image file(s) to process
> 136 Observator image file(s) to process
#Step 1 - Analyze & sort image files
#Output subdir : I:\$Photo_Astro\20220111-13_France\output
>Sorted images - \full      : 65
>Sorted images - \crop      : 124
>Sorted images - \base      : 28
>Sorted images - \cropbase  : 11
>Sorted images - \undefined : 0
>Total  images : 228
#eVscope_SortImages V1.1, end : 2022-01-19 06:33:24

228 images, le compte est bon… Tout est “rangé” et “catégorisé” en 1 min 24… Rem : cela fait 755MB sur disque.

Le nom des fichiers, à ce stade est :
full_<date>-<time>_orig.tif
crop_<date>-<time>_orig.tif


=> “orig” est le marqueur qui indique une image issue du tri et non reconnue au niveau des données de capture à ce stade.

Etape 3 : Programme de “extraction des infos”

Une fois les images triées dans leur sub-dir respectives…
On va donc traiter les “crop” pour extraire les informations de capture.

#TTFonWeb eVscope_extractdata V1.1, run : 2022-01-19 06:39:08
>Operator   input sub_dir  : I:\$Photo_Astro\20220111-13_France\input
>Observator input sub_dir  : I:\$Photo_Astro\20220111-13_France\output
#
#Extract information of observator images
> Number of 'crop' files : 124
> Observator files & extracted capture info
-crop_20220112-222629_orig.tif >crop_20220112-222629_orig >M42 >Great-Nebula-in-0rion >24sec >48DN-7DE >JANV-12-2022<
> rename : I:\$Photo_Astro\20220111-13_France\output\crop\crop_20220112-222629_orig.tif byI:\$Photo_Astro\20220111-13_France\output\crop\crop_20220112-222629_iden_M42_Great-Nebula-in-0rion_24sec_JANV-12-2022.tif<
-crop_20220112-000337_orig.tif >crop_20220112-000337_orig >- >NGC-2158 >5min >48DN-7DE >JANV-12-2022<
> rename : I:\$Photo_Astro\20220111-13_France\output\crop\crop_20220112-000337_orig.tif byI:\$Photo_Astro\20220111-13_France\output\crop\crop_20220112-000337_iden_-_NGC-2158_5min_JANV-12-2022.tif<
-crop_20220112-000354_orig.tif >crop_20220112-000354_orig >- >NGC-2158 >5min >48DN-7DE >JANV-12-2022<
> rename : I:\$Photo_Astro\20220111-13_France\output\crop\crop_20220112-000354_orig.tif byI:\$Photo_Astro\20220111-13_France\output\crop\crop_20220112-000354_iden_-_NGC-2158_5min_JANV-12-2022.tif<
-crop_20220112-001426_orig.tif >crop_20220112-001426_orig >- >6h-7m-3879s-+24D-9'-4215 >4rmin >48DN-7DE >JANV-12-2022<
> rename : I:\$Photo_Astro\20220111-13_France\output\crop\crop_20220112-001426_orig.tif byI:\$Photo_Astro\20220111-13_France\output\crop\crop_20220112-001426_iden_-_6h-7m-3879s-+24D-9'-4215_4rmin_JANV-12-2022.tif<

....

-crop_20220111-235652_orig.tif >crop_20220111-235652_orig >- >M35 >2min >48DN-7DE >JANV-12-2022<
> rename : I:\$Photo_Astro\20220111-13_France\output\crop\crop_20220111-235652_orig.tif byI:\$Photo_Astro\20220111-13_France\output\crop\crop_20220111-235652_iden_-_M35_2min_JANV-12-2022.tif<
#TTFonWeb eVscope_extractdata V1.1, end : 2022-01-19 06:44:45

Et voilà… L’ensemble des images est désormais “renommées” avec des informations plus parlantes…

Le nom des fichiers, à ce stade est :
crop_<date>-<time>_iden_<object>_<common name>_<duration>_<day>.tif

Ex : crop_20220111-233729_iden_M42_Great-Nebula-in-0rion_52sec_JANV-12-2022.tif

=> “iden” est le marqueur qui indique une reconnue au niveau des données de capture. Si une information est manquante, un “-” est indiqué…

Comme le “texte” fourni par l’Evscope est assez variable selon le cas… Il n’est pas dit que la liste actuelle est complète. Et certaines décisions ont été prises pour garder un “nom” utilisable au niveau des conventions pour l’écriture du fichier.

Ex : crop_20220111-175728_iden_-_2h-32m-3724s-+15D-9′-165_52sec_JANV-11-2022.tif

Dans ce cas :
“-” car pas d’objet (pointage manuel)
“2h-32m-3724s” => évidemment, on a 37,24s de coordonnée
“+15D-9′-165” => est un cas qui devrait être approfondi… Les symboles de dégrés (°), min (‘) et secondes (“) et devrait être remplacé par “D”,”M”,”S”… Mais le “5” et “‘” finaux sont incorrects (liés à la séquence des remplacement) ! Mais il faudra aussi trouver une solution pour le caractère “,” (qui manque dans le cas “165”, qui soit se lire : “1,6s”)
Il faudra donc adapter le code avec une fonction plus dédiée aux coordonnées dans une version 1.2.

De toute façon, un fichier “image” pour chaque image est présent, qui permet une vérification “visuelle” si un doute existe…

Image capturée, dans le cas évoqué ci-dessus

=> Je conseille de corriger les cas litigieux avant l’étape suivante, car sinon elle va se transmettre aux autres fichiers…

Voici 128 fichiers annotés (et 124 images d’information extraites) en moins de 6 min, pas trop mal… Si vous le comparez avec le temps nécessaire pour le faire totalement manuellement.

Etape 4 : Programme de “assimilation”

La phase la plus hasardeuse… Car on est tout de même avec une estimation de corrélation entre images, pas une certitude. Le rapport d’exécution montre les quatre phases du traitement :
– la liste des images Crop avec leurs infos ajoutées (programme précédant)
– la liste des “full” à examiner
– le score de “matching” entre les deux types
– la liste des images “full” renommées ou pas.

TTF_eVscope_MathImages V1.1, run : 2022-01-20 07:38:52
124 crop image file(s) to process
19 full image file(s) to process
Original Crop files list
crop_20220112-222629_iden_M42_Great-Nebula-in-0rion_24sec_JANV-12-2022.tif 20220112 80789
crop_20220112-000337_iden_-NGC-2158_5min_JANV-12-2022.tif 20220112 217 
crop_20220112-000354_iden-NGC-2158_5min_JANV-12-2022.tif 20220112 234 
...
Original full files list
full_20220111-181945_orig.tif 20220111 65985
full_20220111-175207_orig.tif 20220111 64327
full_20220111-232707_orig.tif 20220111 84427
full_20220111-234347_orig.tif 20220111 85427
full_20220111-235207_orig.tif 20220111 85927
full_20220111-235653_orig.tif 20220111 86213
full_20220112-000358_orig.tif 20220112 238
...
Start scanning
< 1> 999999 999999 - no match for crop_20220112-222629_iden_M42_Great-Nebula-in-0rion_24sec_JANV-12-2022.tif
< 2> 999999 999999 - no match for crop_20220112-000337_iden_-NGC-2158_5min_JANV-12-2022.tif 
< 3> 000004 003994 - no match for crop_20220112-000354_iden-NGC-2158_5min_JANV-12-2022.tif 
< 4> 999999 999999 - no match for crop_20220112-001426_iden-6h-7m-3879s-+24D-9M-4215_4rmin_JANV-12-2022.tif 
< 5> 000002 000543 * best match for crop_20220112-001439_iden-_6h-7m-3879s-+24D-9M-4215_4rmin_JANV-12-2022.tif is full_20220112-001441_orig.tif
rename : full_20220112-001441_orig.tif byfull_20220112-001441_iden_-_6h-7m-3879s-+24D-9M-4215_4rmin_JANV-12-2022.tif<
63 full image file(s) to process after list regen
< 6> 999999 999999 - no match for crop_20220112-002143_iden_-6h-16m-1200s-+22D-29M-5805_3min_JANV-12-2022.tif 
< 7> 000006 000532 * best match for crop_20220112-002150_iden-6h-16m-1200s-+22D-29M-5805_3min_JANV-12-2022.tif is full_20220112-002156_orig.tif rename : full_20220112-002156_orig.tif byfull_20220112-002156_iden-_6h-16m-1200s-+22D-29M-5805_3min_JANV-12-2022.tif<
62 full image file(s) to process after list regen
< 8> 999999 999999 - no match for crop_20220112-002156_iden_-6h-16m-1200s-+22D-29M-5805_3min_JANV-12-2022.tif < 9> 999999 999999 - no match for crop_20220112-003144_iden-6h-33m-5400s-+10D-8M-2205_5min_JANV-12-2022.tif < 10> 000000 000362 * best match for crop_20220112-003157_iden-6h-33m-5400s-+10D-8M-2205_5min_JANV-12-2022.tif is full_20220112-003157_orig.tif rename : full_20220112-003157_orig.tif byfull_20220112-003157_iden-_6h-33m-5400s-+10D-8M-2205_5min_JANV-12-2022.tif<
61 full image file(s) to process after list regen
...

Final full files list
full_20220111-175207_orig.tif 20220111
full_20220111-175207_orig.tif 20220111
full_20220111-175727_iden_-2h-32m-3724s-+15D-9M-165_52sec_JANV-11-2022.tif 20220111 
full_20220111-180438_iden-Hamal_24sec_JANV-11-2022.tif 20220111 full_20220111-181114_iden-Uranus_3min_JANV-11-2022.tif 20220111 full_20220111-181945_orig.tif 20220111 
full_20220111-181945_orig.tif 20220111 
full_20220111-182855_iden-5heratan_72sec_JANV-11-2022.tif 20220112 
...
TTF_eVscope_MatchImages V1.1, end : 2022-01-20 07:41:08

En 07:41:08 – 07:38:52 = en 2min 16 sec…

Au total : en enchainant les trois programmes, l’ensemble des tâches de rangement des images s’effectue désormais bien plus rapidement “que à la main” ! Le but est atteint.

Topic : “Tout en un”

Si cela présente un intérêt pour quelqu’un il y a (évidemment) moyen d’intégrer les trois codes au sein d’un même script d’invocation.

Mais il faudra prévoir une modification pour ajouter des “arguments” lors de l’invocation et adapter légèrement le code pour cela.

Le but du site est de proposer du “code”, pas de “produit” à gérer selon les machines, les OS ou les versions. Car les méthodes utilisées ici peuvent s’appliquer à d’autres domaines et d’autres types d’acquisitions.

D’un côté : si on sait programmer, c’est facile de le faire…
De l’autre : si on ne sait pas, il faut d’abord installer tous les composants nécessaires sur son PC, avec les spécificités de celui-ci !
=> une présentation complète est nécessaire pour un débutant total…

On verra si ces lignes rencontrent un quelconque intérêt dans les clubs et forums que je fréquente avant de l’écrire…

Vu le nombre très faible de Evscope parmi les astrophotographes, j’en doute… Mais qui vivra verra. 🙂