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("