docs: 발표·특허·AI영상요약 자료 및 작업기록 추가

- 발표자료(좌표투영·측점기반재생) 갱신 + 그림/PDF
- 특허 출원 초안 및 도면
- AI 영상요약 서비스 작업기록(docs/history)
- 관련 scripts 추가

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 11:20:03 +09:00
co-authored by Claude Opus 4.8
parent 5c8b499979
commit fe25ce00f1
78 changed files with 6186 additions and 15 deletions
+131
View File
@@ -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)
+122
View File
@@ -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)
+94
View File
@@ -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)
+121
View File
@@ -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)
+67
View File
@@ -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)