이미지 파일 자동 번역 프로그램 개발 기록 (3) OpenCV·Gemini 원문 제거와 Photoshop 레이어 배치

번역보다 어려웠던 것: 글자를 지운 자리 2편까지로 "어디에 무슨 문장이 있고, 무엇으로 바꿀지"는 정해졌다. 남은 일은 원래 글자를 지우고 그 자리에 번역문을 얹는 것이다. 처음 만든 반자동 도구는 영역을 배경색으로 칠하거나 flood fill로 채웠는데, 단색 배경이 아니면 네모난 자국이 그대로 보였다. 상세페이지는 사진, 그라데이션, 아이콘 위에 글자가 올라간 경우가 많아서 이 방식으로는 쓸 수 없었다. 이번 편에서는 원문 제거 방식을 두 번 바꾼 과정과, Photoshop COM으로 번역문을 레이어로 넣으면서 겪은 위치·크기·폰트 문제를 정리한다. 첫 시도: Gemini에 이미지 전체를 맡기기 생성형 이미지 모델이 배경을 자연스럽게 채워 준다면 가장 간단하다. 그래서 처음에는 Gemini 2.0 Flash에 원본 이미지 전체를 보내고 "글자만 지운 이미지"를 돌려받는 함수를 만들었다. OpenCV 인페인팅은 Gemini가 실패했을 때 쓰는 대체 경로로 두고, 함수 이름도 remove_text_with_opencv_fallback 으로 지었다. 문제는 Gemini가 글자만 지운다는 보장이 없다는 점이었다. 프롬프트에 "아이콘, 로고, 기호, 그래픽, 선, 테두리, 도형은 절대 바꾸지 말 것", "글자인지 아이콘인지 애매하면 지우지 말 것"을 항목별로 길게 적었지만, 이미지 전체를 다시 그려서 돌려주는 방식이라 글자가 아닌 부분도 조금씩 달라질 수 있었다. 상품 이미지에서 아이콘이나 로고가 바뀌면 그대로 쓸 수 없다. 돌려받은 이미지 크기가 원본과 다를 때도 있어서 원본 크기로 다시 리사이즈해야 했다. 바꾼 방법: OpenCV가 지우고, Gemini는 마스크 안만 다듬는다 순서를 뒤집었다. 지우는 일은 OpenCV가 확실하게 하고, Gemini는 그 결과의 얼룩만 다듬게 했다. 그리고 Gemini 결과는 텍스트가 있던 영역에만 합성한다. Vision OCR의 단어 다각형으로 마스크를 그리고 3×3 커널로 한 번 팽창시킨다. cv2.inpaint (Navier-Stokes 방식)를 세 번 반복해 글자를 지운다. 인페인팅 결과를 Gemini에 보내 "번진 자국만 다듬어 달라"고 요청한다. 같은 다각형으로 마스크를 다시 만들어 10×10 커널로 두 번 팽창시키고, 마스크 안은 Gemini 결과, 바깥은 OpenCV 결과를 쓰도록 Image.composite 로 합친다. def remove_text_with_opencv(image_path, text_positions): # 1단계: OpenCV 인페인팅으로 텍스트 영역만 제거 opencv_result = remove_text_with_opencv_fallback(image_path, text_positions) # 2단계: Gemini로 텍스트 영역만 다듬기 (아이콘 등은 OpenCV 결과 유지) return refine_with_gemini(opencv_result, text_positions) # refine_with_gemini 내부: 텍스트 영역만 Gemini 결과 사용 mask = Image.new('L', original_size, 0) draw = ImageDraw.Draw(mask) for vertices in text_positions: draw.polygon([(int(x), int(y)) for x, y in vertices], fill=255) mask_np = cv2.dilate(np.array(mask), np.ones((10, 10), np.uint8), iterations=2) result_img = Image.composite(gemini_img, opencv_img, Image.fromarray(mask_np)) 이렇게 하면 Gemini가 아이콘을 바꿔 그려도 마스크 밖이라 결과에 반영되지 않는다. Gemini 호출이 실패하거나 이미지를 돌려주지 않으면 예외를 삼키고 OpenCV 결과를 그대로 쓴다. 외부 API 하나가 실패해도 PSD는 만들어진다는 원칙을 여기서도 지켰다. 처음 만든 Gemini 전체 제거 함수는 코드에 남아 있지만 지금 흐름에서는 호출하지 않는다. 한글 경로에서 이미지가 안 열리는 문제 OpenCV의 cv2.imread 와 cv2.imwrite 는 Windows에서 한글이 들어간 경로를 제대로 처리하지 못한다. 사용자 파일은 한글 폴더나 한글 파일명인 경우가 많아서 그대로는 쓸 수 없었다. 파일을 np.fromfile 로 바이트 배열로 읽어 cv2.imdecode 로 풀고, 저장할 때는 cv2.imencode 로 인코딩한 뒤 파이썬 파일 객체로 쓰는 래퍼 함수를 만들어 해결했다. 작업 중에 생기는 사본( _edited )과 제거본( _removed )은 원본 옆에 숨김 파일로 만들고, 작업이 끝나거나 오류로 멈추면 지운다. 다음 작업을 시작할 때도 이전에 남은 임시 파일부터 정리한다. 사용자의 작업 폴더를 어지럽히지 않기 위해서였다. Photoshop에 번역문 얹기 PSD 생성은 Photoshop COM으로 원본을 열고, 제거본을 열어 복사해 원본 위에 붙인 뒤, "Translated Texts" 레이어 그룹을 만들어 문장마다 텍스트 레이어를 추가하는 순서다. 결과적으로 원본, 원문 제거본, 번역 텍스트 그룹이 레이어로 나뉜다. 위치를 맞출 때 두 가지를 처리해야 했다. 하나는 기준점이다. OCR 박스의 y는 글자 위쪽인데, Photoshop의 텍스트 위치는 베이스라인 기준이라 그대로 넣으면 글자가 한 줄 위로 올라갔다. 그래서 y에 박스 높이를 더해 넣었다. 다른 하나는 글자 크기다. 박스 높이의 75%를 폰트 크기로 쓰고, 8~72pt 범위로 제한했다. 박스 높이가 5px 이하로 너무 작게 잡히면 12pt를 쓴다. 번역문이 이미지 밖으로 넘치는 문제 기존 PSD/PSB의 텍스트 레이어를 번역할 때는 레이어 크기가 원문에 맞춰져 있어서 이 문제가 바로 드러난다. 한국어 문장을 독일어나 프랑스어로 바꾸면 길이가 크게 늘어서 텍스트 레이어가 문서 오른쪽 밖으로 나갔다. 텍스트를 바꾼 뒤 레이어의 Bounds 를 문서 폭과 비교해, 넘치면 각 줄을 단어 개수 기준으로 반으로 나눠 줄바꿈하고 다시 잰다. 단어가 하나뿐이면 글자 수 기준으로 나눈다. 무한 반복을 막으려고 최대 5번까지만 시도한다. 아래쪽이 문서 높이를 넘으면 넘친 만큼 위로 올리되 위쪽 10px 아래로는 올리지 않는다. 다만 원래부터 여러 줄로 된 본문 단락까지 반으로 쪼개면 레이아웃이 오히려 흐트러질 수 있다. 그래서 줄 수가 설정값(기본 5줄) 이상인 텍스트는 경계 체크를 건너뛰고 원래 자리에 두게 했다. 이 기준은 사용자가 화면에서 바꿀 수 있다. 폰트가 없으면 내용이 안 바뀐다 텍스트 레이어의 내용을 바꾸다 실패하면 원인은 대개 폰트였다. 레이어에 쓰인 폰트가 사용자 PC에 없거나, 원래 폰트가 번역할 언어의 문자를 지원하지 않는 경우다. 그래서 텍스트를 바꾸기 전에 다국어를 폭넓게 지원하는 NotoSansCJKkr-Regular로 폰트를 바꾸고, TXT에서 PSD로 다시 적용하는 경로에서는 단계별로 재시도하게 했다. try: layer.TextItem.Contents = new_text except Exception: # 폰트 문제일 수 있으므로 폰트 변경 후 재시도 try: layer.TextItem.Font = "NotoSansCJKkr-Regular" layer.TextItem.Contents = new_text except Exception: try: layer.TextItem.Font = "Arial" layer.TextItem.Contents = new_text except Exception as e3: print(f"적용 실패: {layer.Name} - {e3}") 그래도 원래 폰트가 설치돼 있어야 결과가 원본과 비슷해지기 때문에, 화면 주의사항에 "PSD를 번역할 때 원래 레이어의 폰트가 있어야 한다"는 문구를 넣었다. 회고 가장 크게 배운 것은 생성형 모델의 결과를 그대로 쓰지 말고 "어디까지 반영할지"를 코드로 정해야 한다는 점이다. 마스크 합성으로 범위를 제한하고 나서야 Gemini를 실무 결과물에 넣을 수 있었다. 반대로 지울 범위를 정하는 데는 OCR 다각형이 정확한 기준이 됐다. 다시 한다면 두 가지를 바꾸고 싶다. 첫째, 경계 넘침을 줄바꿈으로만 해결했는데 글자 크기를 조금씩 줄이는 선택지도 함께 두는 게 맞다. 짧은 제목은 줄을 나누는 것보다 크기를 줄이는 편이 자연스럽다. 둘째, 번역문의 글자 색을 검정으로 고정했는데, 원본 글자 색을 뽑아 쓰는 처리를 넣고 싶다. 어두운 배경 위의 흰 글자는 지금 구조에서는 사람이 PSD에서 직접 색을 바꿔야 한다. 기간: 2025.10 ~ 2025.12 기술: Python, OpenCV(인페인팅), Google Gemini 2.0 Flash, Pillow, Photoshop COM(pywin32), Google Cloud Vision 관련 프로젝트: 이미지 파일 자동 번역 프로그램