Automatiser avec pySiril

Par Cissou8

pySiril est un package Python étendant les capacités de script, présent nativement dans Siril. Il est destiné aux utilisateurs déjà familiarisés avec les scripts à la recherche d’une alternative à l’utilisation de shell complexes ou bat (sous Windows).

Avec pySiril, vous pouvez:

  • écrire des conditions et des boucles (if, else, for …)
  • écrire facilement vos propres fonctions
  • récupérer les valeurs renvoyées par certaines des fonctions Siril telles que stat, bg, cdg, bgnoise

Cette bibliothèque fonctionne pour les 3 principaux OS, Linux, Windows et MacOS.

Installation

python -m pip uninstall pysiril
  • Install the new whl with:
python -m pip install pysiril-0.0.7-py3-none-any.whl

Vous pouvez également créer le package à partir des dernières sources disponibles sur https://gitlab.com/free-astro/pysiril

Modules

pySiril contient 3 modules:

  • Siril: le module principal qui va démarrer Siril, démarrer les tubes nommés.
  • Wrapper: le wrapper de toutes les commandes que vous saisiriez dans la ligne de commande de Siril ou dans un script (fichier *.ssf)
  • Addons: une classe pratique remplie d’outils pratiques pour manipuler des fichiers, les renuméroter, les copier, lire ou écrire des fichiers seq.

Un exemple de première utilisation pourrait être quelque chose comme ceci :

from pysiril.siril import Siril
from pysiril.wrapper import Wrapper
from pysiril.addons import Addons

app=Siril()                                 # Starts pySiril
cmd=Wrapper(app)                            # Starts the command wrapper
                             
help(Siril)                                 # Get help on Siril functions
help(Wrapper)                               # Get help on all Wrapper functions
help(Addons)                                # Get help on all Addons functions

cmd.help()                                  # Lists of all commands
cmd.help('bgnoise')                         # Get help for bgnoise command

del app

Exemple de traitement avec un wrapper de commande

L’exemple ci-dessous montre comment :

  1. Définir des blocs de commandes
  2. Démarrer pySiril, Siril et l’encapsuleur de commandes
  3. Définir certaines préférences
  4. Préparer masterbias, masterflat et masterdark
  5. Pré-traiter les brutes avec masterflat et masterdark, enregistrer et empiler.
  6. Fermer Siril et sortir correctement

Ce flux de travail de traitement est similaire au script Siril standard, OSC_Preprocessing.

import sys
import os

from pysiril.siril   import *
from pysiril.wrapper import *

# ==============================================================================
# EXAMPLE OSC_Processing with functions wrapper
# ==============================================================================

#1. defining command blocks for creating masters and processing lights
def master_bias(bias_dir, process_dir):
    cmd.cd(bias_dir )
    cmd.convert( 'bias', out=process_dir, fitseq=True )
    cmd.cd( process_dir )
    cmd.stack( 'bias', type='rej', sigma_low=3, sigma_high=3, norm='no')
    
def master_flat(flat_dir, process_dir):
    cmd.cd(flat_dir )
    cmd.convert( 'flat', out=process_dir, fitseq=True )
    cmd.cd( process_dir )
    cmd.preprocess( 'flat', bias='bias_stacked' )
    cmd.stack( 'pp_flat', type='rej', sigma_low=3, sigma_high=3, norm='mul')
    
def master_dark(dark_dir, process_dir):
    cmd.cd(dark_dir )
    cmd.convert( 'dark', out=process_dir, fitseq=True )
    cmd.cd( process_dir )
    cmd.stack( 'dark', type='rej', sigma_low=3, sigma_high=3, norm='no')
    
def light(light_dir, process_dir):
    cmd.cd(light_dir)
    cmd.convert( 'light', out=process_dir, fitseq=True )
    cmd.cd( process_dir )
    cmd.preprocess('light', dark='dark_stacked', flat='pp_flat_stacked', cfa=True, equalize_cfa=True, debayer=True )
    cmd.register('pp_light')
    cmd.stack('r_pp_light', type='rej', sigma_low=3, sigma_high=3, norm='addscale', output_norm=True, out='../result')
    cmd.close()
    
# ============================================================================== 
# 2. Starting pySiril
app=Siril()       
workdir  = "D:/_TraitAstro/20-SiriL/work/TestSiril"

