데이터 파이프라인 — 전 구간 프레임·djmd dt 전파·세그먼트 시크·선형 곡선 평활
- geoData: fullFrames(전체 비행 프레임 — 전 구간 측점 검색용, datum·초점 보정 동일 적용), DJI_VIRTUAL_FPS export, 선형 곡선 평활(Centripetal Catmull-Rom — 정점 ~6m 세분, 원 정점 전부 통과로 측점 정합 유지, 60m+ 직선·양끝 제외) - djmdTrack: refineFramesWithDjmd 가 교차상관 dt(초)도 반환 - geoStore: fullFrames·djmdDtSec 상태 — 검색의 CSV↔영상 시간 변환에 사용 - useVideoPlayer.loadLocalFile(startAtSec): 로드 후 지정 시각부터 시작 - VideoPlayer.handleSeekSegment: 측점 검색의 타 세그먼트 점프(전환+시크, 재생 상태 유지), station_lookahead 등 RouteInfo 타입 문서화 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -234,7 +234,7 @@ export async function refineFramesWithDjmd(
|
||||
videoFile: File,
|
||||
frames: DroneFrame[],
|
||||
useEllipsoidal: boolean,
|
||||
): Promise<DroneFrame[] | null> {
|
||||
): Promise<{ frames: DroneFrame[]; dtSec: number } | null> {
|
||||
if (!frames.length) return null;
|
||||
const samples = await parseDjmdPositions(videoFile);
|
||||
if (!samples) return null;
|
||||
@@ -306,7 +306,7 @@ export async function refineFramesWithDjmd(
|
||||
console.log(`[djmd] CSV↔영상 시간 오프셋 ${bestDt.toFixed(2)}s 추정 (잔차 ${bestC.toFixed(1)}m) — 자세 재정렬 적용`);
|
||||
}
|
||||
|
||||
return frames.map((f) => {
|
||||
const refined = frames.map((f) => {
|
||||
const p = at(f.frame); // 위치/고도: djmd (프레임 동기)
|
||||
const a = csvAt(f.frame / FPS + bestDt); // 자세: CSV 를 추정 오프셋으로 재시각화
|
||||
return {
|
||||
@@ -315,4 +315,7 @@ export async function refineFramesWithDjmd(
|
||||
yaw: a.yaw, pitch: a.pitch, roll: a.roll,
|
||||
};
|
||||
});
|
||||
// dtSec: CSV↔영상 시간 오프셋 — 영상시간 = CSV시간 − dtSec.
|
||||
// 측점 검색(CSV 축 인덱스)이 영상 시각으로 변환할 때 사용(0.33s ≈ 2m 오차 제거).
|
||||
return { frames: refined, dtSec: bestDt };
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ export function deriveBaseName(videoFile: File | null): string | null {
|
||||
* 같은 값을 역산해 시간으로 되돌리므로 상수 자체는 임의여도 정합이 유지된다.
|
||||
* 59.94 는 이 데이터셋(DJI 4K60) 실제 fps 와 일치시켜 HUD 표기도 자연스럽게 한 것.
|
||||
*/
|
||||
const DJI_VIRTUAL_FPS = 59.94;
|
||||
export const DJI_VIRTUAL_FPS = 59.94;
|
||||
|
||||
/** 영상 세그먼트 창 — DJI 시간 기반 로그를 특정 영상 구간에 정렬할 때 사용. */
|
||||
export interface SegmentWindow {
|
||||
@@ -690,9 +690,63 @@ export async function parseKmz(
|
||||
}
|
||||
}
|
||||
|
||||
// 곡선 평활 — 측점 정점 삽입/정비가 끝난 최종 선형에 적용(원 정점 전부 보존).
|
||||
if (centerline.length >= 4) {
|
||||
const before = centerline.length;
|
||||
centerline = smoothCenterlineCR(centerline);
|
||||
if (centerline.length !== before) {
|
||||
console.log(`[KML] 선형 곡선 평활(Catmull-Rom): ${before} → ${centerline.length}점`);
|
||||
}
|
||||
}
|
||||
|
||||
return { pois, structures, stations, centerline };
|
||||
}
|
||||
|
||||
/**
|
||||
* 선형 곡선 평활 — Centripetal Catmull-Rom 으로 정점 사이를 세분(모든 원 정점 통과).
|
||||
* 곡선부(정점 ~20m 간격)의 직선 연결이 화면에서 각져 보이는 것을 해소한다.
|
||||
* 측점 삽입 정점도 그대로 통과하므로 선형-측점 정합 유지. 60m 이상 장대 세그먼트
|
||||
* (직선 구간)와 양끝 세그먼트는 세분하지 않는다(불필요·경계 불안정 방지).
|
||||
*/
|
||||
function smoothCenterlineCR(pts: CenterlinePoint[]): CenterlinePoint[] {
|
||||
if (pts.length < 4) return pts;
|
||||
const k = Math.cos((pts[0].lat * Math.PI) / 180) * 111320;
|
||||
const dist = (a: CenterlinePoint, b: CenterlinePoint): number =>
|
||||
Math.hypot((b.lon - a.lon) * k, (b.lat - a.lat) * 111320);
|
||||
const out: CenterlinePoint[] = [];
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const p1 = pts[i], p2 = pts[i + 1];
|
||||
out.push(p1);
|
||||
if (i === 0 || i === pts.length - 2) continue; // 양끝 세그먼트는 직선 유지
|
||||
const len = dist(p1, p2);
|
||||
if (len < 2 || len >= 60) continue; // 중복/장대 세그먼트 제외
|
||||
const p0 = pts[i - 1], p3 = pts[i + 2];
|
||||
const sub = Math.min(6, Math.max(2, Math.ceil(len / 6)));
|
||||
const t0 = 0;
|
||||
const t1 = t0 + Math.max(0.7, Math.sqrt(dist(p0, p1)));
|
||||
const t2 = t1 + Math.max(0.7, Math.sqrt(len));
|
||||
const t3 = t2 + Math.max(0.7, Math.sqrt(dist(p2, p3)));
|
||||
const bg = (c0: number, c1: number, c2: number, c3: number, t: number): number => {
|
||||
const A1 = ((t1 - t) * c0 + (t - t0) * c1) / (t1 - t0);
|
||||
const A2 = ((t2 - t) * c1 + (t - t1) * c2) / (t2 - t1);
|
||||
const A3 = ((t3 - t) * c2 + (t - t2) * c3) / (t3 - t2);
|
||||
const B1 = ((t2 - t) * A1 + (t - t0) * A2) / (t2 - t0);
|
||||
const B2 = ((t3 - t) * A2 + (t - t1) * A3) / (t3 - t1);
|
||||
return ((t2 - t) * B1 + (t - t1) * B2) / (t2 - t1);
|
||||
};
|
||||
for (let j = 1; j < sub; j++) {
|
||||
const t = t1 + ((t2 - t1) * j) / sub;
|
||||
out.push({
|
||||
lat: bg(p0.lat, p1.lat, p2.lat, p3.lat, t),
|
||||
lon: bg(p0.lon, p1.lon, p2.lon, p3.lon, t),
|
||||
z: bg(p0.z, p1.z, p2.z, p3.z, t),
|
||||
});
|
||||
}
|
||||
}
|
||||
out.push(pts[pts.length - 1]);
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── 카메라 파라미터 파일 (<base>.camera.json) ─────────────────────────
|
||||
|
||||
/**
|
||||
@@ -936,8 +990,10 @@ export async function loadFolderGeoData(
|
||||
);
|
||||
}
|
||||
|
||||
const [frames, stationResult, routeMeta, poiOverrides, kmz, cameraInfo, cameraJson, displayJson] = await Promise.all([
|
||||
const [frames, fullFrames, stationResult, routeMeta, poiOverrides, kmz, cameraInfo, cameraJson, displayJson] = await Promise.all([
|
||||
parseDroneFrames(files, baseName, segmentWindow),
|
||||
// 전체 비행(모든 세그먼트) 프레임 — 측점 '전 구간' 검색용(표시용 frames 와 별개)
|
||||
parseDroneFrames(files, baseName, null),
|
||||
parseStations(files),
|
||||
parseRouteMeta(files, baseName),
|
||||
parsePoiOverrides(files, baseName),
|
||||
@@ -968,6 +1024,7 @@ export async function loadFolderGeoData(
|
||||
if (isFinite(geoidN) && Math.abs(geoidN) < 60) {
|
||||
altitudeOffsetM = Math.round(geoidN * 100) / 100;
|
||||
for (const f of frames) f.altitude += altitudeOffsetM;
|
||||
for (const f of fullFrames) f.altitude += altitudeOffsetM;
|
||||
console.log(
|
||||
`[geo] 타원체고 기준 데이터셋 — 드론 고도를 타원체고로 변환 (+${altitudeOffsetM.toFixed(2)}m = ` +
|
||||
`djmd ${cameraInfo.ellipsoidalAltM.toFixed(1)} − CSV ${(frames[0].altitude - altitudeOffsetM).toFixed(1)})`,
|
||||
@@ -978,6 +1035,7 @@ export async function loadFolderGeoData(
|
||||
// 감지된 초점거리를 드론 프레임 focalLen 에도 반영 (검색/투영 FOV 판정 일관성).
|
||||
if (cameraInfo?.focalLen35) {
|
||||
for (const f of frames) f.focalLen = cameraInfo.focalLen35;
|
||||
for (const f of fullFrames) f.focalLen = cameraInfo.focalLen35;
|
||||
console.log(
|
||||
`[camera] ${cameraInfo.model} 감지 — focal ${cameraInfo.focalLen35}mm` +
|
||||
(cameraInfo.width ? `, ${cameraInfo.width}x${cameraInfo.height}@${cameraInfo.fps ?? '?'}fps` : ''),
|
||||
@@ -1047,6 +1105,7 @@ export async function loadFolderGeoData(
|
||||
videoFile,
|
||||
baseName,
|
||||
frames,
|
||||
fullFrames,
|
||||
pois,
|
||||
stations,
|
||||
centerline,
|
||||
|
||||
Reference in New Issue
Block a user