diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6199939 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Virtual Environment +.venv/ +venv/ +ENV/ + +# Python cache files +__pycache__/ +*.pyc +*.pyo +*.pyd + +# IDE and editor files +.vscode/ +.idea/ + +# Runtime generated files +output_results/ + +# uv cache +.uv_cache/ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/README.md b/README.md index e69de29..bdf5b33 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,78 @@ +# PaddleOCR 기반 문서 분석 웹 애플리케이션 + +이 프로젝트는 [PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR)의 강력한 PP-StructureV3 모델을 사용하여 이미지 속 문서의 구조를 분석하고, 그 결과를 시각적으로 보여주는 Streamlit 웹 애플리케이션입니다. + +사용자는 이미지 파일을 업로드하여 문서의 레이아웃, 텍스트, 표 등을 자동으로 분석하고 구조화된 결과를 확인할 수 있습니다. + +## ✨ 주요 기능 + +- **간편한 이미지 업로드**: 웹 인터페이스를 통해 손쉽게 이미지 파일(JPG, PNG, BMP 등)을 업로드할 수 있습니다. +- **지능형 문서 분석**: PP-StructureV3 모델을 사용하여 다음과 같은 복합적인 분석을 수행합니다. + - **레이아웃 분석 (Layout Analysis)**: 문서 내의 제목, 문단, 표, 그림 등의 영역을 자동으로 식별합니다. + - **광학 문자 인식 (OCR)**: 이미지 속 모든 텍스트를 정확하게 추출합니다. + - **표 인식 (Table Recognition)**: 표의 구조를 인식하고 셀 단위로 데이터를 추출하여 HTML로 변환합니다. + - **자동 보정**: 기울어진 문서를 바로잡는 등 OCR 정확도를 높이기 위한 전처리 작업을 수행합니다. +- **시각적인 결과 확인**: 분석 과정에서 생성되는 다양한 결과물(영역 감지, OCR 결과 등)을 단계별 이미지와 상세한 설명으로 확인할 수 있습니다. +- **구조화된 데이터 제공**: 분석된 텍스트와 표 데이터를 화면에 체계적으로 표시하며, 원본 JSON 데이터도 확인할 수 있습니다. + +## 🛠️ 사용 기술 + +- **애플리케이션 프레임워크**: Streamlit +- **OCR 및 문서 분석**: PaddleOCR (PP-StructureV3) +- **패키지 및 환경 관리**: uv +- **컨테이너화**: Docker, Docker Compose + +## 🚀 실행 방법 + +이 프로젝트를 실행하는 가장 권장되는 방법은 Docker를 사용하는 것입니다. Docker는 시스템 의존성 문제를 해결하여 어떤 환경에서든 안정적인 실행을 보장합니다. + +### 1. Docker를 이용한 실행 (권장) + +**요구사항**: Docker, Docker Compose가 설치되어 있어야 합니다. + +터미널에서 다음 명령어를 실행하세요. + +```bash +docker-compose up --build +``` + +빌드가 완료되면, 웹 브라우저에서 `http://localhost:8502` 주소로 접속하여 애플리케이션을 사용할 수 있습니다. + +### 2. 로컬 환경에서 직접 실행 + +**요구사항**: Python 3.12+, `uv` + +**주의**: 이 방법은 시스템에 `opencv-python`이 필요로 하는 라이브러리(예: `libGL.so.1`)가 설치되어 있지 않으면 오류가 발생할 수 있습니다. + +1. **가상 환경 생성 및 활성화**: + ```bash + # 가상 환경 생성 + uv venv + + # (Linux/macOS) + source .venv/bin/activate + ``` + +2. **의존성 패키지 설치**: + ```bash + uv pip install -r pyproject.toml + ``` + +3. **Streamlit 앱 실행**: + ```bash + streamlit run app.py --server.port=8502 + ``` + + 이제 웹 브라우저에서 `http://localhost:8502` 주소로 접속할 수 있습니다. + +## 📂 프로젝트 구조 + +``` +. +├── 📄 app.py # Streamlit 웹 애플리케이션 메인 코드 +├── 🐳 docker-compose.yml # Docker Compose 설정 파일 +├── 🐳 dockerfile # Docker 이미지 빌드를 위한 설정 파일 +├── 📝 pyproject.toml # Python 프로젝트 설정 및 의존성 목록 +├── 🔒 uv.lock # 의존성 버전 고정 파일 +└── 📖 README.md # 프로젝트 설명서 +``` diff --git a/app.py b/app.py new file mode 100644 index 0000000..db0c30d --- /dev/null +++ b/app.py @@ -0,0 +1,178 @@ +import streamlit as st +from pathlib import Path +from paddleocr import PPStructureV3 +from PIL import Image +import json +import os +import uuid + +# 페이지 설정 +st.set_page_config(page_title="PaddleOCR 이미지 분석", layout="wide") + +st.title("📄 PaddleOCR을 이용한 이미지 분석") +st.write("이미지 파일을 업로드하면 문서 방향 분류, 왜곡 보정, 표/수식/차트 인식 등을 수행하고 결과를 보여줍니다.") + +# 모델 초기화 (캐싱 사용) +@st.cache_resource +def load_model(): + return PPStructureV3( + lang="korean", + use_doc_orientation_classify=True, + use_doc_unwarping=True, + use_seal_recognition=False, + use_table_recognition=True, + use_formula_recognition=True, + use_chart_recognition=True, + use_region_detection=True, + ) + +with st.spinner("모델을 불러오는 중입니다..."): + structure = load_model() + +# 파일 업로더 +uploaded_file = st.file_uploader("분석할 이미지 파일을 선택하세요.", type=["jpg", "jpeg", "png", "bmp", "tif"]) + +if uploaded_file is not None: + # 임시 디렉터리 설정 + output_dir = Path("output_results") + output_dir.mkdir(exist_ok=True) + + # 고유한 파일 이름 생성 + unique_id = uuid.uuid4().hex + + try: + # 원본 이미지 표시 + image = Image.open(uploaded_file) + + # 모델이 처리하기 쉽도록 이미지를 RGB 형식으로 변환 + if image.mode != 'RGB': + image = image.convert('RGB') + + col1, col2 = st.columns(2) + with col1: + st.subheader("🖼️ 원본 이미지") + st.image(image, caption="업로드된 이미지", width='stretch') + + # 분석 시작 버튼 + if st.button("분석 시작하기", use_container_width=True): + with st.spinner("이미지를 분석하고 있습니다..."): + # PIL 이미지를 바이트로 변환하여 예측 + # PPStructureV3는 파일 경로 또는 numpy 배열을 입력으로 받습니다. + # 업로드된 파일을 임시 저장하여 경로를 전달합니다. + temp_image_path = output_dir / f"temp_{unique_id}_{uploaded_file.name}" + + # 변환된 이미지를 임시 파일로 저장 + image.save(temp_image_path) + + # PPStructureV3로 예측 수행 + output = structure.predict(input=str(temp_image_path)) + + if not output: + st.warning("이미지에서 구조를 감지하지 못했습니다.") + else: + # 결과 저장 경로 + saved_res_path_base = output_dir / f"result_{unique_id}" + saved_res_path_base.mkdir(exist_ok=True) + + json_paths = [] + # 1. 모든 결과 파일(이미지, JSON)을 먼저 저장 + for i, res in enumerate(output): + res.save_to_img(save_path=str(saved_res_path_base)) + + json_path = saved_res_path_base / f"{i}.json" + res.save_to_json(save_path=str(json_path)) + json_paths.append(json_path) + + # 2. 저장된 모든 결과 이미지를 설명과 함께 col2에 표시 + with col2: + st.subheader("✨ 분석 결과 이미지") + image_files = sorted([f for f in saved_res_path_base.glob('*') if f.suffix.lower() in ('.jpg', '.jpeg', '.png', '.bmp', '.tif')]) + + if not image_files: + st.error(f"결과 이미지 파일을 찾을 수 없습니다. (검색 경로: {saved_res_path_base})") + else: + for img_path in image_files: + result_image = Image.open(img_path) + + title = "" + description = "" + + if "_layout_order_res" in img_path.name: + title = "레이아웃 순서 분석 (Reading Order)" + description = "각 텍스트 영역을 사람이 문서를 읽는 논리적인 순서(예: 위에서 아래로, 왼쪽에서 오른쪽으로)를 파악하여 시각화한 결과입니다. 복잡한 문서에서 텍스트를 올바른 순서로 추출하는 데 중요한 역할을 합니다." + elif "layout_det_res" in img_path.name: + title = "레이아웃 감지 (Layout Detection)" + description = "모델이 문서에서 텍스트, 표, 이미지 등의 영역을 최초로 감지한 결과입니다. 이 결과를 바탕으로 각 영역의 종류를 더 상세하게 분석합니다." + elif "_region_det_res" in img_path.name: + title = "영역 감지 (Region Detection)" + description = "레이아웃 감지 후, 각 영역의 종류('제목', '표', '텍스트' 등)를 구체적으로 식별해낸 결과입니다." + elif "_overall_ocr_res" in img_path.name: + title = "종합 OCR 결과 (Overall OCR Result)" + description = "레이아웃 분석으로 찾아낸 각 텍스트 영역에 대해 광학 문자 인식(OCR)을 수행한 최종 결과를 원본 이미지 위에 표시한 것입니다." + elif "_preprocessed_img" in img_path.name: + title = "전처리된 이미지 (Preprocessed Image)" + description = "OCR의 정확도를 높이기 위해 입력 이미지의 기울기를 보정하거나, 잡음을 제거하는 등의 전처리 과정을 거친 후의 이미지를 보여줍니다." + elif "_table_cell_img" in img_path.name: + title = "표 셀 인식 (Table Cell Recognition)" + description = "'표(Table)' 영역이 감지되면, 표의 구조를 분석하여 각 셀의 경계를 찾고 그 결과를 시각화하여 보여줍니다. 이 결과를 바탕으로 구조화된 데이터를 생성할 수 있습니다." + else: + title = "기타 결과 이미지" + description = "분석 과정에서 생성된 기타 결과 이미지입니다." + + st.markdown(f"##### {title}") + st.image(result_image, width='stretch') + st.info(description) + st.markdown("---") + + # 3. 저장된 모든 JSON 결과를 아래에 순서대로 표시 + st.subheader("📄 분석 내용") + for json_path in sorted(json_paths): + with open(json_path, 'r', encoding='utf-8') as f: + json_data = json.load(f) + + st.markdown(f"---") + st.markdown(f"#### 결과 파일: `{json_path.name}`") + + # 테이블 HTML을 먼저 추출하여 저장 + table_htmls = [table.get('pred_html', '') for table in json_data.get('table_res_list', [])] + table_idx = 0 + + # 각 블록을 순회하며 의미에 맞게 표시 + for block in json_data.get('parsing_res_list', []): + label = block.get('block_label', 'unknown') + content = block.get('block_content', '').strip() + + if label == 'table': + st.markdown(f"##### 📊 표 (Table)") + if table_idx < len(table_htmls): + st.markdown(table_htmls[table_idx], unsafe_allow_html=True) + table_idx += 1 + else: + st.warning("테이블 내용은 찾을 수 없습니다.") + + elif label in ['header', 'doc_title']: + st.markdown(f"##### 📑 제목 / 헤더 ({label})") + st.markdown(f"**{content}**") + + elif label in ['image', 'seal']: + st.markdown(f"##### 🖼️ 이미지 / 직인 ({label})") + st.info(f"'{label}' 영역이 감지되었습니다.") + + elif content: + st.markdown(f"##### 📝 텍스트 ({label})") + st.write(content) + + with st.expander(f"`{json_path.name}` 전체 내용 보기"): + st.json(json_data) + + # 임시 파일 삭제 + if temp_image_path.exists(): + os.remove(temp_image_path) + + except Exception as e: + st.error(f"이미지 처리 중 예상치 못한 오류가 발생했습니다: {e}") + st.error(f"파일: {uploaded_file.name}") + st.info("이미지 파일이 손상되었거나, 모델이 지원하지 않는 형식일 수 있습니다. 다른 이미지로 시도해 보세요.") + +else: + st.info("분석할 이미지를 업로드해주세요.") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..49c7541 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3.8' +services: + app: + build: . + ports: + - "8502:8502" + volumes: + - .:/app diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..1b7c572 --- /dev/null +++ b/dockerfile @@ -0,0 +1,30 @@ +# Use a more complete Python runtime as a parent image to avoid missing system libraries +FROM python:3.12 + +# Install system dependencies required by OpenCV +RUN apt-get update && apt-get install -y libgl1 && rm -rf /var/lib/apt/lists/* + +# Set the working directory in the container +WORKDIR /app + +# Install uv +RUN pip install uv + +# Copy the dependency files +COPY pyproject.toml uv.lock ./ + +# Install dependencies using uv +# Note: We use the lock file for reproducible builds +RUN uv pip sync uv.lock --no-cache --system + +# Copy the rest of the application's code +COPY . . + +# Expose the port that Streamlit runs on +EXPOSE 8502 + +# Set the healthcheck +HEALTHCHECK CMD curl --fail http://localhost:8502/_stcore/health + +# Command to run the Streamlit app +CMD ["streamlit", "run", "app.py", "--server.port=8502", "--server.address=0.0.0.0"] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9d3c851 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "paddleocr-interface" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "streamlit>=1.49.1", + "paddleocr[all]>=3.2.0", + "paddlepaddle>=2.6.1", + "paddleslim>=2.6.0", + "numpy<2.0", +] + +[dependency-groups] +dev = [ + "ruff>=0.12.11", +] \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..a8c6353 --- /dev/null +++ b/uv.lock @@ -0,0 +1,193 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile pyproject.toml -o uv.lock +aistudio-sdk==0.3.6 + # via paddlex +altair==5.5.0 + # via streamlit +annotated-types==0.7.0 + # via pydantic +attrs==25.3.0 + # via + # jsonschema + # referencing +bce-python-sdk==0.9.45 + # via aistudio-sdk +blinker==1.9.0 + # via streamlit +cachetools==6.2.0 + # via streamlit +certifi==2025.8.3 + # via requests +chardet==5.2.0 + # via paddlex +charset-normalizer==3.4.3 + # via requests +click==8.2.1 + # via + # aistudio-sdk + # streamlit +colorlog==6.9.0 + # via paddlex +filelock==3.19.1 + # via + # huggingface-hub + # modelscope + # paddlex +fsspec==2025.9.0 + # via huggingface-hub +future==1.0.0 + # via bce-python-sdk +gitdb==4.0.12 + # via gitpython +gitpython==3.1.45 + # via streamlit +hf-xet==1.1.9 + # via huggingface-hub +huggingface-hub==0.34.4 + # via paddlex +idna==3.10 + # via requests +imagesize==1.4.1 + # via paddlex +jinja2==3.1.6 + # via + # altair + # pydeck +jsonschema==4.25.1 + # via altair +jsonschema-specifications==2025.4.1 + # via jsonschema +markupsafe==3.0.2 + # via jinja2 +modelscope==1.29.2 + # via paddlex +narwhals==2.3.0 + # via altair +numpy==2.3.2 + # via + # opencv-contrib-python + # paddlex + # pandas + # pydeck + # shapely + # streamlit +opencv-contrib-python==4.10.0.84 + # via paddlex +packaging==25.0 + # via + # altair + # huggingface-hub + # paddlex + # streamlit +paddleocr==3.2.0 + # via paddleocr-interface (pyproject.toml) +paddlex==3.2.1 + # via paddleocr +pandas==2.3.2 + # via + # paddlex + # streamlit +pillow==11.3.0 + # via + # paddlex + # streamlit +prettytable==3.16.0 + # via + # aistudio-sdk + # paddlex +protobuf==6.32.0 + # via streamlit +psutil==7.0.0 + # via aistudio-sdk +py-cpuinfo==9.0.0 + # via paddlex +pyarrow==21.0.0 + # via streamlit +pyclipper==1.3.0.post6 + # via paddlex +pycryptodome==3.23.0 + # via bce-python-sdk +pydantic==2.11.7 + # via paddlex +pydantic-core==2.33.2 + # via pydantic +pydeck==0.9.1 + # via streamlit +pypdfium2==4.30.0 + # via paddlex +python-dateutil==2.9.0.post0 + # via pandas +pytz==2025.2 + # via pandas +pyyaml==6.0.2 + # via + # huggingface-hub + # paddleocr + # paddlex +referencing==0.36.2 + # via + # jsonschema + # jsonschema-specifications +requests==2.32.5 + # via + # aistudio-sdk + # huggingface-hub + # modelscope + # paddlex + # streamlit +rpds-py==0.27.1 + # via + # jsonschema + # referencing +ruamel-yaml==0.18.15 + # via paddlex +ruamel-yaml-clib==0.2.12 + # via ruamel-yaml +setuptools==80.9.0 + # via modelscope +shapely==2.1.1 + # via paddlex +six==1.17.0 + # via + # bce-python-sdk + # python-dateutil +smmap==5.0.2 + # via gitdb +streamlit==1.49.1 + # via paddleocr-interface (pyproject.toml) +tenacity==9.1.2 + # via streamlit +toml==0.10.2 + # via streamlit +tornado==6.5.2 + # via streamlit +tqdm==4.67.1 + # via + # aistudio-sdk + # huggingface-hub + # modelscope +typing-extensions==4.15.0 + # via + # altair + # huggingface-hub + # paddleocr + # paddlex + # pydantic + # pydantic-core + # referencing + # streamlit + # typing-inspection +typing-inspection==0.4.1 + # via pydantic +tzdata==2025.2 + # via pandas +ujson==5.11.0 + # via paddlex +urllib3==2.5.0 + # via + # modelscope + # requests +watchdog==6.0.0 + # via streamlit +wcwidth==0.2.13 + # via prettytable