KLPR_v2 / app.py
nice22090's picture
Fix Gradio compatibility: use v4.44.1 with type='pil'
f89b961
Raw
History Blame
6.04 kB
"""
ํ•œ๊ตญ ๋ฒˆํ˜ธํŒ OCR - KLPR_v1 (Model v5)
Hugging Face Gradio App
"""
import gradio as gr
import torch
import torch.nn as nn
from PIL import Image
import torchvision.transforms as transforms
import numpy as np
# ============================================================================
# ๋ชจ๋ธ ์ •์˜
# ============================================================================
class CRNN(nn.Module):
def __init__(self, img_height, num_chars, rnn_hidden=256):
super(CRNN, self).__init__()
# CNN - 32x200 -> 1x50
self.cnn = nn.Sequential(
nn.Conv2d(1, 64, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d((2, 2)),
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d((2, 2)),
nn.Conv2d(128, 256, kernel_size=3, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.MaxPool2d((2, 1)),
nn.Conv2d(256, 512, kernel_size=3, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.MaxPool2d((2, 1)),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.MaxPool2d((2, 1))
)
self.rnn = nn.LSTM(512, rnn_hidden, bidirectional=True, num_layers=2, batch_first=True)
self.fc = nn.Linear(rnn_hidden * 2, num_chars)
def forward(self, x):
conv = self.cnn(x)
b, c, h, w = conv.size()
conv = conv.squeeze(2).permute(0, 2, 1)
rnn_out, _ = self.rnn(conv)
output = self.fc(rnn_out)
return output
# ============================================================================
# CTC ๋””์ฝ”๋”ฉ
# ============================================================================
def decode_predictions(outputs, itos, blank_idx=0):
"""CTC ๋””์ฝ”๋”ฉ"""
preds = outputs.argmax(2).detach().cpu().numpy() # (B, T)
decoded = []
for pred in preds:
char_list = []
prev_idx = blank_idx
for idx in pred:
if idx != blank_idx and idx != prev_idx:
char_list.append(itos[int(idx)])
prev_idx = idx
decoded.append(''.join(char_list))
return decoded
# ============================================================================
# ์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ
# ============================================================================
def preprocess_image(image, img_height=32, max_width=200):
"""๋ฒˆํ˜ธํŒ ์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ"""
# PIL Image๋กœ ๋ณ€ํ™˜ (Gradio 4.x์—์„œ type="pil"๋กœ ์ด๋ฏธ PIL Image)
if not isinstance(image, Image.Image):
if isinstance(image, np.ndarray):
image = Image.fromarray(image.astype('uint8'))
image = image.convert('L')
# ๋ฆฌ์‚ฌ์ด์ฆˆ (aspect ratio ์œ ์ง€)
w, h = image.size
new_w = min(int(img_height * w / h), max_width)
image = image.resize((new_w, img_height), Image.LANCZOS)
# ํŒจ๋”ฉ
new_img = Image.new('L', (max_width, img_height), 255)
new_img.paste(image, (0, 0))
# Transform
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
return transform(new_img).unsqueeze(0) # (1, 1, H, W)
# ============================================================================
# ๋ชจ๋ธ ๋กœ๋“œ
# ============================================================================
print("๋ชจ๋ธ ๋กœ๋”ฉ ์ค‘...")
checkpoint_path = 'best_ocr_one_line.pth'
checkpoint = torch.load(checkpoint_path, map_location='cpu')
img_h = checkpoint.get('img_h', 32)
max_w = checkpoint.get('max_w', 200)
itos = checkpoint['itos']
num_chars = len(itos)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = CRNN(img_h, num_chars, rnn_hidden=256).to(device)
model.load_state_dict(checkpoint['model_state'])
model.eval()
print(f"โœ“ ๋ชจ๋ธ ๋กœ๋“œ ์™„๋ฃŒ (Device: {device})")
print(f" - Epoch: {checkpoint.get('epoch', '?')}")
print(f" - Val Acc: {checkpoint.get('val_acc', '?'):.2%}")
# ============================================================================
# ์ถ”๋ก  ํ•จ์ˆ˜
# ============================================================================
def predict_license_plate(image):
"""๋ฒˆํ˜ธํŒ ์ด๋ฏธ์ง€์—์„œ ํ…์ŠคํŠธ ์˜ˆ์ธก"""
if image is None:
return "์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•ด์ฃผ์„ธ์š”."
try:
# ์ „์ฒ˜๋ฆฌ
image_tensor = preprocess_image(image, img_h, max_w).to(device)
# ์ถ”๋ก 
with torch.no_grad():
outputs = model(image_tensor).log_softmax(2)
predictions = decode_predictions(outputs, itos)
result = predictions[0]
return result if result else "(์ธ์‹ ๊ฒฐ๊ณผ ์—†์Œ)"
except Exception as e:
return f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"
# ============================================================================
# Gradio ์ธํ„ฐํŽ˜์ด์Šค
# ============================================================================
demo = gr.Interface(
fn=predict_license_plate,
inputs=gr.Image(type="pil", label="๋ฒˆํ˜ธํŒ ์ด๋ฏธ์ง€"),
outputs=gr.Textbox(label="์ธ์‹ ๊ฒฐ๊ณผ"),
title="๐Ÿš— ํ•œ๊ตญ ๋ฒˆํ˜ธํŒ OCR - KLPR v2",
description="""
ํ•œ๊ตญ ์ž๋™์ฐจ ๋ฒˆํ˜ธํŒ์„ ์ธ์‹ํ•˜๋Š” OCR ๋ชจ๋ธ์ž…๋‹ˆ๋‹ค.
**๋ชจ๋ธ ์ •๋ณด:**
- Model: CRNN (CNN + Bidirectional LSTM + CTC)
- Validation Accuracy: 91.23%
- Epoch: 18
- ์ง€์› ๋ฌธ์ž: 77๊ฐœ (ํ•œ๊ธ€ + ์ˆซ์ž + ์ถ”๊ฐ€ ํŠน์ˆ˜ ์ง€์—ญ๋ช…)
**์‚ฌ์šฉ ๋ฐฉ๋ฒ•:**
1. ๋ฒˆํ˜ธํŒ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•˜์„ธ์š”
2. ์ž๋™์œผ๋กœ ๋ฒˆํ˜ธํŒ ๋ฒˆํ˜ธ๊ฐ€ ์ธ์‹๋ฉ๋‹ˆ๋‹ค
""",
api_name="predict"
)
if __name__ == "__main__":
demo.launch()