# -*- coding: utf-8 -*- """ 텍스트 렌더링 모듈 - 인페인팅된 이미지에 번역된 텍스트를 자연스럽게 렌더링 (라이브러리화) """ import cv2 import numpy as np from PIL import Image, ImageDraw, ImageFont from typing import List, Dict, Any, Tuple, Optional import os import math import logging class TextRenderingModule: def __init__(self, logger, font_path: Optional[str] = None): self.logger = logger self.font_path = font_path or self._setup_default_fonts() self.default_font_size = 20 self.font_cache = {} self.logger.log("텍스트 렌더링 모듈 초기화 완료", level=logging.INFO) self.logger.log(f"기본 폰트: {self.font_path}", level=logging.INFO) def _setup_default_fonts(self): possible_fonts = [ "/usr/share/fonts/truetype/nanum/NanumGothic.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/System/Library/Fonts/AppleGothic.ttf", "C:/Windows/Fonts/malgun.ttf", "C:/Windows/Fonts/gulim.ttc" ] self.available_fonts = [f for f in possible_fonts if os.path.exists(f)] return self.available_fonts def get_font(self, size: int, font_path: Optional[str] = None) -> ImageFont.FreeTypeFont: font_path = font_path or self.font_path cache_key = f"{font_path}_{size}" if cache_key not in self.font_cache: try: if font_path and os.path.exists(font_path): font = ImageFont.truetype(font_path, size) else: font = ImageFont.load_default() self.font_cache[cache_key] = font except Exception as e: print(f"폰트 로드 오류: {e}") font = ImageFont.load_default() self.font_cache[cache_key] = font return self.font_cache[cache_key] def estimate_text_size(self, text: str, font_size: int, font_path: Optional[str] = None) -> Tuple[int, int]: font = self.get_font(font_size, font_path) try: bbox = font.getbbox(text) width = bbox[2] - bbox[0] height = bbox[3] - bbox[1] except AttributeError: width, height = font.getsize(text) return width, height def calculate_optimal_font_size(self, text: str, target_width: int, target_height: int, min_size: int = 8, max_size: int = 100, font_path: Optional[str] = None) -> int: best_size = min_size for size in range(min_size, max_size + 1): width, height = self.estimate_text_size(text, size, font_path) if width <= target_width and height <= target_height: best_size = size else: break return best_size def _estimate_background_color(self, image: np.ndarray, x1: int, y1: int, x2: int, y2: int) -> Tuple[int, int, int]: margin = 5 y1_exp = max(0, y1 - margin) y2_exp = min(image.shape[0], y2 + margin) x1_exp = max(0, x1 - margin) x2_exp = min(image.shape[1], x2 + margin) region = image[y1_exp:y2_exp, x1_exp:x2_exp] mean_color = np.mean(region, axis=(0, 1)) return (int(mean_color[2]), int(mean_color[1]), int(mean_color[0])) def _get_contrasting_color(self, bg_color: Tuple[int, int, int]) -> Tuple[int, int, int]: brightness = (bg_color[0] * 0.299 + bg_color[1] * 0.587 + bg_color[2] * 0.114) if brightness > 128: return (0, 0, 0) else: return (255, 255, 255) def render_text(self, image: np.ndarray, ocr_results: List[Dict], translated_texts: List[str], font_path: Optional[str] = None) -> np.ndarray: result_image = image.copy() for i, (ocr_result, translated_text) in enumerate(zip(ocr_results, translated_texts)): polygon = ocr_result['polygon'] polygon_array = np.array(polygon) x_coords = polygon_array[:, 0] y_coords = polygon_array[:, 1] x_min, x_max = int(np.min(x_coords)), int(np.max(x_coords)) y_min, y_max = int(np.min(y_coords)), int(np.max(y_coords)) width = x_max - x_min height = y_max - y_min optimal_font_size = self.calculate_optimal_font_size(translated_text, width, height, font_path=font_path) text_width, text_height = self.estimate_text_size(translated_text, optimal_font_size, font_path) center_x = (x_min + x_max) // 2 center_y = (y_min + y_max) // 2 text_x = center_x - text_width // 2 text_y = center_y - text_height // 2 angle = 0 if len(polygon_array) >= 2: dx = polygon_array[1][0] - polygon_array[0][0] dy = polygon_array[1][1] - polygon_array[0][1] angle = math.degrees(math.atan2(dy, dx)) bg_color = self._estimate_background_color(image, x_min, y_min, x_max, y_max) text_color = self._get_contrasting_color(bg_color) result_image = self.render_text_on_image( result_image, translated_text, (text_x, text_y), font_size=optimal_font_size, font_path=font_path, text_color=text_color, background_color=None, angle=angle ) return result_image def render_text_on_image(self, image: np.ndarray, text: str, position: Tuple[int, int], font_size: Optional[int] = None, font_path: Optional[str] = None, text_color: Tuple[int, int, int] = (0, 0, 0), background_color: Optional[Tuple[int, int, int]] = None, angle: float = 0) -> np.ndarray: if font_size is None: font_size = self.default_font_size pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) draw = ImageDraw.Draw(pil_image) font = self.get_font(font_size, font_path) text_width, text_height = self.estimate_text_size(text, font_size, font_path) if background_color is not None: bg_x1 = position[0] - 2 bg_y1 = position[1] - 2 bg_x2 = position[0] + text_width + 2 bg_y2 = position[1] + text_height + 2 draw.rectangle([bg_x1, bg_y1, bg_x2, bg_y2], fill=background_color) if angle != 0: text_image = Image.new('RGBA', (text_width + 10, text_height + 10), (255, 255, 255, 0)) text_draw = ImageDraw.Draw(text_image) text_draw.text((5, 5), text, font=font, fill=text_color + (255,)) rotated_text = text_image.rotate(angle, expand=True) pil_image.paste(rotated_text, position, rotated_text) else: draw.text(position, text, font=font, fill=text_color) result_image = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR) return result_image def create_text_styles(self) -> Dict[str, Dict[str, Any]]: """다양한 텍스트 스타일 정의""" styles = { 'default': { 'color': (0, 0, 0), 'bg_color': None, 'outline': True, 'outline_color': (255, 255, 255), 'outline_width': 1 }, 'bold': { 'color': (0, 0, 0), 'bg_color': (255, 255, 255), 'outline': True, 'outline_color': (128, 128, 128), 'outline_width': 2 }, 'highlight': { 'color': (255, 255, 255), 'bg_color': (255, 0, 0), 'outline': False, 'outline_color': None, 'outline_width': 0 }, 'subtle': { 'color': (128, 128, 128), 'bg_color': None, 'outline': True, 'outline_color': (255, 255, 255), 'outline_width': 1 } } return styles def render_with_style(self, image: np.ndarray, ocr_results: List[Dict], translated_texts: List[str], style_name: str = 'default') -> np.ndarray: """스타일을 적용한 텍스트 렌더링""" styles = self.create_text_styles() if style_name not in styles: print(f"알 수 없는 스타일: {style_name}") style_name = 'default' style = styles[style_name] # 기본 렌더링 후 스타일 적용 result = self.render_text(image, ocr_results, translated_texts) # 추가 스타일 처리는 여기서 구현 # (예: 그림자, 글로우 효과 등) return result def adjust_text_for_space(self, text: str, max_width: int, max_height: int, font_size: int) -> Tuple[str, int]: """ 공간에 맞게 텍스트 조정 Args: text (str): 원본 텍스트 max_width (int): 최대 너비 max_height (int): 최대 높이 font_size (int): 폰트 크기 Returns: Tuple[str, int]: 조정된 텍스트와 폰트 크기 """ # 텍스트가 너무 길면 줄바꿈 또는 생략 if len(text) > 20: # 긴 텍스트는 줄바꿈 words = text.split(' ') if len(words) > 1: mid = len(words) // 2 text = ' '.join(words[:mid]) + '\n' + ' '.join(words[mid:]) else: # 단어가 하나면 생략 text = text[:15] + '...' # 폰트 크기 조정 adjusted_font_size = font_size while adjusted_font_size > 8: # 실제로는 텍스트 크기를 측정해서 비교 estimated_width = len(text) * adjusted_font_size * 0.6 if estimated_width <= max_width: break adjusted_font_size -= 2 return text, adjusted_font_size def _create_style_comparison(self, images: List[np.ndarray], style_names: List[str]): """스타일 비교 이미지 생성""" if not images: return # 이미지 크기 조정 target_width = 200 target_height = int(images[0].shape[0] * target_width / images[0].shape[1]) resized_images = [] for img in images: resized = cv2.resize(img, (target_width, target_height)) resized_images.append(resized) # 비교 이미지 생성 num_images = len(resized_images) comparison_width = target_width * num_images comparison_height = target_height + 30 comparison = np.ones((comparison_height, comparison_width, 3), dtype=np.uint8) * 255 # 원본 이미지 comparison[30:30+target_height, 0:target_width] = resized_images[0] cv2.putText(comparison, "Original", (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1) # 스타일 이미지들 for i, (img, style_name) in enumerate(zip(resized_images[1:], style_names)): x_offset = target_width * (i + 1) comparison[30:30+target_height, x_offset:x_offset+target_width] = img cv2.putText(comparison, style_name, (x_offset + 10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1) cv2.imwrite("test_output/text_style_comparison.jpg", comparison) self.logger.log("스타일 비교 이미지 저장 완료", level=logging.INFO)