diff --git a/src/vstarstack/library/calibration/dark.py b/src/vstarstack/library/calibration/dark.py
index e8b44080..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
@@ -34,8 +36,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}, dark already removed")
continue
if channel in dark.get_channels():
@@ -44,10 +51,73 @@ 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
-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/library/loaders/classic.py b/src/vstarstack/library/loaders/classic.py
index 0e8f0a96..9d560bdc 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,10 +15,10 @@
import numpy as np
from PIL import Image
+import exifread
import vstarstack.library.common
import vstarstack.library.data
-import vstarstack.library.loaders.tags
def readjpeg(fname: str):
"""Read single image (jpg, png, tiff) file"""
@@ -26,32 +26,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..459a93b1 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,22 @@ 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
+
+ if "CCD-TEMP" in plane.header:
+ params["temperature"] = float(plane.header["CCD-TEMP"])
slice_names = []
+ params["weight"] = params["exposure"]*params["gain"]
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)
if shape[0] == 1:
if "FILTER" in plane.header:
@@ -76,6 +76,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
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/calibration.py b/src/vstarstack/tool/calibration.py
index 9eff23ad..15e8cd82 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
@@ -22,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,
@@ -83,20 +86,101 @@ 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
+
+ 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"\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"\tCan 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]
+
+ 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
+
+ if os.path.isdir(input_path):
+ _process_dir_remove_dark_auto(input_path, lib, dark_dfs, output_path)
+ else:
+ _process_file_remove_dark_auto(input_path, lib, dark_dfs, output_path)
+
+
# building darks
def _process_build_dark(project : vstarstack.tool.cfg.Project,
argv : list[str]):
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,
@@ -153,6 +237,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/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)
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/config.py b/src/vstarstack/tool/config.py
index 11092288..c82bc3e6 100644
--- a/src/vstarstack/tool/config.py
+++ b/src/vstarstack/tool/config.py
@@ -60,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),
diff --git a/src/vstarstack/tool/darks_library.py b/src/vstarstack/tool/darks_library.py
new file mode 100644
index 00000000..06a7c787
--- /dev/null
+++ b/src/vstarstack/tool/darks_library.py
@@ -0,0 +1,59 @@
+#
+# 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 json
+import math
+
+class DarksLibrary:
+ """Library for managing darks"""
+ def __init__(self, delta_temperature : float):
+ 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 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)
+
+ @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 = []
+ 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)
+ results = sorted(results, key=lambda item : self._delta_temperature(item["temperature"], temperature))
+ return results
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