File size: 18,136 Bytes
c0e7c98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
"""
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"