Files
GhiVideo_v4/client/src/utils/cameraMeta.ts
T
b23042andClaude Fable 5 7d51ba9945 표고 체계 정비·설정 파일 단순화·선형 클리핑·시연 준비
- 타원체고 기준 자동 변환: route.json ellipsoidal_height 선언 시 드론 고도(CSV 정표고)를
  영상 메타(djmd 6.6 타원체고)와의 차(지오이드고 자동 산출)만큼 보정, 세그먼트 전환에도 유지
- KML 표고 파이프라인: 좌표 고도(z) 사용(POI/측점), *_byDem.kml 자동 생성(DEM 조회·높이 속성 포함)
  + 로더의 _byDem 우선 규칙, 선형중심선(LineString) 로딩·경로표고 앵커
- 설정 파일: camera.json/display.json/route.json 단순 이름 인식, 화면표시 옵션 PC 저장/자동 적용,
  초기화 버튼을 환경설정 파일 기준 복원으로 변경, 저장 파일명 단순화
- 선형 표시: 표출된 마지막 측점까지 클립(경계 세그먼트 절단 + 관통 현 처리 — 구간 실종 수정)
- 스테이션바: 시점(0k000) 마크(실통과 위치), 노선밖 프레임 통과 오인 방지, 커서 배지 1m 단위,
  배너 연장/소요 실측값(측점 체이니지·영상 길이 합) 우선
- 세그먼트 전환 라벨 깜빡임 구조 수정(캐시 초기화·최근접 키 조회·이중 재계산 제거) +
  사전계산 프레임 불변값 호이스팅(재생 지연 해소)
- UI: 슬라이더/값 입력칸 휠 조절, 패널 버튼 위치 정렬, 저장 버튼 중복 제거, dev_mode(route.json)로
  영상제어 UI 표시 제어, 지오이드 최대 200, 이격거리 측점 동반 적용
- 시연: start-demo.bat + docs/DEMO.md
- docs/history: 작업 이력 30건

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 20:37:46 +09:00

114 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* DJI 영상 카메라 메타데이터 자동 감지
*
* DJI 드론 MP4 에는 'djmd' 데이터 트랙(프레임별 protobuf 텔레메트리)이 mdat 에
* 비디오 프레임과 인터리브되어 있다. 파일 앞부분(8MB)에서 첫 djmd 샘플을 찾아 추출:
* - 카메라 모델 문자열 (필드 1.1.2, 예: "DJI ZenmuseP1")
* - 렌즈 초점거리 (필드 8.6, 0.1mm 단위 35mm 환산)
* - 해상도/fps (필드 1.7/1.6/1.8)
*
* 실측 검증(2026-07-09, 제주 데이터): DJI_20260629121835_0002.MP4 →
* model "DJI ZenmuseP1", 8.6=299(=29.9mm — DL 24mm 렌즈 × 4K 비디오 크롭 ≈1.25),
* 3840×2160, 59.94fps. (djmd 필드 6.4/6.5 는 위경도 라디안, 6.6 은 타원체고 mm)
*/
import type { DetectedCameraInfo } from '../types/geo';
const HEAD_BYTES = 8 * 1024 * 1024;
function readVarint(b: Uint8Array, i: number): [number, number] | null {
let v = 0, s = 0;
while (i < b.length && (b[i] & 0x80)) {
v += (b[i] & 0x7f) * 2 ** s;
s += 7; i++;
if (s > 42) return null;
}
if (i >= b.length) return null;
v += (b[i] & 0x7f) * 2 ** s;
return [v, i + 1];
}
/** [start,end) 구간을 protobuf 필드로 파싱해 경로("8.6")→값 수집. 깨진 구간은 조용히 중단. */
function parseFields(
b: Uint8Array, dv: DataView,
start: number, end: number,
prefix: string, out: Map<string, number>,
): void {
let i = start;
while (i < end) {
const kv = readVarint(b, i);
if (!kv) return;
const [key, ni] = kv;
i = ni;
const tag = key >> 3, wire = key & 7;
if (tag === 0 || tag > 60) return;
const path = prefix + tag;
if (wire === 0) {
const r = readVarint(b, i); if (!r) return;
out.set(path, r[0]); i = r[1];
} else if (wire === 5) {
if (i + 4 > end) return;
out.set(path, dv.getFloat32(i, true)); i += 4;
} else if (wire === 1) {
if (i + 8 > end) return;
out.set(path, dv.getFloat64(i, true)); i += 8;
} else if (wire === 2) {
const r = readVarint(b, i); if (!r) return;
const [len, p] = r;
if (p + len > end) return;
// 하위 메시지 시도 — 문자열/바이너리면 내부에서 알아서 중단됨(경로 prefix 로 충돌 없음)
parseFields(b, dv, p, p + len, path + '.', out);
i = p + len;
} else return;
}
}
/** 영상 File 앞부분에서 DJI djmd 카메라 정보를 감지한다. DJI 영상이 아니면 null. */
export async function detectCameraInfo(file: File): Promise<DetectedCameraInfo | null> {
try {
const buf = new Uint8Array(await file.slice(0, HEAD_BYTES).arrayBuffer());
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
// 모델 문자열: 0x12 <len> "DJI ..." (필드 1.1.2)
let m = -1;
for (let i = 2; i < buf.length - 4; i++) {
if (buf[i] === 0x44 && buf[i + 1] === 0x4a && buf[i + 2] === 0x49 && buf[i + 3] === 0x20 &&
buf[i - 2] === 0x12 && buf[i - 1] >= 4 && buf[i - 1] <= 40) { m = i; break; }
}
if (m < 0) return null;
const model = new TextDecoder().decode(buf.slice(m, m + buf[m - 1]));
// 샘플 시작(최상위 필드1, 0x0a <varint len> 0x0a ...)을 모델 문자열에서 역방향 탐색
let sampleStart = -1;
for (let back = 4; back <= 16; back++) {
const i = m - 2 - back;
if (i < 0) break;
if (buf[i] !== 0x0a) continue;
const r = readVarint(buf, i + 1);
if (!r) continue;
const [len, p] = r;
if (buf[p] === 0x0a && p < m && p + len > m && p + len <= buf.length) { sampleStart = i; break; }
}
if (sampleStart < 0) return { model };
const out = new Map<string, number>();
parseFields(buf, dv, sampleStart, Math.min(buf.length, sampleStart + 512), '', out);
const info: DetectedCameraInfo = { model };
const focal10 = out.get('8.6');
if (focal10 !== undefined && focal10 > 80 && focal10 < 3000) info.focalLen35 = focal10 / 10; // 8~300mm 만 신뢰
const w = out.get('1.7'), h = out.get('1.6'), fps = out.get('1.8');
if (w && w > 100) info.width = w;
if (h && h > 100) info.height = h;
if (typeof fps === 'number' && fps > 1 && fps < 1000) info.fps = Math.round(fps * 100) / 100;
// 6.6 = 드론 타원체고(mm) — 영상 시작 시점. CSV 정표고와의 차 = 지역 지오이드고(datum 변환용).
const altMm = out.get('6.6');
if (typeof altMm === 'number' && altMm > 0 && altMm < 10_000_000) {
info.ellipsoidalAltM = Math.round(altMm) / 1000;
}
return info;
} catch {
return null;
}
}