| from __future__ import annotations |
|
|
| from src.schemas.detection import Finding, LocalizedFinding |
|
|
|
|
| |
| |
| |
|
|
| def _pct(v: int) -> str: |
| return f"{v / 10:.0f}%" |
|
|
|
|
| def _bbox_description(box_2d: list[int]) -> str: |
| """Convert [y0, x0, y1, x1] in [0,1000] to a human-readable region string.""" |
| y0, x0, y1, x1 = box_2d |
| w = (x1 - x0) / 10 |
| h = (y1 - y0) / 10 |
| return ( |
| f"region top {_pct(y0)} β bottom {_pct(y1)}, " |
| f"left {_pct(x0)} β right {_pct(x1)} " |
| f"(spans {w:.0f}% wide Γ {h:.0f}% tall of image)" |
| ) |
|
|
|
|
| FINDING_DISCOVERY_PROMPT = """Instructions: |
| Review all provided chest X-ray images from the same radiographic study and identify visible positive abnormal radiographic findings. |
| |
| Study interpretation rules: |
| 1. All provided images belong to the same patient and the same radiographic study. |
| 2. Treat all provided images as complementary views of one examination. |
| 3. Integrate visual evidence across all provided views. |
| 4. Do not treat each image as a separate patient or separate study. |
| 5. Return one consolidated study-level list of findings. |
| 6. Do not duplicate the same finding only because it is visible in multiple views. |
| 7. A finding does not need to be equally visible in every view. |
| 8. If a finding is clearly visible or reasonably supported in at least one provided view, it may be included even when it is subtle, obscured, or not confidently visible in another view. |
| 9. Use additional views as complementary evidence, not as a veto against a finding visible in one view. |
| 10. Analyze image evidence only. |
| 11. Do not assume access to a radiology report, prior report, patient history, laboratory data, or clinical diagnosis. |
| 12. The provided images are different views from the same examination and are not longitudinal studies. |
| 13. Do not infer temporal descriptions such as "new", "increased", "recurrent", "improved", or "worsened" unless actual prior-study images are explicitly supplied separately. |
| |
| Finding rules: |
| 1. Do not generate a radiology report. |
| 2. Do not provide a final clinical diagnosis. |
| 3. Prefer radiographic finding terminology over disease diagnosis terminology. |
| 4. Examples of preferred findings include: |
| - pneumothorax |
| - pleural effusion |
| - focal airspace opacity |
| - consolidation |
| - atelectatic opacity |
| - pulmonary nodule |
| - hilar enlargement |
| - visible fracture |
| 5. Include only positive or suspected visible abnormal findings. |
| 6. Do not include normal anatomical observations. |
| 7. Do not include negative findings such as: |
| - no pneumothorax |
| - no pleural effusion |
| - lungs are clear |
| - heart size is normal |
| 8. For "certainty", use exactly one of: |
| - positive |
| - probable |
| - questionable |
| 9. "Left" and "right" refer to the patient's anatomical left and right. |
| 10. Keep the anatomical location concise and image-grounded. |
| 11. If no positive abnormal finding is visible across the study, return: |
| {"findings":[]} |
| |
| STRICT OUTPUT CONTRACT: |
| Your entire response MUST be exactly one raw JSON object. |
| |
| The first non-whitespace character MUST be "{" |
| and the last non-whitespace character MUST be "}". |
| |
| Do not output: |
| - reasoning |
| - analysis |
| - explanations |
| - headings |
| - markdown |
| - code fences |
| - "FINDINGS:" |
| - "Consolidated list of findings:" |
| - text before the JSON |
| - text after the JSON |
| |
| Do not return a top-level JSON list. |
| |
| Incorrect output: |
| |
| [ |
| { |
| "finding": "nodule", |
| "anatomical_location": "right upper lobe", |
| "certainty": "positive" |
| } |
| ] |
| |
| Correct output: |
| |
| { |
| "findings": [ |
| { |
| "finding": "nodule", |
| "anatomical_location": "right upper lobe", |
| "certainty": "positive" |
| } |
| ] |
| } |
| |
| Return exactly this schema: |
| |
| { |
| "findings": [ |
| { |
| "finding": "radiographic finding", |
| "anatomical_location": "concise anatomical location", |
| "certainty": "positive" |
| } |
| ] |
| } |
| |
| Review all provided views and return only the raw JSON object. |
| """ |
|
|
|
|
| def build_localization_prompt(finding: Finding) -> str: |
| return f"""Instructions: |
| You are given one chest X-ray image and one target radiographic finding that |
| was identified during a study-level review. |
| |
| Target finding: |
| "{finding.finding}" |
| |
| Reported anatomical location: |
| "{finding.anatomical_location}" |
| |
| Your task is to localize the VISIBLE IMAGE REGION corresponding specifically |
| to the target radiographic finding in this single image. |
| |
| IMPORTANT STUDY-LEVEL CONTEXT: |
| The target finding may have been identified using additional chest X-ray views |
| from the same radiographic study. |
| |
| You are now examining ONE specific view only. |
| |
| The finding does not have to be visible in this view. |
| |
| If the target finding is not visually identifiable in the current image, |
| return an empty JSON list: |
| |
| [] |
| |
| CRITICAL LOCALIZATION RULES: |
| 1. A bounding box must represent a visually identifiable instance of the |
| target radiographic finding. |
| 2. Anatomical proximity alone is NOT sufficient visual evidence. |
| 3. Do not return a box merely because the expected anatomical structure is |
| visible. |
| 4. Do not box the entire lung, lobe, hemithorax, shoulder, mediastinum, |
| cardiac silhouette, or other broad anatomical region unless the target |
| abnormal visual pattern is genuinely diffuse throughout that region. |
| 5. Do not substitute another abnormality for the requested target. |
| 6. Do not infer a bounding box from the finding name alone. |
| 7. If you cannot identify a visually supportable target region, return []. |
| 8. It is preferable to return [] rather than provide a speculative, |
| approximate, anatomy-only, or forced bounding box. |
| 9. Do not force a localization merely because the target finding was identified |
| at the study level. |
| 10. The bounding box must be as tight as reasonably possible around the |
| visible abnormal finding. |
| |
| FOCAL FINDING RULES: |
| For focal findings such as: |
| - pulmonary nodule |
| - nodular opacity |
| - focal opacity |
| - fracture |
| - focal consolidation |
| |
| the bounding box should tightly surround the focal abnormality. |
| |
| Do not return a large box covering the expected anatomical region. |
| |
| DIFFUSE FINDING RULES: |
| For findings that may be spatially diffuse, such as: |
| - pleural effusion |
| - diffuse opacity |
| - atelectatic change |
| |
| a larger bounding box is allowed only when the abnormal visual pattern itself |
| is visibly distributed throughout the boxed region. |
| |
| LEFT/RIGHT RULE: |
| "Left" and "right" always refer to the patient's anatomical left and right, |
| not the displayed image side. |
| |
| BOUNDING BOX FORMAT: |
| Use: |
| |
| [y0, x0, y1, x1] |
| |
| where: |
| |
| (y0, x0) = top-left |
| (y1, x1) = bottom-right |
| |
| Normalize every coordinate to: |
| |
| [0, 1000] |
| |
| The coordinates refer to the exact square-padded image supplied with this |
| query. |
| |
| OUTPUT CONTRACT: |
| Return raw JSON only. |
| |
| If the target is visually localizable: |
| |
| [ |
| {{ |
| "box_2d": [y0, x0, y1, x1], |
| "label": "{finding.finding}" |
| }} |
| ] |
| |
| If the target is not visually localizable: |
| |
| [] |
| |
| Do not output: |
| - reasoning |
| - explanation |
| - headings |
| - markdown |
| - code fences |
| - text before JSON |
| - text after JSON |
| |
| Localize the target only when it is visually supported in this image. |
| """ |
|
|
|
|
| def build_report_prompt( |
| findings: list[LocalizedFinding], |
| masks: list | None = None, |
| ) -> str: |
| |
| mask_map: dict[tuple[str, int], object] = {} |
| for m in (masks or []): |
| mask_map[(m.finding_label, m.image_index)] = m |
|
|
| _VIEW_LABEL = {0: "PA", 1: "Lateral"} |
|
|
| if not findings: |
| findings_text = "No positive abnormal radiographic findings were detected." |
| else: |
| blocks: list[str] = [] |
| for idx, f in enumerate(findings, 1): |
| block: list[str] = [ |
| f"Finding {idx}: {f.finding}", |
| f" Certainty : {f.certainty.title()}", |
| f" Anatomical region : {f.anatomical_location}", |
| ] |
|
|
| for v in f.localizations: |
| view_name = _VIEW_LABEL.get(v.image_index, f"View {v.image_index}") |
| prefix = f" View {v.image_index} ({view_name})" |
|
|
| if v.status == "localized" and v.boxes: |
| bbox_desc = _bbox_description(v.boxes[0].box_2d) |
| block.append(f"{prefix} β Localized | {bbox_desc}") |
|
|
| |
| seg = mask_map.get((f.finding, v.image_index)) |
| if seg and getattr(seg, "status", None) == "success": |
| poly = getattr(seg, "polygon", None) |
| n_pts = len(poly) if poly else 0 |
| |
| if poly and n_pts >= 3: |
| xs = [p[0] for p in poly] |
| ys = [p[1] for p in poly] |
| |
| |
| |
| y0, x0, y1, x1 = v.boxes[0].box_2d |
| bbox_w = max(x1 - x0, 1) |
| bbox_h = max(y1 - y0, 1) |
| block.append( |
| f"{'':12} Segmentation (Stage 2): pixel-level mask captured β " |
| f"{n_pts} boundary points, " |
| f"located within the {_pct(x1-x0)} Γ {_pct(y1-y0)} bounding region" |
| ) |
| else: |
| block.append( |
| f"{'':12} Segmentation (Stage 2): completed (mask captured)" |
| ) |
| else: |
| block.append( |
| f"{'':12} Segmentation (Stage 2): not performed for this view" |
| ) |
|
|
| elif v.status == "abstained": |
| block.append(f"{prefix} β Finding not visible in this view") |
| elif v.status == "rejected_by_quality_gate": |
| bbox_desc = "" |
| if v.candidate_boxes: |
| bbox_desc = f" | candidate region: {_bbox_description(v.candidate_boxes[0].box_2d)}" |
| block.append( |
| f"{prefix} β Detected but spatial localization uncertain{bbox_desc}" |
| ) |
| else: |
| block.append(f"{prefix} β {v.status}") |
|
|
| blocks.append("\n".join(block)) |
|
|
| findings_text = "\n\n".join(blocks) |
|
|
| return f"""You are an expert radiologist reviewing a chest X-ray study. |
| |
| The following radiographic findings were automatically detected: |
| |
| {findings_text} |
| |
| Based on the provided chest X-ray image(s) and the quantitative analysis data above \ |
| (spatial bounding regions, segmentation coverage), write a professional, concise, and \ |
| clinically accurate structured radiology report. |
| |
| Apply standard radiology reporting standards to each field below: |
| |
| - "study_type": Identify the imaging modality and views \ |
| (e.g. "Chest X-Ray PA View", "Chest X-Ray PA and Lateral Views"). |
| |
| - "summary": One concise sentence summarizing the overall study result. |
| |
| - "main_findings": JSON array of strings. \ |
| List each key abnormal finding briefly, as you would name them in a FINDINGS \ |
| section header. Use ["No significant abnormalities detected"] if the study \ |
| is unremarkable. |
| |
| - "detail_findings": JSON array of strings. \ |
| For each finding, write a detailed radiological description β one string per finding. \ |
| Use the bounding region percentages and segmentation data provided above to describe \ |
| the precise location, extent, and distribution of each abnormality \ |
| (e.g. occupying the lower-third of the left hemithorax). \ |
| Translate percentage coordinates into anatomical terms \ |
| (e.g. "top 55% to 85% left 10% to 45%" β "lower left hemithorax extending to the \ |
| costophrenic angle"). \ |
| Include appearance, severity, and any multi-view corroboration. \ |
| If segmentation was performed, reference the extent of the segmented region. \ |
| If no findings, state that the study is unremarkable. |
| |
| - "impression": Summarize the key findings and their clinical significance \ |
| in 1-3 sentences β written as you would in an IMPRESSION section. |
| |
| - "recommendations": Suggested clinical follow-up, correlation with history, \ |
| or additional imaging if warranted (string). |
| |
| - "additional_informations": Any other relevant radiological observations, \ |
| incidental findings, or technical notes not covered above. \ |
| Use "None" if not applicable. |
| |
| Output STRICTLY a single JSON object with EXACT keys: |
| "study_type", "summary", "main_findings", "detail_findings", |
| "impression", "recommendations", "additional_informations" |
| |
| Example output: |
| {{ |
| "study_type": "Chest X-Ray PA and Lateral Views", |
| "summary": "The study demonstrates a moderate right pleural effusion with \ |
| associated right lower lobe atelectasis.", |
| "main_findings": [ |
| "Moderate right pleural effusion", |
| "Right lower lobe atelectasis" |
| ], |
| "detail_findings": [ |
| "A moderate right pleural effusion is present, blunting the right \ |
| costophrenic angle and tracking along the lateral chest wall.", |
| "Right lower lobe atelectatic change is noted, likely compressive \ |
| in etiology secondary to the adjacent pleural effusion." |
| ], |
| "impression": "Moderate right pleural effusion with associated compressive \ |
| right lower lobe atelectasis. No pneumothorax or focal consolidation \ |
| identified. Clinical correlation is recommended.", |
| "recommendations": "Correlation with clinical history and laboratory \ |
| findings. Consider diagnostic or therapeutic thoracentesis if clinically \ |
| indicated. Repeat chest radiograph following intervention to assess response.", |
| "additional_informations": "Cardiac silhouette is at the upper limit of \ |
| normal in size. No acute osseous abnormality identified." |
| }} |
| |
| STRICT OUTPUT CONTRACT: |
| Use double quotes for all keys and string values. |
| No markdown, no prose, no code fences, no text before or after the JSON. |
| The first character of your output MUST be "{{" and the last MUST be "}}". |
| """ |
|
|