GhiVideo 소스 복제 — v4 작업 시작 기준
기존 GhiVideo 저장소 HEAD의 트래킹 소스 362개 파일을 복제. (node_modules·storage·빌드 산출물·대용량 미디어는 .gitignore 규칙대로 제외) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user