diff --git a/src/vstarstack/library/movement/basic_movement.py b/src/vstarstack/library/movement/basic_movement.py
index ce9d422..820d9f5 100644
--- a/src/vstarstack/library/movement/basic_movement.py
+++ b/src/vstarstack/library/movement/basic_movement.py
@@ -13,6 +13,7 @@
# along with this program. If not, see .
#
+from typing import List
import numpy as np
from abc import ABC, abstractmethod
@@ -60,3 +61,13 @@ def inverse(self):
@abstractmethod
def __mul__(self, other):
"""Multiply movements"""
+
+ @staticmethod
+ @abstractmethod
+ def average(transformations, percent=100):
+ """Average of multiple movements"""
+
+ @staticmethod
+ @abstractmethod
+ def interpolate(transformations : list, coefficients : List[float]):
+ """Interpolate of multiple movements"""
diff --git a/src/vstarstack/library/movement/flat.py b/src/vstarstack/library/movement/flat.py
index 87d69dd..6d99a2f 100644
--- a/src/vstarstack/library/movement/flat.py
+++ b/src/vstarstack/library/movement/flat.py
@@ -108,6 +108,24 @@ def average(transformations : list):
transformation = Movement(angle, dy, dx)
return transformation
+ @staticmethod
+ def interpolate(transformations : list, coefficients : List[float]):
+ """Interpolate of multiple movements"""
+ s = sum(coefficients)
+ coefficients = [item / s for item in coefficients]
+ angles = []
+ dxs = []
+ dys = []
+ for i, transformation in enumerate(transformations):
+ angles.append(transformation.a * coefficients[i])
+ dxs.append(transformation.dx * coefficients[i])
+ dys.append(transformation.dy * coefficients[i])
+ angle = np.sum(angles)
+ dy = np.sum(dys)
+ dx = np.sum(dxs)
+ transformation = Movement(angle, dy, dx)
+ return transformation
+
def __mul__(self, other):
"""Multiply movements"""
angle1 = self.a
diff --git a/src/vstarstack/library/movement/sphere.py b/src/vstarstack/library/movement/sphere.py
index 6144dfd..f14e207 100644
--- a/src/vstarstack/library/movement/sphere.py
+++ b/src/vstarstack/library/movement/sphere.py
@@ -16,7 +16,7 @@
import logging
import math
import json
-from typing import Any
+from typing import Any, List
import numpy as np
from scipy.spatial.transform import Rotation
@@ -174,6 +174,17 @@ def build(point1_from, point2_from, point1_to, point2_to, debug=False):
return Movement(rot)
+ @staticmethod
+ def build_by_single(point_from, point_to):
+ """Build movement by single pair of stars"""
+ v_from = p2vec(point_from)
+ v_to = p2vec(point_to)
+ angle = vecangle(v_from, v_to)
+ axis = vecmul(v_from, v_to)
+ axis = axis / np.linalg.norm(axis)
+ rot = Rotation.from_rotvec(angle * axis)
+ return Movement(rot)
+
@staticmethod
def average(transformations, percent=100):
"""Average of multiple movements"""
@@ -206,6 +217,20 @@ def average(transformations, percent=100):
transformation = Movement(rot)
return transformation
+ @staticmethod
+ def interpolate(transformations : list, coefficients : List[float]):
+ """Interpolate of multiple movements"""
+ s = sum(coefficients)
+ coefficients = [item / s for item in coefficients]
+ axises = np.zeros((len(transformations), 3))
+ for i, transformation in enumerate(transformations):
+ rotvec = transformation.rot.as_rotvec()
+ axises[i, 0:3] = rotvec * coefficients[i]
+ rotvec = np.sum(axises, axis=0)
+ rot = Rotation.from_rotvec(rotvec)
+ transformation = Movement(rot)
+ return transformation
+
def __mul__(self, other):
"""Multiply movements"""
rot1 = self.rot
diff --git a/src/vstarstack/tool/moving_object_shift.py b/src/vstarstack/tool/moving_object_shift.py
new file mode 100644
index 0000000..409de2e
--- /dev/null
+++ b/src/vstarstack/tool/moving_object_shift.py
@@ -0,0 +1,89 @@
+#
+# Copyright (c) 2025 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 datetime
+import logging
+
+import vstarstack.tool.cfg
+import vstarstack.tool.common
+import vstarstack.library.data
+import vstarstack.library.projection.tools
+import vstarstack.library.movement.sphere
+
+logger = logging.getLogger(__name__)
+
+def linear(project : vstarstack.tool.cfg.Project, argv: list[str]):
+ # path with image files
+ path = argv[0]
+
+ # object position on first file - with lowest UTC
+ x1 = int(argv[1])
+ y1 = int(argv[2])
+
+ # object position of last file - with highest UTC
+ x2 = int(argv[3])
+ y2 = int(argv[4])
+
+ # output shift file
+ shift_file = argv[5]
+
+ # we align all files so object position is the same on all images
+ # we use UTC parameter for determine interpolation position
+ utcs = {}
+ projections = {}
+ lowest_utc = None
+ highest_utc = None
+ lowest_name = None
+ highest_name = None
+ imgs = vstarstack.tool.common.listfiles(path, ".zip")
+ for name, fname in imgs:
+ logging.info(f"Processing {name}")
+ df = vstarstack.library.data.DataFrame.load(fname)
+ utc = df.get_parameter("UTC")
+ if utc is None:
+ logging.warning(f"File {name} doesn't have UTC")
+ continue
+ try:
+ logging.info(f"UTC {utc}")
+ utc = datetime.datetime.fromisoformat(utc)
+ except:
+ logging.error(f"File {name} has incorrect UTC {utc}")
+ continue
+ utcs[name] = utc
+ projections[name] = vstarstack.library.projection.tools.get_projection(df)
+ if lowest_utc is None or utc < lowest_utc:
+ lowest_utc = utc
+ lowest_name = name
+ if highest_utc is None or utc > highest_utc:
+ highest_utc = utc
+ highest_name = name
+
+ first_lonlat = projections[lowest_name].project(x1, y1)
+ last_lonlat = projections[highest_name].project(x2, y2)
+ movement = vstarstack.library.movement.sphere.Movement.build_by_single(last_lonlat, first_lonlat)
+ identity = vstarstack.library.movement.sphere.Movement.identity()
+
+ delta = highest_utc - lowest_utc
+ shifts = {}
+ for name in utcs:
+ interpolation = (utcs[name] - lowest_utc) / delta
+ move = vstarstack.library.movement.sphere.Movement.interpolate([movement, identity], [interpolation, 1-interpolation])
+ shifts[name] = move.serialize()
+ with open(shift_file, "w", encoding='utf8') as f:
+ json.dump(shifts, f, ensure_ascii=False, indent=4)
+
+commands = {
+ "linear-interpolation": (linear, "Interpolate moving object position linear between first and last images", "path/ X1 Y1 X2 Y2 shift-linear.json"),
+}
diff --git a/src/vstarstack/tool/process.py b/src/vstarstack/tool/process.py
index 5f270a9..39f055e 100644
--- a/src/vstarstack/tool/process.py
+++ b/src/vstarstack/tool/process.py
@@ -53,6 +53,8 @@
"fine shift images"),
"photometry": ("vstarstack.tool.photometry.photometry",
"analyze images"),
+ "moving-object-align": ("vstarstack.tool.moving_object_shift",
+ "align moving objects, like comets"),
"pipeline": ("vstarstack.tool.generators.generators",
"generate pipelines for processing"),
}
diff --git a/src/vstarstack/tool/shift.py b/src/vstarstack/tool/shift.py
index 7372bea..8bb3cf9 100644
--- a/src/vstarstack/tool/shift.py
+++ b/src/vstarstack/tool/shift.py
@@ -254,10 +254,10 @@ def apply_shift_extended(project: vstarstack.tool.cfg.Project, argv: list[str]):
"shifts.json shift.json"),
"apply-shift": (apply_shift,
"Apply selected shifts",
- "shift.json npy/ shifted/"),
+ "npy/ shift.json shifted/"),
"apply-extended-shift": (apply_shift_extended,
"Apply selected shifts and save to output with extended size (only perspective projection!)",
- "shift.json npy/ shifted/"),
+ "npy/ shift.json shifted/"),
}
def run(project: vstarstack.tool.cfg.Project, argv: list[str]):