From e20a38e7bb0695283d8b7396d08ce74fc47f14c9 Mon Sep 17 00:00:00 2001 From: Vladislav Tsendrovskii Date: Sat, 26 Oct 2024 15:10:14 +0200 Subject: [PATCH 01/10] Read tags for NEF, JPG/PNG --- src/vstarstack/library/loaders/classic.py | 29 ++++++++++-------- src/vstarstack/library/loaders/fits.py | 16 ++++------ src/vstarstack/library/loaders/nef.py | 19 ++++++------ src/vstarstack/library/loaders/ser.py | 9 ++---- src/vstarstack/library/loaders/tags.py | 36 ----------------------- src/vstarstack/library/loaders/video.py | 17 ++++------- src/vstarstack/library/loaders/yuv.py | 14 ++++----- 7 files changed, 46 insertions(+), 94 deletions(-) delete mode 100644 src/vstarstack/library/loaders/tags.py diff --git a/src/vstarstack/library/loaders/classic.py b/src/vstarstack/library/loaders/classic.py index 0e8f0a96..9a8f31d9 100644 --- a/src/vstarstack/library/loaders/classic.py +++ b/src/vstarstack/library/loaders/classic.py @@ -1,6 +1,6 @@ """Reading common image files: jpg/png/tiff""" # -# Copyright (c) 2023 Vladislav Tsendrovskii +# Copyright (c) 2023-2024 Vladislav Tsendrovskii # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -15,6 +15,7 @@ import numpy as np from PIL import Image +import exifread import vstarstack.library.common import vstarstack.library.data @@ -26,32 +27,36 @@ def readjpeg(fname: str): shape = rgb.shape shape = (shape[0], shape[1]) - tags = vstarstack.library.loaders.tags.read_tags(fname) + with open(fname, 'rb') as file: + tags = exifread.process_file(file) + params = { "w": shape[1], "h": shape[0], } - try: - exposure = tags["shutter"]*tags["iso"] - except KeyError as _: - exposure = 1 + if "EXIF ExposureTime" in tags: + tag = tags["EXIF ExposureTime"] + params["exposure"] = float(tag.values[0]) + else: + params["exposure"] = 1 + + if "EXIF ISOSpeedRatings" in tags: + tag = tags["EXIF ISOSpeedRatings"] + params["gain"] = float(tag.values[0]) + else: + params["gain"] = 1 - weight = np.ones((shape[0], shape[1]))*exposure + params["weight"] = params["exposure"] * params["gain"] dataframe = vstarstack.library.data.DataFrame(params, tags) - dataframe.add_channel(weight, "weight", weight=True) if len(rgb.shape) == 3: dataframe.add_channel(rgb[:, :, 0], "R", brightness=True, signal=True) dataframe.add_channel(rgb[:, :, 1], "G", brightness=True, signal=True) dataframe.add_channel(rgb[:, :, 2], "B", brightness=True, signal=True) - dataframe.add_channel_link("R", "weight", "weight") - dataframe.add_channel_link("G", "weight", "weight") - dataframe.add_channel_link("B", "weight", "weight") elif len(rgb.shape) == 2: dataframe.add_channel(rgb[:, :], "L", brightness=True, signal=True) - dataframe.add_channel_link("L", "weight", "weight") else: # unknown shape! pass diff --git a/src/vstarstack/library/loaders/fits.py b/src/vstarstack/library/loaders/fits.py index e15e6a3d..ee9964de 100644 --- a/src/vstarstack/library/loaders/fits.py +++ b/src/vstarstack/library/loaders/fits.py @@ -1,6 +1,6 @@ """Reading FITS files""" # -# Copyright (c) 2022 Vladislav Tsendrovskii +# Copyright (c) 2022-2024 Vladislav Tsendrovskii # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,7 +13,6 @@ # along with this program. If not, see . # -import numpy as np from astropy.io import fits import vstarstack.library.data @@ -44,21 +43,19 @@ def readfits(filename: str): params["UTC"] = tags["DATE-OBS"] if "EXPTIME" in plane.header: - exptime = plane.header["EXPTIME"] + params["exposure"] = float(plane.header["EXPTIME"]) else: - exptime = 1 + params["exposure"] = 1 if "GAIN" in plane.header: - gain = plane.header["GAIN"] + params["gain"] = float(plane.header["GAIN"]) else: - gain = 1 + params["gain"] = 1 slice_names = [] dataframe = vstarstack.library.data.DataFrame(params, tags) - weight_channel_name = "weight" - weight = np.ones((shape[1], shape[2]))*exptime*gain - dataframe.add_channel(weight, weight_channel_name, weight=True) + params["weight"] = params["exposure"]*params["gain"] if shape[0] == 1: if "FILTER" in plane.header: @@ -76,6 +73,5 @@ def readfits(filename: str): for i, slice_name in enumerate(slice_names): dataframe.add_channel(original[i, :, :], slice_name, brightness=True, signal=True) - dataframe.add_channel_link(slice_name, weight_channel_name, "weight") yield dataframe diff --git a/src/vstarstack/library/loaders/nef.py b/src/vstarstack/library/loaders/nef.py index 84ff0903..92d674e0 100644 --- a/src/vstarstack/library/loaders/nef.py +++ b/src/vstarstack/library/loaders/nef.py @@ -1,6 +1,6 @@ """Reading NEF image files""" # -# Copyright (c) 2023 Vladislav Tsendrovskii +# Copyright (c) 2023-2024 Vladislav Tsendrovskii # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -15,7 +15,6 @@ import rawpy import exifread -from exifread.classes import IfdTag import vstarstack.library.common import vstarstack.library.data @@ -41,13 +40,17 @@ def readnef(filename: str): if "EXIF ExposureTime" in tags: tag = tags["EXIF ExposureTime"] - exposure = float(tag.values[0]) + params["exposure"] = float(tag.values[0]) else: - exposure = 1 + params["exposure"] = 1 - iso = 1 + if "EXIF ISOSpeedRatings" in tags: + tag = tags["EXIF ISOSpeedRatings"] + params["gain"] = float(tag.values[0]) + else: + params["gain"] = 1 - exp = exposure * iso + params["weight"] = params["exposure"] * params["gain"] printable_tags = {} for tag_name in tags: @@ -56,8 +59,4 @@ def readnef(filename: str): dataframe = vstarstack.library.data.DataFrame(params, printable_tags) dataframe.add_channel(image, "raw", encoded=True, brightness=True, signal=True) dataframe.add_parameter(bayer, "format") - dataframe.add_parameter(exp, "weight") - dataframe.add_parameter(exposure, "exposure") - dataframe.add_parameter(iso, "gain") - yield dataframe diff --git a/src/vstarstack/library/loaders/ser.py b/src/vstarstack/library/loaders/ser.py index ddf7521c..685421d4 100644 --- a/src/vstarstack/library/loaders/ser.py +++ b/src/vstarstack/library/loaders/ser.py @@ -1,6 +1,6 @@ """Read SER images""" # -# Copyright (c) 2023 Vladislav Tsendrovskii +# Copyright (c) 2023-2024 Vladislav Tsendrovskii # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -192,6 +192,8 @@ def readser(fname: str): "w": width, "h": height, "format" : image_format, + "exposure" : 1, + "gain" : 1, "weight" : 1, } @@ -206,12 +208,7 @@ def readser(fname: str): frame = _read_to_npy(file, bpp, le16bit, shape) params["UTC"] = utc dataframe = vstarstack.library.data.DataFrame(params, tags) - exptime = 1 - weight = np.ones(frame.data.shape[0:2])*exptime index = 0 for index, channel in enumerate(channels): dataframe.add_channel(frame[:, :, index], channel, **opts) - if dataframe.get_channel_option(channel, "signal"): - dataframe.add_channel(weight, "weight-"+channel, weight=True) - dataframe.add_channel_link(channel, "weight-"+channel, "weight") yield dataframe diff --git a/src/vstarstack/library/loaders/tags.py b/src/vstarstack/library/loaders/tags.py deleted file mode 100644 index 31dc2cca..00000000 --- a/src/vstarstack/library/loaders/tags.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Read tags from EXIF""" -# -# Copyright (c) 2022 Vladislav Tsendrovskii -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, version 3 of the License. -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the GNU General Public License for more details. -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import exifread - -tags_names = { - "shutter": [("EXIF ExposureTime", 0)], - "iso": [("EXIF ISOSpeedRatings", 0), ("MakerNote ISOSetting", 1)], -} - -def read_tags(filename): - """Read EXIF tags from file""" - with open(filename, 'rb') as file: - tags = exifread.process_file(file) - - res = {} - for tag_name, tag in tags_names.items(): - for name, variant_id in tag: - if name in tags: - res[tag_name] = float(tags[name].values[variant_id]) - break - - - return res diff --git a/src/vstarstack/library/loaders/video.py b/src/vstarstack/library/loaders/video.py index 30491910..a0b9eae8 100644 --- a/src/vstarstack/library/loaders/video.py +++ b/src/vstarstack/library/loaders/video.py @@ -1,6 +1,6 @@ """Read video source file""" # -# Copyright (c) 2022 Vladislav Tsendrovskii +# Copyright (c) 2022-2024 Vladislav Tsendrovskii # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,7 +13,6 @@ # along with this program. If not, see . # -import numpy as np import cv2 import vstarstack.library.data @@ -29,25 +28,19 @@ def read_video(fname: str): if not success: break - tags = { - "depth": 8, - } + tags = {} params = { "w": frame.shape[1], "h": frame.shape[0], + "exposure" : 1, + "gain" : 1, + "weight" : 1, } - exptime = 1 - weight = np.ones((frame.shape[0], frame.shape[1]))*exptime - dataframe = vstarstack.library.data.DataFrame(params, tags) dataframe.add_channel(frame[:, :, 0], "R", brightness=True, signal=True) dataframe.add_channel(frame[:, :, 1], "G", brightness=True, signal=True) dataframe.add_channel(frame[:, :, 2], "B", brightness=True, signal=True) - dataframe.add_channel(weight, "weight", weight=True) - dataframe.add_channel_link("R", "weight", "weight") - dataframe.add_channel_link("G", "weight", "weight") - dataframe.add_channel_link("B", "weight", "weight") yield dataframe frame_id += 1 diff --git a/src/vstarstack/library/loaders/yuv.py b/src/vstarstack/library/loaders/yuv.py index 61e27516..53dc5cb5 100644 --- a/src/vstarstack/library/loaders/yuv.py +++ b/src/vstarstack/library/loaders/yuv.py @@ -1,6 +1,6 @@ """Read YUV video to npy frames""" # -# Copyright (c) 2022 Vladislav Tsendrovskii +# Copyright (c) 2022-2024 Vladislav Tsendrovskii # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -24,13 +24,15 @@ def readyuv(fname: str, width: int, height: int): shape = (int(height*2), width) with open(fname, "rb") as files: - tags = { - "depth": 8, - } + tags = {} params = { "w": width, "h": height, + "format" : "yuv_422", + "exposure" : 1, + "gain" : 1, + "weight" : 1, } frame_id = 0 @@ -44,10 +46,6 @@ def readyuv(fname: str, width: int, height: int): print(f"\tprocessing frame {frame_id}") dataframe = vstarstack.library.data.DataFrame(params, tags) - exptime = 1 - dataframe.add_channel(yuv, "raw", encoded=True, signal=True) - dataframe.add_parameter("yuv_422", "format") - dataframe.add_parameter(exptime, "weight") yield dataframe frame_id += 1 From 22976ca578037edd95d0d5c6d6a45dc4ad39637b Mon Sep 17 00:00:00 2001 From: Vladislav Tsendrovskii Date: Sat, 26 Oct 2024 15:31:58 +0200 Subject: [PATCH 02/10] Add darks library class --- src/vstarstack/library/loaders/fits.py | 5 +++- src/vstarstack/tool/calibration.py | 20 ++++++++++++++ src/vstarstack/tool/config.py | 1 + src/vstarstack/tool/darks_library.py | 36 ++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 src/vstarstack/tool/darks_library.py diff --git a/src/vstarstack/library/loaders/fits.py b/src/vstarstack/library/loaders/fits.py index ee9964de..459a93b1 100644 --- a/src/vstarstack/library/loaders/fits.py +++ b/src/vstarstack/library/loaders/fits.py @@ -52,10 +52,13 @@ def readfits(filename: str): else: params["gain"] = 1 + if "CCD-TEMP" in plane.header: + params["temperature"] = float(plane.header["CCD-TEMP"]) + slice_names = [] - dataframe = vstarstack.library.data.DataFrame(params, tags) params["weight"] = params["exposure"]*params["gain"] + dataframe = vstarstack.library.data.DataFrame(params, tags) if shape[0] == 1: if "FILTER" in plane.header: diff --git a/src/vstarstack/tool/calibration.py b/src/vstarstack/tool/calibration.py index 9eff23ad..f1bc470b 100644 --- a/src/vstarstack/tool/calibration.py +++ b/src/vstarstack/tool/calibration.py @@ -13,6 +13,7 @@ # import os import multiprocessing as mp +import json import vstarstack.tool.common import vstarstack.tool.cfg @@ -83,6 +84,22 @@ def _process_remove_dark(_project : vstarstack.tool.cfg.Project, else: _process_file_remove_dark(input_path, dark, output_path) +def _process_remove_dark_auto(project : vstarstack.tool.cfg.Project, + argv : list[str]): + input_path = argv[0] + darks_path = argv[1] + output_path = argv[2] + + with open(os.path.join(darks_path, "darks.json")) as f: + library = json.load(f) + + dark = vstarstack.library.data.DataFrame.load(dark_fname) + if os.path.isdir(input_path): + _process_dir_remove_dark(input_path, dark, output_path) + else: + _process_file_remove_dark(input_path, dark, output_path) + + # building darks def _process_build_dark(project : vstarstack.tool.cfg.Project, argv : list[str]): @@ -153,6 +170,9 @@ def _process_approximate_flat(project : vstarstack.tool.cfg.Project, "remove-dark": (_process_remove_dark, "Substract dark from image", "inputs/ dark.zip outputs/"), + "remove-dark-auto": (_process_remove_dark_auto, + "Substract dark from image with using darks library", + "inputs/ darks/ outputs/"), "build-dark" : (_process_build_dark, "Create dark image", "darks/ dark.zip"), diff --git a/src/vstarstack/tool/config.py b/src/vstarstack/tool/config.py index 11092288..594b49e7 100644 --- a/src/vstarstack/tool/config.py +++ b/src/vstarstack/tool/config.py @@ -39,6 +39,7 @@ "npy": (str, "npy/dark", {"directory" : True}), "result" : (str, "dark.zip"), }, + "darks_library" : (str, "darks", {"directory" : True}), "aligned": (str, "aligned", {"directory" : True}), "descs" : (str, "descs", {"directory" : True}), "relative_shifts": (str, "shifts.json"), diff --git a/src/vstarstack/tool/darks_library.py b/src/vstarstack/tool/darks_library.py new file mode 100644 index 00000000..e6766d03 --- /dev/null +++ b/src/vstarstack/tool/darks_library.py @@ -0,0 +1,36 @@ +# +# Copyright (c) 2024 Vladislav Tsendrovskii +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3 of the License. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +class DarksLibrary: + """Library for managing darks""" + def __init__(self, delta_temperature : float | None): + self.delta_temp = delta_temperature + self.darks = [] + + def append_dark(self, name : str, exposure : float | None, gain : float | None, temperature : float | None) -> None: + """Append dark to library""" + self.darks.append({"exposure":exposure, "gain":gain, "temperature":temperature, "name":name}) + + def find_darks(self, exposure : float | None, gain : float | None, temperature : float | None) -> list: + results = [] + for item in self.darks: + if exposure != item["exposure"]: + continue + if gain != item["gain"]: + continue + if temperature is not None and item["temperature"] is not None: + if abs(temperature - item["temperature"]) > self.delta_temp: + continue + results.append(item) + return results From ea544fcdb41ade2b2ff9816c067bbc5959fe767b Mon Sep 17 00:00:00 2001 From: Vladislav Tsendrovskii Date: Sat, 26 Oct 2024 15:35:16 +0200 Subject: [PATCH 03/10] darks library --- src/vstarstack/tool/calibration.py | 2 ++ src/vstarstack/tool/darks_library.py | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/vstarstack/tool/calibration.py b/src/vstarstack/tool/calibration.py index f1bc470b..e2d53583 100644 --- a/src/vstarstack/tool/calibration.py +++ b/src/vstarstack/tool/calibration.py @@ -23,6 +23,8 @@ import vstarstack.library.calibration.dark import vstarstack.library.calibration.flat +from vstarstack.tool.darks_library import DarksLibrary + # applying flats def _process_file_flatten(input_fname : str, flat : vstarstack.library.data.DataFrame, diff --git a/src/vstarstack/tool/darks_library.py b/src/vstarstack/tool/darks_library.py index e6766d03..0b50c7cb 100644 --- a/src/vstarstack/tool/darks_library.py +++ b/src/vstarstack/tool/darks_library.py @@ -12,9 +12,11 @@ # along with this program. If not, see . # +import json + class DarksLibrary: """Library for managing darks""" - def __init__(self, delta_temperature : float | None): + def __init__(self, delta_temperature : float): self.delta_temp = delta_temperature self.darks = [] @@ -22,7 +24,18 @@ def append_dark(self, name : str, exposure : float | None, gain : float | None, """Append dark to library""" self.darks.append({"exposure":exposure, "gain":gain, "temperature":temperature, "name":name}) + def store(self, fname): + """Save library to file""" + with open(fname, "w", encoding="utf8") as f: + json.dump(self.darks, f) + + def load(self, fname): + """Load library from file""" + with open(fname, encoding="utf8") as f: + self.darks = json.load(f) + def find_darks(self, exposure : float | None, gain : float | None, temperature : float | None) -> list: + """Find list of darks, which match parameters""" results = [] for item in self.darks: if exposure != item["exposure"]: From a32068195d3f138a0ec9e45cb6618d461f87d850 Mon Sep 17 00:00:00 2001 From: Vladislav Tsendrovskii Date: Sat, 26 Oct 2024 16:40:51 +0200 Subject: [PATCH 04/10] Use darks library --- src/vstarstack/library/calibration/dark.py | 10 +++- src/vstarstack/tool/calibration.py | 58 ++++++++++++++++++++-- src/vstarstack/tool/common.py | 3 +- src/vstarstack/tool/darks_library.py | 10 ++++ 4 files changed, 73 insertions(+), 8 deletions(-) diff --git a/src/vstarstack/library/calibration/dark.py b/src/vstarstack/library/calibration/dark.py index e8b44080..af66110d 100644 --- a/src/vstarstack/library/calibration/dark.py +++ b/src/vstarstack/library/calibration/dark.py @@ -34,8 +34,13 @@ def remove_dark(dataframe : vstarstack.library.data.DataFrame, for channel in dataframe.get_channels(): image, opts = dataframe.get_channel(channel) - if not opts["brightness"]: - print(f"Skipping {channel}") + if dataframe.get_channel_option(channel, "weight"): + continue + if not dataframe.get_channel_option(channel, "brightness"): + print(f"Skipping {channel}, not brightness") + continue + if dataframe.get_channel_option(channel, "dark-removed"): + print(f"Skipping {channel}, already dark removed") continue if channel in dark.get_channels(): @@ -44,6 +49,7 @@ def remove_dark(dataframe : vstarstack.library.data.DataFrame, dark_layer, _ = dark.get_channel(dark_channel_name) image = image - dark_layer + opts["dark-removed"] = True dataframe.replace_channel(image, channel, **opts) return dataframe diff --git a/src/vstarstack/tool/calibration.py b/src/vstarstack/tool/calibration.py index e2d53583..9fcea2c8 100644 --- a/src/vstarstack/tool/calibration.py +++ b/src/vstarstack/tool/calibration.py @@ -86,20 +86,68 @@ def _process_remove_dark(_project : vstarstack.tool.cfg.Project, else: _process_file_remove_dark(input_path, dark, output_path) + +# automatic detect corresponding dark +def _process_file_remove_dark_auto(input_fname : str, + lib : DarksLibrary, + dark_dfs : dict, + output_fname : str): + print(f"Processing {input_fname}") + dataframe = vstarstack.library.data.DataFrame.load(input_fname) + params = dataframe.params + exposure = params["exposure"] + gain = params["gain"] + if "temperature" in params: + temperature = params["temperature"] + else: + temperature = None + + darks = lib.find_darks(exposure, gain, temperature) + if len(darks) == 0: + print(f"Can not find corresponsing dark for {input_fname}, skipping") + return + dark_fname = darks[0]["name"] + result = vstarstack.library.calibration.dark.remove_dark(dataframe, dark_dfs[dark_fname]) + if result is None: + print(f"Can not remove dark from {input_fname}") + vstarstack.tool.common.check_dir_exists(output_fname) + result.store(output_fname) + +def _process_dir_remove_dark_auto(input_path : str, + lib : DarksLibrary, + dark_dfs : dict, + output_path : str): + files = vstarstack.tool.common.listfiles(input_path, ".zip") + with mp.Pool(vstarstack.tool.cfg.nthreads) as pool: + args = [(filename, lib, dark_dfs, os.path.join(output_path, name + ".zip")) for name, filename in files] + pool.starmap(_process_file_remove_dark_auto, args) + def _process_remove_dark_auto(project : vstarstack.tool.cfg.Project, argv : list[str]): input_path = argv[0] darks_path = argv[1] output_path = argv[2] - with open(os.path.join(darks_path, "darks.json")) as f: - library = json.load(f) + lib = DarksLibrary(delta_temperature=2) + darks = vstarstack.tool.common.listfiles(darks_path, ".zip") + dark_dfs = {} + for name, fname in darks: + print(f"Loading {name}") + df = vstarstack.library.data.DataFrame.load(fname) + params = df.params + exposure = params["exposure"] + gain = params["gain"] + if "temperature" in params: + temperature = params["temperature"] + else: + temperature = None + lib.append_dark(fname, exposure, gain, temperature) + dark_dfs[fname] = df - dark = vstarstack.library.data.DataFrame.load(dark_fname) if os.path.isdir(input_path): - _process_dir_remove_dark(input_path, dark, output_path) + _process_dir_remove_dark_auto(input_path, lib, dark_dfs, output_path) else: - _process_file_remove_dark(input_path, dark, output_path) + _process_file_remove_dark_auto(input_path, lib, dark_dfs, output_path) # building darks diff --git a/src/vstarstack/tool/common.py b/src/vstarstack/tool/common.py index cf072ada..352ac8eb 100644 --- a/src/vstarstack/tool/common.py +++ b/src/vstarstack/tool/common.py @@ -14,8 +14,9 @@ # import os +from typing import Tuple -def listfiles(path, ext=None, recursive=False): +def listfiles(path, ext=None, recursive=False) -> list[Tuple[str,str]]: images = [] for f in os.listdir(path): filename = os.path.abspath(os.path.join(path, f)) diff --git a/src/vstarstack/tool/darks_library.py b/src/vstarstack/tool/darks_library.py index 0b50c7cb..06a7c787 100644 --- a/src/vstarstack/tool/darks_library.py +++ b/src/vstarstack/tool/darks_library.py @@ -13,6 +13,7 @@ # import json +import math class DarksLibrary: """Library for managing darks""" @@ -34,6 +35,14 @@ def load(self, fname): with open(fname, encoding="utf8") as f: self.darks = json.load(f) + @staticmethod + def _delta_temperature(dark_temp : float, target_temp : float) -> float: + if target_temp is None: + return 0 + if dark_temp is None: + return math.inf + return abs(dark_temp - target_temp) + def find_darks(self, exposure : float | None, gain : float | None, temperature : float | None) -> list: """Find list of darks, which match parameters""" results = [] @@ -46,4 +55,5 @@ def find_darks(self, exposure : float | None, gain : float | None, temperature : if abs(temperature - item["temperature"]) > self.delta_temp: continue results.append(item) + results = sorted(results, key=lambda item : self._delta_temperature(item["temperature"], temperature)) return results From a2d4255ba8432ea31840586ea8ef08cfd3af3c96 Mon Sep 17 00:00:00 2001 From: Vladislav Tsendrovskii Date: Sat, 26 Oct 2024 17:20:04 +0200 Subject: [PATCH 05/10] fix --- src/vstarstack/library/loaders/classic.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vstarstack/library/loaders/classic.py b/src/vstarstack/library/loaders/classic.py index 9a8f31d9..9d560bdc 100644 --- a/src/vstarstack/library/loaders/classic.py +++ b/src/vstarstack/library/loaders/classic.py @@ -19,7 +19,6 @@ import vstarstack.library.common import vstarstack.library.data -import vstarstack.library.loaders.tags def readjpeg(fname: str): """Read single image (jpg, png, tiff) file""" From 60d29331eea841e10fb138cc68c6f5b4cd1439dc Mon Sep 17 00:00:00 2001 From: Vladislav Tsendrovskii Date: Sat, 26 Oct 2024 21:01:26 +0200 Subject: [PATCH 06/10] More debug msg --- src/vstarstack/tool/calibration.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vstarstack/tool/calibration.py b/src/vstarstack/tool/calibration.py index 9fcea2c8..49ec28ba 100644 --- a/src/vstarstack/tool/calibration.py +++ b/src/vstarstack/tool/calibration.py @@ -102,14 +102,20 @@ def _process_file_remove_dark_auto(input_fname : str, else: temperature = None + if temperature is not None: + print(f"\tParameters: exposure = {exposure}, gain = {gain}, temperature = {temperature}") + else: + print(f"\tParameters: exposure = {exposure}, gain = {gain}") + darks = lib.find_darks(exposure, gain, temperature) if len(darks) == 0: - print(f"Can not find corresponsing dark for {input_fname}, skipping") + print(f"\tCan not find corresponsing dark for {input_fname}, skipping") return dark_fname = darks[0]["name"] + print(f"\tUsing dark {dark_fname}") result = vstarstack.library.calibration.dark.remove_dark(dataframe, dark_dfs[dark_fname]) if result is None: - print(f"Can not remove dark from {input_fname}") + print(f"\tCan not remove dark from {input_fname}") vstarstack.tool.common.check_dir_exists(output_fname) result.store(output_fname) From bea53f88bf10b88f3e09538826746c857c0e096a Mon Sep 17 00:00:00 2001 From: Vladislav Tsendrovskii Date: Sat, 26 Oct 2024 21:30:01 +0200 Subject: [PATCH 07/10] fixes --- src/vstarstack/library/calibration/dark.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vstarstack/library/calibration/dark.py b/src/vstarstack/library/calibration/dark.py index af66110d..224282cf 100644 --- a/src/vstarstack/library/calibration/dark.py +++ b/src/vstarstack/library/calibration/dark.py @@ -40,7 +40,7 @@ def remove_dark(dataframe : vstarstack.library.data.DataFrame, print(f"Skipping {channel}, not brightness") continue if dataframe.get_channel_option(channel, "dark-removed"): - print(f"Skipping {channel}, already dark removed") + print(f"Skipping {channel}, dark already removed") continue if channel in dark.get_channels(): From c2b8f7e501407541d74fefdfda161d2d1234c3b6 Mon Sep 17 00:00:00 2001 From: Vladislav Tsendrovskii Date: Sat, 26 Oct 2024 23:50:18 +0200 Subject: [PATCH 08/10] use autosplit dark sources by temperature --- src/vstarstack/library/calibration/dark.py | 72 ++++++++++++++++++++-- src/vstarstack/tool/calibration.py | 15 ++++- src/vstarstack/tool/config.py | 5 +- 3 files changed, 85 insertions(+), 7 deletions(-) diff --git a/src/vstarstack/library/calibration/dark.py b/src/vstarstack/library/calibration/dark.py index 224282cf..b26c72c0 100644 --- a/src/vstarstack/library/calibration/dark.py +++ b/src/vstarstack/library/calibration/dark.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 Vladislav Tsendrovskii +# Copyright (c) 2022-2024 Vladislav Tsendrovskii # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -12,6 +12,8 @@ # along with this program. If not, see . # +import math +from typing import Generator import vstarstack.library.common import vstarstack.library.data import vstarstack.library.merge.simple_mean @@ -53,7 +55,69 @@ def remove_dark(dataframe : vstarstack.library.data.DataFrame, dataframe.replace_channel(image, channel, **opts) return dataframe -def prepare_darks(images : vstarstack.library.common.IImageSource - ) -> vstarstack.library.data.DataFrame: +class TemperatureIndex: + def __init__(self, delta_temperature : float, basic_temperature : float): + self.dt = delta_temperature + self.bt = basic_temperature + + def temperature_to_index(self, temperature : float) -> int | None: + if temperature is None or self.bt is None or self.dt is None: + return None + return math.floor((temperature - self.bt) / self.dt + 0.5) + + def index_to_temperature(self, index : int) -> float: + if index is None or self.bt is None or self.dt is None: + return None + return index * self.dt + self.bt + +class FilterSource(vstarstack.library.common.IImageSource): + """Filter sources with exposure/gain/temperature""" + def __init__(self, source : vstarstack.library.common.IImageSource, + exposure : float, + gain : float, + temperature : float | None, + temperature_indexer : TemperatureIndex): + self.temperature_indexer = temperature_indexer + self.source = source + self.exposure = exposure + self.gain = gain + self.temperature = temperature + self.temperature_idx = self.temperature_indexer.temperature_to_index(self.temperature) + + def items(self) -> Generator[vstarstack.library.data.DataFrame, None, None]: + for df in self.source.items(): + exposure = df.get_parameter("exposure") + gain = df.get_parameter("gain") + temperature = df.get_parameter("temperature") + temperature_idx = self.temperature_indexer.temperature_to_index(temperature) + if exposure == self.exposure and gain == self.gain and temperature_idx == self.temperature_idx: + yield df + + def empty(self) -> bool: + # TODO: better detect if there are matched df in source list + return self.source.empty() + +def prepare_darks(images : vstarstack.library.common.IImageSource, + basic_temperature : float | None, + delta_temperature : float | None) -> list: """Build dark frame""" - return vstarstack.library.merge.simple_mean.mean(images) + parameters = set() + indexer = TemperatureIndex(delta_temperature, basic_temperature) + for df in images.items(): + exposure = df.get_parameter("exposure") + gain = df.get_parameter("gain") + temperature = df.get_parameter("temperature") + index = indexer.temperature_to_index(temperature) + parameters.add((exposure, gain, index)) + + darks = [] + for exposure, gain, index in parameters: + image_source = FilterSource(images, exposure, gain, temperature, indexer) + dark = vstarstack.library.merge.simple_mean.mean(image_source) + dark.add_parameter(exposure, "exposure") + dark.add_parameter(gain, "gain") + temperature = indexer.index_to_temperature(index) + if temperature is not None: + dark.add_parameter(temperature, "temperature") + darks.append((exposure, gain, temperature, dark)) + return darks diff --git a/src/vstarstack/tool/calibration.py b/src/vstarstack/tool/calibration.py index 49ec28ba..15e8cd82 100644 --- a/src/vstarstack/tool/calibration.py +++ b/src/vstarstack/tool/calibration.py @@ -162,14 +162,25 @@ def _process_build_dark(project : vstarstack.tool.cfg.Project, if len(argv) >= 2: input_path = argv[0] dark_fname = argv[1] + basic_temp = float(argv[2]) + delta_temp = float(argv[3]) else: input_path = project.config.paths.dark.npy dark_fname = project.config.paths.dark.result + basic_temp = project.config.darks.basic_temperature + delta_temp = project.config.darks.delta_temperature files = vstarstack.tool.common.listfiles(input_path, ".zip") darks = [item[1] for item in files] src = vstarstack.library.common.FilesImageSource(darks) - dark = vstarstack.library.calibration.dark.prepare_darks(src) - dark.store(dark_fname) + darks = vstarstack.library.calibration.dark.prepare_darks(src, basic_temp, delta_temp) + for exposure, gain, temperature, dark in darks: + dirname = os.path.dirname(dark_fname) + name, ext = os.path.splitext(os.path.basename(dark_fname)) + if temperature < 0: + fname = os.path.join(dirname, f"{name}-{exposure}-{gain}-{abs(temperature)}{ext}") + else: + fname = os.path.join(dirname, f"{name}-{exposure}-{gain}+{temperature}{ext}") + dark.store(fname) # building flats def _process_build_flat_simple(project : vstarstack.tool.cfg.Project, diff --git a/src/vstarstack/tool/config.py b/src/vstarstack/tool/config.py index 594b49e7..c82bc3e6 100644 --- a/src/vstarstack/tool/config.py +++ b/src/vstarstack/tool/config.py @@ -39,7 +39,6 @@ "npy": (str, "npy/dark", {"directory" : True}), "result" : (str, "dark.zip"), }, - "darks_library" : (str, "darks", {"directory" : True}), "aligned": (str, "aligned", {"directory" : True}), "descs" : (str, "descs", {"directory" : True}), "relative_shifts": (str, "shifts.json"), @@ -61,6 +60,10 @@ "shift" : { "interpolate" : (bool, None), }, + "darks" : { + "basic_temperature" : (float, -10), + "delta_temperature" : (float, 2), + }, "merge" : { "sigma_clip_coefficient_begin" : (float, 4.0), "sigma_clip_coefficient_end" : (float, 2.0), From fc85a5d8f8d759a9eab7e58c6f611c6f2a1cc9f7 Mon Sep 17 00:00:00 2001 From: Vladislav Tsendrovskii Date: Sun, 27 Oct 2024 00:14:15 +0200 Subject: [PATCH 09/10] Start tests of dark --- tests/test_darks.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/test_darks.py diff --git a/tests/test_darks.py b/tests/test_darks.py new file mode 100644 index 00000000..511f931c --- /dev/null +++ b/tests/test_darks.py @@ -0,0 +1,26 @@ +# +# Copyright (c) 2024 Vladislav Tsendrovskii +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3 of the License. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +import numpy as np +import vstarstack.library.common +import vstarstack.library.calibration.dark +from vstarstack.library.data import DataFrame + +def test_prepare_dark_1(): + image = np.zeros((100,100)) + df = DataFrame({"exposure" : 1, "gain" : 1, "temperature" : 0}, {}) + df.add_channel(image, "L", brightness=True, signal=True) + src = vstarstack.library.common.ListImageSource([df]) + darks = vstarstack.library.calibration.dark.prepare_darks(src, 0, 1) + assert len(darks) == 1 From ea3d572aa2a306c31b134445339cb209e02b69a7 Mon Sep 17 00:00:00 2001 From: Vladislav Tsendrovskii Date: Sun, 27 Oct 2024 01:17:26 +0200 Subject: [PATCH 10/10] fix weight channel names --- src/vstarstack/library/movement/move_image.py | 2 +- src/vstarstack/tool/cfg.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vstarstack/library/movement/move_image.py b/src/vstarstack/library/movement/move_image.py index 589e6113..5446deda 100644 --- a/src/vstarstack/library/movement/move_image.py +++ b/src/vstarstack/library/movement/move_image.py @@ -127,11 +127,11 @@ def move_dataframe(dataframe: DataFrame, if not dataframe.get_channel_option(channel, "signal"): continue - weight_channel = None if channel in dataframe.links["weight"]: weight_channel = dataframe.links["weight"][channel] weight, _ = dataframe.get_channel(weight_channel) else: + weight_channel = f"weight-{channel}" if (w := dataframe.get_parameter("weight")) is not None: weight = np.ones(image.shape)*w else: diff --git a/src/vstarstack/tool/cfg.py b/src/vstarstack/tool/cfg.py index 053c436a..8c5b99a6 100644 --- a/src/vstarstack/tool/cfg.py +++ b/src/vstarstack/tool/cfg.py @@ -43,12 +43,12 @@ def get_param(name, type_of_var, default): DEBUG = False if "DEBUG" in os.environ: DEBUG = os.environ["DEBUG"].lower() == "true" - print("Debug = {DEBUG}") + print(f"Debug = {DEBUG}") SINGLETHREAD = False if "SINGLETHREAD" in os.environ: SINGLETHREAD = os.environ["SINGLETHREAD"].lower() == "true" - print("Singlethread = {SINGLETHREAD}") + print(f"Singlethread = {SINGLETHREAD}") if not SINGLETHREAD: nthreads = max(int(mp.cpu_count())-1, 1)