Update main.py
Browse files
main.py
CHANGED
|
@@ -8,10 +8,20 @@ import cv2
|
|
| 8 |
import numpy as np
|
| 9 |
from PIL import Image
|
| 10 |
import io
|
| 11 |
-
|
|
|
|
|
|
|
| 12 |
|
| 13 |
app = FastAPI(title="ScanAssured OCR & NER API")
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
# Enable CORS for Flutter app
|
| 16 |
app.add_middleware(
|
| 17 |
CORSMiddleware,
|
|
@@ -192,9 +202,6 @@ def extract_text_structured(result) -> str:
|
|
| 192 |
all_words = []
|
| 193 |
|
| 194 |
for page in result.pages:
|
| 195 |
-
page_height = page.dimensions[0]
|
| 196 |
-
page_width = page.dimensions[1]
|
| 197 |
-
|
| 198 |
for block in page.blocks:
|
| 199 |
for line in block.lines:
|
| 200 |
line_text = ""
|
|
@@ -202,7 +209,6 @@ def extract_text_structured(result) -> str:
|
|
| 202 |
|
| 203 |
for word in line.words:
|
| 204 |
line_text += word.value + " "
|
| 205 |
-
# Get vertical position (y coordinate)
|
| 206 |
min_y = min(min_y, word.geometry[0][1])
|
| 207 |
|
| 208 |
if line_text.strip():
|
|
@@ -212,10 +218,8 @@ def extract_text_structured(result) -> str:
|
|
| 212 |
'x': line.geometry[0][0] if hasattr(line, 'geometry') else 0
|
| 213 |
})
|
| 214 |
|
| 215 |
-
|
| 216 |
-
all_words.sort(key=lambda w: (round(w['y'] * 20) / 20, w['x'])) # Group similar y positions
|
| 217 |
|
| 218 |
-
# Join with newlines for lines at different vertical positions
|
| 219 |
result_text = ""
|
| 220 |
prev_y = -1
|
| 221 |
for word_info in all_words:
|
|
@@ -227,6 +231,106 @@ def extract_text_structured(result) -> str:
|
|
| 227 |
|
| 228 |
return result_text.strip()
|
| 229 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
# --- FastAPI Routes ---
|
| 231 |
|
| 232 |
@app.get("/")
|
|
@@ -316,32 +420,52 @@ async def process_image(
|
|
| 316 |
doc = DocumentFile.from_images([img_bytes])
|
| 317 |
result = ocr_predictor_instance(doc)
|
| 318 |
|
| 319 |
-
#
|
|
|
|
|
|
|
|
|
|
| 320 |
structured_text = extract_text_structured(result)
|
| 321 |
cleaned_text = basic_cleanup(structured_text)
|
|
|
|
| 322 |
|
| 323 |
print(f"OCR Structured Text:\n{structured_text[:500]}...")
|
|
|
|
| 324 |
|
| 325 |
# Perform NER on cleaned text
|
| 326 |
print("Running NER...")
|
| 327 |
entities = ner_pipeline(cleaned_text)
|
| 328 |
|
| 329 |
-
#
|
| 330 |
structured_entities = []
|
| 331 |
for entity in entities:
|
| 332 |
-
if entity.get('score', 0.0) > 0.
|
| 333 |
structured_entities.append({
|
| 334 |
'entity_group': entity['entity_group'],
|
| 335 |
'score': float(entity['score']),
|
| 336 |
'word': entity['word'].strip(),
|
| 337 |
})
|
| 338 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
return {
|
| 340 |
-
"structured_text": structured_text,
|
| 341 |
-
"cleaned_text": cleaned_text,
|
| 342 |
-
"medical_entities":
|
|
|
|
| 343 |
"model_id": NER_MODELS[ner_model_id]["name"],
|
| 344 |
-
"ocr_model": f"{det_arch} + {reco_arch}"
|
|
|
|
|
|
|
| 345 |
}
|
| 346 |
|
| 347 |
except Exception as e:
|
|
|
|
| 8 |
import numpy as np
|
| 9 |
from PIL import Image
|
| 10 |
import io
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
from typing import Dict, Any, Optional, List
|
| 14 |
|
| 15 |
app = FastAPI(title="ScanAssured OCR & NER API")
|
| 16 |
|
| 17 |
+
# --- DRUG INTERACTIONS DATABASE ---
|
| 18 |
+
DRUG_INTERACTIONS = {}
|
| 19 |
+
interactions_path = os.path.join(os.path.dirname(__file__), 'interactions_data.json')
|
| 20 |
+
if os.path.exists(interactions_path):
|
| 21 |
+
with open(interactions_path, 'r') as f:
|
| 22 |
+
DRUG_INTERACTIONS = json.load(f)
|
| 23 |
+
print(f"Loaded {len(DRUG_INTERACTIONS)} drug interaction entries")
|
| 24 |
+
|
| 25 |
# Enable CORS for Flutter app
|
| 26 |
app.add_middleware(
|
| 27 |
CORSMiddleware,
|
|
|
|
| 202 |
all_words = []
|
| 203 |
|
| 204 |
for page in result.pages:
|
|
|
|
|
|
|
|
|
|
| 205 |
for block in page.blocks:
|
| 206 |
for line in block.lines:
|
| 207 |
line_text = ""
|
|
|
|
| 209 |
|
| 210 |
for word in line.words:
|
| 211 |
line_text += word.value + " "
|
|
|
|
| 212 |
min_y = min(min_y, word.geometry[0][1])
|
| 213 |
|
| 214 |
if line_text.strip():
|
|
|
|
| 218 |
'x': line.geometry[0][0] if hasattr(line, 'geometry') else 0
|
| 219 |
})
|
| 220 |
|
| 221 |
+
all_words.sort(key=lambda w: (round(w['y'] * 20) / 20, w['x']))
|
|
|
|
| 222 |
|
|
|
|
| 223 |
result_text = ""
|
| 224 |
prev_y = -1
|
| 225 |
for word_info in all_words:
|
|
|
|
| 231 |
|
| 232 |
return result_text.strip()
|
| 233 |
|
| 234 |
+
def extract_words_with_boxes(result) -> list:
|
| 235 |
+
"""
|
| 236 |
+
Extract all words with their bounding boxes from docTR result.
|
| 237 |
+
Returns list of {word, bbox} where bbox is [[x0,y0], [x1,y1]] normalized 0-1.
|
| 238 |
+
"""
|
| 239 |
+
words_with_boxes = []
|
| 240 |
+
|
| 241 |
+
for page in result.pages:
|
| 242 |
+
for block in page.blocks:
|
| 243 |
+
for line in block.lines:
|
| 244 |
+
for word in line.words:
|
| 245 |
+
# geometry is ((x0, y0), (x1, y1)) normalized
|
| 246 |
+
bbox = [
|
| 247 |
+
[word.geometry[0][0], word.geometry[0][1]],
|
| 248 |
+
[word.geometry[1][0], word.geometry[1][1]]
|
| 249 |
+
]
|
| 250 |
+
words_with_boxes.append({
|
| 251 |
+
'word': word.value,
|
| 252 |
+
'bbox': bbox
|
| 253 |
+
})
|
| 254 |
+
|
| 255 |
+
return words_with_boxes
|
| 256 |
+
|
| 257 |
+
def check_drug_interactions(detected_drugs: List[str]) -> List[Dict]:
|
| 258 |
+
"""
|
| 259 |
+
Check for known interactions between detected drugs.
|
| 260 |
+
Returns list of interaction warnings.
|
| 261 |
+
"""
|
| 262 |
+
interactions = []
|
| 263 |
+
drugs_lower = [d.lower().strip() for d in detected_drugs]
|
| 264 |
+
|
| 265 |
+
# Check each pair of drugs
|
| 266 |
+
for i, drug1 in enumerate(drugs_lower):
|
| 267 |
+
for drug2 in drugs_lower[i+1:]:
|
| 268 |
+
# Check if drug1 interacts with drug2
|
| 269 |
+
if drug1 in DRUG_INTERACTIONS:
|
| 270 |
+
if drug2 in DRUG_INTERACTIONS[drug1]:
|
| 271 |
+
interaction = DRUG_INTERACTIONS[drug1][drug2]
|
| 272 |
+
interactions.append({
|
| 273 |
+
'drug1': detected_drugs[i],
|
| 274 |
+
'drug2': detected_drugs[drugs_lower.index(drug2)],
|
| 275 |
+
'severity': interaction.get('severity', 'info'),
|
| 276 |
+
'description': interaction.get('description', ''),
|
| 277 |
+
'recommendation': interaction.get('recommendation'),
|
| 278 |
+
})
|
| 279 |
+
# Check reverse (drug2 interacts with drug1)
|
| 280 |
+
elif drug2 in DRUG_INTERACTIONS:
|
| 281 |
+
if drug1 in DRUG_INTERACTIONS[drug2]:
|
| 282 |
+
interaction = DRUG_INTERACTIONS[drug2][drug1]
|
| 283 |
+
interactions.append({
|
| 284 |
+
'drug1': detected_drugs[drugs_lower.index(drug2)],
|
| 285 |
+
'drug2': detected_drugs[i],
|
| 286 |
+
'severity': interaction.get('severity', 'info'),
|
| 287 |
+
'description': interaction.get('description', ''),
|
| 288 |
+
'recommendation': interaction.get('recommendation'),
|
| 289 |
+
})
|
| 290 |
+
|
| 291 |
+
return interactions
|
| 292 |
+
|
| 293 |
+
def map_entities_to_boxes(entities: list, words_with_boxes: list, cleaned_text: str) -> list:
|
| 294 |
+
"""
|
| 295 |
+
Map NER entities back to word bounding boxes.
|
| 296 |
+
Uses fuzzy matching to find entity words in OCR words.
|
| 297 |
+
"""
|
| 298 |
+
entities_with_boxes = []
|
| 299 |
+
|
| 300 |
+
for entity in entities:
|
| 301 |
+
entity_word = entity['word'].lower().strip()
|
| 302 |
+
entity_parts = entity_word.split()
|
| 303 |
+
|
| 304 |
+
# Find matching word(s) in OCR output
|
| 305 |
+
matched_boxes = []
|
| 306 |
+
for word_info in words_with_boxes:
|
| 307 |
+
ocr_word = word_info['word'].lower().strip()
|
| 308 |
+
# Check if OCR word matches any part of entity
|
| 309 |
+
for part in entity_parts:
|
| 310 |
+
if part in ocr_word or ocr_word in part:
|
| 311 |
+
matched_boxes.append(word_info['bbox'])
|
| 312 |
+
break
|
| 313 |
+
|
| 314 |
+
# Combine bounding boxes if multiple matches
|
| 315 |
+
if matched_boxes:
|
| 316 |
+
# Get bounding box that encompasses all matched words
|
| 317 |
+
min_x = min(box[0][0] for box in matched_boxes)
|
| 318 |
+
min_y = min(box[0][1] for box in matched_boxes)
|
| 319 |
+
max_x = max(box[1][0] for box in matched_boxes)
|
| 320 |
+
max_y = max(box[1][1] for box in matched_boxes)
|
| 321 |
+
combined_bbox = [[min_x, min_y], [max_x, max_y]]
|
| 322 |
+
else:
|
| 323 |
+
combined_bbox = None
|
| 324 |
+
|
| 325 |
+
entities_with_boxes.append({
|
| 326 |
+
'entity_group': entity['entity_group'],
|
| 327 |
+
'score': entity['score'],
|
| 328 |
+
'word': entity['word'],
|
| 329 |
+
'bbox': combined_bbox
|
| 330 |
+
})
|
| 331 |
+
|
| 332 |
+
return entities_with_boxes
|
| 333 |
+
|
| 334 |
# --- FastAPI Routes ---
|
| 335 |
|
| 336 |
@app.get("/")
|
|
|
|
| 420 |
doc = DocumentFile.from_images([img_bytes])
|
| 421 |
result = ocr_predictor_instance(doc)
|
| 422 |
|
| 423 |
+
# Get image dimensions for frontend highlighting
|
| 424 |
+
img_height, img_width = preprocessed_img.shape[:2]
|
| 425 |
+
|
| 426 |
+
# Extract text and word bounding boxes
|
| 427 |
structured_text = extract_text_structured(result)
|
| 428 |
cleaned_text = basic_cleanup(structured_text)
|
| 429 |
+
words_with_boxes = extract_words_with_boxes(result)
|
| 430 |
|
| 431 |
print(f"OCR Structured Text:\n{structured_text[:500]}...")
|
| 432 |
+
print(f"Extracted {len(words_with_boxes)} words with bounding boxes")
|
| 433 |
|
| 434 |
# Perform NER on cleaned text
|
| 435 |
print("Running NER...")
|
| 436 |
entities = ner_pipeline(cleaned_text)
|
| 437 |
|
| 438 |
+
# Structure entities (return all with score > 0.1, let frontend filter)
|
| 439 |
structured_entities = []
|
| 440 |
for entity in entities:
|
| 441 |
+
if entity.get('score', 0.0) > 0.1:
|
| 442 |
structured_entities.append({
|
| 443 |
'entity_group': entity['entity_group'],
|
| 444 |
'score': float(entity['score']),
|
| 445 |
'word': entity['word'].strip(),
|
| 446 |
})
|
| 447 |
|
| 448 |
+
# Map entities to bounding boxes
|
| 449 |
+
entities_with_boxes = map_entities_to_boxes(structured_entities, words_with_boxes, cleaned_text)
|
| 450 |
+
|
| 451 |
+
# Check for drug interactions
|
| 452 |
+
detected_drugs = []
|
| 453 |
+
for entity in structured_entities:
|
| 454 |
+
if entity['entity_group'] in ['CHEM', 'CHEMICAL', 'TREATMENT']:
|
| 455 |
+
detected_drugs.append(entity['word'])
|
| 456 |
+
|
| 457 |
+
interactions = check_drug_interactions(detected_drugs) if detected_drugs else []
|
| 458 |
+
print(f"Found {len(interactions)} drug interactions")
|
| 459 |
+
|
| 460 |
return {
|
| 461 |
+
"structured_text": structured_text,
|
| 462 |
+
"cleaned_text": cleaned_text,
|
| 463 |
+
"medical_entities": entities_with_boxes,
|
| 464 |
+
"interactions": interactions, # NEW: Drug interaction warnings
|
| 465 |
"model_id": NER_MODELS[ner_model_id]["name"],
|
| 466 |
+
"ocr_model": f"{det_arch} + {reco_arch}",
|
| 467 |
+
"image_width": img_width,
|
| 468 |
+
"image_height": img_height
|
| 469 |
}
|
| 470 |
|
| 471 |
except Exception as e:
|