Spaces:
Sleeping
Sleeping
MakPr016
commited on
Commit
Β·
ed38d82
1
Parent(s):
c590f07
Updated response and returning proper summary
Browse files- app/main.py +8 -17
- app/post_processor.py +155 -101
- tests/create_reports.py +278 -0
app/main.py
CHANGED
|
@@ -11,9 +11,12 @@ import json
|
|
| 11 |
from .text_extractor import extract_text_from_pdf, extract_text_from_image
|
| 12 |
from .image_extractor import extract_images_from_pdf
|
| 13 |
from .ner_processor import load_model, process_text
|
| 14 |
-
from .post_processor import structure_entities, generate_summary, generate_recommendations
|
| 15 |
from .models import EncryptedRequest
|
| 16 |
from .crypto_utils import CryptoManager
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
app = FastAPI(
|
| 19 |
title="Radiology Report NER API",
|
|
@@ -34,7 +37,7 @@ app.add_middleware(
|
|
| 34 |
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
| 35 |
|
| 36 |
nlp_model = None
|
| 37 |
-
SECRET_KEY = os.getenv("ENCRYPTION_KEY"
|
| 38 |
crypto_manager = CryptoManager(SECRET_KEY)
|
| 39 |
|
| 40 |
@app.on_event("startup")
|
|
@@ -198,15 +201,7 @@ async def root():
|
|
| 198 |
<li>β οΈ <strong>Critical finding detection</strong></li>
|
| 199 |
<li>π <strong>Clinical recommendations</strong></li>
|
| 200 |
<li>π¦ <strong>Gzip compression</strong> (25% bandwidth savings)</li>
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
<h2>Model Information</h2>
|
| 204 |
-
<ul>
|
| 205 |
-
<li><strong>Architecture:</strong> spaCy NER (HashEmbedCNN)</li>
|
| 206 |
-
<li><strong>Training Data:</strong> 2,674 radiology reports</li>
|
| 207 |
-
<li><strong>Entity Types:</strong> ANATOMY, OBSERVATION</li>
|
| 208 |
-
<li><strong>OCR Engine:</strong> EasyOCR (95%+ accuracy)</li>
|
| 209 |
-
<li><strong>Deployment:</strong> HuggingFace Spaces</li>
|
| 210 |
</ul>
|
| 211 |
|
| 212 |
<h2>Documentation</h2>
|
|
@@ -215,12 +210,6 @@ async def root():
|
|
| 215 |
π <a href="/redoc" target="_blank">Alternative Documentation (ReDoc)</a><br>
|
| 216 |
π <a href="/health" target="_blank">Health Check Endpoint</a>
|
| 217 |
</p>
|
| 218 |
-
|
| 219 |
-
<h2>Security & Privacy</h2>
|
| 220 |
-
<p>
|
| 221 |
-
This API implements military-grade encryption to ensure HIPAA compliance and protect sensitive medical data.
|
| 222 |
-
All communications are encrypted end-to-end using NaCl cryptography with XSalsa20-Poly1305.
|
| 223 |
-
</p>
|
| 224 |
</div>
|
| 225 |
</body>
|
| 226 |
</html>
|
|
@@ -280,6 +269,7 @@ async def analyze_secure(request: EncryptedRequest):
|
|
| 280 |
entities = process_text(nlp_model, extracted_text)
|
| 281 |
structured = structure_entities(entities)
|
| 282 |
summary = generate_summary(structured)
|
|
|
|
| 283 |
recommendations = generate_recommendations(structured)
|
| 284 |
|
| 285 |
processing_time = time.time() - start_time
|
|
@@ -297,6 +287,7 @@ async def analyze_secure(request: EncryptedRequest):
|
|
| 297 |
"images": images,
|
| 298 |
"structured_report": structured,
|
| 299 |
"summary": summary,
|
|
|
|
| 300 |
"recommendations": recommendations
|
| 301 |
}
|
| 302 |
|
|
|
|
| 11 |
from .text_extractor import extract_text_from_pdf, extract_text_from_image
|
| 12 |
from .image_extractor import extract_images_from_pdf
|
| 13 |
from .ner_processor import load_model, process_text
|
| 14 |
+
from .post_processor import structure_entities, generate_summary, generate_recommendations, generate_patient_friendly_summary
|
| 15 |
from .models import EncryptedRequest
|
| 16 |
from .crypto_utils import CryptoManager
|
| 17 |
+
from dotenv import load_dotenv
|
| 18 |
+
|
| 19 |
+
load_dotenv()
|
| 20 |
|
| 21 |
app = FastAPI(
|
| 22 |
title="Radiology Report NER API",
|
|
|
|
| 37 |
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
| 38 |
|
| 39 |
nlp_model = None
|
| 40 |
+
SECRET_KEY = os.getenv("ENCRYPTION_KEY")
|
| 41 |
crypto_manager = CryptoManager(SECRET_KEY)
|
| 42 |
|
| 43 |
@app.on_event("startup")
|
|
|
|
| 201 |
<li>β οΈ <strong>Critical finding detection</strong></li>
|
| 202 |
<li>π <strong>Clinical recommendations</strong></li>
|
| 203 |
<li>π¦ <strong>Gzip compression</strong> (25% bandwidth savings)</li>
|
| 204 |
+
<li>π₯ <strong>Patient-friendly summaries</strong></li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
</ul>
|
| 206 |
|
| 207 |
<h2>Documentation</h2>
|
|
|
|
| 210 |
π <a href="/redoc" target="_blank">Alternative Documentation (ReDoc)</a><br>
|
| 211 |
π <a href="/health" target="_blank">Health Check Endpoint</a>
|
| 212 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
</div>
|
| 214 |
</body>
|
| 215 |
</html>
|
|
|
|
| 269 |
entities = process_text(nlp_model, extracted_text)
|
| 270 |
structured = structure_entities(entities)
|
| 271 |
summary = generate_summary(structured)
|
| 272 |
+
patient_summary = generate_patient_friendly_summary(structured)
|
| 273 |
recommendations = generate_recommendations(structured)
|
| 274 |
|
| 275 |
processing_time = time.time() - start_time
|
|
|
|
| 287 |
"images": images,
|
| 288 |
"structured_report": structured,
|
| 289 |
"summary": summary,
|
| 290 |
+
"patient_friendly_summary": patient_summary,
|
| 291 |
"recommendations": recommendations
|
| 292 |
}
|
| 293 |
|
app/post_processor.py
CHANGED
|
@@ -1,115 +1,169 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
"dissection", "aneurysm",
|
| 15 |
-
"pulmonary embolism", "embolus"
|
| 16 |
-
]
|
| 17 |
-
|
| 18 |
-
# Negative finding keywords
|
| 19 |
-
NEGATIVE_KEYWORDS = [
|
| 20 |
-
"no", "negative", "absent", "clear",
|
| 21 |
-
"normal", "unremarkable", "stable",
|
| 22 |
-
"within normal limits", "no evidence"
|
| 23 |
-
]
|
| 24 |
-
|
| 25 |
-
def structure_entities(entities: List[Dict]) -> Dict:
|
| 26 |
-
"""
|
| 27 |
-
Convert flat entity list into structured report
|
| 28 |
-
"""
|
| 29 |
-
anatomy = []
|
| 30 |
-
observations = []
|
| 31 |
-
|
| 32 |
-
# Separate by entity type
|
| 33 |
for entity in entities:
|
| 34 |
-
if entity[
|
| 35 |
-
|
| 36 |
-
elif entity[
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
obs for obs in observations
|
| 52 |
-
if obs not in negative_findings
|
| 53 |
-
]
|
| 54 |
-
|
| 55 |
-
# Identify critical findings
|
| 56 |
-
critical_findings = [
|
| 57 |
-
obs for obs in positive_findings
|
| 58 |
-
if any(keyword in obs.lower() for keyword in CRITICAL_KEYWORDS)
|
| 59 |
-
]
|
| 60 |
|
| 61 |
return {
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
}
|
| 68 |
|
| 69 |
-
def generate_summary(
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
| 73 |
return {
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
}
|
| 80 |
|
| 81 |
-
def
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
recommendations = []
|
| 86 |
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
recommendations.append(
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
recommendations.append(
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
)
|
| 108 |
-
|
| 109 |
-
# Normal study
|
| 110 |
-
if not structured_report["positive_findings"]:
|
| 111 |
-
recommendations.append(
|
| 112 |
-
"No significant abnormalities detected in this report."
|
| 113 |
-
)
|
| 114 |
|
| 115 |
return recommendations
|
|
|
|
| 1 |
+
NEGATIVE_TERMS = ['clear', 'normal', 'unremarkable', 'no', 'negative', 'absent', 'within normal limits', 'stable']
|
| 2 |
+
POSITIVE_TERMS = ['opacity', 'infiltrate', 'consolidation', 'fracture', 'abnormal', 'effusion', 'edema', 'pneumothorax', 'cardiomegaly', 'mass', 'nodule', 'atelectasis']
|
| 3 |
+
CRITICAL_TERMS = ['acute', 'severe', 'large', 'extensive', 'pneumothorax', 'fracture', 'mass', 'suspicious']
|
| 4 |
|
| 5 |
+
MEDICAL_GLOSSARY = {
|
| 6 |
+
'pulmonary edema': 'Fluid buildup in the lungs, making breathing difficult',
|
| 7 |
+
'focal consolidation': 'Dense area in lung tissue, possibly indicating infection',
|
| 8 |
+
'pleural effusion': 'Fluid accumulation around the lungs',
|
| 9 |
+
'pneumothorax': 'Collapsed lung due to air leak',
|
| 10 |
+
'cardiomegaly': 'Enlarged heart',
|
| 11 |
+
'opacity': 'Cloudy area on X-ray, may indicate infection or fluid',
|
| 12 |
+
'infiltrate': 'Abnormal substance in lung tissue',
|
| 13 |
+
'atelectasis': 'Partial lung collapse',
|
| 14 |
+
'nodule': 'Small round growth in the lung',
|
| 15 |
+
'mass': 'Larger abnormal tissue growth requiring investigation',
|
| 16 |
+
'consolidation': 'Area where air spaces are filled with fluid or tissue',
|
| 17 |
+
'effusion': 'Abnormal fluid collection'
|
| 18 |
+
}
|
| 19 |
|
| 20 |
+
def structure_entities(entities):
|
| 21 |
+
anatomy_entities = []
|
| 22 |
+
observation_entities = []
|
| 23 |
+
positive_findings = []
|
| 24 |
+
negative_findings = []
|
| 25 |
+
critical_findings = []
|
| 26 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
for entity in entities:
|
| 28 |
+
if entity['label'] == 'ANATOMY':
|
| 29 |
+
anatomy_entities.append(entity['text'])
|
| 30 |
+
elif entity['label'] == 'OBSERVATION':
|
| 31 |
+
observation_entities.append(entity['text'])
|
| 32 |
+
|
| 33 |
+
obs_lower = entity['text'].lower()
|
| 34 |
+
|
| 35 |
+
is_critical = any(term in obs_lower for term in CRITICAL_TERMS)
|
| 36 |
+
is_negative = any(term in obs_lower for term in NEGATIVE_TERMS)
|
| 37 |
+
is_positive = any(term in obs_lower for term in POSITIVE_TERMS)
|
| 38 |
+
|
| 39 |
+
if is_critical:
|
| 40 |
+
critical_findings.append(entity['text'])
|
| 41 |
+
elif is_negative:
|
| 42 |
+
negative_findings.append(entity['text'])
|
| 43 |
+
elif is_positive:
|
| 44 |
+
positive_findings.append(entity['text'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
return {
|
| 47 |
+
'anatomy': list(set(anatomy_entities)),
|
| 48 |
+
'all_observations': observation_entities,
|
| 49 |
+
'positive_findings': list(set(positive_findings)),
|
| 50 |
+
'negative_findings': list(set(negative_findings)),
|
| 51 |
+
'critical_findings': list(set(critical_findings))
|
| 52 |
}
|
| 53 |
|
| 54 |
+
def generate_summary(structured):
|
| 55 |
+
total_entities = len(structured['anatomy']) + len(structured['all_observations'])
|
| 56 |
+
anatomy_count = len(structured['anatomy'])
|
| 57 |
+
observations_count = len(structured['all_observations'])
|
| 58 |
+
has_critical = len(structured['critical_findings']) > 0
|
| 59 |
+
has_abnormalities = len(structured['positive_findings']) > 0
|
| 60 |
+
|
| 61 |
return {
|
| 62 |
+
'total_entities': total_entities,
|
| 63 |
+
'anatomy_count': anatomy_count,
|
| 64 |
+
'observations_count': observations_count,
|
| 65 |
+
'has_critical_findings': has_critical,
|
| 66 |
+
'has_abnormalities': has_abnormalities
|
| 67 |
}
|
| 68 |
|
| 69 |
+
def generate_patient_friendly_summary(structured):
|
| 70 |
+
summary = {
|
| 71 |
+
'overall_status': '',
|
| 72 |
+
'key_findings': [],
|
| 73 |
+
'areas_of_concern': [],
|
| 74 |
+
'next_steps': [],
|
| 75 |
+
'explanation': ''
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
total_abnormalities = len(structured['positive_findings'])
|
| 79 |
+
total_critical = len(structured['critical_findings'])
|
| 80 |
+
|
| 81 |
+
if total_critical > 0:
|
| 82 |
+
summary['overall_status'] = 'URGENT ATTENTION REQUIRED'
|
| 83 |
+
summary['explanation'] = 'Your X-ray shows findings that need immediate medical attention. Please contact your doctor right away.'
|
| 84 |
+
elif total_abnormalities > 0:
|
| 85 |
+
summary['overall_status'] = 'ABNORMALITIES DETECTED'
|
| 86 |
+
summary['explanation'] = 'Your X-ray shows some areas of concern that should be discussed with your doctor. This does not necessarily mean a serious problem, but follow-up is recommended.'
|
| 87 |
+
else:
|
| 88 |
+
summary['overall_status'] = 'NO MAJOR CONCERNS'
|
| 89 |
+
summary['explanation'] = 'Your chest X-ray appears normal with no significant abnormalities detected. Continue regular health maintenance.'
|
| 90 |
+
|
| 91 |
+
for finding in structured['positive_findings']:
|
| 92 |
+
finding_lower = finding.lower()
|
| 93 |
+
explanation = None
|
| 94 |
+
|
| 95 |
+
for term, desc in MEDICAL_GLOSSARY.items():
|
| 96 |
+
if term in finding_lower:
|
| 97 |
+
explanation = desc
|
| 98 |
+
break
|
| 99 |
+
|
| 100 |
+
summary['key_findings'].append({
|
| 101 |
+
'finding': finding,
|
| 102 |
+
'explanation': explanation if explanation else 'Please consult your doctor for clarification on this finding'
|
| 103 |
+
})
|
| 104 |
+
|
| 105 |
+
for critical in structured['critical_findings']:
|
| 106 |
+
critical_lower = critical.lower()
|
| 107 |
+
explanation = None
|
| 108 |
+
|
| 109 |
+
for term, desc in MEDICAL_GLOSSARY.items():
|
| 110 |
+
if term in critical_lower:
|
| 111 |
+
explanation = desc
|
| 112 |
+
break
|
| 113 |
+
|
| 114 |
+
summary['areas_of_concern'].append({
|
| 115 |
+
'finding': critical,
|
| 116 |
+
'explanation': explanation if explanation else 'This requires immediate medical evaluation',
|
| 117 |
+
'severity': 'HIGH'
|
| 118 |
+
})
|
| 119 |
+
|
| 120 |
+
if total_critical > 0:
|
| 121 |
+
summary['next_steps'] = [
|
| 122 |
+
'Contact your doctor immediately',
|
| 123 |
+
'Do not delay seeking medical attention',
|
| 124 |
+
'Bring this report to your appointment',
|
| 125 |
+
'Follow all recommended treatments'
|
| 126 |
+
]
|
| 127 |
+
elif total_abnormalities > 0:
|
| 128 |
+
summary['next_steps'] = [
|
| 129 |
+
'Schedule a follow-up with your doctor within 1-2 weeks',
|
| 130 |
+
'Additional tests or imaging may be recommended',
|
| 131 |
+
'Discuss treatment options if needed',
|
| 132 |
+
'Monitor for any new or worsening symptoms'
|
| 133 |
+
]
|
| 134 |
+
else:
|
| 135 |
+
summary['next_steps'] = [
|
| 136 |
+
'Maintain regular health check-ups',
|
| 137 |
+
'Continue healthy lifestyle practices',
|
| 138 |
+
'Report any new respiratory symptoms to your doctor',
|
| 139 |
+
'Follow recommended screening schedules'
|
| 140 |
+
]
|
| 141 |
+
|
| 142 |
+
return summary
|
| 143 |
+
|
| 144 |
+
def generate_recommendations(structured):
|
| 145 |
recommendations = []
|
| 146 |
|
| 147 |
+
if len(structured['critical_findings']) > 0:
|
| 148 |
+
recommendations.append("Immediate medical consultation required")
|
| 149 |
+
recommendations.append("Consider emergency department visit if symptomatic")
|
| 150 |
+
|
| 151 |
+
if len(structured['positive_findings']) > 0:
|
| 152 |
+
recommendations.append("Follow-up imaging may be needed")
|
| 153 |
+
recommendations.append("Consult with a pulmonologist or radiologist")
|
| 154 |
+
|
| 155 |
+
if 'pneumothorax' in ' '.join(structured['positive_findings']).lower():
|
| 156 |
+
recommendations.append("Urgent evaluation for possible chest tube placement")
|
| 157 |
+
|
| 158 |
+
if 'mass' in ' '.join(structured['positive_findings']).lower() or 'nodule' in ' '.join(structured['positive_findings']).lower():
|
| 159 |
+
recommendations.append("CT scan recommended for further evaluation")
|
| 160 |
+
recommendations.append("Consider biopsy if clinically indicated")
|
| 161 |
+
|
| 162 |
+
if 'effusion' in ' '.join(structured['positive_findings']).lower():
|
| 163 |
+
recommendations.append("Consider thoracentesis if symptomatic")
|
| 164 |
+
|
| 165 |
+
if len(structured['critical_findings']) == 0 and len(structured['positive_findings']) == 0:
|
| 166 |
+
recommendations.append("Continue routine health maintenance")
|
| 167 |
+
recommendations.append("Repeat imaging only if clinically indicated")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
|
| 169 |
return recommendations
|
tests/create_reports.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
from reportlab.lib.pagesizes import letter
|
| 3 |
+
from reportlab.lib import colors
|
| 4 |
+
from reportlab.lib.units import inch
|
| 5 |
+
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image, PageBreak
|
| 6 |
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 7 |
+
from reportlab.lib.enums import TA_CENTER, TA_LEFT
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
import random
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
reports_df = pd.read_csv('indiana_reports.csv')
|
| 13 |
+
projections_df = pd.read_csv('indiana_projections.csv')
|
| 14 |
+
|
| 15 |
+
def header_footer(canvas, doc):
|
| 16 |
+
canvas.saveState()
|
| 17 |
+
|
| 18 |
+
canvas.setFillColor(colors.HexColor('#1a365d'))
|
| 19 |
+
canvas.setFont('Helvetica-Bold', 14)
|
| 20 |
+
canvas.drawString(0.75*inch, letter[1] - 0.5*inch, "ST. LUKE'S MEDICAL CENTER")
|
| 21 |
+
|
| 22 |
+
canvas.setFont('Helvetica', 9)
|
| 23 |
+
canvas.setFillColor(colors.HexColor('#4a5568'))
|
| 24 |
+
canvas.drawString(0.75*inch, letter[1] - 0.65*inch, "Global City β’ We love life")
|
| 25 |
+
|
| 26 |
+
canvas.setFont('Helvetica-Bold', 11)
|
| 27 |
+
canvas.setFillColor(colors.HexColor('#2b6cb0'))
|
| 28 |
+
canvas.drawRightString(letter[0] - 0.75*inch, letter[1] - 0.5*inch, "DIAGNOSTIC X-RAY")
|
| 29 |
+
|
| 30 |
+
canvas.setStrokeColor(colors.HexColor('#e2e8f0'))
|
| 31 |
+
canvas.setLineWidth(0.5)
|
| 32 |
+
canvas.line(0.75*inch, letter[1] - 0.75*inch, letter[0] - 0.75*inch, letter[1] - 0.75*inch)
|
| 33 |
+
|
| 34 |
+
canvas.setFont('Helvetica', 8)
|
| 35 |
+
canvas.setFillColor(colors.HexColor('#718096'))
|
| 36 |
+
canvas.drawCentredString(letter[0]/2, 0.5*inch,
|
| 37 |
+
f"Page {doc.page} of 2 | Validated: {datetime.now().strftime('%d-%b-%Y %I:%M %p')}")
|
| 38 |
+
|
| 39 |
+
canvas.drawCentredString(letter[0]/2, 0.35*inch,
|
| 40 |
+
"This report has been electronically signed and validated. No signature is required.")
|
| 41 |
+
|
| 42 |
+
canvas.restoreState()
|
| 43 |
+
|
| 44 |
+
def create_radiology_report(uid, report_row, output_filename, report_num):
|
| 45 |
+
doc = SimpleDocTemplate(output_filename, pagesize=letter, rightMargin=0.75*inch, leftMargin=0.75*inch,
|
| 46 |
+
topMargin=1*inch, bottomMargin=0.8*inch)
|
| 47 |
+
|
| 48 |
+
story = []
|
| 49 |
+
styles = getSampleStyleSheet()
|
| 50 |
+
|
| 51 |
+
section_style = ParagraphStyle(
|
| 52 |
+
'Section',
|
| 53 |
+
parent=styles['Heading2'],
|
| 54 |
+
fontSize=11,
|
| 55 |
+
textColor=colors.HexColor('#1a365d'),
|
| 56 |
+
spaceAfter=6,
|
| 57 |
+
spaceBefore=10,
|
| 58 |
+
fontName='Helvetica-Bold'
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
body_style = ParagraphStyle(
|
| 62 |
+
'Body',
|
| 63 |
+
parent=styles['Normal'],
|
| 64 |
+
fontSize=10,
|
| 65 |
+
leading=14,
|
| 66 |
+
textColor=colors.HexColor('#2d3748'),
|
| 67 |
+
alignment=TA_LEFT
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
patient_id = f"{random.randint(1000000000, 9999999999)}"
|
| 71 |
+
exam_date = datetime.now().strftime("%d-%b-%Y")
|
| 72 |
+
exam_time = datetime.now().strftime("%I:%M %p")
|
| 73 |
+
dob = f"{random.randint(1,28)}-{random.choice(['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP'])}-{random.randint(1950,2000)}"
|
| 74 |
+
age = random.randint(25,85)
|
| 75 |
+
gender = random.choice(['M', 'F'])
|
| 76 |
+
|
| 77 |
+
print(f"\n{'='*70}")
|
| 78 |
+
print(f" REPORT #{report_num} - UID: {uid}")
|
| 79 |
+
print(f"{'='*70}")
|
| 80 |
+
print(f" Patient ID: {patient_id}")
|
| 81 |
+
print(f" Exam Date: {exam_date} {exam_time}")
|
| 82 |
+
print(f" Age/Gender: {age}Y / {gender}")
|
| 83 |
+
|
| 84 |
+
projection_rows_list = projections_df[projections_df['uid'] == uid]
|
| 85 |
+
print(f" Images Expected: {len(projection_rows_list)}")
|
| 86 |
+
|
| 87 |
+
images_found_count = 0
|
| 88 |
+
for idx, proj_row in projection_rows_list.iterrows():
|
| 89 |
+
filename = proj_row['filename']
|
| 90 |
+
image_path = f"images/{filename}"
|
| 91 |
+
if os.path.exists(image_path):
|
| 92 |
+
exists = "β"
|
| 93 |
+
images_found_count += 1
|
| 94 |
+
else:
|
| 95 |
+
exists = "β"
|
| 96 |
+
print(f" {exists} {filename}")
|
| 97 |
+
|
| 98 |
+
indication = report_row.get('indication', '')
|
| 99 |
+
findings = report_row.get('findings', '')
|
| 100 |
+
impression = report_row.get('impression', '')
|
| 101 |
+
|
| 102 |
+
print(f"\n History: {str(indication)[:60]}..." if len(str(indication)) > 60 else f"\n History: {indication}")
|
| 103 |
+
print(f" Findings: {str(findings)[:60]}..." if len(str(findings)) > 60 else f" Findings: {findings}")
|
| 104 |
+
print(f" Impression: {str(impression)[:60]}..." if len(str(impression)) > 60 else f" Impression: {impression}")
|
| 105 |
+
|
| 106 |
+
patient_name = random.choice(['DE LEON, Debbie Dichoco', 'SANTOS, Maria Clara', 'CRUZ, Juan Pedro', 'SOLOMON, Jannica Mica Estolano'])
|
| 107 |
+
referring_physician = random.choice(['ANG, SAMUEL DEE M.D.', 'REYES, MARIA SANTOS M.D.', 'CRUZ, JOSE ANTONIO M.D.'])
|
| 108 |
+
|
| 109 |
+
patient_info = [
|
| 110 |
+
['', '', Paragraph(f"<b>DATE OF BIRTH:</b>", body_style), Paragraph(dob, body_style), Paragraph(f"<b>AGE:</b>", body_style), Paragraph(f"{age} year/s", body_style)],
|
| 111 |
+
[Paragraph(f"<b>{patient_id}</b>", body_style), '', '', '', Paragraph(f"<b>GENDER:</b>", body_style), Paragraph(gender, body_style)],
|
| 112 |
+
[Paragraph(f"<b>{patient_name}</b>", body_style), '', '', '', '', ''],
|
| 113 |
+
[Paragraph(f"<b>ROOM/BED:</b>", body_style), Paragraph("OutPatient", body_style),
|
| 114 |
+
Paragraph(f"<b>REFERRING<br/>PHYSICIAN:</b>", body_style), Paragraph(referring_physician, body_style),
|
| 115 |
+
Paragraph(f"<b>DATE/TIME OF<br/>EXAM:</b>", body_style), Paragraph(f"{exam_date}<br/>{exam_time}", body_style)],
|
| 116 |
+
['', '', '', '', Paragraph(f"<b>DATE/TIME OF<br/>REQUEST:</b>", body_style), Paragraph(f"{exam_date}<br/>{exam_time}", body_style)]
|
| 117 |
+
]
|
| 118 |
+
|
| 119 |
+
patient_table = Table(patient_info, colWidths=[1.3*inch, 1.2*inch, 1.2*inch, 1.4*inch, 1.2*inch, 1.2*inch])
|
| 120 |
+
patient_table.setStyle(TableStyle([
|
| 121 |
+
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
| 122 |
+
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
| 123 |
+
('BOTTOMPADDING', (0, 0), (-1, -1), 3),
|
| 124 |
+
('TOPPADDING', (0, 0), (-1, -1), 3),
|
| 125 |
+
('LEFTPADDING', (0, 0), (-1, -1), 0),
|
| 126 |
+
('RIGHTPADDING', (0, 0), (-1, -1), 5),
|
| 127 |
+
]))
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
story.append(patient_table)
|
| 131 |
+
story.append(Spacer(1, 0.25*inch))
|
| 132 |
+
|
| 133 |
+
story.append(Paragraph("EXAMINATION:", section_style))
|
| 134 |
+
story.append(Paragraph("CHEST", body_style))
|
| 135 |
+
story.append(Spacer(1, 0.1*inch))
|
| 136 |
+
|
| 137 |
+
if pd.notna(indication) and str(indication).strip() and str(indication).lower() not in ['none', 'nan', '']:
|
| 138 |
+
story.append(Paragraph("HISTORY:", section_style))
|
| 139 |
+
clean_indication = str(indication).replace('XXXX', '[redacted]')
|
| 140 |
+
story.append(Paragraph(clean_indication, body_style))
|
| 141 |
+
story.append(Spacer(1, 0.1*inch))
|
| 142 |
+
|
| 143 |
+
comparison = report_row.get('comparison', '')
|
| 144 |
+
if pd.notna(comparison) and str(comparison).strip() and str(comparison).lower() not in ['none', 'none.', 'nan', '']:
|
| 145 |
+
story.append(Paragraph("COMPARISON:", section_style))
|
| 146 |
+
clean_comparison = str(comparison).replace('XXXX', '[redacted]')
|
| 147 |
+
story.append(Paragraph(clean_comparison, body_style))
|
| 148 |
+
story.append(Spacer(1, 0.1*inch))
|
| 149 |
+
|
| 150 |
+
if len(projection_rows_list) > 0:
|
| 151 |
+
techniques = ", ".join([row['projection'] for _, row in projection_rows_list.iterrows()])
|
| 152 |
+
story.append(Paragraph("TECHNIQUE:", section_style))
|
| 153 |
+
story.append(Paragraph(f"{techniques} view", body_style))
|
| 154 |
+
story.append(Spacer(1, 0.1*inch))
|
| 155 |
+
|
| 156 |
+
story.append(Paragraph("FINDINGS:", section_style))
|
| 157 |
+
if pd.notna(findings) and str(findings).strip() and str(findings).lower() != 'nan':
|
| 158 |
+
clean_findings = str(findings).replace('XXXX', '[redacted]')
|
| 159 |
+
story.append(Paragraph(clean_findings, body_style))
|
| 160 |
+
else:
|
| 161 |
+
story.append(Paragraph("No significant findings.", body_style))
|
| 162 |
+
story.append(Spacer(1, 0.1*inch))
|
| 163 |
+
|
| 164 |
+
story.append(Paragraph("IMPRESSION:", section_style))
|
| 165 |
+
if pd.notna(impression) and str(impression).strip() and str(impression).lower() != 'nan':
|
| 166 |
+
clean_impression = str(impression).replace('XXXX', '[redacted]')
|
| 167 |
+
story.append(Paragraph(clean_impression, body_style))
|
| 168 |
+
else:
|
| 169 |
+
story.append(Paragraph("Normal chest radiograph.", body_style))
|
| 170 |
+
story.append(Spacer(1, 0.2*inch))
|
| 171 |
+
|
| 172 |
+
approval_data = [
|
| 173 |
+
[Paragraph("<b>Approved By:</b>", body_style), '']
|
| 174 |
+
]
|
| 175 |
+
|
| 176 |
+
signature_path = "images/signature.jpg"
|
| 177 |
+
radiologist_name = random.choice(['JOSE ANTONIO CRUZ', 'NERELLA KRISHNA TEJA', 'MARIA ELENA SANTOS', 'MICHAEL VINCENTO CORSINO'])
|
| 178 |
+
|
| 179 |
+
if os.path.exists(signature_path):
|
| 180 |
+
sig_img = Image(signature_path, width=1.2*inch, height=0.4*inch)
|
| 181 |
+
approval_data.append([sig_img, ''])
|
| 182 |
+
|
| 183 |
+
approval_data.append([Paragraph(f"<b>{radiologist_name}, M.D. (Radiologist)</b>", body_style), ''])
|
| 184 |
+
approval_data.append([Paragraph(f"MBBS, MD RADIO DIAGNOSIS | REG NO. APMC/FMR/{random.randint(90000, 99999)}",
|
| 185 |
+
ParagraphStyle('Small', parent=body_style, fontSize=8)), ''])
|
| 186 |
+
|
| 187 |
+
approval_table = Table(approval_data, colWidths=[3*inch, 4*inch])
|
| 188 |
+
approval_table.setStyle(TableStyle([
|
| 189 |
+
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
| 190 |
+
('ALIGN', (0, 0), (0, -1), 'LEFT'),
|
| 191 |
+
('BOTTOMPADDING', (0, 0), (-1, -1), 2),
|
| 192 |
+
('TOPPADDING', (0, 0), (-1, -1), 2),
|
| 193 |
+
]))
|
| 194 |
+
|
| 195 |
+
story.append(approval_table)
|
| 196 |
+
|
| 197 |
+
story.append(PageBreak())
|
| 198 |
+
|
| 199 |
+
if len(projection_rows_list) > 0:
|
| 200 |
+
images_found = []
|
| 201 |
+
for _, proj_row in projection_rows_list.iterrows():
|
| 202 |
+
filename = proj_row['filename']
|
| 203 |
+
image_path = f"images/{filename}"
|
| 204 |
+
|
| 205 |
+
if os.path.exists(image_path):
|
| 206 |
+
try:
|
| 207 |
+
projection_label = proj_row.get('projection', 'View')
|
| 208 |
+
images_found.append((image_path, projection_label))
|
| 209 |
+
except Exception as e:
|
| 210 |
+
print(f" β Error checking image {image_path}: {e}")
|
| 211 |
+
|
| 212 |
+
print(f"\n Images Embedded: {len(images_found)}")
|
| 213 |
+
|
| 214 |
+
if images_found:
|
| 215 |
+
image_table_data = []
|
| 216 |
+
for i in range(0, len(images_found), 2):
|
| 217 |
+
row_images = []
|
| 218 |
+
for j in range(2):
|
| 219 |
+
if i + j < len(images_found):
|
| 220 |
+
img_path, label = images_found[i + j]
|
| 221 |
+
try:
|
| 222 |
+
img = Image(img_path, width=3*inch, height=3*inch)
|
| 223 |
+
caption = Paragraph(f"<b>{label} View</b>",
|
| 224 |
+
ParagraphStyle('Caption', parent=body_style, alignment=TA_CENTER, fontSize=10))
|
| 225 |
+
cell = Table([[img], [caption]], colWidths=[3*inch])
|
| 226 |
+
cell.setStyle(TableStyle([
|
| 227 |
+
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
| 228 |
+
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
| 229 |
+
]))
|
| 230 |
+
row_images.append(cell)
|
| 231 |
+
except Exception as e:
|
| 232 |
+
print(f" β Could not load image {img_path}: {e}")
|
| 233 |
+
row_images.append('')
|
| 234 |
+
else:
|
| 235 |
+
row_images.append('')
|
| 236 |
+
|
| 237 |
+
if row_images:
|
| 238 |
+
image_table_data.append(row_images)
|
| 239 |
+
|
| 240 |
+
if image_table_data:
|
| 241 |
+
image_table = Table(image_table_data, colWidths=[3.3*inch, 3.3*inch])
|
| 242 |
+
image_table.setStyle(TableStyle([
|
| 243 |
+
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
| 244 |
+
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
| 245 |
+
('LEFTPADDING', (0, 0), (-1, -1), 10),
|
| 246 |
+
('RIGHTPADDING', (0, 0), (-1, -1), 10),
|
| 247 |
+
('TOPPADDING', (0, 0), (-1, -1), 15),
|
| 248 |
+
('BOTTOMPADDING', (0, 0), (-1, -1), 15),
|
| 249 |
+
]))
|
| 250 |
+
|
| 251 |
+
story.append(image_table)
|
| 252 |
+
|
| 253 |
+
doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer)
|
| 254 |
+
print(f"\n β PDF Created: {output_filename}")
|
| 255 |
+
print(f"{'='*70}\n")
|
| 256 |
+
|
| 257 |
+
output_dir = "generated_reports"
|
| 258 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 259 |
+
|
| 260 |
+
sample_reports = reports_df.head(5)
|
| 261 |
+
|
| 262 |
+
print("\n" + "="*70)
|
| 263 |
+
print(" RADIOLOGY REPORT GENERATOR")
|
| 264 |
+
print("="*70)
|
| 265 |
+
print(f" Total Reports: {len(sample_reports)}")
|
| 266 |
+
print(f" Output Directory: {output_dir}/")
|
| 267 |
+
print("="*70)
|
| 268 |
+
|
| 269 |
+
for idx, (_, report_row) in enumerate(sample_reports.iterrows(), 1):
|
| 270 |
+
uid = report_row['uid']
|
| 271 |
+
output_filename = f"{output_dir}/radiology_report_{idx}.pdf"
|
| 272 |
+
|
| 273 |
+
create_radiology_report(uid, report_row, output_filename, idx)
|
| 274 |
+
|
| 275 |
+
print("\n" + "="*70)
|
| 276 |
+
print(f" β ALL REPORTS GENERATED SUCCESSFULLY!")
|
| 277 |
+
print(f" β Location: {output_dir}/")
|
| 278 |
+
print("="*70 + "\n")
|