fady-50 commited on
Commit
1eadd76
Β·
verified Β·
1 Parent(s): a1a66ed

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +22 -0
  2. main.py +372 -0
  3. requirements.txt +10 -0
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ libgl1-mesa-glx \
8
+ libglib2.0-0 \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Copy requirements first for better caching
12
+ COPY requirements.txt .
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ # Copy model files and app
16
+ COPY . .
17
+
18
+ # Expose port
19
+ EXPOSE 7860
20
+
21
+ # Run the app
22
+ CMD ["python", "main.py"]
main.py ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import json
4
+ import base64
5
+ from io import BytesIO
6
+ from typing import Optional, List
7
+ from pathlib import Path
8
+
9
+ import torch
10
+ import numpy as np
11
+ from PIL import Image
12
+ from fastapi import FastAPI, File, UploadFile, Form, HTTPException
13
+ from fastapi.responses import JSONResponse
14
+ from fastapi.middleware.cors import CORSMiddleware
15
+ from transformers import TrOCRProcessor, VisionEncoderDecoderModel
16
+
17
+ # ─── Configuration ────────────────────────────────────────────────
18
+ MODEL_PATH = "fady-50/ocr-model" # Your Hugging Face model ID
19
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
20
+
21
+ # ─── Load Model & Processor ──────────────────────────────────────
22
+ print(f"Loading model on {DEVICE}...")
23
+ processor = TrOCRProcessor.from_pretrained(MODEL_PATH, local_files_only=False)
24
+ model = VisionEncoderDecoderModel.from_pretrained(MODEL_PATH, local_files_only=False)
25
+ model.to(DEVICE)
26
+ model.eval()
27
+ print("Model loaded successfully!")
28
+
29
+ # ─── FastAPI App ─────────────────────────────────────────────────
30
+ app = FastAPI(
31
+ title="Prescription OCR API",
32
+ description="Extract medicine names from handwritten prescriptions using TrOCR",
33
+ version="1.0.0"
34
+ )
35
+
36
+ app.add_middleware(
37
+ CORSMiddleware,
38
+ allow_origins=["*"],
39
+ allow_credentials=True,
40
+ allow_methods=["*"],
41
+ allow_headers=["*"],
42
+ )
43
+
44
+
45
+ # ─── Helper Functions ────────────────────────────────────────────
46
+ def preprocess_image(image: Image.Image) -> Image.Image:
47
+ """Preprocess image for TrOCR."""
48
+ # Convert to RGB if needed
49
+ if image.mode != "RGB":
50
+ image = image.convert("RGB")
51
+
52
+ # Resize to model input size (384x384 for TrOCR base)
53
+ image = image.resize((384, 384), Image.Resampling.LANCZOS)
54
+ return image
55
+
56
+
57
+ def extract_text_from_image(image: Image.Image) -> str:
58
+ """Run OCR on image and return extracted text."""
59
+ pixel_values = processor(images=image, return_tensors="pt").pixel_values
60
+ pixel_values = pixel_values.to(DEVICE)
61
+
62
+ # Generate text
63
+ generated_ids = model.generate(
64
+ pixel_values,
65
+ max_length=512,
66
+ num_beams=4,
67
+ early_stopping=True,
68
+ no_repeat_ngram_size=3,
69
+ )
70
+
71
+ generated_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
72
+ return generated_text.strip()
73
+
74
+
75
+ def extract_medicine_names(text: str) -> List[dict]:
76
+ """
77
+ Extract medicine names from OCR text.
78
+ Uses pattern matching and common medical terms.
79
+ """
80
+ medicines = []
81
+
82
+ # Common medicine name patterns (generic + brand names)
83
+ # Pattern 1: Capitalized words that look like drug names (2-4 words)
84
+ drug_pattern = r'(?:[A-Z][a-zA-Z]*(?:\s+[A-Z][a-zA-Z]*){0,3})'
85
+
86
+ # Pattern 2: Words ending with common drug suffixes
87
+ drug_suffixes = [
88
+ 'cin', 'mycin', 'micin', 'cillin', 'xacin', 'floxacin',
89
+ 'zole', 'sone', 'sone', 'nide', 'pril', 'sartan',
90
+ 'olol', 'dipine', 'zepam', 'zolam', 'pram', 'tidine',
91
+ 'tidine', 'mab', 'nib', 'vastatin', 'prastatin',
92
+ 'profen', 'coxib', 'triptan', 'platin', 'taxel'
93
+ ]
94
+
95
+ lines = text.split('\n')
96
+
97
+ for line_num, line in enumerate(lines, 1):
98
+ line = line.strip()
99
+ if not line or len(line) < 3:
100
+ continue
101
+
102
+ # Check for drug suffixes
103
+ has_drug_suffix = any(suffix.lower() in line.lower() for suffix in drug_suffixes)
104
+
105
+ # Check if line looks like a medicine (starts with capital, contains dosage, or has drug suffix)
106
+ dosage_indicators = ['mg', 'ml', 'g', 'mcg', 'IU', 'tablet', 'capsule',
107
+ 'syrup', 'suspension', 'injection', 'drops', 'cream',
108
+ 'ointment', ' inhaler', 'spray', 'patch']
109
+
110
+ has_dosage = any(ind.lower() in line.lower() for ind in dosage_indicators)
111
+
112
+ # Score the line
113
+ score = 0
114
+ if has_drug_suffix:
115
+ score += 3
116
+ if has_dosage:
117
+ score += 2
118
+ if line[0].isupper() and len(line) > 3:
119
+ score += 1
120
+ if re.search(r'\d+', line): # Contains numbers (likely dosage)
121
+ score += 1
122
+
123
+ # Extract potential medicine name (before dosage info)
124
+ if score >= 2:
125
+ # Try to extract just the medicine name (before numbers or dosage)
126
+ name_match = re.match(r'^([A-Za-z][A-Za-z\s\-]+?)(?:\s+\d|\s*(?:mg|ml|g|\%|tablet|cap))', line, re.IGNORECASE)
127
+
128
+ if name_match:
129
+ med_name = name_match.group(1).strip()
130
+ else:
131
+ med_name = line
132
+
133
+ # Clean up the name
134
+ med_name = re.sub(r'[^A-Za-z\s\-]', '', med_name).strip()
135
+
136
+ if len(med_name) > 2:
137
+ medicines.append({
138
+ "name": med_name,
139
+ "full_line": line,
140
+ "line_number": line_num,
141
+ "confidence_score": min(score / 5.0, 1.0)
142
+ })
143
+
144
+ # Also try regex extraction for any missed medicines
145
+ words = re.findall(r'[A-Z][a-zA-Z]{2,}(?:\s+[A-Z][a-zA-Z]{2,}){0,2}', text)
146
+ existing_names = {m["name"].lower() for m in medicines}
147
+
148
+ for word in words:
149
+ word_clean = word.strip()
150
+ if (word_clean.lower() not in existing_names and
151
+ len(word_clean) > 3 and
152
+ any(suffix.lower() in word_clean.lower() for suffix in drug_suffixes)):
153
+ medicines.append({
154
+ "name": word_clean,
155
+ "full_line": word_clean,
156
+ "line_number": 0,
157
+ "confidence_score": 0.6
158
+ })
159
+
160
+ # Sort by confidence
161
+ medicines.sort(key=lambda x: x["confidence_score"], reverse=True)
162
+
163
+ return medicines
164
+
165
+
166
+ # ─── API Endpoints ───────────────────────────────────────────────
167
+ @app.get("/")
168
+ def root():
169
+ return {
170
+ "message": "Prescription OCR API",
171
+ "status": "running",
172
+ "model": "TrOCR Handwritten",
173
+ "device": str(DEVICE)
174
+ }
175
+
176
+
177
+ @app.get("/health")
178
+ def health_check():
179
+ return {"status": "healthy", "model_loaded": True}
180
+
181
+
182
+ @app.post("/predict")
183
+ async def predict(
184
+ file: UploadFile = File(...),
185
+ return_full_text: bool = Form(False),
186
+ extract_medicines: bool = Form(True)
187
+ ):
188
+ """
189
+ Upload a prescription image and extract text/medicine names.
190
+
191
+ - **file**: Prescription image (JPG, PNG, etc.)
192
+ - **return_full_text**: Return full OCR text
193
+ - **extract_medicines**: Extract medicine names from text
194
+ """
195
+ # Validate file
196
+ allowed_types = {"image/jpeg", "image/png", "image/jpg", "image/webp", "image/bmp"}
197
+ if file.content_type not in allowed_types:
198
+ raise HTTPException(
199
+ status_code=400,
200
+ detail=f"Invalid file type. Allowed: {allowed_types}"
201
+ )
202
+
203
+ try:
204
+ # Read and process image
205
+ contents = await file.read()
206
+ image = Image.open(BytesIO(contents))
207
+
208
+ # Preprocess
209
+ processed_image = preprocess_image(image)
210
+
211
+ # Run OCR
212
+ extracted_text = extract_text_from_image(processed_image)
213
+
214
+ result = {
215
+ "success": True,
216
+ "filename": file.filename,
217
+ "medicines_count": 0,
218
+ "medicines": []
219
+ }
220
+
221
+ if return_full_text:
222
+ result["full_text"] = extracted_text
223
+
224
+ if extract_medicines:
225
+ medicines = extract_medicine_names(extracted_text)
226
+ result["medicines"] = medicines
227
+ result["medicines_count"] = len(medicines)
228
+
229
+ return JSONResponse(content=result)
230
+
231
+ except Exception as e:
232
+ raise HTTPException(status_code=500, detail=str(e))
233
+
234
+
235
+ @app.post("/predict_base64")
236
+ async def predict_base64(
237
+ image_base64: str = Form(...),
238
+ return_full_text: bool = Form(False),
239
+ extract_medicines: bool = Form(True)
240
+ ):
241
+ """
242
+ Send base64-encoded image and extract text/medicine names.
243
+ """
244
+ try:
245
+ # Decode base64
246
+ image_data = base64.b64decode(image_base64)
247
+ image = Image.open(BytesIO(image_data))
248
+
249
+ # Preprocess
250
+ processed_image = preprocess_image(image)
251
+
252
+ # Run OCR
253
+ extracted_text = extract_text_from_image(processed_image)
254
+
255
+ result = {
256
+ "success": True,
257
+ "medicines_count": 0,
258
+ "medicines": []
259
+ }
260
+
261
+ if return_full_text:
262
+ result["full_text"] = extracted_text
263
+
264
+ if extract_medicines:
265
+ medicines = extract_medicine_names(extracted_text)
266
+ result["medicines"] = medicines
267
+ result["medicines_count"] = len(medicines)
268
+
269
+ return JSONResponse(content=result)
270
+
271
+ except Exception as e:
272
+ raise HTTPException(status_code=500, detail=str(e))
273
+
274
+
275
+ @app.post("/predict_url")
276
+ async def predict_url(
277
+ image_url: str = Form(...),
278
+ return_full_text: bool = Form(False),
279
+ extract_medicines: bool = Form(True)
280
+ ):
281
+ """
282
+ Provide image URL and extract text/medicine names.
283
+ """
284
+ import requests
285
+
286
+ try:
287
+ response = requests.get(image_url, timeout=30)
288
+ response.raise_for_status()
289
+
290
+ image = Image.open(BytesIO(response.content))
291
+ processed_image = preprocess_image(image)
292
+ extracted_text = extract_text_from_image(processed_image)
293
+
294
+ result = {
295
+ "success": True,
296
+ "image_url": image_url,
297
+ "medicines_count": 0,
298
+ "medicines": []
299
+ }
300
+
301
+ if return_full_text:
302
+ result["full_text"] = extracted_text
303
+
304
+ if extract_medicines:
305
+ medicines = extract_medicine_names(extracted_text)
306
+ result["medicines"] = medicines
307
+ result["medicines_count"] = len(medicines)
308
+
309
+ return JSONResponse(content=result)
310
+
311
+ except Exception as e:
312
+ raise HTTPException(status_code=500, detail=str(e))
313
+
314
+
315
+ # ─── Gradio UI (for Hugging Face Spaces) ─────────────────────────
316
+ def create_gradio_interface():
317
+ """Create Gradio interface for Hugging Face Spaces."""
318
+ try:
319
+ import gradio as gr
320
+
321
+ def process_image(image):
322
+ if image is None:
323
+ return "Please upload an image", "[]"
324
+
325
+ # Convert to PIL if needed
326
+ if isinstance(image, np.ndarray):
327
+ image = Image.fromarray(image)
328
+
329
+ processed = preprocess_image(image)
330
+ text = extract_text_from_image(processed)
331
+ medicines = extract_medicine_names(text)
332
+
333
+ # Format output
334
+ medicines_text = "\n".join([
335
+ f"{i+1}. {med['name']} (confidence: {med['confidence_score']:.0%})"
336
+ for i, med in enumerate(medicines)
337
+ ]) if medicines else "No medicines detected"
338
+
339
+ return text, medicines_text
340
+
341
+ demo = gr.Interface(
342
+ fn=process_image,
343
+ inputs=gr.Image(type="pil", label="Upload Prescription"),
344
+ outputs=[
345
+ gr.Textbox(label="Extracted Text", lines=10),
346
+ gr.Textbox(label="Detected Medicines", lines=10)
347
+ ],
348
+ title="πŸ“‹ Prescription OCR - Medicine Extractor",
349
+ description="Upload a handwritten prescription image to extract medicine names.",
350
+ examples=[]
351
+ )
352
+ return demo
353
+
354
+ except ImportError:
355
+ return None
356
+
357
+
358
+ # ─── Main Entry Point ────────────────────────────────────────────
359
+ if __name__ == "__main__":
360
+ import uvicorn
361
+
362
+ # Check if running in Hugging Face Spaces (Gradio)
363
+ if os.environ.get("SPACE_ID") or os.environ.get("GRADIO_SERVER_NAME"):
364
+ demo = create_gradio_interface()
365
+ if demo:
366
+ demo.launch(server_name="0.0.0.0", server_port=7860)
367
+ else:
368
+ uvicorn.run(app, host="0.0.0.0", port=7860)
369
+ else:
370
+ # Run FastAPI server
371
+ port = int(os.environ.get("PORT", 7860))
372
+ uvicorn.run(app, host="0.0.0.0", port=port)
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ transformers>=4.30.0
2
+ torch>=2.0.0
3
+ Pillow>=9.5.0
4
+ fastapi>=0.100.0
5
+ uvicorn[standard]>=0.23.0
6
+ python-multipart>=0.0.6
7
+ accelerate>=0.20.0
8
+ numpy>=1.24.0
9
+ gradio>=4.0.0
10
+ requests