Compare commits
2
Commits
e0ff5dd6d0
...
fe25ce00f1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe25ce00f1 | ||
|
|
5c8b499979 |
@@ -15,14 +15,14 @@ import { persist } from 'zustand/middleware';
|
|||||||
export const KNOWN_GRADES = ['1종', '2종', '3종', '기타'] as const;
|
export const KNOWN_GRADES = ['1종', '2종', '3종', '기타'] as const;
|
||||||
|
|
||||||
interface SettingsStore {
|
interface SettingsStore {
|
||||||
/** 시설종별별 표시 여부. 기본 1종☑ 2종☑ 3종☐. */
|
/** 시설종별별 표시 여부. 기본 1종☑ 2종☑ 3종☐ 기타☐ (초기 구동 시 1·2종만 체크). */
|
||||||
gradeFilter: Record<string, boolean>;
|
gradeFilter: Record<string, boolean>;
|
||||||
/** 특정 시설종별 체크 토글. */
|
/** 특정 시설종별 체크 토글. */
|
||||||
setGradeFilter: (grade: string, checked: boolean) => void;
|
setGradeFilter: (grade: string, checked: boolean) => void;
|
||||||
/** 화면 POI 겹침제외(겹치면 1개만). true=겹친 것 숨김(기본), false=모든 POI 표시. */
|
/** 화면 POI 겹침제외(겹치면 1개만). true=겹친 것 숨김(기본), false=모든 POI 표시. */
|
||||||
poiOverlapExclude: boolean;
|
poiOverlapExclude: boolean;
|
||||||
setPoiOverlapExclude: (v: boolean) => void;
|
setPoiOverlapExclude: (v: boolean) => void;
|
||||||
/** 좌측 노선 패널(RoutePanel) 표시 여부. '화면표시 옵션' 토글로 제어. 기본 표시. */
|
/** 좌측 노선 패널(RoutePanel) 표시 여부. '화면표시 옵션' 토글로 제어. 기본 숨김(OFF). */
|
||||||
showRoutePanel: boolean;
|
showRoutePanel: boolean;
|
||||||
setShowRoutePanel: (v: boolean) => void;
|
setShowRoutePanel: (v: boolean) => void;
|
||||||
/** 측점 진단 HUD(드론 GPS·투영측점) 하단바 표시 여부. 기본 off. */
|
/** 측점 진단 HUD(드론 GPS·투영측점) 하단바 표시 여부. 기본 off. */
|
||||||
@@ -46,7 +46,7 @@ const DEFAULT_GRADE_FILTER: Record<string, boolean> = {
|
|||||||
'1종': true,
|
'1종': true,
|
||||||
'2종': true,
|
'2종': true,
|
||||||
'3종': false,
|
'3종': false,
|
||||||
'기타': true,
|
'기타': false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSettingsStore = create<SettingsStore>()(
|
export const useSettingsStore = create<SettingsStore>()(
|
||||||
@@ -57,7 +57,7 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||||||
set((s) => ({ gradeFilter: { ...s.gradeFilter, [grade]: checked } })),
|
set((s) => ({ gradeFilter: { ...s.gradeFilter, [grade]: checked } })),
|
||||||
poiOverlapExclude: true,
|
poiOverlapExclude: true,
|
||||||
setPoiOverlapExclude: (v) => set({ poiOverlapExclude: v }),
|
setPoiOverlapExclude: (v) => set({ poiOverlapExclude: v }),
|
||||||
showRoutePanel: true,
|
showRoutePanel: false,
|
||||||
setShowRoutePanel: (v) => set({ showRoutePanel: v }),
|
setShowRoutePanel: (v) => set({ showRoutePanel: v }),
|
||||||
showStationDiag: false,
|
showStationDiag: false,
|
||||||
setShowStationDiag: (v) => set({ showStationDiag: v }),
|
setShowStationDiag: (v) => set({ showStationDiag: v }),
|
||||||
@@ -72,6 +72,16 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'ghivideo.settings',
|
name: 'ghivideo.settings',
|
||||||
|
// 시작 기본값 변경(좌측패널 OFF + 시설등급 1·2종만) 반영 —
|
||||||
|
// 과거 저장본(version 0)은 최초 1회 해당 두 값을 기본으로 강제 재설정한다.
|
||||||
|
version: 1,
|
||||||
|
migrate: (persisted, version) => {
|
||||||
|
const p = (persisted ?? {}) as Partial<SettingsStore>;
|
||||||
|
if (version < 1) {
|
||||||
|
return { ...p, showRoutePanel: false, gradeFilter: { ...DEFAULT_GRADE_FILTER } } as SettingsStore;
|
||||||
|
}
|
||||||
|
return p as SettingsStore;
|
||||||
|
},
|
||||||
// 과거 저장본에 누락된 종별 키가 있으면 기본값으로 보충(스키마 진화 대비).
|
// 과거 저장본에 누락된 종별 키가 있으면 기본값으로 보충(스키마 진화 대비).
|
||||||
merge: (persisted, current) => {
|
merge: (persisted, current) => {
|
||||||
const p = (persisted ?? {}) as Partial<SettingsStore>;
|
const p = (persisted ?? {}) as Partial<SettingsStore>;
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# 초기 구동 기본값 변경 — 좌측패널 OFF + 시설등급 1·2종만 체크
|
||||||
|
|
||||||
|
**소요 시간**: 약 20분
|
||||||
|
**Context 사용량**: input ~48k / output ~7k tokens
|
||||||
|
|
||||||
|
## 요청
|
||||||
|
|
||||||
|
> 처음 플레이어가 구동되면 "좌측패널 OFF"이고, 시설등급은 1종·2종만 체크된 상태여야 한다.
|
||||||
|
|
||||||
|
(같은 세션에서 앞서 "로컬 실행 가능 여부", "지도 나침반 가운데버튼 패닝 가능 여부"는 코드 변경 없이 질의응답만 진행.)
|
||||||
|
|
||||||
|
## 변경 파일
|
||||||
|
|
||||||
|
- `client/src/store/settingsStore.ts`
|
||||||
|
|
||||||
|
## 변경 내용
|
||||||
|
|
||||||
|
1. **좌측 노선 패널 기본값**: `showRoutePanel: true → false` (초기 OFF)
|
||||||
|
2. **시설등급 기본 필터**: `DEFAULT_GRADE_FILTER['기타'] true → false`
|
||||||
|
- 결과: 1종☑ 2종☑ 3종☐ 기타☐ → "1종·2종만 체크" 충족
|
||||||
|
3. **persist 마이그레이션 추가**: `version: 1` + `migrate()` 신설
|
||||||
|
- 사유: 설정이 localStorage(`ghivideo.settings`)에 지속되므로, 기본값만 바꾸면 이미 실행해본 브라우저의 옛 저장값(showRoutePanel:true, 기타:true)이 `merge`에서 우선되어 안 바뀜.
|
||||||
|
- 처리: 과거 저장본(version 0)은 최초 1회 `showRoutePanel:false` + `gradeFilter=기본값`으로 강제 재설정. 이후 사용자의 토글 변경은 정상적으로 지속됨.
|
||||||
|
- 신규 브라우저(저장값 없음)는 initializer 기본값이 그대로 적용됨.
|
||||||
|
4. 관련 주석 2곳 갱신.
|
||||||
|
|
||||||
|
## 검증
|
||||||
|
|
||||||
|
- `npm run build -w client` (tsc + vite build) 성공 — 타입 에러 없음, 220 모듈 변환, dist 재생성.
|
||||||
|
- Node 20.20.2(nvm)로 빌드.
|
||||||
|
|
||||||
|
## 반영 방법
|
||||||
|
|
||||||
|
- 프로덕션(55000, PM2): 정적 dist를 서빙하므로 브라우저 **하드 리로드**로 반영(에셋 해시 변경됨). PM2 재시작 불필요.
|
||||||
|
- 개발(5173): dev 서버가 소스를 직접 읽으므로 즉시 반영.
|
||||||
|
- 기존 브라우저는 첫 로드 시 마이그레이션이 1회 동작해 좌측패널 OFF·시설등급 1·2종으로 재설정됨(수동 localStorage 삭제 불필요).
|
||||||
|
|
||||||
|
## 참고 / 판단 사항
|
||||||
|
|
||||||
|
- "처음 구동 시 OFF"를 "매 실행마다 강제 OFF"가 아니라 "초기 기본값 = OFF, 이후 사용자 토글은 기억"으로 해석함. 만약 매 실행마다 무조건 OFF로 시작하길 원하면 `showRoutePanel`을 persist 대상에서 제외(partialize)하는 추가 작업 필요.
|
||||||
|
- `기타` 등급은 이번에 기본 해제로 바뀌어, `isGradeVisible` 규칙상 '기타' 등급 구조물이 기본 화면에서 숨겨짐(요청 문구 "1종·2종만" 준수 결과).
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# 경영진(사장님) 발표 자료 3장 작성
|
||||||
|
|
||||||
|
**소요 시간**: 약 15분
|
||||||
|
**Context 사용량**: input ~72k / output ~11k tokens
|
||||||
|
|
||||||
|
## 요청
|
||||||
|
|
||||||
|
- 사장님께 GhiVideo 프로그램을 발표·설명할 예정 → **3장 내외 발표 자료** 작성.
|
||||||
|
- 근거 자료: `docs/` 폴더에 취합된 기존 문서들.
|
||||||
|
|
||||||
|
## 산출물
|
||||||
|
|
||||||
|
- `docs/발표_GhiVideo_경영진요약_3장.md` (원본)
|
||||||
|
- `docs/발표_GhiVideo_경영진요약_3장.html` (자체완결)
|
||||||
|
- `docs/발표_GhiVideo_경영진요약_3장.pdf` (A4, 한글 임베드, 3페이지)
|
||||||
|
|
||||||
|
## 구성 (임원 눈높이 · 각 1페이지)
|
||||||
|
|
||||||
|
1. **무엇 / 왜** — 한 줄 요약, 필요성(기존 시간축 탐색의 한계), 핵심 3기능(측점탐색·AR정합·대용량재생), 처리 흐름 도식(SVG)
|
||||||
|
2. **핵심 기술·차별성** — 대표 4기술, 정량 성과 KPI(부각오차 42.7°→11°, DEM 대비 ~0.2m, 4K 60fps, 왕복오차≈0), DEM-free 차별화, 특허 2건
|
||||||
|
3. **현황·기대효과·계획·검토요청** — 동작 제품(55000 시연), 기대효과 표, 향후 로드맵, 의사결정 요청(특허 출원 승인·시범적용)
|
||||||
|
|
||||||
|
## 근거 문서
|
||||||
|
|
||||||
|
- `보고서_GhiVideo_개발내용및핵심기술.md` (핵심 내용·수치·흐름도 SVG 재사용)
|
||||||
|
- `기술명세_GhiVideo_종합기술문서.md`, 각종 특허/쉬운설명 문서
|
||||||
|
|
||||||
|
## 방법 / 검증
|
||||||
|
|
||||||
|
- 기존 파이프라인 `scripts/md2docs.sh`(pandoc + weasyprint, report.css)로 HTML/PDF 생성.
|
||||||
|
- HTML 검증: `break-before: page` 2개(=3페이지) 확인, 흐름도 SVG 포함, 10개 섹션 제목 정상.
|
||||||
|
- PDF 68KB 정상 생성(렌더 이미지 미리보기 도구 poppler 미설치로 화면 캡처 확인은 생략).
|
||||||
|
|
||||||
|
## 참고
|
||||||
|
|
||||||
|
- 파일 상단 KPI·차별화 문구는 사장님 대상이라 기술용어 최소화, 효과·비용·의사결정 중심으로 재구성.
|
||||||
|
- 숫자·특허 현황은 기존 보고서 값 그대로 인용(부각오차·표고 일치도 등).
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# KMZ → 영상 표출 전 과정 그림 설명서 작성
|
||||||
|
|
||||||
|
**소요 시간**: 약 20분
|
||||||
|
**Context 사용량**: input ~95k / output ~14k tokens
|
||||||
|
|
||||||
|
## 요청
|
||||||
|
|
||||||
|
- "KMZ 파일을 시작으로 동영상에 위치시키기까지의 과정을 논리적으로 그림과 함께 자세히 설명"
|
||||||
|
|
||||||
|
## 산출물
|
||||||
|
|
||||||
|
- `docs/쉬운설명_KMZ에서_영상표출까지_전과정.md` (+ .html / .pdf, md2docs.sh)
|
||||||
|
- SVG 도식 6점: 전체 파이프라인 / KMZ 내부구조·Placemark 해부 / 표고 datum / 좌표 3단 변환 / 핀홀 투영 / cover 정렬·평활
|
||||||
|
|
||||||
|
## 문서 구성 (8단계 + 보너스)
|
||||||
|
|
||||||
|
1. KMZ 압축 해제 — fflate `unzipSync`, `.kml` 우선 (`geoData.ts parseKmz` L295)
|
||||||
|
2. KML 폴더 트리 재귀 + Placemark 수집 (`walk` L376, `parseKmlDescProps` L276)
|
||||||
|
3. 폴더명 기반 POI/구조물 분류 — 교량·터널·구교=구조물, 지장물·출입문=POI, KAKAO_RAIL=POI+역사 (`handlePlacemark` L331)
|
||||||
|
4. 표고 결정 — 드래그보정 > DEM > 최근접 측점 정표고, geoidOffset(대전 25.8m) datum 정합
|
||||||
|
5. 좌표 변환 — 위경도 → EPSG:5186 TM(proj4) → 드론 기준 상대 ENU (`projectPoint` L334)
|
||||||
|
6. 회전 — Rz(−yaw)·Rx(pitch)·Ry(roll) → 카메라 좌표, Zc≤0/FOV/거리 필터 (`toCameraCoords` L301)
|
||||||
|
7. 핀홀 투영 — u=0.5+(Xc/Zc)(f/sW) (`pixelFromCamera` L126)
|
||||||
|
8. 화면 배치 — object-fit:cover 정렬(StationOverlay L948) + 속도적응 평활(smoothStep L49) + 60fps RAF
|
||||||
|
★ 역투영 드래그 보정 (`worldFromPixel` L149)
|
||||||
|
|
||||||
|
## 검증
|
||||||
|
|
||||||
|
- 실제 소스(geoData.ts / geoProjection.ts / StationOverlay.tsx)를 읽고 단계·라인 확인 후 작성 (라인은 2026-07-03 기준).
|
||||||
|
- md2docs.sh 변환 정상(HTML 31KB, PDF 140KB).
|
||||||
|
|
||||||
|
## 참고
|
||||||
|
|
||||||
|
- 초등학생 수준 눈높이(메모리 기본값) + 파일·라인 근거 병기 형식. 기존 쉬운설명 시리즈와 동일한 스타일.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# 쉬운설명_프레임기반에서_측점기반_플레이어 — 3절 그림 개선
|
||||||
|
|
||||||
|
**소요 시간**: 약 10분
|
||||||
|
**Context 사용량**: input ~110k / output ~5k tokens
|
||||||
|
|
||||||
|
## 요청
|
||||||
|
|
||||||
|
- 「3. 핵심 비밀 — 매 장면마다 '시간'과 '측점'을 짝지어 둔다」 절의 그림을 더 명료하고 이해하기 쉽게 재작성.
|
||||||
|
|
||||||
|
## 문제 진단 (기존 그림)
|
||||||
|
|
||||||
|
- 마름모꼴 화살표 흐름(위 1박스 → 좌우 2박스 → 아래 1박스)이라 시선 동선이 교차함.
|
||||||
|
- 결과물인 "시간↔측점 표"가 문구로만 존재 — 표(사전)라는 핵심 개념이 시각화되지 않음.
|
||||||
|
- 표를 "왜 만드는지"(재생 중 측점 표시 / 측점 검색 점프)가 그림에 없음.
|
||||||
|
|
||||||
|
## 개선 내용 (docs/쉬운설명_프레임기반에서_측점기반_플레이어.md)
|
||||||
|
|
||||||
|
3단 구성으로 교체 (viewBox 720×330):
|
||||||
|
|
||||||
|
1. **① 사진 1장마다 두 값 계산** — 프레임 카드(번호+GPS) → ⏱시간(파랑)·📐측점(초록) 두 칩으로 분기
|
||||||
|
2. **② '시간↔측점' 표 완성** — 실제 표 모양(헤더+5행, 예시행 191초/192초/193초/⋮/348초, 현재행 하이라이트)으로 시각화. "모든 사진에 반복" 화살표로 ①→② 연결
|
||||||
|
3. **③ 표 하나로 양방향 찾기** — 재생 중("지금 192초, 여긴 어디?"→측점 표시) / 검색("160k130 가줘!"→192초 점프) 두 사용례 배지
|
||||||
|
|
||||||
|
- figcaption도 ①②③ 구조에 맞춰 갱신. 색 체계는 문서 기존 규칙 유지(시간=파랑, 측점=초록, 표=앰버, 화살표=보라).
|
||||||
|
|
||||||
|
## 검증
|
||||||
|
|
||||||
|
- md2docs.sh 재변환 정상 (HTML 24KB, PDF 94KB 재생성).
|
||||||
|
- SVG 좌표 겹침 검토(패널 간격·표 행 경계·하단 배지) 완료.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# SVG 내부 빈 줄로 인한 그림 깨짐(빈 화면) 수정
|
||||||
|
|
||||||
|
**소요 시간**: 약 10분
|
||||||
|
**Context 사용량**: input ~125k / output ~6k tokens
|
||||||
|
|
||||||
|
## 증상
|
||||||
|
|
||||||
|
- 「쉬운설명_프레임기반에서_측점기반_플레이어」 3절 새 그림이 HTML/PDF에서 **빈 상자**로 나오고, 그림 속 글자들이 그림 밖 본문에 줄글로 쏟아짐 (사용자 스크린샷 제보).
|
||||||
|
|
||||||
|
## 원인
|
||||||
|
|
||||||
|
- pandoc(gfm)은 **raw HTML 블록을 빈 줄에서 종료**함 → `<svg>` 내부에 빈 줄이 있으면 블록이 쪼개져 이후 `<text>` 등의 내용이 마크다운 본문으로 처리됨.
|
||||||
|
- 직전 작업(3절 그림 개선)에서 가독성용 빈 줄 4개를 SVG 안에 넣은 것이 직접 원인. 추가로 1절 기존 그림(41행)에도 동일 문제 잠재.
|
||||||
|
|
||||||
|
## 수정
|
||||||
|
|
||||||
|
1. `docs/쉬운설명_프레임기반에서_측점기반_플레이어.md` — SVG 내부 빈 줄 5곳 제거 (41, 72, 86, 91, 113행). 주석(`<!-- -->`)은 빈 줄 없이는 무해하므로 유지.
|
||||||
|
2. **전수 검사**(awk, docs/*.md 전체)로 동일 문제 추가 발견: `발표_GhiVideo_좌표투영과_측점기반재생.md` 117행 → 제거.
|
||||||
|
3. 두 문서 md2docs.sh 재변환.
|
||||||
|
|
||||||
|
## 검증
|
||||||
|
|
||||||
|
- md 전수 검사: SVG 내부 빈 줄 0건.
|
||||||
|
- 생성 HTML 파이썬 검사: svg 블록 제거 후 본문에 그림 전용 문구 누출 **없음** ✅ (두 문서 모두), 3절 svg에 text 25개 정상 포함.
|
||||||
|
|
||||||
|
## 재발 방지 메모
|
||||||
|
|
||||||
|
- **SVG를 md에 인라인할 때 `<svg>...</svg>` 사이에 빈 줄 금지.** 구획 구분은 빈 줄 대신 주석 한 줄로.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# '시간↔측점 대응표' 용어 통일 + 보고서용 단독 그림 출력
|
||||||
|
|
||||||
|
**소요 시간**: 약 15분
|
||||||
|
**Context 사용량**: input ~150k / output ~8k tokens
|
||||||
|
|
||||||
|
## 요청
|
||||||
|
|
||||||
|
1. "시간↔측점 지도" 명칭 개선 논의 → **"시간↔측점 대응표"** 로 결정, 문서 반영("수정해줘")
|
||||||
|
2. 사용자 보고서에 넣을 수 있게 **해당 도식만 따로 그려서 출력**
|
||||||
|
|
||||||
|
## 변경 내용
|
||||||
|
|
||||||
|
### 1. 용어 통일 (지도/표 → 대응표)
|
||||||
|
|
||||||
|
- `docs/발표_GhiVideo_좌표투영과_측점기반재생.md` — "시간↔측점 지도" 4곳 + 그림 내 "시간 ↔ 측점 지도 완성!" → "대응표". figcaption에 "재생 기준을 '시간'에서 '스테이션(측점)'으로 전환하기 위한 색인 구조" 문구 추가
|
||||||
|
- `docs/쉬운설명_프레임기반에서_측점기반_플레이어.md` — "시간↔측점 표" 5곳 + 그림 내 "② '시간 ↔ 측점' 표 완성" → "대응표"
|
||||||
|
- 특허 초안의 "매핑" 표현은 특허 문체에 적합하므로 유지
|
||||||
|
- 두 문서 md2docs.sh 재변환 완료
|
||||||
|
|
||||||
|
### 2. 단독 그림 산출물 (`docs/그림_시간측점대응표.*`)
|
||||||
|
|
||||||
|
- 사용자 보고서 스타일(오피스 단색 박스: 회색/파랑/초록/황금 + 보라 화살표)에 맞춘 SVG 도식 작성
|
||||||
|
- 사용자 재작성본의 오타 정정 반영: "측정"→"측점" 2곳, 예시 "2368940"→"위도·경도", 하단 "지도"→"대응표"
|
||||||
|
- 출력 3종: `.md`(원본), `.pdf`(**여백 0, 도식 크기 페이지** — weasyprint 커스텀 @page 270×117.5mm), `.png`(**2143×933 고해상도** — PyMuPDF 렌더)
|
||||||
|
- PNG 품질 육안 검증 완료 (한글 폰트·화살표·색상 정상)
|
||||||
|
|
||||||
|
## 새로 확보한 방법 (재사용 가능)
|
||||||
|
|
||||||
|
- **SVG 도식 → 보고서 삽입용 PNG 파이프라인**: 그림 전용 HTML(@page 여백0) → weasyprint PDF → PyMuPDF(fitz) Matrix 2.8배 렌더 → PNG. poppler 없이 동작.
|
||||||
|
|
||||||
|
## 남은 사항
|
||||||
|
|
||||||
|
- 사용자 자신의 보고서(외부 파일)에는 직접 붙여넣기 필요 — PNG 경로: `docs/그림_시간측점대응표.png`
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# 단독 그림(시간↔측점 대응표) 글자 아랫부분 잘림 수정 — PIL 재렌더
|
||||||
|
|
||||||
|
**소요 시간**: 약 10분
|
||||||
|
**Context 사용량**: input ~165k / output ~5k tokens
|
||||||
|
|
||||||
|
## 증상 (사용자 제보)
|
||||||
|
|
||||||
|
- `docs/그림_시간측점대응표.png` 글자들의 **아랫부분이 조금씩 잘려** 보임.
|
||||||
|
|
||||||
|
## 원인
|
||||||
|
|
||||||
|
- 확대 검증 결과 weasyprint(61.2)의 SVG `<text>` 렌더러가 **베이스라인 아래 획(디센더)을 클리핑**함.
|
||||||
|
- "fps"의 p 꼬리가 잘려 "fos"처럼 보임, "드론·기록" 등 한글 하단 획 일부 손상.
|
||||||
|
- 이전 파이프라인: 인라인 SVG → weasyprint PDF → fitz PNG. (SVG 텍스트가 weasyprint를 지나는 순간 발생)
|
||||||
|
|
||||||
|
## 수정
|
||||||
|
|
||||||
|
- 렌더러 교체: **PIL 직접 드로잉** (`ImageFont.truetype` malgun/malgunbd + `anchor='mm'`,
|
||||||
|
rectangle/line/polygon 화살촉) → `그림_시간측점대응표.png` (2160×940) 재생성.
|
||||||
|
- PDF: fitz로 PNG 삽입, `deflate=True, garbage=4` 압축 (6MB → 52KB).
|
||||||
|
|
||||||
|
## 검증
|
||||||
|
|
||||||
|
- PNG 육안 검증: fps 디센더·한글 받침 모두 온전 ✅. 임시 파일(_z*, _zoom*) 정리.
|
||||||
|
|
||||||
|
## 파급 참고
|
||||||
|
|
||||||
|
- **본문 문서들의 인라인 SVG 그림도 weasyprint PDF에서는 동일하게 디센더가 잘릴 수 있음**
|
||||||
|
(HTML 보기는 브라우저 렌더라 정상). PDF 품질이 중요한 그림이 생기면 PIL 재렌더 방식 사용.
|
||||||
|
memory(report-doc-toolchain)에 기록함.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# 전체 흐름 다이어그램 작성 (보고서 삽입용)
|
||||||
|
|
||||||
|
**소요 시간**: 약 10분
|
||||||
|
**Context 사용량**: input ~180k / output ~6k tokens
|
||||||
|
|
||||||
|
## 요청
|
||||||
|
|
||||||
|
- 스테이션기반 플레이어의 **전체 흐름을 다이어그램**으로 표현 (사용자 보고서 삽입용).
|
||||||
|
|
||||||
|
## 산출물
|
||||||
|
|
||||||
|
- `docs/그림_전체흐름도.png` (2400×1100, 고해상도) / `docs/그림_전체흐름도.pdf` (압축)
|
||||||
|
- `scripts/fig_전체흐름도.py` — **재생성 스크립트** (문구·색 수정 후 `python3 scripts/fig_전체흐름도.py` 재실행)
|
||||||
|
|
||||||
|
## 다이어그램 구성 (좌→우 4단)
|
||||||
|
|
||||||
|
1. **입력 — 데이터 폴더 1개** (회색 그룹): 영상(mp4) / 드론 비행로그(csv, 위치·자세각·초점) / 측점(csv, 실측 정표고) / POI·구조물(kmz) + 노선 보정(json·선택)
|
||||||
|
2. **① 자동 파싱** (파랑): 인코딩 자동감지 · KMZ 해제
|
||||||
|
3. **② 좌표·측점 계산** (초록 2박스): 시간↔측점 대응표 생성 / 좌표 투영 계산(드론 위치·자세각 → 화면 픽셀)
|
||||||
|
4. **③ 화면 출력** (황금 3박스): 측점 스테이션바 / 영상 AR 오버레이 / 대용량 영상 재생
|
||||||
|
- 하단 엘보 경로: "영상 원본은 계산 없이 곧바로 스트리밍 재생" (계산 우회 표현)
|
||||||
|
|
||||||
|
## 기술 방식
|
||||||
|
|
||||||
|
- 직전 확립한 **PIL 직접 드로잉** 파이프라인 (weasyprint SVG 디센더 잘림 회피). 폰트 자동 축소(fit) 헬퍼로 박스 폭 초과 방지.
|
||||||
|
- 용어는 최근 정정 사항 반영: "스테이션기반", "드론 촬영 철도시설물 영상", "자세각", "시간↔측점 대응표".
|
||||||
|
|
||||||
|
## 검증
|
||||||
|
|
||||||
|
- PNG 육안 검증: 글자 잘림 없음, 배치·화살표 정상 ✅
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# 시간↔측점 대응표 그림 — 단계별 소스 파일 매칭
|
||||||
|
|
||||||
|
**소요 시간**: 5분
|
||||||
|
**Context 사용량**: input 60k / output 4k tokens (추정)
|
||||||
|
|
||||||
|
## 질문
|
||||||
|
|
||||||
|
`docs/그림_시간측점대응표.png` 그림의 4개 단계(영상 프레임 → 프레임÷fps=시간 / GPS→측점 투영 → 시간↔측점 대응표)에 해당하는 실제 소스 파일이 무엇인지.
|
||||||
|
|
||||||
|
## 답변 요약
|
||||||
|
|
||||||
|
| 그림 단계 | 파일 |
|
||||||
|
|-----------|------|
|
||||||
|
| ① 영상 프레임 + 드론 GPS 기록 (데이터 읽기) | `client/src/utils/geoData.ts` (CSV 파싱), `client/src/store/geoStore.ts` (보관), `client/src/types/geo.ts` (타입) |
|
||||||
|
| ② 프레임번호 ÷ fps = 시간 | `client/src/utils/timecode.ts` (`frameToSeconds`), `client/src/stationbar/StationBar.tsx` (videoFps 유도 + `f.frame / videoFps`), `client/src/App.tsx:161` (`handleSeek(frame / fps)`) |
|
||||||
|
| ③ GPS → 측점 (선로에 투영) | `client/src/stationbar/StationBar.tsx` (`projectChainage`, `stationLine`), `client/src/components/overlay/RoutePanel.tsx` (동일 계산), `client/src/utils/chainage.ts` (공유 유틸), 서버측 `server/src/services/geoMatch.ts` |
|
||||||
|
| ④ 시간 ↔ 측점 대응표 | `client/src/stationbar/StationBar.tsx` (ViewedPoint[] precompute = 대응표 그 자체), `client/src/components/overlay/StationOverlay.tsx` (시간→측점 표시), `client/src/components/geo/GeoSearch.tsx` (측점→시간 검색 이동) |
|
||||||
|
|
||||||
|
## 참고
|
||||||
|
|
||||||
|
- `utils/chainage.ts` 는 공유 유틸로 존재하나 현재 import 하는 곳이 없음 — StationBar/RoutePanel 이 각자 동일 로직을 내장하고 있음 (통합 여지 있음).
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# 발표자료 HTML→md 변환 요청 — 확인 결과
|
||||||
|
|
||||||
|
**소요 시간**: 3분
|
||||||
|
**Context 사용량**: input 70k / output 2k tokens (추정)
|
||||||
|
|
||||||
|
## 요청
|
||||||
|
|
||||||
|
`docs/발표_GhiVideo_좌표투영과_측점기반재생.html` 을 md 로도 만들어 달라.
|
||||||
|
|
||||||
|
## 확인 결과 (변환 불필요)
|
||||||
|
|
||||||
|
- `docs/발표_GhiVideo_좌표투영과_측점기반재생.md` 가 **이미 존재** (13:20 수정, 233줄).
|
||||||
|
- HTML(13:21)·PDF(13:21)는 이 md 를 원본으로 `scripts/md2docs.sh`(pandoc) 로 생성된 산출물 — 즉 **md 가 원본**이고 html 이 사본.
|
||||||
|
- 내용 대조: 제목·섹션("한눈에 — GhiVideo는 뭐가 특별한가요?", "시간 ↔ 측점 대응표") 모두 일치 확인 → 새로 만들 것 없음.
|
||||||
|
|
||||||
|
## 참고
|
||||||
|
|
||||||
|
- 해당 md 서두에 "드론 주행영상 플레이어" 표현이 남아 있음 — 용어 정정 방침('주행영상' 아님, 항공 영상/스테이션기반 플레이어)과 어긋나므로 수정 후보.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# 시간↔측점 대응표 '상세판' 다이어그램 — 측량 데이터 분리 + 투영 믹싱 상세화
|
||||||
|
|
||||||
|
**소요 시간**: 약 15분
|
||||||
|
**Context 사용량**: input ~200k / output ~7k tokens
|
||||||
|
|
||||||
|
## 요청
|
||||||
|
|
||||||
|
- 기존 대응표 그림에서 **철도 노선 측량 데이터를 별도 박스로 분리**하고,
|
||||||
|
**GPS와 믹싱되는 부분(선로 수직 투영)을 더 구체적·상세하게** 표현.
|
||||||
|
|
||||||
|
## 산출물
|
||||||
|
|
||||||
|
- `docs/그림_시간측점대응표_상세.png` (2200×1420) / `.pdf` (압축)
|
||||||
|
- `scripts/fig_대응표_상세.py` — 재생성 스크립트
|
||||||
|
- 기존 단순판(`그림_시간측점대응표.*`)은 그대로 유지 (용도별 선택 사용)
|
||||||
|
|
||||||
|
## 구성
|
||||||
|
|
||||||
|
- **소스 2종 분리**: 회색 "영상 프레임(번호+GPS)" [드론이 촬영하며 기록] vs 청록 "철도 노선 측량 데이터(측점 목록·01)측점.csv)" [드론과 무관한 측량 성과·별도 입력] — 출처가 다름을 태그·색으로 명시
|
||||||
|
- **믹싱 패널(초록)**: "드론 GPS를 선로에 수직 투영해 측점값 산출"
|
||||||
|
- 단계 3개: ① 측점들을 이어 선로 기준선 생성 ② GPS 점을 기준선에 수직 투영 ③ 발끝점의 이정 = 측점값
|
||||||
|
- 미니 일러스트: 선로(측점 160k000·160k200 점) + 드론 GPS(파랑) + 빨간 점선 수직 투영 + 발끝점(160k130)
|
||||||
|
- 좌측 파랑(시간 계산) + 하단 황금(대응표 완성)은 기존 구성 유지
|
||||||
|
|
||||||
|
## 기술/검증
|
||||||
|
|
||||||
|
- PIL 직접 드로잉(디센더 잘림 회피), 점선 헬퍼(dash)·폰트 자동축소(fit) 추가.
|
||||||
|
- 1차 렌더 검토에서 라벨 2곳 겹침 발견·수정(GPS 라벨 화살표 겹침, 패널 하단 캡션 테두리 초과) 후 재렌더 육안 확인 ✅
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# 플레이어 비교 그림(시간 축 vs 측점 축) — 선명한 단독 이미지로 재렌더
|
||||||
|
|
||||||
|
**소요 시간**: 10분
|
||||||
|
**Context 사용량**: input 90k / output 4k tokens (추정)
|
||||||
|
|
||||||
|
## 요청
|
||||||
|
|
||||||
|
`쉬운설명_프레임기반에서_측점기반_플레이어.md` 1절의 "① 보통 플레이어(시간 축) vs ② 우리 플레이어(측점 축)" 그림을
|
||||||
|
**더 선명하게, 글자 안 잘리게** 단독 이미지로 재생성.
|
||||||
|
|
||||||
|
## 작업
|
||||||
|
|
||||||
|
- 원본: 해당 md 인라인 SVG (720×250). 기존 렌더(weasyprint)는 텍스트 디센더 잘림 이슈 있음
|
||||||
|
(2026-07-03_1330 히스토리와 동일 원인).
|
||||||
|
- `scripts/fig_플레이어비교.py` 신규 작성 — **PIL 직접 드로잉, 3배 해상도(2160×760)**.
|
||||||
|
`fig_전체흐름도.py` 패턴 재사용 (malgun/malgunbd + anchor='mm', fitz PDF 압축 삽입).
|
||||||
|
- 이모지(😵)는 렌더 가능한 폰트가 없어 캡션에서 제외 (텍스트만 유지).
|
||||||
|
|
||||||
|
## 산출물
|
||||||
|
|
||||||
|
- `docs/그림_플레이어비교.png` (2160×760, 43KB)
|
||||||
|
- `docs/그림_플레이어비교.pdf` (37KB, deflate 압축)
|
||||||
|
|
||||||
|
## 검증
|
||||||
|
|
||||||
|
- 확대 육안 검증: "160k" 배지 원 내부 완전 수용 ✅, "회덕터널"·"대전조차장" 받침 온전 ✅,
|
||||||
|
시간 라벨(0:00/03:12/09:28)·캡션 잘림 없음 ✅.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# 좌표 투영(POI 영상 표출) 상세 도식 작성
|
||||||
|
|
||||||
|
**소요 시간**: 약 15분
|
||||||
|
**Context 사용량**: input ~215k / output ~7k tokens
|
||||||
|
|
||||||
|
## 요청
|
||||||
|
|
||||||
|
- "카메라 영상에 POI를 정확한 위치에 표출하는 기술"을 대응표 상세판과 같은 스타일로 도식화.
|
||||||
|
|
||||||
|
## 산출물
|
||||||
|
|
||||||
|
- `docs/그림_좌표투영_상세.png` (2200×1520) / `.pdf` (압축)
|
||||||
|
- `scripts/fig_투영_상세.py` — 재생성 스크립트
|
||||||
|
|
||||||
|
## 구성 (대응표 상세판과 짝을 이루는 시리즈)
|
||||||
|
|
||||||
|
- **소스 3종**: 회색 "드론 비행로그(csv) — 위치·자세각·초점거리" / 청록 "POI·구조물(kmz) — 명칭·위경도(높이 없음)" / 청록 "측점(csv) — 실측 표고(높이 보완용)"
|
||||||
|
- **투영 패널(초록)**: "3차원 위치를 그 순간 카메라가 보는 화면의 픽셀로 변환"
|
||||||
|
- 단계 4: ① POI 높이 보완(최근접 측점 표고, DEM 불필요) ② 좌표 변환(드론 기준 상대위치) ③ 시선 회전(자세각) ④ 핀홀 투영(중앙+(옆÷앞)×(초점÷센서))
|
||||||
|
- 미니 일러스트: 드론 카메라 + 화면(이미지 평면) + 교량 POI + 주황 점선 광선 → '맺힌 점 = 화면 속 위치'
|
||||||
|
- **출력(황금)**: "영상 속 정확한 자리에 POI 표출!" — 매 프레임 재계산(60fps)
|
||||||
|
- 각주: 어긋나면 라벨 드래그 1회 보정(역투영·자동 저장)
|
||||||
|
|
||||||
|
## 검증
|
||||||
|
|
||||||
|
- 1차 렌더에서 좌측 화살표가 라벨('초점거리')을 관통 → 라벨 이동 후 재렌더, 크롭 확대로 확인 ✅
|
||||||
|
- 실제 코드 순서(geoProjection.ts: 표고 datum → ENU → 회전 → 핀홀) 반영.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# 2026-07-03 발끝점 용어 설명 + 특허 초안 요청 확인
|
||||||
|
|
||||||
|
**소요 시간**: 5분
|
||||||
|
**Context 사용량**: input 45k / output 3k tokens (추정)
|
||||||
|
|
||||||
|
## 작업 내용
|
||||||
|
|
||||||
|
### 1. "발끝점" 용어 설명
|
||||||
|
- 사용자 질문: 발표 그림에 나온 "발끝점"이 무엇인지
|
||||||
|
- 답변: 드론 GPS 위치에서 선로 기준선으로 수직으로 내린 선이 선로와 만나는 점(수학 용어 '수선의 발'). 이 점의 이정을 읽어 측점값을 산출.
|
||||||
|
- 출처: `scripts/fig_대응표_상세.py` 79~103행, `docs/history/2026-07-03_1400_대응표상세판-측량데이터분리-투영믹싱상세.md`
|
||||||
|
- 참고: 공식 문서에는 '수직 투영점'/'수선의 발' 표기가 더 정확할 수 있음을 안내
|
||||||
|
|
||||||
|
### 2. 특허 초안 작성 요청 → 기존 문서 확인
|
||||||
|
- 요청: "시간→스테이션 매핑 + GPS POI 영상 맵핑 플레이어의 특허 초안 작성"
|
||||||
|
- 확인 결과: **동일 내용의 초안이 이미 존재** — `docs/특허_GhiVideo_명세서초안_좌표투영_측점기반재생.md` (2026-07-02 18:27 작성)
|
||||||
|
- 제1발명(좌표투영): GPS 위치 POI를 영상 화면 픽셀에 정합·오버레이 (청구항 7개)
|
||||||
|
- 제2발명(측점기반재생): 시간→측점 매핑 기반 탐색·재생 (청구항 8개)
|
||||||
|
- md/html/pdf 3종 동기화 상태 확인 (html·pdf가 md보다 최신)
|
||||||
|
- 부록 A에 인용된 소스 4개 파일(geoProjection.ts, chainage.ts, StationOverlay.tsx, StationBar.tsx) 실존 확인
|
||||||
|
- 신규 작성 없이 기존 문서 안내로 종료. 수정/보강 요청 시 후속 작업 예정
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# 전체흐름도 검토 — '초점' → '초점거리' 용어 통일
|
||||||
|
|
||||||
|
**소요 시간**: 약 5분
|
||||||
|
**Context 사용량**: input ~230k / output ~2k tokens
|
||||||
|
|
||||||
|
## 요청
|
||||||
|
|
||||||
|
- 보고서에 삽입한 전체흐름도(그림_전체흐름도)에 수정할 것이 있는지 검토.
|
||||||
|
|
||||||
|
## 검토 결과
|
||||||
|
|
||||||
|
- 구조·화살표·용어(스테이션기반 정정 사항 포함) 전반 이상 없음.
|
||||||
|
- 1건 수정: 드론 비행로그 박스 부제 "프레임별 위치·자세각·**초점**" → "**초점거리**"
|
||||||
|
(상세 도식 2장·보고서 문단과 용어 통일). `scripts/fig_전체흐름도.py` 수정 후 재렌더, 크롭 확인 ✅
|
||||||
|
- 참고(수정 불요로 판단): 하단 우회 화살표가 입력 그룹 전체에서 출발하는 것처럼 보이나,
|
||||||
|
라벨("영상 원본은 계산 없이 곧바로 스트리밍 재생")이 대상을 명시하므로 그대로 둠.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# 특허 문서용 동작 원리 1·2 서술 작성
|
||||||
|
|
||||||
|
**소요 시간**: 약 15분
|
||||||
|
**Context 사용량**: input ~245k / output ~8k tokens
|
||||||
|
|
||||||
|
## 요청
|
||||||
|
|
||||||
|
- 사용자 보고서(`D:\_project\20260612_abcvideo\docs\스테이션기반 플레이어_260703.html`)를 참고하여,
|
||||||
|
동작 원리 1(시간을 스테이션으로 표출)·2(POI 영상 맵핑)를 **특허 문서용**으로 작성.
|
||||||
|
|
||||||
|
## 진행
|
||||||
|
|
||||||
|
1. `/mnt/d/...` 경유로 보고서 HTML 텍스트 추출 — 구조 확인 (□ 프로그램 소개 / 필요 데이터 / 동작 원리 1·2 / 전체 흐름도 / 특허)
|
||||||
|
2. 기존 `특허_GhiVideo_명세서초안_좌표투영_측점기반재생.md` 목차·청구항 문체 확인
|
||||||
|
- 대응 관계: 동작 원리 1 ↔ 초안 제2발명(측점 기반 재생), 동작 원리 2 ↔ 초안 제1발명(정합 표출)
|
||||||
|
3. 특허 명세서 문체(기술 요지 → 단계 (a)~(f) → 종속항 후보)로 재서술
|
||||||
|
|
||||||
|
## 산출물
|
||||||
|
|
||||||
|
- `docs/특허서술_동작원리1_2.md` (+ .html / .pdf)
|
||||||
|
- 구성: 동작 원리별 [기술 요지 / 방법 단계 (a)~(f) / 특징적 구성(종속항 후보)] + 결합 관계 출원 전략 메모
|
||||||
|
- 포함한 특허 포인트: 수직 투영 측점 산출, 데이터 기반 fps 산출, 이동거리 비례 탐색축,
|
||||||
|
측점 실측 표고 + 지오이드 datum 정합(DEM-free), 사거리 유지 단일 드래그 역투영, 화각 1점 보정, 속도적응 평활
|
||||||
|
|
||||||
|
## 참고
|
||||||
|
|
||||||
|
- 변리사 검토 전 초안임을 문서 서두에 명시. 세부 수식·도면은 기존 명세서 초안 참조로 연결.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# 2026-07-03 특허 출원 초안 신규 작성 (그림 4장 포함)
|
||||||
|
|
||||||
|
**소요 시간**: 20분
|
||||||
|
**Context 사용량**: input 90k / output 15k tokens (추정)
|
||||||
|
|
||||||
|
## 요청
|
||||||
|
- 기존 특허 초안과 별개로 **새로 작성**하되, **이미지와 같이 정리**할 것
|
||||||
|
- 참고 문서: `D:\_project\20260612_abcvideo\docs\스테이션기반 플레이어_260703.html` (HWP 내보내기, 사내 보고서)
|
||||||
|
- 구성: 프로그램 소개 → 필요 데이터 → 동작 원리 1(시간→스테이션) → 동작 원리 2(POI 맵핑) → 전체 흐름도 → **□ 특허(빈 절)**
|
||||||
|
- 그림 4장(hd1~hd4) 포함
|
||||||
|
|
||||||
|
## 산출물
|
||||||
|
1. **`scripts/fig_POI투영_상세.py`** (신규) — 참고 문서의 투영 그림(hd3)을 PIL 고해상도(2200×1520)로 재작성
|
||||||
|
→ `docs/그림_POI투영_상세.png` / `.pdf` 생성 (소스 3종 → 투영 4단계 ①높이보완 ②좌표변환 ③시선회전 ④핀홀투영 + 미니 일러스트)
|
||||||
|
2. **`docs/특허_GhiVideo_출원초안_스테이션재생_POI맵핑.md`** (신규) + `.html` + `.pdf` (16페이지, md2docs.sh 변환)
|
||||||
|
- 참고 문서의 순서·용어를 따름: 발명 1 = 측점(스테이션) 기반 재생, 발명 2 = POI 영상 정합 표시
|
||||||
|
- 그림 4장 삽입: 그림_플레이어비교 / 그림_시간측점대응표_상세 / 그림_POI투영_상세(신규) / 그림_전체흐름도
|
||||||
|
- 보고서 "□ 특허" 절에 붙여넣기 좋은 **"2. 특허 요지 (한눈 요약)"** 표 포함
|
||||||
|
- 기존 초안 대비 추가된 청구 포인트: **POI 높이 보완(최근접 측점 실측 표고, DEM 불필요)**, **드래그 역투영 좌표 보정** — 코드 확인함 (geoProjection.ts 역투영·표고 역산, StationOverlay.tsx 드래그 처리)
|
||||||
|
- 용어 준수: '스테이션기반 플레이어', '항공영상'(주행영상 아님 — 메모리 ghivideo-video-terminology)
|
||||||
|
|
||||||
|
## 참고
|
||||||
|
- 기존 초안 `docs/특허_GhiVideo_명세서초안_좌표투영_측점기반재생.md`(7/2 작성)는 그대로 유지 (SVG 개념도 버전)
|
||||||
|
- md 내 이미지 상대경로 때문에 변환은 docs 디렉토리에서 실행해야 함: `cd docs && ../scripts/md2docs.sh <파일.md>`
|
||||||
|
- 출원 시 그림은 흑백 선도로 재작성 필요(부록 B에 명기)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# 특허서술 문서에 도면(도 1·도 2) 삽입
|
||||||
|
|
||||||
|
**소요 시간**: 약 10분
|
||||||
|
**Context 사용량**: input ~260k / output ~4k tokens
|
||||||
|
|
||||||
|
## 요청
|
||||||
|
|
||||||
|
- 특허서술_동작원리1_2 문서에 동작 원리별 그림을 각각 삽입.
|
||||||
|
|
||||||
|
## 변경
|
||||||
|
|
||||||
|
- `docs/특허서술_동작원리1_2.md`
|
||||||
|
- 동작 원리 1 기술 요지 뒤: **도 1** = 그림_시간측점대응표_상세.png (+본문 "(도 1 참조)")
|
||||||
|
- 동작 원리 2 기술 요지 뒤: **도 2** = 그림_좌표투영_상세.png (+본문 "(도 2 참조)")
|
||||||
|
- `<figure><img style="width:100%">` + 특허식 figcaption("도 N. …") 형식, 블록 내 빈 줄 없음(pandoc 규칙 준수)
|
||||||
|
|
||||||
|
## 이슈/해결
|
||||||
|
|
||||||
|
- 저장소 루트에서 md2docs.sh 실행 시 pandoc `--self-contained`가 **이미지를 cwd 기준으로 탐색**해 변환 실패(HTML 미갱신).
|
||||||
|
→ **docs 디렉토리에서 실행**하면 정상. (md2docs.sh는 상대 이미지가 있는 md의 경우 md가 있는 폴더에서 실행할 것)
|
||||||
|
|
||||||
|
## 검증
|
||||||
|
|
||||||
|
- HTML에 base64 이미지 2장 임베드 확인(415KB), PDF 4쪽(354KB) 생성.
|
||||||
|
- PDF 페이지 렌더 육안 검증: p1 도 1, p3 도 2 배치·캡션·본문 흐름 정상 ✅
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# AI Video Brief — 온라인 영상 음성다운로드·AI요약 서비스 신규 구축
|
||||||
|
|
||||||
|
**소요 시간**: 35분
|
||||||
|
**Context 사용량**: input 500k / output 20k tokens (추정값)
|
||||||
|
|
||||||
|
## 요청 내용
|
||||||
|
|
||||||
|
유튜브 등 온라인 영상 URL을 입력하면 ① 음성(mp3) 다운로드 ② 내용 AI 요약을 해주는 서비스 구축.
|
||||||
|
|
||||||
|
## 사용자 결정 사항
|
||||||
|
|
||||||
|
- 위치: 별도 새 프로젝트 `/home/hanmac/projects/gitea/b23042/ai_video_brief`
|
||||||
|
- 텍스트 확보: **자막 우선, 없으면 Whisper 음성인식 대체**
|
||||||
|
- 요약 AI: **로컬 무료 모델 (Ollama)** — 유료 API 대신
|
||||||
|
- 형태: 웹 화면
|
||||||
|
|
||||||
|
## 구축 내용
|
||||||
|
|
||||||
|
### 스택
|
||||||
|
- Python 3.10 venv (`/usr/bin/python3.10` — anaconda 3.8은 구버전이라 회피)
|
||||||
|
- FastAPI + uvicorn (포트 **55100**, GhiVideo 55000과 충돌 없음)
|
||||||
|
- yt-dlp 2026.7.4 (다운로드·자막), faster-whisper 1.2.1 (small/CPU/int8)
|
||||||
|
- Ollama v0.31.1 + **exaone3.5:7.8b** (LG 한국어 특화 모델, 4.8GB)
|
||||||
|
|
||||||
|
### 파이프라인
|
||||||
|
1. `download`: yt-dlp → bestaudio → ffmpeg mp3 변환 → 다운로드 링크
|
||||||
|
2. `summarize`: 자막(수동→자동, ko→en) 탐색 → 없으면 mp3 다운로드 후 Whisper 받아쓰기 → 6,000자 초과 시 부분요약→최종요약(map-reduce) → EXAONE 한국어 요약
|
||||||
|
3. 작업은 백그라운드 스레드 + 웹에서 1.5초 폴링으로 진행률 표시
|
||||||
|
|
||||||
|
### 특이사항 (다음 에이전트 참고)
|
||||||
|
- **sudo 사용 불가** (비밀번호 필요) → python3.10-venv apt 설치 실패 → `pip install --user virtualenv`로 우회
|
||||||
|
- Ollama 최신 릴리스는 `.tgz`가 아닌 **`.tar.zst`** 형식 → anaconda의 zstd로 해제, `~/ollama/bin/ollama`에 수동 설치 (systemd 서비스 없음, run.sh가 자동 기동)
|
||||||
|
- GPU 없음 → Whisper·Ollama 모두 CPU 동작 (RAM 31GB로 충분)
|
||||||
|
|
||||||
|
## 테스트 결과
|
||||||
|
|
||||||
|
- ✅ mp3 다운로드: 19초 영상 → 0.4MB mp3 정상 생성
|
||||||
|
- ✅ Whisper 받아쓰기: 19초 영상 정확히 전사
|
||||||
|
- ✅ 요약 전 과정: 자막 발견 → EXAONE 한국어 요약 생성 (약 10초 소요)
|
||||||
|
- ✅ 웹 화면 http://localhost:55100 정상 서빙
|
||||||
|
|
||||||
|
## 실행 방법
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/hanmac/projects/gitea/b23042/ai_video_brief && ./run.sh
|
||||||
|
# 브라우저: http://localhost:55100
|
||||||
|
```
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# AI Video Brief — 요약 보관함(좌측 목록 + DB 저장) 추가
|
||||||
|
|
||||||
|
**소요 시간**: 20분
|
||||||
|
**Context 사용량**: input 550k / output 30k tokens (추정값)
|
||||||
|
|
||||||
|
## 요청 내용
|
||||||
|
|
||||||
|
왼쪽에 요약된 영상의 제목 리스트를 보여주고, 클릭 시 해당 요약본을 본문에 표시.
|
||||||
|
요약 결과는 DB/파일에 저장해서, 이미 요약된 항목은 로컬 저장본을 불러와 표시.
|
||||||
|
|
||||||
|
## 구현 내용 (`ai_video_brief` 프로젝트)
|
||||||
|
|
||||||
|
### 백엔드
|
||||||
|
- `app/db.py` 신규 — SQLite 보관소 (`storage/summaries.db`, 파이썬 내장 sqlite3, 추가 의존성 없음)
|
||||||
|
- summaries 테이블: id, video_id, url, title, duration, source, summary, transcript, created_at
|
||||||
|
- `app/main.py`
|
||||||
|
- 요약 파이프라인 시작 시 `video_id`(없으면 URL)로 저장본 조회 → 있으면 **계산 없이 즉시 반환** (`result.cached=true`)
|
||||||
|
- 요약 완료 시 DB에 자동 저장 (받아쓴 원문 포함)
|
||||||
|
- 신규 API: `GET /api/summaries`(목록), `GET /api/summaries/{id}`(상세), `GET /api/summaries/{id}/transcript`(원문), `DELETE /api/summaries/{id}`(삭제)
|
||||||
|
- `POST /api/jobs`에 `force` 옵션 — 저장본 무시하고 재요약
|
||||||
|
|
||||||
|
### 웹 화면 (2단 레이아웃으로 개편)
|
||||||
|
- 좌측 사이드바: 📚 요약 보관함 — 제목/날짜/길이/출처 목록, 클릭 시 본문에 표시
|
||||||
|
- 우측 본문: URL 입력 + 진행 중 작업 + 선택된 요약 상세
|
||||||
|
- 상세 화면 버튼: 📋 복사 / 📄 원문 보기 / 🔄 다시 요약(force) / 🗑 삭제
|
||||||
|
- 요약 작업 완료 시 목록 자동 갱신 + 해당 요약 자동 표시, 저장본 사용 시 "💾 저장본에서 불러옴" 배지
|
||||||
|
|
||||||
|
## 테스트 결과
|
||||||
|
|
||||||
|
- ✅ 요약 → DB 저장 → 목록 노출 확인
|
||||||
|
- ✅ 같은 영상 재요청 → 2초 내 저장본 반환 (`cached: True`)
|
||||||
|
- ✅ 상세/원문/삭제 API 정상
|
||||||
|
- ✅ 새 2단 화면 정상 서빙
|
||||||
|
|
||||||
|
## 다음 에이전트 참고
|
||||||
|
|
||||||
|
- `pkill -f "uvicorn app.main"`을 서버 재시작 명령과 **한 줄에 쓰면 안 됨** — 명령 문자열 자체가 패턴에 걸려 자기 셸이 죽는다 (exit 144). 별도 호출로 분리할 것.
|
||||||
|
- 서버 재시작: 구 프로세스 종료 후 `cd ai_video_brief && ./run.sh` 또는 nohup uvicorn.
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# AI Video Brief — PWA(폰 설치형 앱) + 영상(mp4) 다운로드 기능 추가
|
||||||
|
|
||||||
|
**소요 시간**: 25분
|
||||||
|
**Context 사용량**: input 600k / output 40k tokens (추정값)
|
||||||
|
|
||||||
|
## 요청 내용
|
||||||
|
|
||||||
|
1. 안드로이드 앱으로 만들기 → 추천안(PWA 설치형 웹앱 + 네트워크 설정) 승인받아 진행
|
||||||
|
2. (작업 중 추가 요청) 영상 자체 다운로드 기능도 추가
|
||||||
|
|
||||||
|
## 구현 내용 (`ai_video_brief` 프로젝트)
|
||||||
|
|
||||||
|
### PWA (폰 홈 화면 설치형 웹앱)
|
||||||
|
- 앱 아이콘 3종 PIL로 생성 (192/512/maskable-512, 파랑→보라 그라데이션 + 재생버튼 도안)
|
||||||
|
- `manifest.json` (standalone, 한국어, short_name "영상요약")
|
||||||
|
- `sw.js` 서비스워커 — 화면은 네트워크 우선+캐시 폴백, `/api/`는 캐시 안 함
|
||||||
|
- FastAPI에 `/manifest.json`, `/sw.js` 루트 라우트 추가 (SW 범위 확보), `/static` 마운트
|
||||||
|
- index.html: manifest/theme-color/아이콘 연결, SW 등록, `beforeinstallprompt` 시 "📲 앱으로 설치" 버튼 표시
|
||||||
|
- 모바일 CSS: 폰에서 입력창이 위·보관함이 아래(order), 버튼 확대
|
||||||
|
|
||||||
|
### 폰 접속 설정
|
||||||
|
- `windows-setup/포트열기.ps1` — 관리자 PowerShell에서 1회 실행:
|
||||||
|
WSL IP 자동 감지 → netsh portproxy(55100) + 방화벽 규칙 + 폰 접속 주소 출력
|
||||||
|
- WSL2 IP는 재부팅마다 바뀌므로 재부팅 후 접속 안 되면 스크립트 재실행 (스크립트가 자동 처리)
|
||||||
|
- README에 단계별 안내 추가: ① 포트 열기 ② 홈 화면 추가 ③ (선택) Tailscale — https 확보 시 완전한 standalone 설치 + 집 밖 접속
|
||||||
|
|
||||||
|
### 영상(mp4) 다운로드
|
||||||
|
- `config.py`: `VIDEO_DIR`, `VIDEO_MAX_HEIGHT`(기본 1080)
|
||||||
|
- `downloader.download_video()`: bestvideo(mp4)+bestaudio(m4a) → ffmpeg 병합 mp4
|
||||||
|
- `main.py`: task `"video"` 파이프라인 추가, `/api/download/{id}`가 mp3/mp4 모두 서빙
|
||||||
|
- 웹 화면: 🎬 영상 다운로드 버튼(청록색) 추가, 작업명/파일 라벨 맵 처리
|
||||||
|
|
||||||
|
## 테스트 결과
|
||||||
|
|
||||||
|
- ✅ manifest/sw.js/아이콘 모두 200 + 올바른 content-type
|
||||||
|
- ✅ 서버 재시작 후 보관함 데이터(SQLite) 유지 확인
|
||||||
|
- ✅ 영상 다운로드: 테스트 영상 → mp4 0.5MB 저장, `/api/download` 200 video/mp4
|
||||||
|
|
||||||
|
## 다음 에이전트 참고
|
||||||
|
|
||||||
|
- **PWA 설치의 한계**: `http://LAN IP` 접속은 보안 컨텍스트가 아니라 Chrome이 "바로가기" 형태로만 추가됨 (주소창 보임). 완전한 standalone 설치는 https 필요 → README의 Tailscale 3단계 (`tailscale serve`) 안내 참조.
|
||||||
|
- 윈도우 쪽 명령(관리자 netsh/방화벽)은 에이전트가 실행 불가 → 사용자가 `포트열기.ps1` 직접 실행해야 함.
|
||||||
|
- `pkill -f "uvicorn app[.]main"` 은 반드시 서버 시작 명령과 **별도 Bash 호출**로 (같은 호출에 start 명령이 있으면 그 문자열에 패턴이 걸려 자기 셸이 죽음, exit 144 재발함).
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# AI Video Brief — 작업 시작 후 주소 입력창 자동 삭제 해제
|
||||||
|
|
||||||
|
**소요 시간**: 2분
|
||||||
|
**Context 사용량**: input 620k / output 42k tokens (추정값)
|
||||||
|
|
||||||
|
## 요청 내용
|
||||||
|
|
||||||
|
영상/음성/요약 버튼을 눌러도 입력한 주소가 지워지지 않게.
|
||||||
|
|
||||||
|
## 수정 내용
|
||||||
|
|
||||||
|
- `ai_video_brief/app/static/index.html` — `submitJob()`에서 작업 시작 후 `$('url').value = ''` 로
|
||||||
|
입력창을 비우던 한 줄 제거. 같은 주소로 영상→음성→요약을 연달아 실행하기 편해짐.
|
||||||
|
- 서버는 index.html을 매 요청마다 읽으므로 재시작 불필요 — 새로고침만으로 반영 확인 완료.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# AI Video Brief — 자막 다운로드 429 오류 시 요약 실패 문제 수정
|
||||||
|
|
||||||
|
**소요 시간**: 6분
|
||||||
|
**Context 사용량**: input 650k / output 45k tokens (추정값)
|
||||||
|
|
||||||
|
## 문제
|
||||||
|
|
||||||
|
요약 버튼 클릭 시 `ERROR: Unable to download video subtitles for 'en': HTTP Error 429: Too Many Requests` 로 작업 전체가 실패.
|
||||||
|
|
||||||
|
- 원인 1: 유튜브가 자막을 자주 요청하는 IP를 일시 차단(429). 기존 코드는 언어 4개(ko/ko-KR/en/en-US)의 수동+자동 자막을 한꺼번에 요청해서 차단에 걸리기 쉬웠음.
|
||||||
|
- 원인 2: 자막 다운로드 예외가 그대로 전파되어, Whisper 음성인식 폴백이 있는데도 작업이 죽음.
|
||||||
|
|
||||||
|
## 수정 (`ai_video_brief`)
|
||||||
|
|
||||||
|
`app/services/downloader.py` — `fetch_subtitles()` 재작성:
|
||||||
|
1. 자막 **목록만 먼저 조회**(다운로드 아님) → 우선순위에 맞는 언어 **딱 1개만** 선택해 다운로드 (요청 수 최소화, `sleep_interval_subtitles: 1` 추가)
|
||||||
|
2. 함수 전체를 try/except로 감싸 **어떤 오류든 None 반환** → 호출부(main.py)가 자동으로 Whisper 음성인식으로 진행
|
||||||
|
3. 수동 자막 우선, 없으면 자동 생성 자막 (기존 우선순위 유지)
|
||||||
|
|
||||||
|
`app/main.py` — 진행 메시지를 "자막을 못 가져와 소리를 내려받는 중 (음성인식으로 진행)"으로 명확화.
|
||||||
|
|
||||||
|
## 테스트
|
||||||
|
|
||||||
|
- ✅ 강제 재요약(force) 전 과정 통과: 자막 1개 선택 다운로드 → 요약 완료 (16초)
|
||||||
|
- ✅ 예외 시 None 폴백 경로는 코드 보장 (Whisper 경로는 이전 세션에서 검증 완료)
|
||||||
|
|
||||||
|
## 다음 에이전트 참고
|
||||||
|
|
||||||
|
- 429는 시간이 지나면 자동으로 풀림. 앞으로 자막 실패는 사용자에게 오류로 보이지 않고 음성인식으로 조용히 전환됨 (원문 출처 표시로 구분 가능: "자막" vs "음성인식(Whisper)").
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# AI Video Brief — 좌측 목록 삭제·이름 수정 기능 추가
|
||||||
|
|
||||||
|
**소요 시간**: 5분
|
||||||
|
**Context 사용량**: input 680k / output 48k tokens (추정값)
|
||||||
|
|
||||||
|
## 요청 내용
|
||||||
|
|
||||||
|
좌측 요약 보관함 목록에서 항목 삭제와 목록 이름(제목) 수정 기능.
|
||||||
|
|
||||||
|
## 구현 내용 (`ai_video_brief`)
|
||||||
|
|
||||||
|
### 백엔드
|
||||||
|
- `db.update_title(rec_id, title)` 추가
|
||||||
|
- `PUT /api/summaries/{id}` — `{title}` 로 이름 수정 (빈 제목 400, 없는 항목 404)
|
||||||
|
- 삭제는 기존 `DELETE /api/summaries/{id}` 재사용
|
||||||
|
|
||||||
|
### 웹 화면
|
||||||
|
- 좌측 목록 각 항목 오른쪽에 ✏️(이름 수정)·✕(삭제) 버튼 추가
|
||||||
|
- 항목 본문(li-body)과 버튼(li-actions)을 분리해 버튼 클릭이 항목 선택으로 번지지 않게 stopPropagation 처리
|
||||||
|
- 이름 수정: prompt 창에 현재 제목 미리 채움 → 저장 후 목록·본문 제목 즉시 갱신
|
||||||
|
- 삭제: 확인창에 대상 제목 표시, 선택 중이던 항목이면 본문도 닫음
|
||||||
|
- 본문 상세 화면에도 "✏️ 이름 수정" 버튼 추가 (목록과 동일 동작)
|
||||||
|
|
||||||
|
## 테스트 결과
|
||||||
|
|
||||||
|
- ✅ PUT 이름 수정 → 목록에 반영 확인
|
||||||
|
- ✅ 빈 제목 400 / 없는 항목 404 정상
|
||||||
|
- ✅ 화면에 rename 함수·버튼 렌더 확인, 테스트 후 원래 제목 복원
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# AI Video Brief — "왜 60프레임이 아니냐" 질문 조사 + 화질/프레임 표시 추가
|
||||||
|
|
||||||
|
**소요 시간**: 10분
|
||||||
|
**Context 사용량**: input 720k / output 52k tokens (추정값)
|
||||||
|
|
||||||
|
## 질문
|
||||||
|
|
||||||
|
저장한 동영상이 60fps가 아니라 30fps 정도밖에 안 되는 이유.
|
||||||
|
|
||||||
|
## 조사 결과 (결론: 다운로드 문제 아님, 원본이 24fps)
|
||||||
|
|
||||||
|
- ffprobe로 저장 파일 확인: 사용자가 받은 영상들은 1080p **23.98fps**(AV1), 1920x960 **29.97fps**
|
||||||
|
- 해당 영상("1시간으로 끝내는 DJI 미니 5 프로 사용 설명서", oItten2naxk)의 유튜브 제공 포맷 전수 조사:
|
||||||
|
144p~2160p(4K) **전부 24fps만 존재** — 원본이 24fps로 업로드된 영상
|
||||||
|
- 유튜브는 원본보다 높은 프레임을 생성하지 않으므로 60fps는 불가능. 다운로더는 제공되는 최고 품질(1080p24)을 정상적으로 받았음
|
||||||
|
- 추가 발견: 이 영상은 4K까지 제공되지만 기본 상한 `VIDEO_MAX_HEIGHT=1080`이라 1080p로 받음 (환경변수로 조정 가능)
|
||||||
|
|
||||||
|
## 보완 구현
|
||||||
|
|
||||||
|
1. `downloader.download_video()` — 포맷 선택에서 `ext=mp4` 코덱 제한 제거
|
||||||
|
(`bestvideo[height<=H]+bestaudio` → mp4 통에 병합). vp9만 60fps를 제공하는 옛 영상도 프레임을 놓치지 않게.
|
||||||
|
반환값을 `(경로, {height, fps})` 튜플로 변경.
|
||||||
|
2. `main.py` — `result["quality"]`에 "1080p 24fps" 형식 저장
|
||||||
|
3. 웹 화면 — 다운로드 완료 링크에 실제 받은 화질/프레임 표시: "⬇️ mp4 저장 (240p 15fps · 0.5MB)"
|
||||||
|
|
||||||
|
## 테스트
|
||||||
|
|
||||||
|
- ✅ 테스트 영상 다운로드 → "240p 15fps · 0.5MB" 정확히 표시 (2005년 저화질 영상이라 정상)
|
||||||
|
|
||||||
|
## 다음 에이전트 참고
|
||||||
|
|
||||||
|
- 사용자가 프레임/화질 관련 질문 시: 먼저 `ffprobe`로 저장 파일 확인 → yt-dlp `extract_info`로 유튜브 제공 포맷 목록과 비교. 원본 fps 이상은 어떤 방법으로도 불가능.
|
||||||
|
- 4K로 받고 싶다는 요청이 오면 `VIDEO_MAX_HEIGHT=2160` 환경변수 (run.sh 실행 전 export).
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# AI Video Brief — 화질/음질 선택 다운로드 기능 추가
|
||||||
|
|
||||||
|
**소요 시간**: 9분
|
||||||
|
**Context 사용량**: input 780k / output 58k tokens (추정값)
|
||||||
|
|
||||||
|
## 요청 내용
|
||||||
|
|
||||||
|
다운로드 가능한 영상 해상도들을 항목으로 보여주고 클릭 시 그 화질로 다운로드.
|
||||||
|
음성도 품질 목록을 보여주고 사용자가 선택.
|
||||||
|
|
||||||
|
## 구현 내용 (`ai_video_brief`)
|
||||||
|
|
||||||
|
### 백엔드
|
||||||
|
- `downloader.list_formats(url)` — 유튜브 제공 포맷을 조회해
|
||||||
|
영상: (해상도, fps) 조합별 대표 1개 (video-only 우선 + 코덱 우선순위 av01>vp9>avc1, 예상 크기 포함)
|
||||||
|
음성: 비트레이트별 대표 1개 (kbps, 확장자, 예상 크기)
|
||||||
|
- `GET /api/formats?url=...` — 선택 화면용 목록 API
|
||||||
|
- `POST /api/jobs` 확장: `format_id`(선택 스트림), `video_only`(병합 필요 여부), `quality_label`(표시용)
|
||||||
|
- `download_video`: format_id 지정 시 `{fid}+bestaudio/...` 폴백 체인, 진행형(소리 포함) 스트림은 `{fid}/best`
|
||||||
|
- `download_audio`: format_id 지정 시 해당 스트림 → mp3 변환
|
||||||
|
|
||||||
|
### 웹 화면
|
||||||
|
- 🎬/🎵 버튼 클릭 → 즉시 다운로드 대신 **품질 선택 패널** 표시
|
||||||
|
- "품질 목록을 가져오는 중…" 로딩 → 항목 목록 (최상단 항목에 "추천" 배지)
|
||||||
|
- 영상: "1080p 24fps · av01 · 약 537MB", 음성: "129kbps · m4a · 약 56MB" 형식
|
||||||
|
- 항목 클릭 → 그 품질로 다운로드 시작, 닫기 버튼으로 취소
|
||||||
|
- 완료 후 링크에 선택/실제 품질 표시
|
||||||
|
|
||||||
|
## 테스트 결과
|
||||||
|
|
||||||
|
- ✅ 목록 API: DJI 영상 → 영상 8개(144p~4K, 크기 포함) / 음성 4개 정상
|
||||||
|
- ✅ 영상 선택 다운로드: 144p 15fps 선택 → ffprobe로 정확히 144p/15fps 수신 확인
|
||||||
|
- ✅ 음성 선택 다운로드: 47kbps 선택 → 완료, 품질 라벨 표시 확인
|
||||||
|
|
||||||
|
## 다음 에이전트 참고
|
||||||
|
|
||||||
|
- 4K 등 1080p 초과 화질도 이제 선택 패널에서 받을 수 있음 (`VIDEO_MAX_HEIGHT`는 '자동 최고' 경로에만 적용)
|
||||||
|
- 영상 항목의 예상 크기는 영상 스트림만의 크기 (소리 병합 시 소폭 증가) — "약" 표기로 처리
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# AI Video Brief — "유튜브에 60프레임이 없나?" 질문 검증·답변
|
||||||
|
|
||||||
|
**소요 시간**: 4분
|
||||||
|
**Context 사용량**: input 820k / output 60k tokens (추정값)
|
||||||
|
|
||||||
|
## 질문
|
||||||
|
|
||||||
|
유튜브 영상은 60프레임이 없는지, 왜 선택 목록에 30프레임까지만 보이는지.
|
||||||
|
|
||||||
|
## 검증 (코드 수정 없음)
|
||||||
|
|
||||||
|
- yt-dlp의 유튜브 검색(ytsearch)으로 실제 60fps 영상을 찾음:
|
||||||
|
"Real 4K HDR 60fps: LG Jazz HDR UHD" (mkggXE5e2yk)
|
||||||
|
- 우리 `/api/formats` 로 조회 → 선택 목록에 **2160p 60fps ~ 360p 60fps** 정상 표시 확인
|
||||||
|
|
||||||
|
## 결론 (사용자 안내)
|
||||||
|
|
||||||
|
- 프레임 수는 **영상 제작자가 촬영·업로드할 때 결정**되며, 유튜브는 원본보다 높은 프레임을 만들지 않음
|
||||||
|
- 사용자가 넣었던 영상들(DJI 강의 24fps 등)은 원본 자체가 24~30fps → 목록에 그것만 보인 것
|
||||||
|
- 60fps로 제작된 영상(게임·스포츠·테스트 영상 등)을 넣으면 목록에 60fps 항목이 정상적으로 뜸 (검증 완료)
|
||||||
|
- 시스템 이상 없음, 수정 사항 없음
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# AI Video Brief — 다운로드 파일명을 "컨텐츠명_스펙"으로 변경
|
||||||
|
|
||||||
|
**소요 시간**: 3분
|
||||||
|
**Context 사용량**: input 860k / output 63k tokens (추정값)
|
||||||
|
|
||||||
|
## 요청 내용
|
||||||
|
|
||||||
|
음성/영상 저장 시 파일명을 컨텐츠 이름으로 하고, 이름 끝에 "_" 뒤에 화질/음질 스펙을 붙여서 저장.
|
||||||
|
|
||||||
|
## 구현 내용 (`ai_video_brief`)
|
||||||
|
|
||||||
|
`app/main.py` — `download_file()` (`GET /api/download/{id}`):
|
||||||
|
- 기존: `{안전한제목}{ext}`
|
||||||
|
- 변경: `{안전한제목}_{스펙}{ext}`
|
||||||
|
- 스펙은 `job.result["quality"]`에서 공백 제거 (`"1080p 24fps"` → `1080p24fps`, `"47kbps"`)
|
||||||
|
- 스펙이 없으면 기존처럼 제목만
|
||||||
|
- 제목 안전화 필터에서 불필요한 `한` 문자 제거 (한글은 `isalnum()`으로 이미 통과)
|
||||||
|
|
||||||
|
## 테스트 결과
|
||||||
|
|
||||||
|
- ✅ 영상: `Me at the zoo_144p15fps.mp4`
|
||||||
|
- ✅ 음성: `Me at the zoo_47kbps.mp3`
|
||||||
|
- Content-Disposition의 `filename*=utf-8''` 로 한글/유니코드 제목도 정상 처리
|
||||||
|
|
||||||
|
## 참고
|
||||||
|
|
||||||
|
- 화질/음질 스펙은 사용자가 선택 패널에서 고른 라벨 기준 (자동 다운로드 경로도 실제 받은 화질을 quality에 기록하므로 스펙이 붙음)
|
||||||
|
- mp3는 소스 비트레이트를 라벨로 쓰지만 실제 인코딩은 192kbps 고정 (기존 동작, 이번 변경 범위 아님)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# AI Video Brief — 음성 선택지 코덱 효율 반영 추천 정렬
|
||||||
|
|
||||||
|
**소요 시간**: 4분
|
||||||
|
**Context 사용량**: input 900k / output 66k tokens (추정값)
|
||||||
|
|
||||||
|
## 요청 내용
|
||||||
|
|
||||||
|
음성 목록에서 같은 상황일 때 자동으로 가장 음질 좋은 걸 "추천"으로 더 똑똑하게 골라주도록.
|
||||||
|
|
||||||
|
## 배경
|
||||||
|
|
||||||
|
기존 음성 정렬은 단순히 비트레이트(abr) 내림차순 → 코덱 효율 차이를 무시.
|
||||||
|
Opus는 AAC보다 ~1.3배 효율적이라, 105kbps Opus ≈ 130kbps AAC 수준인데도 순위에서 밀렸음.
|
||||||
|
|
||||||
|
## 구현 내용 (`ai_video_brief`)
|
||||||
|
|
||||||
|
`app/services/downloader.py`:
|
||||||
|
- `_ACODEC_INFO` / `_audio_codec()` 추가 — acodec → (표시이름, 효율계수)
|
||||||
|
- Opus 1.3, AAC 1.0, Vorbis 0.9, MP3 0.85, Dolby 1.0
|
||||||
|
- `list_formats()` 음성 파트:
|
||||||
|
- 각 항목에 `codec`(표시이름), `score`(= abr × 효율계수, 실제 체감 음질) 추가
|
||||||
|
- 중복 제거 키를 (코덱, abr)로 변경 → 같은 코덱+비트레이트만 병합, 코덱 다르면 둘 다 유지
|
||||||
|
- 정렬을 `-score, -abr` 순으로 (실제 음질 높은 순, 동점 시 비트레이트 높은 순)
|
||||||
|
|
||||||
|
`app/static/index.html`:
|
||||||
|
- 음성 항목 메타에 코덱 표시 추가: "Opus · webm · 약 45MB"
|
||||||
|
|
||||||
|
## 테스트 결과 (DJI 영상)
|
||||||
|
|
||||||
|
```
|
||||||
|
105kbps Opus(webm) 점수=136 ← 추천(맨위) ※ 예전엔 129kbps AAC가 맨위였음
|
||||||
|
129kbps AAC(m4a) 점수=129
|
||||||
|
50kbps Opus(webm) 점수=65
|
||||||
|
49kbps AAC(m4a) 점수=49
|
||||||
|
```
|
||||||
|
|
||||||
|
Opus 105kbps가 AAC 129kbps보다 위로 올라와, 실제 음질 기준으로 정확히 추천됨.
|
||||||
|
|
||||||
|
## 참고
|
||||||
|
|
||||||
|
- 최종 저장은 여전히 mp3 192kbps 재인코딩 (소스 음질이 좋을수록 mp3 결과도 좋음)
|
||||||
|
- 효율계수는 일반적 통념 기반의 근사값 (정밀 측정값 아님)
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# AI Video Brief — 다운로드 기록 목록 + 다시 받기 기능
|
||||||
|
|
||||||
|
**소요 시간**: 12분
|
||||||
|
**Context 사용량**: input 960k / output 72k tokens (추정값)
|
||||||
|
|
||||||
|
## 요청 내용
|
||||||
|
|
||||||
|
음악이든 영화든 다운로드 받았던 것을 히스토리 목록으로 저장해 다시 받을 수 있게.
|
||||||
|
|
||||||
|
## 구현 내용 (`ai_video_brief`)
|
||||||
|
|
||||||
|
### 백엔드
|
||||||
|
- `db.py`: `downloads` 테이블 신규 (id, video_id, url, title, kind, quality, format_id, video_only, size_mb, created_at)
|
||||||
|
- `save_download()` — 같은 (video_id/url, kind, quality)면 옛 기록 지우고 갱신(중복 방지)
|
||||||
|
- `list_downloads()` / `get_download()` / `delete_download()`
|
||||||
|
- `main.py`:
|
||||||
|
- 다운로드 완료 시 `_save_download_record()` 로 기록 저장 (음성/영상 파이프라인 둘 다)
|
||||||
|
- `GET /api/downloads` — 기록 목록 + 각 항목 `file_exists`(파일이 아직 디스크에 있는지) 계산
|
||||||
|
- `DELETE /api/downloads/{id}` — 기록 + 저장 파일 함께 삭제
|
||||||
|
- `download_file()` 개선 — 메모리 작업이 없으면(서버 재시작 등) DB 기록에서 제목/품질 가져와 파일명 생성
|
||||||
|
|
||||||
|
### 웹 화면
|
||||||
|
- 좌측에 "⬇️ 다운로드 기록" 카드 추가 (요약 보관함 아래)
|
||||||
|
- 항목: 🎬/🎵 + 제목 + 품질 + 날짜 + 크기, 파일 없으면 "파일없음" 표시
|
||||||
|
- 파일 있으면 ⬇️(즉시 저장, 직접 링크), 없으면 🔄(저장했던 품질 그대로 다시 받기)
|
||||||
|
- ✕ 버튼: 기록+파일 삭제
|
||||||
|
- 다운로드 작업 완료 시 기록 목록 자동 새로고침
|
||||||
|
|
||||||
|
## 테스트 결과
|
||||||
|
|
||||||
|
- ✅ 영상 다운로드 → 기록에 저장 (제목/품질/크기/파일있음=True)
|
||||||
|
- ✅ 파일 강제 삭제 → file_exists=False 로 전환, format_id/video_only 보존(다시받기 가능)
|
||||||
|
- ✅ DELETE → 기록·파일 삭제 확인
|
||||||
|
- ✅ 화면에 다운로드 기록 UI 정상 렌더
|
||||||
|
|
||||||
|
## 다음 에이전트 참고
|
||||||
|
|
||||||
|
- "다시 받기"는 파일이 있으면 즉시 저장, 없으면 저장된 format_id로 유튜브에서 재다운로드 (새 job id로 새 파일 생성 → 기록은 dedupe로 갱신)
|
||||||
|
- 다운로드 기록과 요약 보관함은 별도 테이블 (downloads vs summaries)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# AI Video Brief — 다운로드 파일명이 제목으로 안 나오는 문제 수정
|
||||||
|
|
||||||
|
**소요 시간**: 10분
|
||||||
|
**Context 사용량**: input 1000k / output 78k tokens (추정값)
|
||||||
|
|
||||||
|
## 문제
|
||||||
|
|
||||||
|
저장은 되는데 파일명이 영상/음원 제목이 아니라 작업 id로 저장됨.
|
||||||
|
|
||||||
|
## 원인 (재현으로 확인)
|
||||||
|
|
||||||
|
- Starlette `FileResponse(filename=...)`는 한글 등 비ASCII 파일명일 때
|
||||||
|
`Content-Disposition: attachment; filename*=utf-8''<인코딩>` **한 형식만** 출력하고
|
||||||
|
구형/모바일 브라우저·다운로드관리자·curl이 인식하는 기본 `filename="..."` 형식을 빠뜨림
|
||||||
|
- `filename*`만 있으면 이를 못 읽는 클라이언트는 **URL 마지막 경로(작업 id)로 저장** → 문제 발생
|
||||||
|
- `curl -OJ` 재현: 파일이 `099fe812f033` 로 저장됨 (제목 아님)
|
||||||
|
|
||||||
|
## 수정 (`ai_video_brief/app/main.py`)
|
||||||
|
|
||||||
|
- `_content_disposition()` 헬퍼 추가 — RFC 6266 방식으로 **두 형식 모두** 출력:
|
||||||
|
`attachment; filename="<ASCII대체>"; filename*=utf-8''<UTF-8>`
|
||||||
|
- `filename*` : 최신 브라우저가 사용 → 완전한 한글 제목
|
||||||
|
- `filename` : ASCII 대체(한글은 빠지고 스펙/확장자는 유지) → 구형/모바일/curl용, 최소한 작업 id는 아님
|
||||||
|
- `download_file()`이 `filename=` 대신 이 헤더를 직접 지정
|
||||||
|
|
||||||
|
## 테스트 결과
|
||||||
|
|
||||||
|
```
|
||||||
|
헤더: content-disposition: attachment; filename="1 DJI 5 _144p24fps.mp4"; filename*=utf-8''1%EC%8B%9C...설명서_144p24fps.mp4
|
||||||
|
curl -OJ 저장: "1 DJI 5 _144p24fps.mp4" (예전엔 "099fe812f033")
|
||||||
|
```
|
||||||
|
- ✅ 최신 브라우저(Chrome/Edge/Firefox)는 filename* 우선 → "1시간으로 끝내는 DJI 미니 5 프로 사용 설명서_144p24fps.mp4" 전체 한글 제목으로 저장
|
||||||
|
- ✅ filename*를 못 읽는 클라이언트도 읽을 수 있는 ASCII 이름으로 저장 (작업 id 아님)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# AI Video Brief — 목록 항목 선택 시 컨텐츠 주소 입력창 자동 채움
|
||||||
|
|
||||||
|
**소요 시간**: 5분
|
||||||
|
**Context 사용량**: input 1040k / output 82k tokens (추정값)
|
||||||
|
|
||||||
|
## 요청 내용
|
||||||
|
|
||||||
|
히스토리(요약 보관함)나 다운로드 기록 항목을 선택하면 그 컨텐츠 주소를 위 입력창에 붙여넣기.
|
||||||
|
|
||||||
|
## 구현 내용 (`ai_video_brief`)
|
||||||
|
|
||||||
|
### 백엔드
|
||||||
|
- `db.list_summaries()` SELECT에 `url` 추가 (다운로드 기록 목록은 이미 url 포함)
|
||||||
|
|
||||||
|
### 웹 화면
|
||||||
|
- `fillUrl(u)` 헬퍼 추가 — 입력창(#url)에 주소 채우고 노란색 플래시 애니메이션 + 스크롤(block:nearest)
|
||||||
|
- CSS `input.flash` / `@keyframes urlflash` — 입력창 잠깐 노랗게 강조
|
||||||
|
- 요약 보관함 항목 클릭: `selectSummary()` 시작 시 `listCache[id].url`로 주소 자동 채움 (요약 본문 표시는 그대로 유지)
|
||||||
|
- 다운로드 기록 항목 본문 클릭: 기존 즉시 재다운로드 → **주소 채우기**로 변경
|
||||||
|
(실제 다운로드는 우측 ⬇️/🔄 버튼이 담당하므로 역할 분리)
|
||||||
|
|
||||||
|
## 테스트 결과
|
||||||
|
|
||||||
|
- ✅ `/api/summaries` 응답에 url 포함 확인 (예: youtube watch url)
|
||||||
|
- ✅ `/api/downloads` 응답에 url 포함 확인
|
||||||
|
- ✅ 화면에 fillUrl 연결 확인
|
||||||
|
|
||||||
|
## 동작
|
||||||
|
|
||||||
|
- 왼쪽에서 항목을 누르면 위 주소창에 그 컨텐츠 URL이 채워지고 잠깐 노랗게 반짝임
|
||||||
|
- 이어서 🎬/🎵/📝 버튼으로 원하는 작업을 바로 실행 가능
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# AI Video Brief — 컴퓨터 켤 때 자동 시작 설정
|
||||||
|
|
||||||
|
**소요 시간**: 15분
|
||||||
|
**Context 사용량**: input 1100k / output 90k tokens (추정값)
|
||||||
|
|
||||||
|
## 요청 내용
|
||||||
|
|
||||||
|
컴퓨터를 켜면 자동으로 서비스가 구동되게.
|
||||||
|
|
||||||
|
## 환경 확인
|
||||||
|
|
||||||
|
- WSL 배포판: **Ubuntu-22.04** (`$WSL_DISTRO_NAME`)
|
||||||
|
- Windows 활성 사용자: **User** (시작폴더 `C:\Users\User\...\Startup`)
|
||||||
|
- 에이전트가 sudo 불가 → 무권한 방식(시작프로그램 폴더 + VBS) 채택
|
||||||
|
|
||||||
|
## 구현 내용 (`ai_video_brief`)
|
||||||
|
|
||||||
|
### `start-service.sh` (신규)
|
||||||
|
- Ollama·uvicorn(55100)을 켠다. 이미 켜져 있으면 건드리지 않음(idempotent).
|
||||||
|
- **핵심: `setsid`로 프로세스를 새 세션으로 완전 독립**시킴.
|
||||||
|
- 처음엔 `nohup ... &`만 썼더니 wsl.exe 명령이 끝날 때 uvicorn이 함께 종료됨(로그도 비어 있음).
|
||||||
|
- `setsid bash -c "exec ... > log 2>&1" &` 로 바꾸니 wsl 세션 종료 후에도 서버 생존.
|
||||||
|
- 뜰 때까지 짧게 대기 루프(부팅 시 프로세스 안착 보장).
|
||||||
|
|
||||||
|
### Windows 시작프로그램 (신규)
|
||||||
|
- `C:\Users\User\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\AI-Video-Brief.vbs`
|
||||||
|
- 내용: `WScript.Shell.Run "wsl.exe -d Ubuntu-22.04 -u hanmac bash -lc ""...start-service.sh""", 0, False`
|
||||||
|
- 창 숨김(0), 비대기(False) → 로그인 시 조용히 실행
|
||||||
|
|
||||||
|
### README
|
||||||
|
- "컴퓨터 켤 때 자동 시작" 절 추가 (동작·확인·해제 방법, 폰 접속은 별도 안내)
|
||||||
|
|
||||||
|
## 테스트 결과
|
||||||
|
|
||||||
|
- ✅ start-service.sh 직접 실행 → Ollama·웹서버 둘 다 ON
|
||||||
|
- ✅ wsl.exe 경유 실행 후 세션 종료 → setsid 덕분에 두 서비스 생존 확인 (uvicorn PID 유지)
|
||||||
|
- ✅ 실제 경로(VBS를 cscript로 실행) → 3초 내 둘 다 ON
|
||||||
|
- (참고: PowerShell Start-Process로 VBS 경로에 공백이 있어 테스트가 한 번 실패했으나, Windows 시작폴더 자체 실행은 경로를 정상 처리하므로 실사용은 문제없음)
|
||||||
|
|
||||||
|
## 다음 에이전트 참고
|
||||||
|
|
||||||
|
- **자동시작은 WSL 안의 서비스만** 켬. 폰(LAN) 접속용 포트포워딩(netsh)은 관리자 권한이라 자동화 제외 — 재부팅 후 `포트열기.ps1` 수동 실행 필요.
|
||||||
|
- 로그인(user logon) 트리거임 — 로그인 전 부팅 단계에서는 안 뜸(일반적 "컴퓨터 켜면"에는 부합).
|
||||||
|
- setsid 없이 nohup만으로는 wsl.exe 호출 종료 시 백그라운드 프로세스가 죽는다 (WSL 특성). 반드시 setsid.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# 2026-07-08 데이터 폴더 입력 명세 작성
|
||||||
|
|
||||||
|
**소요 시간**: 15분
|
||||||
|
**Context 사용량**: input 120k / output 10k tokens (추정)
|
||||||
|
|
||||||
|
## 요청
|
||||||
|
- 웹플레이어가 동영상이 있는 폴더 경로를 전달받아 재생할 때 **필요한 데이터 명세** 작성
|
||||||
|
|
||||||
|
## 산출물
|
||||||
|
- `docs/명세_GhiVideo_데이터폴더_입력명세.md` (+ `.html` / `.pdf`, md2docs.sh 변환)
|
||||||
|
- 폴더 구성 요약(필수 4종 + 선택 2종 + 무시 파일), 파일별 상세 명세(컬럼·단위·폴백 규칙), 인코딩 공통 규칙, 누락 시 동작, 소스 근거 매핑
|
||||||
|
- 근거 코드: `client/src/utils/geoData.ts` (loadFolderGeoData 및 개별 파서), `client/src/types/geo.ts` (RouteMeta/RouteInfo/RouteStructure)
|
||||||
|
- 실데이터 검증: `/mnt/d/_project/20260612_abcvideo/import data/` 상행·하행 v3.0 폴더의 실제 헤더·route.json 확인
|
||||||
|
|
||||||
|
## 발견 이슈 (문서 3.3절 주의사항으로 명기)
|
||||||
|
- **상행 v3.0 `01)측점.csv` 표고 컬럼 불일치**: 실측 표고가 `z` 컬럼에 있고 `Z좌표`=0.0인데,
|
||||||
|
파서(parseStations)는 `Z좌표_한국` → `Z좌표` 순서만 읽음 → **표고가 0으로 로드됨**.
|
||||||
|
하행 파일(`Z좌표_한국` 있음)은 정상. 해결: 데이터 컬럼명 통일 또는 파서에 `z` 폴백 추가 필요.
|
||||||
|
|
||||||
|
## 참고
|
||||||
|
- 필수 파일: `<base>.MP4`, `<base>.csv`(비행로그), `01)측점.csv`(또는 측점.csv), `<base>.kmz`(POI·구조물 유일 소스 — CSV 폴백 없음)
|
||||||
|
- 선택 파일: `<base>.route.json`(노선정보·구조물 보정), `<base>_poi_overrides.json`(드래그 역투영 보정값)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# 외부 접속 방법 검토
|
||||||
|
|
||||||
|
**날짜**: 2026-07-08
|
||||||
|
**소요 시간**: 10분
|
||||||
|
**Context 사용량**: input 45k / output 5k tokens (추정)
|
||||||
|
|
||||||
|
## 질문
|
||||||
|
|
||||||
|
1. GhiVideo 웹플레이어를 회사 내부가 아닌 외부에서 사용하게 하는 방법
|
||||||
|
2. (후속) Cloudflare 서비스를 이용하면 되는 것인지 확인
|
||||||
|
|
||||||
|
## 현재 상태 확인
|
||||||
|
|
||||||
|
- 서버는 `0.0.0.0:55000`으로 리슨 (PM2 `ghiVideo`, ecosystem.config.js)
|
||||||
|
- 인증(로그인) 기능 없음 → 외부에 그대로 열면 누구나 열람/업로드/삭제 가능
|
||||||
|
- 서버가 WSL2 내부에서 구동 중 → 직접 포트포워딩 시 Windows portproxy 단계 추가 필요
|
||||||
|
|
||||||
|
## 제시한 방법
|
||||||
|
|
||||||
|
1. **Tailscale** (소수 인원용 추천): 앱 설치만으로 사설망 구성, 코드 수정 불필요
|
||||||
|
2. **Cloudflare Tunnel + Access**: 도메인 필요, 이메일 인증 관문, 공인 IP 불필요
|
||||||
|
3. **공인 IP + 포트포워딩 + Nginx HTTPS**: IT 승인 및 로그인 개발 필요
|
||||||
|
4. **클라우드 이전**: 정식 공개 서비스용, 현 단계에서는 과함
|
||||||
|
|
||||||
|
## Cloudflare Tunnel 상세 설명 (후속 답변)
|
||||||
|
|
||||||
|
- 서버에 `cloudflared` 설치 → 서버가 Cloudflare로 아웃바운드 연결 유지 → 방화벽/포트포워딩 불필요
|
||||||
|
- 준비물: Cloudflare 계정(무료) + 도메인 1개(연 1~2만 원) + cloudflared 상시 실행
|
||||||
|
- Cloudflare Access(무료 50명)로 허용 이메일만 통과하는 인증 관문 구성 가능 — 앱 코드 수정 불필요
|
||||||
|
- 주의: 영상이 외부 업체(Cloudflare) 경유(보안 규정 확인 필요), 대량 트래픽 시 무료 플랜 부담, 호스트 PC 꺼지면 서비스 중단
|
||||||
|
|
||||||
|
## 다음 행동
|
||||||
|
|
||||||
|
- 사용자 결정 대기: ① 도메인 보유 여부, ② 허용할 사용자 이메일 수
|
||||||
|
- 결정 시 cloudflared 설치 → 터널 연결 → Access 설정 → 외부 접속 확인 순으로 진행 예정
|
||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
@@ -0,0 +1,119 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang xml:lang>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="pandoc" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
|
||||||
|
<title>그림_시간측점대응표</title>
|
||||||
|
<style>
|
||||||
|
code{white-space: pre-wrap;}
|
||||||
|
span.smallcaps{font-variant: small-caps;}
|
||||||
|
span.underline{text-decoration: underline;}
|
||||||
|
div.column{display: inline-block; vertical-align: top; width: 50%;}
|
||||||
|
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
|
||||||
|
ul.task-list{list-style: none;}
|
||||||
|
.display.math{display: block; text-align: center; margin: 0.5rem auto;}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">@page {
|
||||||
|
size: A4;
|
||||||
|
margin: 18mm 16mm 16mm 16mm;
|
||||||
|
@bottom-center {
|
||||||
|
content: counter(page) " / " counter(pages);
|
||||||
|
font-family: "Malgun Gothic", sans-serif;
|
||||||
|
font-size: 9pt;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html { font-size: 11pt; }
|
||||||
|
body {
|
||||||
|
font-family: "Malgun Gothic", "Hancom Gothic", sans-serif;
|
||||||
|
color: #23272e;
|
||||||
|
line-height: 1.65;
|
||||||
|
max-width: 920px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.7rem;
|
||||||
|
color: #b45309;
|
||||||
|
border-bottom: 3px solid #f59e0b;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #b45309;
|
||||||
|
border-bottom: 1px solid #e5d3b3;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
margin-top: 1.6em;
|
||||||
|
}
|
||||||
|
h3 { font-size: 1.05rem; color: #92400e; margin-top: 1.1em; }
|
||||||
|
a { color: #b45309; }
|
||||||
|
hr { border: none; border-top: 1px solid #e2e2e2; margin: 1.6em 0; }
|
||||||
|
ul { padding-left: 1.25em; }
|
||||||
|
li { margin: 0.18em 0; }
|
||||||
|
strong { color: #1f2937; }
|
||||||
|
code {
|
||||||
|
font-family: "D2Coding", Consolas, monospace;
|
||||||
|
background: #f4f1ea;
|
||||||
|
border: 1px solid #e7e0d2;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.5px 5px;
|
||||||
|
font-size: 0.92em;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0.8em 0;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
th, td { border: 1px solid #d8d2c4; padding: 6px 10px; text-align: left; vertical-align: top; }
|
||||||
|
th { background: #fdf3df; color: #7c2d12; }
|
||||||
|
blockquote {
|
||||||
|
border-left: 4px solid #f59e0b;
|
||||||
|
margin: 0.8em 0;
|
||||||
|
padding: 0.2em 0 0.2em 14px;
|
||||||
|
color: #555;
|
||||||
|
background: #fffbf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3 { break-after: avoid; }
|
||||||
|
</style>
|
||||||
|
<!--[if lt IE 9]>
|
||||||
|
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
|
||||||
|
<![endif]-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header id="title-block-header">
|
||||||
|
<h1 class="title">그림_시간측점대응표</h1>
|
||||||
|
</header>
|
||||||
|
<h1 id="시간측점-대응표--재생-기준을-시간에서-스테이션측점으로-전환하기-위한-색인-구조">시간↔︎측점 대응표 — 재생 기준을 '시간'에서 '스테이션(측점)'으로 전환하기 위한 색인 구조</h1>
|
||||||
|
<figure>
|
||||||
|
<svg viewBox="0 0 1080 470" role="img" aria-label="시간↔측점 대응표 도식" font-family="Malgun Gothic, sans-serif">
|
||||||
|
<defs><marker id="ar" markerWidth="10" markerHeight="10" refX="7" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#7c3aed"></path></marker></defs>
|
||||||
|
<rect x="0" y="0" width="1080" height="470" fill="#ffffff"></rect>
|
||||||
|
<!-- 상단: 영상 프레임 -->
|
||||||
|
<rect x="390" y="30" width="300" height="76" fill="#595959"></rect>
|
||||||
|
<text x="540" y="63" text-anchor="middle" font-size="21" font-weight="bold" fill="#ffffff">영상 프레임 (사진 1장)</text>
|
||||||
|
<text x="540" y="90" text-anchor="middle" font-size="15" fill="#e8e8e8">(번호 + 드론 GPS 기록)</text>
|
||||||
|
<!-- 좌: 시간 계산 -->
|
||||||
|
<rect x="110" y="180" width="340" height="80" fill="#2e75b6"></rect>
|
||||||
|
<text x="280" y="215" text-anchor="middle" font-size="21" font-weight="bold" fill="#ffffff">프레임번호 ÷ fps = 시간</text>
|
||||||
|
<text x="280" y="243" text-anchor="middle" font-size="15" fill="#dbe9f7">예: 5760번 ÷ 30 = 192초</text>
|
||||||
|
<!-- 우: 측점 계산 -->
|
||||||
|
<rect x="630" y="180" width="340" height="80" fill="#70ad47"></rect>
|
||||||
|
<text x="800" y="215" text-anchor="middle" font-size="21" font-weight="bold" fill="#ffffff">GPS → 측점 (선로에 투영)</text>
|
||||||
|
<text x="800" y="243" text-anchor="middle" font-size="15" fill="#e6f2dc">예: 위도·경도 → 160k130</text>
|
||||||
|
<!-- 하단: 대응표 완성 -->
|
||||||
|
<rect x="320" y="340" width="440" height="80" fill="#bf9000"></rect>
|
||||||
|
<text x="540" y="375" text-anchor="middle" font-size="21" font-weight="bold" fill="#ffffff">시간 ↔ 측점 대응표 완성!</text>
|
||||||
|
<text x="540" y="403" text-anchor="middle" font-size="15" fill="#f7ecd0">"192초 = 160k130" 처럼 서로 변환 가능</text>
|
||||||
|
<!-- 화살표 -->
|
||||||
|
<line x1="455" y1="106" x2="305" y2="176" stroke="#7c3aed" stroke-width="3" marker-end="url(#ar)"></line>
|
||||||
|
<line x1="625" y1="106" x2="775" y2="176" stroke="#7c3aed" stroke-width="3" marker-end="url(#ar)"></line>
|
||||||
|
<line x1="295" y1="260" x2="445" y2="336" stroke="#7c3aed" stroke-width="3" marker-end="url(#ar)"></line>
|
||||||
|
<line x1="785" y1="260" x2="635" y2="336" stroke="#7c3aed" stroke-width="3" marker-end="url(#ar)"></line>
|
||||||
|
</svg>
|
||||||
|
</figure>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# 시간↔측점 대응표 — 재생 기준을 '시간'에서 '스테이션(측점)'으로 전환하기 위한 색인 구조
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<svg viewBox="0 0 1080 470" role="img" aria-label="시간↔측점 대응표 도식" font-family="Malgun Gothic, sans-serif">
|
||||||
|
<defs><marker id="ar" markerWidth="10" markerHeight="10" refX="7" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#7c3aed"/></marker></defs>
|
||||||
|
<rect x="0" y="0" width="1080" height="470" fill="#ffffff"/>
|
||||||
|
<!-- 상단: 영상 프레임 -->
|
||||||
|
<rect x="390" y="30" width="300" height="76" fill="#595959"/>
|
||||||
|
<text x="540" y="63" text-anchor="middle" font-size="21" font-weight="bold" fill="#ffffff">영상 프레임 (사진 1장)</text>
|
||||||
|
<text x="540" y="90" text-anchor="middle" font-size="15" fill="#e8e8e8">(번호 + 드론 GPS 기록)</text>
|
||||||
|
<!-- 좌: 시간 계산 -->
|
||||||
|
<rect x="110" y="180" width="340" height="80" fill="#2e75b6"/>
|
||||||
|
<text x="280" y="215" text-anchor="middle" font-size="21" font-weight="bold" fill="#ffffff">프레임번호 ÷ fps = 시간</text>
|
||||||
|
<text x="280" y="243" text-anchor="middle" font-size="15" fill="#dbe9f7">예: 5760번 ÷ 30 = 192초</text>
|
||||||
|
<!-- 우: 측점 계산 -->
|
||||||
|
<rect x="630" y="180" width="340" height="80" fill="#70ad47"/>
|
||||||
|
<text x="800" y="215" text-anchor="middle" font-size="21" font-weight="bold" fill="#ffffff">GPS → 측점 (선로에 투영)</text>
|
||||||
|
<text x="800" y="243" text-anchor="middle" font-size="15" fill="#e6f2dc">예: 위도·경도 → 160k130</text>
|
||||||
|
<!-- 하단: 대응표 완성 -->
|
||||||
|
<rect x="320" y="340" width="440" height="80" fill="#bf9000"/>
|
||||||
|
<text x="540" y="375" text-anchor="middle" font-size="21" font-weight="bold" fill="#ffffff">시간 ↔ 측점 대응표 완성!</text>
|
||||||
|
<text x="540" y="403" text-anchor="middle" font-size="15" fill="#f7ecd0">"192초 = 160k130" 처럼 서로 변환 가능</text>
|
||||||
|
<!-- 화살표 -->
|
||||||
|
<line x1="455" y1="106" x2="305" y2="176" stroke="#7c3aed" stroke-width="3" marker-end="url(#ar)"/>
|
||||||
|
<line x1="625" y1="106" x2="775" y2="176" stroke="#7c3aed" stroke-width="3" marker-end="url(#ar)"/>
|
||||||
|
<line x1="295" y1="260" x2="445" y2="336" stroke="#7c3aed" stroke-width="3" marker-end="url(#ar)"/>
|
||||||
|
<line x1="785" y1="260" x2="635" y2="336" stroke="#7c3aed" stroke-width="3" marker-end="url(#ar)"/>
|
||||||
|
</svg>
|
||||||
|
</figure>
|
||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,554 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang xml:lang>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="pandoc" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
|
||||||
|
<title>명세_GhiVideo_데이터폴더_입력명세</title>
|
||||||
|
<style>
|
||||||
|
code{white-space: pre-wrap;}
|
||||||
|
span.smallcaps{font-variant: small-caps;}
|
||||||
|
span.underline{text-decoration: underline;}
|
||||||
|
div.column{display: inline-block; vertical-align: top; width: 50%;}
|
||||||
|
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
|
||||||
|
ul.task-list{list-style: none;}
|
||||||
|
pre > code.sourceCode { white-space: pre; position: relative; }
|
||||||
|
pre > code.sourceCode > span { display: inline-block; line-height: 1.25; }
|
||||||
|
pre > code.sourceCode > span:empty { height: 1.2em; }
|
||||||
|
code.sourceCode > span { color: inherit; text-decoration: inherit; }
|
||||||
|
div.sourceCode { margin: 1em 0; }
|
||||||
|
pre.sourceCode { margin: 0; }
|
||||||
|
@media screen {
|
||||||
|
div.sourceCode { overflow: auto; }
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
pre > code.sourceCode { white-space: pre-wrap; }
|
||||||
|
pre > code.sourceCode > span { text-indent: -5em; padding-left: 5em; }
|
||||||
|
}
|
||||||
|
pre.numberSource code
|
||||||
|
{ counter-reset: source-line 0; }
|
||||||
|
pre.numberSource code > span
|
||||||
|
{ position: relative; left: -4em; counter-increment: source-line; }
|
||||||
|
pre.numberSource code > span > a:first-child::before
|
||||||
|
{ content: counter(source-line);
|
||||||
|
position: relative; left: -1em; text-align: right; vertical-align: baseline;
|
||||||
|
border: none; display: inline-block;
|
||||||
|
-webkit-touch-callout: none; -webkit-user-select: none;
|
||||||
|
-khtml-user-select: none; -moz-user-select: none;
|
||||||
|
-ms-user-select: none; user-select: none;
|
||||||
|
padding: 0 4px; width: 4em;
|
||||||
|
color: #aaaaaa;
|
||||||
|
}
|
||||||
|
pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa; padding-left: 4px; }
|
||||||
|
div.sourceCode
|
||||||
|
{ }
|
||||||
|
@media screen {
|
||||||
|
pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
|
||||||
|
}
|
||||||
|
code span.al { color: #ff0000; font-weight: bold; } /* Alert */
|
||||||
|
code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
|
||||||
|
code span.at { color: #7d9029; } /* Attribute */
|
||||||
|
code span.bn { color: #40a070; } /* BaseN */
|
||||||
|
code span.bu { } /* BuiltIn */
|
||||||
|
code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
|
||||||
|
code span.ch { color: #4070a0; } /* Char */
|
||||||
|
code span.cn { color: #880000; } /* Constant */
|
||||||
|
code span.co { color: #60a0b0; font-style: italic; } /* Comment */
|
||||||
|
code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
|
||||||
|
code span.do { color: #ba2121; font-style: italic; } /* Documentation */
|
||||||
|
code span.dt { color: #902000; } /* DataType */
|
||||||
|
code span.dv { color: #40a070; } /* DecVal */
|
||||||
|
code span.er { color: #ff0000; font-weight: bold; } /* Error */
|
||||||
|
code span.ex { } /* Extension */
|
||||||
|
code span.fl { color: #40a070; } /* Float */
|
||||||
|
code span.fu { color: #06287e; } /* Function */
|
||||||
|
code span.im { } /* Import */
|
||||||
|
code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
|
||||||
|
code span.kw { color: #007020; font-weight: bold; } /* Keyword */
|
||||||
|
code span.op { color: #666666; } /* Operator */
|
||||||
|
code span.ot { color: #007020; } /* Other */
|
||||||
|
code span.pp { color: #bc7a00; } /* Preprocessor */
|
||||||
|
code span.sc { color: #4070a0; } /* SpecialChar */
|
||||||
|
code span.ss { color: #bb6688; } /* SpecialString */
|
||||||
|
code span.st { color: #4070a0; } /* String */
|
||||||
|
code span.va { color: #19177c; } /* Variable */
|
||||||
|
code span.vs { color: #4070a0; } /* VerbatimString */
|
||||||
|
code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
|
||||||
|
.display.math{display: block; text-align: center; margin: 0.5rem auto;}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">@page {
|
||||||
|
size: A4;
|
||||||
|
margin: 18mm 16mm 16mm 16mm;
|
||||||
|
@bottom-center {
|
||||||
|
content: counter(page) " / " counter(pages);
|
||||||
|
font-family: "Malgun Gothic", sans-serif;
|
||||||
|
font-size: 9pt;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html { font-size: 11pt; }
|
||||||
|
body {
|
||||||
|
font-family: "Malgun Gothic", "Hancom Gothic", sans-serif;
|
||||||
|
color: #23272e;
|
||||||
|
line-height: 1.65;
|
||||||
|
max-width: 920px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.7rem;
|
||||||
|
color: #b45309;
|
||||||
|
border-bottom: 3px solid #f59e0b;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #b45309;
|
||||||
|
border-bottom: 1px solid #e5d3b3;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
margin-top: 1.6em;
|
||||||
|
}
|
||||||
|
h3 { font-size: 1.05rem; color: #92400e; margin-top: 1.1em; }
|
||||||
|
a { color: #b45309; }
|
||||||
|
hr { border: none; border-top: 1px solid #e2e2e2; margin: 1.6em 0; }
|
||||||
|
ul { padding-left: 1.25em; }
|
||||||
|
li { margin: 0.18em 0; }
|
||||||
|
strong { color: #1f2937; }
|
||||||
|
code {
|
||||||
|
font-family: "D2Coding", Consolas, monospace;
|
||||||
|
background: #f4f1ea;
|
||||||
|
border: 1px solid #e7e0d2;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.5px 5px;
|
||||||
|
font-size: 0.92em;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0.8em 0;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
th, td { border: 1px solid #d8d2c4; padding: 6px 10px; text-align: left; vertical-align: top; }
|
||||||
|
th { background: #fdf3df; color: #7c2d12; }
|
||||||
|
blockquote {
|
||||||
|
border-left: 4px solid #f59e0b;
|
||||||
|
margin: 0.8em 0;
|
||||||
|
padding: 0.2em 0 0.2em 14px;
|
||||||
|
color: #555;
|
||||||
|
background: #fffbf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3 { break-after: avoid; }
|
||||||
|
</style>
|
||||||
|
<!--[if lt IE 9]>
|
||||||
|
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
|
||||||
|
<![endif]-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header id="title-block-header">
|
||||||
|
<h1 class="title">명세_GhiVideo_데이터폴더_입력명세</h1>
|
||||||
|
</header>
|
||||||
|
<h1 id="데이터-폴더-입력-명세--스테이션기반-플레이어">데이터 폴더 입력 명세 — 스테이션기반 플레이어</h1>
|
||||||
|
<blockquote>
|
||||||
|
<p>웹플레이어가 <strong>동영상이 있는 폴더</strong>를 전달받아 재생할 때, 폴더 안에 어떤 파일이 어떤 형식으로 있어야 하는지를 정의한다. 근거: <code>client/src/utils/geoData.ts</code>(폴더 파서) 기준. 실데이터(v3.0, 대전조차장–회덕)로 검증.</p>
|
||||||
|
</blockquote>
|
||||||
|
<hr />
|
||||||
|
<h2 id="1-동작-개요">1. 동작 개요</h2>
|
||||||
|
<ol type="1">
|
||||||
|
<li>사용자가 브라우저에서 <strong>폴더를 선택</strong>하면(<code><input type="file" webkitdirectory></code>), 폴더 내 전체 파일 목록이 플레이어에 전달된다.</li>
|
||||||
|
<li>플레이어는 파일명 규칙으로 각 파일을 <strong>자동 식별·파싱</strong>한다(별도 설정 불필요).</li>
|
||||||
|
<li>기준 이름 <strong>base</strong> = 영상 파일명에서 확장자를 뺀 것 (예: <code>상행)대전조차장-회덕.MP4</code> → base는 <code>상행)대전조차장-회덕</code>). 나머지 파일은 base 일치를 우선 매칭하고, 없으면 폴백 규칙으로 찾는다.</li>
|
||||||
|
</ol>
|
||||||
|
<hr />
|
||||||
|
<h2 id="2-폴더-구성-요약">2. 폴더 구성 요약</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="header">
|
||||||
|
<th>#</th>
|
||||||
|
<th>파일</th>
|
||||||
|
<th>필수</th>
|
||||||
|
<th>역할</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>1</td>
|
||||||
|
<td><code><base>.MP4</code></td>
|
||||||
|
<td><strong>필수</strong></td>
|
||||||
|
<td>드론 촬영 항공영상 (mp4/webm)</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>2</td>
|
||||||
|
<td><code><base>.csv</code></td>
|
||||||
|
<td><strong>필수</strong></td>
|
||||||
|
<td>드론 비행로그 — 프레임별 위치·자세각·초점거리</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>3</td>
|
||||||
|
<td><code>01)측점.csv</code> 또는 <code>측점.csv</code></td>
|
||||||
|
<td><strong>필수</strong></td>
|
||||||
|
<td>측점 목록(이정·좌표·실측 표고) → 중심선·스테이션바·POI 높이 보완의 근간</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>4</td>
|
||||||
|
<td><code><base>.kmz</code> (또는 <code>.kml</code>)</td>
|
||||||
|
<td><strong>필수</strong>(정책)</td>
|
||||||
|
<td>POI·구조물의 <strong>유일한 소스</strong> (지장물·교량·터널·출입문·구교·철도역)</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>5</td>
|
||||||
|
<td><code><base>.route.json</code> 또는 <code>route.json</code></td>
|
||||||
|
<td>선택</td>
|
||||||
|
<td>노선 기본 정보 + 구조물 위치 보정</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>6</td>
|
||||||
|
<td><code><base>_poi_overrides.json</code></td>
|
||||||
|
<td>선택</td>
|
||||||
|
<td>POI 드래그 보정 저장값 (역투영 결과)</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>—</td>
|
||||||
|
<td><code>.srt</code>, <code>.qgz</code>, <code>.qgs</code> 등 그 외 파일</td>
|
||||||
|
<td>무시</td>
|
||||||
|
<td>플레이어가 읽지 않음 (작업용 부산물)</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p><strong>예시 폴더</strong> (실데이터 v3.0):</p>
|
||||||
|
<pre><code>상행)대전조차장-회덕 데이터(v3.0)/
|
||||||
|
├── 상행)대전조차장-회덕.MP4 ← 영상 (base 기준)
|
||||||
|
├── 상행)대전조차장-회덕.csv ← 드론 비행로그
|
||||||
|
├── 01)측점.csv ← 측점 (루트 또는 building/ 하위 모두 허용)
|
||||||
|
├── 상행)대전조차장-회덕.kmz ← POI·구조물 원본
|
||||||
|
├── 상행)대전조차장-회덕.route.json ← 노선 정보 (선택)
|
||||||
|
├── 상행)대전조차장-회덕.srt ← (무시됨)
|
||||||
|
└── 상행)대전조차장-회덕.qgz ← (무시됨, QGIS 작업파일)</code></pre>
|
||||||
|
<hr />
|
||||||
|
<h2 id="3-파일별-상세-명세">3. 파일별 상세 명세</h2>
|
||||||
|
<h3 id="31-영상--basemp4">3.1 영상 — <code><base>.MP4</code></h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>확장자</strong>: <code>.mp4</code> 또는 <code>.webm</code> (대소문자 무관). <code>building/</code> 하위가 아닌 <strong>루트의 첫 번째 영상 파일</strong>을 사용한다.</li>
|
||||||
|
<li><strong>코덱</strong>: 브라우저 직접 재생이므로 <strong>H.264 권장</strong>.</li>
|
||||||
|
<li><strong>용량</strong>: 2GB 이상 가능 (<code>URL.createObjectURL()</code> 참조 방식 — 메모리에 통째로 올리지 않음).</li>
|
||||||
|
<li>이 파일명이 폴더 전체의 <strong>base</strong> 를 결정한다.</li>
|
||||||
|
</ul>
|
||||||
|
<h3 id="32-드론-비행로그--basecsv">3.2 드론 비행로그 — <code><base>.csv</code></h3>
|
||||||
|
<p>드론이 촬영과 동시에 기록한 프레임별 데이터. <strong>헤더 이름으로 컬럼을 찾으며</strong>(순서 무관), 헤더가 깨졌을 때만 위치 인덱스로 폴백한다.</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="header">
|
||||||
|
<th>컬럼(헤더명)</th>
|
||||||
|
<th>의미</th>
|
||||||
|
<th>단위/규약</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="odd">
|
||||||
|
<td><code>frame_cnt</code></td>
|
||||||
|
<td>프레임 번호 (0부터)</td>
|
||||||
|
<td>정수</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td><code>latitude</code> / <code>longitude</code></td>
|
||||||
|
<td>드론 위치 위도/경도</td>
|
||||||
|
<td>도(WGS84)</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td><code>altitude</code></td>
|
||||||
|
<td>드론 고도</td>
|
||||||
|
<td>m (타원체고)</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td><code>yaw</code></td>
|
||||||
|
<td>기수 방위각</td>
|
||||||
|
<td>도 — 북=0, 시계방향</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td><code>pitch</code></td>
|
||||||
|
<td>카메라 틸트</td>
|
||||||
|
<td>도 — 음수=아래</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td><code>roll</code></td>
|
||||||
|
<td>롤</td>
|
||||||
|
<td>도</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td><code>focal_len</code></td>
|
||||||
|
<td>초점거리</td>
|
||||||
|
<td>mm (<strong>35mm 환산</strong>)</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<ul>
|
||||||
|
<li>추가 컬럼(<code>rel_alt</code>, <code>5186x</code>, <code>5186y</code>, <code>X좌표</code>, <code>Y좌표</code> 등)이 있어도 <strong>무시되고 오류 없음</strong>.</li>
|
||||||
|
<li><code>latitude</code> 가 숫자가 아닌 행은 건너뜀.</li>
|
||||||
|
<li><strong>파일 선택 규칙</strong>: 루트의 <code>.csv</code> 중 ① <code><base>.csv</code> 정확 일치 우선, ② 없으면 파일명에 <code>POI</code> 가 없는 첫 <code>.csv</code> (영상명 비의존 폴백).</li>
|
||||||
|
<li><strong>fps 자동 산출</strong>: 최대 <code>frame_cnt</code> ÷ 영상 길이(초) → 표준 fps(24/25/29.97/30/50/60…)에 스냅. 따라서 <strong>비행로그의 프레임 수는 영상의 실제 프레임 수와 일치해야 한다.</strong></li>
|
||||||
|
</ul>
|
||||||
|
<h3 id="33-측점--01측점csv-또는-측점csv">3.3 측점 — <code>01)측점.csv</code> (또는 <code>측점.csv</code>)</h3>
|
||||||
|
<p>철도 노선 측량 성과. 시간↔︎측점 대응표(스테이션바)와 선로 중심선, POI 높이 보완의 근간 데이터.</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>파일 찾기 규칙</strong> (3단 폴백): ① <code>building/</code> 하위에서 파일명에 <code>01)측점</code> 포함 → ② 루트 포함 아무 위치에서 <code>01)측점</code> 포함 → ③ 파일명에 <code>측점</code> 포함.</li>
|
||||||
|
<li><strong>컬럼</strong> (헤더 이름 기준):</li>
|
||||||
|
</ul>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="header">
|
||||||
|
<th>컬럼</th>
|
||||||
|
<th>필수</th>
|
||||||
|
<th>의미</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="odd">
|
||||||
|
<td><code>측점</code></td>
|
||||||
|
<td><strong>필수</strong></td>
|
||||||
|
<td>이정 표기 (예 <code>157K900</code>) — 스테이션 정렬·표시 기준</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td><code>lat</code> / <code>lon</code></td>
|
||||||
|
<td><strong>필수</strong></td>
|
||||||
|
<td>측점 좌표 (도, WGS84) — 숫자 아니면 해당 행 제외</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td><code>Z좌표_한국</code></td>
|
||||||
|
<td>권장</td>
|
||||||
|
<td><strong>실측 정표고(EL, m)</strong> — 있으면 최우선 사용</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td><code>Z좌표</code></td>
|
||||||
|
<td>폴백</td>
|
||||||
|
<td><code>Z좌표_한국</code> 이 없을 때 사용</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td><code>비고</code></td>
|
||||||
|
<td>선택</td>
|
||||||
|
<td><code>방향전환점(상행->하행, 02:05)</code> 패턴이 있으면 방향전환점으로 추출</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<ul>
|
||||||
|
<li><strong>중심선 자동 생성</strong>: v2.0 이후 <code>center.csv</code> 가 없으므로, 측점을 이정 순으로 이어 만든 폴리라인이 선로 중심선이 된다. → <strong>측점 정확도가 곧 측점 투영·POI 높이 보완의 정확도.</strong></li>
|
||||||
|
</ul>
|
||||||
|
<blockquote>
|
||||||
|
<p>⚠️ <strong>주의 (실데이터에서 발견)</strong>: 상행 v3.0의 <code>01)측점.csv</code> 는 실측 표고가 <code>z</code> 컬럼에 있고 <code>Z좌표</code> 컬럼은 0.0 인데, 파서는 <code>Z좌표_한국</code> → <code>Z좌표</code> 순서로만 읽으므로 <strong>표고가 0으로 로드된다</strong>. 하행 파일처럼 표고 컬럼명을 <strong><code>Z좌표_한국</code> 으로 통일</strong>하거나, 파서에 <code>z</code> 컬럼 폴백 추가가 필요하다.</p>
|
||||||
|
</blockquote>
|
||||||
|
<h3 id="34-poi구조물--basekmz-또는-kml">3.4 POI·구조물 — <code><base>.kmz</code> (또는 <code>.kml</code>)</h3>
|
||||||
|
<p><strong>POI·구조물의 유일한 소스</strong>다 (<code>building/</code> 하위 02)~06) CSV는 더 이상 읽지 않음). 루트의 bare <code>.kml</code> 이 있으면 우선, 없으면 <code>.kmz</code>(zip)를 해제해 <code>doc.kml</code> 을 읽는다.</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>KML 폴더 구조 → 분류 매핑</strong>:</li>
|
||||||
|
</ul>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="header">
|
||||||
|
<th>KML 폴더명 (포함 키워드)</th>
|
||||||
|
<th>분류</th>
|
||||||
|
<th>이름 필드 (description 표)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="odd">
|
||||||
|
<td><code>03)교량</code></td>
|
||||||
|
<td>구조물 bridge</td>
|
||||||
|
<td><code>구분</code> (+ <code>연장(m)</code>, <code>시설종별</code>)</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td><code>04)터널</code></td>
|
||||||
|
<td>구조물 tunnel</td>
|
||||||
|
<td><code>구분</code> (+ <code>연장(m)</code>, <code>시설종별</code>)</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td><code>06)구교</code></td>
|
||||||
|
<td>구조물 bridge(구교)</td>
|
||||||
|
<td><code>시설물명</code> (+ <code>연장(m)</code>)</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td><code>05)출입문번호</code></td>
|
||||||
|
<td>POI (출입문)</td>
|
||||||
|
<td><code>출입문번호</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td><code>02)지장물</code> 등 그 외</td>
|
||||||
|
<td>POI (지장물 등)</td>
|
||||||
|
<td><code>구분</code> → <code>title</code> → placemark <code>name</code> 순</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>— <code>source=KAKAO_RAIL</code> 또는 <code>category_clean=철도역/역사</code></td>
|
||||||
|
<td>POI + <strong>역사 구조물</strong> (스테이션바에도 표출)</td>
|
||||||
|
<td>위와 동일</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Placemark 좌표</strong>: <code><coordinates></code> 의 <code>lon,lat[,alt]</code> 우선. 없으면 description 속성 <code>lat</code>/<code>lon</code>. <strong>한국 범위 검증</strong>(위도 33~39, 경도 124~132) 밖이면 버림.</li>
|
||||||
|
<li><strong>속성(description)</strong>: HTML <code><table></code> 의 (키, 값) 행으로 기재 — <code>구분</code>, <code>연장(m)</code>, <code>시설종별</code>(1종/2종/3종/기타), <code>시설물명</code>, <code>출입문번호</code>, <code>source</code>, <code>category_clean</code>, <code>title</code>, 표고(<code>z</code>/<code>Z좌표</code>/<code>절대고도</code>) 등. 구글어스 재저장본의 표 변형(<code><tbody></code>, 엔티티 등)도 허용.</li>
|
||||||
|
<li><strong>표고</strong>: POI 속성에 <code>z</code>/<code>절대고도</code> 가 있으면 사용, 없으면 0 → 재생 시 <strong>최근접 측점 실측 표고로 보완</strong>된다.</li>
|
||||||
|
<li><strong>누락 시</strong>: 경고 로그 후 POI·구조물 없이 동작(측점·드론·영상은 정상). CSV 폴백은 없다.</li>
|
||||||
|
</ul>
|
||||||
|
<h3 id="35-노선-기본-정보--baseroutejson-선택">3.5 노선 기본 정보 — <code><base>.route.json</code> (선택)</h3>
|
||||||
|
<p>루트의 <code><base>.route.json</code> 우선, 없으면 <code>route.json</code>. UTF-8 JSON.</p>
|
||||||
|
<div class="sourceCode" id="cb2"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="fu">{</span></span>
|
||||||
|
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a> <span class="dt">"routeInfo"</span><span class="fu">:</span> <span class="fu">{</span></span>
|
||||||
|
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a> <span class="dt">"name"</span><span class="fu">:</span> <span class="st">"대전조차장-회덕"</span><span class="fu">,</span> <span class="er">//</span> <span class="er">노선명</span> <span class="er">(표시용)</span></span>
|
||||||
|
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a> <span class="dt">"direction"</span><span class="fu">:</span> <span class="st">"상행"</span><span class="fu">,</span> <span class="er">//</span> <span class="er">방향</span> <span class="er">라벨</span></span>
|
||||||
|
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a> <span class="dt">"lengthKm"</span><span class="fu">:</span> <span class="fl">4.25</span><span class="fu">,</span> <span class="er">//</span> <span class="er">구간</span> <span class="er">연장</span> <span class="er">(km)</span></span>
|
||||||
|
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a> <span class="dt">"durationSec"</span><span class="fu">:</span> <span class="dv">568</span><span class="fu">,</span> <span class="er">//</span> <span class="er">영상</span> <span class="er">길이</span> <span class="er">(초)</span></span>
|
||||||
|
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a> <span class="dt">"startStationName"</span><span class="fu">:</span> <span class="st">"대전"</span><span class="fu">,</span> <span class="er">//</span> <span class="er">시점역</span></span>
|
||||||
|
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a> <span class="dt">"endStationName"</span><span class="fu">:</span> <span class="st">"신탄진"</span><span class="fu">,</span> <span class="er">//</span> <span class="er">종점역</span></span>
|
||||||
|
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a> <span class="dt">"stationTolerance"</span><span class="fu">:</span> <span class="dv">20</span><span class="fu">,</span> <span class="er">//</span> <span class="er">측점</span> <span class="er">인정</span> <span class="er">반경(m)</span> <span class="er">—</span> <span class="er">기본</span> <span class="er">40</span></span>
|
||||||
|
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a> <span class="dt">"endStationGapMeters"</span><span class="fu">:</span> <span class="dv">0</span> <span class="er">//</span> <span class="er">종점</span> <span class="er">미도착</span> <span class="er">거리(m)</span> <span class="er">—</span> <span class="dv">0</span><span class="er">이면</span> <span class="er">도착</span></span>
|
||||||
|
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a> <span class="fu">},</span></span>
|
||||||
|
<span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a> <span class="dt">"structures"</span><span class="fu">:</span> <span class="ot">[</span> <span class="er">//</span> <span class="er">(선택)</span> <span class="er">구조물</span> <span class="er">보정</span> <span class="er">—</span> <span class="er">name</span> <span class="er">매칭으로</span> <span class="er">override</span></span>
|
||||||
|
<span id="cb2-13"><a href="#cb2-13" aria-hidden="true" tabindex="-1"></a> <span class="fu">{</span></span>
|
||||||
|
<span id="cb2-14"><a href="#cb2-14" aria-hidden="true" tabindex="-1"></a> <span class="dt">"name"</span><span class="fu">:</span> <span class="st">"회덕천교"</span><span class="fu">,</span> <span class="er">//</span> <span class="er">괄호</span> <span class="er">이하</span> <span class="er">제거</span> <span class="er">후</span> <span class="er">부분일치</span> <span class="er">매칭</span></span>
|
||||||
|
<span id="cb2-15"><a href="#cb2-15" aria-hidden="true" tabindex="-1"></a> <span class="dt">"station"</span><span class="fu">:</span> <span class="st">"158k400"</span><span class="fu">,</span> <span class="er">//</span> <span class="er">최우선</span><span class="fu">:</span> <span class="er">이</span> <span class="er">측점값</span> <span class="er">위치에</span> <span class="er">배치</span> <span class="er">(숫자</span> <span class="er">m</span> <span class="er">또는</span> <span class="st">"158k400"</span><span class="er">)</span></span>
|
||||||
|
<span id="cb2-16"><a href="#cb2-16" aria-hidden="true" tabindex="-1"></a> <span class="st">"offset"</span><span class="er">:</span> <span class="dv">-10</span><span class="fu">,</span> <span class="er">//</span> <span class="er">진행방향</span> <span class="er">±N</span> <span class="er">m</span> <span class="er">미세조정</span></span>
|
||||||
|
<span id="cb2-17"><a href="#cb2-17" aria-hidden="true" tabindex="-1"></a> <span class="dt">"startMileage"</span><span class="fu">:</span> <span class="dv">158400</span><span class="fu">,</span> <span class="er">//</span> <span class="er">매칭</span> <span class="er">실패</span> <span class="er">시</span> <span class="er">폴백</span> <span class="er">이정</span></span>
|
||||||
|
<span id="cb2-18"><a href="#cb2-18" aria-hidden="true" tabindex="-1"></a> <span class="dt">"endMileage"</span><span class="fu">:</span> <span class="dv">158500</span><span class="fu">,</span></span>
|
||||||
|
<span id="cb2-19"><a href="#cb2-19" aria-hidden="true" tabindex="-1"></a> <span class="dt">"lat"</span><span class="fu">:</span> <span class="fl">36.4031</span><span class="fu">,</span> <span class="dt">"lon"</span><span class="fu">:</span> <span class="fl">127.4216</span> <span class="er">//</span> <span class="er">좌표</span> <span class="er">직접</span> <span class="er">지정도</span> <span class="er">가능</span></span>
|
||||||
|
<span id="cb2-20"><a href="#cb2-20" aria-hidden="true" tabindex="-1"></a> <span class="fu">}</span></span>
|
||||||
|
<span id="cb2-21"><a href="#cb2-21" aria-hidden="true" tabindex="-1"></a> <span class="ot">]</span></span>
|
||||||
|
<span id="cb2-22"><a href="#cb2-22" aria-hidden="true" tabindex="-1"></a><span class="fu">}</span></span></code></pre></div>
|
||||||
|
<ul>
|
||||||
|
<li><code>structures</code> 항목은 KMZ 유래 구조물에 <strong>명시된 필드만 덮어쓰고</strong>, KMZ에 없는 항목은 새 구조물로 추가된다.</li>
|
||||||
|
</ul>
|
||||||
|
<h3 id="36-poi-위치-보정--base_poi_overridesjson-선택">3.6 POI 위치 보정 — <code><base>_poi_overrides.json</code> (선택)</h3>
|
||||||
|
<p>영상에서 POI 라벨을 <strong>드래그(역투영)</strong> 해 보정한 좌표의 저장/배포 파일. 폴더에 있으면 로드 시 자동 적용된다.</p>
|
||||||
|
<div class="sourceCode" id="cb3"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="fu">{</span></span>
|
||||||
|
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a> <span class="dt">"baseName"</span><span class="fu">:</span> <span class="st">"상행)대전조차장-회덕"</span><span class="fu">,</span></span>
|
||||||
|
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a> <span class="dt">"overrides"</span><span class="fu">:</span> <span class="fu">{</span></span>
|
||||||
|
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a> <span class="dt">"회덕터널"</span><span class="fu">:</span> <span class="fu">{</span> <span class="dt">"lat"</span><span class="fu">:</span> <span class="fl">36.40312</span><span class="fu">,</span> <span class="dt">"lon"</span><span class="fu">:</span> <span class="fl">127.42169</span><span class="fu">,</span> <span class="dt">"z"</span><span class="fu">:</span> <span class="fl">66.1</span> <span class="fu">}</span></span>
|
||||||
|
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a> <span class="fu">}</span></span>
|
||||||
|
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a><span class="fu">}</span></span></code></pre></div>
|
||||||
|
<ul>
|
||||||
|
<li><code>overrides</code> 없이 평면 맵(<code>{ "회덕터널": {…} }</code>)도 허용. <code>z</code> 는 정표고(m). POI <code>title</code> 정확 일치로 매칭.</li>
|
||||||
|
</ul>
|
||||||
|
<hr />
|
||||||
|
<h2 id="4-공통-규칙-인코딩파싱">4. 공통 규칙 (인코딩·파싱)</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="header">
|
||||||
|
<th>항목</th>
|
||||||
|
<th>규칙</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>CSV 인코딩</td>
|
||||||
|
<td><strong>자동 감지</strong> — 앞 3바이트가 UTF-8 BOM(EF BB BF)이면 UTF-8, 아니면 EUC-KR. 파일별 하드코딩 없음</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>CSV 컬럼</td>
|
||||||
|
<td><strong>헤더 이름으로 탐색</strong> (순서 무관, 여분 컬럼 무시). 헤더 훼손 시 위치 인덱스 폴백</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>CSV 형식</td>
|
||||||
|
<td>큰따옴표 필드 지원, 빈 줄 무시, <code>\r\n</code>/<code>\n</code> 모두 허용</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>KML</td>
|
||||||
|
<td>UTF-8. <code>.kml</code> > <code>.kmz</code>(내부 <code>doc.kml</code>) 우선순위</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>JSON</td>
|
||||||
|
<td>UTF-8. 파싱 실패 시 조용히 무시(선택 파일이므로)</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>폴더 구조</td>
|
||||||
|
<td><code>building/</code> 하위는 측점 CSV 탐색 외에는 사용하지 않음. 하위 폴더 깊이는 1단계(<code>building/</code>)만 인식</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<hr />
|
||||||
|
<h2 id="5-파일-누락-시-동작">5. 파일 누락 시 동작</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="header">
|
||||||
|
<th>누락 파일</th>
|
||||||
|
<th>동작</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>영상</td>
|
||||||
|
<td>재생 불가 (폴더 인식 실패)</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>드론 비행로그</td>
|
||||||
|
<td>측점 투영·POI 오버레이·fps 산출 불가 → 사실상 일반 플레이어로 전락</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>측점 CSV</td>
|
||||||
|
<td>중심선·스테이션바·POI 높이 보완 불가 (시간축 재생만 가능)</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>KMZ/KML</td>
|
||||||
|
<td><strong>경고 로그</strong> 후 POI·구조물 미표시 (측점·드론·영상은 정상 동작)</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>route.json</td>
|
||||||
|
<td>기본값으로 동작 (노선명 등 미표시, stationTolerance=40)</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>poi_overrides.json</td>
|
||||||
|
<td>보정 없이 원본 좌표 사용</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<hr />
|
||||||
|
<h2 id="부록--구현-근거-소스-위치">부록 — 구현 근거 (소스 위치)</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="header">
|
||||||
|
<th>항목</th>
|
||||||
|
<th>위치</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>폴더 통합 로더</td>
|
||||||
|
<td><code>client/src/utils/geoData.ts</code> — <code>loadFolderGeoData()</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>영상/base 식별</td>
|
||||||
|
<td>같은 파일 — <code>findVideoFile()</code>, <code>deriveBaseName()</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>드론 CSV 파서</td>
|
||||||
|
<td>같은 파일 — <code>parseDroneFrames()</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>측점 CSV 파서 (+방향전환점)</td>
|
||||||
|
<td>같은 파일 — <code>parseStations()</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>KMZ/KML 파서</td>
|
||||||
|
<td>같은 파일 — <code>parseKmz()</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>route.json / 구조물 병합</td>
|
||||||
|
<td>같은 파일 — <code>parseRouteMeta()</code>, <code>mergeStructures()</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>POI 보정 로드/적용</td>
|
||||||
|
<td>같은 파일 — <code>parsePoiOverrides()</code>, <code>applyPoiOverrides()</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>인코딩 자동 감지</td>
|
||||||
|
<td>같은 파일 — <code>decodeBytes()</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>중심선 생성</td>
|
||||||
|
<td>같은 파일 — <code>buildCenterlineFromStations()</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>(참고) 서버 검색 API 용 데이터</td>
|
||||||
|
<td><code>server/src/routes/geo.ts</code> — 환경변수 <code>GEO_DATA_DIR</code> 폴더에 동일 형상 필요</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
# 데이터 폴더 입력 명세 — 스테이션기반 플레이어
|
||||||
|
|
||||||
|
> 웹플레이어가 **동영상이 있는 폴더**를 전달받아 재생할 때, 폴더 안에 어떤 파일이
|
||||||
|
> 어떤 형식으로 있어야 하는지를 정의한다.
|
||||||
|
> 근거: `client/src/utils/geoData.ts`(폴더 파서) 기준. 실데이터(v3.0, 대전조차장–회덕)로 검증.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 동작 개요
|
||||||
|
|
||||||
|
1. 사용자가 브라우저에서 **폴더를 선택**하면(`<input type="file" webkitdirectory>`), 폴더 내 전체 파일 목록이 플레이어에 전달된다.
|
||||||
|
2. 플레이어는 파일명 규칙으로 각 파일을 **자동 식별·파싱**한다(별도 설정 불필요).
|
||||||
|
3. 기준 이름 **base** = 영상 파일명에서 확장자를 뺀 것 (예: `상행)대전조차장-회덕.MP4` → base는 `상행)대전조차장-회덕`). 나머지 파일은 base 일치를 우선 매칭하고, 없으면 폴백 규칙으로 찾는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 폴더 구성 요약
|
||||||
|
|
||||||
|
| # | 파일 | 필수 | 역할 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| 1 | `<base>.MP4` | **필수** | 드론 촬영 항공영상 (mp4/webm) |
|
||||||
|
| 2 | `<base>.csv` | **필수** | 드론 비행로그 — 프레임별 위치·자세각·초점거리 |
|
||||||
|
| 3 | `01)측점.csv` 또는 `측점.csv` | **필수** | 측점 목록(이정·좌표·실측 표고) → 중심선·스테이션바·POI 높이 보완의 근간 |
|
||||||
|
| 4 | `<base>.kmz` (또는 `.kml`) | **필수**(정책) | POI·구조물의 **유일한 소스** (지장물·교량·터널·출입문·구교·철도역) |
|
||||||
|
| 5 | `<base>.route.json` 또는 `route.json` | 선택 | 노선 기본 정보 + 구조물 위치 보정 |
|
||||||
|
| 6 | `<base>_poi_overrides.json` | 선택 | POI 드래그 보정 저장값 (역투영 결과) |
|
||||||
|
| — | `.srt`, `.qgz`, `.qgs` 등 그 외 파일 | 무시 | 플레이어가 읽지 않음 (작업용 부산물) |
|
||||||
|
|
||||||
|
**예시 폴더** (실데이터 v3.0):
|
||||||
|
|
||||||
|
```
|
||||||
|
상행)대전조차장-회덕 데이터(v3.0)/
|
||||||
|
├── 상행)대전조차장-회덕.MP4 ← 영상 (base 기준)
|
||||||
|
├── 상행)대전조차장-회덕.csv ← 드론 비행로그
|
||||||
|
├── 01)측점.csv ← 측점 (루트 또는 building/ 하위 모두 허용)
|
||||||
|
├── 상행)대전조차장-회덕.kmz ← POI·구조물 원본
|
||||||
|
├── 상행)대전조차장-회덕.route.json ← 노선 정보 (선택)
|
||||||
|
├── 상행)대전조차장-회덕.srt ← (무시됨)
|
||||||
|
└── 상행)대전조차장-회덕.qgz ← (무시됨, QGIS 작업파일)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 파일별 상세 명세
|
||||||
|
|
||||||
|
### 3.1 영상 — `<base>.MP4`
|
||||||
|
|
||||||
|
- **확장자**: `.mp4` 또는 `.webm` (대소문자 무관). `building/` 하위가 아닌 **루트의 첫 번째 영상 파일**을 사용한다.
|
||||||
|
- **코덱**: 브라우저 직접 재생이므로 **H.264 권장**.
|
||||||
|
- **용량**: 2GB 이상 가능 (`URL.createObjectURL()` 참조 방식 — 메모리에 통째로 올리지 않음).
|
||||||
|
- 이 파일명이 폴더 전체의 **base** 를 결정한다.
|
||||||
|
|
||||||
|
### 3.2 드론 비행로그 — `<base>.csv`
|
||||||
|
|
||||||
|
드론이 촬영과 동시에 기록한 프레임별 데이터. **헤더 이름으로 컬럼을 찾으며**(순서 무관), 헤더가 깨졌을 때만 위치 인덱스로 폴백한다.
|
||||||
|
|
||||||
|
| 컬럼(헤더명) | 의미 | 단위/규약 |
|
||||||
|
|---|---|---|
|
||||||
|
| `frame_cnt` | 프레임 번호 (0부터) | 정수 |
|
||||||
|
| `latitude` / `longitude` | 드론 위치 위도/경도 | 도(WGS84) |
|
||||||
|
| `altitude` | 드론 고도 | m (타원체고) |
|
||||||
|
| `yaw` | 기수 방위각 | 도 — 북=0, 시계방향 |
|
||||||
|
| `pitch` | 카메라 틸트 | 도 — 음수=아래 |
|
||||||
|
| `roll` | 롤 | 도 |
|
||||||
|
| `focal_len` | 초점거리 | mm (**35mm 환산**) |
|
||||||
|
|
||||||
|
- 추가 컬럼(`rel_alt`, `5186x`, `5186y`, `X좌표`, `Y좌표` 등)이 있어도 **무시되고 오류 없음**.
|
||||||
|
- `latitude` 가 숫자가 아닌 행은 건너뜀.
|
||||||
|
- **파일 선택 규칙**: 루트의 `.csv` 중 ① `<base>.csv` 정확 일치 우선, ② 없으면 파일명에 `POI` 가 없는 첫 `.csv` (영상명 비의존 폴백).
|
||||||
|
- **fps 자동 산출**: 최대 `frame_cnt` ÷ 영상 길이(초) → 표준 fps(24/25/29.97/30/50/60…)에 스냅. 따라서 **비행로그의 프레임 수는 영상의 실제 프레임 수와 일치해야 한다.**
|
||||||
|
|
||||||
|
### 3.3 측점 — `01)측점.csv` (또는 `측점.csv`)
|
||||||
|
|
||||||
|
철도 노선 측량 성과. 시간↔측점 대응표(스테이션바)와 선로 중심선, POI 높이 보완의 근간 데이터.
|
||||||
|
|
||||||
|
- **파일 찾기 규칙** (3단 폴백): ① `building/` 하위에서 파일명에 `01)측점` 포함 → ② 루트 포함 아무 위치에서 `01)측점` 포함 → ③ 파일명에 `측점` 포함.
|
||||||
|
- **컬럼** (헤더 이름 기준):
|
||||||
|
|
||||||
|
| 컬럼 | 필수 | 의미 |
|
||||||
|
|---|---|---|
|
||||||
|
| `측점` | **필수** | 이정 표기 (예 `157K900`) — 스테이션 정렬·표시 기준 |
|
||||||
|
| `lat` / `lon` | **필수** | 측점 좌표 (도, WGS84) — 숫자 아니면 해당 행 제외 |
|
||||||
|
| `Z좌표_한국` | 권장 | **실측 정표고(EL, m)** — 있으면 최우선 사용 |
|
||||||
|
| `Z좌표` | 폴백 | `Z좌표_한국` 이 없을 때 사용 |
|
||||||
|
| `비고` | 선택 | `방향전환점(상행->하행, 02:05)` 패턴이 있으면 방향전환점으로 추출 |
|
||||||
|
|
||||||
|
- **중심선 자동 생성**: v2.0 이후 `center.csv` 가 없으므로, 측점을 이정 순으로 이어 만든 폴리라인이 선로 중심선이 된다. → **측점 정확도가 곧 측점 투영·POI 높이 보완의 정확도.**
|
||||||
|
|
||||||
|
> ⚠️ **주의 (실데이터에서 발견)**: 상행 v3.0의 `01)측점.csv` 는 실측 표고가 `z` 컬럼에 있고 `Z좌표` 컬럼은 0.0 인데, 파서는 `Z좌표_한국` → `Z좌표` 순서로만 읽으므로 **표고가 0으로 로드된다**. 하행 파일처럼 표고 컬럼명을 **`Z좌표_한국` 으로 통일**하거나, 파서에 `z` 컬럼 폴백 추가가 필요하다.
|
||||||
|
|
||||||
|
### 3.4 POI·구조물 — `<base>.kmz` (또는 `.kml`)
|
||||||
|
|
||||||
|
**POI·구조물의 유일한 소스**다 (`building/` 하위 02)~06) CSV는 더 이상 읽지 않음). 루트의 bare `.kml` 이 있으면 우선, 없으면 `.kmz`(zip)를 해제해 `doc.kml` 을 읽는다.
|
||||||
|
|
||||||
|
- **KML 폴더 구조 → 분류 매핑**:
|
||||||
|
|
||||||
|
| KML 폴더명 (포함 키워드) | 분류 | 이름 필드 (description 표) |
|
||||||
|
|---|---|---|
|
||||||
|
| `03)교량` | 구조물 bridge | `구분` (+ `연장(m)`, `시설종별`) |
|
||||||
|
| `04)터널` | 구조물 tunnel | `구분` (+ `연장(m)`, `시설종별`) |
|
||||||
|
| `06)구교` | 구조물 bridge(구교) | `시설물명` (+ `연장(m)`) |
|
||||||
|
| `05)출입문번호` | POI (출입문) | `출입문번호` |
|
||||||
|
| `02)지장물` 등 그 외 | POI (지장물 등) | `구분` → `title` → placemark `name` 순 |
|
||||||
|
| — `source=KAKAO_RAIL` 또는 `category_clean=철도역/역사` | POI + **역사 구조물** (스테이션바에도 표출) | 위와 동일 |
|
||||||
|
|
||||||
|
- **Placemark 좌표**: `<coordinates>` 의 `lon,lat[,alt]` 우선. 없으면 description 속성 `lat`/`lon`. **한국 범위 검증**(위도 33~39, 경도 124~132) 밖이면 버림.
|
||||||
|
- **속성(description)**: HTML `<table>` 의 (키, 값) 행으로 기재 — `구분`, `연장(m)`, `시설종별`(1종/2종/3종/기타), `시설물명`, `출입문번호`, `source`, `category_clean`, `title`, 표고(`z`/`Z좌표`/`절대고도`) 등. 구글어스 재저장본의 표 변형(`<tbody>`, 엔티티 등)도 허용.
|
||||||
|
- **표고**: POI 속성에 `z`/`절대고도` 가 있으면 사용, 없으면 0 → 재생 시 **최근접 측점 실측 표고로 보완**된다.
|
||||||
|
- **누락 시**: 경고 로그 후 POI·구조물 없이 동작(측점·드론·영상은 정상). CSV 폴백은 없다.
|
||||||
|
|
||||||
|
### 3.5 노선 기본 정보 — `<base>.route.json` (선택)
|
||||||
|
|
||||||
|
루트의 `<base>.route.json` 우선, 없으면 `route.json`. UTF-8 JSON.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"routeInfo": {
|
||||||
|
"name": "대전조차장-회덕", // 노선명 (표시용)
|
||||||
|
"direction": "상행", // 방향 라벨
|
||||||
|
"lengthKm": 4.25, // 구간 연장 (km)
|
||||||
|
"durationSec": 568, // 영상 길이 (초)
|
||||||
|
"startStationName": "대전", // 시점역
|
||||||
|
"endStationName": "신탄진", // 종점역
|
||||||
|
"stationTolerance": 20, // 측점 인정 반경(m) — 기본 40
|
||||||
|
"endStationGapMeters": 0 // 종점 미도착 거리(m) — 0이면 도착
|
||||||
|
},
|
||||||
|
"structures": [ // (선택) 구조물 보정 — name 매칭으로 override
|
||||||
|
{
|
||||||
|
"name": "회덕천교", // 괄호 이하 제거 후 부분일치 매칭
|
||||||
|
"station": "158k400", // 최우선: 이 측점값 위치에 배치 (숫자 m 또는 "158k400")
|
||||||
|
"offset": -10, // 진행방향 ±N m 미세조정
|
||||||
|
"startMileage": 158400, // 매칭 실패 시 폴백 이정
|
||||||
|
"endMileage": 158500,
|
||||||
|
"lat": 36.4031, "lon": 127.4216 // 좌표 직접 지정도 가능
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `structures` 항목은 KMZ 유래 구조물에 **명시된 필드만 덮어쓰고**, KMZ에 없는 항목은 새 구조물로 추가된다.
|
||||||
|
|
||||||
|
### 3.6 POI 위치 보정 — `<base>_poi_overrides.json` (선택)
|
||||||
|
|
||||||
|
영상에서 POI 라벨을 **드래그(역투영)** 해 보정한 좌표의 저장/배포 파일. 폴더에 있으면 로드 시 자동 적용된다.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"baseName": "상행)대전조차장-회덕",
|
||||||
|
"overrides": {
|
||||||
|
"회덕터널": { "lat": 36.40312, "lon": 127.42169, "z": 66.1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `overrides` 없이 평면 맵(`{ "회덕터널": {…} }`)도 허용. `z` 는 정표고(m). POI `title` 정확 일치로 매칭.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 공통 규칙 (인코딩·파싱)
|
||||||
|
|
||||||
|
| 항목 | 규칙 |
|
||||||
|
|---|---|
|
||||||
|
| CSV 인코딩 | **자동 감지** — 앞 3바이트가 UTF-8 BOM(EF BB BF)이면 UTF-8, 아니면 EUC-KR. 파일별 하드코딩 없음 |
|
||||||
|
| CSV 컬럼 | **헤더 이름으로 탐색** (순서 무관, 여분 컬럼 무시). 헤더 훼손 시 위치 인덱스 폴백 |
|
||||||
|
| CSV 형식 | 큰따옴표 필드 지원, 빈 줄 무시, `\r\n`/`\n` 모두 허용 |
|
||||||
|
| KML | UTF-8. `.kml` > `.kmz`(내부 `doc.kml`) 우선순위 |
|
||||||
|
| JSON | UTF-8. 파싱 실패 시 조용히 무시(선택 파일이므로) |
|
||||||
|
| 폴더 구조 | `building/` 하위는 측점 CSV 탐색 외에는 사용하지 않음. 하위 폴더 깊이는 1단계(`building/`)만 인식 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 파일 누락 시 동작
|
||||||
|
|
||||||
|
| 누락 파일 | 동작 |
|
||||||
|
|---|---|
|
||||||
|
| 영상 | 재생 불가 (폴더 인식 실패) |
|
||||||
|
| 드론 비행로그 | 측점 투영·POI 오버레이·fps 산출 불가 → 사실상 일반 플레이어로 전락 |
|
||||||
|
| 측점 CSV | 중심선·스테이션바·POI 높이 보완 불가 (시간축 재생만 가능) |
|
||||||
|
| KMZ/KML | **경고 로그** 후 POI·구조물 미표시 (측점·드론·영상은 정상 동작) |
|
||||||
|
| route.json | 기본값으로 동작 (노선명 등 미표시, stationTolerance=40) |
|
||||||
|
| poi_overrides.json | 보정 없이 원본 좌표 사용 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 부록 — 구현 근거 (소스 위치)
|
||||||
|
|
||||||
|
| 항목 | 위치 |
|
||||||
|
|---|---|
|
||||||
|
| 폴더 통합 로더 | `client/src/utils/geoData.ts` — `loadFolderGeoData()` |
|
||||||
|
| 영상/base 식별 | 같은 파일 — `findVideoFile()`, `deriveBaseName()` |
|
||||||
|
| 드론 CSV 파서 | 같은 파일 — `parseDroneFrames()` |
|
||||||
|
| 측점 CSV 파서 (+방향전환점) | 같은 파일 — `parseStations()` |
|
||||||
|
| KMZ/KML 파서 | 같은 파일 — `parseKmz()` |
|
||||||
|
| route.json / 구조물 병합 | 같은 파일 — `parseRouteMeta()`, `mergeStructures()` |
|
||||||
|
| POI 보정 로드/적용 | 같은 파일 — `parsePoiOverrides()`, `applyPoiOverrides()` |
|
||||||
|
| 인코딩 자동 감지 | 같은 파일 — `decodeBytes()` |
|
||||||
|
| 중심선 생성 | 같은 파일 — `buildCenterlineFromStations()` |
|
||||||
|
| (참고) 서버 검색 API 용 데이터 | `server/src/routes/geo.ts` — 환경변수 `GEO_DATA_DIR` 폴더에 동일 형상 필요 |
|
||||||
Binary file not shown.
@@ -0,0 +1,273 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang xml:lang>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="pandoc" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
|
||||||
|
<title>발표_GhiVideo_경영진요약_3장</title>
|
||||||
|
<style>
|
||||||
|
code{white-space: pre-wrap;}
|
||||||
|
span.smallcaps{font-variant: small-caps;}
|
||||||
|
span.underline{text-decoration: underline;}
|
||||||
|
div.column{display: inline-block; vertical-align: top; width: 50%;}
|
||||||
|
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
|
||||||
|
ul.task-list{list-style: none;}
|
||||||
|
.display.math{display: block; text-align: center; margin: 0.5rem auto;}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">@page {
|
||||||
|
size: A4;
|
||||||
|
margin: 18mm 16mm 16mm 16mm;
|
||||||
|
@bottom-center {
|
||||||
|
content: counter(page) " / " counter(pages);
|
||||||
|
font-family: "Malgun Gothic", sans-serif;
|
||||||
|
font-size: 9pt;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html { font-size: 11pt; }
|
||||||
|
body {
|
||||||
|
font-family: "Malgun Gothic", "Hancom Gothic", sans-serif;
|
||||||
|
color: #23272e;
|
||||||
|
line-height: 1.65;
|
||||||
|
max-width: 920px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.7rem;
|
||||||
|
color: #b45309;
|
||||||
|
border-bottom: 3px solid #f59e0b;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #b45309;
|
||||||
|
border-bottom: 1px solid #e5d3b3;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
margin-top: 1.6em;
|
||||||
|
}
|
||||||
|
h3 { font-size: 1.05rem; color: #92400e; margin-top: 1.1em; }
|
||||||
|
a { color: #b45309; }
|
||||||
|
hr { border: none; border-top: 1px solid #e2e2e2; margin: 1.6em 0; }
|
||||||
|
ul { padding-left: 1.25em; }
|
||||||
|
li { margin: 0.18em 0; }
|
||||||
|
strong { color: #1f2937; }
|
||||||
|
code {
|
||||||
|
font-family: "D2Coding", Consolas, monospace;
|
||||||
|
background: #f4f1ea;
|
||||||
|
border: 1px solid #e7e0d2;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.5px 5px;
|
||||||
|
font-size: 0.92em;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0.8em 0;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
th, td { border: 1px solid #d8d2c4; padding: 6px 10px; text-align: left; vertical-align: top; }
|
||||||
|
th { background: #fdf3df; color: #7c2d12; }
|
||||||
|
blockquote {
|
||||||
|
border-left: 4px solid #f59e0b;
|
||||||
|
margin: 0.8em 0;
|
||||||
|
padding: 0.2em 0 0.2em 14px;
|
||||||
|
color: #555;
|
||||||
|
background: #fffbf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3 { break-after: avoid; }
|
||||||
|
</style>
|
||||||
|
<!--[if lt IE 9]>
|
||||||
|
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
|
||||||
|
<![endif]-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header id="title-block-header">
|
||||||
|
<h1 class="title">발표_GhiVideo_경영진요약_3장</h1>
|
||||||
|
</header>
|
||||||
|
<h1 id="ghivideo--철도-점검용-드론-주행영상-지능형-플레이어">GhiVideo — 철도 점검용 드론 주행영상 지능형 플레이어</h1>
|
||||||
|
<p class="subhead">경영진 보고 요약 (3장) · 기술개발센터 · 2026-07-03</p>
|
||||||
|
|
||||||
|
<h2 id="한-줄-요약">한 줄 요약</h2>
|
||||||
|
<blockquote>
|
||||||
|
<p>드론으로 촬영한 철도 주행영상을 <strong>'측점(위치)' 기준으로 찾아보고</strong>, 영상 위에 <strong>교량·터널·역 이름표를 정확한 자리에 겹쳐</strong> 보여주는 <strong>웹 기반 점검·분석 플레이어</strong>입니다. 설치 없이 브라우저로 열고, <strong>데이터 폴더 하나만 넣으면</strong> 바로 동작합니다.</p>
|
||||||
|
</blockquote>
|
||||||
|
<h2 id="왜-필요한가">왜 필요한가</h2>
|
||||||
|
<ul>
|
||||||
|
<li>기존 방식은 긴 영상을 <strong>시간(분·초)</strong> 으로 뒤져야 해, 특정 구조물·위치 찾기가 번거로움</li>
|
||||||
|
<li>현장 점검의 판단 단위는 '몇 분 지점'이 아니라 <strong>'어느 측점 · 어느 교량/터널'</strong></li>
|
||||||
|
<li><strong>2GB 이상 대용량 영상</strong>은 일반 플레이어에서 버벅임·탐색 불안정 발생</li>
|
||||||
|
</ul>
|
||||||
|
<h2 id="무엇을-하는가--3가지-핵심">무엇을 하는가 — 3가지 핵심</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="header">
|
||||||
|
<th style="text-align: center;">#</th>
|
||||||
|
<th>기능</th>
|
||||||
|
<th>설명</th>
|
||||||
|
<th>효과</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="odd">
|
||||||
|
<td style="text-align: center;">①</td>
|
||||||
|
<td><strong>측점 기반 탐색</strong></td>
|
||||||
|
<td>시간축 대신 노선 <strong>측점(위치)축</strong>으로 이동·검색</td>
|
||||||
|
<td>원하는 위치로 바로 이동</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td style="text-align: center;">②</td>
|
||||||
|
<td><strong>영상 AR 정합</strong></td>
|
||||||
|
<td>드론 위치·자세로 측점·구조물·선로중심선을 영상에 <strong>실시간 겹침</strong></td>
|
||||||
|
<td>화면에서 즉시 식별</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td style="text-align: center;">③</td>
|
||||||
|
<td><strong>대용량 재생</strong></td>
|
||||||
|
<td>2GB+ 영상을 웹에서 스트리밍 재생</td>
|
||||||
|
<td>설치 없이 브라우저로</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<figure>
|
||||||
|
<svg viewBox="0 0 760 150" role="img" aria-label="처리 흐름도">
|
||||||
|
<defs><marker id="ha" markerWidth="9" markerHeight="9" refX="6" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6 Z" fill="#14305e"></path></marker></defs>
|
||||||
|
<rect x="8" y="55" width="120" height="46" rx="5" fill="#eef2f8" stroke="#14305e"></rect>
|
||||||
|
<text x="68" y="74" text-anchor="middle" font-size="11" font-weight="bold" fill="#14305e">폴더 입력</text>
|
||||||
|
<text x="68" y="90" text-anchor="middle" font-size="9" fill="#555">영상·로그·CSV·KMZ</text>
|
||||||
|
<rect x="160" y="55" width="120" height="46" rx="5" fill="#fff" stroke="#555"></rect>
|
||||||
|
<text x="220" y="74" text-anchor="middle" font-size="11" font-weight="bold">파싱</text>
|
||||||
|
<text x="220" y="90" text-anchor="middle" font-size="9" fill="#555">인코딩 자동감지</text>
|
||||||
|
<rect x="312" y="55" width="138" height="46" rx="5" fill="#fff7e6" stroke="#b45309"></rect>
|
||||||
|
<text x="381" y="74" text-anchor="middle" font-size="11" font-weight="bold" fill="#b45309">좌표·측점 계산</text>
|
||||||
|
<text x="381" y="90" text-anchor="middle" font-size="9" fill="#7c2d12">투영 · 체이니지</text>
|
||||||
|
<rect x="496" y="10" width="256" height="36" rx="5" fill="#fff" stroke="#16a34a"></rect>
|
||||||
|
<text x="624" y="33" text-anchor="middle" font-size="10.5" font-weight="bold" fill="#14532d">① 영상 AR 오버레이</text>
|
||||||
|
<rect x="496" y="60" width="256" height="36" rx="5" fill="#fff" stroke="#16a34a"></rect>
|
||||||
|
<text x="624" y="83" text-anchor="middle" font-size="10.5" font-weight="bold" fill="#14532d">② 측점 스테이션바(탐색)</text>
|
||||||
|
<rect x="496" y="110" width="256" height="36" rx="5" fill="#fff" stroke="#16a34a"></rect>
|
||||||
|
<text x="624" y="133" text-anchor="middle" font-size="10.5" font-weight="bold" fill="#14532d">③ 대용량 영상 재생</text>
|
||||||
|
<line x1="128" y1="78" x2="158" y2="78" stroke="#14305e" stroke-width="1.5" marker-end="url(#ha)"></line>
|
||||||
|
<line x1="280" y1="78" x2="310" y2="78" stroke="#14305e" stroke-width="1.5" marker-end="url(#ha)"></line>
|
||||||
|
<line x1="450" y1="78" x2="494" y2="28" stroke="#14305e" stroke-width="1.5" marker-end="url(#ha)"></line>
|
||||||
|
<line x1="450" y1="78" x2="494" y2="78" stroke="#14305e" stroke-width="1.5" marker-end="url(#ha)"></line>
|
||||||
|
<line x1="450" y1="78" x2="494" y2="128" stroke="#14305e" stroke-width="1.5" marker-end="url(#ha)"></line>
|
||||||
|
</svg>
|
||||||
|
<figcaption>데이터 폴더 입력 → 자동 파싱 → 좌표·측점 계산 → (오버레이 / 스테이션바 / 재생)</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<div style="break-before: page;"></div>
|
||||||
|
|
||||||
|
<h2 id="핵심-기술과-차별성">핵심 기술과 차별성</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="header">
|
||||||
|
<th>대표 기술</th>
|
||||||
|
<th>핵심 내용</th>
|
||||||
|
<th>차별점</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="odd">
|
||||||
|
<td><strong>측점 표고 기반 정합</strong></td>
|
||||||
|
<td>노선 실측 정표고를 지면고도로 사용 (지오이드 datum 정합)</td>
|
||||||
|
<td>외부 지형데이터(DEM) <strong>불필요</strong>, 더 정밀</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td><strong>단일 드래그 보정</strong></td>
|
||||||
|
<td>화면에서 라벨을 <strong>한 번 끌면</strong> 가로·세로·거리 동시 교정</td>
|
||||||
|
<td>깊이맵/재측량 없이 손쉬운 교정</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td><strong>실시간 투영 렌더링</strong></td>
|
||||||
|
<td>드론 위치·자세·초점 → 화면 픽셀 정렬, 클라이언트 계산</td>
|
||||||
|
<td>서버 없이 <strong>60fps</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td><strong>대용량 스트리밍</strong></td>
|
||||||
|
<td>Range/HLS 분할 스트리밍, 메모리 자동 해제</td>
|
||||||
|
<td>2GB+ 안정 재생</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p>▷ <strong>정량 성과</strong></p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="header">
|
||||||
|
<th style="text-align: center;">영상 정합(하향각) 오차</th>
|
||||||
|
<th style="text-align: center;">표고 일치도(구글 DEM 대비)</th>
|
||||||
|
<th style="text-align: center;">렌더 성능</th>
|
||||||
|
<th style="text-align: center;">위치 보정 정확도</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="odd">
|
||||||
|
<td style="text-align: center;"><strong>42.7° → 11.0°</strong></td>
|
||||||
|
<td style="text-align: center;"><strong>≈ 0.2 m</strong></td>
|
||||||
|
<td style="text-align: center;"><strong>4K · 60 fps</strong></td>
|
||||||
|
<td style="text-align: center;"><strong>왕복오차 ≈ 0</strong></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<blockquote>
|
||||||
|
<p><strong>차별화 한마디</strong> — 경쟁 방식은 외부 지형데이터(DEM)를 별도로 구매·전처리해야 하지만, 우리는 <strong>이미 보유한 측점 실측 표고</strong>를 그대로 활용해 <strong>추가비용 0 · 더 높은 정밀도(~0.2m)</strong> 로 영상 정합을 구현했습니다. 오차가 나도 화면 <strong>드래그 한 번</strong>으로 교정됩니다.</p>
|
||||||
|
</blockquote>
|
||||||
|
<h2 id="지식재산-특허">지식재산 (특허)</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>특허 후보 2건 발굴</strong> — ① <em>측점(위치) 기반 주행영상 재생</em> ② <em>드론 좌표투영·측점표고 정합</em></li>
|
||||||
|
<li>선행기술조사(글로벌·KIPRIS) <strong>일부 수행</strong>, 명세서 초안 작성 완료</li>
|
||||||
|
<li>다음 단계: <strong>KIPRIS 전문검색 → 변리사 검토 → 출원</strong></li>
|
||||||
|
</ul>
|
||||||
|
<div style="break-before: page;"></div>
|
||||||
|
|
||||||
|
<h2 id="개발-현황">개발 현황</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>동작하는 제품 완성</strong> — 사내망 접속(포트 55000)으로 <strong>즉시 시연 가능</strong></li>
|
||||||
|
<li>데이터 폴더만 넣으면 영상·비행로그·측점·구조물을 <strong>자동 적재</strong></li>
|
||||||
|
<li>보고서 <strong>자동 생성 도구</strong>(md → HTML/PDF, 한글 임베드) 구축</li>
|
||||||
|
</ul>
|
||||||
|
<h2 id="기대-효과">기대 효과</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="header">
|
||||||
|
<th>구분</th>
|
||||||
|
<th>효과</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>점검 효율</td>
|
||||||
|
<td>위치(측점)·구조물 단위로 <strong>바로 탐색</strong> → 판독 시간 단축</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>정확도</td>
|
||||||
|
<td>실측 표고 기반 정합으로 <strong>화면 식별 신뢰도↑</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>비용</td>
|
||||||
|
<td>외부 지형데이터 구매 불필요, <strong>웹 기반</strong>이라 설치·배포 부담↓</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>보고</td>
|
||||||
|
<td>결과 문서화 <strong>자동화</strong>로 반복 작업 감소</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h2 id="향후-계획">향후 계획</h2>
|
||||||
|
<ol type="1">
|
||||||
|
<li><strong>특허 출원 추진</strong> — KIPRIS 전문검색 → 변리사 검토 → 출원</li>
|
||||||
|
<li><strong>렌즈 왜곡 자동 캘리브레이션</strong> — 영상별 화각 정합 자동화 고도화</li>
|
||||||
|
<li><strong>측점 미보유 구간 보강</strong> — 필요 구간에 한해 외부 DEM 자동 보조</li>
|
||||||
|
</ol>
|
||||||
|
<h2 id="검토-요청-의사결정">검토 요청 (의사결정)</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>특허 출원 진행 승인</strong> 및 변리사 선임</li>
|
||||||
|
<li>(선택) <strong>현업 점검팀 시범 적용</strong> 대상 노선 지정</li>
|
||||||
|
</ul>
|
||||||
|
<blockquote>
|
||||||
|
<p>상세 기술·수식·소스 근거는 별첨 「기술명세_GhiVideo_종합기술문서」 및 「보고서_GhiVideo_개발내용및핵심기술」 참조.</p>
|
||||||
|
</blockquote>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# GhiVideo — 철도 점검용 드론 주행영상 지능형 플레이어
|
||||||
|
|
||||||
|
<p class="subhead">경영진 보고 요약 (3장) · 기술개발센터 · 2026-07-03</p>
|
||||||
|
|
||||||
|
## 한 줄 요약
|
||||||
|
|
||||||
|
> 드론으로 촬영한 철도 주행영상을 **'측점(위치)' 기준으로 찾아보고**, 영상 위에 **교량·터널·역 이름표를 정확한 자리에 겹쳐** 보여주는 **웹 기반 점검·분석 플레이어**입니다.
|
||||||
|
> 설치 없이 브라우저로 열고, **데이터 폴더 하나만 넣으면** 바로 동작합니다.
|
||||||
|
|
||||||
|
## 왜 필요한가
|
||||||
|
|
||||||
|
- 기존 방식은 긴 영상을 **시간(분·초)** 으로 뒤져야 해, 특정 구조물·위치 찾기가 번거로움
|
||||||
|
- 현장 점검의 판단 단위는 '몇 분 지점'이 아니라 **'어느 측점 · 어느 교량/터널'**
|
||||||
|
- **2GB 이상 대용량 영상**은 일반 플레이어에서 버벅임·탐색 불안정 발생
|
||||||
|
|
||||||
|
## 무엇을 하는가 — 3가지 핵심
|
||||||
|
|
||||||
|
| # | 기능 | 설명 | 효과 |
|
||||||
|
|:-:|------|------|------|
|
||||||
|
| ① | **측점 기반 탐색** | 시간축 대신 노선 **측점(위치)축**으로 이동·검색 | 원하는 위치로 바로 이동 |
|
||||||
|
| ② | **영상 AR 정합** | 드론 위치·자세로 측점·구조물·선로중심선을 영상에 **실시간 겹침** | 화면에서 즉시 식별 |
|
||||||
|
| ③ | **대용량 재생** | 2GB+ 영상을 웹에서 스트리밍 재생 | 설치 없이 브라우저로 |
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<svg viewBox="0 0 760 150" role="img" aria-label="처리 흐름도">
|
||||||
|
<defs><marker id="ha" markerWidth="9" markerHeight="9" refX="6" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6 Z" fill="#14305e"/></marker></defs>
|
||||||
|
<rect x="8" y="55" width="120" height="46" rx="5" fill="#eef2f8" stroke="#14305e"/>
|
||||||
|
<text x="68" y="74" text-anchor="middle" font-size="11" font-weight="bold" fill="#14305e">폴더 입력</text>
|
||||||
|
<text x="68" y="90" text-anchor="middle" font-size="9" fill="#555">영상·로그·CSV·KMZ</text>
|
||||||
|
<rect x="160" y="55" width="120" height="46" rx="5" fill="#fff" stroke="#555"/>
|
||||||
|
<text x="220" y="74" text-anchor="middle" font-size="11" font-weight="bold">파싱</text>
|
||||||
|
<text x="220" y="90" text-anchor="middle" font-size="9" fill="#555">인코딩 자동감지</text>
|
||||||
|
<rect x="312" y="55" width="138" height="46" rx="5" fill="#fff7e6" stroke="#b45309"/>
|
||||||
|
<text x="381" y="74" text-anchor="middle" font-size="11" font-weight="bold" fill="#b45309">좌표·측점 계산</text>
|
||||||
|
<text x="381" y="90" text-anchor="middle" font-size="9" fill="#7c2d12">투영 · 체이니지</text>
|
||||||
|
<rect x="496" y="10" width="256" height="36" rx="5" fill="#fff" stroke="#16a34a"/>
|
||||||
|
<text x="624" y="33" text-anchor="middle" font-size="10.5" font-weight="bold" fill="#14532d">① 영상 AR 오버레이</text>
|
||||||
|
<rect x="496" y="60" width="256" height="36" rx="5" fill="#fff" stroke="#16a34a"/>
|
||||||
|
<text x="624" y="83" text-anchor="middle" font-size="10.5" font-weight="bold" fill="#14532d">② 측점 스테이션바(탐색)</text>
|
||||||
|
<rect x="496" y="110" width="256" height="36" rx="5" fill="#fff" stroke="#16a34a"/>
|
||||||
|
<text x="624" y="133" text-anchor="middle" font-size="10.5" font-weight="bold" fill="#14532d">③ 대용량 영상 재생</text>
|
||||||
|
<line x1="128" y1="78" x2="158" y2="78" stroke="#14305e" stroke-width="1.5" marker-end="url(#ha)"/>
|
||||||
|
<line x1="280" y1="78" x2="310" y2="78" stroke="#14305e" stroke-width="1.5" marker-end="url(#ha)"/>
|
||||||
|
<line x1="450" y1="78" x2="494" y2="28" stroke="#14305e" stroke-width="1.5" marker-end="url(#ha)"/>
|
||||||
|
<line x1="450" y1="78" x2="494" y2="78" stroke="#14305e" stroke-width="1.5" marker-end="url(#ha)"/>
|
||||||
|
<line x1="450" y1="78" x2="494" y2="128" stroke="#14305e" stroke-width="1.5" marker-end="url(#ha)"/>
|
||||||
|
</svg>
|
||||||
|
<figcaption>데이터 폴더 입력 → 자동 파싱 → 좌표·측점 계산 → (오버레이 / 스테이션바 / 재생)</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<div style="break-before: page;"></div>
|
||||||
|
|
||||||
|
## 핵심 기술과 차별성
|
||||||
|
|
||||||
|
| 대표 기술 | 핵심 내용 | 차별점 |
|
||||||
|
|------|----------|--------|
|
||||||
|
| **측점 표고 기반 정합** | 노선 실측 정표고를 지면고도로 사용 (지오이드 datum 정합) | 외부 지형데이터(DEM) **불필요**, 더 정밀 |
|
||||||
|
| **단일 드래그 보정** | 화면에서 라벨을 **한 번 끌면** 가로·세로·거리 동시 교정 | 깊이맵/재측량 없이 손쉬운 교정 |
|
||||||
|
| **실시간 투영 렌더링** | 드론 위치·자세·초점 → 화면 픽셀 정렬, 클라이언트 계산 | 서버 없이 **60fps** |
|
||||||
|
| **대용량 스트리밍** | Range/HLS 분할 스트리밍, 메모리 자동 해제 | 2GB+ 안정 재생 |
|
||||||
|
|
||||||
|
▷ **정량 성과**
|
||||||
|
|
||||||
|
| 영상 정합(하향각) 오차 | 표고 일치도(구글 DEM 대비) | 렌더 성능 | 위치 보정 정확도 |
|
||||||
|
|:-:|:-:|:-:|:-:|
|
||||||
|
| **42.7° → 11.0°** | **≈ 0.2 m** | **4K · 60 fps** | **왕복오차 ≈ 0** |
|
||||||
|
|
||||||
|
> **차별화 한마디** — 경쟁 방식은 외부 지형데이터(DEM)를 별도로 구매·전처리해야 하지만, 우리는 **이미 보유한 측점 실측 표고**를 그대로 활용해 **추가비용 0 · 더 높은 정밀도(~0.2m)** 로 영상 정합을 구현했습니다. 오차가 나도 화면 **드래그 한 번**으로 교정됩니다.
|
||||||
|
|
||||||
|
## 지식재산 (특허)
|
||||||
|
|
||||||
|
- **특허 후보 2건 발굴** — ① *측점(위치) 기반 주행영상 재생* ② *드론 좌표투영·측점표고 정합*
|
||||||
|
- 선행기술조사(글로벌·KIPRIS) **일부 수행**, 명세서 초안 작성 완료
|
||||||
|
- 다음 단계: **KIPRIS 전문검색 → 변리사 검토 → 출원**
|
||||||
|
|
||||||
|
<div style="break-before: page;"></div>
|
||||||
|
|
||||||
|
## 개발 현황
|
||||||
|
|
||||||
|
- **동작하는 제품 완성** — 사내망 접속(포트 55000)으로 **즉시 시연 가능**
|
||||||
|
- 데이터 폴더만 넣으면 영상·비행로그·측점·구조물을 **자동 적재**
|
||||||
|
- 보고서 **자동 생성 도구**(md → HTML/PDF, 한글 임베드) 구축
|
||||||
|
|
||||||
|
## 기대 효과
|
||||||
|
|
||||||
|
| 구분 | 효과 |
|
||||||
|
|------|------|
|
||||||
|
| 점검 효율 | 위치(측점)·구조물 단위로 **바로 탐색** → 판독 시간 단축 |
|
||||||
|
| 정확도 | 실측 표고 기반 정합으로 **화면 식별 신뢰도↑** |
|
||||||
|
| 비용 | 외부 지형데이터 구매 불필요, **웹 기반**이라 설치·배포 부담↓ |
|
||||||
|
| 보고 | 결과 문서화 **자동화**로 반복 작업 감소 |
|
||||||
|
|
||||||
|
## 향후 계획
|
||||||
|
|
||||||
|
1. **특허 출원 추진** — KIPRIS 전문검색 → 변리사 검토 → 출원
|
||||||
|
2. **렌즈 왜곡 자동 캘리브레이션** — 영상별 화각 정합 자동화 고도화
|
||||||
|
3. **측점 미보유 구간 보강** — 필요 구간에 한해 외부 DEM 자동 보조
|
||||||
|
|
||||||
|
## 검토 요청 (의사결정)
|
||||||
|
|
||||||
|
- **특허 출원 진행 승인** 및 변리사 선임
|
||||||
|
- (선택) **현업 점검팀 시범 적용** 대상 노선 지정
|
||||||
|
|
||||||
|
> 상세 기술·수식·소스 근거는 별첨 「기술명세_GhiVideo_종합기술문서」 및 「보고서_GhiVideo_개발내용및핵심기술」 참조.
|
||||||
Binary file not shown.
@@ -212,14 +212,20 @@ h2{border-top:3px solid #e7e0d2;padding-top:14px;margin-top:34px;}
|
|||||||
<circle cx="300" cy="51" r="10" fill="#374151"></circle>
|
<circle cx="300" cy="51" r="10" fill="#374151"></circle>
|
||||||
<text x="45" y="82" font-size="11" fill="#374151">0:00</text><text x="330" y="82" font-size="11" fill="#374151">03:12</text><text x="632" y="82" font-size="11" fill="#374151">09:28</text>
|
<text x="45" y="82" font-size="11" fill="#374151">0:00</text><text x="330" y="82" font-size="11" fill="#374151">03:12</text><text x="632" y="82" font-size="11" fill="#374151">09:28</text>
|
||||||
<text x="350" y="104" text-anchor="middle" font-size="11" fill="#6b7280">"3분 12초 지점" — 여기가 어디 선로인지 모름 😵</text>
|
<text x="350" y="104" text-anchor="middle" font-size="11" fill="#6b7280">"3분 12초 지점" — 여기가 어디 선로인지 모름 😵</text>
|
||||||
|
<text x="350" y="150" text-anchor="middle" font-size="13" font-weight="bold" fill="#166534">GhiVideo (측점 축)</text>
|
||||||
<p><text x="350" y="150" text-anchor="middle" font-size="13" font-weight="bold" fill="#166534">GhiVideo (측점 축)</text> <rect x="40" y="164" width="620" height="26" rx="6" fill="#fde9c8" stroke="#e0a94f"></rect> <rect x="40" y="164" width="300" height="26" rx="6" fill="#f59e0b"></rect> <circle cx="340" cy="177" r="11" fill="#ea580c"></circle><text x="340" y="181" text-anchor="middle" font-size="9" fill="#fff" font-weight="bold">160k</text> <line x1="200" y1="160" x2="200" y2="194" stroke="#0284c7" stroke-width="2"></line><text x="200" y="210" text-anchor="middle" font-size="9" fill="#0369a1">회덕터널</text> <line x1="470" y1="160" x2="470" y2="194" stroke="#0284c7" stroke-width="2"></line><text x="470" y="210" text-anchor="middle" font-size="9" fill="#0369a1">회덕천교</text> <text x="45" y="228" font-size="11" fill="#166534">대전조차장</text><text x="600" y="228" font-size="11" fill="#166534">신탄진</text> </svg></p>
|
<rect x="40" y="164" width="620" height="26" rx="6" fill="#fde9c8" stroke="#e0a94f"></rect>
|
||||||
|
<rect x="40" y="164" width="300" height="26" rx="6" fill="#f59e0b"></rect>
|
||||||
|
<circle cx="340" cy="177" r="11" fill="#ea580c"></circle><text x="340" y="181" text-anchor="middle" font-size="9" fill="#fff" font-weight="bold">160k</text>
|
||||||
|
<line x1="200" y1="160" x2="200" y2="194" stroke="#0284c7" stroke-width="2"></line><text x="200" y="210" text-anchor="middle" font-size="9" fill="#0369a1">회덕터널</text>
|
||||||
|
<line x1="470" y1="160" x2="470" y2="194" stroke="#0284c7" stroke-width="2"></line><text x="470" y="210" text-anchor="middle" font-size="9" fill="#0369a1">회덕천교</text>
|
||||||
|
<text x="45" y="228" font-size="11" fill="#166534">대전조차장</text><text x="600" y="228" font-size="11" fill="#166534">신탄진</text>
|
||||||
|
</svg>
|
||||||
<figcaption>시간 축은 "몇 분"만 안다. 측점 축은 "지금 어느 선로 위치(측점)"와 "무슨 구조물 근처"인지 바로 보인다.</figcaption>
|
<figcaption>시간 축은 "몇 분"만 안다. 측점 축은 "지금 어느 선로 위치(측점)"와 "무슨 구조물 근처"인지 바로 보인다.</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<h2 id="2-2-측점이-뭐예요-">2-2. 측점이 뭐예요? 📍</h2>
|
<h2 id="2-2-측점이-뭐예요-">2-2. 측점이 뭐예요? 📍</h2>
|
||||||
<p>철도에서 <strong>출발점부터 몇 미터 왔는지</strong>를 나타내는 '거리 이정표'예요. <code>160k130</code> = 출발점에서 <strong>160km + 130m</strong> 지점. (도로의 'km 표지판'과 똑같아요!)</p>
|
<p>철도에서 <strong>출발점부터 몇 미터 왔는지</strong>를 나타내는 '거리 이정표'예요. <code>160k130</code> = 출발점에서 <strong>160km + 130m</strong> 지점. (도로의 'km 표지판'과 똑같아요!)</p>
|
||||||
<h2 id="2-3-어떻게-만들었나--시간측점-지도-️">2-3. 어떻게 만들었나 — "시간↔︎측점 지도" 🗺️</h2>
|
<h2 id="2-3-어떻게-만들었나--시간측점-대응표-️">2-3. 어떻게 만들었나 — "시간↔︎측점 대응표" 🗺️</h2>
|
||||||
<p>비밀은 <strong>드론 GPS</strong>예요. 영상의 <strong>매 프레임(사진 한 장)</strong> 마다 두 가지를 계산해 <strong>짝지어</strong> 둬요:</p>
|
<p>비밀은 <strong>드론 GPS</strong>예요. 영상의 <strong>매 프레임(사진 한 장)</strong> 마다 두 가지를 계산해 <strong>짝지어</strong> 둬요:</p>
|
||||||
<figure class="fig">
|
<figure class="fig">
|
||||||
<svg viewBox="0 0 700 250" role="img" aria-label="프레임마다 시간과 측점을 짝짓기">
|
<svg viewBox="0 0 700 250" role="img" aria-label="프레임마다 시간과 측점을 짝짓기">
|
||||||
@@ -238,10 +244,10 @@ h2{border-top:3px solid #e7e0d2;padding-top:14px;margin-top:34px;}
|
|||||||
<line x1="200" y1="134" x2="330" y2="170" stroke="#7c3aed" stroke-width="2" marker-end="url(#ad)"></line>
|
<line x1="200" y1="134" x2="330" y2="170" stroke="#7c3aed" stroke-width="2" marker-end="url(#ad)"></line>
|
||||||
<line x1="505" y1="134" x2="370" y2="170" stroke="#7c3aed" stroke-width="2" marker-end="url(#ad)"></line>
|
<line x1="505" y1="134" x2="370" y2="170" stroke="#7c3aed" stroke-width="2" marker-end="url(#ad)"></line>
|
||||||
<rect x="210" y="172" width="280" height="46" rx="8" fill="#fef3c7" stroke="#ca8a04"></rect>
|
<rect x="210" y="172" width="280" height="46" rx="8" fill="#fef3c7" stroke="#ca8a04"></rect>
|
||||||
<text x="350" y="193" text-anchor="middle" font-size="12" font-weight="bold" fill="#854d0e">시간 ↔ 측점 지도 완성!</text>
|
<text x="350" y="193" text-anchor="middle" font-size="12" font-weight="bold" fill="#854d0e">시간 ↔ 측점 대응표 완성!</text>
|
||||||
<text x="350" y="210" text-anchor="middle" font-size="10" fill="#a16207">"192초 = 160k130" 처럼 서로 변환 가능</text>
|
<text x="350" y="210" text-anchor="middle" font-size="10" fill="#a16207">"192초 = 160k130" 처럼 서로 변환 가능</text>
|
||||||
</svg>
|
</svg>
|
||||||
<figcaption>프레임마다 (시간)과 (측점)을 계산해 짝지으면, 시간↔측점을 자유롭게 오갈 수 있다.</figcaption>
|
<figcaption>프레임마다 (시간)과 (측점)을 계산해 짝지으면, 시간↔측점을 자유롭게 오갈 수 있다 — 재생 기준을 '시간'에서 '스테이션(측점)'으로 전환하기 위한 색인 구조.</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<p><strong>GPS를 측점으로 바꾸는 법(②):</strong> 드론 GPS 점을 <strong>선로(측점들을 이은 선)에 수직으로 툭 떨어뜨려</strong>, 그 발자국이 몇 측점인지 읽어요. (코드: <code>projectToChain</code> / <code>projectChainage</code>)</p>
|
<p><strong>GPS를 측점으로 바꾸는 법(②):</strong> 드론 GPS 점을 <strong>선로(측점들을 이은 선)에 수직으로 툭 떨어뜨려</strong>, 그 발자국이 몇 측점인지 읽어요. (코드: <code>projectToChain</code> / <code>projectChainage</code>)</p>
|
||||||
@@ -275,7 +281,7 @@ h2{border-top:3px solid #e7e0d2;padding-top:14px;margin-top:34px;}
|
|||||||
<h3 id="③-구조물을-제자리에--교량터널역은-측점값에-배치">③ 구조물을 제자리에 — 교량·터널·역은 '측점값'에 배치</h3>
|
<h3 id="③-구조물을-제자리에--교량터널역은-측점값에-배치">③ 구조물을 제자리에 — 교량·터널·역은 '측점값'에 배치</h3>
|
||||||
<p>교량·터널·역사를 각자의 <strong>측점값 위치</strong>에 표시해서, "이 구조물이 어디쯤인지" 막대만 봐도 알 수 있어요.</p>
|
<p>교량·터널·역사를 각자의 <strong>측점값 위치</strong>에 표시해서, "이 구조물이 어디쯤인지" 막대만 봐도 알 수 있어요.</p>
|
||||||
<h2 id="2-5-측점으로-찾아가기seek">2-5. 측점으로 '찾아가기'(Seek)</h2>
|
<h2 id="2-5-측점으로-찾아가기seek">2-5. 측점으로 '찾아가기'(Seek)</h2>
|
||||||
<p>측점값을 입력하거나 막대를 누르면 → <strong>그 측점이 나오는 시간을 지도에서 찾아 → 그 시간으로 이동</strong>해요. (시간↔︎측점 지도가 있으니 가능한 일!)</p>
|
<p>측점값을 입력하거나 막대를 누르면 → <strong>그 측점이 나오는 시간을 지도에서 찾아 → 그 시간으로 이동</strong>해요. (시간↔︎측점 대응표가 있으니 가능한 일!)</p>
|
||||||
<figure class="fig">
|
<figure class="fig">
|
||||||
<svg viewBox="0 0 700 90" role="img" aria-label="측점 입력으로 이동">
|
<svg viewBox="0 0 700 90" role="img" aria-label="측점 입력으로 이동">
|
||||||
<defs><marker id="as" markerWidth="10" markerHeight="10" refX="7" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#7c3aed"></path></marker></defs>
|
<defs><marker id="as" markerWidth="10" markerHeight="10" refX="7" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#7c3aed"></path></marker></defs>
|
||||||
@@ -308,7 +314,7 @@ h2{border-top:3px solid #e7e0d2;padding-top:14px;margin-top:34px;}
|
|||||||
<td>VideoPlayer.tsx <code>effectiveFps</code></td>
|
<td>VideoPlayer.tsx <code>effectiveFps</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="odd">
|
||||||
<td>시간↔︎측점 지도</td>
|
<td>시간↔︎측점 대응표</td>
|
||||||
<td>프레임별 precompute</td>
|
<td>프레임별 precompute</td>
|
||||||
<td><a href="../client/src/stationbar/StationBar.tsx">StationBar.tsx</a></td>
|
<td><a href="../client/src/stationbar/StationBar.tsx">StationBar.tsx</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -328,7 +334,7 @@ h2{border-top:3px solid #e7e0d2;padding-top:14px;margin-top:34px;}
|
|||||||
<h2 id="마무리--한-문장씩-">마무리 — 한 문장씩 🎁</h2>
|
<h2 id="마무리--한-문장씩-">마무리 — 한 문장씩 🎁</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Part 1 (좌표 투영):</strong> "드론이 <strong>어디서·어느 쪽</strong>을 보는지 알기 때문에, 지도 속 터널이 <strong>영상 화면 어디</strong>에 보일지 계산해 이름표를 딱 붙인다."</li>
|
<li><strong>Part 1 (좌표 투영):</strong> "드론이 <strong>어디서·어느 쪽</strong>을 보는지 알기 때문에, 지도 속 터널이 <strong>영상 화면 어디</strong>에 보일지 계산해 이름표를 딱 붙인다."</li>
|
||||||
<li><strong>Part 2 (측점 기반):</strong> "드론 <strong>GPS로 매 프레임의 측점</strong>을 계산해 <strong>시간↔︎측점 지도</strong>를 만들어, <strong>시간이 아니라 측점으로</strong> 영상을 탐색한다."</li>
|
<li><strong>Part 2 (측점 기반):</strong> "드론 <strong>GPS로 매 프레임의 측점</strong>을 계산해 <strong>시간↔︎측점 대응표</strong>를 만들어, <strong>시간이 아니라 측점으로</strong> 영상을 탐색한다."</li>
|
||||||
</ul>
|
</ul>
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<p>두 기술 모두 <strong>드론의 GPS·자세 데이터</strong>를 똑똑하게 활용한 결과예요. 그래서 GhiVideo는 단순 영상이 아니라 <strong>"선로 위 어디를 보고 있는지 아는" 주행영상 플레이어</strong>가 됩니다. 🚁📏</p>
|
<p>두 기술 모두 <strong>드론의 GPS·자세 데이터</strong>를 똑똑하게 활용한 결과예요. 그래서 GhiVideo는 단순 영상이 아니라 <strong>"선로 위 어디를 보고 있는지 아는" 주행영상 플레이어</strong>가 됩니다. 🚁📏</p>
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ h2{border-top:3px solid #e7e0d2;padding-top:14px;margin-top:34px;}
|
|||||||
<circle cx="300" cy="51" r="10" fill="#374151"/>
|
<circle cx="300" cy="51" r="10" fill="#374151"/>
|
||||||
<text x="45" y="82" font-size="11" fill="#374151">0:00</text><text x="330" y="82" font-size="11" fill="#374151">03:12</text><text x="632" y="82" font-size="11" fill="#374151">09:28</text>
|
<text x="45" y="82" font-size="11" fill="#374151">0:00</text><text x="330" y="82" font-size="11" fill="#374151">03:12</text><text x="632" y="82" font-size="11" fill="#374151">09:28</text>
|
||||||
<text x="350" y="104" text-anchor="middle" font-size="11" fill="#6b7280">"3분 12초 지점" — 여기가 어디 선로인지 모름 😵</text>
|
<text x="350" y="104" text-anchor="middle" font-size="11" fill="#6b7280">"3분 12초 지점" — 여기가 어디 선로인지 모름 😵</text>
|
||||||
|
|
||||||
<text x="350" y="150" text-anchor="middle" font-size="13" font-weight="bold" fill="#166534">GhiVideo (측점 축)</text>
|
<text x="350" y="150" text-anchor="middle" font-size="13" font-weight="bold" fill="#166534">GhiVideo (측점 축)</text>
|
||||||
<rect x="40" y="164" width="620" height="26" rx="6" fill="#fde9c8" stroke="#e0a94f"/>
|
<rect x="40" y="164" width="620" height="26" rx="6" fill="#fde9c8" stroke="#e0a94f"/>
|
||||||
<rect x="40" y="164" width="300" height="26" rx="6" fill="#f59e0b"/>
|
<rect x="40" y="164" width="300" height="26" rx="6" fill="#f59e0b"/>
|
||||||
@@ -131,7 +130,7 @@ h2{border-top:3px solid #e7e0d2;padding-top:14px;margin-top:34px;}
|
|||||||
철도에서 **출발점부터 몇 미터 왔는지**를 나타내는 '거리 이정표'예요.
|
철도에서 **출발점부터 몇 미터 왔는지**를 나타내는 '거리 이정표'예요.
|
||||||
`160k130` = 출발점에서 **160km + 130m** 지점. (도로의 'km 표지판'과 똑같아요!)
|
`160k130` = 출발점에서 **160km + 130m** 지점. (도로의 'km 표지판'과 똑같아요!)
|
||||||
|
|
||||||
## 2-3. 어떻게 만들었나 — "시간↔측점 지도" 🗺️
|
## 2-3. 어떻게 만들었나 — "시간↔측점 대응표" 🗺️
|
||||||
|
|
||||||
비밀은 **드론 GPS**예요. 영상의 **매 프레임(사진 한 장)** 마다 두 가지를 계산해 **짝지어** 둬요:
|
비밀은 **드론 GPS**예요. 영상의 **매 프레임(사진 한 장)** 마다 두 가지를 계산해 **짝지어** 둬요:
|
||||||
|
|
||||||
@@ -152,10 +151,10 @@ h2{border-top:3px solid #e7e0d2;padding-top:14px;margin-top:34px;}
|
|||||||
<line x1="200" y1="134" x2="330" y2="170" stroke="#7c3aed" stroke-width="2" marker-end="url(#ad)"/>
|
<line x1="200" y1="134" x2="330" y2="170" stroke="#7c3aed" stroke-width="2" marker-end="url(#ad)"/>
|
||||||
<line x1="505" y1="134" x2="370" y2="170" stroke="#7c3aed" stroke-width="2" marker-end="url(#ad)"/>
|
<line x1="505" y1="134" x2="370" y2="170" stroke="#7c3aed" stroke-width="2" marker-end="url(#ad)"/>
|
||||||
<rect x="210" y="172" width="280" height="46" rx="8" fill="#fef3c7" stroke="#ca8a04"/>
|
<rect x="210" y="172" width="280" height="46" rx="8" fill="#fef3c7" stroke="#ca8a04"/>
|
||||||
<text x="350" y="193" text-anchor="middle" font-size="12" font-weight="bold" fill="#854d0e">시간 ↔ 측점 지도 완성!</text>
|
<text x="350" y="193" text-anchor="middle" font-size="12" font-weight="bold" fill="#854d0e">시간 ↔ 측점 대응표 완성!</text>
|
||||||
<text x="350" y="210" text-anchor="middle" font-size="10" fill="#a16207">"192초 = 160k130" 처럼 서로 변환 가능</text>
|
<text x="350" y="210" text-anchor="middle" font-size="10" fill="#a16207">"192초 = 160k130" 처럼 서로 변환 가능</text>
|
||||||
</svg>
|
</svg>
|
||||||
<figcaption>프레임마다 (시간)과 (측점)을 계산해 짝지으면, 시간↔측점을 자유롭게 오갈 수 있다.</figcaption>
|
<figcaption>프레임마다 (시간)과 (측점)을 계산해 짝지으면, 시간↔측점을 자유롭게 오갈 수 있다 — 재생 기준을 '시간'에서 '스테이션(측점)'으로 전환하기 위한 색인 구조.</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
**GPS를 측점으로 바꾸는 법(②):** 드론 GPS 점을 **선로(측점들을 이은 선)에 수직으로 툭 떨어뜨려**, 그 발자국이 몇 측점인지 읽어요. (코드: `projectToChain` / `projectChainage`)
|
**GPS를 측점으로 바꾸는 법(②):** 드론 GPS 점을 **선로(측점들을 이은 선)에 수직으로 툭 떨어뜨려**, 그 발자국이 몇 측점인지 읽어요. (코드: `projectToChain` / `projectChainage`)
|
||||||
@@ -200,7 +199,7 @@ h2{border-top:3px solid #e7e0d2;padding-top:14px;margin-top:34px;}
|
|||||||
## 2-5. 측점으로 '찾아가기'(Seek)
|
## 2-5. 측점으로 '찾아가기'(Seek)
|
||||||
|
|
||||||
측점값을 입력하거나 막대를 누르면 → **그 측점이 나오는 시간을 지도에서 찾아 → 그 시간으로 이동**해요.
|
측점값을 입력하거나 막대를 누르면 → **그 측점이 나오는 시간을 지도에서 찾아 → 그 시간으로 이동**해요.
|
||||||
(시간↔측점 지도가 있으니 가능한 일!)
|
(시간↔측점 대응표가 있으니 가능한 일!)
|
||||||
|
|
||||||
<figure class="fig">
|
<figure class="fig">
|
||||||
<svg viewBox="0 0 700 90" role="img" aria-label="측점 입력으로 이동">
|
<svg viewBox="0 0 700 90" role="img" aria-label="측점 입력으로 이동">
|
||||||
@@ -219,7 +218,7 @@ h2{border-top:3px solid #e7e0d2;padding-top:14px;margin-top:34px;}
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| GPS → 측점값 | 선분에 수직 투영 | [chainage.ts](../client/src/utils/chainage.ts) `projectToChain` |
|
| GPS → 측점값 | 선분에 수직 투영 | [chainage.ts](../client/src/utils/chainage.ts) `projectToChain` |
|
||||||
| 프레임 → 시간 | fps 자동 산출 | VideoPlayer.tsx `effectiveFps` |
|
| 프레임 → 시간 | fps 자동 산출 | VideoPlayer.tsx `effectiveFps` |
|
||||||
| 시간↔측점 지도 | 프레임별 precompute | [StationBar.tsx](../client/src/stationbar/StationBar.tsx) |
|
| 시간↔측점 대응표 | 프레임별 precompute | [StationBar.tsx](../client/src/stationbar/StationBar.tsx) |
|
||||||
| 이동거리 축 | 측점 변화량 누적(정규화) | StationBar.tsx `frac` |
|
| 이동거리 축 | 측점 변화량 누적(정규화) | StationBar.tsx `frac` |
|
||||||
| 방향 색·구조물 배치 | 측점값 기준 렌더 | stationbar/components/* |
|
| 방향 색·구조물 배치 | 측점값 기준 렌더 | stationbar/components/* |
|
||||||
|
|
||||||
@@ -228,7 +227,7 @@ h2{border-top:3px solid #e7e0d2;padding-top:14px;margin-top:34px;}
|
|||||||
## 마무리 — 한 문장씩 🎁
|
## 마무리 — 한 문장씩 🎁
|
||||||
|
|
||||||
- **Part 1 (좌표 투영):** "드론이 **어디서·어느 쪽**을 보는지 알기 때문에, 지도 속 터널이 **영상 화면 어디**에 보일지 계산해 이름표를 딱 붙인다."
|
- **Part 1 (좌표 투영):** "드론이 **어디서·어느 쪽**을 보는지 알기 때문에, 지도 속 터널이 **영상 화면 어디**에 보일지 계산해 이름표를 딱 붙인다."
|
||||||
- **Part 2 (측점 기반):** "드론 **GPS로 매 프레임의 측점**을 계산해 **시간↔측점 지도**를 만들어, **시간이 아니라 측점으로** 영상을 탐색한다."
|
- **Part 2 (측점 기반):** "드론 **GPS로 매 프레임의 측점**을 계산해 **시간↔측점 대응표**를 만들어, **시간이 아니라 측점으로** 영상을 탐색한다."
|
||||||
|
|
||||||
> 두 기술 모두 **드론의 GPS·자세 데이터**를 똑똑하게 활용한 결과예요.
|
> 두 기술 모두 **드론의 GPS·자세 데이터**를 똑똑하게 활용한 결과예요.
|
||||||
> 그래서 GhiVideo는 단순 영상이 아니라 **"선로 위 어디를 보고 있는지 아는" 주행영상 플레이어**가 됩니다. 🚁📏
|
> 그래서 GhiVideo는 단순 영상이 아니라 **"선로 위 어디를 보고 있는지 아는" 주행영상 플레이어**가 됩니다. 🚁📏
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,454 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang xml:lang>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="pandoc" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
|
||||||
|
<title>쉬운설명_KMZ에서_영상표출까지_전과정</title>
|
||||||
|
<style>
|
||||||
|
code{white-space: pre-wrap;}
|
||||||
|
span.smallcaps{font-variant: small-caps;}
|
||||||
|
span.underline{text-decoration: underline;}
|
||||||
|
div.column{display: inline-block; vertical-align: top; width: 50%;}
|
||||||
|
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
|
||||||
|
ul.task-list{list-style: none;}
|
||||||
|
.display.math{display: block; text-align: center; margin: 0.5rem auto;}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">@page {
|
||||||
|
size: A4;
|
||||||
|
margin: 18mm 16mm 16mm 16mm;
|
||||||
|
@bottom-center {
|
||||||
|
content: counter(page) " / " counter(pages);
|
||||||
|
font-family: "Malgun Gothic", sans-serif;
|
||||||
|
font-size: 9pt;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html { font-size: 11pt; }
|
||||||
|
body {
|
||||||
|
font-family: "Malgun Gothic", "Hancom Gothic", sans-serif;
|
||||||
|
color: #23272e;
|
||||||
|
line-height: 1.65;
|
||||||
|
max-width: 920px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.7rem;
|
||||||
|
color: #b45309;
|
||||||
|
border-bottom: 3px solid #f59e0b;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #b45309;
|
||||||
|
border-bottom: 1px solid #e5d3b3;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
margin-top: 1.6em;
|
||||||
|
}
|
||||||
|
h3 { font-size: 1.05rem; color: #92400e; margin-top: 1.1em; }
|
||||||
|
a { color: #b45309; }
|
||||||
|
hr { border: none; border-top: 1px solid #e2e2e2; margin: 1.6em 0; }
|
||||||
|
ul { padding-left: 1.25em; }
|
||||||
|
li { margin: 0.18em 0; }
|
||||||
|
strong { color: #1f2937; }
|
||||||
|
code {
|
||||||
|
font-family: "D2Coding", Consolas, monospace;
|
||||||
|
background: #f4f1ea;
|
||||||
|
border: 1px solid #e7e0d2;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.5px 5px;
|
||||||
|
font-size: 0.92em;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0.8em 0;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
th, td { border: 1px solid #d8d2c4; padding: 6px 10px; text-align: left; vertical-align: top; }
|
||||||
|
th { background: #fdf3df; color: #7c2d12; }
|
||||||
|
blockquote {
|
||||||
|
border-left: 4px solid #f59e0b;
|
||||||
|
margin: 0.8em 0;
|
||||||
|
padding: 0.2em 0 0.2em 14px;
|
||||||
|
color: #555;
|
||||||
|
background: #fffbf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3 { break-after: avoid; }
|
||||||
|
</style>
|
||||||
|
<!--[if lt IE 9]>
|
||||||
|
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
|
||||||
|
<![endif]-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header id="title-block-header">
|
||||||
|
<h1 class="title">쉬운설명_KMZ에서_영상표출까지_전과정</h1>
|
||||||
|
</header>
|
||||||
|
<h1 id="kmz-파일에서-영상-위-이름표까지--전-과정-그림-설명서">KMZ 파일에서 영상 위 이름표까지 — 전 과정 그림 설명서</h1>
|
||||||
|
<blockquote>
|
||||||
|
<p>이 글은 <strong>초등학생도 이해할 수 있게</strong> 그림과 이야기로 쓴 설명서예요. "지도 파일(KMZ) 하나가 어떤 여행을 거쳐, 드론 영상 속 <strong>정확한 자리</strong>에 다리·터널 이름표로 나타날까?" 그 여정을 <strong>8단계</strong>로 하나씩 따라가 봅니다. (파일·줄 번호는 2026-07-03 기준)</p>
|
||||||
|
</blockquote>
|
||||||
|
<hr />
|
||||||
|
<h2 id="한눈에-보는-전체-여정">한눈에 보는 전체 여정</h2>
|
||||||
|
<p>KMZ는 "지도 위 핀들을 모아 둔 <strong>선물 상자</strong>"예요. 이 상자가 열려서, 안의 핀 하나하나가 영상 화면의 정확한 픽셀에 이름표로 붙기까지 — 전체 흐름은 이렇습니다.</p>
|
||||||
|
<figure>
|
||||||
|
<svg viewBox="0 0 760 210" role="img" aria-label="전체 파이프라인">
|
||||||
|
<defs><marker id="f1a" markerWidth="9" markerHeight="9" refX="6" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6 Z" fill="#14305e"></path></marker></defs>
|
||||||
|
<!-- 1행: KMZ 처리 -->
|
||||||
|
<rect x="8" y="18" width="108" height="52" rx="6" fill="#fff7e6" stroke="#b45309"></rect>
|
||||||
|
<text x="62" y="40" text-anchor="middle" font-size="11" font-weight="bold" fill="#b45309">① KMZ 파일</text>
|
||||||
|
<text x="62" y="56" text-anchor="middle" font-size="9" fill="#7c2d12">(압축 선물상자)</text>
|
||||||
|
<rect x="146" y="18" width="108" height="52" rx="6" fill="#fff" stroke="#555"></rect>
|
||||||
|
<text x="200" y="40" text-anchor="middle" font-size="11" font-weight="bold">② 압축 풀기</text>
|
||||||
|
<text x="200" y="56" text-anchor="middle" font-size="9" fill="#555">doc.kml 꺼내기</text>
|
||||||
|
<rect x="284" y="18" width="108" height="52" rx="6" fill="#fff" stroke="#555"></rect>
|
||||||
|
<text x="338" y="40" text-anchor="middle" font-size="11" font-weight="bold">③ KML 읽기</text>
|
||||||
|
<text x="338" y="56" text-anchor="middle" font-size="9" fill="#555">폴더→핀(Placemark)</text>
|
||||||
|
<rect x="422" y="18" width="130" height="52" rx="6" fill="#eef2f8" stroke="#14305e"></rect>
|
||||||
|
<text x="487" y="40" text-anchor="middle" font-size="11" font-weight="bold" fill="#14305e">④ 목록 완성</text>
|
||||||
|
<text x="487" y="56" text-anchor="middle" font-size="9" fill="#555">POI·구조물 (이름+위경도)</text>
|
||||||
|
<line x1="116" y1="44" x2="144" y2="44" stroke="#14305e" stroke-width="1.5" marker-end="url(#f1a)"></line>
|
||||||
|
<line x1="254" y1="44" x2="282" y2="44" stroke="#14305e" stroke-width="1.5" marker-end="url(#f1a)"></line>
|
||||||
|
<line x1="392" y1="44" x2="420" y2="44" stroke="#14305e" stroke-width="1.5" marker-end="url(#f1a)"></line>
|
||||||
|
<!-- 보조입력 -->
|
||||||
|
<rect x="8" y="120" width="150" height="40" rx="6" fill="#f0fdf4" stroke="#16a34a"></rect>
|
||||||
|
<text x="83" y="137" text-anchor="middle" font-size="10" font-weight="bold" fill="#14532d">측점 CSV (실측 표고)</text>
|
||||||
|
<text x="83" y="152" text-anchor="middle" font-size="9" fill="#555">⑤ 높이 결정에 사용</text>
|
||||||
|
<rect x="178" y="120" width="160" height="40" rx="6" fill="#f0fdf4" stroke="#16a34a"></rect>
|
||||||
|
<text x="258" y="137" text-anchor="middle" font-size="10" font-weight="bold" fill="#14532d">드론 비행로그 (SRT/CSV)</text>
|
||||||
|
<text x="258" y="152" text-anchor="middle" font-size="9" fill="#555">위치·자세(yaw·pitch·roll)·초점</text>
|
||||||
|
<!-- 2행: 계산 -->
|
||||||
|
<rect x="368" y="114" width="170" height="52" rx="6" fill="#fff7e6" stroke="#b45309"></rect>
|
||||||
|
<text x="453" y="136" text-anchor="middle" font-size="11" font-weight="bold" fill="#b45309">⑥⑦ 좌표변환 + 투영</text>
|
||||||
|
<text x="453" y="152" text-anchor="middle" font-size="9" fill="#7c2d12">위경도→미터→카메라→화면</text>
|
||||||
|
<rect x="568" y="114" width="184" height="52" rx="6" fill="#fef2f2" stroke="#dc2626"></rect>
|
||||||
|
<text x="660" y="136" text-anchor="middle" font-size="11" font-weight="bold" fill="#991b1b">⑧ 영상 위 이름표</text>
|
||||||
|
<text x="660" y="152" text-anchor="middle" font-size="9" fill="#555">60fps 정합 + 떨림 평활</text>
|
||||||
|
<line x1="487" y1="70" x2="470" y2="112" stroke="#14305e" stroke-width="1.5" marker-end="url(#f1a)"></line>
|
||||||
|
<line x1="158" y1="140" x2="366" y2="140" stroke="#16a34a" stroke-width="1.2" marker-end="url(#f1a)"></line>
|
||||||
|
<line x1="338" y1="140" x2="366" y2="140" stroke="#16a34a" stroke-width="1.2"></line>
|
||||||
|
<line x1="538" y1="140" x2="566" y2="140" stroke="#14305e" stroke-width="1.5" marker-end="url(#f1a)"></line>
|
||||||
|
<text x="380" y="196" text-anchor="middle" font-size="9" fill="#888">— KMZ의 핀(④) + 측점 표고(⑤) + 드론 포즈를 합쳐 매 프레임 계산(⑥⑦) → 화면(⑧) —</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>그림 1. KMZ → 압축해제 → 파싱 → 목록 → (표고·드론 포즈 결합) → 좌표변환·투영 → 영상 표출</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<h2 id="1단계--kmz는-압축된-선물-상자">1단계 — KMZ는 '압축된 선물 상자'</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>KMZ = KML을 ZIP으로 압축한 것</strong>이에요. 상자(KMZ)를 열면 안에 설명서(doc.kml)가 들어 있어요.</li>
|
||||||
|
<li>사용자가 폴더를 선택하면, 프로그램은 폴더 안에서 <code>.kml</code>(맨 설명서)이 있으면 그걸 먼저 쓰고, 없으면 <code>.kmz</code>(상자)를 찾아 엽니다.</li>
|
||||||
|
<li>압축 풀기는 브라우저 안에서 <code>fflate</code> 라이브러리의 <code>unzipSync</code> 로 처리해요 — <strong>서버에 보내지 않고 내 컴퓨터에서 바로</strong> 풉니다.</li>
|
||||||
|
</ul>
|
||||||
|
<blockquote>
|
||||||
|
<p>코드: <a href="../client/src/utils/geoData.ts#L295">geoData.ts:295</a> <code>parseKmz()</code> — <code>.kml</code> 우선, 없으면 <code>.kmz</code>를 <code>unzipSync</code>로 해제해 <code>doc.kml</code>을 꺼냄. 실패하면 경고 후 건너뜀.</p>
|
||||||
|
</blockquote>
|
||||||
|
<h2 id="2단계--설명서kml-속은-폴더-나무--핀">2단계 — 설명서(KML) 속은 '폴더 나무 + 핀'</h2>
|
||||||
|
<p>KML은 XML이라는 형식의 글이에요. 그 안은 <strong>폴더 나무</strong> 모양이고, 잎사귀마다 <strong>핀(Placemark)</strong> 이 달려 있어요.</p>
|
||||||
|
<figure>
|
||||||
|
<svg viewBox="0 0 760 240" role="img" aria-label="KMZ 내부 구조와 핀 해부도">
|
||||||
|
<!-- 좌: 폴더 트리 -->
|
||||||
|
<rect x="8" y="10" width="300" height="220" rx="8" fill="#fdf8ef" stroke="#b45309"></rect>
|
||||||
|
<text x="158" y="32" text-anchor="middle" font-size="11.5" font-weight="bold" fill="#b45309">KMZ 상자 속 doc.kml (폴더 나무)</text>
|
||||||
|
<text x="30" y="58" font-size="10.5" font-weight="bold" fill="#333">📁 Document</text>
|
||||||
|
<text x="52" y="80" font-size="10.5" fill="#333">📁 02)지장물 — 📍📍📍 (POI)</text>
|
||||||
|
<text x="52" y="102" font-size="10.5" fill="#333">📁 03)교량 — 📍📍 (구조물)</text>
|
||||||
|
<text x="52" y="124" font-size="10.5" fill="#333">📁 04)터널 — 📍📍 (구조물)</text>
|
||||||
|
<text x="52" y="146" font-size="10.5" fill="#333">📁 05)출입문번호 — 📍 (POI)</text>
|
||||||
|
<text x="52" y="168" font-size="10.5" fill="#333">📁 06)구교 — 📍 (구조물)</text>
|
||||||
|
<text x="52" y="190" font-size="10.5" fill="#333">📍 철도역(KAKAO_RAIL) → POI+역사</text>
|
||||||
|
<text x="158" y="216" text-anchor="middle" font-size="9" fill="#888">폴더 이름을 보고 핀의 '종류'를 결정</text>
|
||||||
|
<!-- 우: Placemark 해부 -->
|
||||||
|
<rect x="340" y="10" width="412" height="220" rx="8" fill="#eef2f8" stroke="#14305e"></rect>
|
||||||
|
<text x="546" y="32" text-anchor="middle" font-size="11.5" font-weight="bold" fill="#14305e">핀(Placemark) 하나의 해부도</text>
|
||||||
|
<rect x="360" y="46" width="372" height="34" rx="4" fill="#fff" stroke="#94a3b8"></rect>
|
||||||
|
<text x="374" y="67" font-size="10.5" fill="#333"><tspan font-weight="bold"><name></tspan> 대전천교 — 핀의 이름</text>
|
||||||
|
<rect x="360" y="88" width="372" height="62" rx="4" fill="#fff" stroke="#94a3b8"></rect>
|
||||||
|
<text x="374" y="107" font-size="10.5" fill="#333"><tspan font-weight="bold"><description></tspan> 속성표(HTML 표)</text>
|
||||||
|
<text x="388" y="124" font-size="9.5" fill="#555">구분=대전천교 · 연장(m)=124.5 · 시설종별=2종 …</text>
|
||||||
|
<text x="388" y="140" font-size="9.5" fill="#555">→ 표의 (키, 값)을 한 줄씩 뽑아 저장</text>
|
||||||
|
<rect x="360" y="158" width="372" height="34" rx="4" fill="#fff" stroke="#94a3b8"></rect>
|
||||||
|
<text x="374" y="179" font-size="10.5" fill="#333"><tspan font-weight="bold"><coordinates></tspan> 127.41, 36.33, 12 ← 경도,위도[,고도]</text>
|
||||||
|
<text x="546" y="214" text-anchor="middle" font-size="9" fill="#888">좌표가 없으면 속성표의 lat/lon 사용 · 한국 범위(위도33~39, 경도124~132) 밖이면 버림</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>그림 2. (왼쪽) 폴더 나무를 따라 내려가며 핀 수집 · (오른쪽) 핀 1개에서 이름·속성표·좌표를 추출</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<p>프로그램은 나무를 <strong>재귀적으로</strong>(가지 끝까지) 걸어 내려가면서, 각 핀이 <strong>어느 폴더에 속했는지</strong>를 함께 기억해요.</p>
|
||||||
|
<blockquote>
|
||||||
|
<p>코드: <a href="../client/src/utils/geoData.ts#L376">geoData.ts:376</a> <code>walk()</code> — 폴더 트리 재귀. <a href="../client/src/utils/geoData.ts#L276">geoData.ts:276</a> <code>parseKmlDescProps()</code> — 속성표(HTML <code><tr><td></code>) 파싱. 구글어스 재저장본의 태그 변형(<code><tbody></code>, <code>&gt;</code> 엔티티 등)도 견고하게 처리.</p>
|
||||||
|
</blockquote>
|
||||||
|
<h2 id="3단계--핀을-종류별로-나누기">3단계 — 핀을 종류별로 나누기</h2>
|
||||||
|
<p>폴더 이름이 곧 분류 기준이에요. 학교에서 반 나누듯이:</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="header">
|
||||||
|
<th>폴더</th>
|
||||||
|
<th>분류</th>
|
||||||
|
<th>어디에 표시되나</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>03)교량 · 04)터널 · 06)구교</td>
|
||||||
|
<td><strong>구조물</strong> (bridge/tunnel)</td>
|
||||||
|
<td>영상 라벨 + 하단 스테이션바 마커</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>02)지장물 · 05)출입문</td>
|
||||||
|
<td><strong>POI</strong> (지점)</td>
|
||||||
|
<td>영상 라벨</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>철도역 (KAKAO_RAIL)</td>
|
||||||
|
<td>POI <strong>+ 역사 구조물</strong></td>
|
||||||
|
<td>영상 라벨 + 스테이션바 <strong>양쪽</strong></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<ul>
|
||||||
|
<li>이때 속성표에서 <strong>연장(m)</strong>, <strong>시설종별(1종/2종/3종)</strong> 같은 값도 같이 담아요 → 나중에 시설등급 필터(1·2종만 보기)와 팝업 정보에 쓰입니다.</li>
|
||||||
|
<li>결과: <code>pois[]</code>(지점 목록) + <code>structures[]</code>(구조물 목록). <strong>KMZ가 이 데이터의 유일한 원본</strong>이라, 없으면 "데이터 누락" 경고를 띄워요.</li>
|
||||||
|
</ul>
|
||||||
|
<blockquote>
|
||||||
|
<p>코드: <a href="../client/src/utils/geoData.ts#L331">geoData.ts:331</a> <code>handlePlacemark()</code> — 폴더명 분기. <a href="../client/src/utils/geoData.ts#L539">geoData.ts:539</a> <code>loadGeoData</code> — KMZ 누락 시 <code>kmzMissing</code> 경고.</p>
|
||||||
|
</blockquote>
|
||||||
|
<h2 id="4단계--높이표고-정하기-핀은-몇-m-높이에-있나">4단계 — 높이(표고) 정하기: "핀은 몇 m 높이에 있나?"</h2>
|
||||||
|
<p>KMZ의 핀은 <strong>평면 위치(위도·경도)는 정확</strong>하지만, <strong>높이(z)는 없거나 부정확</strong>한 경우가 많아요. 높이가 틀리면 이름표가 하늘에 뜨거나 땅에 파묻혀 보여요. 그래서 이렇게 정합니다.</p>
|
||||||
|
<figure>
|
||||||
|
<svg viewBox="0 0 760 200" role="img" aria-label="표고 결정과 datum 정합">
|
||||||
|
<!-- 땅/지오이드/타원체 -->
|
||||||
|
<path d="M20 150 Q 200 120 380 140 T 740 130" fill="none" stroke="#8b5e34" stroke-width="3"></path>
|
||||||
|
<text x="60" y="140" font-size="10" fill="#8b5e34" font-weight="bold">땅(선로)</text>
|
||||||
|
<line x1="20" y1="178" x2="740" y2="178" stroke="#0ea5e9" stroke-width="1.5" stroke-dasharray="6 3"></line>
|
||||||
|
<text x="76" y="192" font-size="10" fill="#0369a1">바다 기준면(지오이드) — '정표고 0m'</text>
|
||||||
|
<!-- POI -->
|
||||||
|
<circle cx="420" cy="136" r="5" fill="#dc2626"></circle>
|
||||||
|
<text x="420" y="124" text-anchor="middle" font-size="10" font-weight="bold" fill="#991b1b">POI(교량)</text>
|
||||||
|
<line x1="420" y1="141" x2="420" y2="178" stroke="#dc2626" stroke-width="1.2" stroke-dasharray="3 2"></line>
|
||||||
|
<text x="452" y="166" font-size="9.5" fill="#991b1b">정표고 z = 측점 표고</text>
|
||||||
|
<!-- 드론 -->
|
||||||
|
<text x="560" y="48" text-anchor="middle" font-size="18">🛸</text>
|
||||||
|
<line x1="560" y1="58" x2="560" y2="130" stroke="#555" stroke-width="1" stroke-dasharray="3 2"></line>
|
||||||
|
<text x="640" y="70" font-size="9.5" fill="#555">드론 고도 = '타원체고' 기준</text>
|
||||||
|
<text x="640" y="84" font-size="9.5" fill="#b45309" font-weight="bold">→ 기준이 서로 다름!</text>
|
||||||
|
<rect x="60" y="20" width="360" height="52" rx="6" fill="#fff7e6" stroke="#b45309"></rect>
|
||||||
|
<text x="240" y="41" text-anchor="middle" font-size="10.5" font-weight="bold" fill="#b45309">높이 기준 맞추기(datum 정합)</text>
|
||||||
|
<text x="240" y="58" text-anchor="middle" font-size="10" fill="#7c2d12">타원체고 = 정표고 + 지오이드고(대전 ≈ 25.8m)</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>그림 3. 핀의 높이는 <b>가장 가까운 측점의 실측 표고</b>를 빌려 쓰고, 드론 고도와 기준을 맞추기 위해 지오이드고(+25.8m)를 더한다</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<p>높이 결정 우선순위 (셋 중 위에서부터):</p>
|
||||||
|
<ol type="1">
|
||||||
|
<li><strong>드래그 보정값</strong> — 사용자가 화면에서 직접 고친 값 (5단계 뒤 '보너스' 참고)</li>
|
||||||
|
<li><strong>DEM 자동 조회값</strong> — open-meteo API로 받은 그 지점 지면고도 (선택 기능)</li>
|
||||||
|
<li><strong>최근접 선로(측점) 표고</strong> — 기본값. 측점 CSV의 <strong>실측 정표고</strong>를 빌려 씀</li>
|
||||||
|
</ol>
|
||||||
|
<p>이 "측점 표고 빌려 쓰기" 덕분에 <strong>비싼 지형데이터(DEM) 없이도</strong> 하향각 오차가 42.7°→11.0°로 줄었어요.</p>
|
||||||
|
<blockquote>
|
||||||
|
<p>코드: <a href="../client/src/components/overlay/StationOverlay.tsx#L767">StationOverlay.tsx:767</a> — 지면고도 gz 결정. <a href="../client/src/utils/geoProjection.ts">geoProjection.ts</a> <code>geoidOffset</code> — datum 정합.</p>
|
||||||
|
</blockquote>
|
||||||
|
<h2 id="5단계--좌표-바꾸기-주소위경도를-미터-자로">5단계 — 좌표 바꾸기: 주소(위경도)를 '미터 자'로</h2>
|
||||||
|
<p>위도·경도는 <strong>둥근 지구 위의 주소</strong>라서, 그대로는 "몇 m 옆에 있나"를 계산하기 어려워요. 그래서 3번 갈아탑니다.</p>
|
||||||
|
<figure>
|
||||||
|
<svg viewBox="0 0 760 150" role="img" aria-label="좌표 3단 변환">
|
||||||
|
<defs><marker id="f4a" markerWidth="9" markerHeight="9" refX="6" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6 Z" fill="#14305e"></path></marker></defs>
|
||||||
|
<rect x="8" y="35" width="160" height="72" rx="8" fill="#eef2f8" stroke="#14305e"></rect>
|
||||||
|
<text x="88" y="60" text-anchor="middle" font-size="11" font-weight="bold" fill="#14305e">🌍 위도·경도 (도)</text>
|
||||||
|
<text x="88" y="78" text-anchor="middle" font-size="9.5" fill="#555">36.33°N, 127.41°E</text>
|
||||||
|
<text x="88" y="94" text-anchor="middle" font-size="9" fill="#888">둥근 지구 주소</text>
|
||||||
|
<rect x="216" y="35" width="170" height="72" rx="8" fill="#fff" stroke="#555"></rect>
|
||||||
|
<text x="301" y="60" text-anchor="middle" font-size="11" font-weight="bold">🗺 평면 지도 좌표 (m)</text>
|
||||||
|
<text x="301" y="78" text-anchor="middle" font-size="9.5" fill="#555">EPSG:5186 한국 TM</text>
|
||||||
|
<text x="301" y="94" text-anchor="middle" font-size="9" fill="#888">동쪽 x m · 북쪽 y m</text>
|
||||||
|
<rect x="434" y="35" width="150" height="72" rx="8" fill="#fff" stroke="#555"></rect>
|
||||||
|
<text x="509" y="60" text-anchor="middle" font-size="11" font-weight="bold">🛸 드론 기준 (m)</text>
|
||||||
|
<text x="509" y="78" text-anchor="middle" font-size="9.5" fill="#555">동·북·위 상대거리</text>
|
||||||
|
<text x="509" y="94" text-anchor="middle" font-size="9" fill="#888">"드론에서 동쪽 40m…"</text>
|
||||||
|
<rect x="632" y="35" width="120" height="72" rx="8" fill="#fff7e6" stroke="#b45309"></rect>
|
||||||
|
<text x="692" y="60" text-anchor="middle" font-size="11" font-weight="bold" fill="#b45309">📷 카메라 눈</text>
|
||||||
|
<text x="692" y="78" text-anchor="middle" font-size="9.5" fill="#7c2d12">(Xc, Yc, Zc)</text>
|
||||||
|
<text x="692" y="94" text-anchor="middle" font-size="9" fill="#888">6단계에서</text>
|
||||||
|
<line x1="168" y1="71" x2="214" y2="71" stroke="#14305e" stroke-width="1.5" marker-end="url(#f4a)"></line>
|
||||||
|
<line x1="386" y1="71" x2="432" y2="71" stroke="#14305e" stroke-width="1.5" marker-end="url(#f4a)"></line>
|
||||||
|
<line x1="584" y1="71" x2="630" y2="71" stroke="#14305e" stroke-width="1.5" marker-end="url(#f4a)"></line>
|
||||||
|
<text x="191" y="60" text-anchor="middle" font-size="9" fill="#888">proj4</text>
|
||||||
|
<text x="409" y="60" text-anchor="middle" font-size="9" fill="#888">빼기(−드론)</text>
|
||||||
|
<text x="607" y="60" text-anchor="middle" font-size="9" fill="#888">회전행렬</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>그림 4. 위경도 → 한국 TM 평면(m) → 드론 기준 상대좌표 → 카메라 좌표, 3번의 갈아타기</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><strong>위경도 → TM(m)</strong>: proj4 라이브러리로 한국 표준 평면좌표(EPSG:5186)로 변환. 이제 "미터"로 잴 수 있어요.</li>
|
||||||
|
<li><strong>TM → 드론 기준</strong>: 핀 위치에서 드론 위치를 <strong>빼면</strong>, "드론에서 동쪽 40m, 북쪽 12m, 아래 30m" 같은 상대 위치가 나와요.</li>
|
||||||
|
</ul>
|
||||||
|
<blockquote>
|
||||||
|
<p>코드: <a href="../client/src/utils/geoProjection.ts#L75">geoProjection.ts:75</a> EPSG:5186 정의, <a href="../client/src/utils/geoProjection.ts#L334">geoProjection.ts:334</a> <code>projectPoint()</code> 1부 — ENU 상대좌표 계산.</p>
|
||||||
|
</blockquote>
|
||||||
|
<h2 id="6단계--카메라의-눈으로-회전-드론이-보는-방향은">6단계 — 카메라의 눈으로 회전: "드론이 보는 방향은?"</h2>
|
||||||
|
<p>드론이 어느 쪽을 보고 있는지(비행로그의 <strong>yaw·pitch·roll</strong>)에 따라, 같은 핀도 화면 왼쪽에 보일 수도, 오른쪽에 보일 수도 있어요. 그래서 상대좌표를 <strong>회전행렬</strong>로 돌려서 "카메라가 보는 세계"의 좌표 (Xc, Yc, Zc)로 바꿉니다.</p>
|
||||||
|
<pre><code>R = Rz(−yaw) × Rx(pitch) × Ry(roll) ← 드론의 고개 돌림·끄덕임·갸웃을 그대로 재현
|
||||||
|
(Xc, Yc, Zc) = 카메라축변환 × Rᵀ × (동, 북, 위)</code></pre>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Zc</strong> 는 "카메라 앞쪽으로 몇 m인가"예요. <strong>Zc ≤ 0이면 카메라 뒤</strong> → 그리지 않아요.</li>
|
||||||
|
<li>너무 멀거나(거리 필터) 화각(시야) 밖이어도 걸러냅니다.</li>
|
||||||
|
</ul>
|
||||||
|
<blockquote>
|
||||||
|
<p>코드: <a href="../client/src/utils/geoProjection.ts#L301">geoProjection.ts:301</a> <code>toCameraCoords()</code>, 회전 수식 주석은 <a href="../client/src/utils/geoProjection.ts#L319">geoProjection.ts:319</a>~.</p>
|
||||||
|
</blockquote>
|
||||||
|
<h2 id="7단계--핀홀-투영-3d-세상을-2d-사진으로">7단계 — 핀홀 투영: 3D 세상을 2D 사진으로</h2>
|
||||||
|
<p>카메라는 <strong>바늘구멍 사진기</strong>와 같아요. 3D 점이 구멍(렌즈)을 지나 필름(화면)에 맺힙니다.</p>
|
||||||
|
<figure>
|
||||||
|
<svg viewBox="0 0 760 190" role="img" aria-label="핀홀 투영">
|
||||||
|
<!-- 카메라 -->
|
||||||
|
<rect x="40" y="70" width="56" height="44" rx="6" fill="#333"></rect>
|
||||||
|
<circle cx="96" cy="92" r="7" fill="#111" stroke="#888"></circle>
|
||||||
|
<text x="68" y="132" text-anchor="middle" font-size="10" fill="#555">카메라(핀홀)</text>
|
||||||
|
<!-- 이미지 평면 -->
|
||||||
|
<rect x="150" y="30" width="10" height="124" fill="#e2e8f0" stroke="#94a3b8"></rect>
|
||||||
|
<text x="155" y="170" text-anchor="middle" font-size="10" fill="#555">화면(필름)</text>
|
||||||
|
<!-- 대상 -->
|
||||||
|
<circle cx="620" cy="52" r="7" fill="#dc2626"></circle>
|
||||||
|
<text x="620" y="38" text-anchor="middle" font-size="10.5" font-weight="bold" fill="#991b1b">교량 핀 (Xc, Yc, Zc)</text>
|
||||||
|
<!-- 광선 -->
|
||||||
|
<line x1="620" y1="52" x2="96" y2="92" stroke="#f59e0b" stroke-width="1.6" stroke-dasharray="6 3"></line>
|
||||||
|
<circle cx="155" cy="88" r="4.5" fill="#f59e0b"></circle>
|
||||||
|
<text x="182" y="80" font-size="10" fill="#b45309" font-weight="bold">맺힌 자리 = 화면 위치!</text>
|
||||||
|
<!-- 공식 -->
|
||||||
|
<rect x="240" y="120" width="480" height="54" rx="6" fill="#fffbeb" stroke="#f59e0b"></rect>
|
||||||
|
<text x="480" y="142" text-anchor="middle" font-size="11.5" font-weight="bold" fill="#92400e">가로 위치 = 0.5 + (Xc ÷ Zc) × (초점거리 ÷ 센서폭)</text>
|
||||||
|
<text x="480" y="162" text-anchor="middle" font-size="11.5" font-weight="bold" fill="#92400e">세로 위치 = 0.5 + (Yc ÷ Zc) × (초점거리 ÷ 센서높이)</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>그림 5. 핀홀 투영 — "멀수록(Zc↑) 가운데로 모이고, 옆에 있을수록(Xc↑) 가장자리로" 라는 원근법 그 자체</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>결과는 <strong>0~1 사이의 정규 좌표</strong>예요. (0.5, 0.5)면 화면 정중앙.</li>
|
||||||
|
<li>÷Zc 가 바로 <strong>원근법</strong>이에요 — 멀리 있는 것일수록 화면 가운데 근처로 작게 모여요.</li>
|
||||||
|
<li>초점거리(focal)는 비행로그에서 프레임마다 읽고, 센서 크기는 16:9 영상 기준(36×20.25mm)을 써요. 영상마다 화각이 다르면 <strong>라벨 1개 드래그로 세로 화각을 역산</strong>해 맞출 수도 있어요(FOV 1점 보정).</li>
|
||||||
|
</ul>
|
||||||
|
<blockquote>
|
||||||
|
<p>코드: <a href="../client/src/utils/geoProjection.ts#L126">geoProjection.ts:126</a> <code>pixelFromCamera()</code> — 위 공식 그대로.</p>
|
||||||
|
</blockquote>
|
||||||
|
<h2 id="8단계--화면에-붙이기-액자-맞추기--떨림-잡기">8단계 — 화면에 붙이기: 액자 맞추기 + 떨림 잡기</h2>
|
||||||
|
<p>마지막으로 "0~1 정규 좌표"를 <strong>진짜 화면 픽셀</strong>로 바꿔 이름표를 놓아요. 여기엔 두 가지 마무리가 있어요.</p>
|
||||||
|
<figure>
|
||||||
|
<svg viewBox="0 0 760 180" role="img" aria-label="cover 정렬과 평활">
|
||||||
|
<!-- cover 정렬 -->
|
||||||
|
<rect x="20" y="24" width="330" height="130" rx="6" fill="#f8fafc" stroke="#94a3b8"></rect>
|
||||||
|
<text x="185" y="16" text-anchor="middle" font-size="10.5" font-weight="bold" fill="#333">① 액자 맞추기 (object-fit: cover)</text>
|
||||||
|
<rect x="45" y="44" width="280" height="90" fill="#e0f2fe" stroke="#0284c7" stroke-dasharray="5 3"></rect>
|
||||||
|
<rect x="20" y="44" width="330" height="90" fill="none" stroke="#dc2626" stroke-width="1.6"></rect>
|
||||||
|
<text x="185" y="76" text-anchor="middle" font-size="9.5" fill="#0369a1">플레이어 창(파랑 점선) 안에</text>
|
||||||
|
<text x="185" y="92" text-anchor="middle" font-size="9.5" fill="#991b1b">영상(빨강)이 꽉 차게 늘어나 양옆이 잘림</text>
|
||||||
|
<text x="185" y="112" text-anchor="middle" font-size="9.5" fill="#555">→ 오버레이도 <tspan font-weight="bold">같은 사각형</tspan>으로 계산해야 어긋나지 않음</text>
|
||||||
|
<text x="185" y="170" text-anchor="middle" font-size="9" fill="#888">정규좌표(0~1) × 영상표시영역 + 잘림offset = 화면 px</text>
|
||||||
|
<!-- 평활 -->
|
||||||
|
<rect x="400" y="24" width="340" height="130" rx="6" fill="#f8fafc" stroke="#94a3b8"></rect>
|
||||||
|
<text x="570" y="16" text-anchor="middle" font-size="10.5" font-weight="bold" fill="#333">② 떨림 잡기 (속도적응 평활)</text>
|
||||||
|
<polyline points="420,110 445,70 470,105 495,68 520,108 545,72" fill="none" stroke="#dc2626" stroke-width="1.4"></polyline>
|
||||||
|
<text x="482" y="130" text-anchor="middle" font-size="9.5" fill="#991b1b">GPS 떨림 그대로 → 라벨이 덜덜덜</text>
|
||||||
|
<polyline points="575,95 600,88 625,86 650,85 675,84 700,84" fill="none" stroke="#16a34a" stroke-width="2"></polyline>
|
||||||
|
<text x="640" y="130" text-anchor="middle" font-size="9.5" fill="#14532d">평활 후 → 착 붙음</text>
|
||||||
|
<text x="570" y="150" text-anchor="middle" font-size="9" fill="#888">떨림(방향 왔다갔다)=강하게 진정 · 실제 이동(방향 일정)=즉시 따라감</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>그림 6. (왼쪽) 영상이 창에 '꽉 차게' 잘려 보이므로 오버레이도 같은 규칙으로 정렬 · (오른쪽) 떨림만 골라 진정시키는 평활</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<ol type="1">
|
||||||
|
<li><strong>액자 맞추기(cover 정렬)</strong> — 영상은 플레이어 창을 비율 유지한 채 <strong>꽉 채우며 잘려</strong> 보여요. 그래서 이름표도 "창 기준"이 아니라 <strong>"잘린 영상 사각형 기준"</strong> 으로 놓아야 정확히 붙어요.</li>
|
||||||
|
<li><strong>떨림 잡기(평활)</strong> — GPS·자세값은 미세하게 떨려요. "방향이 왔다갔다(=떨림)"면 강하게 진정시키고, "방향이 일정(=실제 이동)"하면 즉시 따라가는 <strong>속도적응 평활</strong>로, 지연 없이 떨림만 없앱니다.</li>
|
||||||
|
<li>이 계산 전부를 매 화면 프레임(<strong>60fps, requestAnimationFrame</strong>)마다 다시 해요 — 그런데도 빠른 이유는, 무거운 준비(가시성 선별)는 미리 해 두고 프레임마다는 <strong>가벼운 재투영만</strong> 하기 때문이에요.</li>
|
||||||
|
</ol>
|
||||||
|
<blockquote>
|
||||||
|
<p>코드: <a href="../client/src/components/overlay/StationOverlay.tsx#L948">StationOverlay.tsx:948</a> cover 정렬, <a href="../client/src/components/overlay/StationOverlay.tsx#L49">StationOverlay.tsx:49</a> <code>smoothStep()</code> 평활.</p>
|
||||||
|
</blockquote>
|
||||||
|
<hr />
|
||||||
|
<h2 id="보너스--그래도-어긋나면-드래그-한-번">보너스 — 그래도 어긋나면? "드래그 한 번"</h2>
|
||||||
|
<p>지오코딩(주소→좌표 변환) 자체에 오차가 있으면 이름표가 조금 비껴 붙을 수 있어요. 그럴 땐 <strong>이름표를 마우스로 끌어 제자리에 놓기만 하면</strong> 돼요.</p>
|
||||||
|
<ul>
|
||||||
|
<li>화면에서 끈 위치(2D)를 7단계 공식의 <strong>역방향</strong>으로 계산해(역투영), 실제 좌표(위경도+높이)를 되찾아 저장해요.</li>
|
||||||
|
<li>2D→3D는 원래 답이 무한히 많지만(깊이를 모르므로), <strong>"카메라와의 거리는 그대로"</strong> 라는 약속을 둬서 드래그 한 번으로 가로·세로·높이가 <strong>동시에</strong> 맞춰집니다.</li>
|
||||||
|
<li>고친 값은 데이터셋별로 저장돼 다음에 열어도 유지돼요. (왕복 검증 오차 ≈ 0)</li>
|
||||||
|
</ul>
|
||||||
|
<blockquote>
|
||||||
|
<p>코드: <a href="../client/src/utils/geoProjection.ts#L149">geoProjection.ts:149</a> <code>worldFromPixel()</code> — 역투영.</p>
|
||||||
|
</blockquote>
|
||||||
|
<hr />
|
||||||
|
<h2 id="단계별-요약표">단계별 요약표</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="header">
|
||||||
|
<th style="text-align: center;">단계</th>
|
||||||
|
<th>하는 일</th>
|
||||||
|
<th>비유</th>
|
||||||
|
<th>코드 위치</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="odd">
|
||||||
|
<td style="text-align: center;">1</td>
|
||||||
|
<td>KMZ 압축 해제 → doc.kml</td>
|
||||||
|
<td>선물상자 열기</td>
|
||||||
|
<td>geoData.ts <code>parseKmz</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td style="text-align: center;">2</td>
|
||||||
|
<td>KML 폴더 나무 걷기, 핀 수집</td>
|
||||||
|
<td>설명서 읽기</td>
|
||||||
|
<td>geoData.ts <code>walk</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td style="text-align: center;">3</td>
|
||||||
|
<td>폴더명으로 POI/구조물 분류</td>
|
||||||
|
<td>반 나누기</td>
|
||||||
|
<td>geoData.ts <code>handlePlacemark</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td style="text-align: center;">4</td>
|
||||||
|
<td>높이 결정(측점 표고) + 기준 맞춤(+25.8m)</td>
|
||||||
|
<td>키 재기, 자 통일</td>
|
||||||
|
<td>StationOverlay + geoidOffset</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td style="text-align: center;">5</td>
|
||||||
|
<td>위경도 → 평면 m → 드론 기준 상대좌표</td>
|
||||||
|
<td>주소를 미터 자로</td>
|
||||||
|
<td>geoProjection.ts <code>projectPoint</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td style="text-align: center;">6</td>
|
||||||
|
<td>yaw·pitch·roll 회전 → 카메라 좌표</td>
|
||||||
|
<td>카메라 눈으로 보기</td>
|
||||||
|
<td>geoProjection.ts <code>toCameraCoords</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td style="text-align: center;">7</td>
|
||||||
|
<td>핀홀 투영 → 화면 정규좌표(0~1)</td>
|
||||||
|
<td>바늘구멍 사진기</td>
|
||||||
|
<td>geoProjection.ts <code>pixelFromCamera</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td style="text-align: center;">8</td>
|
||||||
|
<td>cover 정렬 + 평활 + 60fps 렌더</td>
|
||||||
|
<td>액자 맞추고 떨림 잡기</td>
|
||||||
|
<td>StationOverlay.tsx</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td style="text-align: center;">★</td>
|
||||||
|
<td>어긋나면 드래그 1회 역투영 보정</td>
|
||||||
|
<td>이름표 손으로 옮기기</td>
|
||||||
|
<td>geoProjection.ts <code>worldFromPixel</code></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<blockquote>
|
||||||
|
<p>더 깊은 수식·구현 근거: 「기술명세_GhiVideo_종합기술문서」 · 「구현상세_드론좌표_영상투영_소스코드매칭」 참조.</p>
|
||||||
|
</blockquote>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
# KMZ 파일에서 영상 위 이름표까지 — 전 과정 그림 설명서
|
||||||
|
|
||||||
|
> 이 글은 **초등학생도 이해할 수 있게** 그림과 이야기로 쓴 설명서예요.
|
||||||
|
> "지도 파일(KMZ) 하나가 어떤 여행을 거쳐, 드론 영상 속 **정확한 자리**에 다리·터널 이름표로 나타날까?"
|
||||||
|
> 그 여정을 **8단계**로 하나씩 따라가 봅니다. (파일·줄 번호는 2026-07-03 기준)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 한눈에 보는 전체 여정
|
||||||
|
|
||||||
|
KMZ는 "지도 위 핀들을 모아 둔 **선물 상자**"예요. 이 상자가 열려서, 안의 핀 하나하나가
|
||||||
|
영상 화면의 정확한 픽셀에 이름표로 붙기까지 — 전체 흐름은 이렇습니다.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<svg viewBox="0 0 760 210" role="img" aria-label="전체 파이프라인">
|
||||||
|
<defs><marker id="f1a" markerWidth="9" markerHeight="9" refX="6" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6 Z" fill="#14305e"/></marker></defs>
|
||||||
|
<!-- 1행: KMZ 처리 -->
|
||||||
|
<rect x="8" y="18" width="108" height="52" rx="6" fill="#fff7e6" stroke="#b45309"/>
|
||||||
|
<text x="62" y="40" text-anchor="middle" font-size="11" font-weight="bold" fill="#b45309">① KMZ 파일</text>
|
||||||
|
<text x="62" y="56" text-anchor="middle" font-size="9" fill="#7c2d12">(압축 선물상자)</text>
|
||||||
|
<rect x="146" y="18" width="108" height="52" rx="6" fill="#fff" stroke="#555"/>
|
||||||
|
<text x="200" y="40" text-anchor="middle" font-size="11" font-weight="bold">② 압축 풀기</text>
|
||||||
|
<text x="200" y="56" text-anchor="middle" font-size="9" fill="#555">doc.kml 꺼내기</text>
|
||||||
|
<rect x="284" y="18" width="108" height="52" rx="6" fill="#fff" stroke="#555"/>
|
||||||
|
<text x="338" y="40" text-anchor="middle" font-size="11" font-weight="bold">③ KML 읽기</text>
|
||||||
|
<text x="338" y="56" text-anchor="middle" font-size="9" fill="#555">폴더→핀(Placemark)</text>
|
||||||
|
<rect x="422" y="18" width="130" height="52" rx="6" fill="#eef2f8" stroke="#14305e"/>
|
||||||
|
<text x="487" y="40" text-anchor="middle" font-size="11" font-weight="bold" fill="#14305e">④ 목록 완성</text>
|
||||||
|
<text x="487" y="56" text-anchor="middle" font-size="9" fill="#555">POI·구조물 (이름+위경도)</text>
|
||||||
|
<line x1="116" y1="44" x2="144" y2="44" stroke="#14305e" stroke-width="1.5" marker-end="url(#f1a)"/>
|
||||||
|
<line x1="254" y1="44" x2="282" y2="44" stroke="#14305e" stroke-width="1.5" marker-end="url(#f1a)"/>
|
||||||
|
<line x1="392" y1="44" x2="420" y2="44" stroke="#14305e" stroke-width="1.5" marker-end="url(#f1a)"/>
|
||||||
|
<!-- 보조입력 -->
|
||||||
|
<rect x="8" y="120" width="150" height="40" rx="6" fill="#f0fdf4" stroke="#16a34a"/>
|
||||||
|
<text x="83" y="137" text-anchor="middle" font-size="10" font-weight="bold" fill="#14532d">측점 CSV (실측 표고)</text>
|
||||||
|
<text x="83" y="152" text-anchor="middle" font-size="9" fill="#555">⑤ 높이 결정에 사용</text>
|
||||||
|
<rect x="178" y="120" width="160" height="40" rx="6" fill="#f0fdf4" stroke="#16a34a"/>
|
||||||
|
<text x="258" y="137" text-anchor="middle" font-size="10" font-weight="bold" fill="#14532d">드론 비행로그 (SRT/CSV)</text>
|
||||||
|
<text x="258" y="152" text-anchor="middle" font-size="9" fill="#555">위치·자세(yaw·pitch·roll)·초점</text>
|
||||||
|
<!-- 2행: 계산 -->
|
||||||
|
<rect x="368" y="114" width="170" height="52" rx="6" fill="#fff7e6" stroke="#b45309"/>
|
||||||
|
<text x="453" y="136" text-anchor="middle" font-size="11" font-weight="bold" fill="#b45309">⑥⑦ 좌표변환 + 투영</text>
|
||||||
|
<text x="453" y="152" text-anchor="middle" font-size="9" fill="#7c2d12">위경도→미터→카메라→화면</text>
|
||||||
|
<rect x="568" y="114" width="184" height="52" rx="6" fill="#fef2f2" stroke="#dc2626"/>
|
||||||
|
<text x="660" y="136" text-anchor="middle" font-size="11" font-weight="bold" fill="#991b1b">⑧ 영상 위 이름표</text>
|
||||||
|
<text x="660" y="152" text-anchor="middle" font-size="9" fill="#555">60fps 정합 + 떨림 평활</text>
|
||||||
|
<line x1="487" y1="70" x2="470" y2="112" stroke="#14305e" stroke-width="1.5" marker-end="url(#f1a)"/>
|
||||||
|
<line x1="158" y1="140" x2="366" y2="140" stroke="#16a34a" stroke-width="1.2" marker-end="url(#f1a)"/>
|
||||||
|
<line x1="338" y1="140" x2="366" y2="140" stroke="#16a34a" stroke-width="1.2"/>
|
||||||
|
<line x1="538" y1="140" x2="566" y2="140" stroke="#14305e" stroke-width="1.5" marker-end="url(#f1a)"/>
|
||||||
|
<text x="380" y="196" text-anchor="middle" font-size="9" fill="#888">— KMZ의 핀(④) + 측점 표고(⑤) + 드론 포즈를 합쳐 매 프레임 계산(⑥⑦) → 화면(⑧) —</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>그림 1. KMZ → 압축해제 → 파싱 → 목록 → (표고·드론 포즈 결합) → 좌표변환·투영 → 영상 표출</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1단계 — KMZ는 '압축된 선물 상자'
|
||||||
|
|
||||||
|
- **KMZ = KML을 ZIP으로 압축한 것**이에요. 상자(KMZ)를 열면 안에 설명서(doc.kml)가 들어 있어요.
|
||||||
|
- 사용자가 폴더를 선택하면, 프로그램은 폴더 안에서 `.kml`(맨 설명서)이 있으면 그걸 먼저 쓰고,
|
||||||
|
없으면 `.kmz`(상자)를 찾아 엽니다.
|
||||||
|
- 압축 풀기는 브라우저 안에서 `fflate` 라이브러리의 `unzipSync` 로 처리해요 — **서버에 보내지 않고 내 컴퓨터에서 바로** 풉니다.
|
||||||
|
|
||||||
|
> 코드: [geoData.ts:295](../client/src/utils/geoData.ts#L295) `parseKmz()` — `.kml` 우선, 없으면 `.kmz`를 `unzipSync`로 해제해 `doc.kml`을 꺼냄. 실패하면 경고 후 건너뜀.
|
||||||
|
|
||||||
|
## 2단계 — 설명서(KML) 속은 '폴더 나무 + 핀'
|
||||||
|
|
||||||
|
KML은 XML이라는 형식의 글이에요. 그 안은 **폴더 나무** 모양이고, 잎사귀마다 **핀(Placemark)** 이 달려 있어요.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<svg viewBox="0 0 760 240" role="img" aria-label="KMZ 내부 구조와 핀 해부도">
|
||||||
|
<!-- 좌: 폴더 트리 -->
|
||||||
|
<rect x="8" y="10" width="300" height="220" rx="8" fill="#fdf8ef" stroke="#b45309"/>
|
||||||
|
<text x="158" y="32" text-anchor="middle" font-size="11.5" font-weight="bold" fill="#b45309">KMZ 상자 속 doc.kml (폴더 나무)</text>
|
||||||
|
<text x="30" y="58" font-size="10.5" font-weight="bold" fill="#333">📁 Document</text>
|
||||||
|
<text x="52" y="80" font-size="10.5" fill="#333">📁 02)지장물 — 📍📍📍 (POI)</text>
|
||||||
|
<text x="52" y="102" font-size="10.5" fill="#333">📁 03)교량 — 📍📍 (구조물)</text>
|
||||||
|
<text x="52" y="124" font-size="10.5" fill="#333">📁 04)터널 — 📍📍 (구조물)</text>
|
||||||
|
<text x="52" y="146" font-size="10.5" fill="#333">📁 05)출입문번호 — 📍 (POI)</text>
|
||||||
|
<text x="52" y="168" font-size="10.5" fill="#333">📁 06)구교 — 📍 (구조물)</text>
|
||||||
|
<text x="52" y="190" font-size="10.5" fill="#333">📍 철도역(KAKAO_RAIL) → POI+역사</text>
|
||||||
|
<text x="158" y="216" text-anchor="middle" font-size="9" fill="#888">폴더 이름을 보고 핀의 '종류'를 결정</text>
|
||||||
|
<!-- 우: Placemark 해부 -->
|
||||||
|
<rect x="340" y="10" width="412" height="220" rx="8" fill="#eef2f8" stroke="#14305e"/>
|
||||||
|
<text x="546" y="32" text-anchor="middle" font-size="11.5" font-weight="bold" fill="#14305e">핀(Placemark) 하나의 해부도</text>
|
||||||
|
<rect x="360" y="46" width="372" height="34" rx="4" fill="#fff" stroke="#94a3b8"/>
|
||||||
|
<text x="374" y="67" font-size="10.5" fill="#333"><tspan font-weight="bold"><name></tspan> 대전천교 — 핀의 이름</text>
|
||||||
|
<rect x="360" y="88" width="372" height="62" rx="4" fill="#fff" stroke="#94a3b8"/>
|
||||||
|
<text x="374" y="107" font-size="10.5" fill="#333"><tspan font-weight="bold"><description></tspan> 속성표(HTML 표)</text>
|
||||||
|
<text x="388" y="124" font-size="9.5" fill="#555">구분=대전천교 · 연장(m)=124.5 · 시설종별=2종 …</text>
|
||||||
|
<text x="388" y="140" font-size="9.5" fill="#555">→ 표의 (키, 값)을 한 줄씩 뽑아 저장</text>
|
||||||
|
<rect x="360" y="158" width="372" height="34" rx="4" fill="#fff" stroke="#94a3b8"/>
|
||||||
|
<text x="374" y="179" font-size="10.5" fill="#333"><tspan font-weight="bold"><coordinates></tspan> 127.41, 36.33, 12 ← 경도,위도[,고도]</text>
|
||||||
|
<text x="546" y="214" text-anchor="middle" font-size="9" fill="#888">좌표가 없으면 속성표의 lat/lon 사용 · 한국 범위(위도33~39, 경도124~132) 밖이면 버림</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>그림 2. (왼쪽) 폴더 나무를 따라 내려가며 핀 수집 · (오른쪽) 핀 1개에서 이름·속성표·좌표를 추출</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
프로그램은 나무를 **재귀적으로**(가지 끝까지) 걸어 내려가면서, 각 핀이 **어느 폴더에 속했는지**를 함께 기억해요.
|
||||||
|
|
||||||
|
> 코드: [geoData.ts:376](../client/src/utils/geoData.ts#L376) `walk()` — 폴더 트리 재귀. [geoData.ts:276](../client/src/utils/geoData.ts#L276) `parseKmlDescProps()` — 속성표(HTML `<tr><td>`) 파싱. 구글어스 재저장본의 태그 변형(`<tbody>`, `>` 엔티티 등)도 견고하게 처리.
|
||||||
|
|
||||||
|
## 3단계 — 핀을 종류별로 나누기
|
||||||
|
|
||||||
|
폴더 이름이 곧 분류 기준이에요. 학교에서 반 나누듯이:
|
||||||
|
|
||||||
|
| 폴더 | 분류 | 어디에 표시되나 |
|
||||||
|
|------|------|----------------|
|
||||||
|
| 03)교량 · 04)터널 · 06)구교 | **구조물** (bridge/tunnel) | 영상 라벨 + 하단 스테이션바 마커 |
|
||||||
|
| 02)지장물 · 05)출입문 | **POI** (지점) | 영상 라벨 |
|
||||||
|
| 철도역 (KAKAO_RAIL) | POI **+ 역사 구조물** | 영상 라벨 + 스테이션바 **양쪽** |
|
||||||
|
|
||||||
|
- 이때 속성표에서 **연장(m)**, **시설종별(1종/2종/3종)** 같은 값도 같이 담아요 →
|
||||||
|
나중에 시설등급 필터(1·2종만 보기)와 팝업 정보에 쓰입니다.
|
||||||
|
- 결과: `pois[]`(지점 목록) + `structures[]`(구조물 목록). **KMZ가 이 데이터의 유일한 원본**이라, 없으면 "데이터 누락" 경고를 띄워요.
|
||||||
|
|
||||||
|
> 코드: [geoData.ts:331](../client/src/utils/geoData.ts#L331) `handlePlacemark()` — 폴더명 분기. [geoData.ts:539](../client/src/utils/geoData.ts#L539) `loadGeoData` — KMZ 누락 시 `kmzMissing` 경고.
|
||||||
|
|
||||||
|
## 4단계 — 높이(표고) 정하기: "핀은 몇 m 높이에 있나?"
|
||||||
|
|
||||||
|
KMZ의 핀은 **평면 위치(위도·경도)는 정확**하지만, **높이(z)는 없거나 부정확**한 경우가 많아요.
|
||||||
|
높이가 틀리면 이름표가 하늘에 뜨거나 땅에 파묻혀 보여요. 그래서 이렇게 정합니다.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<svg viewBox="0 0 760 200" role="img" aria-label="표고 결정과 datum 정합">
|
||||||
|
<!-- 땅/지오이드/타원체 -->
|
||||||
|
<path d="M20 150 Q 200 120 380 140 T 740 130" fill="none" stroke="#8b5e34" stroke-width="3"/>
|
||||||
|
<text x="60" y="140" font-size="10" fill="#8b5e34" font-weight="bold">땅(선로)</text>
|
||||||
|
<line x1="20" y1="178" x2="740" y2="178" stroke="#0ea5e9" stroke-width="1.5" stroke-dasharray="6 3"/>
|
||||||
|
<text x="76" y="192" font-size="10" fill="#0369a1">바다 기준면(지오이드) — '정표고 0m'</text>
|
||||||
|
<!-- POI -->
|
||||||
|
<circle cx="420" cy="136" r="5" fill="#dc2626"/>
|
||||||
|
<text x="420" y="124" text-anchor="middle" font-size="10" font-weight="bold" fill="#991b1b">POI(교량)</text>
|
||||||
|
<line x1="420" y1="141" x2="420" y2="178" stroke="#dc2626" stroke-width="1.2" stroke-dasharray="3 2"/>
|
||||||
|
<text x="452" y="166" font-size="9.5" fill="#991b1b">정표고 z = 측점 표고</text>
|
||||||
|
<!-- 드론 -->
|
||||||
|
<text x="560" y="48" text-anchor="middle" font-size="18">🛸</text>
|
||||||
|
<line x1="560" y1="58" x2="560" y2="130" stroke="#555" stroke-width="1" stroke-dasharray="3 2"/>
|
||||||
|
<text x="640" y="70" font-size="9.5" fill="#555">드론 고도 = '타원체고' 기준</text>
|
||||||
|
<text x="640" y="84" font-size="9.5" fill="#b45309" font-weight="bold">→ 기준이 서로 다름!</text>
|
||||||
|
<rect x="60" y="20" width="360" height="52" rx="6" fill="#fff7e6" stroke="#b45309"/>
|
||||||
|
<text x="240" y="41" text-anchor="middle" font-size="10.5" font-weight="bold" fill="#b45309">높이 기준 맞추기(datum 정합)</text>
|
||||||
|
<text x="240" y="58" text-anchor="middle" font-size="10" fill="#7c2d12">타원체고 = 정표고 + 지오이드고(대전 ≈ 25.8m)</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>그림 3. 핀의 높이는 <b>가장 가까운 측점의 실측 표고</b>를 빌려 쓰고, 드론 고도와 기준을 맞추기 위해 지오이드고(+25.8m)를 더한다</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
높이 결정 우선순위 (셋 중 위에서부터):
|
||||||
|
|
||||||
|
1. **드래그 보정값** — 사용자가 화면에서 직접 고친 값 (5단계 뒤 '보너스' 참고)
|
||||||
|
2. **DEM 자동 조회값** — open-meteo API로 받은 그 지점 지면고도 (선택 기능)
|
||||||
|
3. **최근접 선로(측점) 표고** — 기본값. 측점 CSV의 **실측 정표고**를 빌려 씀
|
||||||
|
|
||||||
|
이 "측점 표고 빌려 쓰기" 덕분에 **비싼 지형데이터(DEM) 없이도** 하향각 오차가 42.7°→11.0°로 줄었어요.
|
||||||
|
|
||||||
|
> 코드: [StationOverlay.tsx:767](../client/src/components/overlay/StationOverlay.tsx#L767) — 지면고도 gz 결정. [geoProjection.ts](../client/src/utils/geoProjection.ts) `geoidOffset` — datum 정합.
|
||||||
|
|
||||||
|
## 5단계 — 좌표 바꾸기: 주소(위경도)를 '미터 자'로
|
||||||
|
|
||||||
|
위도·경도는 **둥근 지구 위의 주소**라서, 그대로는 "몇 m 옆에 있나"를 계산하기 어려워요.
|
||||||
|
그래서 3번 갈아탑니다.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<svg viewBox="0 0 760 150" role="img" aria-label="좌표 3단 변환">
|
||||||
|
<defs><marker id="f4a" markerWidth="9" markerHeight="9" refX="6" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6 Z" fill="#14305e"/></marker></defs>
|
||||||
|
<rect x="8" y="35" width="160" height="72" rx="8" fill="#eef2f8" stroke="#14305e"/>
|
||||||
|
<text x="88" y="60" text-anchor="middle" font-size="11" font-weight="bold" fill="#14305e">🌍 위도·경도 (도)</text>
|
||||||
|
<text x="88" y="78" text-anchor="middle" font-size="9.5" fill="#555">36.33°N, 127.41°E</text>
|
||||||
|
<text x="88" y="94" text-anchor="middle" font-size="9" fill="#888">둥근 지구 주소</text>
|
||||||
|
<rect x="216" y="35" width="170" height="72" rx="8" fill="#fff" stroke="#555"/>
|
||||||
|
<text x="301" y="60" text-anchor="middle" font-size="11" font-weight="bold">🗺 평면 지도 좌표 (m)</text>
|
||||||
|
<text x="301" y="78" text-anchor="middle" font-size="9.5" fill="#555">EPSG:5186 한국 TM</text>
|
||||||
|
<text x="301" y="94" text-anchor="middle" font-size="9" fill="#888">동쪽 x m · 북쪽 y m</text>
|
||||||
|
<rect x="434" y="35" width="150" height="72" rx="8" fill="#fff" stroke="#555"/>
|
||||||
|
<text x="509" y="60" text-anchor="middle" font-size="11" font-weight="bold">🛸 드론 기준 (m)</text>
|
||||||
|
<text x="509" y="78" text-anchor="middle" font-size="9.5" fill="#555">동·북·위 상대거리</text>
|
||||||
|
<text x="509" y="94" text-anchor="middle" font-size="9" fill="#888">"드론에서 동쪽 40m…"</text>
|
||||||
|
<rect x="632" y="35" width="120" height="72" rx="8" fill="#fff7e6" stroke="#b45309"/>
|
||||||
|
<text x="692" y="60" text-anchor="middle" font-size="11" font-weight="bold" fill="#b45309">📷 카메라 눈</text>
|
||||||
|
<text x="692" y="78" text-anchor="middle" font-size="9.5" fill="#7c2d12">(Xc, Yc, Zc)</text>
|
||||||
|
<text x="692" y="94" text-anchor="middle" font-size="9" fill="#888">6단계에서</text>
|
||||||
|
<line x1="168" y1="71" x2="214" y2="71" stroke="#14305e" stroke-width="1.5" marker-end="url(#f4a)"/>
|
||||||
|
<line x1="386" y1="71" x2="432" y2="71" stroke="#14305e" stroke-width="1.5" marker-end="url(#f4a)"/>
|
||||||
|
<line x1="584" y1="71" x2="630" y2="71" stroke="#14305e" stroke-width="1.5" marker-end="url(#f4a)"/>
|
||||||
|
<text x="191" y="60" text-anchor="middle" font-size="9" fill="#888">proj4</text>
|
||||||
|
<text x="409" y="60" text-anchor="middle" font-size="9" fill="#888">빼기(−드론)</text>
|
||||||
|
<text x="607" y="60" text-anchor="middle" font-size="9" fill="#888">회전행렬</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>그림 4. 위경도 → 한국 TM 평면(m) → 드론 기준 상대좌표 → 카메라 좌표, 3번의 갈아타기</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
- **위경도 → TM(m)**: proj4 라이브러리로 한국 표준 평면좌표(EPSG:5186)로 변환. 이제 "미터"로 잴 수 있어요.
|
||||||
|
- **TM → 드론 기준**: 핀 위치에서 드론 위치를 **빼면**, "드론에서 동쪽 40m, 북쪽 12m, 아래 30m" 같은 상대 위치가 나와요.
|
||||||
|
|
||||||
|
> 코드: [geoProjection.ts:75](../client/src/utils/geoProjection.ts#L75) EPSG:5186 정의, [geoProjection.ts:334](../client/src/utils/geoProjection.ts#L334) `projectPoint()` 1부 — ENU 상대좌표 계산.
|
||||||
|
|
||||||
|
## 6단계 — 카메라의 눈으로 회전: "드론이 보는 방향은?"
|
||||||
|
|
||||||
|
드론이 어느 쪽을 보고 있는지(비행로그의 **yaw·pitch·roll**)에 따라, 같은 핀도 화면 왼쪽에 보일 수도, 오른쪽에 보일 수도 있어요.
|
||||||
|
그래서 상대좌표를 **회전행렬**로 돌려서 "카메라가 보는 세계"의 좌표 (Xc, Yc, Zc)로 바꿉니다.
|
||||||
|
|
||||||
|
```
|
||||||
|
R = Rz(−yaw) × Rx(pitch) × Ry(roll) ← 드론의 고개 돌림·끄덕임·갸웃을 그대로 재현
|
||||||
|
(Xc, Yc, Zc) = 카메라축변환 × Rᵀ × (동, 북, 위)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Zc** 는 "카메라 앞쪽으로 몇 m인가"예요. **Zc ≤ 0이면 카메라 뒤** → 그리지 않아요.
|
||||||
|
- 너무 멀거나(거리 필터) 화각(시야) 밖이어도 걸러냅니다.
|
||||||
|
|
||||||
|
> 코드: [geoProjection.ts:301](../client/src/utils/geoProjection.ts#L301) `toCameraCoords()`, 회전 수식 주석은 [geoProjection.ts:319](../client/src/utils/geoProjection.ts#L319)~.
|
||||||
|
|
||||||
|
## 7단계 — 핀홀 투영: 3D 세상을 2D 사진으로
|
||||||
|
|
||||||
|
카메라는 **바늘구멍 사진기**와 같아요. 3D 점이 구멍(렌즈)을 지나 필름(화면)에 맺힙니다.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<svg viewBox="0 0 760 190" role="img" aria-label="핀홀 투영">
|
||||||
|
<!-- 카메라 -->
|
||||||
|
<rect x="40" y="70" width="56" height="44" rx="6" fill="#333"/>
|
||||||
|
<circle cx="96" cy="92" r="7" fill="#111" stroke="#888"/>
|
||||||
|
<text x="68" y="132" text-anchor="middle" font-size="10" fill="#555">카메라(핀홀)</text>
|
||||||
|
<!-- 이미지 평면 -->
|
||||||
|
<rect x="150" y="30" width="10" height="124" fill="#e2e8f0" stroke="#94a3b8"/>
|
||||||
|
<text x="155" y="170" text-anchor="middle" font-size="10" fill="#555">화면(필름)</text>
|
||||||
|
<!-- 대상 -->
|
||||||
|
<circle cx="620" cy="52" r="7" fill="#dc2626"/>
|
||||||
|
<text x="620" y="38" text-anchor="middle" font-size="10.5" font-weight="bold" fill="#991b1b">교량 핀 (Xc, Yc, Zc)</text>
|
||||||
|
<!-- 광선 -->
|
||||||
|
<line x1="620" y1="52" x2="96" y2="92" stroke="#f59e0b" stroke-width="1.6" stroke-dasharray="6 3"/>
|
||||||
|
<circle cx="155" cy="88" r="4.5" fill="#f59e0b"/>
|
||||||
|
<text x="182" y="80" font-size="10" fill="#b45309" font-weight="bold">맺힌 자리 = 화면 위치!</text>
|
||||||
|
<!-- 공식 -->
|
||||||
|
<rect x="240" y="120" width="480" height="54" rx="6" fill="#fffbeb" stroke="#f59e0b"/>
|
||||||
|
<text x="480" y="142" text-anchor="middle" font-size="11.5" font-weight="bold" fill="#92400e">가로 위치 = 0.5 + (Xc ÷ Zc) × (초점거리 ÷ 센서폭)</text>
|
||||||
|
<text x="480" y="162" text-anchor="middle" font-size="11.5" font-weight="bold" fill="#92400e">세로 위치 = 0.5 + (Yc ÷ Zc) × (초점거리 ÷ 센서높이)</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>그림 5. 핀홀 투영 — "멀수록(Zc↑) 가운데로 모이고, 옆에 있을수록(Xc↑) 가장자리로" 라는 원근법 그 자체</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
- 결과는 **0~1 사이의 정규 좌표**예요. (0.5, 0.5)면 화면 정중앙.
|
||||||
|
- ÷Zc 가 바로 **원근법**이에요 — 멀리 있는 것일수록 화면 가운데 근처로 작게 모여요.
|
||||||
|
- 초점거리(focal)는 비행로그에서 프레임마다 읽고, 센서 크기는 16:9 영상 기준(36×20.25mm)을 써요. 영상마다 화각이 다르면 **라벨 1개 드래그로 세로 화각을 역산**해 맞출 수도 있어요(FOV 1점 보정).
|
||||||
|
|
||||||
|
> 코드: [geoProjection.ts:126](../client/src/utils/geoProjection.ts#L126) `pixelFromCamera()` — 위 공식 그대로.
|
||||||
|
|
||||||
|
## 8단계 — 화면에 붙이기: 액자 맞추기 + 떨림 잡기
|
||||||
|
|
||||||
|
마지막으로 "0~1 정규 좌표"를 **진짜 화면 픽셀**로 바꿔 이름표를 놓아요. 여기엔 두 가지 마무리가 있어요.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<svg viewBox="0 0 760 180" role="img" aria-label="cover 정렬과 평활">
|
||||||
|
<!-- cover 정렬 -->
|
||||||
|
<rect x="20" y="24" width="330" height="130" rx="6" fill="#f8fafc" stroke="#94a3b8"/>
|
||||||
|
<text x="185" y="16" text-anchor="middle" font-size="10.5" font-weight="bold" fill="#333">① 액자 맞추기 (object-fit: cover)</text>
|
||||||
|
<rect x="45" y="44" width="280" height="90" fill="#e0f2fe" stroke="#0284c7" stroke-dasharray="5 3"/>
|
||||||
|
<rect x="20" y="44" width="330" height="90" fill="none" stroke="#dc2626" stroke-width="1.6"/>
|
||||||
|
<text x="185" y="76" text-anchor="middle" font-size="9.5" fill="#0369a1">플레이어 창(파랑 점선) 안에</text>
|
||||||
|
<text x="185" y="92" text-anchor="middle" font-size="9.5" fill="#991b1b">영상(빨강)이 꽉 차게 늘어나 양옆이 잘림</text>
|
||||||
|
<text x="185" y="112" text-anchor="middle" font-size="9.5" fill="#555">→ 오버레이도 <tspan font-weight="bold">같은 사각형</tspan>으로 계산해야 어긋나지 않음</text>
|
||||||
|
<text x="185" y="170" text-anchor="middle" font-size="9" fill="#888">정규좌표(0~1) × 영상표시영역 + 잘림offset = 화면 px</text>
|
||||||
|
<!-- 평활 -->
|
||||||
|
<rect x="400" y="24" width="340" height="130" rx="6" fill="#f8fafc" stroke="#94a3b8"/>
|
||||||
|
<text x="570" y="16" text-anchor="middle" font-size="10.5" font-weight="bold" fill="#333">② 떨림 잡기 (속도적응 평활)</text>
|
||||||
|
<polyline points="420,110 445,70 470,105 495,68 520,108 545,72" fill="none" stroke="#dc2626" stroke-width="1.4"/>
|
||||||
|
<text x="482" y="130" text-anchor="middle" font-size="9.5" fill="#991b1b">GPS 떨림 그대로 → 라벨이 덜덜덜</text>
|
||||||
|
<polyline points="575,95 600,88 625,86 650,85 675,84 700,84" fill="none" stroke="#16a34a" stroke-width="2"/>
|
||||||
|
<text x="640" y="130" text-anchor="middle" font-size="9.5" fill="#14532d">평활 후 → 착 붙음</text>
|
||||||
|
<text x="570" y="150" text-anchor="middle" font-size="9" fill="#888">떨림(방향 왔다갔다)=강하게 진정 · 실제 이동(방향 일정)=즉시 따라감</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>그림 6. (왼쪽) 영상이 창에 '꽉 차게' 잘려 보이므로 오버레이도 같은 규칙으로 정렬 · (오른쪽) 떨림만 골라 진정시키는 평활</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
1. **액자 맞추기(cover 정렬)** — 영상은 플레이어 창을 비율 유지한 채 **꽉 채우며 잘려** 보여요. 그래서 이름표도 "창 기준"이 아니라 **"잘린 영상 사각형 기준"** 으로 놓아야 정확히 붙어요.
|
||||||
|
2. **떨림 잡기(평활)** — GPS·자세값은 미세하게 떨려요. "방향이 왔다갔다(=떨림)"면 강하게 진정시키고, "방향이 일정(=실제 이동)"하면 즉시 따라가는 **속도적응 평활**로, 지연 없이 떨림만 없앱니다.
|
||||||
|
3. 이 계산 전부를 매 화면 프레임(**60fps, requestAnimationFrame**)마다 다시 해요 — 그런데도 빠른 이유는, 무거운 준비(가시성 선별)는 미리 해 두고 프레임마다는 **가벼운 재투영만** 하기 때문이에요.
|
||||||
|
|
||||||
|
> 코드: [StationOverlay.tsx:948](../client/src/components/overlay/StationOverlay.tsx#L948) cover 정렬, [StationOverlay.tsx:49](../client/src/components/overlay/StationOverlay.tsx#L49) `smoothStep()` 평활.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 보너스 — 그래도 어긋나면? "드래그 한 번"
|
||||||
|
|
||||||
|
지오코딩(주소→좌표 변환) 자체에 오차가 있으면 이름표가 조금 비껴 붙을 수 있어요.
|
||||||
|
그럴 땐 **이름표를 마우스로 끌어 제자리에 놓기만 하면** 돼요.
|
||||||
|
|
||||||
|
- 화면에서 끈 위치(2D)를 7단계 공식의 **역방향**으로 계산해(역투영), 실제 좌표(위경도+높이)를 되찾아 저장해요.
|
||||||
|
- 2D→3D는 원래 답이 무한히 많지만(깊이를 모르므로), **"카메라와의 거리는 그대로"** 라는 약속을 둬서 드래그 한 번으로 가로·세로·높이가 **동시에** 맞춰집니다.
|
||||||
|
- 고친 값은 데이터셋별로 저장돼 다음에 열어도 유지돼요. (왕복 검증 오차 ≈ 0)
|
||||||
|
|
||||||
|
> 코드: [geoProjection.ts:149](../client/src/utils/geoProjection.ts#L149) `worldFromPixel()` — 역투영.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 단계별 요약표
|
||||||
|
|
||||||
|
| 단계 | 하는 일 | 비유 | 코드 위치 |
|
||||||
|
|:-:|------|------|----------|
|
||||||
|
| 1 | KMZ 압축 해제 → doc.kml | 선물상자 열기 | geoData.ts `parseKmz` |
|
||||||
|
| 2 | KML 폴더 나무 걷기, 핀 수집 | 설명서 읽기 | geoData.ts `walk` |
|
||||||
|
| 3 | 폴더명으로 POI/구조물 분류 | 반 나누기 | geoData.ts `handlePlacemark` |
|
||||||
|
| 4 | 높이 결정(측점 표고) + 기준 맞춤(+25.8m) | 키 재기, 자 통일 | StationOverlay + geoidOffset |
|
||||||
|
| 5 | 위경도 → 평면 m → 드론 기준 상대좌표 | 주소를 미터 자로 | geoProjection.ts `projectPoint` |
|
||||||
|
| 6 | yaw·pitch·roll 회전 → 카메라 좌표 | 카메라 눈으로 보기 | geoProjection.ts `toCameraCoords` |
|
||||||
|
| 7 | 핀홀 투영 → 화면 정규좌표(0~1) | 바늘구멍 사진기 | geoProjection.ts `pixelFromCamera` |
|
||||||
|
| 8 | cover 정렬 + 평활 + 60fps 렌더 | 액자 맞추고 떨림 잡기 | StationOverlay.tsx |
|
||||||
|
| ★ | 어긋나면 드래그 1회 역투영 보정 | 이름표 손으로 옮기기 | geoProjection.ts `worldFromPixel` |
|
||||||
|
|
||||||
|
> 더 깊은 수식·구현 근거: 「기술명세_GhiVideo_종합기술문서」 · 「구현상세_드론좌표_영상투영_소스코드매칭」 참조.
|
||||||
Binary file not shown.
@@ -0,0 +1,339 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang xml:lang>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="pandoc" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
|
||||||
|
<title>쉬운설명_프레임기반에서_측점기반_플레이어</title>
|
||||||
|
<style>
|
||||||
|
code{white-space: pre-wrap;}
|
||||||
|
span.smallcaps{font-variant: small-caps;}
|
||||||
|
span.underline{text-decoration: underline;}
|
||||||
|
div.column{display: inline-block; vertical-align: top; width: 50%;}
|
||||||
|
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
|
||||||
|
ul.task-list{list-style: none;}
|
||||||
|
.display.math{display: block; text-align: center; margin: 0.5rem auto;}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">@page {
|
||||||
|
size: A4;
|
||||||
|
margin: 18mm 16mm 16mm 16mm;
|
||||||
|
@bottom-center {
|
||||||
|
content: counter(page) " / " counter(pages);
|
||||||
|
font-family: "Malgun Gothic", sans-serif;
|
||||||
|
font-size: 9pt;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html { font-size: 11pt; }
|
||||||
|
body {
|
||||||
|
font-family: "Malgun Gothic", "Hancom Gothic", sans-serif;
|
||||||
|
color: #23272e;
|
||||||
|
line-height: 1.65;
|
||||||
|
max-width: 920px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.7rem;
|
||||||
|
color: #b45309;
|
||||||
|
border-bottom: 3px solid #f59e0b;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #b45309;
|
||||||
|
border-bottom: 1px solid #e5d3b3;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
margin-top: 1.6em;
|
||||||
|
}
|
||||||
|
h3 { font-size: 1.05rem; color: #92400e; margin-top: 1.1em; }
|
||||||
|
a { color: #b45309; }
|
||||||
|
hr { border: none; border-top: 1px solid #e2e2e2; margin: 1.6em 0; }
|
||||||
|
ul { padding-left: 1.25em; }
|
||||||
|
li { margin: 0.18em 0; }
|
||||||
|
strong { color: #1f2937; }
|
||||||
|
code {
|
||||||
|
font-family: "D2Coding", Consolas, monospace;
|
||||||
|
background: #f4f1ea;
|
||||||
|
border: 1px solid #e7e0d2;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.5px 5px;
|
||||||
|
font-size: 0.92em;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0.8em 0;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
th, td { border: 1px solid #d8d2c4; padding: 6px 10px; text-align: left; vertical-align: top; }
|
||||||
|
th { background: #fdf3df; color: #7c2d12; }
|
||||||
|
blockquote {
|
||||||
|
border-left: 4px solid #f59e0b;
|
||||||
|
margin: 0.8em 0;
|
||||||
|
padding: 0.2em 0 0.2em 14px;
|
||||||
|
color: #555;
|
||||||
|
background: #fffbf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3 { break-after: avoid; }
|
||||||
|
</style>
|
||||||
|
<!--[if lt IE 9]>
|
||||||
|
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
|
||||||
|
<![endif]-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header id="title-block-header">
|
||||||
|
<h1 class="title">쉬운설명_프레임기반에서_측점기반_플레이어</h1>
|
||||||
|
</header>
|
||||||
|
<h1 id="시간이-아니라-측점으로-움직이는-영상-플레이어-그림-설명서">시간이 아니라 '측점'으로 움직이는 영상 플레이어 (그림 설명서)</h1>
|
||||||
|
<blockquote>
|
||||||
|
<p>이 글은 <strong>초등학생도 이해할 수 있게</strong> 그림과 이야기로 쓴 설명서예요. "보통 영상 플레이어는 <strong>시간(프레임)</strong> 으로 움직이는데, 우리는 어떻게 <strong>측점(선로 위치)</strong> 으로 움직이게 바꿨을까?" 그 비밀을 하나씩 풀어 줄게요. (진짜 코드는 <a href="../client/src/utils/chainage.ts">chainage.ts</a> · <a href="../client/src/stationbar/StationBar.tsx">StationBar.tsx</a> 참고)</p>
|
||||||
|
</blockquote>
|
||||||
|
<style>
|
||||||
|
figure.fig{margin:22px 0;text-align:center;page-break-inside:avoid;break-inside:avoid;}
|
||||||
|
figure.fig svg{width:100%;max-width:720px;height:auto;border:1px solid #e7e0d2;border-radius:10px;background:#fffdf8;}
|
||||||
|
figure.fig figcaption{font-size:0.9em;color:#777;margin-top:6px;}
|
||||||
|
text{font-family:'Malgun Gothic','Hancom Gothic',sans-serif;}
|
||||||
|
h2{border-top:3px solid #e7e0d2;padding-top:14px;margin-top:34px;}
|
||||||
|
code{background:#f5f3ee;padding:1px 4px;border-radius:4px;}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<h2 id="0-한-문장으로-말하면">0. 한 문장으로 말하면</h2>
|
||||||
|
<p><strong>"영상의 매 장면마다 드론 GPS로 '지금 몇 측점인지'를 계산해 두어서, 시간(몇 분 몇 초)이 아니라 측점(160k130 같은 선로 위치)으로 영상을 찾아보게 만든 기술"</strong> 이에요.</p>
|
||||||
|
<blockquote>
|
||||||
|
<p>🎯 비유: 보통 영화는 "32분 10초로 가줘"라고 하죠. 우리 플레이어는 <strong>"회덕터널 앞으로 가줘"</strong> 처럼 <strong>장소로</strong> 찾아가요.</p>
|
||||||
|
</blockquote>
|
||||||
|
<hr />
|
||||||
|
<h2 id="1-보통-플레이어-vs-우리-플레이어">1. 보통 플레이어 vs 우리 플레이어</h2>
|
||||||
|
<p>보통 영상 플레이어의 아래 막대는 <strong>시간</strong>(0:00 ~ 끝)이에요. 그런데 철도 점검하는 분들은 "몇 분"이 아니라 <strong>"몇 측점"</strong> 으로 이야기해요. 그래서 막대를 <strong>측점 기준</strong>으로 바꿨어요.</p>
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 720 250" role="img" aria-label="시간축 재생바 vs 측점축 재생바 비교">
|
||||||
|
<text x="360" y="24" text-anchor="middle" font-size="13" font-weight="bold" fill="#475569">① 보통 플레이어 (시간 축)</text>
|
||||||
|
<rect x="40" y="38" width="640" height="26" rx="6" fill="#e5e7eb" stroke="#9ca3af"></rect>
|
||||||
|
<rect x="40" y="38" width="250" height="26" rx="6" fill="#9ca3af"></rect>
|
||||||
|
<circle cx="290" cy="51" r="10" fill="#374151"></circle>
|
||||||
|
<text x="45" y="82" font-size="11" fill="#374151">0:00</text>
|
||||||
|
<text x="300" y="82" font-size="11" fill="#374151">03:12</text>
|
||||||
|
<text x="648" y="82" font-size="11" fill="#374151">09:28</text>
|
||||||
|
<text x="360" y="104" text-anchor="middle" font-size="11" fill="#6b7280">"3분 12초 지점" — 여기가 어느 선로인지 모름 😵</text>
|
||||||
|
<text x="360" y="150" text-anchor="middle" font-size="13" font-weight="bold" fill="#166534">② 우리 플레이어 (측점 축)</text>
|
||||||
|
<rect x="40" y="164" width="640" height="26" rx="6" fill="#fde9c8" stroke="#e0a94f"></rect>
|
||||||
|
<rect x="40" y="164" width="310" height="26" rx="6" fill="#f59e0b"></rect>
|
||||||
|
<circle cx="350" cy="177" r="11" fill="#ea580c"></circle><text x="350" y="181" text-anchor="middle" font-size="9" fill="#fff" font-weight="bold">160k</text>
|
||||||
|
<line x1="205" y1="160" x2="205" y2="194" stroke="#0284c7" stroke-width="2"></line><text x="205" y="210" text-anchor="middle" font-size="9" fill="#0369a1">회덕터널</text>
|
||||||
|
<line x1="480" y1="160" x2="480" y2="194" stroke="#0284c7" stroke-width="2"></line><text x="480" y="210" text-anchor="middle" font-size="9" fill="#0369a1">회덕천교</text>
|
||||||
|
<text x="45" y="230" font-size="11" fill="#166534">대전조차장</text>
|
||||||
|
<text x="612" y="230" font-size="11" fill="#166534">신탄진</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>시간 축은 "몇 분"만 알려준다. 측점 축은 "지금 어느 선로 위치(측점)", "무슨 구조물 근처"인지 한눈에 보인다.</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<h2 id="2-측점이-뭐예요-">2. '측점'이 뭐예요? 📍</h2>
|
||||||
|
<p>철도에서 <strong>출발점부터 몇 미터 왔는지</strong>를 나타내는 '거리 이정표'예요. 도로의 <strong>km 표지판</strong>과 똑같아요!</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>160k130</code> = 출발점에서 <strong>160km + 130m</strong> 지점</li>
|
||||||
|
<li>숫자가 커지면 앞으로 간 것, 작아지면 되돌아온 것</li>
|
||||||
|
</ul>
|
||||||
|
<hr />
|
||||||
|
<h2 id="3-핵심-비밀--매-장면마다-시간과-측점을-짝지어-둔다-️">3. 핵심 비밀 — 매 장면마다 '시간'과 '측점'을 짝지어 둔다 🗺️</h2>
|
||||||
|
<p>비밀은 <strong>드론 GPS</strong>예요. 드론 영상의 <strong>매 프레임(사진 한 장)</strong> 에는 그때 드론의 <strong>위도·경도(GPS)</strong> 가 기록돼 있어요. 이걸 이용해 프레임마다 두 가지를 계산해 <strong>짝지어</strong> 둬요.</p>
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 720 330" role="img" aria-label="사진 1장마다 시간과 측점을 계산해 표로 모으고, 표로 양방향 변환">
|
||||||
|
<defs><marker id="ad" markerWidth="10" markerHeight="10" refX="7" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#7c3aed"></path></marker></defs>
|
||||||
|
<!-- ① 왼쪽: 사진 1장 → 두 값 계산 -->
|
||||||
|
<text x="178" y="22" text-anchor="middle" font-size="12" font-weight="bold" fill="#334155">① 사진 1장마다 두 값을 계산</text>
|
||||||
|
<rect x="88" y="34" width="180" height="58" rx="8" fill="#f1f5f9" stroke="#475569"></rect>
|
||||||
|
<text x="178" y="57" text-anchor="middle" font-size="11.5" font-weight="bold" fill="#334155">🎞 프레임 5760번</text>
|
||||||
|
<text x="178" y="78" text-anchor="middle" font-size="10" fill="#475569">📍 드론 GPS 함께 기록됨</text>
|
||||||
|
<line x1="133" y1="92" x2="106" y2="118" stroke="#7c3aed" stroke-width="2" marker-end="url(#ad)"></line>
|
||||||
|
<line x1="223" y1="92" x2="250" y2="118" stroke="#7c3aed" stroke-width="2" marker-end="url(#ad)"></line>
|
||||||
|
<rect x="20" y="122" width="160" height="58" rx="8" fill="#eff6ff" stroke="#2563eb"></rect>
|
||||||
|
<text x="100" y="144" text-anchor="middle" font-size="11.5" font-weight="bold" fill="#1e3a8a">⏱ 시간 = 192초</text>
|
||||||
|
<text x="100" y="166" text-anchor="middle" font-size="9.5" fill="#1e40af">번호 5760 ÷ 30fps</text>
|
||||||
|
<rect x="196" y="122" width="160" height="58" rx="8" fill="#f0fdf4" stroke="#16a34a"></rect>
|
||||||
|
<text x="276" y="144" text-anchor="middle" font-size="11.5" font-weight="bold" fill="#166534">📐 측점 = 160k130</text>
|
||||||
|
<text x="276" y="166" text-anchor="middle" font-size="9.5" fill="#15803d">GPS를 선로에 '툭' 투영</text>
|
||||||
|
<!-- 가운데 화살표: 반복 -->
|
||||||
|
<line x1="368" y1="106" x2="418" y2="106" stroke="#7c3aed" stroke-width="3" marker-end="url(#ad)"></line>
|
||||||
|
<text x="393" y="92" text-anchor="middle" font-size="10" font-weight="bold" fill="#7c3aed">모든 사진에</text>
|
||||||
|
<text x="393" y="128" text-anchor="middle" font-size="10" font-weight="bold" fill="#7c3aed">반복</text>
|
||||||
|
<!-- ② 오른쪽: 시간↔측점 대응표 -->
|
||||||
|
<text x="560" y="22" text-anchor="middle" font-size="12" font-weight="bold" fill="#854d0e">② '시간↔측점 대응표' 완성</text>
|
||||||
|
<rect x="440" y="34" width="240" height="26" fill="#fef3c7" stroke="#ca8a04"></rect>
|
||||||
|
<line x1="560" y1="34" x2="560" y2="190" stroke="#ca8a04" stroke-width="1"></line>
|
||||||
|
<text x="500" y="52" text-anchor="middle" font-size="11" font-weight="bold" fill="#1e3a8a">⏱ 시간</text>
|
||||||
|
<text x="620" y="52" text-anchor="middle" font-size="11" font-weight="bold" fill="#166534">📐 측점</text>
|
||||||
|
<rect x="440" y="60" width="240" height="26" fill="#ffffff" stroke="#ca8a04"></rect>
|
||||||
|
<text x="500" y="78" text-anchor="middle" font-size="10.5" fill="#334155">191초</text>
|
||||||
|
<text x="620" y="78" text-anchor="middle" font-size="10.5" fill="#334155">160k100</text>
|
||||||
|
<rect x="440" y="86" width="240" height="26" fill="#fef9e7" stroke="#ca8a04"></rect>
|
||||||
|
<text x="500" y="104" text-anchor="middle" font-size="10.5" font-weight="bold" fill="#1e3a8a">192초</text>
|
||||||
|
<text x="620" y="104" text-anchor="middle" font-size="10.5" font-weight="bold" fill="#166534">160k130</text>
|
||||||
|
<rect x="440" y="112" width="240" height="26" fill="#ffffff" stroke="#ca8a04"></rect>
|
||||||
|
<text x="500" y="130" text-anchor="middle" font-size="10.5" fill="#334155">193초</text>
|
||||||
|
<text x="620" y="130" text-anchor="middle" font-size="10.5" fill="#334155">160k160</text>
|
||||||
|
<rect x="440" y="138" width="240" height="26" fill="#ffffff" stroke="#ca8a04"></rect>
|
||||||
|
<text x="500" y="156" text-anchor="middle" font-size="10.5" fill="#94a3b8">⋮</text>
|
||||||
|
<text x="620" y="156" text-anchor="middle" font-size="10.5" fill="#94a3b8">⋮</text>
|
||||||
|
<rect x="440" y="164" width="240" height="26" fill="#ffffff" stroke="#ca8a04"></rect>
|
||||||
|
<text x="500" y="182" text-anchor="middle" font-size="10.5" fill="#334155">348초</text>
|
||||||
|
<text x="620" y="182" text-anchor="middle" font-size="10.5" fill="#334155">165k000</text>
|
||||||
|
<!-- ③ 아래: 표를 쓰는 두 방향 -->
|
||||||
|
<text x="360" y="232" text-anchor="middle" font-size="12" font-weight="bold" fill="#334155">③ 이 표 하나로, 어느 쪽으로든 찾기</text>
|
||||||
|
<rect x="110" y="246" width="500" height="30" rx="7" fill="#eff6ff" stroke="#2563eb"></rect>
|
||||||
|
<text x="360" y="266" text-anchor="middle" font-size="10.5" fill="#1e3a8a">재생 중: <tspan font-weight="bold">"지금 192초… 여긴 어디?"</tspan> → 표에서 찾아 → <tspan font-weight="bold" fill="#166534">측점 160k130 표시</tspan></text>
|
||||||
|
<rect x="110" y="284" width="500" height="30" rx="7" fill="#f0fdf4" stroke="#16a34a"></rect>
|
||||||
|
<text x="360" y="304" text-anchor="middle" font-size="10.5" fill="#166534">검색: <tspan font-weight="bold">"160k130 가줘!"</tspan> → 표에서 찾아 → <tspan font-weight="bold" fill="#1e3a8a">192초로 점프</tspan></text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>사진 1장마다 (시간, 측점) 짝을 계산해(①) 표로 모아 두면(②), "지금 어디?"와 "거기로 가줘!"를 모두 이 표 하나로 해결한다(③). (코드: StationBar precompute)</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<h3 id="gps를-측점으로-바꾸는-법--선로에-수직으로-툭-">GPS를 측점으로 바꾸는 법 — '선로에 수직으로 툭' 📐</h3>
|
||||||
|
<p>드론은 선로 바로 위가 아니라 옆에서 비스듬히 날 때도 많아요. 그래서 드론 GPS 점을 <strong>선로(측점들을 이은 선)에 수직으로 툭 떨어뜨려</strong>, 그 발자국이 몇 측점인지 읽어요.</p>
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 720 170" role="img" aria-label="드론 GPS를 선로에 수직 투영해 측점 읽기">
|
||||||
|
<line x1="60" y1="120" x2="660" y2="70" stroke="#8a7a5c" stroke-width="6"></line>
|
||||||
|
<text x="70" y="145" font-size="11" fill="#6b4f2a">선로 (측점 158k·159k·160k…를 이은 선)</text>
|
||||||
|
<circle cx="360" cy="32" r="7" fill="#2563eb"></circle><text x="360" y="24" text-anchor="middle" font-size="10" fill="#1e3a8a">드론 GPS</text>
|
||||||
|
<line x1="360" y1="32" x2="374" y2="94" stroke="#ef4444" stroke-width="2" stroke-dasharray="4 3"></line>
|
||||||
|
<circle cx="374" cy="94" r="5" fill="#ef4444"></circle>
|
||||||
|
<text x="410" y="98" font-size="11" fill="#b91c1c" font-weight="bold">수직으로 내린 발자국 = 160k130</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>드론이 선로 옆에 있어도, 수직으로 내린 발자국 위치로 '측점값'을 정확히 읽는다. (코드: chainage.ts projectToChain)</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<h2 id="4-재생바의-가로축을-시간이-아니라-실제-이동-거리로-">4. 재생바의 가로축을 '시간'이 아니라 '실제 이동 거리'로 🚂</h2>
|
||||||
|
<p>여기가 특히 똑똑한 부분이에요. 드론이 <strong>멈춰서 맴돌면</strong> 시간은 계속 흐르지만 <strong>위치(측점)는 그대로</strong>예요. 이때 막대 커서가 계속 가면 이상하죠.</p>
|
||||||
|
<p>그래서 가로축을 <strong>"실제로 이동한 거리"</strong>(측점이 변한 양을 쌓은 값)로 만들었어요. → <strong>멈추면 커서도 멈추고, 움직일 때만 앞으로 가요.</strong></p>
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 720 150" role="img" aria-label="이동거리 축 - 멈추면 커서 정지">
|
||||||
|
<text x="180" y="22" text-anchor="middle" font-size="12" font-weight="bold" fill="#475569">시간 축 (옛 방식)</text>
|
||||||
|
<rect x="40" y="34" width="280" height="20" rx="5" fill="#e5e7eb" stroke="#9ca3af"></rect><circle cx="240" cy="44" r="8" fill="#374151"></circle>
|
||||||
|
<text x="180" y="78" text-anchor="middle" font-size="10" fill="#b91c1c">드론이 멈춰 맴돌아도 커서가 계속 감 😵</text>
|
||||||
|
<text x="540" y="22" text-anchor="middle" font-size="12" font-weight="bold" fill="#166534">이동거리 축 (새 방식)</text>
|
||||||
|
<rect x="400" y="34" width="280" height="20" rx="5" fill="#fde9c8" stroke="#e0a94f"></rect><circle cx="560" cy="44" r="8" fill="#ea580c"></circle>
|
||||||
|
<text x="540" y="78" text-anchor="middle" font-size="10" fill="#166534">멈추면 커서도 멈춤, 움직일 때만 전진 😎</text>
|
||||||
|
<text x="360" y="120" text-anchor="middle" font-size="11" fill="#475569">→ 그래서 커서 위치만 봐도 "드론이 실제로 얼마나 이동했는지"를 알 수 있어요.</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>가로축 = 측점 변화량을 쌓은 '이동거리'. 전진이든 후진이든 움직이면 커서는 오른쪽으로 간다(방향은 색으로 구분). (코드: cumFracAtTime / timeAtFrac)</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<h2 id="5-방향을-색으로--앞으로-가면-주황-되돌아오면-파랑-">5. 방향을 색으로 — 앞으로 가면 주황, 되돌아오면 파랑 🎨</h2>
|
||||||
|
<p>드론이 앞으로 가서 측점이 커지면 <strong>주황</strong>, 되돌아와서 측점이 작아지면 <strong>하늘색</strong> 으로 칠해요. 그래서 막대만 봐도 <strong>어디서 앞으로/뒤로 갔는지</strong> 알 수 있어요.</p>
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 720 90" role="img" aria-label="진행 방향 색 구분">
|
||||||
|
<rect x="40" y="30" width="230" height="24" rx="5" fill="#f59e0b"></rect><text x="155" y="47" text-anchor="middle" font-size="11" fill="#7a3d00" font-weight="bold">전진(측점 ↑) 주황</text>
|
||||||
|
<rect x="270" y="30" width="150" height="24" rx="5" fill="#3fb6d4"></rect><text x="345" y="47" text-anchor="middle" font-size="11" fill="#06455a" font-weight="bold">후진(측점 ↓) 파랑</text>
|
||||||
|
<rect x="420" y="30" width="260" height="24" rx="5" fill="#f59e0b"></rect><text x="550" y="47" text-anchor="middle" font-size="11" fill="#7a3d00" font-weight="bold">다시 전진 주황</text>
|
||||||
|
<text x="360" y="78" text-anchor="middle" font-size="10" fill="#475569">색이 바뀌는 지점 = 드론이 방향을 바꾼 곳</text>
|
||||||
|
</svg>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<h2 id="6-구조물을-측점-위치에-딱-배치-">6. 구조물을 '측점 위치'에 딱 배치 🚉</h2>
|
||||||
|
<p>교량·터널·역을 각자의 <strong>측점값 위치</strong>에 표시해요. 각 시설물의 실제 측점(예: 회덕터널 160k)과, 드론이 <strong>그 측점을 실제로 지나는 시점</strong>을 맞춰서 막대에 점으로 찍어요. 그래서 "이 구조물이 어디쯤인지"를 막대만 봐도 알 수 있어요.</p>
|
||||||
|
<blockquote>
|
||||||
|
<p>드론이 같은 곳을 <strong>여러 번 지나가면</strong>(조차장에서 왔다갔다) 그 시설물 마커도 <strong>지날 때마다</strong> 찍혀요.</p>
|
||||||
|
</blockquote>
|
||||||
|
<hr />
|
||||||
|
<h2 id="7-측점으로-찾아가기-검색-">7. 측점으로 '찾아가기' (검색) 🔎</h2>
|
||||||
|
<p>측점값을 입력하면 → <strong>그 측점이 나오는 시간을 표에서 찾아 → 그 시간으로 이동</strong>해요. (3번에서 만든 '시간↔︎측점 대응표' 덕분!)</p>
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 720 90" role="img" aria-label="측점 입력으로 이동">
|
||||||
|
<defs><marker id="as" markerWidth="10" markerHeight="10" refX="7" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#7c3aed"></path></marker></defs>
|
||||||
|
<rect x="30" y="30" width="180" height="34" rx="6" fill="#f0fdf4" stroke="#16a34a"></rect><text x="120" y="52" text-anchor="middle" font-size="12" fill="#166534">"160k130" 입력</text>
|
||||||
|
<line x1="210" y1="47" x2="258" y2="47" stroke="#7c3aed" stroke-width="2" marker-end="url(#as)"></line>
|
||||||
|
<rect x="263" y="30" width="210" height="34" rx="6" fill="#fef3c7" stroke="#ca8a04"></rect><text x="368" y="52" text-anchor="middle" font-size="11" fill="#854d0e">표에서 시간 찾기 (192초)</text>
|
||||||
|
<line x1="473" y1="47" x2="521" y2="47" stroke="#7c3aed" stroke-width="2" marker-end="url(#as)"></line>
|
||||||
|
<rect x="526" y="30" width="164" height="34" rx="6" fill="#eff6ff" stroke="#2563eb"></rect><text x="608" y="52" text-anchor="middle" font-size="12" fill="#1e3a8a">그 시간으로 재생</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>여러 번 지난 측점이면 Enter 를 반복해 다음 통과 지점으로 순환 이동. 없는 측점이면 '측점 없음' 안내.</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><strong>실제 측점(드론이 진짜 지난 곳)</strong> 으로만 이동해요. 이 영상에 없는 측점이면 엉뚱한 데로 안 가고 <strong>"측점 없음"</strong> 을 알려줘요.</li>
|
||||||
|
<li>드론이 <strong>끝(종점)까지 못 간</strong> 경우, 종점 측점은 막대 맨 끝에 <strong>'미도착'</strong> 으로 표시해요.</li>
|
||||||
|
</ul>
|
||||||
|
<hr />
|
||||||
|
<h2 id="8-전체-흐름-한눈에-컨베이어-벨트처럼-">8. 전체 흐름 한눈에 (컨베이어 벨트처럼) 🏭</h2>
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 720 300" role="img" aria-label="전체 흐름">
|
||||||
|
<defs><marker id="a8" markerWidth="11" markerHeight="11" refX="7" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#7c3aed"></path></marker></defs>
|
||||||
|
<rect x="210" y="12" width="300" height="40" rx="8" fill="#f1f5f9" stroke="#475569"></rect>
|
||||||
|
<text x="360" y="30" text-anchor="middle" font-size="12" font-weight="bold" fill="#334155">드론 영상 + GPS 기록 + 측점 데이터</text>
|
||||||
|
<text x="360" y="45" text-anchor="middle" font-size="10" fill="#475569">(폴더 선택으로 불러옴)</text>
|
||||||
|
<line x1="360" y1="52" x2="360" y2="70" stroke="#7c3aed" stroke-width="2" marker-end="url(#a8)"></line>
|
||||||
|
<g font-size="11">
|
||||||
|
<rect x="150" y="72" width="420" height="30" rx="6" fill="#f0fdf4" stroke="#16a34a"></rect>
|
||||||
|
<text x="360" y="92" text-anchor="middle" fill="#166534">① 프레임마다: GPS → 측점(선로에 수직투영), 프레임번호 → 시간</text>
|
||||||
|
<line x1="360" y1="102" x2="360" y2="118" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#a8)"></line>
|
||||||
|
<rect x="150" y="120" width="420" height="30" rx="6" fill="#fef3c7" stroke="#ca8a04"></rect>
|
||||||
|
<text x="360" y="140" text-anchor="middle" fill="#854d0e">② 시간↔측점 대응표 + '이동거리 축' 만들기(멈추면 커서 정지)</text>
|
||||||
|
<line x1="360" y1="150" x2="360" y2="166" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#a8)"></line>
|
||||||
|
<rect x="150" y="168" width="420" height="30" rx="6" fill="#eff6ff" stroke="#2563eb"></rect>
|
||||||
|
<text x="360" y="188" text-anchor="middle" fill="#1e3a8a">③ 막대 그리기: 방향색(주황/파랑) + 구조물(교량·터널·역) 배치</text>
|
||||||
|
<line x1="360" y1="198" x2="360" y2="214" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#a8)"></line>
|
||||||
|
<rect x="150" y="216" width="420" height="30" rx="6" fill="#fdf2f8" stroke="#db2777"></rect>
|
||||||
|
<text x="360" y="236" text-anchor="middle" fill="#9d174d">④ 측점 입력 검색 → 표에서 시간 찾아 그 위치로 이동</text>
|
||||||
|
<line x1="360" y1="246" x2="360" y2="262" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#a8)"></line>
|
||||||
|
<rect x="210" y="264" width="300" height="28" rx="6" fill="#f5f3ff" stroke="#7c3aed"></rect>
|
||||||
|
<text x="360" y="283" text-anchor="middle" fill="#5b21b6" font-weight="bold">🎯 측점으로 보는 주행영상 플레이어 완성</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="header">
|
||||||
|
<th>단계</th>
|
||||||
|
<th>하는 일</th>
|
||||||
|
<th>파일·함수</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>① GPS→측점</td>
|
||||||
|
<td>드론 위치를 선로에 수직투영</td>
|
||||||
|
<td><a href="../client/src/utils/chainage.ts">chainage.ts</a> <code>projectToChain</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>① 프레임→시간</td>
|
||||||
|
<td>프레임번호 ÷ fps</td>
|
||||||
|
<td>StationBar <code>videoFps</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>② 시간↔︎측점 대응표</td>
|
||||||
|
<td>프레임별 사전계산</td>
|
||||||
|
<td>StationBar precompute</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>② 이동거리 축</td>
|
||||||
|
<td>측점 변화량 누적</td>
|
||||||
|
<td><code>cumFracAtTime</code> / <code>timeAtFrac</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>③ 방향색·구조물</td>
|
||||||
|
<td>측점 증감/측점값 기준</td>
|
||||||
|
<td><code>barSegments</code> / <code>structureMarks</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>④ 측점 검색</td>
|
||||||
|
<td>측점→시간 역변환</td>
|
||||||
|
<td><code>handleJumpToMileage</code></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<hr />
|
||||||
|
<h2 id="9-한-문장-결론-">9. 한 문장 결론 🎁</h2>
|
||||||
|
<blockquote>
|
||||||
|
<p><strong>"드론 GPS로 매 장면의 측점을 계산해 '시간↔︎측점 대응표'를 만들어 두었기 때문에, 시간이 아니라 측점(선로 위치)으로 영상을 찾아보고 이동할 수 있게 된 것"</strong> 이에요.</p>
|
||||||
|
</blockquote>
|
||||||
|
<p>이 덕분에 GhiVideo는 단순 영상이 아니라 <strong>"지금 선로 어디를 보고 있는지 아는" 주행영상 플레이어</strong>가 됩니다. 🚁📏</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
# 시간이 아니라 '측점'으로 움직이는 영상 플레이어 (그림 설명서)
|
||||||
|
|
||||||
|
> 이 글은 **초등학생도 이해할 수 있게** 그림과 이야기로 쓴 설명서예요.
|
||||||
|
> "보통 영상 플레이어는 **시간(프레임)** 으로 움직이는데, 우리는 어떻게 **측점(선로 위치)** 으로 움직이게 바꿨을까?"
|
||||||
|
> 그 비밀을 하나씩 풀어 줄게요.
|
||||||
|
> (진짜 코드는 [chainage.ts](../client/src/utils/chainage.ts) · [StationBar.tsx](../client/src/stationbar/StationBar.tsx) 참고)
|
||||||
|
|
||||||
|
<style>
|
||||||
|
figure.fig{margin:22px 0;text-align:center;page-break-inside:avoid;break-inside:avoid;}
|
||||||
|
figure.fig svg{width:100%;max-width:720px;height:auto;border:1px solid #e7e0d2;border-radius:10px;background:#fffdf8;}
|
||||||
|
figure.fig figcaption{font-size:0.9em;color:#777;margin-top:6px;}
|
||||||
|
text{font-family:'Malgun Gothic','Hancom Gothic',sans-serif;}
|
||||||
|
h2{border-top:3px solid #e7e0d2;padding-top:14px;margin-top:34px;}
|
||||||
|
code{background:#f5f3ee;padding:1px 4px;border-radius:4px;}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 한 문장으로 말하면
|
||||||
|
|
||||||
|
**"영상의 매 장면마다 드론 GPS로 '지금 몇 측점인지'를 계산해 두어서, 시간(몇 분 몇 초)이 아니라 측점(160k130 같은 선로 위치)으로 영상을 찾아보게 만든 기술"** 이에요.
|
||||||
|
|
||||||
|
> 🎯 비유: 보통 영화는 "32분 10초로 가줘"라고 하죠. 우리 플레이어는 **"회덕터널 앞으로 가줘"** 처럼 **장소로** 찾아가요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 보통 플레이어 vs 우리 플레이어
|
||||||
|
|
||||||
|
보통 영상 플레이어의 아래 막대는 **시간**(0:00 ~ 끝)이에요. 그런데 철도 점검하는 분들은 "몇 분"이 아니라 **"몇 측점"** 으로 이야기해요. 그래서 막대를 **측점 기준**으로 바꿨어요.
|
||||||
|
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 720 250" role="img" aria-label="시간축 재생바 vs 측점축 재생바 비교">
|
||||||
|
<text x="360" y="24" text-anchor="middle" font-size="13" font-weight="bold" fill="#475569">① 보통 플레이어 (시간 축)</text>
|
||||||
|
<rect x="40" y="38" width="640" height="26" rx="6" fill="#e5e7eb" stroke="#9ca3af"/>
|
||||||
|
<rect x="40" y="38" width="250" height="26" rx="6" fill="#9ca3af"/>
|
||||||
|
<circle cx="290" cy="51" r="10" fill="#374151"/>
|
||||||
|
<text x="45" y="82" font-size="11" fill="#374151">0:00</text>
|
||||||
|
<text x="300" y="82" font-size="11" fill="#374151">03:12</text>
|
||||||
|
<text x="648" y="82" font-size="11" fill="#374151">09:28</text>
|
||||||
|
<text x="360" y="104" text-anchor="middle" font-size="11" fill="#6b7280">"3분 12초 지점" — 여기가 어느 선로인지 모름 😵</text>
|
||||||
|
<text x="360" y="150" text-anchor="middle" font-size="13" font-weight="bold" fill="#166534">② 우리 플레이어 (측점 축)</text>
|
||||||
|
<rect x="40" y="164" width="640" height="26" rx="6" fill="#fde9c8" stroke="#e0a94f"/>
|
||||||
|
<rect x="40" y="164" width="310" height="26" rx="6" fill="#f59e0b"/>
|
||||||
|
<circle cx="350" cy="177" r="11" fill="#ea580c"/><text x="350" y="181" text-anchor="middle" font-size="9" fill="#fff" font-weight="bold">160k</text>
|
||||||
|
<line x1="205" y1="160" x2="205" y2="194" stroke="#0284c7" stroke-width="2"/><text x="205" y="210" text-anchor="middle" font-size="9" fill="#0369a1">회덕터널</text>
|
||||||
|
<line x1="480" y1="160" x2="480" y2="194" stroke="#0284c7" stroke-width="2"/><text x="480" y="210" text-anchor="middle" font-size="9" fill="#0369a1">회덕천교</text>
|
||||||
|
<text x="45" y="230" font-size="11" fill="#166534">대전조차장</text>
|
||||||
|
<text x="612" y="230" font-size="11" fill="#166534">신탄진</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>시간 축은 "몇 분"만 알려준다. 측점 축은 "지금 어느 선로 위치(측점)", "무슨 구조물 근처"인지 한눈에 보인다.</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. '측점'이 뭐예요? 📍
|
||||||
|
|
||||||
|
철도에서 **출발점부터 몇 미터 왔는지**를 나타내는 '거리 이정표'예요. 도로의 **km 표지판**과 똑같아요!
|
||||||
|
|
||||||
|
- `160k130` = 출발점에서 **160km + 130m** 지점
|
||||||
|
- 숫자가 커지면 앞으로 간 것, 작아지면 되돌아온 것
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 핵심 비밀 — 매 장면마다 '시간'과 '측점'을 짝지어 둔다 🗺️
|
||||||
|
|
||||||
|
비밀은 **드론 GPS**예요. 드론 영상의 **매 프레임(사진 한 장)** 에는 그때 드론의 **위도·경도(GPS)** 가 기록돼 있어요. 이걸 이용해 프레임마다 두 가지를 계산해 **짝지어** 둬요.
|
||||||
|
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 720 330" role="img" aria-label="사진 1장마다 시간과 측점을 계산해 표로 모으고, 표로 양방향 변환">
|
||||||
|
<defs><marker id="ad" markerWidth="10" markerHeight="10" refX="7" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#7c3aed"/></marker></defs>
|
||||||
|
<!-- ① 왼쪽: 사진 1장 → 두 값 계산 -->
|
||||||
|
<text x="178" y="22" text-anchor="middle" font-size="12" font-weight="bold" fill="#334155">① 사진 1장마다 두 값을 계산</text>
|
||||||
|
<rect x="88" y="34" width="180" height="58" rx="8" fill="#f1f5f9" stroke="#475569"/>
|
||||||
|
<text x="178" y="57" text-anchor="middle" font-size="11.5" font-weight="bold" fill="#334155">🎞 프레임 5760번</text>
|
||||||
|
<text x="178" y="78" text-anchor="middle" font-size="10" fill="#475569">📍 드론 GPS 함께 기록됨</text>
|
||||||
|
<line x1="133" y1="92" x2="106" y2="118" stroke="#7c3aed" stroke-width="2" marker-end="url(#ad)"/>
|
||||||
|
<line x1="223" y1="92" x2="250" y2="118" stroke="#7c3aed" stroke-width="2" marker-end="url(#ad)"/>
|
||||||
|
<rect x="20" y="122" width="160" height="58" rx="8" fill="#eff6ff" stroke="#2563eb"/>
|
||||||
|
<text x="100" y="144" text-anchor="middle" font-size="11.5" font-weight="bold" fill="#1e3a8a">⏱ 시간 = 192초</text>
|
||||||
|
<text x="100" y="166" text-anchor="middle" font-size="9.5" fill="#1e40af">번호 5760 ÷ 30fps</text>
|
||||||
|
<rect x="196" y="122" width="160" height="58" rx="8" fill="#f0fdf4" stroke="#16a34a"/>
|
||||||
|
<text x="276" y="144" text-anchor="middle" font-size="11.5" font-weight="bold" fill="#166534">📐 측점 = 160k130</text>
|
||||||
|
<text x="276" y="166" text-anchor="middle" font-size="9.5" fill="#15803d">GPS를 선로에 '툭' 투영</text>
|
||||||
|
<!-- 가운데 화살표: 반복 -->
|
||||||
|
<line x1="368" y1="106" x2="418" y2="106" stroke="#7c3aed" stroke-width="3" marker-end="url(#ad)"/>
|
||||||
|
<text x="393" y="92" text-anchor="middle" font-size="10" font-weight="bold" fill="#7c3aed">모든 사진에</text>
|
||||||
|
<text x="393" y="128" text-anchor="middle" font-size="10" font-weight="bold" fill="#7c3aed">반복</text>
|
||||||
|
<!-- ② 오른쪽: 시간↔측점 대응표 -->
|
||||||
|
<text x="560" y="22" text-anchor="middle" font-size="12" font-weight="bold" fill="#854d0e">② '시간↔측점 대응표' 완성</text>
|
||||||
|
<rect x="440" y="34" width="240" height="26" fill="#fef3c7" stroke="#ca8a04"/>
|
||||||
|
<line x1="560" y1="34" x2="560" y2="190" stroke="#ca8a04" stroke-width="1"/>
|
||||||
|
<text x="500" y="52" text-anchor="middle" font-size="11" font-weight="bold" fill="#1e3a8a">⏱ 시간</text>
|
||||||
|
<text x="620" y="52" text-anchor="middle" font-size="11" font-weight="bold" fill="#166534">📐 측점</text>
|
||||||
|
<rect x="440" y="60" width="240" height="26" fill="#ffffff" stroke="#ca8a04"/>
|
||||||
|
<text x="500" y="78" text-anchor="middle" font-size="10.5" fill="#334155">191초</text>
|
||||||
|
<text x="620" y="78" text-anchor="middle" font-size="10.5" fill="#334155">160k100</text>
|
||||||
|
<rect x="440" y="86" width="240" height="26" fill="#fef9e7" stroke="#ca8a04"/>
|
||||||
|
<text x="500" y="104" text-anchor="middle" font-size="10.5" font-weight="bold" fill="#1e3a8a">192초</text>
|
||||||
|
<text x="620" y="104" text-anchor="middle" font-size="10.5" font-weight="bold" fill="#166534">160k130</text>
|
||||||
|
<rect x="440" y="112" width="240" height="26" fill="#ffffff" stroke="#ca8a04"/>
|
||||||
|
<text x="500" y="130" text-anchor="middle" font-size="10.5" fill="#334155">193초</text>
|
||||||
|
<text x="620" y="130" text-anchor="middle" font-size="10.5" fill="#334155">160k160</text>
|
||||||
|
<rect x="440" y="138" width="240" height="26" fill="#ffffff" stroke="#ca8a04"/>
|
||||||
|
<text x="500" y="156" text-anchor="middle" font-size="10.5" fill="#94a3b8">⋮</text>
|
||||||
|
<text x="620" y="156" text-anchor="middle" font-size="10.5" fill="#94a3b8">⋮</text>
|
||||||
|
<rect x="440" y="164" width="240" height="26" fill="#ffffff" stroke="#ca8a04"/>
|
||||||
|
<text x="500" y="182" text-anchor="middle" font-size="10.5" fill="#334155">348초</text>
|
||||||
|
<text x="620" y="182" text-anchor="middle" font-size="10.5" fill="#334155">165k000</text>
|
||||||
|
<!-- ③ 아래: 표를 쓰는 두 방향 -->
|
||||||
|
<text x="360" y="232" text-anchor="middle" font-size="12" font-weight="bold" fill="#334155">③ 이 표 하나로, 어느 쪽으로든 찾기</text>
|
||||||
|
<rect x="110" y="246" width="500" height="30" rx="7" fill="#eff6ff" stroke="#2563eb"/>
|
||||||
|
<text x="360" y="266" text-anchor="middle" font-size="10.5" fill="#1e3a8a">재생 중: <tspan font-weight="bold">"지금 192초… 여긴 어디?"</tspan> → 표에서 찾아 → <tspan font-weight="bold" fill="#166534">측점 160k130 표시</tspan></text>
|
||||||
|
<rect x="110" y="284" width="500" height="30" rx="7" fill="#f0fdf4" stroke="#16a34a"/>
|
||||||
|
<text x="360" y="304" text-anchor="middle" font-size="10.5" fill="#166534">검색: <tspan font-weight="bold">"160k130 가줘!"</tspan> → 표에서 찾아 → <tspan font-weight="bold" fill="#1e3a8a">192초로 점프</tspan></text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>사진 1장마다 (시간, 측점) 짝을 계산해(①) 표로 모아 두면(②), "지금 어디?"와 "거기로 가줘!"를 모두 이 표 하나로 해결한다(③). (코드: StationBar precompute)</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
### GPS를 측점으로 바꾸는 법 — '선로에 수직으로 툭' 📐
|
||||||
|
|
||||||
|
드론은 선로 바로 위가 아니라 옆에서 비스듬히 날 때도 많아요. 그래서 드론 GPS 점을 **선로(측점들을 이은 선)에 수직으로 툭 떨어뜨려**, 그 발자국이 몇 측점인지 읽어요.
|
||||||
|
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 720 170" role="img" aria-label="드론 GPS를 선로에 수직 투영해 측점 읽기">
|
||||||
|
<line x1="60" y1="120" x2="660" y2="70" stroke="#8a7a5c" stroke-width="6"/>
|
||||||
|
<text x="70" y="145" font-size="11" fill="#6b4f2a">선로 (측점 158k·159k·160k…를 이은 선)</text>
|
||||||
|
<circle cx="360" cy="32" r="7" fill="#2563eb"/><text x="360" y="24" text-anchor="middle" font-size="10" fill="#1e3a8a">드론 GPS</text>
|
||||||
|
<line x1="360" y1="32" x2="374" y2="94" stroke="#ef4444" stroke-width="2" stroke-dasharray="4 3"/>
|
||||||
|
<circle cx="374" cy="94" r="5" fill="#ef4444"/>
|
||||||
|
<text x="410" y="98" font-size="11" fill="#b91c1c" font-weight="bold">수직으로 내린 발자국 = 160k130</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>드론이 선로 옆에 있어도, 수직으로 내린 발자국 위치로 '측점값'을 정확히 읽는다. (코드: chainage.ts projectToChain)</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 재생바의 가로축을 '시간'이 아니라 '실제 이동 거리'로 🚂
|
||||||
|
|
||||||
|
여기가 특히 똑똑한 부분이에요. 드론이 **멈춰서 맴돌면** 시간은 계속 흐르지만 **위치(측점)는 그대로**예요. 이때 막대 커서가 계속 가면 이상하죠.
|
||||||
|
|
||||||
|
그래서 가로축을 **"실제로 이동한 거리"**(측점이 변한 양을 쌓은 값)로 만들었어요. → **멈추면 커서도 멈추고, 움직일 때만 앞으로 가요.**
|
||||||
|
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 720 150" role="img" aria-label="이동거리 축 - 멈추면 커서 정지">
|
||||||
|
<text x="180" y="22" text-anchor="middle" font-size="12" font-weight="bold" fill="#475569">시간 축 (옛 방식)</text>
|
||||||
|
<rect x="40" y="34" width="280" height="20" rx="5" fill="#e5e7eb" stroke="#9ca3af"/><circle cx="240" cy="44" r="8" fill="#374151"/>
|
||||||
|
<text x="180" y="78" text-anchor="middle" font-size="10" fill="#b91c1c">드론이 멈춰 맴돌아도 커서가 계속 감 😵</text>
|
||||||
|
<text x="540" y="22" text-anchor="middle" font-size="12" font-weight="bold" fill="#166534">이동거리 축 (새 방식)</text>
|
||||||
|
<rect x="400" y="34" width="280" height="20" rx="5" fill="#fde9c8" stroke="#e0a94f"/><circle cx="560" cy="44" r="8" fill="#ea580c"/>
|
||||||
|
<text x="540" y="78" text-anchor="middle" font-size="10" fill="#166534">멈추면 커서도 멈춤, 움직일 때만 전진 😎</text>
|
||||||
|
<text x="360" y="120" text-anchor="middle" font-size="11" fill="#475569">→ 그래서 커서 위치만 봐도 "드론이 실제로 얼마나 이동했는지"를 알 수 있어요.</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>가로축 = 측점 변화량을 쌓은 '이동거리'. 전진이든 후진이든 움직이면 커서는 오른쪽으로 간다(방향은 색으로 구분). (코드: cumFracAtTime / timeAtFrac)</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 방향을 색으로 — 앞으로 가면 주황, 되돌아오면 파랑 🎨
|
||||||
|
|
||||||
|
드론이 앞으로 가서 측점이 커지면 **주황**, 되돌아와서 측점이 작아지면 **하늘색** 으로 칠해요. 그래서 막대만 봐도 **어디서 앞으로/뒤로 갔는지** 알 수 있어요.
|
||||||
|
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 720 90" role="img" aria-label="진행 방향 색 구분">
|
||||||
|
<rect x="40" y="30" width="230" height="24" rx="5" fill="#f59e0b"/><text x="155" y="47" text-anchor="middle" font-size="11" fill="#7a3d00" font-weight="bold">전진(측점 ↑) 주황</text>
|
||||||
|
<rect x="270" y="30" width="150" height="24" rx="5" fill="#3fb6d4"/><text x="345" y="47" text-anchor="middle" font-size="11" fill="#06455a" font-weight="bold">후진(측점 ↓) 파랑</text>
|
||||||
|
<rect x="420" y="30" width="260" height="24" rx="5" fill="#f59e0b"/><text x="550" y="47" text-anchor="middle" font-size="11" fill="#7a3d00" font-weight="bold">다시 전진 주황</text>
|
||||||
|
<text x="360" y="78" text-anchor="middle" font-size="10" fill="#475569">색이 바뀌는 지점 = 드론이 방향을 바꾼 곳</text>
|
||||||
|
</svg>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 구조물을 '측점 위치'에 딱 배치 🚉
|
||||||
|
|
||||||
|
교량·터널·역을 각자의 **측점값 위치**에 표시해요. 각 시설물의 실제 측점(예: 회덕터널 160k)과, 드론이 **그 측점을 실제로 지나는 시점**을 맞춰서 막대에 점으로 찍어요. 그래서 "이 구조물이 어디쯤인지"를 막대만 봐도 알 수 있어요.
|
||||||
|
|
||||||
|
> 드론이 같은 곳을 **여러 번 지나가면**(조차장에서 왔다갔다) 그 시설물 마커도 **지날 때마다** 찍혀요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 측점으로 '찾아가기' (검색) 🔎
|
||||||
|
|
||||||
|
측점값을 입력하면 → **그 측점이 나오는 시간을 표에서 찾아 → 그 시간으로 이동**해요. (3번에서 만든 '시간↔측점 대응표' 덕분!)
|
||||||
|
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 720 90" role="img" aria-label="측점 입력으로 이동">
|
||||||
|
<defs><marker id="as" markerWidth="10" markerHeight="10" refX="7" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#7c3aed"/></marker></defs>
|
||||||
|
<rect x="30" y="30" width="180" height="34" rx="6" fill="#f0fdf4" stroke="#16a34a"/><text x="120" y="52" text-anchor="middle" font-size="12" fill="#166534">"160k130" 입력</text>
|
||||||
|
<line x1="210" y1="47" x2="258" y2="47" stroke="#7c3aed" stroke-width="2" marker-end="url(#as)"/>
|
||||||
|
<rect x="263" y="30" width="210" height="34" rx="6" fill="#fef3c7" stroke="#ca8a04"/><text x="368" y="52" text-anchor="middle" font-size="11" fill="#854d0e">표에서 시간 찾기 (192초)</text>
|
||||||
|
<line x1="473" y1="47" x2="521" y2="47" stroke="#7c3aed" stroke-width="2" marker-end="url(#as)"/>
|
||||||
|
<rect x="526" y="30" width="164" height="34" rx="6" fill="#eff6ff" stroke="#2563eb"/><text x="608" y="52" text-anchor="middle" font-size="12" fill="#1e3a8a">그 시간으로 재생</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>여러 번 지난 측점이면 Enter 를 반복해 다음 통과 지점으로 순환 이동. 없는 측점이면 '측점 없음' 안내.</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
- **실제 측점(드론이 진짜 지난 곳)** 으로만 이동해요. 이 영상에 없는 측점이면 엉뚱한 데로 안 가고 **"측점 없음"** 을 알려줘요.
|
||||||
|
- 드론이 **끝(종점)까지 못 간** 경우, 종점 측점은 막대 맨 끝에 **'미도착'** 으로 표시해요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 전체 흐름 한눈에 (컨베이어 벨트처럼) 🏭
|
||||||
|
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 720 300" role="img" aria-label="전체 흐름">
|
||||||
|
<defs><marker id="a8" markerWidth="11" markerHeight="11" refX="7" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#7c3aed"/></marker></defs>
|
||||||
|
<rect x="210" y="12" width="300" height="40" rx="8" fill="#f1f5f9" stroke="#475569"/>
|
||||||
|
<text x="360" y="30" text-anchor="middle" font-size="12" font-weight="bold" fill="#334155">드론 영상 + GPS 기록 + 측점 데이터</text>
|
||||||
|
<text x="360" y="45" text-anchor="middle" font-size="10" fill="#475569">(폴더 선택으로 불러옴)</text>
|
||||||
|
<line x1="360" y1="52" x2="360" y2="70" stroke="#7c3aed" stroke-width="2" marker-end="url(#a8)"/>
|
||||||
|
<g font-size="11">
|
||||||
|
<rect x="150" y="72" width="420" height="30" rx="6" fill="#f0fdf4" stroke="#16a34a"/>
|
||||||
|
<text x="360" y="92" text-anchor="middle" fill="#166534">① 프레임마다: GPS → 측점(선로에 수직투영), 프레임번호 → 시간</text>
|
||||||
|
<line x1="360" y1="102" x2="360" y2="118" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#a8)"/>
|
||||||
|
<rect x="150" y="120" width="420" height="30" rx="6" fill="#fef3c7" stroke="#ca8a04"/>
|
||||||
|
<text x="360" y="140" text-anchor="middle" fill="#854d0e">② 시간↔측점 대응표 + '이동거리 축' 만들기(멈추면 커서 정지)</text>
|
||||||
|
<line x1="360" y1="150" x2="360" y2="166" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#a8)"/>
|
||||||
|
<rect x="150" y="168" width="420" height="30" rx="6" fill="#eff6ff" stroke="#2563eb"/>
|
||||||
|
<text x="360" y="188" text-anchor="middle" fill="#1e3a8a">③ 막대 그리기: 방향색(주황/파랑) + 구조물(교량·터널·역) 배치</text>
|
||||||
|
<line x1="360" y1="198" x2="360" y2="214" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#a8)"/>
|
||||||
|
<rect x="150" y="216" width="420" height="30" rx="6" fill="#fdf2f8" stroke="#db2777"/>
|
||||||
|
<text x="360" y="236" text-anchor="middle" fill="#9d174d">④ 측점 입력 검색 → 표에서 시간 찾아 그 위치로 이동</text>
|
||||||
|
<line x1="360" y1="246" x2="360" y2="262" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#a8)"/>
|
||||||
|
<rect x="210" y="264" width="300" height="28" rx="6" fill="#f5f3ff" stroke="#7c3aed"/>
|
||||||
|
<text x="360" y="283" text-anchor="middle" fill="#5b21b6" font-weight="bold">🎯 측점으로 보는 주행영상 플레이어 완성</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
| 단계 | 하는 일 | 파일·함수 |
|
||||||
|
|---|---|---|
|
||||||
|
| ① GPS→측점 | 드론 위치를 선로에 수직투영 | [chainage.ts](../client/src/utils/chainage.ts) `projectToChain` |
|
||||||
|
| ① 프레임→시간 | 프레임번호 ÷ fps | StationBar `videoFps` |
|
||||||
|
| ② 시간↔측점 대응표 | 프레임별 사전계산 | StationBar precompute |
|
||||||
|
| ② 이동거리 축 | 측점 변화량 누적 | `cumFracAtTime` / `timeAtFrac` |
|
||||||
|
| ③ 방향색·구조물 | 측점 증감/측점값 기준 | `barSegments` / `structureMarks` |
|
||||||
|
| ④ 측점 검색 | 측점→시간 역변환 | `handleJumpToMileage` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 한 문장 결론 🎁
|
||||||
|
|
||||||
|
> **"드론 GPS로 매 장면의 측점을 계산해 '시간↔측점 대응표'를 만들어 두었기 때문에, 시간이 아니라 측점(선로 위치)으로 영상을 찾아보고 이동할 수 있게 된 것"** 이에요.
|
||||||
|
|
||||||
|
이 덕분에 GhiVideo는 단순 영상이 아니라 **"지금 선로 어디를 보고 있는지 아는" 주행영상 플레이어**가 됩니다. 🚁📏
|
||||||
Binary file not shown.
@@ -0,0 +1,461 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang xml:lang>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="pandoc" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
|
||||||
|
<title>특허_GhiVideo_명세서초안_좌표투영_측점기반재생</title>
|
||||||
|
<style>
|
||||||
|
code{white-space: pre-wrap;}
|
||||||
|
span.smallcaps{font-variant: small-caps;}
|
||||||
|
span.underline{text-decoration: underline;}
|
||||||
|
div.column{display: inline-block; vertical-align: top; width: 50%;}
|
||||||
|
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
|
||||||
|
ul.task-list{list-style: none;}
|
||||||
|
.display.math{display: block; text-align: center; margin: 0.5rem auto;}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">@page {
|
||||||
|
size: A4;
|
||||||
|
margin: 18mm 16mm 16mm 16mm;
|
||||||
|
@bottom-center {
|
||||||
|
content: counter(page) " / " counter(pages);
|
||||||
|
font-family: "Malgun Gothic", sans-serif;
|
||||||
|
font-size: 9pt;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html { font-size: 11pt; }
|
||||||
|
body {
|
||||||
|
font-family: "Malgun Gothic", "Hancom Gothic", sans-serif;
|
||||||
|
color: #23272e;
|
||||||
|
line-height: 1.65;
|
||||||
|
max-width: 920px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.7rem;
|
||||||
|
color: #b45309;
|
||||||
|
border-bottom: 3px solid #f59e0b;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #b45309;
|
||||||
|
border-bottom: 1px solid #e5d3b3;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
margin-top: 1.6em;
|
||||||
|
}
|
||||||
|
h3 { font-size: 1.05rem; color: #92400e; margin-top: 1.1em; }
|
||||||
|
a { color: #b45309; }
|
||||||
|
hr { border: none; border-top: 1px solid #e2e2e2; margin: 1.6em 0; }
|
||||||
|
ul { padding-left: 1.25em; }
|
||||||
|
li { margin: 0.18em 0; }
|
||||||
|
strong { color: #1f2937; }
|
||||||
|
code {
|
||||||
|
font-family: "D2Coding", Consolas, monospace;
|
||||||
|
background: #f4f1ea;
|
||||||
|
border: 1px solid #e7e0d2;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.5px 5px;
|
||||||
|
font-size: 0.92em;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0.8em 0;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
th, td { border: 1px solid #d8d2c4; padding: 6px 10px; text-align: left; vertical-align: top; }
|
||||||
|
th { background: #fdf3df; color: #7c2d12; }
|
||||||
|
blockquote {
|
||||||
|
border-left: 4px solid #f59e0b;
|
||||||
|
margin: 0.8em 0;
|
||||||
|
padding: 0.2em 0 0.2em 14px;
|
||||||
|
color: #555;
|
||||||
|
background: #fffbf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3 { break-after: avoid; }
|
||||||
|
</style>
|
||||||
|
<!--[if lt IE 9]>
|
||||||
|
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
|
||||||
|
<![endif]-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header id="title-block-header">
|
||||||
|
<h1 class="title">특허_GhiVideo_명세서초안_좌표투영_측점기반재생</h1>
|
||||||
|
</header>
|
||||||
|
<h1 id="특허-명세서-초안--ghivideo-2개-발명">특허 명세서 초안 — GhiVideo 2개 발명</h1>
|
||||||
|
<blockquote>
|
||||||
|
<p><strong>성격</strong>: 아래는 특허 출원을 위한 <strong>기술 정리 초안</strong>입니다. 실제 출원 전 <strong>변리사(특허사무소) 검토</strong>가 반드시 필요합니다. 청구항은 권리범위를 정하는 핵심이므로, 아래 청구항은 <strong>초안(방향 제시용)</strong> 이며 변리사와 함께 확정하십시오. <strong>선행기술조사(공지기술 확인)</strong> 후 신규성·진보성 관점에서 청구항을 조정해야 합니다.</p>
|
||||||
|
<p><strong>대상 발명</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>제1발명</strong>: 드론 항공영상에 지리정보를 실시간 정합·표시하는 방법 및 장치 (좌표투영)</li>
|
||||||
|
<li><strong>제2발명</strong>: 측점(체이니지) 기반 주행영상 탐색·재생 방법 및 장치 (측점기반재생)</li>
|
||||||
|
</ul>
|
||||||
|
</blockquote>
|
||||||
|
<style>
|
||||||
|
figure.fig{margin:20px 0;text-align:center;page-break-inside:avoid;break-inside:avoid;}
|
||||||
|
figure.fig svg{width:100%;max-width:700px;height:auto;border:1px solid #e7e0d2;border-radius:8px;background:#fffdf8;}
|
||||||
|
figure.fig figcaption{font-size:0.9em;color:#777;margin-top:6px;}
|
||||||
|
text{font-family:'Malgun Gothic','Hancom Gothic',sans-serif;}
|
||||||
|
code{background:#f5f3ee;padding:1px 4px;border-radius:4px;}
|
||||||
|
h2{border-top:3px solid #e7e0d2;padding-top:12px;margin-top:30px;}
|
||||||
|
h3{margin-top:22px;}
|
||||||
|
.claim{background:#fbfaf6;border:1px solid #e7e0d2;border-radius:6px;padding:10px 14px;margin:8px 0;}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<h2 id="0-공통-개요-및-출원-전략">0. 공통 개요 및 출원 전략</h2>
|
||||||
|
<h3 id="01-두-발명의-관계">0.1 두 발명의 관계</h3>
|
||||||
|
<p>두 발명은 모두 <strong>드론으로 촬영한 선로 주행영상</strong>과 <strong>드론의 위치·자세 로그(GPS/IMU)</strong>, <strong>선로 측점·시설물 지리데이터</strong>를 입력으로 사용하나, 해결 과제와 수단이 서로 독립적이다.</p>
|
||||||
|
<ul>
|
||||||
|
<li>제1발명: "촬영 시점의 카메라 기하를 복원하여 지리정보를 <strong>영상 화면의 정확한 픽셀 위치</strong>에 정합·표시"</li>
|
||||||
|
<li>제2발명: "드론 위치를 선로 측점으로 환산하여 영상을 <strong>시간이 아닌 측점(위치)으로 탐색·재생</strong>"</li>
|
||||||
|
</ul>
|
||||||
|
<h3 id="02-출원-형태변리사와-협의">0.2 출원 형태(변리사와 협의)</h3>
|
||||||
|
<ul>
|
||||||
|
<li>(안 A) <strong>2건 개별 출원</strong>: 각 발명을 독립 출원. 권리 명확.</li>
|
||||||
|
<li>(안 B) <strong>1건에 복수 청구항 세트</strong>: 단일성(발명의 단일성) 인정 여부를 변리사가 검토. 두 발명이 "드론 주행영상 처리"라는 공통 기술사상으로 묶일 수 있으나, 단일성 부정 시 분할출원 필요.</li>
|
||||||
|
<li>소프트웨어 발명이므로 <strong>"방법 청구항 + 장치(시스템) 청구항 + 컴퓨터 판독가능 기록매체 청구항"</strong> 3종 세트로 구성 권장(권리 확보 폭 확대).</li>
|
||||||
|
</ul>
|
||||||
|
<h3 id="03-명세서-작성-체크리스트">0.3 명세서 작성 체크리스트</h3>
|
||||||
|
<ul class="task-list">
|
||||||
|
<li><input type="checkbox" disabled></input>
|
||||||
|
선행기술조사(KIPRIS 등) — 항공영상 AR 오버레이, 지오레퍼런싱, 체이니지 기반 검색 관련</li>
|
||||||
|
<li><input type="checkbox" disabled></input>
|
||||||
|
각 청구항 구성요소가 실시예로 뒷받침되는지(기재불비 방지)</li>
|
||||||
|
<li><input type="checkbox" disabled></input>
|
||||||
|
도면 부호 일관성</li>
|
||||||
|
<li><input type="checkbox" disabled></input>
|
||||||
|
효과가 과제와 대응되는지</li>
|
||||||
|
</ul>
|
||||||
|
<hr />
|
||||||
|
<h1 id="제1발명--드론-항공영상에-지리정보를-실시간-정합표시하는-방법-및-장치">제1발명 — 드론 항공영상에 지리정보를 실시간 정합·표시하는 방법 및 장치</h1>
|
||||||
|
<h2 id="11-발명의-명칭">1.1 발명의 명칭</h2>
|
||||||
|
<p>드론 항공영상에 대한 지리정보 실시간 정합 및 오버레이 표시 방법, 장치 및 기록매체 (영문 예: Method and apparatus for real-time geo-registration and overlay of geographic information onto UAV aerial video)</p>
|
||||||
|
<h2 id="12-기술분야">1.2 기술분야</h2>
|
||||||
|
<p>본 발명은 무인비행체(드론)로 촬영한 항공영상 위에, 지도상의 지리정보 객체(측점, 교량, 터널, 역사 등)를 촬영 당시의 카메라 기하에 맞추어 <strong>영상 화면의 정확한 위치에 실시간으로 겹쳐 표시</strong>하는 영상처리 기술에 관한 것이다.</p>
|
||||||
|
<h2 id="13-배경기술">1.3 배경기술</h2>
|
||||||
|
<ul>
|
||||||
|
<li>종래 드론 점검영상은 단순 재생만 가능하여, 영상 속 특정 지점이 <strong>지도상 어느 시설물인지</strong> 육안으로 대조해야 했다.</li>
|
||||||
|
<li>일반적인 증강현실(AR) 오버레이는 정밀 IMU·짐벌 캘리브레이션이 필요하거나, 실시간 카메라 트래킹(SLAM 등) 부하가 크다.</li>
|
||||||
|
<li>지오레퍼런싱(정사영상 생성)은 후처리·연산량이 크고 실시간 재생 중 라벨을 프레임 정합해 표시하기 어렵다.</li>
|
||||||
|
</ul>
|
||||||
|
<h2 id="14-해결하고자-하는-과제">1.4 해결하고자 하는 과제</h2>
|
||||||
|
<ol type="1">
|
||||||
|
<li>별도 마커/실시간 SLAM 없이, <strong>드론의 위치·자세 로그와 카메라 내부파라미터만으로</strong> 지리정보를 영상에 정합.</li>
|
||||||
|
<li>영상 프레임율과 무관하게 <strong>화면 갱신 주기(예: 60fps)로 매끄럽게</strong> 라벨이 대상에 붙어 이동.</li>
|
||||||
|
<li>드론 GPS/자세의 노이즈·떨림에도 <strong>라벨이 안정적으로</strong> 표시.</li>
|
||||||
|
<li>영상별 프레임율(fps)이 상이해도 <strong>자동 정합</strong>.</li>
|
||||||
|
</ol>
|
||||||
|
<h2 id="15-과제의-해결-수단-요지">1.5 과제의 해결 수단 (요지)</h2>
|
||||||
|
<p>프레임별 드론 자세로부터 회전행렬을 구성하고, 지리정보 객체의 위경도를 평면직교좌표(TM)로 변환한 뒤 드론 기준 상대좌표(ENU)로 산출하고, 회전행렬로 카메라좌표계로 변환한 다음 <strong>핀홀 카메라 모델</strong>로 정규화 화면좌표에 투영하여 오버레이한다. 프레임 사이는 <strong>자세 보간</strong>으로 채우고, 화면좌표는 <strong>속도적응 평활(이동평균+지수이동평균)</strong> 로 안정화하며, 영상 fps 는 <strong>데이터로부터 자동 산출</strong>한다.</p>
|
||||||
|
<h2 id="16-도면의-간단한-설명">1.6 도면의 간단한 설명</h2>
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 700 150" role="img" aria-label="도1 제1발명 처리 파이프라인">
|
||||||
|
<defs><marker id="p1" markerWidth="10" markerHeight="10" refX="7" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#475569"></path></marker></defs>
|
||||||
|
<rect x="10" y="55" width="120" height="44" rx="6" fill="#ecfdf5" stroke="#16a34a"></rect><text x="70" y="74" text-anchor="middle" font-size="10" fill="#166534">위경도→미터</text><text x="70" y="90" text-anchor="middle" font-size="9" fill="#166534">(TM 변환)</text>
|
||||||
|
<line x1="130" y1="77" x2="150" y2="77" stroke="#475569" stroke-width="1.5" marker-end="url(#p1)"></line>
|
||||||
|
<rect x="152" y="55" width="120" height="44" rx="6" fill="#fefce8" stroke="#ca8a04"></rect><text x="212" y="74" text-anchor="middle" font-size="10" fill="#854d0e">드론기준 상대좌표</text><text x="212" y="90" text-anchor="middle" font-size="9" fill="#854d0e">(ENU)</text>
|
||||||
|
<line x1="272" y1="77" x2="292" y2="77" stroke="#475569" stroke-width="1.5" marker-end="url(#p1)"></line>
|
||||||
|
<rect x="294" y="55" width="120" height="44" rx="6" fill="#eff6ff" stroke="#2563eb"></rect><text x="354" y="74" text-anchor="middle" font-size="10" fill="#1e3a8a">회전행렬 변환</text><text x="354" y="90" text-anchor="middle" font-size="9" fill="#1e3a8a">(카메라좌표)</text>
|
||||||
|
<line x1="414" y1="77" x2="434" y2="77" stroke="#475569" stroke-width="1.5" marker-end="url(#p1)"></line>
|
||||||
|
<rect x="436" y="55" width="120" height="44" rx="6" fill="#fdf2f8" stroke="#db2777"></rect><text x="496" y="74" text-anchor="middle" font-size="10" fill="#9d174d">핀홀 투영</text><text x="496" y="90" text-anchor="middle" font-size="9" fill="#9d174d">(화면좌표 u,v)</text>
|
||||||
|
<line x1="556" y1="77" x2="576" y2="77" stroke="#475569" stroke-width="1.5" marker-end="url(#p1)"></line>
|
||||||
|
<rect x="578" y="55" width="112" height="44" rx="6" fill="#f5f3ff" stroke="#7c3aed"></rect><text x="634" y="74" text-anchor="middle" font-size="10" fill="#5b21b6">평활·오버레이</text><text x="634" y="90" text-anchor="middle" font-size="9" fill="#5b21b6">(라벨 표시)</text>
|
||||||
|
<text x="350" y="20" text-anchor="middle" font-size="11" font-weight="bold" fill="#334155">[도 1] 제1발명 전체 처리 파이프라인</text>
|
||||||
|
</svg>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 700 200" role="img" aria-label="도2 핀홀 투영 원리">
|
||||||
|
<line x1="90" y1="40" x2="90" y2="160" stroke="#16a34a" stroke-width="6"></line><text x="90" y="180" text-anchor="middle" font-size="10" fill="#166534">대상(3D)</text>
|
||||||
|
<line x1="360" y1="25" x2="360" y2="175" stroke="#94a3b8" stroke-width="5"></line><circle cx="360" cy="100" r="5" fill="#111"></circle><text x="360" y="193" text-anchor="middle" font-size="10" fill="#475569">투영중심(렌즈)</text>
|
||||||
|
<line x1="90" y1="40" x2="590" y2="150" stroke="#f59e0b" stroke-width="1.3"></line>
|
||||||
|
<line x1="90" y1="160" x2="590" y2="50" stroke="#f59e0b" stroke-width="1.3"></line>
|
||||||
|
<line x1="590" y1="50" x2="590" y2="150" stroke="#ef4444" stroke-width="5"></line><text x="590" y="170" text-anchor="middle" font-size="10" fill="#b91c1c">상(화면, u·v)</text>
|
||||||
|
<text x="350" y="16" text-anchor="middle" font-size="11" font-weight="bold" fill="#334155">[도 2] 핀홀 카메라 투영 (원근: 멀수록 작게)</text>
|
||||||
|
</svg>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><strong>[도 1]</strong> 제1발명의 전체 처리 파이프라인(좌표변환→상대좌표→회전→투영→표시).</li>
|
||||||
|
<li><strong>[도 2]</strong> 핀홀 카메라 모델에 의한 3D→2D 투영 개념.</li>
|
||||||
|
<li>(추가 도면 예시: [도 3] 자세각 yaw/pitch/roll 정의, [도 4] 프레임간 보간 및 평활 개념 — 변리사와 함께 정식 도면화.)</li>
|
||||||
|
</ul>
|
||||||
|
<h2 id="17-발명을-실시하기-위한-구체적인-내용-실시예">1.7 발명을 실시하기 위한 구체적인 내용 (실시예)</h2>
|
||||||
|
<h3 id="1-입력-데이터">(1) 입력 데이터</h3>
|
||||||
|
<ul>
|
||||||
|
<li>프레임별 드론 상태: 위도 φ, 경도 λ, 고도 h(타원체고 또는 절대고도), 자세 yaw(기수방위), pitch(틸트), roll.</li>
|
||||||
|
<li>카메라 내부파라미터: 초점거리 f(35mm 환산), 센서 폭 sW, 센서 높이 sH, 주점 보정 (cx0, cy0).</li>
|
||||||
|
<li>지리정보 객체 O: 위경도 (φo, λo), 표고 zo(또는 추정), 식별정보(명칭/속성).</li>
|
||||||
|
</ul>
|
||||||
|
<h3 id="2-좌표계-변환--위경도--평면직교tm--상대좌표enu">(2) 좌표계 변환 — 위경도 → 평면직교(TM) → 상대좌표(ENU)</h3>
|
||||||
|
<p>지구 곡률로 인해 각도(위경도)로는 거리 계산이 부정확하므로, 대상과 드론의 위경도를 <strong>평면직교좌표계(예: EPSG:5186 한국 TM, 횡축 메르카토르)</strong> 로 변환하여 미터 단위 좌표 (E, N) 를 얻는다. 드론을 기준으로 한 상대좌표(동-북-상, ENU)는</p>
|
||||||
|
<pre><code>rel = (E_o − E_d − offX, N_o − N_d − offY, z_o − h_d − offZ)</code></pre>
|
||||||
|
<p>로 산출한다(offX/offY/offZ 는 드론 위치 미세보정 파라미터). 대상 표고는 정표고(EL)에 지오이드고를 가산하여 드론 고도 기준(타원체고)과 datum 을 일치시킨다.</p>
|
||||||
|
<h3 id="3-자세--회전행렬">(3) 자세 → 회전행렬</h3>
|
||||||
|
<p>드론 자세(yaw, pitch, roll)로부터 회전행렬을 다음과 같이 구성한다.</p>
|
||||||
|
<pre><code>R_b2w = Rz(−yaw) · Rx(pitch) · Ry(roll)
|
||||||
|
R_align = [[1,0,0],[0,0,−1],[0,1,0]] (기체좌표계 → 카메라좌표계 축 정렬)
|
||||||
|
R_w2c = R_align · R_b2wᵀ</code></pre>
|
||||||
|
<p>카메라 좌표는</p>
|
||||||
|
<pre><code>(Xc, Yc, Zc)ᵀ = R_w2c · rel</code></pre>
|
||||||
|
<p>로 얻는다(Zc>0 이 카메라 전방).</p>
|
||||||
|
<h3 id="4-핀홀-카메라-투영--정규화-화면좌표">(4) 핀홀 카메라 투영 → 정규화 화면좌표</h3>
|
||||||
|
<pre><code>u = 0.5 + cx0 + (Xc / Zc) · (f / sW) (0=좌, 1=우)
|
||||||
|
v = 0.5 + cy0 + (Yc / Zc) · (f / sH) (0=상, 1=하)</code></pre>
|
||||||
|
<p>멀수록(Zc↑) 화면 중심에 가깝고 작게 투영되는 원근이 자연히 반영된다. (u, v) 가 [0,1] 범위(또는 표시허용 범위) 내이면 해당 정규화 좌표에 식별정보를 오버레이한다.</p>
|
||||||
|
<h3 id="5-프레임간-보간-매끄러운-추종">(5) 프레임간 보간 (매끄러운 추종)</h3>
|
||||||
|
<p>영상 프레임율보다 화면 갱신주기가 클 수 있으므로(예: 30fps 영상, 60Hz 화면), 재생 시각에 대응하는 <strong>연속 프레임 인덱스</strong>에서 인접 프레임의 위치·자세를 선형보간(방위는 최단각 보간)하여 중간 포즈를 산출하고, 상기 (2)~(4)를 재수행한다. 이로써 라벨이 매 화면 갱신마다 대상에 붙어 매끄럽게 이동한다.</p>
|
||||||
|
<h3 id="6-안정화--속도적응-평활-및-이상치-거부">(6) 안정화 — 속도적응 평활 및 이상치 거부</h3>
|
||||||
|
<p>GPS/자세 노이즈로 인한 라벨 떨림을 억제하기 위해,</p>
|
||||||
|
<ul>
|
||||||
|
<li>자세는 인접 프레임 <strong>이동평균</strong>(회전 경계 보존형 적응 창)으로 평활하고,</li>
|
||||||
|
<li>화면좌표는 <strong>속도적응 지수이동평균(EMA)</strong> 으로 평활한다. 즉 이동속도가 작을수록(정지·떨림) 강하게 평활하고, 클수록(실제 이동) 즉시 추종하도록 평활계수 α 를 이동속도에 따라 가변한다.</li>
|
||||||
|
<li>1프레임에 임계 이상 급이동하는 좌표는 이상치로 보아 일정 횟수 무시(이전 위치 유지)한다.</li>
|
||||||
|
</ul>
|
||||||
|
<h3 id="7-영상-fps-자동-산출">(7) 영상 fps 자동 산출</h3>
|
||||||
|
<p>프레임 번호↔︎시간 변환을 위한 fps 를, 드론 로그의 <strong>최대 프레임 번호 ÷ 영상 길이(초)</strong> 로 산출하고 표준 fps(23.976/24/25/29.97/30/50/60 등) 중 근접값에 스냅한다. 이로써 영상별 상이한 fps 에 자동 대응한다.</p>
|
||||||
|
<h3 id="8-렌더링-및-대상-표고-추정선택적-실시예">(8) 렌더링 및 대상 표고 추정(선택적 실시예)</h3>
|
||||||
|
<ul>
|
||||||
|
<li>표시는 화면 갱신주기(예: 60fps)로 캔버스에 수행하고, 가시성/겹침 판정 등 고부하 연산은 유휴 시간에 사전계산하여 분리한다.</li>
|
||||||
|
<li>대상 표고가 미상일 때, (i) 최근접 선로 중심선의 표고, (ii) 외부 수치표고모델(DEM), (iii) 드론 고도로부터의 일정 이격거리 중 하나로 추정할 수 있다.</li>
|
||||||
|
</ul>
|
||||||
|
<h2 id="18-발명의-효과">1.8 발명의 효과</h2>
|
||||||
|
<ul>
|
||||||
|
<li>마커/실시간 SLAM 없이 로그+내부파라미터만으로 정합 → 저부하·오프라인 처리 가능.</li>
|
||||||
|
<li>프레임율 무관 매끄러운 라벨 추종, 노이즈에 강인, fps 자동 대응.</li>
|
||||||
|
<li>점검·분석 시 영상 속 지점의 지리적 식별이 즉시 가능.</li>
|
||||||
|
</ul>
|
||||||
|
<h2 id="19-청구항-초안">1.9 청구항 (초안)</h2>
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
<p><strong>[청구항 1] (방법)</strong> 무인비행체로 촬영한 항공영상에 지리정보를 표시하는 방법으로서, (a) 상기 항공영상의 프레임에 대응하는 무인비행체의 위치정보(위도·경도·고도) 및 자세정보(방위각·틸트각·롤각)를 획득하는 단계; (b) 지리정보 객체의 위경도 좌표를 평면직교좌표계로 변환하고, 상기 무인비행체의 위치를 기준으로 한 상대좌표를 산출하는 단계; (c) 상기 자세정보로부터 회전행렬을 구성하여 상기 상대좌표를 카메라 좌표계의 좌표로 변환하는 단계; (d) 상기 카메라 좌표계의 좌표를, 초점거리 및 센서규격을 포함하는 카메라 파라미터에 기초한 핀홀 카메라 모델로 정규화 화면좌표에 투영하는 단계; 및 (e) 상기 정규화 화면좌표에 대응하는 상기 영상의 화면 위치에 상기 지리정보 객체의 식별정보를 오버레이하여 표시하는 단계; 를 포함하는, 항공영상 지리정보 표시 방법.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
<p><strong>[청구항 2]</strong> 제1항에 있어서, 상기 (b) 단계는 대상의 정표고에 지오이드고를 가산하여 상기 무인비행체의 고도 기준과 datum 을 일치시켜 상대좌표의 상하 성분을 산출하는 것을 특징으로 하는 방법.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
<p><strong>[청구항 3]</strong> 제1항에 있어서, 재생 시각에 대응하는 연속 프레임 인덱스에서 인접 프레임의 위치 및 자세를 보간하여 중간 포즈를 산출하고, 상기 중간 포즈를 이용해 화면 갱신주기마다 상기 (b)~(e)를 수행하여, 영상 프레임율보다 높은 빈도로 상기 식별정보의 표시 위치를 갱신하는 것을 특징으로 하는 방법.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
<p><strong>[청구항 4]</strong> 제1항에 있어서, 상기 정규화 화면좌표를, 이동속도가 작을수록 강하게 평활하고 클수록 약하게 평활하는 속도적응 평활을 적용하여 안정화하는 단계를 더 포함하는 방법.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
<p><strong>[청구항 5]</strong> 제1항에 있어서, 상기 무인비행체 로그의 최대 프레임 번호를 상기 영상의 길이로 나눈 값에 기초하여 프레임율을 산출하고, 산출된 값을 사전 정의된 표준 프레임율 중 근접값으로 결정하여 프레임 번호와 시간의 변환에 사용하는 것을 특징으로 하는 방법.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
<p><strong>[청구항 6] (장치)</strong> 제1항 내지 제5항 중 어느 한 항의 방법을 수행하도록 구성된 하나 이상의 프로세서 및 메모리를 포함하는, 항공영상 지리정보 표시 장치.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
<p><strong>[청구항 7] (기록매체)</strong> 제1항 내지 제5항 중 어느 한 항의 방법을 컴퓨터에 실행시키기 위한 프로그램이 기록된, 컴퓨터로 판독 가능한 기록매체.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="110-요약서초안">1.10 요약서(초안)</h2>
|
||||||
|
<p>무인비행체 항공영상의 각 프레임에 대응하는 위치·자세와 카메라 파라미터를 이용하여, 지리정보 객체의 위경도를 평면직교좌표 및 드론 기준 상대좌표로 변환하고 회전행렬과 핀홀 모델로 정규화 화면좌표에 투영하여 영상에 식별정보를 정합·오버레이한다. 프레임간 보간과 속도적응 평활, 프레임율 자동산출을 통해 별도 마커 없이 프레임율과 무관하게 안정적으로 라벨을 표시한다.</p>
|
||||||
|
<hr />
|
||||||
|
<h1 id="제2발명--측점체이니지-기반-주행영상-탐색재생-방법-및-장치">제2발명 — 측점(체이니지) 기반 주행영상 탐색·재생 방법 및 장치</h1>
|
||||||
|
<h2 id="21-발명의-명칭">2.1 발명의 명칭</h2>
|
||||||
|
<p>선로 측점 기반의 주행영상 탐색 및 재생 방법, 장치 및 기록매체 (영문 예: Chainage(station)-based navigation and playback method and apparatus for route inspection video)</p>
|
||||||
|
<h2 id="22-기술분야">2.2 기술분야</h2>
|
||||||
|
<p>본 발명은 선로 등 선형 구조물을 촬영한 주행영상을, 재생 시간이 아닌 <strong>선로상의 위치값(측점, 체이니지)</strong> 을 기준으로 탐색·표시·재생하는 영상 재생 인터페이스 기술에 관한 것이다.</p>
|
||||||
|
<h2 id="23-배경기술">2.3 배경기술</h2>
|
||||||
|
<ul>
|
||||||
|
<li>통상의 영상 플레이어는 <strong>시간축</strong>(경과 시간)으로만 탐색되어, 특정 시간이 선로상 어느 지점인지 알기 어렵다.</li>
|
||||||
|
<li>철도 점검 실무는 "몇 분"이 아니라 <strong>"몇 측점"</strong> 으로 위치를 지칭한다.</li>
|
||||||
|
<li>드론이 동일 구간을 왕복하거나 정지·선회하는 경우, 시간축 탐색바는 실제 이동과 불일치한다.</li>
|
||||||
|
</ul>
|
||||||
|
<h2 id="24-해결하고자-하는-과제">2.4 해결하고자 하는 과제</h2>
|
||||||
|
<ol type="1">
|
||||||
|
<li>영상을 <strong>측점(위치)</strong> 으로 탐색·이동.</li>
|
||||||
|
<li>정지·선회 등 <strong>비이동 구간</strong>에서 탐색바 커서가 무의미하게 진행되지 않도록.</li>
|
||||||
|
<li>진행 방향(전진/후진), 통과 시설물을 <strong>위치 기준</strong>으로 표시.</li>
|
||||||
|
<li>동일 측점을 여러 번 통과한 경우 및 미도착 종점의 합리적 처리.</li>
|
||||||
|
</ol>
|
||||||
|
<h2 id="25-과제의-해결-수단-요지">2.5 과제의 해결 수단 (요지)</h2>
|
||||||
|
<p>영상 각 프레임의 드론 위치를, 측점들을 연결한 <strong>기준 폴리라인에 수직 투영</strong>하여 연속 측점값을 산출하고, <strong>프레임번호↔︎시간↔︎측점</strong> 매핑을 생성한다. 탐색바의 가로축을 시간이 아닌 <strong>누적 이동거리(측점 변화량의 누적)</strong> 로 구성하여 비이동 구간에서 커서가 정지하게 한다. 측점 증감으로 진행방향을 색 구분하고, 시설물을 측점값 위치에 배치하며, 입력 측점값을 매핑으로 역산하여 해당 시점으로 이동(검색)한다.</p>
|
||||||
|
<h2 id="26-도면의-간단한-설명">2.6 도면의 간단한 설명</h2>
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 700 160" role="img" aria-label="도5 GPS를 측점 폴리라인에 수직투영">
|
||||||
|
<line x1="60" y1="120" x2="640" y2="70" stroke="#8a7a5c" stroke-width="5"></line>
|
||||||
|
<circle cx="180" cy="112" r="3" fill="#111"></circle><circle cx="360" cy="97" r="3" fill="#111"></circle><circle cx="540" cy="82" r="3" fill="#111"></circle>
|
||||||
|
<text x="180" y="138" text-anchor="middle" font-size="9" fill="#6b4f2a">159k</text><text x="360" y="138" text-anchor="middle" font-size="9" fill="#6b4f2a">160k</text><text x="540" y="138" text-anchor="middle" font-size="9" fill="#6b4f2a">161k</text>
|
||||||
|
<circle cx="360" cy="35" r="6" fill="#2563eb"></circle><text x="360" y="26" text-anchor="middle" font-size="10" fill="#1e3a8a">드론 GPS</text>
|
||||||
|
<line x1="360" y1="35" x2="373" y2="93" stroke="#ef4444" stroke-width="2" stroke-dasharray="4 3"></line><circle cx="373" cy="93" r="4.5" fill="#ef4444"></circle>
|
||||||
|
<text x="410" y="97" font-size="10" fill="#b91c1c" font-weight="bold">수직 투영 발자국 = 160k130</text>
|
||||||
|
<text x="350" y="16" text-anchor="middle" font-size="11" font-weight="bold" fill="#334155">[도 5] 드론 GPS의 측점 기준선 수직 투영</text>
|
||||||
|
</svg>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 700 150" role="img" aria-label="도6 이동거리축 탐색바">
|
||||||
|
<text x="360" y="16" text-anchor="middle" font-size="11" font-weight="bold" fill="#334155">[도 6] 누적 이동거리축 탐색바(방향색·시설물)</text>
|
||||||
|
<rect x="40" y="45" width="240" height="24" fill="#f59e0b"></rect><rect x="280" y="45" width="140" height="24" fill="#3fb6d4"></rect><rect x="420" y="45" width="240" height="24" fill="#f59e0b"></rect>
|
||||||
|
<circle cx="470" cy="57" r="9" fill="#ea580c"></circle>
|
||||||
|
<line x1="150" y1="41" x2="150" y2="73" stroke="#0284c7" stroke-width="2"></line><text x="150" y="90" text-anchor="middle" font-size="9" fill="#0369a1">교량</text>
|
||||||
|
<line x1="520" y1="41" x2="520" y2="73" stroke="#0284c7" stroke-width="2"></line><text x="520" y="90" text-anchor="middle" font-size="9" fill="#0369a1">터널</text>
|
||||||
|
<text x="360" y="120" text-anchor="middle" font-size="10" fill="#475569">주황=전진(측점↑) · 파랑=후진(측점↓) · 세로선=시설물 통과지점</text>
|
||||||
|
</svg>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><strong>[도 5]</strong> 드론 GPS 를 측점 기준선(폴리라인)에 수직 투영하여 연속 측점값을 산출.</li>
|
||||||
|
<li><strong>[도 6]</strong> 누적 이동거리를 가로축으로 하는 탐색바(진행방향 색, 시설물 배치).</li>
|
||||||
|
<li>(추가 도면 예시: [도 7] 시간↔︎측점 매핑 생성, [도 8] 측점 검색→시간 역산 흐름.)</li>
|
||||||
|
</ul>
|
||||||
|
<h2 id="27-발명을-실시하기-위한-구체적인-내용-실시예">2.7 발명을 실시하기 위한 구체적인 내용 (실시예)</h2>
|
||||||
|
<h3 id="1-측점-기준선폴리라인-구성">(1) 측점 기준선(폴리라인) 구성</h3>
|
||||||
|
<p>측점 데이터(측점명 φ,λ)를 측점값 오름차순으로 정렬하여 평면좌표로 변환하고, 인접 측점을 연결한 폴리라인을 구성한다. 각 정점에 측점값(m) 을 부여한다.</p>
|
||||||
|
<h3 id="2-드론-위치--연속-측점값-수직-투영">(2) 드론 위치 → 연속 측점값 (수직 투영)</h3>
|
||||||
|
<p>프레임의 드론 위치 P 를 폴리라인의 각 선분 [A,B] 에 대해 수선의 발을 구하여 투영한다. 선분 매개변수</p>
|
||||||
|
<pre><code>t = clamp( ((P−A)·(B−A)) / |B−A|² , 0, 1 )
|
||||||
|
투영점 C = A + t·(B−A)</code></pre>
|
||||||
|
<p>로부터 최근접 선분을 선택하고, 그 선분의 측점값을</p>
|
||||||
|
<pre><code>km = km_A + t · (km_B − km_A)</code></pre>
|
||||||
|
<p>로 선형보간하여 <strong>연속 측점값(10m 이하 해상도)</strong> 을 산출한다. 동시에 선로 수직이격(offset)도 산출할 수 있다. (100m 양자화된 최근접 측점과 달리, 전진/후진 전환 시점이 실제 이동과 일치)</p>
|
||||||
|
<h3 id="3-프레임번호시간측점-매핑-생성">(3) 프레임번호↔︎시간↔︎측점 매핑 생성</h3>
|
||||||
|
<p>각 프레임에 대해 (프레임번호, 시간=프레임번호÷fps, 측점값)을 산출하여 배열로 저장(사전계산)한다. 이 매핑으로 시간↔︎측점을 상호 변환한다.</p>
|
||||||
|
<h3 id="4-누적-이동거리축-탐색바">(4) 누적 이동거리축 탐색바</h3>
|
||||||
|
<p>측점값을 이동평균으로 평활한 뒤, 프레임간 측점 변화량의 절대값을 누적하여 <strong>누적 이동거리</strong> 를 얻고, 전체값으로 정규화하여 [0,1] 진행도(frac)를 산출한다. 탐색바 가로축을 이 진행도로 하면,</p>
|
||||||
|
<ul>
|
||||||
|
<li>드론 <strong>정지·선회 구간</strong>은 측점 변화가 없어 진행도가 정체 → <strong>커서 정지</strong>,</li>
|
||||||
|
<li>이동 시(전진·후진 무관) 진행도 증가 → 커서 전진. 시간 t↔︎진행도 frac 은 단조 비감소 관계이므로 이진 탐색으로 상호 변환한다.</li>
|
||||||
|
</ul>
|
||||||
|
<h3 id="5-진행-방향-색-구분">(5) 진행 방향 색 구분</h3>
|
||||||
|
<p>평활 측점값의 증감 추세를 히스테리시스로 판정하여, 증가 구간(전진)과 감소 구간(후진)을 서로 다른 색으로 표시하고 방향 전환점을 표기한다.</p>
|
||||||
|
<h3 id="6-시설물의-측점-위치-배치">(6) 시설물의 측점 위치 배치</h3>
|
||||||
|
<p>교량/터널/역사 등 시설물을, 그 측점값에 대응하는 통과 시점(또는 좌표 근접 시점)의 탐색바 위치에 마커로 배치한다. 드론이 동일 시설물을 복수 회 통과하면 통과마다 마커를 배치한다.</p>
|
||||||
|
<h3 id="7-측점-검색탐색-이동">(7) 측점 검색(탐색 이동)</h3>
|
||||||
|
<p>입력된 측점값 q 에 대해, 매핑에서 |측점값−q| 가 허용오차 이내인 프레임 구간(연속 구간=1통과)을 찾아 각 통과의 시점을 산출하고, 현재 시점 이후의 다음 통과로 이동한다(반복 입력 시 순환). 허용오차 내 통과가 없으면 이동하지 않고 "해당 측점 없음"을 통지한다.</p>
|
||||||
|
<h3 id="8-마커-표시-정책-및-종점-처리선택적-실시예">(8) 마커 표시 정책 및 종점 처리(선택적 실시예)</h3>
|
||||||
|
<ul>
|
||||||
|
<li>시설물(특히 역사) 마커는 드론의 <strong>수직 투영 측점이 시설물 측점과 허용오차 이내로 일치</strong>하는 통과에서만 표시하여, 좌표만 근접하고 측점이 상이한 오표시를 방지한다.</li>
|
||||||
|
<li>영상이 종점 측점에 도달하지 못한 경우, 실제 통과 마커는 실제 위치에 유지하고 종점 마커는 탐색바 말단에 '미도착' 표시로 부가한다.</li>
|
||||||
|
</ul>
|
||||||
|
<h2 id="28-발명의-효과">2.8 발명의 효과</h2>
|
||||||
|
<ul>
|
||||||
|
<li>시간이 아닌 <strong>측점(위치)</strong> 으로 직관적 탐색 → 점검 효율 향상.</li>
|
||||||
|
<li>정지·선회 구간에서 <strong>커서 정지</strong> → 탐색바가 실제 이동을 반영.</li>
|
||||||
|
<li>진행방향·시설물을 위치 기준으로 파악, 동일 측점 복수통과·미도착 종점의 합리적 처리.</li>
|
||||||
|
</ul>
|
||||||
|
<h2 id="29-청구항-초안">2.9 청구항 (초안)</h2>
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
<p><strong>[청구항 1] (방법)</strong> 선형 구조물을 촬영한 주행영상을 탐색·재생하는 방법으로서, (a) 상기 주행영상의 프레임에 대응하는 이동체의 위치정보를 획득하는 단계; (b) 위치값(측점)이 부여된 복수의 기준점을 연결한 기준선에 상기 위치정보를 수직 투영하여, 해당 프레임의 연속 위치값을 산출하는 단계; (c) 상기 프레임의 시간정보와 상기 연속 위치값을 대응시킨 매핑을 생성하는 단계; 및 (d) 입력된 위치값에 대응하는 시간을 상기 매핑으로부터 산출하여, 상기 주행영상의 재생 위치를 해당 시간으로 이동시키는 단계; 를 포함하는, 주행영상 탐색·재생 방법.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
<p><strong>[청구항 2]</strong> 제1항에 있어서, 상기 매핑에 기초하여 프레임간 위치값 변화량을 누적한 누적 이동거리를 가로축으로 하는 탐색바를 구성하여, 상기 이동체의 비이동 구간에서 상기 탐색바의 커서가 정지하도록 하는 단계를 더 포함하는 방법.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
<p><strong>[청구항 3]</strong> 제1항에 있어서, 상기 위치값의 증가 구간과 감소 구간을 판별하여 상기 이동체의 진행 방향을 서로 다른 시각적 속성으로 표시하는 단계를 더 포함하는 방법.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
<p><strong>[청구항 4]</strong> 제1항에 있어서, 시설물의 위치값에 대응하는 상기 주행영상 내 통과 시점을 상기 매핑으로부터 산출하여 상기 시설물을 상기 탐색바의 대응 위치에 표시하되, 동일 시설물의 복수 통과에 대해 각각 표시하는 단계를 더 포함하는 방법.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
<p><strong>[청구항 5]</strong> 제4항에 있어서, 상기 시설물 중 역사(驛舍)는, 상기 이동체의 수직 투영 위치값이 상기 역사의 위치값과 허용오차 이내로 일치하는 통과에 대해서만 표시하는 것을 특징으로 하는 방법.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
<p><strong>[청구항 6]</strong> 제1항에 있어서, 상기 (d) 단계는, 입력된 위치값과 허용오차 이내인 통과가 복수인 경우 현재 재생 시점 이후의 다음 통과로 순환 이동하고, 허용오차 이내의 통과가 존재하지 않는 경우 재생 위치를 이동하지 않고 부존재를 통지하는 것을 특징으로 하는 방법.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
<p><strong>[청구항 7]</strong> 제1항에 있어서, 상기 이동체의 프레임 번호와 시간의 변환에 사용되는 프레임율을, 로그의 최대 프레임 번호를 영상 길이로 나눈 값에 기초하여 산출하는 것을 특징으로 하는 방법.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
<p><strong>[청구항 8] (장치·기록매체)</strong> 제1항 내지 제7항 중 어느 한 항의 방법을 수행하도록 구성된 프로세서 및 메모리를 포함하는 주행영상 탐색·재생 장치, 및 상기 방법을 컴퓨터에 실행시키는 프로그램이 기록된 컴퓨터로 판독 가능한 기록매체.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="210-요약서초안">2.10 요약서(초안)</h2>
|
||||||
|
<p>주행영상 각 프레임의 이동체 위치를 측점 기준선에 수직 투영하여 연속 측점값을 산출하고, 프레임번호·시간·측점값의 매핑을 생성한다. 누적 이동거리를 가로축으로 하는 탐색바로 비이동 구간에서 커서가 정지하며, 진행방향과 시설물을 위치 기준으로 표시하고, 입력 측점값을 시간으로 역산하여 재생 위치를 이동한다. 이로써 시간이 아닌 선로 위치로 영상을 직관적으로 탐색·재생한다.</p>
|
||||||
|
<hr />
|
||||||
|
<h2 id="부록-a-구현-근거소스-매핑--명세서-뒷받침용">부록 A. 구현 근거(소스 매핑) — 명세서 뒷받침용</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="header">
|
||||||
|
<th>발명 구성요소</th>
|
||||||
|
<th>구현 파일·함수</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>위경도→TM, ENU 상대좌표</td>
|
||||||
|
<td><a href="../client/src/utils/geoProjection.ts">geoProjection.ts</a> <code>latLonToTM</code>, <code>buildRelEnu</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>회전행렬·카메라좌표</td>
|
||||||
|
<td>geoProjection.ts <code>buildRotation</code>, <code>applyRw2c</code>, <code>toCameraCoords</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>핀홀 투영(u,v)</td>
|
||||||
|
<td>geoProjection.ts <code>pixelFromCamera</code>, <code>projectPoint</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>프레임 보간</td>
|
||||||
|
<td><a href="../client/src/components/overlay/StationOverlay.tsx">StationOverlay.tsx</a> <code>poseAt</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>평활(이동평균/EMA)</td>
|
||||||
|
<td>StationOverlay.tsx <code>smoothFrame</code>, <code>smoothStep</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>fps 자동산출</td>
|
||||||
|
<td>VideoPlayer.tsx <code>effectiveFps</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>GPS→측점 수직투영</td>
|
||||||
|
<td><a href="../client/src/utils/chainage.ts">chainage.ts</a> <code>projectToChain</code>, <code>buildChainLine</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>시간↔︎측점 매핑·이동거리축</td>
|
||||||
|
<td><a href="../client/src/stationbar/StationBar.tsx">StationBar.tsx</a> precompute, <code>cumFracAtTime</code>/<code>timeAtFrac</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>방향색·시설물 배치</td>
|
||||||
|
<td>StationBar.tsx <code>barSegments</code>, <code>structureMarks</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>측점 검색</td>
|
||||||
|
<td>StationBar.tsx <code>handleJumpToMileage</code></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h2 id="부록-b-출원-실무-유의사항">부록 B. 출원 실무 유의사항</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>공지/사용 이력 주의</strong>: 이미 공개(시연·논문·웹 게시)한 경우 신규성 상실 우려 → 공개일 확인, 필요 시 공지예외(자기공지) 12개월 이내 출원 검토.</li>
|
||||||
|
<li><strong>직무발명</strong>: 회사 소속이면 직무발명 신고·승계 절차 확인.</li>
|
||||||
|
<li><strong>명세서 뒷받침</strong>: 각 청구항 구성요소가 실시예(1.7/2.7)에 기재되어야 함(기재불비 방지).</li>
|
||||||
|
<li><strong>도면 정식화</strong>: 상기 SVG 개념도를 특허 도면 규격(흑백 선도, 부호 표기)으로 재작성.</li>
|
||||||
|
<li><strong>선행기술조사</strong>: KIPRIS/특허문헌에서 "UAV video AR overlay", "georeferencing overlay", "chainage-based video navigation" 등 검색.</li>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,372 @@
|
|||||||
|
# 특허 명세서 초안 — GhiVideo 2개 발명
|
||||||
|
|
||||||
|
> **성격**: 아래는 특허 출원을 위한 **기술 정리 초안**입니다. 실제 출원 전 **변리사(특허사무소) 검토**가 반드시 필요합니다.
|
||||||
|
> 청구항은 권리범위를 정하는 핵심이므로, 아래 청구항은 **초안(방향 제시용)** 이며 변리사와 함께 확정하십시오.
|
||||||
|
> **선행기술조사(공지기술 확인)** 후 신규성·진보성 관점에서 청구항을 조정해야 합니다.
|
||||||
|
>
|
||||||
|
> **대상 발명**
|
||||||
|
> - **제1발명**: 드론 항공영상에 지리정보를 실시간 정합·표시하는 방법 및 장치 (좌표투영)
|
||||||
|
> - **제2발명**: 측점(체이니지) 기반 주행영상 탐색·재생 방법 및 장치 (측점기반재생)
|
||||||
|
|
||||||
|
<style>
|
||||||
|
figure.fig{margin:20px 0;text-align:center;page-break-inside:avoid;break-inside:avoid;}
|
||||||
|
figure.fig svg{width:100%;max-width:700px;height:auto;border:1px solid #e7e0d2;border-radius:8px;background:#fffdf8;}
|
||||||
|
figure.fig figcaption{font-size:0.9em;color:#777;margin-top:6px;}
|
||||||
|
text{font-family:'Malgun Gothic','Hancom Gothic',sans-serif;}
|
||||||
|
code{background:#f5f3ee;padding:1px 4px;border-radius:4px;}
|
||||||
|
h2{border-top:3px solid #e7e0d2;padding-top:12px;margin-top:30px;}
|
||||||
|
h3{margin-top:22px;}
|
||||||
|
.claim{background:#fbfaf6;border:1px solid #e7e0d2;border-radius:6px;padding:10px 14px;margin:8px 0;}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 공통 개요 및 출원 전략
|
||||||
|
|
||||||
|
### 0.1 두 발명의 관계
|
||||||
|
두 발명은 모두 **드론으로 촬영한 선로 주행영상**과 **드론의 위치·자세 로그(GPS/IMU)**, **선로 측점·시설물 지리데이터**를 입력으로 사용하나, 해결 과제와 수단이 서로 독립적이다.
|
||||||
|
- 제1발명: "촬영 시점의 카메라 기하를 복원하여 지리정보를 **영상 화면의 정확한 픽셀 위치**에 정합·표시"
|
||||||
|
- 제2발명: "드론 위치를 선로 측점으로 환산하여 영상을 **시간이 아닌 측점(위치)으로 탐색·재생**"
|
||||||
|
|
||||||
|
### 0.2 출원 형태(변리사와 협의)
|
||||||
|
- (안 A) **2건 개별 출원**: 각 발명을 독립 출원. 권리 명확.
|
||||||
|
- (안 B) **1건에 복수 청구항 세트**: 단일성(발명의 단일성) 인정 여부를 변리사가 검토. 두 발명이 "드론 주행영상 처리"라는 공통 기술사상으로 묶일 수 있으나, 단일성 부정 시 분할출원 필요.
|
||||||
|
- 소프트웨어 발명이므로 **"방법 청구항 + 장치(시스템) 청구항 + 컴퓨터 판독가능 기록매체 청구항"** 3종 세트로 구성 권장(권리 확보 폭 확대).
|
||||||
|
|
||||||
|
### 0.3 명세서 작성 체크리스트
|
||||||
|
- [ ] 선행기술조사(KIPRIS 등) — 항공영상 AR 오버레이, 지오레퍼런싱, 체이니지 기반 검색 관련
|
||||||
|
- [ ] 각 청구항 구성요소가 실시예로 뒷받침되는지(기재불비 방지)
|
||||||
|
- [ ] 도면 부호 일관성
|
||||||
|
- [ ] 효과가 과제와 대응되는지
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 제1발명 — 드론 항공영상에 지리정보를 실시간 정합·표시하는 방법 및 장치
|
||||||
|
|
||||||
|
## 1.1 발명의 명칭
|
||||||
|
드론 항공영상에 대한 지리정보 실시간 정합 및 오버레이 표시 방법, 장치 및 기록매체
|
||||||
|
(영문 예: Method and apparatus for real-time geo-registration and overlay of geographic information onto UAV aerial video)
|
||||||
|
|
||||||
|
## 1.2 기술분야
|
||||||
|
본 발명은 무인비행체(드론)로 촬영한 항공영상 위에, 지도상의 지리정보 객체(측점, 교량, 터널, 역사 등)를 촬영 당시의 카메라 기하에 맞추어 **영상 화면의 정확한 위치에 실시간으로 겹쳐 표시**하는 영상처리 기술에 관한 것이다.
|
||||||
|
|
||||||
|
## 1.3 배경기술
|
||||||
|
- 종래 드론 점검영상은 단순 재생만 가능하여, 영상 속 특정 지점이 **지도상 어느 시설물인지** 육안으로 대조해야 했다.
|
||||||
|
- 일반적인 증강현실(AR) 오버레이는 정밀 IMU·짐벌 캘리브레이션이 필요하거나, 실시간 카메라 트래킹(SLAM 등) 부하가 크다.
|
||||||
|
- 지오레퍼런싱(정사영상 생성)은 후처리·연산량이 크고 실시간 재생 중 라벨을 프레임 정합해 표시하기 어렵다.
|
||||||
|
|
||||||
|
## 1.4 해결하고자 하는 과제
|
||||||
|
1. 별도 마커/실시간 SLAM 없이, **드론의 위치·자세 로그와 카메라 내부파라미터만으로** 지리정보를 영상에 정합.
|
||||||
|
2. 영상 프레임율과 무관하게 **화면 갱신 주기(예: 60fps)로 매끄럽게** 라벨이 대상에 붙어 이동.
|
||||||
|
3. 드론 GPS/자세의 노이즈·떨림에도 **라벨이 안정적으로** 표시.
|
||||||
|
4. 영상별 프레임율(fps)이 상이해도 **자동 정합**.
|
||||||
|
|
||||||
|
## 1.5 과제의 해결 수단 (요지)
|
||||||
|
프레임별 드론 자세로부터 회전행렬을 구성하고, 지리정보 객체의 위경도를 평면직교좌표(TM)로 변환한 뒤 드론 기준 상대좌표(ENU)로 산출하고, 회전행렬로 카메라좌표계로 변환한 다음 **핀홀 카메라 모델**로 정규화 화면좌표에 투영하여 오버레이한다. 프레임 사이는 **자세 보간**으로 채우고, 화면좌표는 **속도적응 평활(이동평균+지수이동평균)** 로 안정화하며, 영상 fps 는 **데이터로부터 자동 산출**한다.
|
||||||
|
|
||||||
|
## 1.6 도면의 간단한 설명
|
||||||
|
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 700 150" role="img" aria-label="도1 제1발명 처리 파이프라인">
|
||||||
|
<defs><marker id="p1" markerWidth="10" markerHeight="10" refX="7" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#475569"/></marker></defs>
|
||||||
|
<rect x="10" y="55" width="120" height="44" rx="6" fill="#ecfdf5" stroke="#16a34a"/><text x="70" y="74" text-anchor="middle" font-size="10" fill="#166534">위경도→미터</text><text x="70" y="90" text-anchor="middle" font-size="9" fill="#166534">(TM 변환)</text>
|
||||||
|
<line x1="130" y1="77" x2="150" y2="77" stroke="#475569" stroke-width="1.5" marker-end="url(#p1)"/>
|
||||||
|
<rect x="152" y="55" width="120" height="44" rx="6" fill="#fefce8" stroke="#ca8a04"/><text x="212" y="74" text-anchor="middle" font-size="10" fill="#854d0e">드론기준 상대좌표</text><text x="212" y="90" text-anchor="middle" font-size="9" fill="#854d0e">(ENU)</text>
|
||||||
|
<line x1="272" y1="77" x2="292" y2="77" stroke="#475569" stroke-width="1.5" marker-end="url(#p1)"/>
|
||||||
|
<rect x="294" y="55" width="120" height="44" rx="6" fill="#eff6ff" stroke="#2563eb"/><text x="354" y="74" text-anchor="middle" font-size="10" fill="#1e3a8a">회전행렬 변환</text><text x="354" y="90" text-anchor="middle" font-size="9" fill="#1e3a8a">(카메라좌표)</text>
|
||||||
|
<line x1="414" y1="77" x2="434" y2="77" stroke="#475569" stroke-width="1.5" marker-end="url(#p1)"/>
|
||||||
|
<rect x="436" y="55" width="120" height="44" rx="6" fill="#fdf2f8" stroke="#db2777"/><text x="496" y="74" text-anchor="middle" font-size="10" fill="#9d174d">핀홀 투영</text><text x="496" y="90" text-anchor="middle" font-size="9" fill="#9d174d">(화면좌표 u,v)</text>
|
||||||
|
<line x1="556" y1="77" x2="576" y2="77" stroke="#475569" stroke-width="1.5" marker-end="url(#p1)"/>
|
||||||
|
<rect x="578" y="55" width="112" height="44" rx="6" fill="#f5f3ff" stroke="#7c3aed"/><text x="634" y="74" text-anchor="middle" font-size="10" fill="#5b21b6">평활·오버레이</text><text x="634" y="90" text-anchor="middle" font-size="9" fill="#5b21b6">(라벨 표시)</text>
|
||||||
|
<text x="350" y="20" text-anchor="middle" font-size="11" font-weight="bold" fill="#334155">[도 1] 제1발명 전체 처리 파이프라인</text>
|
||||||
|
</svg>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 700 200" role="img" aria-label="도2 핀홀 투영 원리">
|
||||||
|
<line x1="90" y1="40" x2="90" y2="160" stroke="#16a34a" stroke-width="6"/><text x="90" y="180" text-anchor="middle" font-size="10" fill="#166534">대상(3D)</text>
|
||||||
|
<line x1="360" y1="25" x2="360" y2="175" stroke="#94a3b8" stroke-width="5"/><circle cx="360" cy="100" r="5" fill="#111"/><text x="360" y="193" text-anchor="middle" font-size="10" fill="#475569">투영중심(렌즈)</text>
|
||||||
|
<line x1="90" y1="40" x2="590" y2="150" stroke="#f59e0b" stroke-width="1.3"/>
|
||||||
|
<line x1="90" y1="160" x2="590" y2="50" stroke="#f59e0b" stroke-width="1.3"/>
|
||||||
|
<line x1="590" y1="50" x2="590" y2="150" stroke="#ef4444" stroke-width="5"/><text x="590" y="170" text-anchor="middle" font-size="10" fill="#b91c1c">상(화면, u·v)</text>
|
||||||
|
<text x="350" y="16" text-anchor="middle" font-size="11" font-weight="bold" fill="#334155">[도 2] 핀홀 카메라 투영 (원근: 멀수록 작게)</text>
|
||||||
|
</svg>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
- **[도 1]** 제1발명의 전체 처리 파이프라인(좌표변환→상대좌표→회전→투영→표시).
|
||||||
|
- **[도 2]** 핀홀 카메라 모델에 의한 3D→2D 투영 개념.
|
||||||
|
- (추가 도면 예시: [도 3] 자세각 yaw/pitch/roll 정의, [도 4] 프레임간 보간 및 평활 개념 — 변리사와 함께 정식 도면화.)
|
||||||
|
|
||||||
|
## 1.7 발명을 실시하기 위한 구체적인 내용 (실시예)
|
||||||
|
|
||||||
|
### (1) 입력 데이터
|
||||||
|
- 프레임별 드론 상태: 위도 φ, 경도 λ, 고도 h(타원체고 또는 절대고도), 자세 yaw(기수방위), pitch(틸트), roll.
|
||||||
|
- 카메라 내부파라미터: 초점거리 f(35mm 환산), 센서 폭 sW, 센서 높이 sH, 주점 보정 (cx0, cy0).
|
||||||
|
- 지리정보 객체 O: 위경도 (φo, λo), 표고 zo(또는 추정), 식별정보(명칭/속성).
|
||||||
|
|
||||||
|
### (2) 좌표계 변환 — 위경도 → 평면직교(TM) → 상대좌표(ENU)
|
||||||
|
지구 곡률로 인해 각도(위경도)로는 거리 계산이 부정확하므로, 대상과 드론의 위경도를 **평면직교좌표계(예: EPSG:5186 한국 TM, 횡축 메르카토르)** 로 변환하여 미터 단위 좌표 (E, N) 를 얻는다.
|
||||||
|
드론을 기준으로 한 상대좌표(동-북-상, ENU)는
|
||||||
|
```
|
||||||
|
rel = (E_o − E_d − offX, N_o − N_d − offY, z_o − h_d − offZ)
|
||||||
|
```
|
||||||
|
로 산출한다(offX/offY/offZ 는 드론 위치 미세보정 파라미터). 대상 표고는 정표고(EL)에 지오이드고를 가산하여 드론 고도 기준(타원체고)과 datum 을 일치시킨다.
|
||||||
|
|
||||||
|
### (3) 자세 → 회전행렬
|
||||||
|
드론 자세(yaw, pitch, roll)로부터 회전행렬을 다음과 같이 구성한다.
|
||||||
|
```
|
||||||
|
R_b2w = Rz(−yaw) · Rx(pitch) · Ry(roll)
|
||||||
|
R_align = [[1,0,0],[0,0,−1],[0,1,0]] (기체좌표계 → 카메라좌표계 축 정렬)
|
||||||
|
R_w2c = R_align · R_b2wᵀ
|
||||||
|
```
|
||||||
|
카메라 좌표는
|
||||||
|
```
|
||||||
|
(Xc, Yc, Zc)ᵀ = R_w2c · rel
|
||||||
|
```
|
||||||
|
로 얻는다(Zc>0 이 카메라 전방).
|
||||||
|
|
||||||
|
### (4) 핀홀 카메라 투영 → 정규화 화면좌표
|
||||||
|
```
|
||||||
|
u = 0.5 + cx0 + (Xc / Zc) · (f / sW) (0=좌, 1=우)
|
||||||
|
v = 0.5 + cy0 + (Yc / Zc) · (f / sH) (0=상, 1=하)
|
||||||
|
```
|
||||||
|
멀수록(Zc↑) 화면 중심에 가깝고 작게 투영되는 원근이 자연히 반영된다. (u, v) 가 [0,1] 범위(또는 표시허용 범위) 내이면 해당 정규화 좌표에 식별정보를 오버레이한다.
|
||||||
|
|
||||||
|
### (5) 프레임간 보간 (매끄러운 추종)
|
||||||
|
영상 프레임율보다 화면 갱신주기가 클 수 있으므로(예: 30fps 영상, 60Hz 화면), 재생 시각에 대응하는 **연속 프레임 인덱스**에서 인접 프레임의 위치·자세를 선형보간(방위는 최단각 보간)하여 중간 포즈를 산출하고, 상기 (2)~(4)를 재수행한다. 이로써 라벨이 매 화면 갱신마다 대상에 붙어 매끄럽게 이동한다.
|
||||||
|
|
||||||
|
### (6) 안정화 — 속도적응 평활 및 이상치 거부
|
||||||
|
GPS/자세 노이즈로 인한 라벨 떨림을 억제하기 위해,
|
||||||
|
- 자세는 인접 프레임 **이동평균**(회전 경계 보존형 적응 창)으로 평활하고,
|
||||||
|
- 화면좌표는 **속도적응 지수이동평균(EMA)** 으로 평활한다. 즉 이동속도가 작을수록(정지·떨림) 강하게 평활하고, 클수록(실제 이동) 즉시 추종하도록 평활계수 α 를 이동속도에 따라 가변한다.
|
||||||
|
- 1프레임에 임계 이상 급이동하는 좌표는 이상치로 보아 일정 횟수 무시(이전 위치 유지)한다.
|
||||||
|
|
||||||
|
### (7) 영상 fps 자동 산출
|
||||||
|
프레임 번호↔시간 변환을 위한 fps 를, 드론 로그의 **최대 프레임 번호 ÷ 영상 길이(초)** 로 산출하고 표준 fps(23.976/24/25/29.97/30/50/60 등) 중 근접값에 스냅한다. 이로써 영상별 상이한 fps 에 자동 대응한다.
|
||||||
|
|
||||||
|
### (8) 렌더링 및 대상 표고 추정(선택적 실시예)
|
||||||
|
- 표시는 화면 갱신주기(예: 60fps)로 캔버스에 수행하고, 가시성/겹침 판정 등 고부하 연산은 유휴 시간에 사전계산하여 분리한다.
|
||||||
|
- 대상 표고가 미상일 때, (i) 최근접 선로 중심선의 표고, (ii) 외부 수치표고모델(DEM), (iii) 드론 고도로부터의 일정 이격거리 중 하나로 추정할 수 있다.
|
||||||
|
|
||||||
|
## 1.8 발명의 효과
|
||||||
|
- 마커/실시간 SLAM 없이 로그+내부파라미터만으로 정합 → 저부하·오프라인 처리 가능.
|
||||||
|
- 프레임율 무관 매끄러운 라벨 추종, 노이즈에 강인, fps 자동 대응.
|
||||||
|
- 점검·분석 시 영상 속 지점의 지리적 식별이 즉시 가능.
|
||||||
|
|
||||||
|
## 1.9 청구항 (초안)
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 1] (방법)** 무인비행체로 촬영한 항공영상에 지리정보를 표시하는 방법으로서,
|
||||||
|
(a) 상기 항공영상의 프레임에 대응하는 무인비행체의 위치정보(위도·경도·고도) 및 자세정보(방위각·틸트각·롤각)를 획득하는 단계;
|
||||||
|
(b) 지리정보 객체의 위경도 좌표를 평면직교좌표계로 변환하고, 상기 무인비행체의 위치를 기준으로 한 상대좌표를 산출하는 단계;
|
||||||
|
(c) 상기 자세정보로부터 회전행렬을 구성하여 상기 상대좌표를 카메라 좌표계의 좌표로 변환하는 단계;
|
||||||
|
(d) 상기 카메라 좌표계의 좌표를, 초점거리 및 센서규격을 포함하는 카메라 파라미터에 기초한 핀홀 카메라 모델로 정규화 화면좌표에 투영하는 단계; 및
|
||||||
|
(e) 상기 정규화 화면좌표에 대응하는 상기 영상의 화면 위치에 상기 지리정보 객체의 식별정보를 오버레이하여 표시하는 단계;
|
||||||
|
를 포함하는, 항공영상 지리정보 표시 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 2]** 제1항에 있어서, 상기 (b) 단계는 대상의 정표고에 지오이드고를 가산하여 상기 무인비행체의 고도 기준과 datum 을 일치시켜 상대좌표의 상하 성분을 산출하는 것을 특징으로 하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 3]** 제1항에 있어서, 재생 시각에 대응하는 연속 프레임 인덱스에서 인접 프레임의 위치 및 자세를 보간하여 중간 포즈를 산출하고, 상기 중간 포즈를 이용해 화면 갱신주기마다 상기 (b)~(e)를 수행하여, 영상 프레임율보다 높은 빈도로 상기 식별정보의 표시 위치를 갱신하는 것을 특징으로 하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 4]** 제1항에 있어서, 상기 정규화 화면좌표를, 이동속도가 작을수록 강하게 평활하고 클수록 약하게 평활하는 속도적응 평활을 적용하여 안정화하는 단계를 더 포함하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 5]** 제1항에 있어서, 상기 무인비행체 로그의 최대 프레임 번호를 상기 영상의 길이로 나눈 값에 기초하여 프레임율을 산출하고, 산출된 값을 사전 정의된 표준 프레임율 중 근접값으로 결정하여 프레임 번호와 시간의 변환에 사용하는 것을 특징으로 하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 6] (장치)** 제1항 내지 제5항 중 어느 한 항의 방법을 수행하도록 구성된 하나 이상의 프로세서 및 메모리를 포함하는, 항공영상 지리정보 표시 장치.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 7] (기록매체)** 제1항 내지 제5항 중 어느 한 항의 방법을 컴퓨터에 실행시키기 위한 프로그램이 기록된, 컴퓨터로 판독 가능한 기록매체.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 1.10 요약서(초안)
|
||||||
|
무인비행체 항공영상의 각 프레임에 대응하는 위치·자세와 카메라 파라미터를 이용하여, 지리정보 객체의 위경도를 평면직교좌표 및 드론 기준 상대좌표로 변환하고 회전행렬과 핀홀 모델로 정규화 화면좌표에 투영하여 영상에 식별정보를 정합·오버레이한다. 프레임간 보간과 속도적응 평활, 프레임율 자동산출을 통해 별도 마커 없이 프레임율과 무관하게 안정적으로 라벨을 표시한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 제2발명 — 측점(체이니지) 기반 주행영상 탐색·재생 방법 및 장치
|
||||||
|
|
||||||
|
## 2.1 발명의 명칭
|
||||||
|
선로 측점 기반의 주행영상 탐색 및 재생 방법, 장치 및 기록매체
|
||||||
|
(영문 예: Chainage(station)-based navigation and playback method and apparatus for route inspection video)
|
||||||
|
|
||||||
|
## 2.2 기술분야
|
||||||
|
본 발명은 선로 등 선형 구조물을 촬영한 주행영상을, 재생 시간이 아닌 **선로상의 위치값(측점, 체이니지)** 을 기준으로 탐색·표시·재생하는 영상 재생 인터페이스 기술에 관한 것이다.
|
||||||
|
|
||||||
|
## 2.3 배경기술
|
||||||
|
- 통상의 영상 플레이어는 **시간축**(경과 시간)으로만 탐색되어, 특정 시간이 선로상 어느 지점인지 알기 어렵다.
|
||||||
|
- 철도 점검 실무는 "몇 분"이 아니라 **"몇 측점"** 으로 위치를 지칭한다.
|
||||||
|
- 드론이 동일 구간을 왕복하거나 정지·선회하는 경우, 시간축 탐색바는 실제 이동과 불일치한다.
|
||||||
|
|
||||||
|
## 2.4 해결하고자 하는 과제
|
||||||
|
1. 영상을 **측점(위치)** 으로 탐색·이동.
|
||||||
|
2. 정지·선회 등 **비이동 구간**에서 탐색바 커서가 무의미하게 진행되지 않도록.
|
||||||
|
3. 진행 방향(전진/후진), 통과 시설물을 **위치 기준**으로 표시.
|
||||||
|
4. 동일 측점을 여러 번 통과한 경우 및 미도착 종점의 합리적 처리.
|
||||||
|
|
||||||
|
## 2.5 과제의 해결 수단 (요지)
|
||||||
|
영상 각 프레임의 드론 위치를, 측점들을 연결한 **기준 폴리라인에 수직 투영**하여 연속 측점값을 산출하고, **프레임번호↔시간↔측점** 매핑을 생성한다. 탐색바의 가로축을 시간이 아닌 **누적 이동거리(측점 변화량의 누적)** 로 구성하여 비이동 구간에서 커서가 정지하게 한다. 측점 증감으로 진행방향을 색 구분하고, 시설물을 측점값 위치에 배치하며, 입력 측점값을 매핑으로 역산하여 해당 시점으로 이동(검색)한다.
|
||||||
|
|
||||||
|
## 2.6 도면의 간단한 설명
|
||||||
|
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 700 160" role="img" aria-label="도5 GPS를 측점 폴리라인에 수직투영">
|
||||||
|
<line x1="60" y1="120" x2="640" y2="70" stroke="#8a7a5c" stroke-width="5"/>
|
||||||
|
<circle cx="180" cy="112" r="3" fill="#111"/><circle cx="360" cy="97" r="3" fill="#111"/><circle cx="540" cy="82" r="3" fill="#111"/>
|
||||||
|
<text x="180" y="138" text-anchor="middle" font-size="9" fill="#6b4f2a">159k</text><text x="360" y="138" text-anchor="middle" font-size="9" fill="#6b4f2a">160k</text><text x="540" y="138" text-anchor="middle" font-size="9" fill="#6b4f2a">161k</text>
|
||||||
|
<circle cx="360" cy="35" r="6" fill="#2563eb"/><text x="360" y="26" text-anchor="middle" font-size="10" fill="#1e3a8a">드론 GPS</text>
|
||||||
|
<line x1="360" y1="35" x2="373" y2="93" stroke="#ef4444" stroke-width="2" stroke-dasharray="4 3"/><circle cx="373" cy="93" r="4.5" fill="#ef4444"/>
|
||||||
|
<text x="410" y="97" font-size="10" fill="#b91c1c" font-weight="bold">수직 투영 발자국 = 160k130</text>
|
||||||
|
<text x="350" y="16" text-anchor="middle" font-size="11" font-weight="bold" fill="#334155">[도 5] 드론 GPS의 측점 기준선 수직 투영</text>
|
||||||
|
</svg>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<figure class="fig">
|
||||||
|
<svg viewBox="0 0 700 150" role="img" aria-label="도6 이동거리축 탐색바">
|
||||||
|
<text x="360" y="16" text-anchor="middle" font-size="11" font-weight="bold" fill="#334155">[도 6] 누적 이동거리축 탐색바(방향색·시설물)</text>
|
||||||
|
<rect x="40" y="45" width="240" height="24" fill="#f59e0b"/><rect x="280" y="45" width="140" height="24" fill="#3fb6d4"/><rect x="420" y="45" width="240" height="24" fill="#f59e0b"/>
|
||||||
|
<circle cx="470" cy="57" r="9" fill="#ea580c"/>
|
||||||
|
<line x1="150" y1="41" x2="150" y2="73" stroke="#0284c7" stroke-width="2"/><text x="150" y="90" text-anchor="middle" font-size="9" fill="#0369a1">교량</text>
|
||||||
|
<line x1="520" y1="41" x2="520" y2="73" stroke="#0284c7" stroke-width="2"/><text x="520" y="90" text-anchor="middle" font-size="9" fill="#0369a1">터널</text>
|
||||||
|
<text x="360" y="120" text-anchor="middle" font-size="10" fill="#475569">주황=전진(측점↑) · 파랑=후진(측점↓) · 세로선=시설물 통과지점</text>
|
||||||
|
</svg>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
- **[도 5]** 드론 GPS 를 측점 기준선(폴리라인)에 수직 투영하여 연속 측점값을 산출.
|
||||||
|
- **[도 6]** 누적 이동거리를 가로축으로 하는 탐색바(진행방향 색, 시설물 배치).
|
||||||
|
- (추가 도면 예시: [도 7] 시간↔측점 매핑 생성, [도 8] 측점 검색→시간 역산 흐름.)
|
||||||
|
|
||||||
|
## 2.7 발명을 실시하기 위한 구체적인 내용 (실시예)
|
||||||
|
|
||||||
|
### (1) 측점 기준선(폴리라인) 구성
|
||||||
|
측점 데이터(측점명 φ,λ)를 측점값 오름차순으로 정렬하여 평면좌표로 변환하고, 인접 측점을 연결한 폴리라인을 구성한다. 각 정점에 측점값(m) 을 부여한다.
|
||||||
|
|
||||||
|
### (2) 드론 위치 → 연속 측점값 (수직 투영)
|
||||||
|
프레임의 드론 위치 P 를 폴리라인의 각 선분 [A,B] 에 대해 수선의 발을 구하여 투영한다. 선분 매개변수
|
||||||
|
```
|
||||||
|
t = clamp( ((P−A)·(B−A)) / |B−A|² , 0, 1 )
|
||||||
|
투영점 C = A + t·(B−A)
|
||||||
|
```
|
||||||
|
로부터 최근접 선분을 선택하고, 그 선분의 측점값을
|
||||||
|
```
|
||||||
|
km = km_A + t · (km_B − km_A)
|
||||||
|
```
|
||||||
|
로 선형보간하여 **연속 측점값(10m 이하 해상도)** 을 산출한다. 동시에 선로 수직이격(offset)도 산출할 수 있다. (100m 양자화된 최근접 측점과 달리, 전진/후진 전환 시점이 실제 이동과 일치)
|
||||||
|
|
||||||
|
### (3) 프레임번호↔시간↔측점 매핑 생성
|
||||||
|
각 프레임에 대해 (프레임번호, 시간=프레임번호÷fps, 측점값)을 산출하여 배열로 저장(사전계산)한다. 이 매핑으로 시간↔측점을 상호 변환한다.
|
||||||
|
|
||||||
|
### (4) 누적 이동거리축 탐색바
|
||||||
|
측점값을 이동평균으로 평활한 뒤, 프레임간 측점 변화량의 절대값을 누적하여 **누적 이동거리** 를 얻고, 전체값으로 정규화하여 [0,1] 진행도(frac)를 산출한다. 탐색바 가로축을 이 진행도로 하면,
|
||||||
|
- 드론 **정지·선회 구간**은 측점 변화가 없어 진행도가 정체 → **커서 정지**,
|
||||||
|
- 이동 시(전진·후진 무관) 진행도 증가 → 커서 전진.
|
||||||
|
시간 t↔진행도 frac 은 단조 비감소 관계이므로 이진 탐색으로 상호 변환한다.
|
||||||
|
|
||||||
|
### (5) 진행 방향 색 구분
|
||||||
|
평활 측점값의 증감 추세를 히스테리시스로 판정하여, 증가 구간(전진)과 감소 구간(후진)을 서로 다른 색으로 표시하고 방향 전환점을 표기한다.
|
||||||
|
|
||||||
|
### (6) 시설물의 측점 위치 배치
|
||||||
|
교량/터널/역사 등 시설물을, 그 측점값에 대응하는 통과 시점(또는 좌표 근접 시점)의 탐색바 위치에 마커로 배치한다. 드론이 동일 시설물을 복수 회 통과하면 통과마다 마커를 배치한다.
|
||||||
|
|
||||||
|
### (7) 측점 검색(탐색 이동)
|
||||||
|
입력된 측점값 q 에 대해, 매핑에서 |측점값−q| 가 허용오차 이내인 프레임 구간(연속 구간=1통과)을 찾아 각 통과의 시점을 산출하고, 현재 시점 이후의 다음 통과로 이동한다(반복 입력 시 순환). 허용오차 내 통과가 없으면 이동하지 않고 "해당 측점 없음"을 통지한다.
|
||||||
|
|
||||||
|
### (8) 마커 표시 정책 및 종점 처리(선택적 실시예)
|
||||||
|
- 시설물(특히 역사) 마커는 드론의 **수직 투영 측점이 시설물 측점과 허용오차 이내로 일치**하는 통과에서만 표시하여, 좌표만 근접하고 측점이 상이한 오표시를 방지한다.
|
||||||
|
- 영상이 종점 측점에 도달하지 못한 경우, 실제 통과 마커는 실제 위치에 유지하고 종점 마커는 탐색바 말단에 '미도착' 표시로 부가한다.
|
||||||
|
|
||||||
|
## 2.8 발명의 효과
|
||||||
|
- 시간이 아닌 **측점(위치)** 으로 직관적 탐색 → 점검 효율 향상.
|
||||||
|
- 정지·선회 구간에서 **커서 정지** → 탐색바가 실제 이동을 반영.
|
||||||
|
- 진행방향·시설물을 위치 기준으로 파악, 동일 측점 복수통과·미도착 종점의 합리적 처리.
|
||||||
|
|
||||||
|
## 2.9 청구항 (초안)
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 1] (방법)** 선형 구조물을 촬영한 주행영상을 탐색·재생하는 방법으로서,
|
||||||
|
(a) 상기 주행영상의 프레임에 대응하는 이동체의 위치정보를 획득하는 단계;
|
||||||
|
(b) 위치값(측점)이 부여된 복수의 기준점을 연결한 기준선에 상기 위치정보를 수직 투영하여, 해당 프레임의 연속 위치값을 산출하는 단계;
|
||||||
|
(c) 상기 프레임의 시간정보와 상기 연속 위치값을 대응시킨 매핑을 생성하는 단계; 및
|
||||||
|
(d) 입력된 위치값에 대응하는 시간을 상기 매핑으로부터 산출하여, 상기 주행영상의 재생 위치를 해당 시간으로 이동시키는 단계;
|
||||||
|
를 포함하는, 주행영상 탐색·재생 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 2]** 제1항에 있어서, 상기 매핑에 기초하여 프레임간 위치값 변화량을 누적한 누적 이동거리를 가로축으로 하는 탐색바를 구성하여, 상기 이동체의 비이동 구간에서 상기 탐색바의 커서가 정지하도록 하는 단계를 더 포함하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 3]** 제1항에 있어서, 상기 위치값의 증가 구간과 감소 구간을 판별하여 상기 이동체의 진행 방향을 서로 다른 시각적 속성으로 표시하는 단계를 더 포함하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 4]** 제1항에 있어서, 시설물의 위치값에 대응하는 상기 주행영상 내 통과 시점을 상기 매핑으로부터 산출하여 상기 시설물을 상기 탐색바의 대응 위치에 표시하되, 동일 시설물의 복수 통과에 대해 각각 표시하는 단계를 더 포함하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 5]** 제4항에 있어서, 상기 시설물 중 역사(驛舍)는, 상기 이동체의 수직 투영 위치값이 상기 역사의 위치값과 허용오차 이내로 일치하는 통과에 대해서만 표시하는 것을 특징으로 하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 6]** 제1항에 있어서, 상기 (d) 단계는, 입력된 위치값과 허용오차 이내인 통과가 복수인 경우 현재 재생 시점 이후의 다음 통과로 순환 이동하고, 허용오차 이내의 통과가 존재하지 않는 경우 재생 위치를 이동하지 않고 부존재를 통지하는 것을 특징으로 하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 7]** 제1항에 있어서, 상기 이동체의 프레임 번호와 시간의 변환에 사용되는 프레임율을, 로그의 최대 프레임 번호를 영상 길이로 나눈 값에 기초하여 산출하는 것을 특징으로 하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 8] (장치·기록매체)** 제1항 내지 제7항 중 어느 한 항의 방법을 수행하도록 구성된 프로세서 및 메모리를 포함하는 주행영상 탐색·재생 장치, 및 상기 방법을 컴퓨터에 실행시키는 프로그램이 기록된 컴퓨터로 판독 가능한 기록매체.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 2.10 요약서(초안)
|
||||||
|
주행영상 각 프레임의 이동체 위치를 측점 기준선에 수직 투영하여 연속 측점값을 산출하고, 프레임번호·시간·측점값의 매핑을 생성한다. 누적 이동거리를 가로축으로 하는 탐색바로 비이동 구간에서 커서가 정지하며, 진행방향과 시설물을 위치 기준으로 표시하고, 입력 측점값을 시간으로 역산하여 재생 위치를 이동한다. 이로써 시간이 아닌 선로 위치로 영상을 직관적으로 탐색·재생한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 부록 A. 구현 근거(소스 매핑) — 명세서 뒷받침용
|
||||||
|
|
||||||
|
| 발명 구성요소 | 구현 파일·함수 |
|
||||||
|
|---|---|
|
||||||
|
| 위경도→TM, ENU 상대좌표 | [geoProjection.ts](../client/src/utils/geoProjection.ts) `latLonToTM`, `buildRelEnu` |
|
||||||
|
| 회전행렬·카메라좌표 | geoProjection.ts `buildRotation`, `applyRw2c`, `toCameraCoords` |
|
||||||
|
| 핀홀 투영(u,v) | geoProjection.ts `pixelFromCamera`, `projectPoint` |
|
||||||
|
| 프레임 보간 | [StationOverlay.tsx](../client/src/components/overlay/StationOverlay.tsx) `poseAt` |
|
||||||
|
| 평활(이동평균/EMA) | StationOverlay.tsx `smoothFrame`, `smoothStep` |
|
||||||
|
| fps 자동산출 | VideoPlayer.tsx `effectiveFps` |
|
||||||
|
| GPS→측점 수직투영 | [chainage.ts](../client/src/utils/chainage.ts) `projectToChain`, `buildChainLine` |
|
||||||
|
| 시간↔측점 매핑·이동거리축 | [StationBar.tsx](../client/src/stationbar/StationBar.tsx) precompute, `cumFracAtTime`/`timeAtFrac` |
|
||||||
|
| 방향색·시설물 배치 | StationBar.tsx `barSegments`, `structureMarks` |
|
||||||
|
| 측점 검색 | StationBar.tsx `handleJumpToMileage` |
|
||||||
|
|
||||||
|
## 부록 B. 출원 실무 유의사항
|
||||||
|
- **공지/사용 이력 주의**: 이미 공개(시연·논문·웹 게시)한 경우 신규성 상실 우려 → 공개일 확인, 필요 시 공지예외(자기공지) 12개월 이내 출원 검토.
|
||||||
|
- **직무발명**: 회사 소속이면 직무발명 신고·승계 절차 확인.
|
||||||
|
- **명세서 뒷받침**: 각 청구항 구성요소가 실시예(1.7/2.7)에 기재되어야 함(기재불비 방지).
|
||||||
|
- **도면 정식화**: 상기 SVG 개념도를 특허 도면 규격(흑백 선도, 부호 표기)으로 재작성.
|
||||||
|
- **선행기술조사**: KIPRIS/특허문헌에서 "UAV video AR overlay", "georeferencing overlay", "chainage-based video navigation" 등 검색.
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,386 @@
|
|||||||
|
# 특허 출원 초안 — 스테이션 기반 영상 플레이어 (발명 2건)
|
||||||
|
|
||||||
|
> **성격**: 특허 출원을 위한 **기술 정리 초안**입니다. 실제 출원 전 **변리사(특허사무소) 검토**가 반드시 필요합니다.
|
||||||
|
> 청구항은 권리범위를 정하는 핵심이므로 아래 청구항은 **방향 제시용 초안**이며, **선행기술조사(KIPRIS 등)** 후 변리사와 함께 확정하십시오.
|
||||||
|
>
|
||||||
|
> **구성·용어·그림**은 사내 보고서 『스테이션기반 플레이어_260703』(기술개발센터 그래픽스개발팀)을 따랐습니다.
|
||||||
|
>
|
||||||
|
> - **발명 1**: 시간을 스테이션으로 — 측점(스테이션) 기반 영상 탐색·재생 방법 및 장치
|
||||||
|
> - **발명 2**: POI를 영상 속 자리에 — 지리좌표 POI의 영상 내 정합 표시(좌표 투영) 방법 및 장치
|
||||||
|
|
||||||
|
<style>
|
||||||
|
figure.fig{margin:22px 0;text-align:center;page-break-inside:avoid;break-inside:avoid;}
|
||||||
|
figure.fig img{max-width:100%;height:auto;border:1px solid #e7e0d2;border-radius:8px;}
|
||||||
|
figure.fig figcaption{font-size:0.9em;color:#777;margin-top:6px;}
|
||||||
|
.claim{background:#fbfaf6;border:1px solid #e7e0d2;border-radius:6px;padding:10px 14px;margin:8px 0;page-break-inside:avoid;}
|
||||||
|
h2{border-top:3px solid #e7e0d2;padding-top:12px;margin-top:30px;}
|
||||||
|
h3{margin-top:22px;}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 발명의 개요 (프로그램 소개)
|
||||||
|
|
||||||
|
드론이 철길 상공을 비행하며 철도 시설물(교량·터널·역사 등)을 촬영한 **항공영상**을,
|
||||||
|
|
||||||
|
1. 시간(몇 분 몇 초)이 아니라 **스테이션(측점 — 선로 위 km 위치)** 을 기준으로 재생·탐색하고,
|
||||||
|
2. 시설물(POI) 정보를 **영상 속 실제 위치에 정합하여 표출**하는
|
||||||
|
|
||||||
|
**스테이션 기반 영상 플레이어**에 관한 발명이다. 두 기능은 각각 독립된 발명(발명 1, 발명 2)으로 정리한다.
|
||||||
|
|
||||||
|
<figure class="fig">
|
||||||
|
<img src="그림_플레이어비교.png" alt="보통 플레이어(시간 축)와 스테이션 플레이어(측점 축) 비교">
|
||||||
|
<figcaption>[그림 1] 보통 플레이어(시간 축) vs 스테이션 플레이어(측점 축) — 같은 지점을 "3분 12초"가 아닌 "160k"로 지칭·탐색</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 특허 요지 (한눈 요약)
|
||||||
|
|
||||||
|
| 구분 | 발명 1 — 측점 기반 재생 | 발명 2 — POI 영상 정합 표시 |
|
||||||
|
|---|---|---|
|
||||||
|
| 해결 과제 | 시간축 플레이어로는 영상 속 지점이 선로 어디인지 알 수 없음 | 위경도로 기록된 POI를 좌표 개념이 없는 영상 화면에 표시할 수 없음 |
|
||||||
|
| 핵심 수단 | 드론 GPS를 측점 기준선에 **수직 투영**하여 프레임별 측점값 산출 → **시간↔측점 대응표** | POI 좌표를 **높이 보완 → 상대좌표 → 시선 회전 → 핀홀 투영**으로 화면 픽셀 위치로 변환 |
|
||||||
|
| 차별 포인트(신규성 후보) | 누적 이동거리축 탐색바(정지 시 커서 정지) · 전진/후진 색 구분 · 측점 검색·복수 통과 순환 이동 | 최근접 측점 실측 표고로 POI 높이 보완(**DEM 불필요**) · 속도적응 평활 · **드래그 역투영 좌표 보정** |
|
||||||
|
| 청구 형태 | 방법 + 장치 + 기록매체 | 방법 + 장치 + 기록매체 |
|
||||||
|
|
||||||
|
- **출원 형태(변리사 협의)**: (안 A) 2건 개별 출원 — 권리 명확. (안 B) 1건 통합 출원 — "드론 항공영상 처리"의 단일성 인정 여부 검토, 부정 시 분할출원.
|
||||||
|
- 소프트웨어 발명이므로 **방법 + 장치(시스템) + 컴퓨터 판독가능 기록매체** 3종 청구항 세트 권장.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 입력 데이터의 정의 (필요 데이터)
|
||||||
|
|
||||||
|
| 항목 | 내용 | 포맷 | 기록 주체 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 드론 영상 | 드론이 선로를 따라 비행하며 철도 시설물을 촬영한 항공영상 | mp4 | 드론 |
|
||||||
|
| 드론 비행로그 | 프레임별 위치(위도·경도·고도), 카메라 자세각(yaw·pitch·roll), 초점거리 | csv | 드론 |
|
||||||
|
| 측점 데이터 | 측점 목록 — 이정(측점명)·좌표·**실측 표고** | csv | 철도 측량 성과 |
|
||||||
|
| POI·구조물 | 교량·터널·지장물·출입문·철도역의 명칭·좌표·속성 (높이 정보 없음) | kmz | 지도·측량 성과 |
|
||||||
|
| 노선 기본 정보 | 노선명·방향·시점/종점역 정보, 구조물 위치 보정값 (선택) | json | 운영자 입력 |
|
||||||
|
|
||||||
|
드론이 촬영과 동시에 기록하는 데이터(영상·비행로그)와, 드론과 무관한 철도 측량 성과(측점·POI)를 **소프트웨어적으로 결합(믹싱)** 하는 것이 두 발명의 공통 기반이다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 발명 1 — 시간을 스테이션으로: 측점(스테이션) 기반 영상 탐색·재생
|
||||||
|
|
||||||
|
## 4.1 발명의 명칭
|
||||||
|
|
||||||
|
선로 측점(스테이션) 기반의 드론 항공영상 탐색 및 재생 방법, 장치 및 기록매체
|
||||||
|
(영문 예: Chainage(station)-based navigation and playback method and apparatus for UAV railway inspection video)
|
||||||
|
|
||||||
|
## 4.2 기술분야
|
||||||
|
|
||||||
|
본 발명은 선로 등 선형 구조물을 따라 촬영한 드론 항공영상을, 재생 시간이 아닌 **선로상의 위치값(측점, 스테이션)** 을 기준으로 탐색·표시·재생하는 영상 재생 인터페이스 기술에 관한 것이다.
|
||||||
|
|
||||||
|
## 4.3 배경기술
|
||||||
|
|
||||||
|
- 통상의 영상 플레이어는 **시간축**으로만 탐색되어, 특정 재생 시각이 선로상 어느 지점인지 알 수 없다.
|
||||||
|
- 철도 점검 실무는 "몇 분 몇 초"가 아니라 **"몇 측점(예: 160k130)"** 으로 위치를 지칭한다.
|
||||||
|
- 드론이 동일 구간을 왕복하거나 정지(호버링)·선회하는 경우, 시간축 탐색바는 실제 이동과 불일치한다.
|
||||||
|
|
||||||
|
## 4.4 해결하고자 하는 과제
|
||||||
|
|
||||||
|
1. 영상을 **측점(위치)** 으로 탐색·이동할 수 있게 한다.
|
||||||
|
2. 정지·선회 등 **비이동 구간**에서 탐색바 커서가 무의미하게 진행되지 않게 한다.
|
||||||
|
3. 진행 방향(전진/후진)과 통과 시설물을 **위치 기준**으로 표시한다.
|
||||||
|
4. 동일 측점의 복수 통과, 미도착 종점을 합리적으로 처리한다.
|
||||||
|
|
||||||
|
## 4.5 과제의 해결 수단 (요지)
|
||||||
|
|
||||||
|
프레임번호·GPS는 드론이 촬영과 동시에 기록한 데이터이고, 측점은 철도 노선의 측량 데이터이다. 본 발명은 드론 GPS를 **측점 기준선(선로)에 수직 투영**하여 프레임별 측점값을 산출함으로써 두 데이터를 결합하고, **프레임번호↔시간↔측점 대응표**를 생성한다. 탐색바 가로축을 시간이 아닌 **누적 이동거리**로 구성하고, 입력 측점값을 대응표로 역산하여 해당 재생 시점으로 이동한다.
|
||||||
|
|
||||||
|
<figure class="fig">
|
||||||
|
<img src="그림_시간측점대응표_상세.png" alt="시간-측점 대응표 생성 과정: 드론 GPS를 선로 기준선에 수직 투영">
|
||||||
|
<figcaption>[그림 2] 동작 원리 — 드론 GPS를 선로에 수직 투영해 측점값을 산출하고 시간↔측점 대응표를 완성</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
## 4.6 발명을 실시하기 위한 구체적인 내용 (실시예)
|
||||||
|
|
||||||
|
### (1) 측점 기준선(폴리라인) 구성
|
||||||
|
측점 데이터(이정, 좌표)를 이정 오름차순으로 정렬하여 평면직교좌표로 변환하고, 인접 측점을 연결한 폴리라인(기준선)을 구성한다. 각 정점에 이정값(m)을 부여한다.
|
||||||
|
|
||||||
|
### (2) 드론 GPS → 연속 측점값 (수직 투영)
|
||||||
|
프레임의 드론 위치 P를 폴리라인의 각 선분 [A,B]에 수직 투영(수선의 발)한다.
|
||||||
|
|
||||||
|
```
|
||||||
|
t = clamp( ((P−A)·(B−A)) / |B−A|² , 0, 1 )
|
||||||
|
투영점(발끝점) C = A + t·(B−A)
|
||||||
|
측점값 km = km_A + t·(km_B − km_A)
|
||||||
|
```
|
||||||
|
|
||||||
|
최근접 선분을 선택하고 이정을 선형보간하여 **연속 측점값**(10m 이하 해상도)을 얻는다. 드론이 선로 옆을 비행해도 수직 투영점으로 측점을 읽으므로, 100m 단위 최근접 측점 방식과 달리 전진/후진 전환 시점이 실제 이동과 일치한다. 선로 수직 이격거리(offset)도 함께 산출할 수 있다.
|
||||||
|
|
||||||
|
### (3) 프레임번호↔시간↔측점 대응표 생성
|
||||||
|
각 프레임에 대해 (프레임번호, 시간 = 프레임번호 ÷ fps, 측점값)을 산출하여 배열로 사전계산·저장한다. 예: "5760번 프레임 ÷ 30fps = 192초 = 160k130". 이 대응표로 시간과 측점을 상호 변환한다.
|
||||||
|
|
||||||
|
### (4) 누적 이동거리축 탐색바 (스테이션바)
|
||||||
|
측점값을 이동평균으로 평활한 뒤 프레임간 측점 변화량의 절대값을 누적하여 **누적 이동거리**를 얻고, 전체값으로 정규화한 진행도(0~1)를 탐색바 가로축으로 사용한다.
|
||||||
|
- 드론 **정지·선회 구간**: 측점 변화 없음 → 진행도 정체 → **커서 정지**
|
||||||
|
- 이동 구간(전진·후진 무관): 진행도 증가 → 커서 전진
|
||||||
|
- 시간↔진행도는 단조 비감소 관계이므로 이진 탐색으로 상호 변환한다.
|
||||||
|
|
||||||
|
### (5) 진행 방향 색 구분
|
||||||
|
평활 측점값의 증감 추세를 히스테리시스로 판정하여, 증가 구간(전진)과 감소 구간(후진)을 서로 다른 색으로 표시하고 방향 전환점을 표기한다.
|
||||||
|
|
||||||
|
### (6) 시설물의 측점 위치 배치
|
||||||
|
교량·터널·역사 등 시설물을, 해당 측점값에 대응하는 통과 시점의 탐색바 위치에 마커로 배치한다. 동일 시설물을 복수 회 통과하면 통과마다 마커를 배치한다. 역사(驛舍) 마커는 드론의 수직 투영 측점이 시설물 측점과 **허용오차 이내로 일치하는 통과에서만** 표시하여, 좌표만 근접하고 측점이 다른 오표시를 방지한다.
|
||||||
|
|
||||||
|
### (7) 측점 검색 (탐색 이동)
|
||||||
|
입력 측점값 q에 대해, 대응표에서 |측점값−q| 가 허용오차 이내인 연속 프레임 구간(= 1통과)들을 찾아 각 통과의 시점을 산출하고, 현재 재생 시점 이후의 다음 통과로 이동한다(반복 입력 시 순환). 허용오차 내 통과가 없으면 이동하지 않고 "해당 측점 없음"을 통지한다.
|
||||||
|
|
||||||
|
### (8) 영상 fps 자동 산출
|
||||||
|
비행로그의 **최대 프레임번호 ÷ 영상 길이(초)** 로 fps를 산출하고, 표준 fps(23.976/24/25/29.97/30/50/60 등) 중 근접값에 스냅한다. 영상별로 다른 fps에 자동 대응한다.
|
||||||
|
|
||||||
|
### (9) 종점 처리 (선택적 실시예)
|
||||||
|
영상이 종점 측점에 도달하지 못한 경우, 실제 통과 마커는 실제 위치에 유지하고 종점 마커는 탐색바 말단에 '미도착' 표시로 부가한다.
|
||||||
|
|
||||||
|
## 4.7 발명의 효과
|
||||||
|
|
||||||
|
- 시간이 아닌 **측점(위치)** 으로 직관적 탐색 → 철도 점검 효율 향상.
|
||||||
|
- 정지·선회 구간에서 **커서 정지** → 탐색바가 실제 이동을 반영.
|
||||||
|
- 진행 방향·시설물을 위치 기준으로 파악, 복수 통과·미도착 종점의 합리적 처리.
|
||||||
|
- 별도 센서·장비 추가 없이 **드론 기본 기록 + 기존 측량 성과**만으로 구현.
|
||||||
|
|
||||||
|
## 4.8 청구범위 (초안)
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 1] (방법)** 선형 구조물을 촬영한 항공영상을 탐색·재생하는 방법으로서,
|
||||||
|
(a) 상기 항공영상의 프레임에 대응하는 무인비행체의 위치정보를 획득하는 단계;
|
||||||
|
(b) 위치값(측점)이 부여된 복수의 기준점을 연결한 기준선에 상기 위치정보를 수직 투영하여, 해당 프레임의 연속 위치값을 산출하는 단계;
|
||||||
|
(c) 상기 프레임의 시간정보와 상기 연속 위치값을 대응시킨 매핑(대응표)을 생성하는 단계; 및
|
||||||
|
(d) 입력된 위치값에 대응하는 시간을 상기 매핑으로부터 산출하여, 상기 항공영상의 재생 위치를 해당 시간으로 이동시키는 단계;
|
||||||
|
를 포함하는, 항공영상 탐색·재생 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 2]** 제1항에 있어서, 상기 매핑에 기초하여 프레임간 위치값 변화량을 누적한 누적 이동거리를 가로축으로 하는 탐색바를 구성하여, 상기 무인비행체의 비이동 구간에서 상기 탐색바의 커서가 정지하도록 하는 단계를 더 포함하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 3]** 제1항에 있어서, 상기 위치값의 증가 구간과 감소 구간을 판별하여 진행 방향을 서로 다른 시각적 속성으로 표시하는 단계를 더 포함하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 4]** 제1항에 있어서, 시설물의 위치값에 대응하는 통과 시점을 상기 매핑으로부터 산출하여 상기 시설물을 탐색바의 대응 위치에 표시하되, 동일 시설물의 복수 통과에 대해 각각 표시하는 단계를 더 포함하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 5]** 제4항에 있어서, 상기 시설물 중 역사(驛舍)는 상기 무인비행체의 수직 투영 위치값이 상기 역사의 위치값과 허용오차 이내로 일치하는 통과에 대해서만 표시하는 것을 특징으로 하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 6]** 제1항에 있어서, 상기 (d) 단계는, 입력된 위치값과 허용오차 이내인 통과가 복수인 경우 현재 재생 시점 이후의 다음 통과로 순환 이동하고, 허용오차 이내의 통과가 존재하지 않는 경우 재생 위치를 이동하지 않고 부존재를 통지하는 것을 특징으로 하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 7]** 제1항에 있어서, 프레임번호와 시간의 변환에 사용되는 프레임율을, 비행로그의 최대 프레임번호를 영상 길이로 나눈 값에 기초하여 산출하고 사전 정의된 표준 프레임율 중 근접값으로 결정하는 것을 특징으로 하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 8] (장치·기록매체)** 제1항 내지 제7항 중 어느 한 항의 방법을 수행하도록 구성된 프로세서 및 메모리를 포함하는 항공영상 탐색·재생 장치, 및 상기 방법을 컴퓨터에 실행시키는 프로그램이 기록된 컴퓨터로 판독 가능한 기록매체.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 발명 2 — POI를 영상 속 자리에: 지리좌표 POI의 영상 내 정합 표시 (좌표 투영)
|
||||||
|
|
||||||
|
## 5.1 발명의 명칭
|
||||||
|
|
||||||
|
드론 항공영상에 대한 지리정보(POI) 실시간 정합 및 오버레이 표시 방법, 장치 및 기록매체
|
||||||
|
(영문 예: Method and apparatus for real-time geo-registration and overlay of POI onto UAV aerial video)
|
||||||
|
|
||||||
|
## 5.2 기술분야
|
||||||
|
|
||||||
|
본 발명은 드론으로 촬영한 항공영상 위에, 지도상 좌표(위경도)로 기록된 지리정보 객체(POI — 교량, 터널, 지장물, 출입문, 철도역 등)를 촬영 당시의 카메라 기하에 맞추어 **영상 화면의 정확한 픽셀 위치에 실시간으로 겹쳐 표시**하는 영상처리 기술에 관한 것이다.
|
||||||
|
|
||||||
|
## 5.3 배경기술
|
||||||
|
|
||||||
|
- POI는 지도상 좌표(위경도)로 기록되어 있으나 **영상 화면에는 좌표 개념이 없으므로**, "화면의 어느 위치에 해당하는가"로의 변환이 필요하다.
|
||||||
|
- 일반적인 증강현실(AR) 오버레이는 정밀 IMU·짐벌 캘리브레이션이 필요하거나, 실시간 카메라 트래킹(SLAM 등) 연산 부하가 크다.
|
||||||
|
- 지오레퍼런싱(정사영상 생성)은 후처리 연산량이 크고, 재생 중 프레임 단위로 라벨을 정합 표시하기 어렵다.
|
||||||
|
- POI 데이터에는 통상 **높이(표고) 정보가 없어** 3차원 투영이 불가능하고, 외부 수치표고모델(DEM)을 도입하면 데이터 준비 부담이 커진다.
|
||||||
|
|
||||||
|
## 5.4 해결하고자 하는 과제
|
||||||
|
|
||||||
|
1. 별도 마커/실시간 SLAM 없이, **드론 비행로그와 카메라 내부 파라미터만으로** POI를 영상에 정합한다.
|
||||||
|
2. **높이 정보가 없는 POI**를 외부 지형데이터 없이 3차원 투영 가능하게 한다.
|
||||||
|
3. 영상 프레임율과 무관하게 **화면 갱신 주기(예: 60fps)로 매끄럽게** 라벨이 대상에 붙어 이동하게 한다.
|
||||||
|
4. GPS·자세 노이즈에도 라벨이 **떨리지 않고 안정적으로** 표시되게 한다.
|
||||||
|
5. 투영 오차 발생 시 **현장에서 즉시 보정**할 수 있게 한다.
|
||||||
|
|
||||||
|
## 5.5 과제의 해결 수단 (요지)
|
||||||
|
|
||||||
|
① 높이 정보가 없는 POI에 **최근접 측점의 실측 표고**를 부여하고(외부 지형데이터 불필요), ② POI 좌표를 드론 기준 상대위치(전방·측방·상하 거리)로 변환한 뒤, ③ 비행로그의 자세각(yaw·pitch·roll)을 적용해 카메라 시선 기준으로 회전하고, ④ **핀홀 카메라 모델**로 화면 내 위치를 산출한다. 이 계산을 매 프레임 반복하여 드론이 이동·회전해도 POI 표지가 영상 속 실제 위치에 지속 표출되며, 오차 발생 시 표지를 드래그하면 **역투영**으로 좌표가 보정·저장된다.
|
||||||
|
|
||||||
|
<figure class="fig">
|
||||||
|
<img src="그림_POI투영_상세.png" alt="POI 투영 과정: 높이 보완, 좌표 변환, 시선 회전, 핀홀 투영">
|
||||||
|
<figcaption>[그림 3] 동작 원리 — 3차원 POI 위치를 그 순간 카메라가 보는 화면의 픽셀로 변환(①높이 보완 ②좌표 변환 ③시선 회전 ④핀홀 투영)</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
## 5.6 발명을 실시하기 위한 구체적인 내용 (실시예)
|
||||||
|
|
||||||
|
### (1) 입력 데이터
|
||||||
|
- 프레임별 드론 상태: 위도 φ, 경도 λ, 고도 h(타원체고), 자세각 yaw(기수방위)·pitch(틸트)·roll.
|
||||||
|
- 카메라 내부 파라미터: 초점거리 f(35mm 환산), 센서 폭 sW·높이 sH, 주점 보정 (cx0, cy0).
|
||||||
|
- POI: 위경도 (φo, λo), 식별정보(명칭·속성). 표고는 통상 미기록.
|
||||||
|
|
||||||
|
### (2) POI 높이 보완 — 최근접 측점의 실측 표고 사용
|
||||||
|
POI에 높이 정보가 없으므로, 측점 데이터(실측 표고 포함) 중 **POI에 최근접한 측점의 실측 표고**를 해당 POI의 표고로 부여한다. 선로 주변 시설물은 선로 지면 높이에 근접하므로 별도 수치표고모델(DEM) 없이 실용적 정확도를 얻는다. 대상 정표고(EL)에는 지오이드고를 가산하여 드론 고도(타원체고)와 기준면(datum)을 일치시킨다. 필요 시 POI 전용 표고 보정값(오프셋)을 부가한다.
|
||||||
|
|
||||||
|
### (3) 좌표 변환 — 위경도 → 평면직교(TM) → 드론 기준 상대좌표(ENU)
|
||||||
|
지구 곡률로 인해 위경도(각도)로는 거리 계산이 부정확하므로, POI와 드론의 위경도를 **평면직교좌표계(예: EPSG:5186 한국 TM)** 로 변환하여 미터 단위 좌표를 얻고, 드론 기준 상대좌표(동-북-상, ENU)를 산출한다.
|
||||||
|
|
||||||
|
```
|
||||||
|
rel = (E_o − E_d − offX, N_o − N_d − offY, z_o − h_d − offZ)
|
||||||
|
```
|
||||||
|
|
||||||
|
(offX/offY/offZ 는 드론 위치 미세 보정 파라미터. 예: 드론에서 앞 120m, 옆 15m, 아래 30m)
|
||||||
|
|
||||||
|
### (4) 시선 회전 — 자세각으로 회전행렬 구성
|
||||||
|
드론 자세각(yaw, pitch, roll)으로 회전행렬을 구성하고, 상대좌표를 '카메라가 보는 방향' 기준 좌표로 회전한다.
|
||||||
|
|
||||||
|
```
|
||||||
|
R_b2w = Rz(−yaw) · Rx(pitch) · Ry(roll)
|
||||||
|
R_align = [[1,0,0],[0,0,−1],[0,1,0]] (기체좌표 → 카메라좌표 축 정렬)
|
||||||
|
R_w2c = R_align · R_b2wᵀ
|
||||||
|
(Xc, Yc, Zc)ᵀ = R_w2c · rel (Zc > 0 이 카메라 전방)
|
||||||
|
```
|
||||||
|
|
||||||
|
### (5) 핀홀 투영 — 정규화 화면좌표 산출
|
||||||
|
|
||||||
|
```
|
||||||
|
u = 0.5 + cx0 + (Xc / Zc) · (f / sW) (0=좌, 1=우)
|
||||||
|
v = 0.5 + cy0 + (Yc / Zc) · (f / sH) (0=상, 1=하)
|
||||||
|
```
|
||||||
|
|
||||||
|
즉 "화면 위치 = 중앙 + (옆÷앞) × (초점거리÷센서)". 멀수록(Zc↑) 화면 중심에 가깝고 작게 투영되는 원근이 자연히 반영된다. (u,v)가 표시 허용 범위 내이면 해당 위치에 POI 식별정보를 오버레이한다.
|
||||||
|
|
||||||
|
### (6) 프레임간 보간 — 매끄러운 추종
|
||||||
|
영상 프레임율(예: 30fps)보다 화면 갱신 주기(예: 60Hz)가 높을 수 있으므로, 재생 시각에 대응하는 연속 프레임 인덱스에서 인접 프레임의 위치·자세를 선형보간(방위각은 최단각 보간)하여 중간 포즈를 만들고 상기 (3)~(5)를 재수행한다. 라벨이 매 화면 갱신마다 대상에 붙어 매끄럽게 이동한다.
|
||||||
|
|
||||||
|
### (7) 안정화 — 속도적응 평활 및 이상치 거부
|
||||||
|
- 자세각은 인접 프레임 **이동평균**(회전 경계 보존형 적응 창)으로 평활.
|
||||||
|
- 화면좌표는 **속도적응 지수이동평균(EMA)** 으로 평활 — 이동속도가 작을수록(정지·떨림) 강하게, 클수록(실제 이동) 약하게 평활하도록 계수를 가변.
|
||||||
|
- 1프레임에 임계 이상 급이동하는 좌표는 이상치로 보고 일정 횟수 무시(이전 위치 유지).
|
||||||
|
|
||||||
|
### (8) 오차 보정 — 드래그 역투영 (현장 보정)
|
||||||
|
투영 위치가 실제와 어긋나는 경우, 사용자가 화면에서 POI 라벨을 **마우스로 드래그**하면, 드래그된 화면 좌표로부터 현재 카메라 기하를 역산(**역투영**)하여
|
||||||
|
- 카메라 광선과 지면(표고면)의 교차로 POI의 위경도를 갱신하거나,
|
||||||
|
- 위경도는 고정하고 화면 세로 위치에 맞는 표고만 역산 갱신하며,
|
||||||
|
보정된 좌표를 저장하여 이후 재생에 반영한다. 한 번의 드래그로 전 구간 표출이 보정된다.
|
||||||
|
|
||||||
|
### (9) 영상 fps 자동 산출 및 렌더링 분리
|
||||||
|
fps는 비행로그의 최대 프레임번호 ÷ 영상 길이로 산출 후 표준값에 스냅(발명 1과 공통). 표시는 화면 갱신 주기로 캔버스에 수행하고, 가시성·겹침 판정 등 고부하 연산은 유휴 시간에 사전계산하여 분리한다.
|
||||||
|
|
||||||
|
## 5.7 발명의 효과
|
||||||
|
|
||||||
|
- 마커·SLAM·DEM 없이 **비행로그 + 측량 성과만으로** 정합 → 저부하, 오프라인 처리 가능.
|
||||||
|
- 높이 없는 POI도 최근접 측점 실측 표고로 **즉시 3차원 투영** 가능.
|
||||||
|
- 프레임율 무관 매끄러운 라벨 추종, 노이즈에 강인.
|
||||||
|
- 드래그 한 번으로 좌표 보정·저장 → 현장 운용성 우수.
|
||||||
|
- 점검·분석 시 영상 속 지점의 시설물 식별이 즉시 가능.
|
||||||
|
|
||||||
|
## 5.8 청구범위 (초안)
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 1] (방법)** 무인비행체로 촬영한 항공영상에 지리정보 객체(POI)를 표시하는 방법으로서,
|
||||||
|
(a) 상기 항공영상의 프레임에 대응하는 무인비행체의 위치정보(위도·경도·고도) 및 자세정보(방위각·틸트각·롤각)를 획득하는 단계;
|
||||||
|
(b) 상기 POI의 위경도 좌표를 평면직교좌표계로 변환하고, 상기 무인비행체를 기준으로 한 상대좌표를 산출하는 단계;
|
||||||
|
(c) 상기 자세정보로부터 회전행렬을 구성하여 상기 상대좌표를 카메라 좌표계의 좌표로 변환하는 단계;
|
||||||
|
(d) 상기 카메라 좌표계의 좌표를, 초점거리 및 센서 규격을 포함하는 카메라 파라미터에 기초한 핀홀 카메라 모델로 정규화 화면좌표에 투영하는 단계; 및
|
||||||
|
(e) 상기 정규화 화면좌표에 대응하는 화면 위치에 상기 POI의 식별정보를 오버레이하여 표시하는 단계;
|
||||||
|
를 포함하는, 항공영상 지리정보 표시 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 2] (높이 보완)** 제1항에 있어서, 높이 정보가 없는 POI에 대하여, 위치값이 부여된 복수의 측점 중 상기 POI에 최근접한 측점의 실측 표고를 상기 POI의 표고로 부여하고, 정표고에 지오이드고를 가산하여 상기 무인비행체의 고도 기준면과 일치시키는 단계를 더 포함하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 3] (보간)** 제1항에 있어서, 재생 시각에 대응하는 연속 프레임 인덱스에서 인접 프레임의 위치 및 자세를 보간하여 중간 포즈를 산출하고, 화면 갱신 주기마다 상기 (b)~(e)를 수행하여 영상 프레임율보다 높은 빈도로 표시 위치를 갱신하는 것을 특징으로 하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 4] (평활)** 제1항에 있어서, 상기 정규화 화면좌표에 대해, 화면상 이동속도가 작을수록 강하게 평활하고 클수록 약하게 평활하는 속도적응 평활을 적용하여 표시 위치를 안정화하는 단계를 더 포함하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 5] (역투영 보정)** 제1항에 있어서, 표시된 상기 식별정보가 사용자 입력에 의해 화면상에서 이동(드래그)되는 경우, 이동된 화면좌표와 해당 시점의 카메라 기하로부터 역투영 연산을 수행하여 상기 POI의 지리좌표 및 표고 중 적어도 하나를 갱신·저장하는 단계를 더 포함하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 6] (fps 산출)** 제1항에 있어서, 프레임번호와 시간의 변환에 사용되는 프레임율을, 비행로그의 최대 프레임번호를 영상 길이로 나눈 값에 기초하여 산출하고 사전 정의된 표준 프레임율 중 근접값으로 결정하는 것을 특징으로 하는 방법.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim">
|
||||||
|
|
||||||
|
**[청구항 7] (장치·기록매체)** 제1항 내지 제6항 중 어느 한 항의 방법을 수행하도록 구성된 프로세서 및 메모리를 포함하는 항공영상 지리정보 표시 장치, 및 상기 방법을 컴퓨터에 실행시키는 프로그램이 기록된 컴퓨터로 판독 가능한 기록매체.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 전체 시스템 구성 (공통 실시예 — 통합 출원 시 시스템 청구 근거)
|
||||||
|
|
||||||
|
데이터 폴더 1개를 입력하면 드론 기록과 철도 측량 데이터가 **자동 파싱·결합**되어 스테이션 기반 플레이어로 동작한다.
|
||||||
|
|
||||||
|
- **입력**: 데이터 폴더 (드론 생성 데이터 + 철도 측점 데이터)
|
||||||
|
- **처리**: 자동 파싱(인코딩 자동감지·KMZ 해제) 후 ① 시간↔측점 대응표 생성(발명 1) ② 좌표 투영 계산(발명 2)
|
||||||
|
- **출력**: 측점 스테이션바 · 영상 AR 오버레이 · 대용량 영상 재생(2GB+ 스트리밍)
|
||||||
|
|
||||||
|
<figure class="fig">
|
||||||
|
<img src="그림_전체흐름도.png" alt="전체 흐름도: 데이터 폴더 입력, 자동 파싱, 좌표·측점 계산, 화면 출력">
|
||||||
|
<figcaption>[그림 4] 전체 흐름도 — 폴더 1개로 드론 기록과 철도 측량 데이터가 자동 결합되어 측점 기반 플레이어로 동작</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
통합 출원(안 B) 선택 시, 위 구성(자동 파싱부 + 대응표 생성부 + 투영 연산부 + 표시부)을 하나의 **시스템 청구항**으로 묶는 방안을 변리사와 협의한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 요약서 (초안)
|
||||||
|
|
||||||
|
### 발명 1 (측점 기반 재생)
|
||||||
|
드론 항공영상 각 프레임의 GPS 위치를 측점 기준선에 수직 투영하여 연속 측점값을 산출하고, 프레임번호·시간·측점값의 대응표를 생성한다. 누적 이동거리를 가로축으로 하는 탐색바로 비이동 구간에서 커서가 정지하며, 진행 방향과 시설물을 위치 기준으로 표시하고, 입력 측점값을 시간으로 역산하여 재생 위치를 이동한다. 이로써 시간이 아닌 선로 위치(스테이션)로 영상을 직관적으로 탐색·재생한다.
|
||||||
|
|
||||||
|
### 발명 2 (POI 영상 정합 표시)
|
||||||
|
드론 비행로그(위치·자세각·초점거리)와 카메라 파라미터를 이용하여, 높이 정보가 없는 POI에 최근접 측점의 실측 표고를 부여하고, 위경도를 평면직교좌표 및 드론 기준 상대좌표로 변환한 뒤 회전행렬과 핀홀 카메라 모델로 정규화 화면좌표에 투영하여 영상에 식별정보를 정합·오버레이한다. 프레임간 보간과 속도적응 평활로 안정 표시하고, 라벨 드래그 시 역투영으로 좌표를 보정·저장한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 부록 A. 구현 근거 (소스 매핑) — 명세서 뒷받침용
|
||||||
|
|
||||||
|
| 발명 구성요소 | 구현 파일·함수 |
|
||||||
|
|---|---|
|
||||||
|
| GPS→측점 수직 투영, 기준선 구성 | `client/src/utils/chainage.ts` — `projectToChain`, `buildChainLine` |
|
||||||
|
| 시간↔측점 대응표·이동거리축 | `client/src/stationbar/StationBar.tsx` — precompute, `cumFracAtTime`/`timeAtFrac` |
|
||||||
|
| 방향색·시설물 마커 배치 | StationBar.tsx — `barSegments`, `structureMarks` |
|
||||||
|
| 측점 검색 | StationBar.tsx — `handleJumpToMileage` |
|
||||||
|
| 위경도→TM, ENU 상대좌표 | `client/src/utils/geoProjection.ts` — `latLonToTM`, `buildRelEnu` |
|
||||||
|
| 회전행렬·카메라좌표·핀홀 투영 | geoProjection.ts — `buildRotation`, `toCameraCoords`, `projectPoint` |
|
||||||
|
| 역투영(드래그 보정)·표고 역산 | geoProjection.ts — 역투영·표고 역산 함수, `StationOverlay.tsx` 드래그 처리 |
|
||||||
|
| 프레임 보간·평활 | `client/src/components/overlay/StationOverlay.tsx` — `poseAt`, `smoothFrame`, `smoothStep` |
|
||||||
|
| fps 자동 산출 | VideoPlayer.tsx — `effectiveFps` |
|
||||||
|
| POI 표고 보완(최근접 측점) | StationOverlay.tsx — `nearestCL` 등 |
|
||||||
|
|
||||||
|
## 부록 B. 출원 실무 유의사항
|
||||||
|
|
||||||
|
- **공지·사용 이력 주의**: 시연·보고서 배포·웹 게시 등으로 이미 공개된 경우 신규성 상실 우려 → 공개일 확인, 필요 시 **공지예외(자기공지) 12개월 이내** 출원 검토.
|
||||||
|
- **직무발명**: 회사 소속 발명이므로 직무발명 신고·승계 절차 확인.
|
||||||
|
- **명세서 뒷받침**: 각 청구항 구성요소가 실시예(4.6/5.6)에 기재되어야 함(기재불비 방지).
|
||||||
|
- **도면 정식화**: 본 초안의 [그림 1]~[그림 4]는 설명용 컬러 개념도이므로, 출원 시 특허 도면 규격(**흑백 선도, 도면부호 표기**)으로 재작성 필요.
|
||||||
|
- **선행기술조사 키워드**: "UAV video AR overlay", "georeferencing video overlay", "chainage based video navigation", "railway inspection video station", "핀홀 투영 오버레이", "측점 영상 검색" 등.
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,76 @@
|
|||||||
|
# 특허 문서용 서술 — 동작 원리 1·2
|
||||||
|
|
||||||
|
> 보고서 「스테이션 기반 영상 플레이어」(2026-07-03)의 동작 원리 1·2를 **특허 명세서 문체**로 재서술한 것.
|
||||||
|
> 용어는 기존 「특허_GhiVideo_명세서초안」과 일치시킴 (동작 원리 1 ↔ 초안 제2발명, 동작 원리 2 ↔ 초안 제1발명).
|
||||||
|
> 실제 출원 전 변리사 검토 필수. 청구항 확정 시 본 서술의 단계 (a)~(f)가 방법 청구항의 구성요소로 대응됨.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [동작 원리 1] 시간-측점 대응 정보에 기반한 측점 기준 영상 재생
|
||||||
|
|
||||||
|
### 기술 요지
|
||||||
|
|
||||||
|
본 발명의 일 측면은, 무인비행체로 촬영한 노선 영상의 재생 기준을 시간축이 아닌 노선의 측점(체이니지)축으로 전환하는 방법에 관한 것이다. 종래의 영상 재생 장치는 재생 위치를 시간(프레임)으로만 특정하므로, 노선 상 특정 위치(측점)에 대응하는 장면을 탐색하기 위해서는 사용자가 시간과 위치의 관계를 알고 있어야 하는 문제가 있다. 본 발명은 무인비행체가 촬영과 동시에 기록한 프레임별 위치정보와, 노선의 측량 성과인 측점 데이터를 결합하여, 영상의 매 프레임에 대한 시간-측점 대응 정보를 사전 구축함으로써 이 문제를 해결한다(도 1 참조).
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="그림_시간측점대응표_상세.png" style="width:100%" alt="도 1"/>
|
||||||
|
<figcaption><b>도 1.</b> 시간-측점 대응 정보의 생성 — 무인비행체 기록(프레임 번호·위치정보)과 노선 측량 데이터(측점)를 기준선 수직 투영으로 결합하는 과정</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
### 구성 (방법의 단계)
|
||||||
|
|
||||||
|
일 실시예에 따른 측점 기준 영상 재생 방법은 다음 단계를 포함한다.
|
||||||
|
|
||||||
|
- **(a) 기준선 구성 단계**: 노선의 측점 데이터(측점값, 평면좌표, 표고)를 이정 순서로 연결하여 기준 폴리라인을 구성한다.
|
||||||
|
- **(b) 측점값 산출 단계**: 영상의 각 프레임에 대하여, 비행 로그로부터 취득한 무인비행체의 위치좌표를 상기 기준 폴리라인에 수직 투영하고, 투영점의 이정을 해당 프레임의 연속 측점값(체이니지)으로 산출한다. 이에 따라 무인비행체가 노선 측방을 비행하는 경우에도 노선 상의 위치가 일의적으로 결정된다.
|
||||||
|
- **(c) 시간 환산 단계**: 비행 로그의 최대 프레임 번호를 영상의 재생 길이로 나눈 값에 기초하여 프레임율을 산출하고, 각 프레임 번호를 프레임율로 나누어 시간값으로 환산한다. 프레임율을 고정 상수가 아닌 실측 데이터로부터 유도함으로써, 상이한 프레임율의 영상에 대해서도 누적 오차 없이 시간을 산출한다.
|
||||||
|
- **(d) 대응 정보 구축 단계**: 각 프레임의 (시간값, 측점값) 쌍을 사전계산하여 메모리 상의 대응 테이블(색인)로 유지한다. 상기 대응 테이블은 원천 데이터(비행 로그, 측점 데이터)로부터 결정적으로 재생성 가능하므로 별도의 영속 저장을 요하지 아니한다.
|
||||||
|
- **(e) 상호 조회 단계**: 영상 재생 중에는 현재 재생 시각으로 상기 대응 테이블을 조회하여 대응 측점값을 실시간 표출하고, 사용자가 측점값을 입력하면 역방향 조회로 대응 시각을 산출하여 재생 위치를 해당 시각으로 이동시킨다.
|
||||||
|
- **(f) 탐색 인터페이스 구성 단계(선택적)**: 탐색바의 가로축을 시간이 아닌 측점 변화량의 누적치(이동거리)에 비례하도록 구성하여, 무인비행체가 정지(호버)한 구간에서는 탐색 커서가 정지하고 이동 구간에서만 진행하도록 한다. 측점값의 증감 방향에 따라 탐색바의 구간 색상을 구분하여 진행·역행을 시각화하며, 동일 측점을 복수 회 통과한 경우 측점 입력의 반복에 따라 각 통과 시점으로 순환 이동한다. 입력된 측점이 실제 통과 구간에 존재하지 아니하는 경우 이동을 차단하고 부재를 안내한다.
|
||||||
|
|
||||||
|
### 특징적 구성 (종속항 후보)
|
||||||
|
|
||||||
|
1. 상기 (b)의 수직 투영에 의하여, GPS 오차 및 측방 비행에도 불구하고 노선 상 위치가 안정적으로 산출되는 점.
|
||||||
|
2. 상기 (c)의 데이터 기반 프레임율 산출(최대 프레임 번호 ÷ 재생 길이)로 가변·비표준 프레임율 영상에 대응하는 점.
|
||||||
|
3. 상기 (f)의 이동거리 비례 탐색축에 의하여, 시간축 탐색바에서 발생하는 "정지 구간에서도 커서가 진행하는" 표시 왜곡이 제거되는 점.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [동작 원리 2] 지리정보 객체의 영상 내 실시간 정합 표출 (좌표 투영)
|
||||||
|
|
||||||
|
### 기술 요지
|
||||||
|
|
||||||
|
본 발명의 다른 측면은, 평면좌표(위경도)로 기록된 지리정보 객체(POI·구조물)를, 무인비행체로 촬영한 영상 화면 내의 대응 위치에 실시간으로 정합하여 표출하는 방법에 관한 것이다. 지리정보 객체는 통상 높이 정보를 보유하지 아니하고, 영상 화면에는 좌표 개념이 존재하지 아니하므로, 3차원 위치를 촬영 시점 카메라의 화면좌표로 변환하는 구성이 요구된다. 특히 본 발명은 외부 수치표고모델(DEM) 없이 노선 측점의 실측 표고를 이용하여 객체의 높이를 결정하는 점을 특징으로 한다(도 2 참조).
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="그림_좌표투영_상세.png" style="width:100%" alt="도 2"/>
|
||||||
|
<figcaption><b>도 2.</b> 지리정보 객체의 화면좌표 산출 — 높이 보완(측점 실측 표고), 상대좌표 변환, 자세각 회전, 핀홀 투영의 4단계 및 표출 과정</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
### 구성 (방법의 단계)
|
||||||
|
|
||||||
|
일 실시예에 따른 정합 표출 방법은, 영상의 매 화면 갱신주기마다 다음 단계를 수행한다.
|
||||||
|
|
||||||
|
- **(a) 높이 결정 단계**: 높이 정보가 없는 지리정보 객체에 대하여, 상기 객체의 평면좌표에 최근접한 측점의 실측 표고를 지면고도로 부여한다. 이때 측점 표고(정표고)에 지역 지오이드고를 가산하여 무인비행체 고도(타원체고)와 기준면(datum)을 일치시킨다. 이에 따라 외부 수치표고모델의 취득·전처리 없이 노선 인접 지형의 높이가 실측 정밀도로 결정된다.
|
||||||
|
- **(b) 상대좌표 산출 단계**: 객체의 위경도를 평면직교좌표(TM)로 변환한 후, 무인비행체의 위치좌표를 감산하여 무인비행체 기준의 상대좌표(동·북·상하 성분)를 산출한다.
|
||||||
|
- **(c) 카메라 좌표 변환 단계**: 비행 로그의 자세각(yaw·pitch·roll)으로부터 회전행렬을 구성하고, 상기 상대좌표를 카메라 좌표계의 좌표 (Xc, Yc, Zc)로 변환한다. 카메라 후방(Zc≤0)의 객체 및 화각·거리 조건을 벗어나는 객체는 표출 대상에서 제외한다.
|
||||||
|
- **(d) 화면좌표 산출 단계**: 핀홀 카메라 모델에 따라 정규화 화면좌표를 산출한다.
|
||||||
|
u = 0.5 + (Xc/Zc)·(f/Sw), v = 0.5 + (Yc/Zc)·(f/Sh)
|
||||||
|
(f: 초점거리, Sw·Sh: 센서 폭·높이. Zc에 의한 제산이 원근 축소를 구현함)
|
||||||
|
- **(e) 표시 및 안정화 단계**: 산출된 정규화 좌표를 영상 표시영역(비율 유지 크롭 사각형)에 매핑하여 객체 식별정보(명칭 등)를 중첩 표출한다. 위치 신호의 잡음에 대하여, 이동속도가 작을수록 강하게, 클수록 약하게 평활하는 속도적응 평활을 적용하여 표출 위치를 안정화한다. 상기 (a)~(e)는 매 화면 갱신주기(예: 초당 60회) 반복되어, 무인비행체의 이동·회전에 추종한다.
|
||||||
|
- **(f) 역투영 보정 단계(선택적)**: 표출 위치에 오차가 있는 경우, 사용자가 식별정보를 화면 상에서 드래그하는 단일 조작에 의하여, 카메라와 객체 간 사거리(slant range)를 유지하는 조건 하에 상기 (d)의 역변환을 수행함으로써 객체의 3차원 좌표(평면좌표 및 표고)를 동시에 복원·갱신하고, 갱신된 좌표를 데이터셋별로 저장하여 재사용한다.
|
||||||
|
|
||||||
|
### 특징적 구성 (종속항 후보)
|
||||||
|
|
||||||
|
1. 상기 (a)의 측점 실측 표고 활용 및 지오이드고 가산에 의한 datum 정합 — 외부 DEM 불요, 노선 종단 기복의 자동 반영.
|
||||||
|
2. 상기 (f)의 단일 드래그 역투영 — 깊이 정보 없이 사거리 유지 조건으로 2차원 조작에서 3차원 좌표를 복원하는 구성.
|
||||||
|
3. 표시 객체 1점의 드래그로부터 세로 화각(센서 높이 상당값)을 역산하여 영상별 화각을 교정하는 구성(화각 1점 보정).
|
||||||
|
4. 상기 (e)의 속도적응 평활 — 정지·저속에서의 잡음 억제와 고속 이동 시의 무지연 추종의 양립.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 두 동작 원리의 결합 관계 (출원 전략 메모)
|
||||||
|
|
||||||
|
- 동작 원리 1은 "어느 시각이 어느 측점인가"(탐색·색인), 동작 원리 2는 "그 측점·객체가 화면 어디인가"(표출·정합)로서 상호 보완적이며, 하나의 시스템 청구항에서 결합 구성으로 기재 가능함.
|
||||||
|
- 공통 전제 구성: 무인비행체가 촬영과 동시에 기록한 프레임별 위치·자세 로그, 및 노선 측량 성과(측점)의 결합.
|
||||||
|
- 세부 수식·좌표계 정의·도면은 「특허_GhiVideo_명세서초안_좌표투영_측점기반재생」 제1·2발명 실시예 참조.
|
||||||
Binary file not shown.
@@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# fig_POI투영_상세.py — POI 좌표를 영상 속 화면 위치로 변환(투영)하는 과정
|
||||||
|
# 소스 3종(비행로그·POI·측점)이 투영 패널로 모여 단계(①②③④)를 거쳐
|
||||||
|
# 화면 위치가 산출되는 흐름 + 핀홀 투영 미니 일러스트.
|
||||||
|
# 실행: python3 scripts/fig_POI투영_상세.py → docs/그림_POI투영_상세.png / .pdf
|
||||||
|
import math, os
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
OUT_DIR = os.path.join(os.path.dirname(__file__), '..', 'docs')
|
||||||
|
FB = os.path.expanduser('~/.local/share/fonts/malgunbd.ttf')
|
||||||
|
FR = os.path.expanduser('~/.local/share/fonts/malgun.ttf')
|
||||||
|
|
||||||
|
W, H = 2200, 1520
|
||||||
|
im = Image.new('RGB', (W, H), 'white')
|
||||||
|
d = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
GRAY, BLUE, GREEN, AMBER, TEAL = (89, 89, 89), (46, 117, 182), (112, 173, 71), (191, 144, 0), (49, 132, 155)
|
||||||
|
GREEN_BG, PURPLE, NOTE = (240, 247, 234), (124, 58, 237), (102, 102, 102)
|
||||||
|
ORANGE, RED = (234, 88, 12), (220, 38, 38)
|
||||||
|
|
||||||
|
def fit(text, path, size, maxw):
|
||||||
|
while size > 18:
|
||||||
|
f = ImageFont.truetype(path, size)
|
||||||
|
if d.textlength(text, font=f) <= maxw:
|
||||||
|
return f
|
||||||
|
size -= 2
|
||||||
|
return ImageFont.truetype(path, size)
|
||||||
|
|
||||||
|
def box(x1, y1, x2, y2, color, main, sub, subcolor):
|
||||||
|
d.rectangle((x1, y1, x2, y2), fill=color)
|
||||||
|
cx, cy, maxw = (x1 + x2) // 2, (y1 + y2) // 2, (x2 - x1) - 40
|
||||||
|
d.text((cx, cy - 24), main, font=fit(main, FB, 40, maxw), fill='white', anchor='mm')
|
||||||
|
d.text((cx, cy + 32), sub, font=fit(sub, FR, 28, maxw), fill=subcolor, anchor='mm')
|
||||||
|
|
||||||
|
def arrow(x1, y1, x2, y2, w=7):
|
||||||
|
d.line((x1, y1, x2, y2), fill=PURPLE, width=w)
|
||||||
|
ang = math.atan2(y2 - y1, x2 - x1)
|
||||||
|
L, Wd = 32, 19
|
||||||
|
d.polygon([(x2, y2),
|
||||||
|
(x2 - L*math.cos(ang) + Wd*math.sin(ang), y2 - L*math.sin(ang) - Wd*math.cos(ang)),
|
||||||
|
(x2 - L*math.cos(ang) - Wd*math.sin(ang), y2 - L*math.sin(ang) + Wd*math.cos(ang))],
|
||||||
|
fill=PURPLE)
|
||||||
|
|
||||||
|
def dash(x1, y1, x2, y2, color, w=5, seg=14, gap=10):
|
||||||
|
length = math.hypot(x2 - x1, y2 - y1)
|
||||||
|
ux, uy = (x2 - x1) / length, (y2 - y1) / length
|
||||||
|
t = 0
|
||||||
|
while t < length:
|
||||||
|
e = min(t + seg, length)
|
||||||
|
d.line((x1 + ux*t, y1 + uy*t, x1 + ux*e, y1 + uy*e), fill=color, width=w)
|
||||||
|
t = e + gap
|
||||||
|
|
||||||
|
note = ImageFont.truetype(FR, 26)
|
||||||
|
lab = ImageFont.truetype(FB, 28)
|
||||||
|
|
||||||
|
# ── 소스 3종 (드론 기록 vs 지도·측량 성과) ──
|
||||||
|
d.text((470, 48), '드론이 촬영하며 기록', font=note, fill=NOTE, anchor='mm')
|
||||||
|
box(160, 78, 780, 230, GRAY, '드론 비행로그 (csv)', '프레임별 위치 · 자세각 · 초점거리', (232, 232, 232))
|
||||||
|
d.text((1450, 48), '드론과 무관한 지도·측량 성과 (별도 입력)', font=note, fill=NOTE, anchor='mm')
|
||||||
|
box(880, 78, 1460, 230, BLUE, 'POI·구조물 (kmz)', '명칭 · 위경도 (높이 정보 없음)', (219, 233, 247))
|
||||||
|
box(1540, 78, 2060, 230, TEAL, '측점 (csv)', '실측 표고 — POI 높이 보완용', (214, 235, 240))
|
||||||
|
|
||||||
|
# ── 투영 패널 ──
|
||||||
|
d.rectangle((160, 440, 2060, 1130), fill=GREEN_BG, outline=GREEN, width=3)
|
||||||
|
d.rectangle((160, 360, 2060, 440), fill=GREEN)
|
||||||
|
head = '투영 — 3차원 위치를 그 순간 카메라가 보는 화면의 픽셀로 변환'
|
||||||
|
d.text((1110, 400), head, font=fit(head, FB, 36, 1800), fill='white', anchor='mm')
|
||||||
|
|
||||||
|
arrow(470, 230, 610, 352)
|
||||||
|
d.text((430, 300), '위치·자세각·초점거리', font=note, fill=NOTE, anchor='mm')
|
||||||
|
arrow(1170, 230, 1130, 352)
|
||||||
|
d.text((1280, 295), 'POI 위경도', font=note, fill=NOTE, anchor='mm')
|
||||||
|
arrow(1800, 230, 1680, 352)
|
||||||
|
d.text((1880, 300), '실측 표고', font=note, fill=NOTE, anchor='mm')
|
||||||
|
|
||||||
|
# 단계 4개 (패널 좌측)
|
||||||
|
steps = [
|
||||||
|
('① POI 높이 보완 — 최근접 측점 실측 표고 사용', 'POI엔 높이가 없음 → 외부 지형데이터(DEM) 불필요'),
|
||||||
|
('② 좌표 변환 — 드론 기준 상대위치(m)로', '예: 드론에서 앞 120m · 옆 15m · 아래 30m'),
|
||||||
|
('③ 시선 회전 — 자세각(yaw·pitch·roll) 적용', "'카메라가 보는 방향' 기준 좌표로 회전"),
|
||||||
|
('④ 핀홀 투영 — 화면 위치 산출', '화면 위치 = 중앙 + (옆÷앞) × (초점거리÷센서)'),
|
||||||
|
]
|
||||||
|
sy = 480
|
||||||
|
for main, sub in steps:
|
||||||
|
d.rectangle((200, sy, 1180, sy + 130), fill='white', outline=GREEN, width=3)
|
||||||
|
d.text((690, sy + 44), main, font=fit(main, FB, 32, 940), fill=(47, 91, 24), anchor='mm')
|
||||||
|
d.text((690, sy + 94), sub, font=fit(sub, FR, 25, 940), fill=NOTE, anchor='mm')
|
||||||
|
sy += 160
|
||||||
|
|
||||||
|
# 미니 일러스트 (패널 우측) — 카메라 + 화면 + POI + 광선
|
||||||
|
cam = (1330, 830)
|
||||||
|
d.rectangle((cam[0]-55, cam[1]-42, cam[0]+55, cam[1]+42), fill=(30, 30, 30))
|
||||||
|
d.rectangle((cam[0]+55, cam[1]-20, cam[0]+80, cam[1]+20), fill=(30, 30, 30))
|
||||||
|
d.text((cam[0], cam[1]+78), '드론 카메라', font=lab, fill=(30, 30, 30), anchor='mm')
|
||||||
|
|
||||||
|
scr_x = 1560
|
||||||
|
d.rectangle((scr_x-8, 620, scr_x+8, 1000), fill=(180, 190, 200))
|
||||||
|
d.text((scr_x, 1035), '화면(영상 프레임)', font=note, fill=(71, 85, 105), anchor='mm')
|
||||||
|
|
||||||
|
poi = (1965, 545)
|
||||||
|
dash(cam[0]+80, cam[1]-6, poi[0], poi[1], ORANGE, w=6)
|
||||||
|
d.ellipse((poi[0]-13, poi[1]-13, poi[0]+13, poi[1]+13), fill=RED)
|
||||||
|
d.text((1810, 505), '교량 POI (위경도+표고)', font=lab, fill=RED, anchor='mm')
|
||||||
|
|
||||||
|
# 광선과 화면의 교차점(맺힌 점)
|
||||||
|
t = (scr_x - (cam[0]+80)) / (poi[0] - (cam[0]+80))
|
||||||
|
hit = (scr_x, (cam[1]-6) + t * (poi[1] - (cam[1]-6)))
|
||||||
|
d.ellipse((hit[0]-11, hit[1]-11, hit[0]+11, hit[1]+11), fill=ORANGE)
|
||||||
|
d.text((hit[0]+30, hit[1]+40), '맺힌 점 = 화면 속 위치', font=lab, fill=ORANGE, anchor='lm')
|
||||||
|
|
||||||
|
cap = "3차원 위치가 렌즈를 지나 화면에 '맺히는' 자리를 계산 — 원근법 그대로"
|
||||||
|
d.text((1620, 1095), cap, font=fit(cap, FR, 26, 800), fill=NOTE, anchor='mm')
|
||||||
|
|
||||||
|
# ── 하단: 결과 ──
|
||||||
|
box(660, 1210, 1560, 1370, AMBER, '영상 속 정확한 자리에 POI 표출!', '매 프레임 재계산(60fps) → 드론이 움직여도 이름표가 따라붙음', (247, 236, 208))
|
||||||
|
arrow(1110, 1130, 1110, 1202)
|
||||||
|
foot = '※ 위치가 어긋나면 라벨을 마우스로 끌어 한 번에 보정 (역투영 · 자동 저장)'
|
||||||
|
d.text((1110, 1440), foot, font=fit(foot, FR, 28, 1600), fill=NOTE, anchor='mm')
|
||||||
|
|
||||||
|
png = os.path.join(OUT_DIR, '그림_POI투영_상세.png')
|
||||||
|
im.save(png)
|
||||||
|
print('PNG 저장:', png, im.size)
|
||||||
|
|
||||||
|
import fitz
|
||||||
|
doc = fitz.open()
|
||||||
|
rect = fitz.Rect(0, 0, W / 2, H / 2)
|
||||||
|
page = doc.new_page(width=rect.width, height=rect.height)
|
||||||
|
page.insert_image(rect, filename=png)
|
||||||
|
pdf = os.path.join(OUT_DIR, '그림_POI투영_상세.pdf')
|
||||||
|
doc.save(pdf, deflate=True, garbage=4)
|
||||||
|
print('PDF 저장:', pdf)
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# fig_대응표_상세.py — 시간↔측점 대응표 (상세판)
|
||||||
|
# 철도 측량 데이터를 별도 소스 박스로 분리하고, 드론 GPS와 믹싱되는
|
||||||
|
# '선로 수직 투영' 과정을 단계(①②③) + 미니 일러스트로 구체화.
|
||||||
|
# 실행: python3 scripts/fig_대응표_상세.py → docs/그림_시간측점대응표_상세.png / .pdf
|
||||||
|
import math, os
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
OUT_DIR = os.path.join(os.path.dirname(__file__), '..', 'docs')
|
||||||
|
FB = os.path.expanduser('~/.local/share/fonts/malgunbd.ttf')
|
||||||
|
FR = os.path.expanduser('~/.local/share/fonts/malgun.ttf')
|
||||||
|
|
||||||
|
W, H = 2200, 1420
|
||||||
|
im = Image.new('RGB', (W, H), 'white')
|
||||||
|
d = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
GRAY, BLUE, GREEN, AMBER, TEAL = (89, 89, 89), (46, 117, 182), (112, 173, 71), (191, 144, 0), (49, 132, 155)
|
||||||
|
GREEN_BG, PURPLE, NOTE = (240, 247, 234), (124, 58, 237), (102, 102, 102)
|
||||||
|
|
||||||
|
def fit(text, path, size, maxw):
|
||||||
|
while size > 18:
|
||||||
|
f = ImageFont.truetype(path, size)
|
||||||
|
if d.textlength(text, font=f) <= maxw:
|
||||||
|
return f
|
||||||
|
size -= 2
|
||||||
|
return ImageFont.truetype(path, size)
|
||||||
|
|
||||||
|
def box(x1, y1, x2, y2, color, main, sub, subcolor):
|
||||||
|
d.rectangle((x1, y1, x2, y2), fill=color)
|
||||||
|
cx, cy, maxw = (x1 + x2) // 2, (y1 + y2) // 2, (x2 - x1) - 40
|
||||||
|
d.text((cx, cy - 24), main, font=fit(main, FB, 40, maxw), fill='white', anchor='mm')
|
||||||
|
d.text((cx, cy + 32), sub, font=fit(sub, FR, 28, maxw), fill=subcolor, anchor='mm')
|
||||||
|
|
||||||
|
def arrow(x1, y1, x2, y2, w=7):
|
||||||
|
d.line((x1, y1, x2, y2), fill=PURPLE, width=w)
|
||||||
|
ang = math.atan2(y2 - y1, x2 - x1)
|
||||||
|
L, Wd = 32, 19
|
||||||
|
d.polygon([(x2, y2),
|
||||||
|
(x2 - L*math.cos(ang) + Wd*math.sin(ang), y2 - L*math.sin(ang) - Wd*math.cos(ang)),
|
||||||
|
(x2 - L*math.cos(ang) - Wd*math.sin(ang), y2 - L*math.sin(ang) + Wd*math.cos(ang))],
|
||||||
|
fill=PURPLE)
|
||||||
|
|
||||||
|
def dash(x1, y1, x2, y2, color, w=5, seg=14, gap=10):
|
||||||
|
length = math.hypot(x2 - x1, y2 - y1)
|
||||||
|
ux, uy = (x2 - x1) / length, (y2 - y1) / length
|
||||||
|
t = 0
|
||||||
|
while t < length:
|
||||||
|
e = min(t + seg, length)
|
||||||
|
d.line((x1 + ux*t, y1 + uy*t, x1 + ux*e, y1 + uy*e), fill=color, width=w)
|
||||||
|
t = e + gap
|
||||||
|
|
||||||
|
note = ImageFont.truetype(FR, 26)
|
||||||
|
lab = ImageFont.truetype(FB, 28)
|
||||||
|
|
||||||
|
# ── 소스 2종 (드론 기록 vs 측량 성과) ──
|
||||||
|
d.text((550, 48), '드론이 촬영하며 기록', font=note, fill=NOTE, anchor='mm')
|
||||||
|
box(200, 78, 900, 230, GRAY, '영상 프레임 (사진 1장)', '(번호 + 드론 GPS 기록)', (232, 232, 232))
|
||||||
|
d.text((1650, 48), '드론과 무관한 철도 측량 성과 (별도 입력)', font=note, fill=NOTE, anchor='mm')
|
||||||
|
box(1300, 78, 2000, 230, TEAL, '철도 노선 측량 데이터', "측점 목록 — 좌표·실측 표고 (01)측점.csv)", (214, 235, 240))
|
||||||
|
|
||||||
|
# ── 좌: 시간 계산 ──
|
||||||
|
box(140, 400, 700, 560, BLUE, '프레임번호 ÷ fps = 시간', '예: 5760번 ÷ 30 = 192초', (219, 233, 247))
|
||||||
|
arrow(450, 230, 425, 392)
|
||||||
|
d.text((350, 300), '프레임번호', font=note, fill=NOTE, anchor='mm')
|
||||||
|
|
||||||
|
# ── 우: 믹싱 패널 (GPS × 측량 데이터 → 측점) ──
|
||||||
|
d.rectangle((820, 440, 2060, 1090), fill=GREEN_BG, outline=GREEN, width=3)
|
||||||
|
d.rectangle((820, 360, 2060, 440), fill=GREEN)
|
||||||
|
d.text((1440, 400), '믹싱 — 드론 GPS를 선로에 수직 투영해 측점값 산출',
|
||||||
|
font=fit('믹싱 — 드론 GPS를 선로에 수직 투영해 측점값 산출', FB, 36, 1200), fill='white', anchor='mm')
|
||||||
|
arrow(760, 230, 1010, 352)
|
||||||
|
d.text((690, 305), 'GPS (위도·경도)', font=note, fill=NOTE, anchor='mm')
|
||||||
|
arrow(1650, 230, 1580, 352)
|
||||||
|
d.text((1760, 285), '측점 좌표·이정', font=note, fill=NOTE, anchor='mm')
|
||||||
|
|
||||||
|
# 단계 3개 (패널 좌측)
|
||||||
|
steps = [
|
||||||
|
('① 측점들을 이어 선로 기준선 생성', '측량 데이터의 측점을 이정 순서로 연결'),
|
||||||
|
('② 드론 GPS 점을 기준선에 수직 투영', '매 프레임의 GPS 점 → 선로 위 발끝점'),
|
||||||
|
('③ 발끝점의 이정 읽기 = 측점값', '예: 160k130 (프레임마다 산출)'),
|
||||||
|
]
|
||||||
|
sy = 480
|
||||||
|
for main, sub in steps:
|
||||||
|
d.rectangle((860, sy, 1480, sy + 150), fill='white', outline=GREEN, width=3)
|
||||||
|
d.text((1170, sy + 52), main, font=fit(main, FB, 32, 580), fill=(47, 91, 24), anchor='mm')
|
||||||
|
d.text((1170, sy + 106), sub, font=fit(sub, FR, 25, 580), fill=NOTE, anchor='mm')
|
||||||
|
sy += 190
|
||||||
|
|
||||||
|
# 미니 일러스트 (패널 우측) — 선로 + 측점 + 드론 GPS + 수직 투영
|
||||||
|
rail_a, rail_b = (1550, 940), (2000, 690)
|
||||||
|
d.line((*rail_a, *rail_b), fill=(138, 90, 43), width=10)
|
||||||
|
for f, tag in ((0.12, '160k000'), (0.88, '160k200')):
|
||||||
|
px, py = rail_a[0] + (rail_b[0]-rail_a[0])*f, rail_a[1] + (rail_b[1]-rail_a[1])*f
|
||||||
|
d.ellipse((px-8, py-8, px+8, py+8), fill=(107, 79, 42))
|
||||||
|
d.text((px, py + 34), tag, font=note, fill=(107, 79, 42), anchor='mm')
|
||||||
|
gps = (1660, 600)
|
||||||
|
d.ellipse((gps[0]-12, gps[1]-12, gps[0]+12, gps[1]+12), fill=BLUE)
|
||||||
|
d.text((gps[0], gps[1] - 40), '드론 GPS', font=lab, fill=BLUE, anchor='mm')
|
||||||
|
foot = (1778, 813)
|
||||||
|
dash(*gps, *foot, (220, 38, 38))
|
||||||
|
d.ellipse((foot[0]-9, foot[1]-9, foot[0]+9, foot[1]+9), fill=(220, 38, 38))
|
||||||
|
d.text((1800, 905), '발끝점 → 160k130', font=lab, fill=(220, 38, 38), anchor='mm')
|
||||||
|
cap_txt = "선로 옆을 날아도 수직 '발끝점'으로 측점을 읽음"
|
||||||
|
d.text((1770, 1040), cap_txt, font=fit(cap_txt, FR, 26, 540), fill=NOTE, anchor='mm')
|
||||||
|
|
||||||
|
# ── 하단: 대응표 완성 ──
|
||||||
|
box(700, 1180, 1580, 1340, AMBER, '시간 ↔ 측점 대응표 완성!', '"192초 = 160k130" 처럼 서로 변환 가능', (247, 236, 208))
|
||||||
|
arrow(425, 560, 850, 1172)
|
||||||
|
arrow(1440, 1090, 1240, 1172)
|
||||||
|
|
||||||
|
png = os.path.join(OUT_DIR, '그림_시간측점대응표_상세.png')
|
||||||
|
im.save(png)
|
||||||
|
print('PNG 저장:', png, im.size)
|
||||||
|
|
||||||
|
import fitz
|
||||||
|
doc = fitz.open()
|
||||||
|
rect = fitz.Rect(0, 0, W / 2, H / 2)
|
||||||
|
page = doc.new_page(width=rect.width, height=rect.height)
|
||||||
|
page.insert_image(rect, filename=png)
|
||||||
|
pdf = os.path.join(OUT_DIR, '그림_시간측점대응표_상세.pdf')
|
||||||
|
doc.save(pdf, deflate=True, garbage=4)
|
||||||
|
print('PDF 저장:', pdf)
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# fig_전체흐름도.py — 스테이션기반 플레이어 전체 흐름 다이어그램 (보고서 삽입용 PNG/PDF)
|
||||||
|
# 입력(폴더) → 자동 파싱 → 좌표·측점 계산 → 화면 출력, 오피스 단색 스타일.
|
||||||
|
# weasyprint SVG 텍스트 디센더 잘림 이슈 회피를 위해 PIL 직접 드로잉.
|
||||||
|
# 실행: python3 scripts/fig_전체흐름도.py → docs/그림_전체흐름도.png / .pdf
|
||||||
|
import math, os
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
OUT_DIR = os.path.join(os.path.dirname(__file__), '..', 'docs')
|
||||||
|
FB = os.path.expanduser('~/.local/share/fonts/malgunbd.ttf')
|
||||||
|
FR = os.path.expanduser('~/.local/share/fonts/malgun.ttf')
|
||||||
|
|
||||||
|
W, H = 2400, 1100
|
||||||
|
im = Image.new('RGB', (W, H), 'white')
|
||||||
|
d = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
GRAY, BLUE, GREEN, AMBER = (89, 89, 89), (46, 117, 182), (112, 173, 71), (191, 144, 0)
|
||||||
|
PURPLE, CAPTION = (124, 58, 237), (64, 64, 64)
|
||||||
|
|
||||||
|
def fit(text, path, size, maxw):
|
||||||
|
"""maxw 안에 들어올 때까지 폰트 크기를 줄여 반환."""
|
||||||
|
while size > 20:
|
||||||
|
f = ImageFont.truetype(path, size)
|
||||||
|
if d.textlength(text, font=f) <= maxw:
|
||||||
|
return f
|
||||||
|
size -= 2
|
||||||
|
return ImageFont.truetype(path, size)
|
||||||
|
|
||||||
|
def box(x1, y1, x2, y2, color, main, sub, subcolor):
|
||||||
|
d.rectangle((x1, y1, x2, y2), fill=color)
|
||||||
|
cx, cy, maxw = (x1 + x2) // 2, (y1 + y2) // 2, (x2 - x1) - 40
|
||||||
|
d.text((cx, cy - 24), main, font=fit(main, FB, 40, maxw), fill='white', anchor='mm')
|
||||||
|
d.text((cx, cy + 32), sub, font=fit(sub, FR, 28, maxw), fill=subcolor, anchor='mm')
|
||||||
|
|
||||||
|
def arrow(x1, y1, x2, y2):
|
||||||
|
d.line((x1, y1, x2, y2), fill=PURPLE, width=7)
|
||||||
|
ang = math.atan2(y2 - y1, x2 - x1)
|
||||||
|
L, Wd = 34, 20
|
||||||
|
d.polygon([(x2, y2),
|
||||||
|
(x2 - L*math.cos(ang) + Wd*math.sin(ang), y2 - L*math.sin(ang) - Wd*math.cos(ang)),
|
||||||
|
(x2 - L*math.cos(ang) - Wd*math.sin(ang), y2 - L*math.sin(ang) + Wd*math.cos(ang))],
|
||||||
|
fill=PURPLE)
|
||||||
|
|
||||||
|
cap = ImageFont.truetype(FB, 34)
|
||||||
|
d.text((310, 100), '입력 — 데이터 폴더 1개', font=cap, fill=CAPTION, anchor='mm')
|
||||||
|
d.text((880, 100), '① 자동 파싱', font=cap, fill=CAPTION, anchor='mm')
|
||||||
|
d.text((1440, 100), '② 좌표·측점 계산', font=cap, fill=CAPTION, anchor='mm')
|
||||||
|
d.text((2080, 100), '③ 화면 출력', font=cap, fill=CAPTION, anchor='mm')
|
||||||
|
|
||||||
|
# 입력 그룹 (테두리 + 4종)
|
||||||
|
d.rectangle((60, 150, 560, 950), fill=(245, 245, 245), outline=(176, 176, 176), width=3)
|
||||||
|
box(90, 190, 530, 340, GRAY, '영상 (mp4)', '드론 촬영 철도시설물 영상', (232, 232, 232))
|
||||||
|
box(90, 380, 530, 530, GRAY, '드론 비행로그 (csv)', '프레임별 위치·자세각·초점거리', (232, 232, 232))
|
||||||
|
box(90, 570, 530, 720, GRAY, '측점 (csv)', '좌표 · 실측 정표고', (232, 232, 232))
|
||||||
|
box(90, 760, 530, 910, GRAY, 'POI·구조물 (kmz)', '+ 노선 보정 (json·선택)', (232, 232, 232))
|
||||||
|
|
||||||
|
# 파싱
|
||||||
|
box(680, 460, 1080, 640, BLUE, '자동 파싱', '인코딩 자동감지 · KMZ 해제', (219, 233, 247))
|
||||||
|
|
||||||
|
# 계산 2종
|
||||||
|
box(1180, 320, 1700, 480, GREEN, '시간↔측점 대응표 생성', 'GPS를 선로에 투영 → 시간·측점 짝', (230, 242, 220))
|
||||||
|
box(1180, 580, 1700, 740, GREEN, '좌표 투영 계산', '드론 위치·자세각 → 화면 픽셀', (230, 242, 220))
|
||||||
|
|
||||||
|
# 출력 3종
|
||||||
|
box(1820, 210, 2340, 370, AMBER, '측점 스테이션바', '측점축 탐색 · 측점 검색', (247, 236, 208))
|
||||||
|
box(1820, 480, 2340, 640, AMBER, '영상 AR 오버레이', '시설물 정보 실시간 정합 표출', (247, 236, 208))
|
||||||
|
box(1820, 750, 2340, 910, AMBER, '대용량 영상 재생', '2GB+ 스트리밍 (Range/HLS)', (247, 236, 208))
|
||||||
|
|
||||||
|
# 화살표
|
||||||
|
arrow(560, 550, 672, 550) # 입력 → 파싱
|
||||||
|
arrow(1080, 510, 1172, 420) # 파싱 → 대응표
|
||||||
|
arrow(1080, 590, 1172, 640) # 파싱 → 투영
|
||||||
|
arrow(1700, 400, 1812, 305) # 대응표 → 스테이션바
|
||||||
|
arrow(1700, 660, 1812, 575) # 투영 → AR 오버레이
|
||||||
|
# 영상 원본 → 재생 (계산 우회, 하단 엘보 경로)
|
||||||
|
d.line((310, 950, 310, 1030), fill=PURPLE, width=7)
|
||||||
|
d.line((310, 1030, 2080, 1030), fill=PURPLE, width=7)
|
||||||
|
arrow(2080, 1030, 2080, 922)
|
||||||
|
lbl = ImageFont.truetype(FR, 26)
|
||||||
|
d.text((1195, 998), '영상 원본은 계산 없이 곧바로 스트리밍 재생', font=lbl, fill=(102, 102, 102), anchor='mm')
|
||||||
|
|
||||||
|
png = os.path.join(OUT_DIR, '그림_전체흐름도.png')
|
||||||
|
im.save(png)
|
||||||
|
print('PNG 저장:', png, im.size)
|
||||||
|
|
||||||
|
# PDF (이미지 삽입 + 압축)
|
||||||
|
import fitz
|
||||||
|
doc = fitz.open()
|
||||||
|
rect = fitz.Rect(0, 0, W / 2, H / 2)
|
||||||
|
page = doc.new_page(width=rect.width, height=rect.height)
|
||||||
|
page.insert_image(rect, filename=png)
|
||||||
|
pdf = os.path.join(OUT_DIR, '그림_전체흐름도.pdf')
|
||||||
|
doc.save(pdf, deflate=True, garbage=4)
|
||||||
|
print('PDF 저장:', pdf)
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# fig_투영_상세.py — 좌표 투영(POI를 영상 속 정확한 자리에 표출) 상세 도식
|
||||||
|
# 소스 3종(비행로그 / POI·구조물 / 측점 표고) → 투영 4단계 + 핀홀 미니 일러스트 → 표출.
|
||||||
|
# 대응표 상세판(fig_대응표_상세.py)과 같은 오피스 단색 스타일.
|
||||||
|
# 실행: python3 scripts/fig_투영_상세.py → docs/그림_좌표투영_상세.png / .pdf
|
||||||
|
import math, os
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
OUT_DIR = os.path.join(os.path.dirname(__file__), '..', 'docs')
|
||||||
|
FB = os.path.expanduser('~/.local/share/fonts/malgunbd.ttf')
|
||||||
|
FR = os.path.expanduser('~/.local/share/fonts/malgun.ttf')
|
||||||
|
|
||||||
|
W, H = 2200, 1520
|
||||||
|
im = Image.new('RGB', (W, H), 'white')
|
||||||
|
d = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
GRAY, GREEN, AMBER, TEAL = (89, 89, 89), (112, 173, 71), (191, 144, 0), (49, 132, 155)
|
||||||
|
GREEN_BG, PURPLE, NOTE, ORANGE, RED = (240, 247, 234), (124, 58, 237), (102, 102, 102), (230, 126, 34), (220, 38, 38)
|
||||||
|
|
||||||
|
def fit(text, path, size, maxw):
|
||||||
|
while size > 18:
|
||||||
|
f = ImageFont.truetype(path, size)
|
||||||
|
if d.textlength(text, font=f) <= maxw:
|
||||||
|
return f
|
||||||
|
size -= 2
|
||||||
|
return ImageFont.truetype(path, size)
|
||||||
|
|
||||||
|
def box(x1, y1, x2, y2, color, main, sub, subcolor):
|
||||||
|
d.rectangle((x1, y1, x2, y2), fill=color)
|
||||||
|
cx, cy, maxw = (x1 + x2) // 2, (y1 + y2) // 2, (x2 - x1) - 40
|
||||||
|
d.text((cx, cy - 24), main, font=fit(main, FB, 38, maxw), fill='white', anchor='mm')
|
||||||
|
d.text((cx, cy + 32), sub, font=fit(sub, FR, 27, maxw), fill=subcolor, anchor='mm')
|
||||||
|
|
||||||
|
def arrow(x1, y1, x2, y2, w=7):
|
||||||
|
d.line((x1, y1, x2, y2), fill=PURPLE, width=w)
|
||||||
|
ang = math.atan2(y2 - y1, x2 - x1)
|
||||||
|
L, Wd = 32, 19
|
||||||
|
d.polygon([(x2, y2),
|
||||||
|
(x2 - L*math.cos(ang) + Wd*math.sin(ang), y2 - L*math.sin(ang) - Wd*math.cos(ang)),
|
||||||
|
(x2 - L*math.cos(ang) - Wd*math.sin(ang), y2 - L*math.sin(ang) + Wd*math.cos(ang))],
|
||||||
|
fill=PURPLE)
|
||||||
|
|
||||||
|
def dash(x1, y1, x2, y2, color, w=5, seg=14, gap=10):
|
||||||
|
length = math.hypot(x2 - x1, y2 - y1)
|
||||||
|
ux, uy = (x2 - x1) / length, (y2 - y1) / length
|
||||||
|
t = 0
|
||||||
|
while t < length:
|
||||||
|
e = min(t + seg, length)
|
||||||
|
d.line((x1 + ux*t, y1 + uy*t, x1 + ux*e, y1 + uy*e), fill=color, width=w)
|
||||||
|
t = e + gap
|
||||||
|
|
||||||
|
note = ImageFont.truetype(FR, 26)
|
||||||
|
lab = ImageFont.truetype(FB, 28)
|
||||||
|
|
||||||
|
# ── 소스 3종 ──
|
||||||
|
d.text((450, 48), '드론이 촬영하며 기록', font=note, fill=NOTE, anchor='mm')
|
||||||
|
box(140, 78, 760, 230, GRAY, '드론 비행로그 (csv)', '프레임별 위치 · 자세각 · 초점거리', (232, 232, 232))
|
||||||
|
d.text((1480, 48), '드론과 무관한 지도·측량 성과 (별도 입력)', font=note, fill=NOTE, anchor='mm')
|
||||||
|
box(840, 78, 1460, 230, TEAL, 'POI·구조물 (kmz)', '명칭 · 위경도 (높이 정보 없음)', (214, 235, 240))
|
||||||
|
box(1540, 78, 2120, 230, TEAL, '측점 (csv)', '실측 표고 — POI 높이 보완용', (214, 235, 240))
|
||||||
|
|
||||||
|
# ── 투영 패널 ──
|
||||||
|
d.rectangle((140, 460, 2060, 1160), fill=GREEN_BG, outline=GREEN, width=3)
|
||||||
|
d.rectangle((140, 380, 2060, 460), fill=GREEN)
|
||||||
|
hdr = '투영 — 3차원 위치를 그 순간 카메라가 보는 화면의 픽셀로 변환'
|
||||||
|
d.text((1100, 420), hdr, font=fit(hdr, FB, 36, 1800), fill='white', anchor='mm')
|
||||||
|
arrow(450, 230, 560, 372)
|
||||||
|
d.text((325, 300), '위치·자세각·초점거리', font=note, fill=NOTE, anchor='mm')
|
||||||
|
arrow(1150, 230, 1120, 372)
|
||||||
|
d.text((1250, 300), 'POI 위경도', font=note, fill=NOTE, anchor='mm')
|
||||||
|
arrow(1830, 230, 1700, 372)
|
||||||
|
d.text((1900, 300), '실측 표고', font=note, fill=NOTE, anchor='mm')
|
||||||
|
|
||||||
|
# 단계 4개 (패널 좌측)
|
||||||
|
steps = [
|
||||||
|
('① POI 높이 보완 — 최근접 측점 표고 사용', 'POI엔 높이가 없음 → 외부 지형데이터(DEM) 불필요'),
|
||||||
|
('② 좌표 변환 — 드론 기준 상대위치(m)로', '예: 드론에서 앞 120m · 옆 15m · 아래 30m'),
|
||||||
|
('③ 시선 회전 — 자세각(yaw·pitch·roll) 적용', "'카메라가 보는 방향' 기준 좌표로 회전"),
|
||||||
|
('④ 핀홀 투영 — 화면 위치 산출', '화면 위치 = 중앙 + (옆÷앞) × (초점거리÷센서)'),
|
||||||
|
]
|
||||||
|
sy = 500
|
||||||
|
for main, sub in steps:
|
||||||
|
d.rectangle((180, sy, 1080, sy + 130), fill='white', outline=GREEN, width=3)
|
||||||
|
d.text((630, sy + 44), main, font=fit(main, FB, 32, 860), fill=(47, 91, 24), anchor='mm')
|
||||||
|
d.text((630, sy + 94), sub, font=fit(sub, FR, 25, 860), fill=NOTE, anchor='mm')
|
||||||
|
sy += 160
|
||||||
|
|
||||||
|
# 미니 일러스트 (패널 우측) — 카메라 + 화면(이미지 평면) + POI + 광선
|
||||||
|
cam = (1295, 700)
|
||||||
|
d.rectangle((1200, 660, 1290, 740), fill=(64, 64, 64))
|
||||||
|
d.ellipse((cam[0]-14, cam[1]-14, cam[0]+14, cam[1]+14), fill=(32, 32, 32), outline=(150, 150, 150), width=3)
|
||||||
|
d.text((1245, 780), '드론 카메라', font=note, fill=NOTE, anchor='mm')
|
||||||
|
d.rectangle((1395, 500, 1415, 950), fill=(223, 231, 238), outline=(148, 163, 184), width=2)
|
||||||
|
d.text((1405, 990), '화면(영상 프레임)', font=note, fill=NOTE, anchor='mm')
|
||||||
|
poi = (1930, 560)
|
||||||
|
d.ellipse((poi[0]-12, poi[1]-12, poi[0]+12, poi[1]+12), fill=RED)
|
||||||
|
d.text((1890, 512), '교량 POI (위경도+표고)', font=lab, fill=RED, anchor='mm')
|
||||||
|
dash(*poi, *cam, ORANGE)
|
||||||
|
hit = (1405, 676) # 광선이 화면과 만나는 점
|
||||||
|
d.ellipse((hit[0]-10, hit[1]-10, hit[0]+10, hit[1]+10), fill=ORANGE)
|
||||||
|
d.text((1590, 745), '맺힌 점 = 화면 속 위치', font=lab, fill=ORANGE, anchor='mm')
|
||||||
|
cap = "3차원 위치가 렌즈를 지나 화면에 '맺히는' 자리를 계산 — 원근법 그대로"
|
||||||
|
d.text((1570, 1090), cap, font=fit(cap, FR, 26, 800), fill=NOTE, anchor='mm')
|
||||||
|
|
||||||
|
# ── 하단: 표출 ──
|
||||||
|
box(660, 1250, 1540, 1410, AMBER, '영상 속 정확한 자리에 POI 표출!', '매 프레임 재계산(60fps) → 드론이 움직여도 이름표가 따라붙음', (247, 236, 208))
|
||||||
|
arrow(1100, 1160, 1100, 1242)
|
||||||
|
d.text((1100, 1465), '※ 위치가 어긋나면 라벨을 마우스로 끌어 한 번에 보정 (역투영 · 자동 저장)', font=note, fill=NOTE, anchor='mm')
|
||||||
|
|
||||||
|
png = os.path.join(OUT_DIR, '그림_좌표투영_상세.png')
|
||||||
|
im.save(png)
|
||||||
|
print('PNG 저장:', png, im.size)
|
||||||
|
|
||||||
|
import fitz
|
||||||
|
doc = fitz.open()
|
||||||
|
rect = fitz.Rect(0, 0, W / 2, H / 2)
|
||||||
|
page = doc.new_page(width=rect.width, height=rect.height)
|
||||||
|
page.insert_image(rect, filename=png)
|
||||||
|
pdf = os.path.join(OUT_DIR, '그림_좌표투영_상세.pdf')
|
||||||
|
doc.save(pdf, deflate=True, garbage=4)
|
||||||
|
print('PDF 저장:', pdf)
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# fig_플레이어비교.py — 보통 플레이어(시간 축) vs 우리 플레이어(측점 축) 비교 그림 (PNG/PDF)
|
||||||
|
# 원본: docs/쉬운설명_프레임기반에서_측점기반_플레이어.md 1절 인라인 SVG (720×250)
|
||||||
|
# weasyprint SVG 텍스트 디센더 잘림 이슈 회피를 위해 PIL 직접 드로잉 (3배 해상도).
|
||||||
|
# 실행: python3 scripts/fig_플레이어비교.py → docs/그림_플레이어비교.png / .pdf
|
||||||
|
import os
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
OUT_DIR = os.path.join(os.path.dirname(__file__), '..', 'docs')
|
||||||
|
FB = os.path.expanduser('~/.local/share/fonts/malgunbd.ttf')
|
||||||
|
FR = os.path.expanduser('~/.local/share/fonts/malgun.ttf')
|
||||||
|
|
||||||
|
W, H = 2160, 760
|
||||||
|
im = Image.new('RGB', (W, H), 'white')
|
||||||
|
d = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
SLATE = (71, 85, 105) # #475569 제목1
|
||||||
|
GRAY_TXT = (55, 65, 81) # #374151 시간 라벨
|
||||||
|
GRAY_SUB = (107, 114, 128) # #6b7280 캡션
|
||||||
|
GREEN = (22, 101, 52) # #166534 제목2·역명
|
||||||
|
BLUE_LINE = (2, 132, 199) # #0284c7 구조물 마커
|
||||||
|
BLUE_TXT = (3, 105, 161) # #0369a1 구조물 라벨
|
||||||
|
|
||||||
|
# 카드 배경 (문서 figure 스타일: #fffdf8 / #e7e0d2)
|
||||||
|
d.rounded_rectangle((6, 6, W - 7, H - 7), radius=30, fill=(255, 253, 248), outline=(231, 224, 210), width=3)
|
||||||
|
|
||||||
|
# ① 보통 플레이어 (시간 축)
|
||||||
|
d.text((1080, 72), '① 보통 플레이어 (시간 축)', font=ImageFont.truetype(FB, 42), fill=SLATE, anchor='mm')
|
||||||
|
d.rounded_rectangle((120, 114, 2040, 192), radius=18, fill=(229, 231, 235), outline=(156, 163, 175), width=3)
|
||||||
|
d.rounded_rectangle((120, 114, 870, 192), radius=18, fill=(156, 163, 175))
|
||||||
|
d.ellipse((840, 123, 900, 183), fill=(55, 65, 81))
|
||||||
|
lbl = ImageFont.truetype(FR, 34)
|
||||||
|
d.text((135, 240), '0:00', font=lbl, fill=GRAY_TXT, anchor='lm')
|
||||||
|
d.text((870, 240), '03:12', font=lbl, fill=GRAY_TXT, anchor='mm')
|
||||||
|
d.text((2025, 240), '09:28', font=lbl, fill=GRAY_TXT, anchor='rm')
|
||||||
|
d.text((1080, 314), '"3분 12초 지점" — 여기가 어느 선로인지 모름', font=lbl, fill=GRAY_SUB, anchor='mm')
|
||||||
|
|
||||||
|
# ② 우리 플레이어 (측점 축)
|
||||||
|
d.text((1080, 450), '② 우리 플레이어 (측점 축)', font=ImageFont.truetype(FB, 42), fill=GREEN, anchor='mm')
|
||||||
|
d.rounded_rectangle((120, 492, 2040, 570), radius=18, fill=(253, 233, 200), outline=(224, 169, 79), width=3)
|
||||||
|
d.rounded_rectangle((120, 492, 1050, 570), radius=18, fill=(245, 158, 11))
|
||||||
|
# 구조물 마커 (회덕터널 / 회덕천교)
|
||||||
|
mk = ImageFont.truetype(FR, 30)
|
||||||
|
for x, name in ((615, '회덕터널'), (1440, '회덕천교')):
|
||||||
|
d.line((x, 478, x, 584), fill=BLUE_LINE, width=6)
|
||||||
|
d.text((x, 632), name, font=mk, fill=BLUE_TXT, anchor='mm')
|
||||||
|
# 현재 위치 배지 (160k)
|
||||||
|
d.ellipse((1014, 495, 1086, 567), fill=(234, 88, 12))
|
||||||
|
d.text((1050, 531), '160k', font=ImageFont.truetype(FB, 28), fill='white', anchor='mm')
|
||||||
|
# 시점/종점 역명
|
||||||
|
st = ImageFont.truetype(FR, 34)
|
||||||
|
d.text((135, 690), '대전조차장', font=st, fill=GREEN, anchor='lm')
|
||||||
|
d.text((2025, 690), '신탄진', font=st, fill=GREEN, anchor='rm')
|
||||||
|
|
||||||
|
png = os.path.join(OUT_DIR, '그림_플레이어비교.png')
|
||||||
|
im.save(png)
|
||||||
|
print('PNG 저장:', png, im.size)
|
||||||
|
|
||||||
|
# PDF (이미지 삽입 + 압축)
|
||||||
|
import fitz
|
||||||
|
doc = fitz.open()
|
||||||
|
rect = fitz.Rect(0, 0, W / 2, H / 2)
|
||||||
|
page = doc.new_page(width=rect.width, height=rect.height)
|
||||||
|
page.insert_image(rect, filename=png)
|
||||||
|
pdf = os.path.join(OUT_DIR, '그림_플레이어비교.pdf')
|
||||||
|
doc.save(pdf, deflate=True, garbage=4)
|
||||||
|
print('PDF 저장:', pdf)
|
||||||
Reference in New Issue
Block a user