diff --git a/src/vstarstack/library/calibration/flat.py b/src/vstarstack/library/calibration/flat.py
index a75cbab4..41e8ed89 100644
--- a/src/vstarstack/library/calibration/flat.py
+++ b/src/vstarstack/library/calibration/flat.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,8 +12,11 @@
# along with this program. If not, see .
#
+import math
+from typing import Tuple
import cv2
import numpy as np
+import scipy.signal
import vstarstack.library.common
import vstarstack.library.data
@@ -94,3 +97,109 @@ def prepare_flat_sky(images : vstarstack.library.common.IImageSource,
continue
flat.remove_channel(channel)
return flat
+
+
+def get_quarters(image : np.ndarray, x : int, y : int) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
+ """Get quarters of the image"""
+ w = image.shape[1]
+ h = image.shape[0]
+ left_top = image[0:y,0:x]
+ left_bottom = image[y:h,0:x]
+ right_top = image[0:y,x:w]
+ right_bottom = image[y:h,x:w]
+ left_top = left_top[::-1,::-1]
+ left_bottom = left_bottom[:,::-1]
+ right_top = right_top[::-1,:]
+ minw = min(left_top.shape[1], left_bottom.shape[1], right_top.shape[1], right_bottom.shape[1])
+ minh = min(left_top.shape[0], left_bottom.shape[0], right_top.shape[0], right_bottom.shape[0])
+ left_top = left_top[0:minh,0:minw]
+ left_bottom = left_bottom[0:minh,0:minw]
+ right_top = right_top[0:minh,0:minw]
+ right_bottom = right_bottom[0:minh,0:minw]
+ return left_top, left_bottom, right_top, right_bottom
+
+def correlate_images(im1 : np.ndarray, im2 : np.ndarray) -> float:
+ """Find correlation coefficient between 2 images"""
+ im1 = im1 - np.mean(im1)
+ im2 = im2 - np.mean(im2)
+ top = np.sum(im1*im2)
+ bottom1 = np.sum(im1**2)
+ bottom2 = np.sum(im2**2)
+ return top / math.sqrt(bottom1 * bottom2)
+
+def approximate_flat(image : np.ndarray) -> Tuple[int, int, float, float, float]:
+ """Find smooth polynomial approximation of flat"""
+ # We need to find center of vignetting and it's parameters
+ # We fill find them with gradient descend
+ w = image.shape[1]
+ h = image.shape[0]
+
+ x0, y0 = int(w/2), int(h/2)
+ corrmax = -1
+
+ centers = [(int(h/2), int(w/2))]
+ for y in range(int(h/3),int(2*h/3)+1):
+ for x in range(int(w/3),int(2*w/3)+1):
+ centers.append((y,x))
+
+ image = np.clip(image, 0, None)
+ for y,x in centers:
+ p1,p2,p3,p4 = get_quarters(image, x, y)
+ c12 = correlate_images(p1, p2)
+ c13 = correlate_images(p1, p3)
+ c14 = correlate_images(p1, p4)
+ c23 = correlate_images(p2, p3)
+ c24 = correlate_images(p2, p4)
+ c34 = correlate_images(p3, p3)
+ corr = sum([c12, c13, c14, c23, c24, c34])/6
+ if corr > corrmax:
+ corrmax = corr
+ x0,y0 = x,y
+
+ p1,p2,p3,p4 = get_quarters(image, x0, y0)
+ ww = p1.shape[1]
+ hh = p1.shape[0]
+
+ sw = int(ww/16)
+ sh = int(hh/16)
+
+ val0 = np.average([np.average(item[:sh,:sw]) for item in [p1,p2,p3,p4]])
+ val_x = np.average([np.average(item[:sh,ww-sw:ww]) for item in [p1,p2,p3,p4]])/val0
+ val_y = np.average([np.average(item[hh-sh:hh,:sw]) for item in [p1,p2,p3,p4]])/val0
+
+ k_x = (1-val_x)/ww**2
+ k_y = (1-val_y)/hh**2
+ return x0, y0, val0, k_x, k_y
+
+def generate_flat(w : int, h : int, x0 : int, y0 : int, val0 : float, k_x : float, k_y : float) -> np.ndarray:
+ """Generate flat from parameters"""
+ x, y = np.meshgrid(np.arange(0,w),np.arange(0,h))
+ approx = val0*(1-k_x*(x-x0)**2-k_y*(y-y0)**2)
+ return approx
+
+def detect_spots(image : np.ndarray, approximated : np.ndarray) -> np.ndarray:
+ """Detect spots on original flat and append them to approximated flat"""
+ return image
+
+def approximate_flat_image(flat : vstarstack.library.data.DataFrame) -> vstarstack.library.data.DataFrame:
+ """Approximate flat"""
+ for channel in flat.get_channels():
+ if not flat.get_channel_option(channel, "signal"):
+ continue
+ layer,opts = flat.get_channel(channel)
+ layer = layer.astype(np.float64)
+ h,w = layer.shape
+ minv = min(w,h)
+ k = 128/minv
+ h = int(h*k)
+ w = int(w*k)
+ layer_small = cv2.resize(layer, (w,h), interpolation=cv2.INTER_LINEAR)
+ x0, y0, val0, kx, ky = approximate_flat(layer_small)
+ x0 = x0 / k
+ y0 = y0 / k
+ kx = kx * k**2
+ ky = ky * k**2
+ layer_approximated = generate_flat(layer.shape[1], layer.shape[0], x0, y0, val0, kx, ky)
+ layer_approximated = detect_spots(layer, layer_approximated)
+ flat.replace_channel(layer_approximated, channel, **opts)
+ return flat
diff --git a/src/vstarstack/tool/calibration.py b/src/vstarstack/tool/calibration.py
index 52875bb1..e44af890 100644
--- a/src/vstarstack/tool/calibration.py
+++ b/src/vstarstack/tool/calibration.py
@@ -116,6 +116,14 @@ def _process_build_flat_sky(_project : vstarstack.tool.cfg.Project,
vstarstack.tool.common.check_dir_exists(flat_fname)
flat.store(flat_fname)
+def _process_approximate_flat(_project : vstarstack.tool.cfg.Project,
+ argv : list[str]):
+ input_fname = argv[0]
+ output_fname = argv[1]
+ df = vstarstack.library.data.DataFrame.load(input_fname)
+ df = vstarstack.library.calibration.flat.approximate_flat_image(df)
+ df.store(output_fname)
+
commands = {
"flatten": (_process_flatten,
"Flatten image",
@@ -131,5 +139,8 @@ def _process_build_flat_sky(_project : vstarstack.tool.cfg.Project,
"flats/ flat.zip"),
"build-flat-sky" : (_process_build_flat_sky,
"Create flat image - use sky images",
- "flats/ flat.zip")
+ "flats/ flat.zip"),
+ "approximate-flat" : (_process_approximate_flat,
+ "Approximate flat with polynomic function",
+ "original_flat.zip result_flat.zip")
}
diff --git a/tests/flat/flat.jpg b/tests/flat/flat.jpg
new file mode 100644
index 00000000..f51fe396
Binary files /dev/null and b/tests/flat/flat.jpg differ
diff --git a/tests/test_flat_approximation.py b/tests/test_flat_approximation.py
new file mode 100644
index 00000000..65cdd298
--- /dev/null
+++ b/tests/test_flat_approximation.py
@@ -0,0 +1,31 @@
+#
+# 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 vstarstack.library.calibration.flat
+import vstarstack.library.loaders.classic
+import os
+import numpy as np
+
+dir_path = os.path.dirname(os.path.realpath(__file__))
+
+def test_flat():
+ dfg = vstarstack.library.loaders.classic.readjpeg(os.path.join(dir_path, 'flat/flat.jpg'))
+ df = next(dfg)
+ flat,_ = df.get_channel('L')
+ approx = vstarstack.library.calibration.flat.approximate_flat_image(df.copy())
+ flat_approx,_ = approx.get_channel('L')
+ assert flat_approx.shape == flat.shape
+ rel = flat_approx / flat
+ assert np.average(rel) < 1.05
+ assert np.average(rel) > 0.95
+ assert np.std(rel) < 0.05