03. 이미지 분석 API 직접 호출 v2
이 노트북은 이미지 파일을 base64 data URL로 바꾸어 Responses API에 직접 전달합니다. detail 옵션과 Pydantic 구조화 출력까지 한 흐름에서 확인합니다.
# Colab 또는 로컬 노트북 실행 환경을 구분하기 위해 sys를 가져옵니다.
import sys
# 패키지 설치 명령을 현재 Python 커널에서 실행하기 위해 subprocess를 가져옵니다.
import subprocess
# 패키지 설치 여부를 확인하기 위해 importlib.util을 가져옵니다.
import importlib.util
# 환경 변수에서 API 키를 읽고 설정하기 위해 os를 가져옵니다.
import os
# API 키를 화면에 노출하지 않고 입력받기 위해 getpass를 가져옵니다.
import getpass
# .env 파일 위치를 다루기 위해 pathlib의 Path를 가져옵니다.
from pathlib import Path
# google.colab 모듈이 있으면 현재 런타임이 Google Colab이라고 판단합니다.
IN_COLAB = "google.colab" in sys.modules
# 노트북에서 사용하는 import 이름과 pip 패키지 이름을 짝지어 둡니다.
REQUIRED_PACKAGES = {
"openai": "openai>=2.26.0",
"dotenv": "python-dotenv>=1.2.2",
"pydantic": "pydantic>=2.11.0",
"PIL": "pillow>=12.1.1",
"requests": "requests>=2.32.5",
"numpy": "numpy>=2.3.3",
"websockets": "websockets>=15.0.1",
"websocket": "websocket-client>=1.8.0",
"nest_asyncio": "nest-asyncio>=1.6.0",
}
def ensure_package(import_name: str, package_name: str) -> None:
if importlib.util.find_spec(import_name) is not None:
return
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "-q", package_name]
)
if IN_COLAB:
for import_name, package_name in REQUIRED_PACKAGES.items():
ensure_package(import_name, package_name)
from dotenv import load_dotenv
load_dotenv(Path.cwd() / ".env")
if not os.getenv("OPENAI_API_KEY"):
secret_key = None
try:
from google.colab import userdata
secret_key = userdata.get("OPENAI_API_KEY")
except Exception:
secret_key = None
if secret_key:
os.environ["OPENAI_API_KEY"] = secret_key
else:
entered_key = getpass.getpass("OPENAI_API_KEY를 입력하세요: ").strip()
if entered_key:
os.environ["OPENAI_API_KEY"] = entered_key
if not os.getenv("OPENAI_API_KEY"):
raise RuntimeError(
"OPENAI_API_KEY가 없습니다. Colab Secrets, 수동 입력, 또는 .env 파일로 설정해 주세요."
)
print(f"개발환경: {'Colab' if IN_COLAB else '로컬'}")
print("OPENAI_API_KEY 준비 완료")
이미지 실습 폴더: c:\Users\USER\Desktop\AS.1.1\d02\05.image
결과 저장 폴더: c:\Users\USER\Desktop\AS.1.1\d02\05.image\output\v2
Responses 모델: gpt-5-mini
Images 모델: gpt-image-1-mini
2단계. 분석용 이미지 준비
분석 API는 이미지 URL이나 base64 data URL을 입력으로 받을 수 있습니다. 외부 이미지 없이 반복 실행할 수 있도록 두 장의 비교 이미지를 직접 생성합니다.
# 첫 번째 분석 이미지 경로를 정합니다.
image_a_path = OUTPUT_DIR / "03-02_analysis_original.png"
# 두 번째 분석 이미지 경로를 정합니다.
image_b_path = OUTPUT_DIR / "03-02_analysis_variant.png"
# 밝은 배경의 첫 번째 이미지를 만듭니다.
image_a = Image.new("RGB", (768, 512), (245, 248, 252))
# 첫 번째 이미지에 그릴 도구를 만듭니다.
draw_a = ImageDraw.Draw(image_a)
# 첫 번째 이미지에 제품 카드 형태를 그립니다.
draw_a.rounded_rectangle((80, 80, 688, 432), radius=28, fill=(255, 255, 255), outline=(45, 90, 160), width=5)
# 첫 번째 이미지에 작은 강조 원을 그립니다.
draw_a.ellipse((330, 180, 438, 288), fill=(90, 180, 140))
# 첫 번째 이미지를 저장합니다.
image_a.save(image_a_path)
# 두 번째 이미지를 복사해 변형 기준으로 사용합니다.
image_b = image_a.copy()
# 두 번째 이미지에 그릴 도구를 만듭니다.
draw_b = ImageDraw.Draw(image_b)
# 두 번째 이미지의 강조 색과 위치를 바꿉니다.
draw_b.ellipse((300, 160, 468, 328), fill=(230, 120, 80))
# 두 번째 이미지에 추가 요소를 그립니다.
draw_b.rounded_rectangle((500, 110, 650, 180), radius=16, fill=(40, 40, 40))
# 두 번째 이미지를 저장합니다.
image_b.save(image_b_path)
# 첫 번째 이미지를 base64 data URL로 변환합니다.
data_url_a = "data:image/png;base64," + base64.b64encode(image_a_path.read_bytes()).decode("utf-8")
# 두 번째 이미지를 base64 data URL로 변환합니다.
data_url_b = "data:image/png;base64," + base64.b64encode(image_b_path.read_bytes()).decode("utf-8")
# 준비된 이미지 경로를 출력합니다.
print(f"원본 이미지: {workspace_path(image_a_path)}")
# 비교 이미지 경로를 출력합니다.
print(f"비교 이미지: {workspace_path(image_b_path)}")
원본 이미지: 05.image/output/v2/03-02_analysis_original.png
비교 이미지: 05.image/output/v2/03-02_analysis_variant.png
3단계. 기본 비교 분석과 detail 옵션
이미지를 API에 넣을 때 detail 옵션은 토큰 사용량과 관찰 수준에 영향을 줍니다. 먼저 기본 비교를 하고, 이어서 low/high 설정을 직접 바꿔 호출합니다.
# 기본 비교 분석 프롬프트를 준비합니다.
prompt = "두 이미지를 비교해 무엇이 달라졌는지 초보 디자이너가 이해할 수 있게 한국어로 설명해 주세요."
# Responses API를 직접 호출해 두 이미지를 함께 전달합니다.
response = client.responses.create(
# 이미지 이해가 가능한 Responses 모델을 지정합니다.
model=RESPONSES_MODEL,
# 사용자 메시지를 content 배열로 구성합니다.
input=[
{
# 사용자 역할 메시지임을 지정합니다.
"role": "user",
# 텍스트 질문과 두 장의 이미지를 같은 메시지에 함께 담습니다.
"content": [
# 비교 분석 요청 문장을 넣습니다.
{"type": "input_text", "text": prompt},
# 첫 번째 비교 이미지를 입력합니다.
{"type": "input_image", "image_url": data_url_a},
# 두 번째 비교 이미지를 입력합니다.
{"type": "input_image", "image_url": data_url_b},
],
}
],
# 비교 설명이 잘리지 않도록 출력 토큰 한도를 지정합니다.
max_output_tokens=900,
)
# 최종 텍스트 답변을 읽습니다.
comparison_text = response.output_text
# 비교 분석 결과를 텍스트 파일로 저장합니다.
(OUTPUT_DIR / "03-03_basic_analysis.txt").write_text(
comparison_text + "\n",
encoding="utf-8",
)
# 결과 미리보기를 출력합니다.
print(comparison_text[:500])
# detail 옵션별 결과를 담을 dict를 만듭니다.
detail_results = {}
# low와 high detail을 차례대로 비교합니다.
for detail in ["low", "high"]:
# 현재 detail 값으로 API를 직접 호출합니다.
detail_response = client.responses.create(
# 이미지 이해 모델을 지정합니다.
model=RESPONSES_MODEL,
# 이미지 입력에 detail 값을 직접 넣습니다.
input=[
{
# 사용자 요청 메시지임을 지정합니다.
"role": "user",
# 비교 요약 요청과 두 이미지 입력을 함께 담습니다.
"content": [
# detail 비교용 요약 요청 문장입니다.
{
"type": "input_text",
"text": "이미지의 차이를 3개 항목으로 요약해 주세요.",
},
# 첫 번째 이미지에 현재 detail 값을 적용합니다.
{
"type": "input_image",
"image_url": data_url_a,
"detail": detail,
},
# 두 번째 이미지에도 같은 detail 값을 적용합니다.
{
"type": "input_image",
"image_url": data_url_b,
"detail": detail,
},
],
}
],
# 짧은 요약만 필요하므로 토큰 한도를 낮게 둡니다.
max_output_tokens=500,
)
# detail별 결과 텍스트를 저장합니다.
detail_results[detail] = detail_response.output_text
# detail 비교 결과를 JSON으로 저장합니다.
(OUTPUT_DIR / "03-03_detail_comparison.json").write_text(
json.dumps(detail_results, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
# 비교한 detail 키를 출력합니다.
print(f"detail 비교 완료: {list(detail_results.keys())}")
요약: 첫 번째 이미지에선 중앙 상단에 작은 녹색 원 하나만 있어 단일 초점(포인트)이었고, 두 번째 이미지에선 원이 훨씬 커지고 색이 주황색으로 바뀌었으며 우상단에 검은색의 작은 둥근 사각형(뱃지 또는 버튼 같은 요소)이 추가되어 전체 구성(밸런스)과 시선 흐름이 달라졌습니다.
구체적 차이 (초보 디자이너 관점)
- 원의 크기: 작음 → 큼. 두 번째 이미지의 원이 훨씬 커져 시각적 무게(visual weight)가 증가함.
- 원의 색상: 차가운 초록 → 따뜻한 주황. 색상 온도가 바뀌어 주황색이 더 눈에 띔(시선을 더 빨리 끌음).
- 원의 위치: 첫 이미지에서는 거의 중앙(약간 위) 배치 → 두 번째에서는 좌쪽으로 치우치고(완전 중앙은 아님) 약간 아래로 내려옴.
- 새 요소 추가: 우상단에 작은 검은색(또는 짙은 회색) 둥근 사각형이 생김. 이것이 두 번째 이미지의 2차 초점이 됨.
- 컴포지션(구도) 변화: 첫 이미지는 단일 중심 초점으로 안정감이 있지만, 두 번째는 비
detail 비교 완료: ['low', 'high']
4단계. 구조화 이미지 리뷰
실무에서는 이미지 분석 결과를 사람이 읽는 문장뿐 아니라 다음 자동화 단계가 사용할 JSON으로 받아야 합니다. responses.parse에 Pydantic 모델을 직접 전달해 구조화 결과를 얻습니다.
# 구조화 이미지 리뷰 스키마를 정의합니다.
class ImageReview(BaseModel):
# 전체 품질 점수입니다.
score: int = Field(description="1에서 10 사이의 전체 품질 점수")
# 이미지에서 잘된 점 목록입니다.
strengths: list[str] = Field(description="잘된 점 목록")
# 개선이 필요한 점 목록입니다.
improvements: list[str] = Field(description="개선점 목록")
# 다음 이미지 생성에 사용할 수 있는 프롬프트입니다.
next_prompt: str = Field(description="개선 이미지를 생성하기 위한 다음 프롬프트")
# 구조화 리뷰를 요청합니다.
review_response = client.responses.parse(
# 이미지 이해가 가능한 모델을 지정합니다.
model=RESPONSES_MODEL,
# 모델의 평가 역할을 지정합니다.
instructions=(
"당신은 교육용 이미지 리뷰어입니다. "
"관찰 내용을 명확하고 실행 가능하게 구조화하세요."
),
# 비교 이미지와 평가 요청을 함께 전달합니다.
input=[
{
"role": "user",
"content": [
{
"type": "input_text",
"text": "두 번째 이미지를 수업 자료 썸네일로 개선하려면 어떻게 고쳐야 하는지 평가해 주세요.",
},
{
"type": "input_image",
"image_url": data_url_b,
"detail": "high",
},
],
}
],
# Pydantic 모델을 구조화 출력 형식으로 직접 전달합니다.
text_format=ImageReview,
# 구조화 JSON이 잘리지 않도록 출력 토큰 한도를 둡니다.
max_output_tokens=1600,
# gpt-5 계열 모델이면 구조화 출력에 집중하도록 낮은 reasoning을 지정합니다.
reasoning={"effort": "low"} if RESPONSES_MODEL.lower().startswith("gpt-5") else None,
)
# SDK 버전에 따라 최상위 output_parsed에 파싱 결과가 들어올 수 있습니다.
parsed_review = getattr(review_response, "output_parsed", None)
# 최상위에 없으면 output 배열의 content block을 확인합니다.
if parsed_review is None:
# output 배열을 안전하게 순회합니다.
for item in getattr(review_response, "output", []) or []:
# content block 목록을 읽습니다.
content_blocks = getattr(item, "content", []) or []
# 각 content block을 확인합니다.
for block in content_blocks:
# block 안의 parsed 값을 읽습니다.
parsed_review = getattr(block, "parsed", None)
# parsed 값이 있으면 반복을 멈춥니다.
if parsed_review is not None:
break
# parsed 값이 있으면 바깥 반복도 멈춥니다.
if parsed_review is not None:
break
# 파싱 결과를 찾지 못하면 상태를 포함해 명확하게 중단합니다.
if parsed_review is None:
raise RuntimeError(
"구조화 리뷰 파싱 결과를 찾지 못했습니다. "
f"status={getattr(review_response, 'status', None)}"
)
# Pydantic 모델을 dict로 변환합니다.
review_data = (
parsed_review.model_dump()
if hasattr(parsed_review, "model_dump")
else dict(parsed_review)
)
# 구조화 리뷰를 JSON 파일로 저장합니다.
(OUTPUT_DIR / "03-04_structured_review.json").write_text(
json.dumps(review_data, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
# 구조화 리뷰를 출력합니다.
print(json.dumps(review_data, ensure_ascii=False, indent=2))
{
"score": 7,
"strengths": [
"구성 간결하고 요소가 적어 썸네일로 사용 시 시선 유도하기 쉬움",
"배경이 깔끔해 텍스트·아이콘 추가 시 가독성 확보 용이",
"주황 원과 검정 사각이 시각적 대비를 만들어 중심과 보조 요소가 구분됨"
],
"improvements": [
"주제 전달을 위한 텍스트(제목/부제)가 없음 — 한눈에 수업 주제를 알 수 있도록 간결한 제목 추가",
"요소 배치가 빈 공간을 많이 남김 — 중앙·우상단 요소를 재배치해 시각적 균형 강화(예: 원은 좌측 중앙으로, 텍스트는 우측 또는 하단에)",
"텍스트 색상·폰트 미표시 — 굵은 산세리프 계열로 큰 글씨(예: 24–36pt 수준 비율), 대비 높은 색(진한 남색/검정) 사용",
"컬러 팔레트 정리 필요 — 배경, 포인트(주황), 강조(검정) 색의 조화 유지; 보조 색 1개(예: 짙은 파랑) 추가로 계층 만들기",
"아이콘·심볼이 무엇을 의미하는지 불명확 — 수업 주제에 맞는 심볼(책, 연필, 그래프 등) 또는 카메라 모티프 정교화",
"테두리(파란 선)가 얇고 둥근 모서리와 어울리지만 썸네일 축소 시 가시성 저하 — 테두리 굵기 약간 증가 또는 그림자 추가",
"해상도·여백 고려 필요 — 모바일/플랫폼 썸네일로 축소될 때 핵심 요소가 잘 보이도록 대비·크기 조정"
],
"next_prompt": "수업 주제(예: '기초 사진 촬영', '데이터 시각화 입문' 등)와 사용될 플랫폼(유튜브 썸네일, LMS 목록 등)을 알려주세요. 원하시면 제목 텍스트(한 줄 또는 두 줄)와 선호 색상도 함께 알려주시면, 그 정보를 반영한 구체적 레이아웃(요소 위치, 폰트·크기·색상 제안)과 축소 후 보이는 미리보기까지 제안해 드리겠습니다."
}'AI System > OpenAI API와 바이브 코딩으로 배우는 AI 서비스 개발' 카테고리의 다른 글
| d02 - 06. Audio - 01. stt (0) | 2026.06.02 |
|---|---|
| d02 - 05. Image - 04. workflow (0) | 2026.06.02 |
| d02 - 05. Image - 02. editing (0) | 2026.06.02 |
| d02 - 05. Image - 01. generation (0) | 2026.06.02 |
| d01 - 04. Web Search - 05. multi query analysis v2 (0) | 2026.06.02 |