# starflattener.py : to be cool the image of everyday stars
# Copyright (c) 2020-2026 by Kappa v.v.v , All rights reserved.
# last updated: 2026/1/24 by Kappa v.v.v

prgname= "starflattener"
version= "2.0.8"
cpright= "Copyright (c) 2020-2026 by Kappa v.v.v , All rights reserved."

import os
import sys
import shutil
import glob
import signal
import numpy
import rawpy
from PIL import Image
from PIL import ImageTk
import astropy.io.fits as fits
import piexif
import cv2
import tkinter as tk
import tkinter.filedialog as tkdialog
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import threading
from concurrent.futures import ThreadPoolExecutor
import copy
import time
import datetime

## for CPU
import numpy as np
from scipy.ndimage import gaussian_filter as gaussian_filter
from scipy.ndimage import minimum_filter as minimum_filter

## for CUDA
#import cupy
#import cupy as np
#from cupyx.scipy.ndimage import gaussian_filter as gaussian_filter
#from cupyx.scipy.ndimage import minimum_filter as minimum_filter

exif_out_tags= {
  "0th":  [
     270, #ImageDescription
     271, #Make
     272, #Model
     274, #Orientation
     305, #Software
     306, #DateTime
     315, #Artist
     33432, #Copyright
     ],
  "Exif": [
    34850, #ExposureProgram
    33434, #ExposureTime
    33437, #FNumber
    34855, #ISOSpeedRatings
    36867, #DateTimeOriginal
    36868, #DateTimeDigitized
    36880, #OffsetTime
    36881, #OffsetTimeOriginal
    36882, #OffsetTimeDigitized
    37380, #ExposureBiasValue
    37381, #MaxApertureValue
    37383, #MeteringMode
    37385, #Flash
    37386, #FocalLength
    41987, #WhiteBalance
    41989, #FocalLengthIn35mmFilm
    42034, #LensSpecification
    42036, #LensModel
    ],
}

def fitting_optflat( array_org, flat_in ):
  if len( flat_in[3] ) == 0:
    print( "- fitting optflat... to sharpen histogram..." )
    array_optflat= flat_in[2]
    array_fitflat= copy.deepcopy( array_optflat )
    a= [ 1, 1, 1 ]
    b= [ 0, 0, 0 ]
    x= [ 0, 0, 0 ]
    sorted_hist= [ [], [], [] ]
    n= 0
    msg= ""
    for i in range( 10 ):
      list_ab= [
        [ 0.9, 0 ], [ 1.1, 0 ],
        [ 0.97, 0 ], [ 1.03, 0 ],
        [ 1, -0.05 ], [ 1, 0.05 ],
      ]
      for xa, xb in list_ab:
        print( "- fitting #%d R=%.2f G=%.2f B=%.2f sharpness=%.0f:%.0f:%.0f...   "%( i+1,
          a[0], a[1], a[2], x[0], x[1], x[2] ), end= "\r" )
        fskip= [ 0, 0, 0 ]
        for c in [ 0, 1, 2 ]:
          ax= a[c] * xa
          bx= b[c] + xb
          array_fitflat[:,:,c]= ax * array_optflat[:,:,c] + bx
          if np.mean( array_fitflat[:,:,c] ) < 0:
            fskip[c]= 1
        array_fit= fdiff( array_org, array_fitflat, 0, 0 )
        img_fit= array2img( array_fit )
        hist= img_fit.histogram()
        sorted_hist[0]= sorted( hist[1:255] )
        sorted_hist[1]= sorted( hist[257:511] )
        sorted_hist[2]= sorted( hist[513:767] )
        nc= 0
        for c in [ 0, 1, 2 ]:
          if fskip[c]:
            nc += 1
            continue
          m= sorted_hist[c][-1] - numpy.mean( sorted_hist[c][-20:-1] )
          if m > x[c]:
            a[c]= ax
            b[c]= bx
            x[c]= m
          else:
            nc += 1
        if nc < 3:
          n= 0
        else:
          n += 1
      if n > len( list_ab ):
        break
    else:
      msg= "clipped..."
    print( " "*76, end="\r" )
    for c in [ 0, 1, 2 ]:
      print( "color=%s a=%.2f b=%.2f sharpness=%.0f %s"%( "RGB"[c], a[c], b[c], x[c], msg ) )
      array_fitflat[:,:,c]= a[c] * array_optflat[:,:,c] + b[c]
    flat_in[2]= array_fitflat
    flat_in[3]= [ a, b ]
  return fdiff( array_org, flat_in[2], 0, 0 )

def raw2imgF( filename, dark_in, flat_in, bright= 0.5 ):
  ext= filename[filename.rfind("."):].lower()
  if ext == ".jpg" or ext == ".tif":
    try:
      img2= Image.open( filename )
    except:
      print( "warning: skip input file (%s)..."%( filename ) )
      return 0
    img1= array2img( np.full( (img2.height, img2.width, 3), 0 ) )
    imgF= [ img2, img1 ]
  elif ext == ".fit":
    try:
      hdulist= fits.open( filename )
    except:
      print( "warning: skip input file (%s)..."%( filename ) )
      return 0
    h_data= hdulist[0].data
    if len( h_data.shape ) == 2:
      h_data= cv2.cvtColor( h_data, cv2.COLOR_BayerGRBG2RGB_EA )
      rgb= h_data.astype( np.float32 )
    else:
      rgb= h_data.swapaxes( 0, 2 ).swapaxes( 0, 1 ).astype( np.float32 )
    if "cupy" in globals():
      rgb= np.asarray( rgb )
    min= np.min( rgb )
    max= np.max( rgb )
    rgb= ( rgb - min ) / ( max - min )
    img= array2img( rgb )
    hist= img.histogram()
    n_tgt= sum( hist[:256] + hist[512:] ) *0.01
    n= 0
    for min in range( 256 ):
      n += hist[min] + hist[512+min]
      if n >= n_tgt:
        break
    n= 0
    for max in range( 255, 0, -1 ):
      n += hist[max] + hist[512+max]
      if n >= n_tgt:
        break
    if min >= max:
      if min > 0:
        min -= 1
      if max < 255:
        max += 1
    min /= 256
    max /= 256
    rgb= ( rgb - min ) / ( max - min )
    rgb= fsigmo( rgb * bright, 0.4, 0 )
    if "cupy" in globals():
      rgb= np.asnumpy( rgb )
    rgb= ( rgb *65535 ).astype( np.uint16 )
    img1= Image.fromarray( (rgb %255).astype( np.uint8 ) )
    img2= Image.fromarray( (rgb >>8).astype( np.uint8 ) )
    imgF= [ img2, img1 ]
  else:
    try:
      raw= rawpy.imread( filename )
    except:
      print( "warning: skip input file (%s)..."%( filename ) )
      return 0
    rgb= raw.postprocess( output_bps= 16, use_camera_wb= True, bright= bright )
    img1= Image.fromarray( (rgb %255).astype( np.uint8 ) )
    img2= Image.fromarray( (rgb >>8).astype( np.uint8 ) )
    imgF= [ img2, img1 ]
  f_dark= ( dark_in[1] > 0 and dark_in[0] != ""  )
  f_flat= ( flat_in[1] > 0 and flat_in[0] != "" )
  if f_dark or f_flat:
    array_org= imgF2array( imgF )
    if f_dark:
      if len( dark_in[3] ) == 0:
        dark_in[3]= gaussian_filter( array_org, sigma= [ 5, 5, 0 ] )
      array_org= fdark( array_org, dark_in[2], dark_in[3] )
    if f_flat:
      array_org= fitting_optflat( array_org, flat_in )
    imgF= array2imgF( array_org )
  return imgF

