Files
GhiVideo/scripts/fig_대응표_상세.py
b23042andClaude Opus 4.8 fe25ce00f1 docs: 발표·특허·AI영상요약 자료 및 작업기록 추가
- 발표자료(좌표투영·측점기반재생) 갱신 + 그림/PDF
- 특허 출원 초안 및 도면
- AI 영상요약 서비스 작업기록(docs/history)
- 관련 scripts 추가

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 11:20:03 +09:00

123 lines
5.8 KiB
Python

#!/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)