try:
    cmd=Wrapper(app)    #2. its wrapper
    app.Open()          #2. ...and finally Siril

    #3. Set preferences
    process_dir = '../process'
    cmd.set16bits()
    cmd.setext('fit')

    #4. Prepare master frames
    master_bias(workdir+ '/biases' ,process_dir)
    master_flat(workdir+ '/flats'  ,process_dir)
    master_dark(workdir+ '/darks'  ,process_dir)

    #5. Calibrate the light frames, register and stack them
    light(workdir+ '/lights' ,process_dir)
    
except Exception as e :
    print("\n**** ERROR *** " +  str(e) + "\n" )    

#6. Closing Siril and deleting Siril instance
app.Close()
del app

Bien sûr, vous pouvez utiliser le script Siril pour faire exactement la même chose. Mais maintenant, imaginez que vous n’avez pas pris de flats, pour une raison quelconque (cela ne peut pas être une bonne raison, il n’y a aucune bonne raison de ne pas prendre de flat !). Avec de petites modifications au code ci-dessus, vous pouvez :

  • tester pour vérifier si le dossier flats contient un fichier ou est lui-même présent,
  • adapter le script pour sauter la préparation du master-flat,
  • adapter le traitement des brutes pour éviter la calibration avec le master-flat.

Vous devrez faire ce qui suit.

Modifiez d’abord la fonction light() pour accepter un kwarg afin d’indiquer si les flats sont présents:

def light(light_dir, process_dir,hasflats=True):
    cmd.cd(light_dir)
    cmd.convert( 'light', out=process_dir, fitseq=True )
    cmd.cd( process_dir )
    if hasflats:
        cmd.preprocess( 'light', dark='dark_stacked', flat='pp_flat_stacked', cfa=True, equalize_cfa=True, debayer=True )
    else:
        cmd.preprocess( 'light', dark='dark_stacked', cfa=True, debayer=True )
    cmd.register('pp_light')
    cmd.stack('r_pp_light', type='rej', sigma_low=3, sigma_high=3, norm='addscale', output_norm=True, out='../result')
    cmd.close()

