FC2
This commit is contained in:
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@@ -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/
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.12
|
||||
78
README.md
78
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 # 프로젝트 설명서
|
||||
```
|
||||
|
||||
178
app.py
Normal file
178
app.py
Normal file
@@ -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("분석할 이미지를 업로드해주세요.")
|
||||
8
docker-compose.yml
Normal file
8
docker-compose.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "8502:8502"
|
||||
volumes:
|
||||
- .:/app
|
||||
30
dockerfile
Normal file
30
dockerfile
Normal file
@@ -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"]
|
||||
18
pyproject.toml
Normal file
18
pyproject.toml
Normal file
@@ -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",
|
||||
]
|
||||
193
uv.lock
generated
Normal file
193
uv.lock
generated
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user