Spaces:
Sleeping
Sleeping
| """ | |
| Assnani Dental Chatbot — Symptom-to-X-ray Correlation Engine | |
| Matches patient-reported symptoms with YOLO detection findings | |
| and generates natural language explanations. | |
| """ | |
| class CorrelationEngine: | |
| """ | |
| Correlates patient symptoms with YOLO X-ray detection results. | |
| Produces human-readable explanations linking complaints to findings. | |
| """ | |
| # Mapping of detection classes to symptom-relevant conditions | |
| CONDITION_MAP = { | |
| "cavity": { | |
| "clinical_name": "Dental Caries (Cavity)", | |
| "description": "tooth decay that has damaged the tooth structure", | |
| "pain_correlations": { | |
| "throbbing": { | |
| "explanation": "The cavity may have reached the tooth nerve (pulp), causing inflammation (pulpitis) and throbbing pain.", | |
| "severity": "high", | |
| "urgency": "Prompt treatment recommended to prevent further nerve damage." | |
| }, | |
| "sharp": { | |
| "explanation": "The cavity has exposed sensitive tooth layers (dentin), causing sharp pain when stimulated.", | |
| "severity": "moderate", | |
| "urgency": "Treatment recommended within a few days." | |
| }, | |
| "dull": { | |
| "explanation": "The cavity may be causing chronic low-grade inflammation in the tooth.", | |
| "severity": "moderate", | |
| "urgency": "Schedule a dental visit for evaluation." | |
| }, | |
| "sensitivity": { | |
| "explanation": "The cavity has likely penetrated the enamel, exposing the dentin layer which is sensitive to temperature changes.", | |
| "severity": "moderate", | |
| "urgency": "A dental filling can resolve this sensitivity." | |
| }, | |
| }, | |
| "swelling_correlation": { | |
| "explanation": "Deep decay may have led to an infection at the tooth root (periapical abscess), causing swelling.", | |
| "severity": "high", | |
| "urgency": "Urgent dental care is needed. Antibiotics and root canal treatment may be required." | |
| }, | |
| "default_correlation": { | |
| "explanation": "Dental caries (cavity) was detected on this tooth. Even without current symptoms, untreated cavities can progress and cause pain.", | |
| "severity": "moderate", | |
| "urgency": "Treatment with a dental filling is recommended." | |
| } | |
| }, | |
| "impacted": { | |
| "clinical_name": "Impacted Tooth", | |
| "description": "a tooth that has not fully erupted and is trapped in the jawbone or gum tissue", | |
| "pain_correlations": { | |
| "throbbing": { | |
| "explanation": "The impacted tooth may be pressing against adjacent teeth or causing inflammation in surrounding tissue (pericoronitis).", | |
| "severity": "high", | |
| "urgency": "Surgical extraction may be necessary to relieve pressure." | |
| }, | |
| "sharp": { | |
| "explanation": "The impacted tooth may be causing pressure on the nerve of an adjacent tooth.", | |
| "severity": "moderate", | |
| "urgency": "Evaluation for possible extraction recommended." | |
| }, | |
| "dull": { | |
| "explanation": "The impacted tooth is causing chronic pressure in the jaw area.", | |
| "severity": "moderate", | |
| "urgency": "Monitor and consult with an oral surgeon." | |
| }, | |
| "sensitivity": { | |
| "explanation": "Partial eruption of the impacted tooth may be trapping food and causing decay on adjacent teeth.", | |
| "severity": "moderate", | |
| "urgency": "Evaluation for extraction recommended." | |
| }, | |
| }, | |
| "swelling_correlation": { | |
| "explanation": "The impacted tooth has likely caused pericoronitis — an infection of the gum tissue around the partially erupted tooth, resulting in swelling.", | |
| "severity": "high", | |
| "urgency": "Antibiotics may be needed, followed by surgical extraction." | |
| }, | |
| "default_correlation": { | |
| "explanation": "An impacted tooth was detected. Impacted teeth can cause problems over time including cysts, infection, or damage to neighboring teeth.", | |
| "severity": "moderate", | |
| "urgency": "Consultation with an oral surgeon is recommended." | |
| } | |
| }, | |
| "filling": { | |
| "clinical_name": "Existing Dental Filling", | |
| "description": "a previously placed dental restoration", | |
| "pain_correlations": { | |
| "throbbing": { | |
| "explanation": "The existing filling may have developed secondary caries (new decay) underneath, potentially reaching the nerve.", | |
| "severity": "high", | |
| "urgency": "The filling may need to be replaced. Root canal treatment might be necessary." | |
| }, | |
| "sharp": { | |
| "explanation": "The filling may have a marginal gap or fracture, allowing stimuli to reach the underlying tooth structure.", | |
| "severity": "moderate", | |
| "urgency": "The filling should be evaluated and possibly replaced." | |
| }, | |
| "dull": { | |
| "explanation": "The existing filling may be deteriorating, causing low-grade irritation to the tooth.", | |
| "severity": "low", | |
| "urgency": "Schedule an evaluation to assess filling integrity." | |
| }, | |
| "sensitivity": { | |
| "explanation": "The filling margins may have opened, allowing temperature changes to reach the sensitive dentin layer.", | |
| "severity": "moderate", | |
| "urgency": "The filling may need replacement with a new restoration." | |
| }, | |
| }, | |
| "swelling_correlation": { | |
| "explanation": "Secondary infection may have developed beneath the existing filling, causing swelling.", | |
| "severity": "high", | |
| "urgency": "Urgent evaluation needed. The filling must be removed and the tooth assessed." | |
| }, | |
| "default_correlation": { | |
| "explanation": "An existing dental filling was detected. Regular monitoring is recommended to check for secondary caries and filling integrity.", | |
| "severity": "low", | |
| "urgency": "Routine clinical monitoring at regular checkups." | |
| } | |
| }, | |
| "implant": { | |
| "clinical_name": "Dental Implant", | |
| "description": "a previously placed dental implant", | |
| "pain_correlations": { | |
| "throbbing": { | |
| "explanation": "Throbbing pain near an implant may indicate peri-implantitis — inflammation and possible bone loss around the implant.", | |
| "severity": "high", | |
| "urgency": "Urgent evaluation by a periodontist is recommended." | |
| }, | |
| "sharp": { | |
| "explanation": "Sharp pain near the implant area may indicate the implant is affecting adjacent tooth nerves or has a loose component.", | |
| "severity": "moderate", | |
| "urgency": "Evaluation to check implant stability and adjacent structures." | |
| }, | |
| "dull": { | |
| "explanation": "Dull discomfort around an implant may indicate early signs of peri-implant mucositis (gum inflammation).", | |
| "severity": "moderate", | |
| "urgency": "Schedule evaluation to assess implant health." | |
| }, | |
| "sensitivity": { | |
| "explanation": "While implants themselves don't have nerves, sensitivity near the implant may be from adjacent natural teeth being affected.", | |
| "severity": "low", | |
| "urgency": "Monitor and report if symptoms worsen." | |
| }, | |
| }, | |
| "swelling_correlation": { | |
| "explanation": "Swelling around the implant strongly suggests peri-implantitis — a serious condition that can lead to implant failure and bone loss.", | |
| "severity": "high", | |
| "urgency": "Immediate evaluation by a periodontist or implant specialist is critical." | |
| }, | |
| "default_correlation": { | |
| "explanation": "A dental implant was detected. Regular maintenance is important to prevent peri-implantitis and ensure long-term success.", | |
| "severity": "low", | |
| "urgency": "Routine monitoring of bone levels and implant stability." | |
| } | |
| } | |
| } | |
| # Quadrant mapping based on X-ray image coordinates | |
| # Standard dental X-ray: upper teeth at top, lower at bottom | |
| # Patient's right = image left, Patient's left = image right (mirrored) | |
| QUADRANT_MAP = { | |
| "upper-right": "Upper Right (Quadrant 1, teeth 11-18)", | |
| "upper-left": "Upper Left (Quadrant 2, teeth 21-28)", | |
| "lower-left": "Lower Left (Quadrant 3, teeth 31-38)", | |
| "lower-right": "Lower Right (Quadrant 4, teeth 41-48)", | |
| } | |
| def correlate(self, symptoms: dict, detections: list, image_width: int = 0, image_height: int = 0) -> dict: | |
| """ | |
| Correlate patient symptoms with YOLO detection results. | |
| Args: | |
| symptoms: Patient symptom data from the chatbot conversation | |
| detections: List of detection dicts from YOLO API | |
| image_width: Width of the X-ray image (for quadrant mapping) | |
| image_height: Height of the X-ray image (for quadrant mapping) | |
| Returns: | |
| dict with correlations, unmatched symptoms, and unmatched findings | |
| """ | |
| pain_location = symptoms.get("pain_location", "").lower() | |
| pain_type = symptoms.get("pain_type", "").lower() | |
| has_swelling = symptoms.get("has_swelling", False) | |
| has_pain = symptoms.get("has_pain", False) | |
| correlations = [] | |
| matched_detection_indices = set() | |
| for idx, det in enumerate(detections): | |
| cls = det.get("class_name", "").lower() | |
| conf = det.get("confidence", 0.0) | |
| condition = self.CONDITION_MAP.get(cls) | |
| if not condition: | |
| continue | |
| # Determine detection quadrant from bounding box position | |
| det_quadrant = self._get_quadrant(det, image_width, image_height) | |
| det_region_label = self.QUADRANT_MAP.get(det_quadrant, det_quadrant) | |
| # Check if symptom location matches detection region | |
| location_match = self._locations_match(pain_location, det_quadrant) | |
| correlation_entry = { | |
| "detection_index": idx, | |
| "class_name": cls, | |
| "clinical_name": condition["clinical_name"], | |
| "confidence": conf, | |
| "confidence_label": self._confidence_label(conf), | |
| "detection_region": det_region_label, | |
| "location_match": location_match, | |
| "severity": "low", | |
| "explanation": "", | |
| "urgency": "", | |
| } | |
| if location_match and has_pain and pain_type in condition["pain_correlations"]: | |
| # Strong correlation: location + pain type match | |
| corr_data = condition["pain_correlations"][pain_type] | |
| correlation_entry["explanation"] = ( | |
| f"You mentioned {pain_type} pain in the {pain_location} region. " | |
| f"Our AI detected {condition['description']} in the same area " | |
| f"({conf:.0%} confidence). {corr_data['explanation']}" | |
| ) | |
| correlation_entry["severity"] = corr_data["severity"] | |
| correlation_entry["urgency"] = corr_data["urgency"] | |
| correlation_entry["match_type"] = "strong" | |
| matched_detection_indices.add(idx) | |
| elif location_match and has_swelling: | |
| # Swelling correlation | |
| corr_data = condition["swelling_correlation"] | |
| correlation_entry["explanation"] = ( | |
| f"You reported swelling in the {pain_location} region. " | |
| f"Our AI detected {condition['description']} in that area " | |
| f"({conf:.0%} confidence). {corr_data['explanation']}" | |
| ) | |
| correlation_entry["severity"] = corr_data["severity"] | |
| correlation_entry["urgency"] = corr_data["urgency"] | |
| correlation_entry["match_type"] = "swelling" | |
| matched_detection_indices.add(idx) | |
| elif location_match: | |
| # Location match but no specific symptom correlation | |
| corr_data = condition["default_correlation"] | |
| correlation_entry["explanation"] = ( | |
| f"Our AI detected {condition['description']} in the {pain_location} region " | |
| f"({conf:.0%} confidence). {corr_data['explanation']}" | |
| ) | |
| correlation_entry["severity"] = corr_data["severity"] | |
| correlation_entry["urgency"] = corr_data["urgency"] | |
| correlation_entry["match_type"] = "location" | |
| matched_detection_indices.add(idx) | |
| else: | |
| # No location match — still report the finding | |
| corr_data = condition["default_correlation"] | |
| correlation_entry["explanation"] = ( | |
| f"Our AI also detected {condition['description']} in the {det_region_label} " | |
| f"({conf:.0%} confidence). {corr_data['explanation']}" | |
| ) | |
| correlation_entry["severity"] = corr_data["severity"] | |
| correlation_entry["urgency"] = corr_data["urgency"] | |
| correlation_entry["match_type"] = "additional" | |
| correlations.append(correlation_entry) | |
| # Sort: strong matches first, then by severity | |
| severity_order = {"high": 0, "moderate": 1, "low": 2} | |
| match_order = {"strong": 0, "swelling": 1, "location": 2, "additional": 3} | |
| correlations.sort(key=lambda c: ( | |
| match_order.get(c.get("match_type", "additional"), 4), | |
| severity_order.get(c["severity"], 3) | |
| )) | |
| # Check for unmatched symptoms | |
| unmatched_symptoms = [] | |
| if has_pain and not any(c.get("match_type") in ["strong", "swelling", "location"] for c in correlations): | |
| unmatched_symptoms.append( | |
| f"You reported {pain_type} pain in the {pain_location} region, but our AI " | |
| f"did not detect any specific findings in that exact area. This could mean " | |
| f"the condition is not visible on X-ray (e.g., cracked tooth, soft tissue issue) " | |
| f"or may require a different imaging angle. A clinical examination is recommended." | |
| ) | |
| return { | |
| "correlations": correlations, | |
| "unmatched_symptoms": unmatched_symptoms, | |
| "total_detections": len(detections), | |
| "total_correlations": len([c for c in correlations if c.get("match_type") != "additional"]), | |
| } | |
| def _get_quadrant(self, detection: dict, img_width: int, img_height: int) -> str: | |
| """ | |
| Estimate dental quadrant from bounding box position. | |
| Standard panoramic X-ray orientation: | |
| - Top half = upper teeth, Bottom half = lower teeth | |
| - Left side of image = patient's right, Right side = patient's left | |
| """ | |
| if img_width <= 0 or img_height <= 0: | |
| # If image dimensions unknown, return generic | |
| return "unknown" | |
| cx = detection.get("x", (detection.get("x1", 0) + detection.get("x2", 0)) / 2) | |
| cy = detection.get("y", (detection.get("y1", 0) + detection.get("y2", 0)) / 2) | |
| is_upper = cy < img_height / 2 | |
| is_right_side = cx < img_width / 2 # Image left = patient's right | |
| if is_upper and is_right_side: | |
| return "upper-right" | |
| elif is_upper and not is_right_side: | |
| return "upper-left" | |
| elif not is_upper and not is_right_side: | |
| return "lower-left" | |
| else: | |
| return "lower-right" | |
| def _locations_match(self, symptom_location: str, detection_quadrant: str) -> bool: | |
| """Check if the patient's reported pain location matches the detection quadrant.""" | |
| if not symptom_location or detection_quadrant == "unknown": | |
| # If no location specified or unknown quadrant, consider it a potential match | |
| return True | |
| symptom_loc = symptom_location.lower().replace(" ", "-") | |
| # Direct match | |
| if symptom_loc == detection_quadrant: | |
| return True | |
| # Partial matches (e.g., "lower" matches both lower-left and lower-right) | |
| if symptom_loc in ["upper", "top"] and "upper" in detection_quadrant: | |
| return True | |
| if symptom_loc in ["lower", "bottom"] and "lower" in detection_quadrant: | |
| return True | |
| if symptom_loc in ["left"] and "left" in detection_quadrant: | |
| return True | |
| if symptom_loc in ["right"] and "right" in detection_quadrant: | |
| return True | |
| if symptom_loc in ["all", "everywhere", "general"]: | |
| return True | |
| return False | |
| def _confidence_label(self, confidence: float) -> str: | |
| """Return a human-readable confidence label.""" | |
| if confidence >= 0.85: | |
| return "high confidence" | |
| elif confidence >= 0.70: | |
| return "good confidence" | |
| elif confidence >= 0.50: | |
| return "moderate confidence" | |
| else: | |
| return "low confidence" | |