초기 커밋: DefVideo 소스 등록

abcVideo 플레이어 소스 (client / server / shared / pythonsource / docs / .claude).
.gitignore 적용으로 node_modules·storage·samplevideo·미디어 등 대용량 일괄 제외.
103 files, ~964K.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 03:20:27 +00:00
commit 82662d417d
103 changed files with 17213 additions and 0 deletions

View File

@@ -0,0 +1,560 @@
import sys
import os
import re
import cv2
import numpy as np
import pandas as pd
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QFileDialog, QTabWidget,
QSlider, QDoubleSpinBox, QProgressBar, QComboBox, QGroupBox, QFormLayout,
QSpinBox)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
from PyQt5.QtGui import QImage, QPixmap
from scipy.spatial.transform import Rotation as R
from pyproj import Transformer
# =================================================================
# [NEXT GEN] advanced_tuner_v2.py: Streamlined GIS GUI Tuner
# 설계 원칙:
# 1. MP4 + SRT + CSV (WGS84) 워크플로우 통합
# 2. 실시간 좌표 변환 (Lat/Lon -> EPSG:5186)
# 3. 사용자 친화적 튜닝 인터페이스 제공
# =================================================================
class SRTParser:
@staticmethod
def parse(srt_path):
data_dict = {}
if not os.path.exists(srt_path): return data_dict
try:
with open(srt_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
if "latitude" not in content.lower():
with open(srt_path, 'r', encoding='cp949', errors='ignore') as f:
content = f.read()
blocks = content.split('\n\n')
for block in blocks:
f_match = re.search(r'FrameCnt:\s*(\d+)', block)
if not f_match: continue
idx = int(f_match.group(1))
lat = float(re.search(r'latitude:\s*([\d\.-]+)', block).group(1))
lon = float(re.search(r'longitude:\s*([\d\.-]+)', block).group(1))
alt = float(re.search(r'abs_alt:\s*([\d\.-]+)', block).group(1))
yaw = float(re.search(r'gb_yaw:\s*([\d\.-]+)', block).group(1))
pitch = float(re.search(r'gb_pitch:\s*([\d\.-]+)', block).group(1))
roll = float(re.search(r'gb_roll:\s*([\d\.-]+)', block).group(1))
focal = float(re.search(r'focal_len:\s*([\d\.-]+)', block).group(1))
data_dict[idx] = {
'lat': lat, 'lon': lon, 'alt': alt,
'yaw': yaw, 'pitch': pitch, 'roll': roll,
'focal': focal
}
# Post-process: Filling gaps and Smoothing
if not data_dict: return {}
indices = sorted(data_dict.keys())
min_idx, max_idx = indices[0], indices[-1]
# 1. Interpolate Gaps
all_indices = np.arange(min_idx, max_idx + 1)
new_data = {}
fields = ['lat', 'lon', 'alt', 'yaw', 'pitch', 'roll', 'focal']
temp_arrays = {f: [] for f in fields}
for idx in indices:
for f in fields:
temp_arrays[f].append(data_dict[idx][f])
interp_arrays = {}
for f in fields:
# Handle Yaw wrapping for smoother rotation (Optional but safer)
if f == 'yaw':
y_arr = np.array(temp_arrays[f])
# Ensure continuous angles
y_arr = np.unwrap(np.radians(y_arr))
interp_y = np.interp(all_indices, indices, y_arr)
# 2. Smooth (Moving Average)
smooth_y = np.convolve(interp_y, np.ones(5)/5, mode='same')
interp_arrays[f] = np.degrees(smooth_y)
else:
interp_f = np.interp(all_indices, indices, temp_arrays[f])
# 2. Smooth (Moving Average)
interp_arrays[f] = np.convolve(interp_f, np.ones(5)/5, mode='same')
for i, idx in enumerate(all_indices):
new_data[int(idx)] = {f: interp_arrays[f][i] for f in fields}
return new_data
except Exception as e:
print(f"SRT Error: {e}")
return {}
class RenderWorker(QThread):
progress = pyqtSignal(int)
finished = pyqtSignal(str)
def __init__(self, params):
super().__init__()
self.p = params
self.is_running = True
def stop(self):
self.is_running = False
def run(self):
try:
cap = cv2.VideoCapture(self.p['video_path'])
original_fps = cap.get(cv2.CAP_PROP_FPS)
cap.set(cv2.CAP_PROP_POS_FRAMES, self.p['range'][0])
target_w, target_h = self.p['resolution']
target_fps = self.p['fps'] if self.p['fps'] > 0 else original_fps
start_f, end_f = self.p['range']
total_work = end_f - start_f + 1
fourcc = cv2.VideoWriter_fourcc(*self.p['codec'])
out = cv2.VideoWriter(self.p['output_path'], fourcc, target_fps, (target_w, target_h))
transformer = Transformer.from_crs("epsg:4326", "epsg:5186")
# Pre-swap line points if needed
eff_line_pts = self.p['line_pts']
if self.p['swap_xy']:
eff_line_pts = eff_line_pts.copy()
eff_line_pts[:, [0, 1]] = eff_line_pts[:, [1, 0]]
for f_idx in range(start_f, end_f + 1):
if not self.is_running: break
ret, frame = cap.read()
if not ret: break
# Check for Sync
if f_idx in self.p['srt_data']:
meta = self.p['srt_data'][f_idx]
dx, dy = transformer.transform(meta['lat'], meta['lon'])
if self.p['swap_xy']:
dx, dy = dy, dx
# Resize frame if needed
original_h, original_w = frame.shape[:2]
if (target_w, target_h) != (original_w, original_h):
draw_frame = cv2.resize(frame, (target_w, target_h))
else:
draw_frame = frame.copy()
drone_pos = np.array([dx + self.p['off_x'], dy + self.p['off_y'], meta['alt'] + self.p['off_z']])
rvec, tvec, K = self.get_proj(meta, drone_pos, target_w, target_h)
R_w2c, _ = cv2.Rodrigues(rvec)
pts_cam = (eff_line_pts @ R_w2c.T) + tvec.T
for i in range(0, len(pts_cam), 2):
p1c, p2c = pts_cam[i], pts_cam[i+1]
if p1c[2] < 0.1 and p2c[2] < 0.1: continue
if p1c[2] < 0.1 or p2c[2] < 0.1:
t = (0.1 - p1c[2]) / (p2c[2] - p1c[2])
p_mid = p1c + t * (p2c - p1c)
if p1c[2] < 0.1: p1c = p_mid
else: p2c = p_mid
u1 = int(K[0, 0] * (p1c[0] / p1c[2]) + K[0, 2])
v1 = int(K[1, 1] * (p1c[1] / p1c[2]) + K[1, 2])
u2 = int(K[0, 0] * (p2c[0] / p2c[2]) + K[0, 2])
v2 = int(K[1, 1] * (p2c[1] / p2c[2]) + K[1, 2])
ret_cli, pt1, pt2 = cv2.clipLine((0, 0, target_w, target_h), (u1, v1), (u2, v2))
if ret_cli:
cv2.line(draw_frame, pt1, pt2, (0, 0, 255), 10)
out.write(draw_frame)
else:
if (target_w, target_h) != (frame.shape[1], frame.shape[0]):
out.write(cv2.resize(frame, (target_w, target_h)))
else:
out.write(frame)
self.progress.emit(int(((f_idx - start_f + 1) / total_work) * 100))
cap.release()
out.release()
self.finished.emit(self.p['output_path'])
except Exception as e:
self.finished.emit(f"Error: {e}")
def get_proj(self, meta, drone_pos, w, h):
yaw = np.radians(meta['yaw'] + self.p['off_yaw'])
pitch = np.radians(meta['pitch'] + self.p['off_pitch'])
roll = np.radians(meta['roll'] + self.p['off_roll'])
R_b2w = (R.from_euler('z', -yaw) * R.from_euler('x', pitch) * R.from_euler('y', roll)).as_matrix()
R_w2c = np.array([[1, 0, 0], [0, 0, -1], [0, 1, 0]]) @ R_b2w.T
rvec, _ = cv2.Rodrigues(R_w2c)
tvec = -R_w2c @ drone_pos
f_px = (self.p['focal'] / self.p['sensor_w']) * w
K = np.array([[f_px, 0, w/2], [0, f_px, h/2], [0, 0, 1]])
return rvec, tvec, K
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Advanced GIS Tuner v2 (Streamlined)")
self.resize(1280, 850)
self.srt_data = {}
self.line_points = np.array([])
self.transformer = Transformer.from_crs("epsg:4326", "epsg:5186")
self.init_ui()
def init_ui(self):
tabs = QTabWidget()
self.setCentralWidget(tabs)
# --- Tab 1: Data Input ---
tab1 = QWidget()
layout1 = QVBoxLayout()
form1 = QFormLayout()
self.btn_video = QPushButton("Find MP4")
self.txt_video = QLineEdit("하행)회덕-대전조차장.MP4")
self.btn_video.clicked.connect(lambda: self.find_file(self.txt_video, "Video (*.mp4)"))
form1.addRow(self.btn_video, self.txt_video)
self.btn_srt = QPushButton("Find SRT")
self.txt_srt = QLineEdit("하행)회덕-대전조차장.srt")
self.btn_srt.clicked.connect(lambda: self.find_file(self.txt_srt, "SRT (*.srt)"))
form1.addRow(self.btn_srt, self.txt_srt)
self.btn_csv = QPushButton("Find CSV (Lat/Lon/H)")
self.txt_csv = QLineEdit("center.csv")
self.btn_csv.clicked.connect(lambda: self.find_file(self.txt_csv, "CSV (*.csv)"))
form1.addRow(self.btn_csv, self.txt_csv)
layout1.addLayout(form1)
self.btn_load = QPushButton("LOAD ALL DATA")
self.btn_load.setStyleSheet("background-color: #4CAF50; color: white; height: 50px; font-weight: bold;")
self.btn_load.clicked.connect(self.load_all_data)
layout1.addWidget(self.btn_load)
layout1.addStretch()
tab1.setLayout(layout1)
tabs.addTab(tab1, "1. 자료입력")
# --- Tab 2: Tuner ---
tab2 = QWidget()
hbox2 = QHBoxLayout()
# Left: Preview
vbox_prev = QVBoxLayout()
self.lbl_preview = QLabel("Video Preview (Load Data First)")
self.lbl_preview.setAlignment(Qt.AlignCenter)
self.lbl_preview.setStyleSheet("border: 2px solid gray; background-color: black;")
self.lbl_preview.setMinimumSize(800, 450)
vbox_prev.addWidget(self.lbl_preview)
self.sld_frame = QSlider(Qt.Horizontal)
self.sld_frame.valueChanged.connect(self.update_sync_frame)
self.spn_frame = QDoubleSpinBox()
self.spn_frame.setDecimals(0)
self.spn_frame.setSingleStep(1)
self.spn_frame.valueChanged.connect(self.update_sync_frame)
self.btn_play = QPushButton("▶ PLAY")
self.btn_play.setCheckable(True)
self.btn_play.clicked.connect(self.toggle_play)
self.play_timer = QTimer()
self.play_timer.timeout.connect(self.next_frame)
frame_hbox = QHBoxLayout()
frame_hbox.addWidget(self.btn_play)
frame_hbox.addWidget(QLabel("Frame:"))
frame_hbox.addWidget(self.spn_frame)
frame_hbox.addWidget(self.sld_frame, 10)
vbox_prev.addLayout(frame_hbox)
hbox2.addLayout(vbox_prev, 7)
# Right: Controls
vbox_ctrl = QVBoxLayout()
group_offsets = QGroupBox("Offsets (Yaw/Pitch/Roll/XYZ)")
form2 = QFormLayout()
self.chk_swap_xy = QPushButton("Swap XY Coordinates: OFF")
self.chk_swap_xy.setCheckable(True)
self.chk_swap_xy.clicked.connect(self.toggle_swap_xy)
form2.addRow("Axis:", self.chk_swap_xy)
self.spn_yaw = self.create_spinbox(-180, 180, 0, form2, "Yaw Off")
self.spn_pitch = self.create_spinbox(-180, 180, 0, form2, "Pitch Off")
self.spn_roll = self.create_spinbox(-180, 180, 0, form2, "Roll Off")
form2.addRow(QLabel("<hr>"))
self.spn_off_x = self.create_spinbox(-1000, 1000, 0, form2, "Pos X Off (m)")
self.spn_off_y = self.create_spinbox(-1000, 1000, 0, form2, "Pos Y Off (m)")
self.spn_off_z = self.create_spinbox(-1000, 1000, 0, form2, "Pos Z Off (m)")
form2.addRow(QLabel("<hr>"))
self.spn_focal = self.create_spinbox(1, 200, 24, form2, "Focal (mm)")
self.spn_sensor = self.create_spinbox(1, 100, 36, form2, "Sensor W (mm)")
group_offsets.setLayout(form2)
vbox_ctrl.addWidget(group_offsets)
vbox_ctrl.addStretch()
hbox2.addLayout(vbox_ctrl, 3)
tab2.setLayout(hbox2)
tabs.addTab(tab2, "2. 튜너")
# --- Tab 3: Export ---
tab3 = QWidget()
vbox3 = QVBoxLayout()
form3 = QFormLayout()
self.cmb_res = QComboBox()
self.cmb_res.addItems(["Original", "4K (3840x2160)", "2K (2560x1440)", "1080p (1920x1080)"])
form3.addRow("Resolution", self.cmb_res)
self.cmb_fps = QComboBox()
self.cmb_fps.addItems(["Original", "60", "30"])
form3.addRow("FPS", self.cmb_fps)
self.cmb_codec = QComboBox()
self.cmb_codec.addItems(["H.264 (avc1)", "MPEG-4 (mp4v)"])
form3.addRow("Codec", self.cmb_codec)
form3.addRow(QLabel("<hr>"))
self.spn_start = QSpinBox()
self.spn_start.setRange(0, 999999)
form3.addRow("Start Frame", self.spn_start)
self.spn_end = QSpinBox()
self.spn_end.setRange(0, 999999)
form3.addRow("End Frame", self.spn_end)
vbox3.addLayout(form3)
self.btn_export = QPushButton("START EXPORT")
self.btn_export.setStyleSheet("height: 60px; font-weight: bold; background-color: #f44336; color: white;")
self.btn_export.clicked.connect(self.start_export)
vbox3.addWidget(self.btn_export)
self.pbar = QProgressBar()
vbox3.addWidget(self.pbar)
vbox3.addStretch()
tab3.setLayout(vbox3)
tabs.addTab(tab3, "3. 출력")
def create_spinbox(self, min_v, max_v, def_v, layout, label):
sb = QDoubleSpinBox()
sb.setRange(min_v, max_v)
sb.setValue(def_v)
sb.setDecimals(2)
sb.setSingleStep(0.1)
sb.valueChanged.connect(self.update_preview)
layout.addRow(label, sb)
return sb
def toggle_play(self):
if self.btn_play.isChecked():
self.btn_play.setText("■ PAUSE")
# Calculate interval based on FPS (or default 33ms for 30fps)
cap = cv2.VideoCapture(self.txt_video.text())
fps = cap.get(cv2.CAP_PROP_FPS)
cap.release()
interval = int(1000 / fps) if fps > 0 else 33
self.play_timer.start(interval)
else:
self.btn_play.setText("▶ PLAY")
self.play_timer.stop()
def next_frame(self):
curr = self.sld_frame.value()
if curr < self.total_frames - 1:
self.sld_frame.setValue(curr + 1)
else:
self.btn_play.setChecked(False)
self.toggle_play()
def toggle_swap_xy(self):
if self.chk_swap_xy.isChecked():
self.chk_swap_xy.setText("Swap XY Coordinates: ON")
else:
self.chk_swap_xy.setText("Swap XY Coordinates: OFF")
self.update_preview()
def update_sync_frame(self, val):
# 도우미: 슬라이더와 스핀박스 동기화
sender = self.sender()
if sender == self.sld_frame:
self.spn_frame.blockSignals(True)
self.spn_frame.setValue(val)
self.spn_frame.blockSignals(False)
else:
self.sld_frame.blockSignals(True)
self.sld_frame.setValue(int(val))
self.sld_frame.blockSignals(False)
self.update_preview()
def find_file(self, line_edit, filt):
path, _ = QFileDialog.getOpenFileName(self, "Select File", "", filt)
if path: line_edit.setText(path)
def load_all_data(self):
print("Loading...")
self.srt_data = SRTParser.parse(self.txt_srt.text())
# Load CSV
try:
df = pd.read_csv(self.txt_csv.text(), encoding='cp949')
# lat, lon, 타원체고(h)
raw_pts = []
for _, row in df.iterrows():
x, y = self.transformer.transform(row['lat'], row['lon'])
raw_pts.append([x, y, row['타원체고(h)']])
# Reconstruct segments: 0-1, 1-2, 2-3...
seg_pts = []
for i in range(len(raw_pts)-1):
seg_pts.extend([raw_pts[i], raw_pts[i+1]])
self.line_points = np.array(seg_pts, dtype=np.float64)
print(f"Loaded {len(self.line_points)//2} segments.")
except Exception as e:
print(f"CSV Load Error: {e}")
# Video Info
cap = cv2.VideoCapture(self.txt_video.text())
self.total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
self.video_fps = cap.get(cv2.CAP_PROP_FPS)
self.sld_frame.setRange(0, self.total_frames - 1)
self.spn_frame.setRange(0, self.total_frames - 1)
self.spn_start.setRange(0, self.total_frames - 1)
self.spn_end.setRange(0, self.total_frames - 1)
self.spn_end.setValue(self.total_frames - 1)
cap.release()
self.update_preview()
def update_preview(self):
if not self.srt_data or self.line_points.size == 0: return
f_idx = self.sld_frame.value()
cap = cv2.VideoCapture(self.txt_video.text())
cap.set(cv2.CAP_PROP_POS_FRAMES, f_idx)
ret, frame = cap.read()
cap.release()
if not ret: return
h, w = frame.shape[:2]
if f_idx in self.srt_data:
meta = self.srt_data[f_idx]
dx, dy = self.transformer.transform(meta['lat'], meta['lon'])
# Swap Logic
if self.chk_swap_xy.isChecked():
dx, dy = dy, dx
line_pts_eff = self.line_points.copy()
line_pts_eff[:, [0, 1]] = line_pts_eff[:, [1, 0]]
else:
line_pts_eff = self.line_points
drone_pos = np.array([dx + self.spn_off_x.value(),
dy + self.spn_off_y.value(),
meta['alt'] + self.spn_off_z.value()])
# 실시간 디버그 로그 (콘솔 출력)
print(f"[Frame {f_idx}] XY_Order: {'Y,X' if self.chk_swap_xy.isChecked() else 'X,Y'} | Drone X={drone_pos[0]:.2f}, Y={drone_pos[1]:.2f}")
# Projection
yaw = np.radians(meta['yaw'] + self.spn_yaw.value())
pitch = np.radians(meta['pitch'] + self.spn_pitch.value())
roll = np.radians(meta['roll'] + self.spn_roll.value())
r_yaw = R.from_euler('z', -yaw)
r_pitch = R.from_euler('x', pitch)
r_roll = R.from_euler('y', roll)
R_b2w = (r_yaw * r_pitch * r_roll).as_matrix()
R_align = np.array([[1, 0, 0], [0, 0, -1], [0, 1, 0]])
R_w2c = R_align @ R_b2w.T
rvec, _ = cv2.Rodrigues(R_w2c)
tvec = -R_w2c @ drone_pos
f_px = (self.spn_focal.value() / self.spn_sensor.value()) * w
K = np.array([[f_px, 0, w/2], [0, f_px, h/2], [0, 0, 1]])
pts_cam = (line_pts_eff @ R_w2c.T) + tvec.T
for i in range(0, len(pts_cam), 2):
p1c, p2c = pts_cam[i], pts_cam[i+1]
if p1c[2] < 0.1 and p2c[2] < 0.1: continue
if p1c[2] < 0.1 or p2c[2] < 0.1:
t = (0.1 - p1c[2]) / (p2c[2] - p1c[2])
p_mid = p1c + t * (p2c - p1c)
if p1c[2] < 0.1: p1c = p_mid
else: p2c = p_mid
u1 = int(K[0, 0] * (p1c[0] / p1c[2]) + K[0, 2])
v1 = int(K[1, 1] * (p1c[1] / p1c[2]) + K[1, 2])
u2 = int(K[0, 0] * (p2c[0] / p2c[2]) + K[0, 2])
v2 = int(K[1, 1] * (p2c[1] / p2c[2]) + K[1, 2])
ret_cli, pt1, pt2 = cv2.clipLine((0, 0, w, h), (u1, v1), (u2, v2))
if ret_cli:
cv2.line(frame, pt1, pt2, (0, 0, 255), 10)
# Convert to QPixmap
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
img = QImage(frame.data, w, h, w*3, QImage.Format_RGB888)
pixmap = QPixmap.fromImage(img).scaled(self.lbl_preview.width(), self.lbl_preview.height(), Qt.KeepAspectRatio)
self.lbl_preview.setPixmap(pixmap)
def start_export(self):
res_map = {"Original": (0, 0), "4K (3840x2160)": (3840, 2160), "2K (2560x1440)": (2560, 1440), "1080p (1920x1080)": (1920, 1080)}
codec_map = {"H.264 (avc1)": "avc1", "MPEG-4 (mp4v)": "mp4v"}
target_res = res_map[self.cmb_res.currentText()]
if target_res == (0, 0):
cap = cv2.VideoCapture(self.txt_video.text())
target_res = (int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
cap.release()
params = {
'video_path': self.txt_video.text(),
'srt_data': self.srt_data,
'line_pts': self.line_points,
'off_yaw': self.spn_yaw.value(),
'off_pitch': self.spn_pitch.value(),
'off_roll': self.spn_roll.value(),
'off_x': self.spn_off_x.value(),
'off_y': self.spn_off_y.value(),
'off_z': self.spn_off_z.value(),
'focal': self.spn_focal.value(),
'sensor_w': self.spn_sensor.value(),
'resolution': target_res,
'fps': int(self.cmb_fps.currentText()) if self.cmb_fps.currentText() != "Original" else 0,
'range': (self.spn_start.value(), self.spn_end.value()),
'swap_xy': self.chk_swap_xy.isChecked(),
'codec': codec_map[self.cmb_codec.currentText()],
'output_path': "output_tuner_v2.mp4"
}
self.btn_export.setEnabled(False)
self.worker = RenderWorker(params)
self.worker.progress.connect(self.pbar.setValue)
self.worker.finished.connect(self.export_done)
self.worker.start()
def export_done(self, msg):
self.btn_export.setEnabled(True)
print(f"Export Result: {msg}")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())