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 self.default_font_size = 20 self.font_cache = {} 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) print(f"render_text_on_image font: {font}") 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)