diff --git a/.claude/hooks/guard-history-fields.py b/.claude/hooks/guard-history-fields.py index 6ffd553..82b2b9e 100644 --- a/.claude/hooks/guard-history-fields.py +++ b/.claude/hooks/guard-history-fields.py @@ -9,6 +9,15 @@ import json import re import os +# Windows 기본 stdin/stderr 인코딩(cp949)에서 UTF-8 한글이 깨져 +# 정규식 매칭 실패(false block) + 에러 출력 mojibake가 발생. +# stdin/stderr를 UTF-8로 강제해 한글 필드를 정상 검사/출력한다. +for _stream in (sys.stdin, sys.stderr): + try: + _stream.reconfigure(encoding="utf-8") + except Exception: + pass + # ── path.json 로드 ──────────────────────────────────────────────────────────── HOOKS_DIR = os.path.dirname(os.path.abspath(__file__)) PATH_JSON = os.path.join(HOOKS_DIR, "path.json") diff --git a/docs/geo-station-mapping.html b/docs/geo-station-mapping.html new file mode 100644 index 0000000..98c5d94 --- /dev/null +++ b/docs/geo-station-mapping.html @@ -0,0 +1,272 @@ + + +
+ + +abcvideo — 시간 기준 → Station(측점) 기준 재생 전환을 위한 데이터 맵핑 정리 · 2026-06-17
+ +frame_cnt)다.
+ 측점 기준 재생이란 결국 측점 → 대표 프레임 → currentTime = frame / fps 로 seek 하는 역방향 흐름이다.
+ 화살표는 현재 구현된 시간 → 지리정보(표시) 단방향. Station 기준 재생은 이 화살표를 반대로 타는 것 (§5).
+ +geo(lat, lon, z)
+ └─ 위경도+표고를 월드 ENU(m) 좌표로 변환 (원점 = 첫 측점)
+ └─ 드론(카메라) 위치를 빼서 상대 벡터
+ └─ 카메라 회전 적용 R_w2c = R_align · R_b2w(yaw,pitch,roll)ᵀ
+ └─ 핀홀 투영 px = 0.5 + (Xc/Zc)·(f/sensorW)
+ py = 0.5 + (Yc/Zc)·(f/sensorH)
+ └─ 화면 정규화 좌표 (0~1) → 캔버스 오버레이
+
+
+ 위치: GEO_DATA_DIR (기본 samplevideo/). 인코딩은 EUC-KR/UTF-8 자동 감지.
| 파일 | 역할 | 핵심 컬럼 / 키 | 로더 |
|---|---|---|---|
| *회덕*.csv (POI·측점 제외) |
+ 드론 비행 로그 — 영상↔지리 연결의 본체. SRT 자막에서 추출된 프레임별 GPS·자세. | +frame_cnt, latitude, longitude, altitude, yaw, pitch, roll, focal_len |
+ geoMatch.ts loadFrames() |
+
| building/ *POI*위경도*.csv |
+ 지장물·건물·터널·교량·역사. (_타원체고 버전 우선 사용) |
+ title, category_clean, lat, lon, z | +geoMatch.ts loadPois() |
+
| building/ *측점*위경도*.csv |
+ 측점 — km측점(예 12K345). type=station. |
+ title, lat, lon, z | +geoMatch.ts loadPois() |
+
| pythonsource/ input/center.csv |
+ 선로 중심선 224점 — 측점/POI의 표고(z) 스냅 기준선. | +lat(1), lon(2), 타원체고 h(5) | +geoMatch.ts loadCenterline() |
+
| *.srt | +terrain offset(abs_alt − rel_alt) 산출용. 현재 로드만 하고 거의 미사용. | +rel_alt, abs_alt | +geoMatch.ts loadTerrainOffset() |
+
| 파일 | 책임 |
|---|---|
| server/src/services/geoMatch.ts | +CSV 로드(싱글턴 캐시) · 평면근사 ENU 투영 · findFramesForPoi(이름→프레임) · findPoisForFrame(프레임→POI) · getWorldOrigin(원점=첫 측점) · stationOrder(km 정렬) |
| server/src/routes/geo.ts | +REST: /api/geo/pois · /search · /frame/:n · /frames · /centerline |
| client/src/utils/geoProjection.ts | +정밀 투영 — EPSG:5186 TM(proj4), Python advanced_tuner_v2.py와 동일 회전식. 오버레이가 사용. |
| 파일 | 역할 |
|---|---|
| client/.../overlay/StationOverlay.tsx | +실시간 캔버스 오버레이. 전 프레임 픽셀 위치를 Map<frameNum,…>로 사전계산 → RAF 루프에서 조회·보간·EMA 스무딩 후 그림. |
| client/.../geo/StationVerify.tsx | +측점 탭. 측점 목록 클릭 → /api/geo/search → 최적 프레임으로 seek. 측점 재생의 토대 |
| client/.../geo/GeoSearch.tsx | +지리정보 탭. 이름→프레임 검색 + 현재 프레임→보이는 건물 역조회. |
| client/.../player/VideoPlayer.tsx | +frame↔time 변환의 단일 출처. frame = round(currentTime·VIDEO_FPS), seek는 currentTime(frame/VIDEO_FPS). |
| client/src/App.tsx | +우측 사이드바 탭(주석/지리정보/측점) 배선. onSeekToFrame={f => handleSeek(f/fps)}. |
// 영상 시간 ↔ 프레임 (CFR 가정, 누적연산 금지)
+time = frame / fps
+frame = round(currentTime * fps)
+
+// fps 값이 코드에 두 개 존재
+client VIDEO_FPS = 30000/1001 = 29.97 // VideoPlayer.tsx, StationOverlay.tsx
+server DEFAULT_FPS = 30 // geoMatch.ts (FrameMatch.time 계산용)
+ FrameMatch.time 값은 30 기준이라 부정확. 측점 기준 타임라인에서 시간(초)을 직접 쓰려면 반드시 29.97로 통일할 것.
+ | 방향 | 함수 / API | 동작 | UI |
|---|---|---|---|
| 이름 → 프레임 | +findFramesForPoi() GET /api/geo/search?q= |
+ 측점/건물명 검색 → 전 프레임 투영 → FOV 안 프레임 수집 → 연속구간 그룹화(GAP=30프레임) → 구간별 가장 중심에 가까운 프레임 1개 선택 | +StationVerify / GeoSearch | +
| 프레임 → 이름 | +findPoisForFrame() GET /api/geo/frame/:n |
+ 해당 프레임 드론 자세로 전 POI 투영 → FOV 안 목록 반환 (거리순) | +GeoSearch 역조회 | +
findFramesForPoi(측점명) 가 측점 → 대표 프레임 을, onSeekToFrame 이 프레임 → seek 를 담당.
+ 남은 일은 이를 전 측점에 대해 한 번에 묶어 km순 타임라인으로 만드는 것뿐.
+ | 현재 — 시간 기준 | 목표 — 측점 기준 | |
|---|---|---|
| 타임라인 단위 | 초 / 프레임 | 측점 (km측점, stationOrder 순) |
| 탐색 동작 | seek bar 드래그 | 측점 선택 → 그 측점이 가장 잘 보이는 프레임으로 점프 |
| "다음" 동작 | +N초 | 다음 km측점의 대표 프레임 |
| 핵심 데이터 | frame ↔ time | 측점 → 대표 프레임 인덱스 테이블 |
interface StationCue {
+ title: string; // "12K345"
+ km: number; // stationOrder()/1000 = 12.345
+ frame: number; // findFramesForPoi 대표 프레임 (구간 중심)
+ time: number; // frame / 29.97 ← fps 통일 필수
+ distance: number; // 카메라~측점 수평거리 (정확도 참고)
+ pixelX: number; pixelY: number; // 화면 내 위치 (품질 판단)
+}
+type StationTimeline = StationCue[]; // km 오름차순
+
+ A. 클라이언트 일괄 호출 서버 변경 없음
+ 측점 목록을 받아 측점마다 /api/geo/search 를 호출(또는 1회 Promise.all)해 StationCue[] 구성.
+ StationVerify 가 이미 측점별 단건으로 하는 일을 전체로 확장하는 수준. 측점 수가 수십~수백이면 호출 비용 고려.
B. 서버 일괄 인덱스 API 신설 권장
+ GET /api/geo/station-index — 서버가 전 측점에 대해 findFramesForPoi 를 한 번에 돌려
+ StationCue[](km순)를 반환. 프레임/POI 캐시가 이미 싱글턴이라 추가 I/O 없음.
+ 클라는 1회 호출로 측점 타임라인 확보 → 측점 슬라이더/리스트로 바로 재생.
// 이미 존재: 측점 클릭 → seek (StationVerify.tsx → App.tsx)
+onSeekToFrame = (frame) => handleSeek(frame / (fps || 30)); // fps=29.97 통일
+
+// 추가: 측점 슬라이더 / 이전·다음 측점 버튼
+nextStation() → timeline[idx+1].frame → onSeekToFrame
+prevStation() → timeline[idx-1].frame → onSeekToFrame
+
+// 현재 재생 위치 → 현재 측점 역표시 (선택)
+currentFrame → timeline 에서 가장 가까운 cue → "현재 12K345 부근"
+
+ nearestCL). 대표 프레임 산출도 동일 기준이라 표시와 재생이 일치.findFramesForPoi 가 여러 구간을 반환 → 측점 1개에 cue 여러 개 가능. km 타임라인은 대표 1개, 나머지는 보조 점프로.DEFAULT_FPS=30 → 30000/1001. 측점 시간축 도입 전 선행./station-index). 측점 수·반응성으로 결정.생성: Claude Code · 소스 기준 docs/history/2026-06-17_지리정보-측점-프레임-맵핑구조-분석.md
+ +