def img2array( img ):
  array= np.array( img ).astype( np.float32 ) /255
  return array

def img2nparray( img ):
  array= numpy.array( img ).astype( numpy.float32 ) /255
  return array

def imgF2array( imgF ):
  [ img2, img1 ]= imgF
  array1= np.array( img1 ).astype( np.float32 ) /255
  array2= np.array( img2 ).astype( np.float32 ) /256
  array= array2 + 0.00390625* array1
  return array

def array2img( array ):
  if "cupy" in globals():
    array= np.asnumpy( array )
  img= Image.fromarray( (array *255).astype( np.uint8 ) )
  return img

def array2imgF( array ):
  array2= (array *255).astype( np.uint8 )
  array1= ((array *255 - array2) *255).astype( np.uint8 )
  if "cupy" in globals():
    with ThreadPoolExecutor( max_workers= 2 ) as exec:
      task0= exec.submit( np.asnumpy, array1 )
      task1= exec.submit( np.asnumpy, array2 )
    array1= task0.result()
    array2= task1.result()
  img1= Image.fromarray( array1 )
  img2= Image.fromarray( array2 )
  return [ img2, img1 ]

def fdark( org, dark, correct ):
  dark1= 0.9* dark
  dark2= 1.1* dark
  return np.where( (org > dark1) & (org < dark2), correct, org )

