2025-06-17 10:05:16 +09:00

166 lines
6.8 KiB
Python

"""
이미지 처리 유틸리티
이미지 분석, 변환, 최적화 기능 제공
"""
import os
from typing import Dict, Any, Tuple
from PIL import Image, ImageOps
import io
class ImageProcessor:
"""이미지 처리 클래스"""
def __init__(self):
"""이미지 프로세서 초기화"""
self.supported_formats = {'JPEG', 'PNG', 'WEBP', 'GIF'}
self.max_size = (2048, 2048) # 최대 크기
self.thumbnail_size = (400, 400) # 썸네일 크기
def get_image_info(self, image_path: str) -> Dict[str, Any]:
"""
이미지 기본 정보 추출
Args:
image_path: 이미지 파일 경로
Returns:
이미지 정보 딕셔너리
"""
try:
with Image.open(image_path) as image:
info = {
'filename': os.path.basename(image_path),
'format': image.format,
'mode': image.mode,
'size': image.size,
'width': image.width,
'height': image.height,
'file_size': os.path.getsize(image_path),
'aspect_ratio': round(image.width / image.height, 2) if image.height > 0 else 0
}
# 이미지 특성 분석
info['is_landscape'] = image.width > image.height
info['is_portrait'] = image.height > image.width
info['is_square'] = abs(image.width - image.height) < 50
return info
except Exception as e:
return {
'filename': os.path.basename(image_path),
'error': str(e)
}
def resize_image(self, image_path: str, target_size: Tuple[int, int],
maintain_aspect: bool = True) -> Image.Image:
"""
이미지 크기 조정
Args:
image_path: 원본 이미지 경로
target_size: 목표 크기 (width, height)
maintain_aspect: 종횡비 유지 여부
Returns:
리사이즈된 PIL 이미지
"""
try:
with Image.open(image_path) as image:
if maintain_aspect:
# 종횡비 유지하며 리사이즈
image.thumbnail(target_size, Image.Resampling.LANCZOS)
return image.copy()
else:
# 강제 리사이즈
return image.resize(target_size, Image.Resampling.LANCZOS)
except Exception as e:
raise Exception(f"이미지 리사이즈 실패: {str(e)}")
def optimize_image(self, image_path: str, quality: int = 85) -> bytes:
"""
이미지 최적화 (파일 크기 줄이기)
Args:
image_path: 원본 이미지 경로
quality: JPEG 품질 (1-100)
Returns:
최적화된 이미지 바이트
"""
try:
with Image.open(image_path) as image:
# RGBA를 RGB로 변환 (JPEG 저장을 위해)
if image.mode == 'RGBA':
background = Image.new('RGB', image.size, (255, 255, 255))
background.paste(image, mask=image.split()[-1])
image = background
# 크기가 너무 크면 줄이기
if image.width > self.max_size[0] or image.height > self.max_size[1]:
image.thumbnail(self.max_size, Image.Resampling.LANCZOS)
# 바이트 스트림으로 저장
img_buffer = io.BytesIO()
image.save(img_buffer, format='JPEG', quality=quality, optimize=True)
return img_buffer.getvalue()
except Exception as e:
raise Exception(f"이미지 최적화 실패: {str(e)}")
def create_thumbnail(self, image_path: str, size: Tuple[int, int] = None) -> Image.Image:
"""
썸네일 생성
Args:
image_path: 원본 이미지 경로
size: 썸네일 크기 (기본값: self.thumbnail_size)
Returns:
썸네일 PIL 이미지
"""
if size is None:
size = self.thumbnail_size
try:
with Image.open(image_path) as image:
# 정사각형 썸네일 생성
thumbnail = ImageOps.fit(image, size, Image.Resampling.LANCZOS)
return thumbnail
except Exception as e:
raise Exception(f"썸네일 생성 실패: {str(e)}")
def analyze_colors(self, image_path: str, num_colors: int = 5) -> list:
"""
이미지의 주요 색상 추출
Args:
image_path: 이미지 파일 경로
num_colors: 추출할 색상 개수
Returns:
주요 색상 리스트 [(R, G, B), ...]
"""
try:
with Image.open(image_path) as image:
# RGB로 변환
if image.mode != 'RGB':
image = image.convert('RGB')
# 이미지 크기 줄여서 처리 속도 향상
image.thumbnail((150, 150))
# 색상 히스토그램 생성
colors = image.getcolors(maxcolors=256*256*256)
if colors:
# 빈도순으로 정렬
colors.sort(key=lambda x: x[0], reverse=True)
# 상위 색상들 반환
dominant_colors = []
for count, color in colors[:num_colors]:
dominant_colors.append(color)
return dominant_colors
return [(128, 128, 128)] # 기본 회색
except Exception as e:
print(f"색상 분석 실패: {e}")
return [(128, 128, 128)] # 기본 회색
def is_food_image(self, image_path: str) -> bool:
"""
음식 이미지 여부 간단 판별
(실제로는 AI 모델이 필요하지만, 여기서는 기본적인 휴리스틱 사용)
Args:
image_path: 이미지 파일 경로
Returns:
음식 이미지 여부
"""
try:
# 파일명에서 키워드 확인
filename = os.path.basename(image_path).lower()
food_keywords = ['food', 'meal', 'dish', 'menu', '음식', '메뉴', '요리']
for keyword in food_keywords:
if keyword in filename:
return True
# 색상 분석으로 간단 판별 (음식은 따뜻한 색조가 많음)
colors = self.analyze_colors(image_path, 3)
warm_color_count = 0
for r, g, b in colors:
# 따뜻한 색상 (빨강, 노랑, 주황 계열) 확인
if r > 150 or (r > g and r > b):
warm_color_count += 1
return warm_color_count >= 2
except:
return False