lunereclipse

9/8未明に皆既月食がありましたね。2022年の皆既月食はしっかり撮影しましたが、今回は時間が時間なので寝ている間にSeestar S30で撮影してみることにしました。

結果から言うと、皆既月食になっている時間は全く写っていなかったのですが、タイムラプス撮影で月が欠けていく様子は撮影できました。月が戻るところはバッテリー切れで少ししか写っていませんでしたが。
写っていない部分をカットして編集した動画が下記になります。これはこれで面白いですね。

月食の120倍タイムラプス動画

Seestar S30では、月を表示するようにして、AFをあわせ、追尾するようにして、タイムラプス撮影(1s)をしました。撮影開始したのは月食の1時間半前くらいからで、あとは寝て放置だったので、途中で露出を変えるとかはできませんでした。何か良い設定があるのでしょうか?撮影したオリジナル動画は下記です。

途中大きくぶれたりしていますが、追尾がうまく行っていない感じですかね?
動画編集ソフトの手ぶれ補正でもうまく解消できなかったのですが、Ollama+gpt-oss-20bで、下記プロンプトで作ってもらったプログラムで一発補正できました。さらに、一部カット、色調補正、4倍速、2倍拡大、BGM追加などをしたのが先の動画です。

プロンプト

動画の画像のブレを補正するプログラムをPythonで作成してください 
・フレームごとにブレがなくなるようにシフトします。大きく移動するフレームも補正します 
・回転や拡大縮小の補正は不要で、平行移動のみで良いです 
・動画はmp4形式です

LLMが生成した stabilize.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
動画の画像ブレ(平行移動)を補正するプログラム
- mp4 入力 → mp4 出力
- シフトのみ補正(回転・スケールは不要)
- 大きなシフトにも対応(phase‑correlation)
"""

import cv2
import numpy as np
import argparse
import os
from tqdm import tqdm

def parse_args():
    parser = argparse.ArgumentParser(
        description="動画のブレ(平行移動)を補正するプログラム")
    parser.add_argument("input", help="入力 MP4 ファイルパス")
    parser.add_argument("output", help="出力 MP4 ファイルパス")
    parser.add_argument("--fps", type=float, default=None,
                        help="出力 FPS。None なら入力と同じ")
    parser.add_argument("--scale", type=float, default=1.0,
                        help="補正前にフレームをリサイズする倍率。"
                             "大きいシフトを検出しやすくするために使う。"
                             "デフォルトは 1.0(リサイズなし)")
    parser.add_argument("--gpu", action='store_true',
                        help="GPU が利用可能なら GPU 版 (cv2.cuda) を使用")
    return parser.parse_args()

def estimate_shift(prev_gray, curr_gray, scale=1.0):
    """
    相関を用いて前フレームと現在フレームの平行移動量を推定する
    返り値: (dx, dy)  ただし dx は x 方向シフト量(右に移動すると +)
    """
    # 画像をスケールダウン(高速化&大シフト検出)
    if scale != 1.0:
        prev_gray = cv2.resize(prev_gray, (0,0), fx=scale, fy=scale,
                               interpolation=cv2.INTER_AREA)
        curr_gray = cv2.resize(curr_gray, (0,0), fx=scale, fy=scale,
                               interpolation=cv2.INTER_AREA)

    # 相関関数を計算
    # (shift, response) = cv2.phaseCorrelate(prev, curr)
    # ただし shift は (x, y) で float で返る
    shift, response = cv2.phaseCorrelate(prev_gray.astype(np.float32),
                                          curr_gray.astype(np.float32))
    # スケールを戻す
    dx, dy = shift[0] / scale, shift[1] / scale
    return dx, dy

def main():
    args = parse_args()

    # 動画を読み込み
    cap = cv2.VideoCapture(args.input)
    if not cap.isOpened():
        raise IOError(f"入力ファイルを開けません: {args.input}")

    # 入力ビデオの情報取得
    fps_in = cap.get(cv2.CAP_PROP_FPS)
    width  = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))

    # 出力 FPS
    fps_out = args.fps if args.fps is not None else fps_in

    # VideoWriter を作成
    fourcc = cv2.VideoWriter_fourcc(*'mp4v')   # mp4v はほぼすべての環境で動く
    out = cv2.VideoWriter(args.output, fourcc, fps_out,
                          (width, height))

    # 変数初期化
    prev_frame = None
    cumulative_dx = 0.0
    cumulative_dy = 0.0

    print(f"処理開始: {args.input} → {args.output}")
    print(f"入力: {width}x{height} @ {fps_in}fps, {total_frames} フレーム")

    with tqdm(total=total_frames, unit="フレーム") as pbar:
        for _ in range(total_frames):
            ret, frame = cap.read()
            if not ret:
                break

            # 1st フレームはそのまま書き出す
            if prev_frame is None:
                out.write(frame)
                prev_frame = frame
                pbar.update(1)
                continue

            # グレースケールに変換
            prev_gray = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)
            curr_gray = cv2.cvtColor(frame,     cv2.COLOR_BGR2GRAY)

            # シフト量を推定
            dx, dy = estimate_shift(prev_gray, curr_gray, scale=args.scale)

            # 累積シフト量に追加
            cumulative_dx += dx
            cumulative_dy += dy

            # 変換行列(2x3)を作成
            M = np.array([[1, 0, -cumulative_dx],
                          [0, 1, -cumulative_dy]], dtype=np.float32)

            # 補正
            stabilized = cv2.warpAffine(frame, M, (width, height),
                                        flags=cv2.INTER_LINEAR,
                                        borderMode=cv2.BORDER_REPLICATE)

            out.write(stabilized)
            prev_frame = frame
            pbar.update(1)

    cap.release()
    out.release()
    print("完了!")

if __name__ == "__main__":
    main()

使い方

pip install opencv-python numpy tqdm

# 例: input.mp4 を補正し、output.mp4 へ保存
python stabilize.py input.mp4 output.mp4
\楽天ポイント4倍セール!/
楽天市場
\ポイント5%還元!/
Yahooショッピング