Modifiez ensuite le script principal pour vérifier la présence des flats et adaptez le workflow de traitement en conséquence.

    #4. Prepare master frames
    flatsdir=workdir+ '/flats'
    hasflats=True
    if not(os.path.isdir(flatsdir)) or (len(os.listdir(flatsdir) == 0): # flats folder does not contain any file or none is present in workdir
        hasflats=False

    if hasflats:
        master_bias(workdir+ '/biases' ,process_dir)
        master_flat(workdir+ '/flats'  ,process_dir)
    
    master_dark(workdir+ '/darks'  ,process_dir)  

    #5. Calibrate the light frames, register and stack them
    light(workdir+ '/lights' ,process_dir,hasflats)

Ceci n’est qu’un exemple, vous pouvez faire de même pour les darks bien sûr, rendre tous les noms de dossier modulables, en faire un module avec des E/S, en passant le nom du répertoire de travail etc …

Exemple de traitement avec Execute

Ce code fait la même chose que l’exemple ci-dessus, sauf qu’il utilise largement la méthode Execute de la classe Siril. Execute(‘une commande’) fonctionne exactement comme si vous tapiez ‘une commande’ dans la ligne de commande Siril.

import sys
import os

from pysiril.siril   import *

# ==============================================================================
# EXAMPLE OSC_Processing with Execute function without wrapper functions
# ==============================================================================

def master_bias(bias_dir, process_dir):
    app.Execute("cd " + bias_dir )
    app.Execute("convert bias -out=" + process_dir + " -fitseq" )
    app.Execute("cd " + process_dir )
    app.Execute("stack bias rej 3 3  -nonorm")
    
def master_flat(flat_dir, process_dir):
    app.Execute("cd " + flat_dir + "\n"
                "convert flat -out=" + process_dir + " -fitseq"   + "\n"
                "cd " + process_dir  + "\n"
                "preprocess flat  -bias=bias_stacked"  + "\n"
                "stack  pp_flat rej  3 3 -norm=mul")
    
def master_dark(dark_dir, process_dir):
    app.Execute(""" cd %s
                    convert dark -out=%s -fitseq
                    cd %s
                    stack dark rej 3 3 -nonorm """ % (dark_dir,process_dir,process_dir) )
    
def light(light_dir, process_dir):
    app.Execute("cd " + light_dir)
    app.Execute("convert light -out=" + process_dir + " -fitseq"  )
    app.Execute("cd " + process_dir )
    app.Execute("preprocess light -dark=dark_stacked -flat=pp_flat_stacked -cfa -equalize-cfa -debayer" )
    app.Execute("register pp_light")
    app.Execute("stack r_pp_light rej 3 3 -norm=addscale -output_norm -out=../result")
    app.Execute("close")
    
# ==============================================================================
workdir     = "/home/barch/siril/work/TestSiril"
try:
    app.Open(  )
    process_dir = '../process'
    app.Execute("set16bits")
    app.Execute("setext fit")
    master_bias(workdir+ '/biases' ,process_dir)
    master_flat(workdir+ '/flats'  ,process_dir)
    master_dark(workdir+ '/darks'  ,process_dir)
    light(workdir+ '/lights' ,process_dir)
except Exception as e :
    print("\n**** ERROR *** " +  str(e) + "\n" )    
    
app.Close( )
del app

Libre à vous d’utiliser les fonctions Execute ou wrapper : vous connaissez les 2 principaux moyens de contrôler Siril avec Python.

Utiliser Addons

L’exemple ci-dessous montre comment utiliser certaines des fonctions de la classe Addons.

import sys
import os

from pysiril.siril   import *
from pysiril.wrapper import *
from pysiril.addons  import *

# ==============================================================================
# Example of addons functions 
# ==============================================================================
app=Siril()
try:
    cmd=Wrapper(app)
    fct=Addons(app)
    
    
    # Create a seqfile
    
    workdir     = "D:/_TraitAstro/20-SiriL/work/TestSiril"
    processdir  = workdir + "/" + "xxxx"
    
    fct.CleanFolder(processdir,ext_list=[ ".cr2",".seq"])
    
    fct.MkDirs(processdir)
    
    NbImage= fct.GetNbFiles(workdir+ '/lights/*.CR2')
    print( "CR2 number:",NbImage)
    
    number=fct.NumberImages(workdir+ '/lights/*.CR2',processdir,"offsets",start=10,bOverwrite=True)
    
    if number == NbImage  :
        fct.CreateSeqFile(processdir+"/toto.seq", number )
    else:
        print("error of images number:",number, "<>",NbImage)
        
except Exception as e :
    print("\n**** ERROR *** " +  str(e) + "\n" )    
    
app.Close()
del app

Fonctions renvoyant des valeurs

L’exemple ci-dessous montre comment utiliser les fonctions renvoyant des valeurs. Il prend en entrée un fichier *.seq et écrit un csv avec les valeurs renvoyées par la commande stat. Remarque: cet exemple est désormais obsolète grâce à la commande seqstat ajoutée dans Siril 0.99.8. Néanmoins, il peut vous donner des idées sur la façon d’utiliser ces fonctions.

import os,sys
import re
import glob
from pysiril.siril import Siril
from pysiril.wrapper import Wrapper
from distutils.util import strtobool
import pandas as pd

       
def Run(inputfilename):

    folder,filename=os.path.split(inputfilename)
    fileroot,_=os.path.splitext(filename)
    os.chdir(folder)

    with open(inputfilename) as f:
        lines = list(line for line in (l.strip() for l in f) if line)

    img=[]
    for i,l in enumerate(lines):
        if l.startswith('S'):
            specline=l.split()
            lenseq=int(specline[3])
            
        if l.startswith('I'):
            tl=l.split()
            img.append(int(tl[1]))

    for ff in glob.iglob(fileroot+'*.f*'):
        _,fitext=os.path.splitext(ff)
        if not(fitext=='.seq'):
            break


    app=Siril(bStable=False)
    app.Open()
    cmd=Wrapper(app)


    res=[]
    for i in range(lenseq):
        fitfile='{0:s}{1:05d}{2:s}'.format(fileroot,img[i],fitext)
        cmd.load(fitfile)
        _,stats=cmd.stat()
        for j in range(len(stats)):
            stats[j]['file']=fitfile
            stats[j]['image#']=img[i]
            res.append(stats[j])
    app.Close()
    del app

    data=pd.DataFrame.from_dict(res)
    data.set_index(['file','image#','layer'],inplace=True)
    data.reset_index(inplace=True)
    data.to_csv(fileroot+'stats.csv',index=False)


if __name__ == "__main__":
    args=[]
    kwargs={}
    for a in sys.argv[1:]:
        if '=' in a:
            f,v=a.split('=')
            kwargs[f]=v
        else:
            args.append(a)
    Run(*tuple(args),**kwargs)

Pour l’exécuter:

  • copiez / collez ce code dans votre éditeur préféré
  • enregistrez-le sous seqstat.py
  • dans un shell, tapez:
python seqstat.py "C:\Users\myusername\Pictures\astro\myseqfile_.seq"

Cela enregistrera myseqfile_stats.csv dans le même dossier.

Appliquer une commande pour image seule à toute une séquence

Ce dernier exemple est l’équivalent python des scripts shell décrits ici . Il utilise toutes les notions abordées auparavant. Copiez le code suivant et sauvez le fichier sous le nom genseqscript.py.

# Usage
#######
# genseqscript.py command seqname [prefix ext]
# Examples:
###########
#
# Apply a median filter to all images from C:\MyImages\r_pp_light_.seq with "fit" extension and save them with "med_" prefix
# python genseqscript.py "fmedian 5 1" "C:\MyImages\r_pp_light_.seq" med_ fit
#
# Apply a 90 deg rotation w/o crop to all images from pp_light_.seq located in current folder
# python genseqscript.py "rotate 90 -nocrop" pp_light_ rot90_


# User settings
# command: the command to be applied to each image of the sequence. Enclosed in double quotes if there is more than one word
# seqname: name of the sequence, can be a full path or just a sequence namein the current directory (w or w/o .seq extension). Enclosed in double quotes if there are spaces in the path
# prefix: (optional) the prefix to be preprended to the processed file names, def: ""
# ext: (optional) chosen FITS extension, def: fits (can also be fit or fts)
import os,sys
from pysiril.siril import Siril
from pysiril.wrapper import Wrapper
from pysiril.addons import Addons

def Run(command,seqname,prefix='',ext='fits'):

    print('Command to be run: {0:s}'.format(command))
    print('prefix: {0:s}'.format(prefix))
    print('FITS extension: {0:s}'.format(ext))

    if os.path.isabs(seqname):
        currdir,seqname=os.path.split(seqname)
    else:
        currdir=os.getcwd()

    seqname,seqext=os.path.splitext(seqname)
    if len(seqext)==0:
        seqext='.seq'

    print('Working directory: {0:s}'.format(currdir))
    print('Sequence to be processed: {0:s}{1:s}'.format(seqname,seqext))

    if not(os.path.isfile(os.path.join(currdir,seqname+seqext))):
        print('The specified sequence does not exist - aborting')
        sys.exit()

    print('Starting PySiril')
    app = Siril(R'C:\Program Files\SiriL\bin\siril-cli.exe')
    AO = Addons(app)
    cmd = Wrapper(app)

    print('Starting Siril')
    try:
        app.Open()
        seqfile = AO.GetSeqFile(os.path.join(currdir,seqname+seqext))
        app.Execute('setext {:s}'.format(ext))
        app.Execute('cd "{:s}"'.format(currdir))
        for im in seqfile['images']:
            currframenb=im['filenum']
            currframe='{0:s}{1:05d}.{2:s}'.format(seqname,currframenb,ext)
            if not(os.path.isfile(os.path.join(currdir,currframe))):
                print('First file {0:s} does not exist... check if the .seq file is valid or the selected FITS extension ("{1:s}" defined here) matches your files - aborting'.format(currframe,ext))
                sys.exit()
            print('processing file: {0:s}'.format(currframe))
            savename=prefix+currframe
            cmd.load(currframe)
            app.Execute(command)
            cmd.save(savename)
        app.Close()
        del app
    except Exception as e :
        print("\n**** ERROR *** " +  str(e) + "\n" )


if __name__ == "__main__":
    args=[]
    kwargs={}
    for a in sys.argv[1:]:
        if '=' in a:
            f,v=a.split('=',1)
            kwargs[f]=v
        else:
            args.append(a)
    Run(*tuple(args),**kwargs)