2026-05-26 강의 정리
이미지-텍스트 통합 처리 개요
목차
- 문제와 접근
- 이미지 data URI 변환
- 단일 이미지 Vision 호출
- build_vision_message
- 차트 Markdown 표 추출
- image_type별 JSON 추출
- 대시보드 멀티턴 분석
- 두 이미지 비교
- 핵심 개념 총정리
- 자주 나오는 실수/주의사항
- 이미지 분석 결과의 확인 가능성
1. 문제와 접근
Vision 모델로 이미지를 읽을 때는 관찰한 내용과 해석한 내용을 구분하는 출력 형식이 필요하다. 차트·대시보드·이미지 비교를 예제로 입력 메시지를 구성하고, 추출한 수치와 문장을 원본으로 확인하는 흐름을 살펴본다. 이미지 처리 편의성과 결과 검증 가능성을 함께 고려한다.
1
2
3
4
5
6
7
8
9
10
11
import io
import base64
import json
from pathlib import Path
from PIL import Image
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
vision = ChatOpenAI(model="gpt-4o-mini", max_tokens=500)
IMAGE_DIR = Path("data/images")
2. 이미지 data URI 변환
이미지를 base64로 인코딩해서 Vision 모델 메시지에 넣는다.
1
2
3
4
5
6
7
8
9
def image_to_data_uri(image_or_path) -> str:
if isinstance(image_or_path, (str, Path)):
img = Image.open(image_or_path)
else:
img = image_or_path
buf = io.BytesIO()
img.save(buf, format="PNG")
return f"data:image/png;base64,{base64.b64encode(buf.getvalue()).decode()}"
3. 단일 이미지 Vision 호출
1
2
3
4
5
6
7
8
9
10
img = Image.open(IMAGE_DIR / "ir_report_page.png")
data_uri = image_to_data_uri(img)
msg = HumanMessage(content=[
{"type": "text", "text": "이 IR 보고서의 핵심 지표 3개를 한 줄씩 알려줘"},
{"type": "image_url", "image_url": {"url": data_uri, "detail": "high"}},
])
r = vision.invoke([msg])
r.content
4. build_vision_message
여러 이미지를 한 번에 보낼 수 있게 메시지 생성 함수를 만든다.
1
2
3
4
5
6
def build_vision_message(text: str, images: list, detail: str = "auto") -> HumanMessage:
content = [{"type": "text", "text": text}]
for img in images:
uri = image_to_data_uri(img)
content.append({"type": "image_url", "image_url": {"url": uri, "detail": detail}})
return HumanMessage(content=content)
5. 차트 Markdown 표 추출
차트에서 추출한 표는 축 단위, 범례, 기간을 함께 확인한다. 값이 잘 보이지 않는 경우를 임의 추정치와 구분할 수 있도록 출력 규칙을 정하고, 원본 이미지와 대응되는 항목을 남겨 결과를 검토한다.
차트를 보고 표와 추세 요약을 생성한다.
1
2
3
4
5
6
7
8
9
10
11
chart = Image.open(IMAGE_DIR / "chart_monthly_sales.png")
prompts = [
"이 이미지를 설명해줘",
"이 막대 차트의 각 막대 값을 `| 월 | 값` Markdown 표로. 마지막에 추세 한 줄 요약해줘",
]
for p in prompts:
msg = build_vision_message(p, [chart], detail="high")
r = vision.invoke([msg])
print(r.content)
6. image_type별 JSON 추출
이미지 종류별로 다른 추출 프롬프트를 사용한다.
1
2
3
4
5
6
7
def build_extraction_prompt(image_type):
prompts = {
"chart": '이 차트의 각 항목 값을 json으로 추출 : {"title":"", "data":[{"label":"", "value":0}], "trend":""}',
"invoice": '이 인보이스 정보를 json으로 추출 : {"invoice_no":"", "data":"", "items":[{"name":"","qty":0,"total":""}], "total":""}',
"document": '이 문서의 정보를 json으로 추출 : {"title":"", "summary":"", "key_points":[]}',
}
return prompts.get(image_type, "이 이미지의 핵심 내용을 3줄로 요약해주세요")
1
2
3
4
5
6
7
8
9
10
11
def analyze_image_typed(image_path, image_type) -> dict:
prompt = build_extraction_prompt(image_type)
msg = build_vision_message(prompt, [image_path], detail="high")
text = vision.invoke([msg]).content.strip()
if "```" in text:
text = text.split("```")[1].replace("json", "").strip()
try:
return json.loads(text)
except json.JSONDecodeError:
return {"raw_text": text, "error": "failed"}
7. 대시보드 멀티턴 분석
첫 턴에는 이미지를 넣고, 이후 턴에는 텍스트 질문만 이어 간다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dashboard = Image.open(IMAGE_DIR / "dashboard_v1.png")
history = [SystemMessage(content="당신은 데이터 분석가입니다. 주어진 대시보드를 분석해주세요")]
questions = [
"이 대시보드의 핵심 지표 3가지를 알려줘",
"Revenue와 Orders 사이의 비율은?",
"이 추세대로면 다음 분기는 어떨까?",
]
for i, q in enumerate(questions, 1):
if i == 1:
history.append(build_vision_message(q, [dashboard], detail="high"))
else:
history.append(HumanMessage(content=q))
ans = vision.invoke(history).content
history.append(AIMessage(content=ans))
8. 두 이미지 비교
두 이미지를 비교할 때는 어느 것이 기준이고 어느 것이 변경본인지 입력에서 명시한다. 크기나 잘린 영역이 달라 생긴 차이와 실제 내용 변경을 구분하고, 확인 가능한 변경 위치를 결과에 포함하도록 요청한다.
대시보드 v1/v2를 동시에 넣고 변경점을 분석한다.
1
2
3
4
5
6
7
8
9
10
11
v1 = Image.open(IMAGE_DIR / "dashboard_v1.png")
v2 = Image.open(IMAGE_DIR / "dashboard_v2.png")
msg = build_vision_message(
"두 대시보드의 변경 사항을 (1) 수치 변화 (2) 시각적 변화 두 섹션으로 정리해줘",
[v1, v2],
detail="high",
)
r = vision.invoke([msg])
print(r.content)
9. 핵심 개념 총정리
| 개념 | 역할 |
|---|---|
| data URI | 로컬 이미지를 LLM 메시지에 넣는 형식 |
| image_url content | Vision 모델에 이미지 전달 |
| detail | auto/high 등 이미지 해상도 처리 힌트 |
| typed extraction | 차트/문서/인보이스별 JSON 추출 |
| multi-turn vision | 첫 턴 이미지 후 후속 질문 |
| image comparison | 여러 이미지를 동시에 넣고 차이 분석 |
10. 자주 나오는 실수/주의사항
실수 1: data URI 형식 오류
1
f"data:image/png;base64,{...}"
base64앞에 세미콜론이 들어가야 한다.
실수 2: JSON 응답에 코드블록이 섞임
1
2
if "```" in text:
text = text.split("```")[1].replace("json", "").strip()
실수 3: 후속 질문에 이미지를 매번 넣어야 한다고 착각
1
대화 history에 첫 이미지 분석 결과가 남아 있으면 후속 질문은 텍스트로 이어갈 수 있다.
실수 4: 이미지 크기 차이
1
v1.size, v2.size
비교 전에 크기나 해상도가 너무 다르면 모델이 불필요한 차이에 집중할 수 있다.
11. 이미지 분석 결과의 확인 가능성
이미지 분석이 성공했다는 기준을 자연스러운 설명으로만 잡지 않는다. 표의 숫자, 표시된 문구, 비교 위치처럼 원본과 대조할 수 있는 항목을 먼저 정의하면 응답 형식도 그 목적에 맞춰 정할 수 있다.
해상도나 잘린 영역을 바꾸는 실험에서는 같은 질문과 출력 형식을 사용한다. 정보가 부족한 입력에서 확인 불가를 표현하는지까지 살펴야, 읽을 수 없는 부분을 그럴듯하게 채운 응답을 구분할 수 있다.