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 @@ + + + + + +지리정보 · 측점 · 프레임 맵핑 / Station 기준 재생 설계 + + + +
+ +

지리정보 · 측점 · 프레임 맵핑 구조

+

abcvideo — 시간 기준 → Station(측점) 기준 재생 전환을 위한 데이터 맵핑 정리 · 2026-06-17

+ +
+ 한 줄 요약 — 영상과 지리정보를 잇는 단 하나의 키는 프레임 번호(frame_cnt)다. + 측점 기준 재생이란 결국 측점 → 대표 프레임 → currentTime = frame / fps 로 seek 하는 역방향 흐름이다. +
+ + +

1. 전체 맵핑 흐름

+ +
+
영상 프레임currentTime
+ +
frame_cnt드론 CSV 행
+ +
드론 자세lat·lon·alt·yaw·pitch·roll
+ +
투영핀홀 카메라
+ +
측점·POI·중심선화면 픽셀(0~1)
+
+

화살표는 현재 구현된 시간 → 지리정보(표시) 단방향. 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) → 캔버스 오버레이
+ + +

2. 어떤 파일이 무엇을 맵핑하나

+ +

데이터 소스 (입력)

+

위치: GEO_DATA_DIR (기본 samplevideo/). 인코딩은 EUC-KR/UTF-8 자동 감지.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
파일역할핵심 컬럼 / 키로더
*회덕*.csv
(POI·측점 제외)
드론 비행 로그 — 영상↔지리 연결의 본체. SRT 자막에서 추출된 프레임별 GPS·자세.frame_cnt, latitude, longitude, altitude, yaw, pitch, roll, focal_lengeoMatch.ts
loadFrames()
building/
*POI*위경도*.csv
지장물·건물·터널·교량·역사. (_타원체고 버전 우선 사용)title, category_clean, lat, lon, zgeoMatch.ts
loadPois()
building/
*측점*위경도*.csv
측점 — km측점(예 12K345). type=station.title, lat, lon, zgeoMatch.ts
loadPois()
pythonsource/
input/center.csv
선로 중심선 224점 — 측점/POI의 표고(z) 스냅 기준선.lat(1), lon(2), 타원체고 h(5)geoMatch.ts
loadCenterline()
*.srtterrain offset(abs_alt − rel_alt) 산출용. 현재 로드만 하고 거의 미사용.rel_alt, abs_altgeoMatch.ts
loadTerrainOffset()
+ +

처리 / 변환 (로직)

+ + + + + + + + +
파일책임
server/src/services/geoMatch.tsCSV 로드(싱글턴 캐시) · 평면근사 ENU 투영 · findFramesForPoi(이름→프레임) · findPoisForFrame(프레임→POI) · getWorldOrigin(원점=첫 측점) · stationOrder(km 정렬)
server/src/routes/geo.tsREST: /api/geo/pois · /search · /frame/:n · /frames · /centerline
client/src/utils/geoProjection.ts정밀 투영 — EPSG:5186 TM(proj4), Python advanced_tuner_v2.py와 동일 회전식. 오버레이가 사용.
+ +

표시 / 조작 (UI)

+ + + + + + + + + + + + +
파일역할
client/.../overlay/StationOverlay.tsx실시간 캔버스 오버레이. 전 프레임 픽셀 위치를 Map<frameNum,…>로 사전계산 → RAF 루프에서 조회·보간·EMA 스무딩 후 그림.
client/.../geo/StationVerify.tsx측점 탭. 측점 목록 클릭 → /api/geo/search → 최적 프레임으로 seek. 측점 재생의 토대
client/.../geo/GeoSearch.tsx지리정보 탭. 이름→프레임 검색 + 현재 프레임→보이는 건물 역조회.
client/.../player/VideoPlayer.tsxframe↔time 변환의 단일 출처. frame = round(currentTime·VIDEO_FPS), seek는 currentTime(frame/VIDEO_FPS).
client/src/App.tsx우측 사이드바 탭(주석/지리정보/측점) 배선. onSeekToFrame={f => handleSeek(f/fps)}.
+ + +

3. 프레임 번호 = 모든 맵핑의 조인 키

+
+
// 영상 시간 ↔ 프레임  (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 계산용)
+
+
+ 주의 — fps 불일치(29.97 vs 30). seek는 프레임 번호로 하므로 실질 오차는 작지만, + 서버 FrameMatch.time 값은 30 기준이라 부정확. 측점 기준 타임라인에서 시간(초)을 직접 쓰려면 반드시 29.97로 통일할 것. +
+ + +

4. 현재 제공되는 두 방향 질의

+ + + + + + + + + + + + + + +
방향함수 / 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순 타임라인으로 만드는 것뿐. +
+ + +

5. Station(측점) 기준 재생 설계

+ +

현재(시간축) vs 목표(측점축)

+ + + + + + +
현재 — 시간 기준목표 — 측점 기준
타임라인 단위초 / 프레임측점 (km측점, stationOrder 순)
탐색 동작seek bar 드래그측점 선택 → 그 측점이 가장 잘 보이는 프레임으로 점프
"다음" 동작+N초다음 km측점의 대표 프레임
핵심 데이터frame ↔ time측점 → 대표 프레임 인덱스 테이블
+ +

제안 흐름

+
+
측점 N개/api/geo/pois (station)
+ +
측점→프레임findFramesForPoi ×N
+ +
km순 정렬stationOrder
+ +
측점 타임라인[{측점, frame, time}]
+ +
seekcurrentTime=frame/29.97
+
+ +

측점 인덱스 데이터 모델

+
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회 호출로 측점 타임라인 확보 → 측점 슬라이더/리스트로 바로 재생.

+
+ +

재생 UI 결선 (기존 자산 재사용)

