File size: 4,641 Bytes
41fa3a5
 
 
 
 
 
 
e23252d
 
41fa3a5
 
 
 
 
e23252d
 
41fa3a5
e23252d
41fa3a5
e23252d
41fa3a5
 
e23252d
41fa3a5
e23252d
 
41fa3a5
7841fe0
e23252d
 
41fa3a5
e23252d
 
 
41fa3a5
 
e23252d
 
 
 
 
 
41fa3a5
 
 
 
 
e23252d
 
41fa3a5
 
 
 
 
 
 
 
297f772
e23252d
297f772
e23252d
297f772
 
e23252d
 
 
297f772
e23252d
 
 
297f772
 
 
 
 
 
e23252d
297f772
 
e23252d
 
 
 
 
 
 
 
 
 
41fa3a5
 
 
79321e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41fa3a5
79321e5
 
 
 
e23252d
41fa3a5
79321e5
 
 
41fa3a5
 
 
 
 
 
79321e5
41fa3a5
e23252d
d036b64
41fa3a5
 
 
7841fe0
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import gradio as gr
import torch
import string
from PIL import Image
import torchvision.transforms as transforms
from torch import nn
import torch.nn.functional as F
from torchvision import models
from itertools import groupby

# Device configuration
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# Constants
IMG_HEIGHT = 32
IMG_WIDTH = 128
characters = string.ascii_letters + string.digits
char_to_idx = {c: i for i, c in enumerate(characters)}
idx_to_char = {i: c for i, c in enumerate(characters)}
VOCAB_SIZE = len(characters) + 1  # +1 for CTC blank token

# --------------------------
# Model Architecture (Same as Training)
# --------------------------
class FastCRNN(nn.Module):
    def __init__(self, num_classes):
        super().__init__()
        resnet = models.resnet18(weights=None)
        resnet.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)
        self.cnn = nn.Sequential(*list(resnet.children())[:-3])  # Output: [B, 256, 4, 16]
        
        self.lstm_input_size = 128 * (IMG_HEIGHT // 8)  # 256 * 4
        self.rnn = nn.LSTM(self.lstm_input_size, 256, num_layers=2, bidirectional=True, dropout=0.1)
        self.fc = nn.Linear(512, num_classes)

    def forward(self, x):
        x = self.cnn(x)
        x = x.permute(3, 0, 1, 2)  # [W, B, C, H]
        x = x.contiguous().view(x.size(0), x.size(1), -1)  # [W, B, C*H]
        x, _ = self.rnn(x)
        x = self.fc(x)
        return x

# --------------------------
# Model Loading
# --------------------------
def load_model():
    model = FastCRNN(num_classes=VOCAB_SIZE).to(device)
    model.load_state_dict(torch.load('fast_crnn_captcha_model.pth', map_location=device))
    model.eval()
    return model

model = load_model()

# --------------------------
# Prediction Logic
# --------------------------

def decode_predictions(preds):
    """More robust CTC decoding"""
    preds = preds.permute(1, 0, 2)  # [B, W, C]
    preds = torch.softmax(preds, dim=2)
    pred_indices = torch.argmax(preds, dim=2)
    
    texts = []
    for pred in pred_indices:
        # Merge repeated and remove blank (VOCAB_SIZE-1)
        decoded = []
        prev_char = None
        for idx in pred:
            char_idx = idx.item()
            if char_idx < len(idx_to_char) and char_idx != (VOCAB_SIZE - 1):
                char = idx_to_char[char_idx]
                if char != prev_char:
                    decoded.append(char)
                prev_char = char
        texts.append(''.join(decoded))
    
    return texts[0] if len(texts) == 1 else texts    

def preprocess_image(image):
    """Convert input to model-compatible format"""
    transform = transforms.Compose([
        transforms.Resize((IMG_HEIGHT, IMG_WIDTH)),
        transforms.Grayscale(),
        transforms.ToTensor(),
        transforms.Normalize((0.5,), (0.5,))
    ])
    return transform(image).unsqueeze(0).to(device)

def predict(image):
    try:
        if image is None:
            return "No image provided"

        # Optional: handle Gradio dictionary format
        if isinstance(image, dict) and 'data' in image:
            image = image['data']

        # Convert to PIL Image if not already
        if isinstance(image, str) and image.startswith('data:image'):
            from io import BytesIO
            import base64
            image_data = base64.b64decode(image.split(',')[1])
            image = Image.open(BytesIO(image_data))
        elif not isinstance(image, Image.Image):
            image = Image.open(BytesIO(image))

        # Preprocessing (must match training)
        transform = transforms.Compose([
            transforms.Resize((IMG_HEIGHT, IMG_WIDTH)),  # 32x128
            transforms.Grayscale(),
            transforms.ToTensor(),
            transforms.Normalize((0.5,), (0.5,))
        ])
        image_tensor = transform(image).unsqueeze(0).to(device)

        with torch.no_grad():
            output = model(image_tensor)
            prediction = decode_predictions(output)
            print(f"Predicted text: {prediction}")
            return prediction

    except Exception as e:
        print(f"Error details: {str(e)}")
        return f"Error processing image: {str(e)}"


# --------------------------
# Gradio Interface
# --------------------------
iface = gr.Interface(
    fn=predict,
    inputs=gr.Image(type="pil", label="Upload CAPTCHA Image"),
    outputs=gr.Textbox(label="Predicted Text"),
    title="CAPTCHA Solver (FastCRNN)",
    description="Upload a CAPTCHA image to extract text using ResNet18 + BiLSTM"
)

if __name__ == "__main__":
    iface.launch(
        share=True
    )