def img2flat( img, r_flat= 0.005 ):
  array_org= img2array( img )
  if r_flat > 0:
    size_tmp= ( img.width //10, img.height //10 )
    r_gauss= img.width * r_flat
    array_tmp= img2array( img.resize( size_tmp ) )
    array_r= array_tmp[:,:,0]
    array_g= array_tmp[:,:,1]
    array_b= array_tmp[:,:,2]
    if r_flat > 0.01:
      r_limit= min( np.sqrt( ( r_flat -0.01 ) /0.005 ), 1 )
      limit_r= np.mean( array_r ) * r_limit
      limit_g= np.mean( array_g ) * r_limit
      limit_b= np.mean( array_b ) * r_limit
      array_r= np.where( array_r < limit_r, limit_r, array_r )
      array_g= np.where( array_g < limit_g, limit_g, array_g )
      array_b= np.where( array_b < limit_b, limit_b, array_b )
    flat_r= gaussian_filter( array_r, sigma= r_gauss )
    flat_g= gaussian_filter( array_g, sigma= r_gauss )
    flat_b= gaussian_filter( array_b, sigma= r_gauss )
    if "cupy" in globals():
      flat_r= cupy.asnumpy( flat_r )
      flat_g= cupy.asnumpy( flat_g )
      flat_b= cupy.asnumpy( flat_b )
    array_flat= numpy.stack( [ flat_r, flat_g, flat_b ], axis= -1 )
    imgF_flat= array2imgF( array_flat )
    img_flat2= imgF_flat[0].resize( img.size, resample= Image.Resampling.BILINEAR )
    img_flat1= imgF_flat[1].resize( img.size, resample= Image.Resampling.BILINEAR )
    array_flat= imgF2array( [ img_flat2, img_flat1 ] )
  else:
    size_img= ( img.height, img.width )
    array_flat_r= np.full( size_img, np.mean( array_org[:,:,0] ) )
    array_flat_g= np.full( size_img, np.mean( array_org[:,:,1] ) )
    array_flat_b= np.full( size_img, np.mean( array_org[:,:,2] ) )
    array_flat= np.stack( [ array_flat_r, array_flat_g, array_flat_b ], axis= -1 )
  return array_flat

def denoise_array( array_org, size ):
  r_size= ( size, size )
  flat_r= minimum_filter( array_org[:,:,0], size= r_size )
  flat_g= minimum_filter( array_org[:,:,1], size= r_size )
  flat_b= minimum_filter( array_org[:,:,2], size= r_size )
  if "cupy" in globals():
    flat_r= cupy.asnumpy( flat_r )
    flat_g= cupy.asnumpy( flat_g )
    flat_b= cupy.asnumpy( flat_b )
    array_org= cupy.asnumpy( array_org )
  array_denoise= numpy.stack( [ flat_r, flat_g, flat_b ], axis= -1 )
  if "cupy" in globals():
    array_denoise= cupy.asarray( array_denoise )
  return array_denoise

def fsigmo( x, y, z ):
  # y: -0.5~0.5
  # z: 0.0~1.0
  sigmo= (0.5+ x * z) / (0.5+ z + np.exp( -10 * (x - y) ))
  return sigmo

def fdiff( b, f, r_dark, r_light ):
  if r_light >= 0:
    y= r_dark
    z= 1- r_light
  else:
    y= r_dark - 0.5* r_light
    z= 1
  r= fsigmo( (b - f) / (1- f), y, z );
  return r

def fbrightness( x, g ):
  if g == 1:
    return x
  return 1- ( 1 - x ) ** ( 1/ g )

def brightness_array( array_org, r, g, b ):
  # r, g, b : -1.0 ~ 1.0
  bright_r= 1- r /3
  bright_g= 1- g /3
  bright_b= 1- b /3
  array_out_r= fbrightness( array_org[:,:,0], bright_r )
  array_out_g= fbrightness( array_org[:,:,1], bright_g )
  array_out_b= fbrightness( array_org[:,:,2], bright_b )
  array_out= np.stack( [ array_out_r, array_out_g, array_out_b ], axis= -1 )
  return array_out

def selfflat_array( imgF_org, r_flat, param, mix_org ):
  array_org= denoise_array( imgF2array( imgF_org ), 1 )
  r, g, b, e= param
  array_flat= img2flat( imgF_org[0], r_flat )
  array_out= fdiff( array_org, array_flat, 0.01, e )
  if mix_org > 0:
    array_out= array_out * (1- mix_org) + array_org * mix_org
  array_out= brightness_array( array_out, r, g, b )
  return array_out

def selfflat_array_pre( imgF_org ):
  array_org= denoise_array( imgF2array( imgF_org ), 2 )
  return array_org

def selfflat_array_post( array_org, r_flat, param, mix_org ):
  r, g, b, e= param
  array_flat= img2flat( array2img( array_org ), r_flat )
  array_out= fdiff( array_org, array_flat, 0.01, e )
  if mix_org > 0:
    array_out= array_out * (1- mix_org) + array_org * mix_org
  array_out= brightness_array( array_out, r, g, b )
  return array_out

def color2mono( array_color ):
  array_mono= 0.4* array_color[:,:,0] + 0.2* array_color[:,:,1] + 0.4* array_color[:,:,2]
  return array_mono

def selfflat_img_align( img_org ):
  array_org= color2mono( img2array( img_org ) )
  array_flat= color2mono( img2flat( img_org, 0.001 ) )
  array_denoise= gaussian_filter( array_org, sigma= 3 )
  array_out= fdiff( array_denoise, array_flat, 0.1, 0 )
  return array2img( array_out )

def max_array_lighten( array_base, array_add ):
  array_out= np.where( array_base > 1.03* array_add, array_base,
    np.where( array_base < 0.97* array_add, array_add,
    (array_base + array_add)/2 ) )
  return array_out

class align:

  def __init__( s, img, spots, crop_size ):
    img_align= selfflat_img_align( img )
    find_size= crop_size //2
    s.xn= - find_size
    s.xp= find_size +1
    s.area_array= []
    for spot in spots:
      area= s.crop_area( spot )
      img_crop= img_align.crop( area )
      s.area_array += [[ area, img2nparray( img_crop ) ]]
    n= find_size *2 +1
    aligns= []
    for i in range( n ):
      for j in range( n ):
        aligns += [( i-find_size, j-find_size) ]
    s.align_crops= []
    for align in aligns:
      crops= []
      for spot in spots:
        crops += [ s.crop_area(( spot[0]+align[0], spot[1]+align[1] )) ]
      s.align_crops += [[ align, crops ]]
    s.nalign= len( s.align_crops )

  def crop_area( s, spot ):
    return [ spot[0]+s.xn, spot[1]+s.xn, spot[0]+s.xp, spot[1]+s.xp ]

  def task_align( s, i ):
    align1, crops= s.align_crops[i]
    score= 0
    nscore= 0
    for j in range( len( crops ) ):
      area0, array0= s.area_array[j]
      img1= s.img_align.crop( crops[j] )
      array1= img2nparray( img1 )
      score += numpy.sum( ( array1 - array0 ) **2 )
      nscore += 1
    if nscore == 0:
      return []
    return [ score / nscore, align1 ]

  def align( s, img ):
    s.img_align= selfflat_img_align( img )
    tasks= [ 0 for i in range( s.nalign ) ]
    ncore= max( 2, os.cpu_count() -1 )
    with ThreadPoolExecutor( max_workers= ncore ) as exec:
      for i in range( s.nalign ):
        tasks[i]= exec.submit( s.task_align, i )
    scores= []
    for i in range( s.nalign ):
      scores += [ tasks[i].result() ]
    scores.sort()
    return scores[0][1]

class composite:

  def __init__( s, print_log ):
    s.raw_imgs= []
    s.bright= 0.5
    s.tif_out= "output.tif"
    s.mode= 0
    s.rotate_dr= 0.0
    s.align_pos= []
    s.r_flat= 0.005
    s.param= [ 0, 0, 0, 0 ]
    s.mix_org= 0
    s.crop_size= 100
    s.dark_in= [ "", 0, [], [] ]
    s.flat_in= [ "", 0, [], [] ]
    s.lens= ""
    s.time= ""
    s.date= ""
    s.prilog= print_log
    s.time1= 0
    s.time2= 0
    s.dt= datetime.datetime.now()
    tmp_dir= os.environ.get( "temp" )
    if tmp_dir:
      s.tmp_mp4= tmp_dir+"/tmp_"+prgname+"_%d.mp4"%( os.getpid() )
    else:
      s.tmp_mp4= "tmp_"+prgname+"_%d.mp4"%( os.getpid() )
    s.video= 0
    s.vsize= (1080, 1080)

  def set_argv( s, argv ):
    flag= 0
    f_opt= 0
    wildarg= []
    for arg in argv:
      argex= glob.glob( arg )
      if argex:
        wildarg += argex
      else:
        wildarg += [ arg ]
    n= len( wildarg )
    i= 0
    while i < n:
      argex= wildarg[i]
      if argex[0] == "-":
        f_opt += 1
      if argex == "-out":
        i += 1
        if i >= n:
          break
        s.tif_out= wildarg[i]
      elif argex == "-lighten":
        s.mode= 0
      elif argex == "-average":
        s.mode= 1
      elif argex == "-rotate":
        i += 1
        if i >= n:
          break
        s.rotate_dr= float( wildarg[i] )
      elif argex == "-align_pos" or argex == "-spot":
        i += 2
        if i >= n:
          break
        s.align_pos += [ [ int( wildarg[i-1] ), int( wildarg[i] ) ] ]
      elif argex == "-gauss" or argex == "-r_flat":
        i += 1
        if i >= n:
          break
        s.r_flat= float( wildarg[i] ) / 1000
      elif argex == "-bright" or argex == "-color":
        i += 3
        if i >= n:
          break
        r= float( wildarg[i-2] )
        g= float( wildarg[i-1] )
        b= float( wildarg[i] )
        s.param[:3]= [ r, g, b ]
      elif argex == "-enhance":
        i += 1
        if i >= n:
          break
        en= float( wildarg[i] )
        s.param[3]= en
      elif argex == "-mix_org":
        i += 1
        if i >= n:
          break
        s.mix_org= float( wildarg[i] )
      elif argex == "-raw_bright":
        i += 1
        if i >= n:
          break
        s.bright= float( wildarg[i] )
      elif argex == "-align_size" or argex == "-crop_size":
        i += 1
        if i >= n:
          break
        s.crop_size= int( wildarg[i] )
      elif argex == "-dark":
        i += 1
        if i >= n:
          break
        file= wildarg[i]
        s.dark_in[:2]= [ file, 1 ]
      elif argex == "-dark_ratio":
        i += 1
        if i >= n:
          break
        ratio= float( wildarg[i] )
        s.dark_in[1]= ratio
      elif argex == "-flat":
        i += 1
        if i >= n:
          break
        file= wildarg[i]
        s.flat_in[:2]= [ file, 1 ]
      elif argex == "-flat_ratio":
        i += 1
        if i >= n:
          break
        ratio= float( wildarg[i] )
        s.flat_in[1]= ratio
      elif argex == "-vsize":
        i += 2
        f_opt -= 1
        if i >= n:
          break
        s.vsize= ( int( wildarg[i-1] ), int( wildarg[i] ) )
      elif argex[0] == "-":
        print( "error: unknown option %s. see -help for usage."%( argex ) )
        sys.exit( 0 )
      else:
        s.raw_imgs += [ argex ]
      i += 1
    s.n_raw= len( s.raw_imgs )
    return f_opt

  def shift_img( s, img, offset ):
    return img.transform( img.size, Image.Transform.AFFINE,
      (1,0,offset[0],0,1,offset[1]), resample=Image.Resampling.BICUBIC, fillcolor=(0,0,0) )

  def calc_array( s, raw_in ):
    imgF_org= raw2imgF( raw_in, s.dark_in, s.flat_in, s.bright )
    if imgF_org == 0:
      return 0
    if s.rotate_dr != 0.0:
      with ThreadPoolExecutor( max_workers= 2 ) as exec:
        task0= exec.submit( imgF_org[0].rotate, s.rotate_r )
        task1= exec.submit( imgF_org[1].rotate, s.rotate_r )
      imgF_org[0]= task0.result()
      imgF_org[1]= task1.result()
      s.rotate_r += s.rotate_dr
    if s.align_pos:
      if len( s.array_sum ) == 0:
        s.align_img= align( imgF_org[0], s.align_pos, s.crop_size )
      else:
        with ThreadPoolExecutor( max_workers= 2 ) as exec:
          task0= exec.submit( s.shift_img, imgF_org[0], s.sum_offset )
          task1= exec.submit( s.shift_img, imgF_org[1], s.sum_offset )
        imgF_org[0]= task0.result()
        imgF_org[1]= task1.result()
        s.offset= s.align_img.align( imgF_org[0] )
        with ThreadPoolExecutor( max_workers= 2 ) as exec:
          task0= exec.submit( s.shift_img, imgF_org[0], s.offset )
          task1= exec.submit( s.shift_img, imgF_org[1], s.offset )
        imgF_org[0]= task0.result()
        imgF_org[1]= task1.result()
        s.sum_offset[0] += s.offset[0]
        s.sum_offset[1] += s.offset[1]
    if s.mode == 0:
      array_gen= selfflat_array_pre( imgF_org )
    elif s.mode == 1:
      array_gen= selfflat_array( imgF_org, s.r_flat, s.param, s.mix_org )
    else:
      print( "error: unknown mode (%d)."%( s.mode ) )
      return 0
    if len( s.array_sum ) == 0:
      s.array_sum= array_gen
    elif s.mode == 0:
      s.array_sum= max_array_lighten( s.array_sum, array_gen )
    elif s.mode == 1:
      s.array_sum += array_gen
    if s.mode == 0:
      array_gen= selfflat_array_post( s.array_sum, s.r_flat, s.param, s.mix_org )
    vimg= array2img( array_gen )
    rvideo= s.vsize[0] / s.vsize[1]
    if vimg.width / vimg.height >= rvideo:
      vw2= int( vimg.height * rvideo )
      vcrop= [ (vimg.width-vw2)//2, 0, (vimg.width+vw2)//2, vimg.height ]
    else:
      vh2= int( vimg.width / rvideo )
      vcrop= [ 0, (vimg.height-vh2)//2, vimg.width, (vimg.height+vh2)//2 ]
    vimg= np.array( vimg.crop( vcrop ).resize( s.vsize ) )
    if "cupy" in globals():
      vimg= np.asnumpy( vimg )
    vimg = cv2.cvtColor( vimg, cv2.COLOR_RGB2BGR )
    s.video.write( vimg )
    return 1

  def init( s ):
    s.array_sum= []
    s.n= 0
    s.rotate_r= 0
    s.sum_offset= [ 0, 0 ]
    if  s.dark_in[1] > 0 and s.dark_in[0] != "":
      s.dark_in[2]= img2array( Image.open( s.dark_in[0] ) ) * s.dark_in[1]
    if  s.flat_in[1] > 0 and s.flat_in[0] != "":
      s.flat_in[2]= img2array( Image.open( s.flat_in[0] ) ) * s.flat_in[1]
      s.flat_in[3]= []
    if s.video:
      s.video.release()
    fourcc= cv2.VideoWriter_fourcc( *"mp4v" )
    s.video= cv2.VideoWriter( s.tmp_mp4, fourcc, 10, s.vsize )
    s.time1= time.time()

  def prilog_setting( s ):
    s.prilog.init()
    s.prilog.print_update( "- process setting..." )
    if s.mode == 0:
      s.prilog.print_update( "composite mode     : lighten" )
    elif s.mode == 1:
      s.prilog.print_update( "composite mode     : average" )
    else:
      s.prilog.print_update( "composite mode     : unknown" )
    r, g, b, en= s.param
    s.prilog.print_update( "self-flat gauss    : %.0f"%( s.r_flat *1000 ) )
    s.prilog.print_update( "brightness RGB     : %.1f %.1f %.1f"%( r, g, b ) )
    s.prilog.print_update( "enhance curve      : %.1f"%( en ) )
    s.prilog.print_update( "mix original       : %.1f"%( s.mix_org ) )
    s.prilog.print_update( "raw brightness     : %.1f"%( s.bright ) )
    s.prilog.print_update( "alignment size pos : %d %s"%( s.crop_size, str( s.align_pos ) ) )
    s.prilog.print_update( "rotation [deg]     : %.2f"%( s.rotate_dr ) )
    s.prilog.print_update( "dark image ratio   : %.2f %s"%( s.dark_in[1], os.path.basename( s.dark_in[0] ) ) )
    s.prilog.print_update( "flat image ratio   : %.2f %s"%( s.flat_in[1], os.path.basename( s.flat_in[0] ) ) )
    s.prilog.print_update( "mp4 video size     : %s"%( str( s.vsize ) ) )

  def prilog_post( s ):
    if not s.prilog:
      return
    s.time2= time.time()
    s.dt= datetime.datetime.now()
    s.prilog.print_update( "- information..." )
    s.prilog.print_update( "#image used : %d"%( s.n ) )
    s.prilog.print_update( "date & time : %s"%( s.dt.strftime( "%Y:%m:%d %H:%M:%S" ) ) )
    s.prilog.print_update( "run time    : %.1fs"%( s.time2 - s.time1 ) )

  def next_img( s ):
    if s.n == 0:
      return "[%d/%d] %s"%( s.n+1, s.n_raw, s.raw_imgs[s.n] )
    elif s.n < s.n_raw:
      return "[%d/%d] %s"%( s.n+1, s.n_raw, os.path.basename( s.raw_imgs[s.n] ) )
    return None

  def orig( s ):
    if s.n < s.n_raw:
      imgF_org= raw2imgF( s.raw_imgs[s.n], s.dark_in, s.flat_in, s.bright )
      if imgF_org == 0:
        return 0
      s.array_sum= imgF2array( imgF_org )
      s.prilog_post()
      return 1
    else:
      return 0

  def exec( s, file_img ):
    if not s.calc_array( file_img ):
      return 0
    s.n += 1
    return 1

  def exec_post( s ):
    if s.mode == 0:
      s.array_sum= selfflat_array_post( s.array_sum, s.r_flat, s.param, s.mix_org )
    elif s.mode == 1:
      s.array_sum /= s.n
    s.align_pos= []
    s.mode= 2
    s.prilog_post()
    s.video.release()

  def get_img( s ):
    array_now= copy.deepcopy( s.array_sum )
    if s.mode == 0:
      array_now= selfflat_array_post( array_now, s.r_flat, s.param, s.mix_org )
    elif s.mode == 1:
      array_now /= s.n
    elif s.mode == 2:
      pass
    else:
      print( "error: unknown mode (%d)."%( s.mode ) )
      return
    return array2img( array_now )

  def final( s ):
    img_gen= array2img( s.array_sum )
    s.exif_out["0th"][305]= (prgname+" "+version).encode()
    s.exif_out["0th"][306]= s.dt.strftime( "%Y:%m:%d %H:%M:%S" ).encode()
    exif_bytes= piexif.dump( s.exif_out )
    file_out= os.path.splitext( s.tif_out )
    if file_out[-1] == ".mp4":
      file_mp4= file_out[0] +".mp4"
      shutil.copy2( s.tmp_mp4, file_mp4 )
    else:
      img_gen.save( s.tif_out, quality= 100, exif= exif_bytes )
    file_txt= file_out[0] +".txt"
    s.prilog.save( file_txt )

  def get_exif( s ):
    s.exif_out= {}
    if s.n_raw > 0:
      filename= s.raw_imgs[0]
      ext= filename[filename.rfind("."):].lower()
      if ext == ".fit":
        exif_dic= s.get_fitinfo( filename )
      else:
        try:
          exif_dic= piexif.load( filename )
        except:
          return
      for key1 in exif_out_tags:
        if key1 in exif_dic:
          if not key1 in s.exif_out:
            s.exif_out[key1]= {}
          for key2 in exif_out_tags[key1]:
            if key2 in exif_dic[key1]:
              s.exif_out[key1][key2]= exif_dic[key1][key2]
      if "Exif" in s.exif_out:
        s.lens= ""
        s.time= ""
        if 37386 in s.exif_out["Exif"]:
          leng0= s.exif_out["Exif"][37386]
          leng= leng0[0] / leng0[1]
          s.lens += "%.0fmm"%( leng )
        if 33437 in s.exif_out["Exif"]:
          fnum0= s.exif_out["Exif"][33437]
          fnum= fnum0[0] / fnum0[1]
          s.lens += " f%.1f"%( fnum )
        if 33434 in s.exif_out["Exif"]:
          etime0= s.exif_out["Exif"][33434]
          etime= etime0[0] / etime0[1]
          if etime < 1:
            s.time += "%d/%ds"%( etime0[0], etime0[1] )
          else:
            s.time += "%.0fs"%( etime )
        if 34855 in s.exif_out["Exif"]:
          iso= s.exif_out["Exif"][34855]
          s.time += " iso%d"%( iso )
        if 36867 in s.exif_out["Exif"]:
          date0= s.exif_out["Exif"][36867]
          s.date= date0.decode()[2:-3]
          s.date= s.date.replace( ":", "/", 2 )
        s.prilog.print_index( "- metadata..." )
        s.prilog.print_index( "focal-length F-num : %s"%( s.lens ) )
        s.prilog.print_index( "exposure-time ISO  : %s"%( s.time ) )
        s.prilog.print_index( "date & time        : %s"%( s.date ) )
    if not "0th" in s.exif_out:
      s.exif_out["0th"]= {}

  def get_fitinfo( s, filename ):
    exif_fit= {}
    exif_fit["0th"]= {}
    exif_fit["Exif"]= {}
    try:
      hdulist= fits.open( filename )
    except:
      return
    h_fit= hdulist[0].header
    if "PRODUCER" in h_fit:
      exif_fit["0th"][271]= h_fit["PRODUCER"].encode()
    if "INSTRUME" in h_fit:
      exif_fit["0th"][272]= h_fit["INSTRUME"].encode()
    if "EXPOSURE" in h_fit:
      exposure= h_fit["EXPOSURE"]
      if exposure < 1:
        exif_fit["Exif"][33434]= ( 1, int( 1/exposure ) )
      else:
        exif_fit["Exif"][33434]= ( int( 10*exposure ), 10 )
    if "APERTURE" in h_fit:
      aperture= h_fit["APERTURE"]
      exif_fit["Exif"][33437]= ( int( 10*aperture ), 10 )
    if "GAIN" in h_fit:
      gain= h_fit["GAIN"]
      exif_fit["Exif"][34855]= int( 100*2**(gain/60) )
    if "DATE-OBS" in h_fit:
      datetime= h_fit["DATE-OBS"]
      datetime= datetime.replace( "T", " " ).replace( "-", ":", 2 )[:22]
      exif_fit["Exif"][36867]= datetime.encode()
    if "FOCALLEN" in h_fit:
      focallen= h_fit["FOCALLEN"]
      exif_fit["Exif"][37386]= ( int( 10*focallen ), 10 )
    return exif_fit

class window:

  def __init__( s, compo ):
    font= ( "Arial", 12 )
    font_bold= ( "Arial", 12, "bold" )
    s.compo= compo
    s.compo.init()
    s.workdir= os.path.dirname( s.compo.raw_imgs[0] )
    s.item= None
    s.app= tk.Tk()
    s.app.title( "%s version %s  %s"%( prgname, version, cpright ) )
    s.app.geometry( "1200x660" )
    s.canvas= tk.Canvas( s.app, bg="black" )
    s.frame1= tk.Frame( s.app )
    s.frame2= tk.Frame( s.app )
    s.frame3= tk.Frame( s.app )
    s.label_info= tk.Label( s.app, font= font, text= "mode: init" )
    s.label_mode= tk.Label( s.frame1, font= font_bold, text= "composite mode" )
    s.radio= tk.IntVar( value= s.compo.mode )
    s.radio1= tk.Radiobutton( s.frame1, text= "lighten", font= font, value= 0, var= s.radio )
    s.radio2= tk.Radiobutton( s.frame1, text= "average", font= font, value= 1, var= s.radio )
    s.label_flat= tk.Label( s.frame1, font= font_bold, text= "self-flat gauss" )
    s.n_flat= tk.IntVar( value= 5 )
    s.scale_flat= tk.Scale( s.frame1, variable= s.n_flat, orient= "horizontal", from_= 0, to= 15 )
    s.label_rgb= tk.Label( s.frame1, font= font_bold, text= "brightness" )
    s.n_rgb= [ tk.IntVar( value= 0 ), tk.IntVar( value= 0 ), tk.IntVar( value= 0 ) ]
    s.scale_r= tk.Scale( s.frame1, variable= s.n_rgb[0], orient= "horizontal", from_= -10, to= 10, troughcolor="#faa" )
    s.scale_g= tk.Scale( s.frame1, variable= s.n_rgb[1], orient= "horizontal", from_= -10, to= 10, troughcolor="#afa" )
    s.scale_b= tk.Scale( s.frame1, variable= s.n_rgb[2], orient= "horizontal", from_= -10, to= 10, troughcolor="#aaf" )
    s.label_en= tk.Label( s.frame1, font= font_bold, text= "enhance curve" )
    s.n_en= tk.IntVar( value= 0 )
    s.scale_en= tk.Scale( s.frame1, variable= s.n_en, orient= "horizontal", from_= -10, to= 10 )
    s.label_mix= tk.Label( s.frame1, font= font_bold, text= "mix original" )
    s.n_mix= tk.IntVar( value= 0 )
    s.scale_mix= tk.Scale( s.frame1, variable= s.n_mix, orient= "horizontal", from_= 0, to= 10 )
    s.label_raw= tk.Label( s.frame2, font= font_bold, text= "raw bright 1/10" )
    s.n_raw= tk.IntVar( value= 5 )
    s.scale_raw= tk.Scale( s.frame2, variable= s.n_raw, orient= "horizontal", from_= 1, to= 10 )
    s.label_crop= tk.Label( s.frame2, font= font_bold, text= "alignment size" )
    s.n_crop= tk.IntVar( value= 100 )
    s.scale_crop= tk.Scale( s.frame2, variable= s.n_crop, orient= "horizontal", from_= 50, to= 200 )
    s.label_rot= tk.Label( s.frame2, font= font_bold, text= "rotation 1/400deg" )
    s.n_rot= tk.IntVar( value= 0 )
    s.scale_rot= tk.Scale( s.frame2, variable= s.n_rot, orient= "horizontal", from_= -100, to= 100 )
    s.button_preview= tk.Button( s.frame1, text= "preview", font= font, command= s.f_preview )
    s.button_compo= tk.Button( s.frame1, text= "composite\nstart / stop", font= font, command= s.f_compo )
    s.button_export= tk.Button( s.frame2, text= "export image", font= font, command= s.f_export )
    s.label_darkflat= tk.Label( s.frame2, font= font_bold, text= "dark / flat correct" )
    s.button_dark= tk.Button( s.frame2, text= "dark image", font= font, command= s.f_dark )
    s.n_dark= tk.IntVar( value= 0 )
    s.scale_dark= tk.Scale( s.frame2, variable= s.n_dark, orient= "horizontal", from_= 0, to= 100 )
    s.button_optflat= tk.Button( s.frame2, text= "flat image", font= font, command= s.f_optflat )
    s.n_optflat= tk.IntVar( value= 0 )
    s.scale_optflat= tk.Scale( s.frame2, variable= s.n_optflat, orient= "horizontal", from_= 0, to= 100 )
    s.label_data= tk.Label( s.frame2, font= font_bold, text= "metadata" )
    s.label_lens= tk.Label( s.frame2, font= font, text= s.compo.lens )
    s.label_time= tk.Label( s.frame2, font= font, text= s.compo.time )
    s.label_date= tk.Label( s.frame2, font= font, text= s.compo.date )
    s.fig= Figure()
    s.fig.set_facecolor( color= (0.9, 0.9, 0.9) )
    s.axes= s.fig.add_subplot( 1, 1, 1 )
    s.axes.patch.set_facecolor( color= (0.9, 0.9, 0.9) )
    s.fig_canvas= FigureCanvasTkAgg( s.fig, s.frame3 )
    s.fig_canvas.get_tk_widget().pack()
    s.label_info.pack( side= tk.TOP, anchor= tk.W, padx= 10, pady= 5 )
    s.frame1.pack( side= tk.LEFT, anchor= tk.N, fill= tk.Y )
    s.frame2.pack( side= tk.LEFT, anchor= tk.N, fill= tk.Y )
    s.canvas.pack( side= tk.TOP, anchor= tk.W, fill= tk.X, expand= True )
    s.canvas.config( width= 1200, height= 800 )
    s.frame3.pack( side= tk.TOP, anchor= tk.W, fill= tk.X, expand= True, pady= 5 )
    s.label_mode.pack( side= tk.TOP, anchor= tk.W, pady= 5 )
    s.radio1.pack( side= tk.TOP, anchor= tk.W, padx= 10 )
    s.radio2.pack( side= tk.TOP, anchor= tk.W, padx= 10 )
    s.label_flat.pack( side= tk.TOP, anchor= tk.W, pady= 5 )
    s.scale_flat.pack( side= tk.TOP, anchor= tk.W, padx= 10 )
    s.button_export.pack( side= tk.BOTTOM, anchor= tk.W, padx= 10, pady= 10 )
    s.button_compo.pack( side= tk.BOTTOM, anchor= tk.W, padx= 10, pady= 10 )
    s.button_preview.pack( side= tk.BOTTOM, anchor= tk.W, padx= 20, pady= 10 )
    s.label_rgb.pack( side= tk.TOP, anchor= tk.W, pady= 5 )
    s.scale_r.pack( side= tk.TOP, anchor= tk.W, padx= 10 )
    s.scale_g.pack( side= tk.TOP, anchor= tk.W, padx= 10 )
    s.scale_b.pack( side= tk.TOP, anchor= tk.W, padx= 10 )
    s.label_en.pack( side= tk.TOP, anchor= tk.W, pady= 5 )
    s.scale_en.pack( side= tk.TOP, anchor= tk.W, padx= 10 )
    s.label_mix.pack( side= tk.TOP, anchor= tk.W, pady= 5 )
    s.scale_mix.pack( side= tk.TOP, anchor= tk.W, padx= 10 )
    s.label_raw.pack( side= tk.TOP, anchor= tk.W, pady= 5 )
    s.scale_raw.pack( side= tk.TOP, anchor= tk.W, padx= 10 )
    s.label_crop.pack( side= tk.TOP, anchor= tk.W, pady= 5 )
    s.scale_crop.pack( side= tk.TOP, anchor= tk.W, padx= 10 )
    s.label_rot.pack( side= tk.TOP, anchor= tk.W, pady= 5 )
    s.scale_rot.pack( side= tk.TOP, anchor= tk.W, padx= 10 )
    s.label_darkflat.pack( side= tk.TOP, anchor= tk.W, pady= 5 )
    s.button_dark.pack( side= tk.TOP, anchor= tk.W, padx= 20, pady= 10 )
    s.scale_dark.pack( side= tk.TOP, anchor= tk.W, padx= 10 )
    s.button_optflat.pack( side= tk.TOP, anchor= tk.W, padx= 20, pady= 10 )
    s.scale_optflat.pack( side= tk.TOP, anchor= tk.W, padx= 10 )
    s.label_data.pack( side= tk.TOP, anchor= tk.W, pady= 5 )
    s.label_lens.pack( side= tk.TOP, anchor= tk.W, padx= 10 )
    s.label_time.pack( side= tk.TOP, anchor= tk.W, padx= 10 )
    s.label_date.pack( side= tk.TOP, anchor= tk.W, padx= 5 )
    s.spotid= 0
    #s.app.bind( "<KeyPress>", s.f_key )
    s.app.bind( "<ButtonPress>", s.f_mouse )
    s.loop_exec= 0
    s.busy= 1
    s.thread0= threading.Thread( target= s.exec_orig )
    s.thread0.start()

  def image2tk( s, image ):
    s.width= s.canvas.winfo_width()
    s.xw= s.width / image.width
    s.height= int( image.height * s.xw )
    s.canvas.config( height= s.height )
    size= ( s.width, s.height )
    resize_image= image.resize( size )
    s.tk_image= ImageTk.PhotoImage( resize_image )
    s.canvas.create_image( 2, 0, image=s.tk_image, anchor=tk.NW )
    hist= resize_image.histogram()
    xindex= [ i/253 for i in range( 254 ) ]
    s.axes.cla()
    s.axes.patch.set_facecolor( color= (0.9, 0.9, 0.9) )
    s.axes.plot( xindex, hist[1:255], color= "red" )
    s.axes.plot( xindex, hist[257:511], color= "green" )
    s.axes.plot( xindex, hist[513:767], color= "blue" )
    s.fig_canvas.draw()

  def mainloop( s ):
    s.app.mainloop()

  def init( s ):
    s.width= s.canvas.winfo_width()
    s.height= s.canvas.winfo_height()

  def get_setting( s ):
    s.compo.mode= s.radio.get()
    s.compo.r_flat= s.n_flat.get() /1000
    r= s.n_rgb[0].get() /10
    g= s.n_rgb[1].get() /10
    b= s.n_rgb[2].get() /10
    enhance= s.n_en.get() /10
    s.compo.param= [ r, g, b, enhance ]
    s.compo.mix_org= s.n_mix.get() /10
    s.compo.bright= s.n_raw.get() /10
    s.compo.crop_size= s.n_crop.get()
    s.compo.rotate_dr= s.n_rot.get() /400
    s.compo.dark_in[1]= s.n_dark.get() /100
    s.compo.flat_in[1]= s.n_optflat.get() /100

  def f_preview( s ):
    if s.busy:
      return
    s.busy= 1
    s.label_info["text"]= "mode: preview"
    s.get_setting()
    s.thread1= threading.Thread( target= s.exec_preview )
    s.thread1.start()
    return

  def f_compo( s ):
    if s.spotid:
      s.canvas.delete( s.spotid )
    if s.loop_exec:
      s.loop_exec= 0
      s.label_info["text"]= "stopping: composite"
      return
    if s.busy:
      return
    s.busy= 1
    s.label_info["text"]= "executing: composite"
    s.get_setting()
    s.loop_exec= 1
    s.thread2= threading.Thread( target= s.exec_compo )
    s.thread2.start()

  def f_export( s ):
    if s.busy:
      return
    s.busy= 1
    s.label_info["text"]= "executing: export as..."
    s.compo.tif_out= tkdialog.asksaveasfilename(
      filetypes= [("Tiff","*.tif"),("Jpeg","*.jpg"),("Mpeg4","*.mp4")],
      initialdir=s.workdir, defaultextension="tif" )
    if len( s.compo.tif_out ) > 0:
      s.label_info["text"]= "exported to: %s"%( s.compo.tif_out )
      s.compo.final()
    s.busy= 0

  def f_dark( s ):
    s.label_info["text"]= "executing: assign dark image..."
    s.compo.dark_in[0]= tkdialog.askopenfilename(
      filetypes= [("Tiff","*.tif"),("Jpeg","*.jpg")], initialdir=s.workdir )
    if len( s.compo.dark_in[0] ) > 0:
      s.label_info["text"]= "assigned dark image: %s"%( s.compo.dark_in[0] )

  def f_optflat( s ):
    s.label_info["text"]= "executing: assign flat image ..."
    s.compo.flat_in[0]= tkdialog.askopenfilename(
      filetypes= [("Tiff","*.tif"),("Jpeg","*.jpg")], initialdir=s.workdir )
    if len( s.compo.flat_in[0] ) > 0:
      s.label_info["text"]= "assigned flat image: %s"%( s.compo.flat_in[0] )

  def f_key( s, event ):
    key= event.char
    sym= event.keysym

  def f_mouse( s, event ):
    widget= event.widget
    x= event.x
    y= event.y
    if widget == s.canvas and s.loop_exec == 0:
      s.get_setting()
      crop_size= int( s.compo.crop_size * s.xw )
      c= crop_size //2
      xw= int( x / s.xw )
      yw= int( y / s.xw )
      s.label_info["text"]= "assigned spot: (%d,%d)"%( xw, yw )
      s.compo.align_pos= [ [ xw, yw ] ]
      if s.spotid:
        s.canvas.delete( s.spotid )
      s.spotid= s.canvas.create_rectangle( x-c, y-c, x+c, y+c, width=3, outline="green" )

  def exec_orig( s ):
    s.compo.time1= time.time()
    s.compo.init()
    next_img= s.compo.next_img()
    if next_img:
      s.label_info["text"]= "processing: original"
      if s.compo.orig():
        s.image2tk( array2img( s.compo.array_sum ) )
    s.label_info["text"]= "finished: original"
    s.busy= 0

  def exec_preview( s ):
    s.compo.time1= time.time()
    s.compo.prilog_setting()
    s.compo.init()
    for file_img in s.compo.raw_imgs:
      s.label_info["text"]= "processing: preview"
      if s.compo.exec( file_img ):
        s.compo.exec_post()
        s.image2tk( s.compo.get_img() )
        break
    if s.spotid:
      s.canvas.delete( s.spotid )
    s.loop_exec= 0
    s.label_info["text"]= "finished: preview"
    s.busy= 0

  def exec_compo( s ):
    s.compo.time1= time.time()
    s.compo.prilog_setting()
    s.compo.init()
    for file_img in s.compo.raw_imgs:
      if not s.loop_exec:
        s.compo.exec_post()
        s.image2tk( s.compo.get_img() )
        break
      s.label_info["text"]= "processing: %s"%( s.compo.next_img() )
      if s.compo.exec( file_img ):
        s.image2tk( s.compo.get_img() )
    else:
      s.compo.exec_post()
      s.image2tk( s.compo.get_img() )
    if s.spotid:
      s.canvas.delete( s.spotid )
    s.loop_exec= 0
    s.label_info["text"]= "finished: composite"
    s.busy= 0

class print_log:

  def __init__( s ):
    s.log_index= [ "produced by %s %s"%( prgname, version ) ]
    s.log_update= []

  def init( s ):
    s.log_update= []

  def print_index( s, str ):
    print( str )
    s.log_index += [ str ]

  def print_update( s, str ):
    print( str )
    s.log_update += [ str ]

  def save( s, file ):
    f= open( file, "w" )
    for str in s.log_index + s.log_update:
      print( str, file=f )
    f.close()

def cleanup( signum= 0, frame= 0 ):
  if "compo" in globals():
    if os.path.isfile( compo.tmp_mp4 ):
      compo.video.release()
      os.remove( compo.tmp_mp4 )
  sys.exit( 0 )

if __name__ == "__main__":
  signal.signal( signal.SIGINT, cleanup )
  print( "%s (python edition) version %s"%( prgname, version ) )
  print( "\t%s"%( cpright ) )
  if len( sys.argv ) <= 1 or sys.argv[1] == "-help":
    print( "usage: starflattener [options] img1 [...imgN]" )
    print( "        to execute w/ gui mode if no option is assigned." )
    print( "option: -out <image_file>   : output tif/jpg/mp4 filename. (* output.tif)" )
    print( "        -lighten          * : composite w/ lighten mode." )
    print( "        -average            : composite w/ average mode." )
    print( "        -gauss <D>          : gaussian ratio for self flatten." )
    print( "        -bright <R> <G> <B> : brightness control for R/G/B." )
    print( "        -enhance <R>        : tone curve enhancement ratio." )
    print( "        -mix_org <R>        : mixing ratio with original image." )
    print( "        -raw_bright <B>     : brightness in raw processing." )
    print( "        -align_size <S>     : size of alignment box." )
    print( "        -align_pos <X> <Y>  : alignment at position (X,Y)." )
    print( "        -rotate <R>         : rotation image R deg / n." )
    print( "        -dark <dark_file>   : input dark image filename." )
    print( "        -dark_ratio <R>     : ratio to apply dark correction." )
    print( "        -flat <flat_file>   : input optical flat image filename." )
    print( "        -flat_ratio <R>     : ratio to apply optical flat correction." )
    print( "        -vsize <W> <H>      : set video size when output mp4." )
    print( "                          * : default setting" )
  else:
    prilog= print_log()
    global compo
    compo= composite( prilog )
    f_opt= compo.set_argv( sys.argv[1:] )
    if "cupy" in globals():
      print( "- CUDA mode is sellected. to comment cupy and cupyx if unselect." )
    if f_opt:
      compo.prilog_setting()
      compo.get_exif()
      compo.init()
      for file_img in compo.raw_imgs:
        prilog.print_update( "- processing: %s..."%( file_img ) )
        compo.exec( file_img )
      else:
        compo.exec_post()
      prilog.print_update( "- written to: %s"%( compo.tif_out ) )
      compo.final()
    else:
      prilog.print_index( "- image files..." )
      for i in range( len( compo.raw_imgs ) ):
        prilog.print_index( "%3d %s"%( i+1, compo.raw_imgs[i] ) )
      compo.get_exif()
      win= window( compo )
      win.mainloop()
  cleanup()

"""
----- license of starflattener -----
Copyright (c) 2023-2025 by Kappa v.v.v , All rights reserved.

Redistribution without modification, use in source and binary forms
with or without modification, are permitted under all conditions.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

----- used packages w/ license -----
 Name                       Version      License
 astropy                    6.1.7               BSD License 
 matplotlib                 3.10.0       Python Software Foundation License
 numpy                      2.2.4        BSD License
 opencv-python              4.11.0.86    Apache Software License
 piexif                     1.1.3        MIT License
 pillow                     11.1.0       CMU License (MIT-CMU)
 rawpy                      0.24.0       MIT License
 scipy                      1.15.1       BSD License

----- matplotlib -----
License agreement for matplotlib versions prior to 1.3.0
========================================================

1. This LICENSE AGREEMENT is between John D. Hunter ("JDH"), and the
Individual or Organization ("Licensee") accessing and otherwise using
matplotlib software in source or binary form and its associated
documentation.

2. Subject to the terms and conditions of this License Agreement, JDH
hereby grants Licensee a nonexclusive, royalty-free, world-wide license
to reproduce, analyze, test, perform and/or display publicly, prepare
derivative works, distribute, and otherwise use matplotlib
alone or in any derivative version, provided, however, that JDH's
License Agreement and JDH's notice of copyright, i.e., "Copyright (c)
2002-2011 John D. Hunter; All Rights Reserved" are retained in
matplotlib  alone or in any derivative version prepared by
Licensee.

3. In the event Licensee prepares a derivative work that is based on or
incorporates matplotlib  or any part thereof, and wants to
make the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to matplotlib.

4. JDH is making matplotlib  available to Licensee on an "AS
IS" basis.  JDH MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, JDH MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF MATPLOTLIB
WILL NOT INFRINGE ANY THIRD PARTY RIGHTS.

5. JDH SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF MATPLOTLIB
 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR
LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING
MATPLOTLIB , OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF
THE POSSIBILITY THEREOF.

6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.

7. Nothing in this License Agreement shall be deemed to create any
relationship of agency, partnership, or joint venture between JDH and
Licensee.  This License Agreement does not grant permission to use JDH
trademarks or trade name in a trademark sense to endorse or promote
products or services of Licensee, or any third party.

8. By copying, installing or otherwise using matplotlib,
Licensee agrees to be bound by the terms and conditions of this License
Agreement.

----- numpy -----
Copyright (c) 2005-2024, NumPy Developers.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

    * Redistributions of source code must retain the above copyright
       notice, this list of conditions and the following disclaimer.

    * Redistributions in binary form must reproduce the above
       copyright notice, this list of conditions and the following
       disclaimer in the documentation and/or other materials provided
       with the distribution.

    * Neither the name of the NumPy Developers nor the names of any
       contributors may be used to endorse or promote products derived
       from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

----- opencv-python -----
MIT License

Copyright (c) Olli-Pekka Heinisuo

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

----- piexif -----
The MIT License (MIT)

Copyright (c) 2014, 2015 hMatoba

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

----- pillow -----
The Python Imaging Library (PIL) is

    Copyright © 1997-2011 by Secret Labs AB
    Copyright © 1995-2011 by Fredrik Lundh and contributors

Pillow is the friendly PIL fork. It is

    Copyright © 2010-2024 by Jeffrey A. Clark and contributors

Like PIL, Pillow is licensed under the open source HPND License:

By obtaining, using, and/or copying this software and/or its associated
documentation, you agree that you have read, understood, and will comply
with the following terms and conditions:

Permission to use, copy, modify and distribute this software and its
documentation for any purpose and without fee is hereby granted,
provided that the above copyright notice appears in all copies, and that
both that copyright notice and this permission notice appear in supporting
documentation, and that the name of Secret Labs AB or the author not be
used in advertising or publicity pertaining to distribution of the software
without specific, written prior permission.

SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL,
INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

----- rawpy -----
The MIT License (MIT)

Copyright (c) 2014 Maik Riechert

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

----- scipy -----
Copyright (c) 2001-2002 Enthought, Inc. 2003-2024, SciPy Developers.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

1. Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above
   copyright notice, this list of conditions and the following
   disclaimer in the documentation and/or other materials provided
   with the distribution.

3. Neither the name of the copyright holder nor the names of its
   contributors may be used to endorse or promote products derived
   from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

----- cupy -----
License
=======

Copyright (c) 2015 Preferred Infrastructure, Inc.

Copyright (c) 2015 Preferred Networks, Inc.


Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

"""