+
// 이미 존재: 측점 클릭 → 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 부근"
+ +
+ 설계 포인트 + +
+ +
+

후속 / 결정 필요

+ + +

생성: Claude Code · 소스 기준 docs/history/2026-06-17_지리정보-측점-프레임-맵핑구조-분석.md

+ +
+ + diff --git a/docs/history/2026-06-17_지리정보-측점-프레임-맵핑구조-분석.md b/docs/history/2026-06-17_지리정보-측점-프레임-맵핑구조-분석.md new file mode 100644 index 0000000..e9aae40 --- /dev/null +++ b/docs/history/2026-06-17_지리정보-측점-프레임-맵핑구조-분석.md @@ -0,0 +1,69 @@ +# 지리정보 + 측점 + 프레임 맵핑 구조 분석 + +**작업일**: 2026-06-17 +**작업명**: 지리정보·측점·프레임 맵핑 구조 소스 분석 + station 기준 재생 HTML 문서화 +**소요 시간**: 18분 +**Context 사용량**: input 60k / output 8k tokens +**이슈**: #0 + +--- + +## 목적 + +영상 프레임 ↔ 측점 ↔ 지리정보(POI/중심선)가 현재 소스에서 어떻게 맵핑되는지 코드 기반으로 파악. 추가로 "시간 기준이 아닌 station(측점) 기준 재생"을 위한 맵핑 구조를 HTML 문서로 출력. + +## 분석 대상 파일 + +- [server/src/services/geoMatch.ts](../../server/src/services/geoMatch.ts) — 서버 투영/매칭 (평면근사 ENU) +- [server/src/routes/geo.ts](../../server/src/routes/geo.ts) — geo API 엔드포인트 +- [client/src/utils/geoProjection.ts](../../client/src/utils/geoProjection.ts) — 클라 투영 (EPSG:5186 TM, Python advanced_tuner_v2.py 동일) +- [client/src/components/overlay/StationOverlay.tsx](../../client/src/components/overlay/StationOverlay.tsx) — 실시간 캔버스 오버레이 +- [client/src/components/geo/StationVerify.tsx](../../client/src/components/geo/StationVerify.tsx) — 측점→프레임 점프 검증 패널 +- [client/src/components/player/VideoPlayer.tsx](../../client/src/components/player/VideoPlayer.tsx) — frame↔time 변환 + +## 핵심 결론 + +### 맵핑 키 = 프레임 번호 +- 드론 CSV의 `frame_cnt` = 영상 프레임 ↔ GPS+자세 연결고리 +- `time = frame / fps` +- **fps 불일치 발견**: 클라 `VIDEO_FPS = 30000/1001 = 29.97`, 서버 `DEFAULT_FPS = 30`. seek는 frame번호 기반이라 실害 적으나 서버 `FrameMatch.time` 부정확. + +### 데이터 소스 (GEO_DATA_DIR 기본 samplevideo/) +1. `*회덕*.csv` — 드론 비행로그 (frame_cnt, lat, lon, alt, yaw, pitch, roll, focal_len) +2. `building/*POI*위경도*.csv` — 지장물 (타원체고 버전 우선) +3. `building/*측점*위경도*.csv` — 측점 +4. `pythonsource/input/center.csv` — 선로 중심선 224점 (타원체고 col5) +5. `*.srt` — terrain offset (로드만, 거의 미사용) + +### 투영 파이프라인 +``` +geo(lat,lon,z) → ENU world(m) → 카메라 상대벡터 → 회전(yaw/pitch/roll) → 핀홀 투영 px,py(0~1) +``` +- 월드 원점 = **첫 측점** (측량 기준점), 드론 frame[0] 아님 → 드론 GPS 오차 회피 +- 서버/클라 투영 **구현 2벌** (서버=cos(lat) 평면근사, 클라=EPSG:5186 TM proj4) — 정밀도 차이 + +### 두 방향 질의 +- `findFramesForPoi(name)`: 측점명 → 전 프레임 투영 → FOV 안 → 연속구간 그룹화(GAP=30) → 구간별 중심 프레임. (StationVerify 점프) +- `findPoisForFrame(n)`: 프레임 → 보이는 POI 목록. + +### 클라 실시간 오버레이 +- idle time에 `Map` 사전계산 +- RAF 루프: Map 조회 + frameNum→+1 보간 + EMA 스무딩 → canvas +- 측점은 투영 전 가장 가까운 중심선 점에 스냅 (nearestCL, z도 중심선값) +- smoothFrame: 드론 자세 ±N프레임 이동평균으로 GPS/IMU 노이즈 제거 + +### 측점 순서 +- `stationOrder`: title `\d+K\d+` 파싱 = km측점 ("12K345" = 12.345km). 서버/클라 동일 함수. + +## Station 기준 재생 관점 + +현재 구조는 "시간(프레임) → 지리정보" 단방향 표시 중심. station 기준 재생 = **측점 → 대표 프레임 → seek** 역방향이 핵심. + +- 이미 존재하는 빌딩블록: `findFramesForPoi` (측점→최적 프레임), `onSeekToFrame` (프레임→`currentTime(frame/fps)`). +- station 기준 타임라인을 만들려면: 전 측점에 대해 `findFramesForPoi` 1회 일괄 호출 → 측점별 대표 프레임 테이블 구성 → km 순 정렬 → 측점 간 이동 = seek. +- 산출물: 맵핑 구조 + station 기준 재생 설계를 정리한 HTML (`docs/geo-station-mapping.html`). + +## 후속 참고 +- fps 불일치(30 vs 29.97) 정리 검토 가치 있음 +- 서버/클라 투영 알고리즘 통일 여부 결정 필요 (현재 클라가 정밀) +- station 기준 재생용 측점→프레임 테이블 API(`/api/geo/station-index` 등) 신설 검토