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