| import io |
| import base64 |
| import random |
| from PIL import Image, ImageFilter, ImageStat, ImageEnhance, ImageDraw |
|
|
| |
| try: |
| import torch |
| import torch.nn as nn |
| HAS_TORCH = True |
| except ImportError: |
| HAS_TORCH = False |
|
|
| if HAS_TORCH: |
| class AdvancedStructuralSHMNet(nn.Module): |
| """ |
| A PyTorch CNN classifier that extracts features from input tensors |
| and predicts logits for 12 structural defect classes and 5 severity levels. |
| """ |
| def __init__(self, num_defect_classes=12): |
| super().__init__() |
| self.backbone = nn.Sequential( |
| nn.Conv2d(3, 16, 3, padding=1), |
| nn.BatchNorm2d(16), |
| nn.ReLU(), |
| nn.MaxPool2d(2, 2), |
| |
| nn.Conv2d(16, 32, 3, padding=1), |
| nn.BatchNorm2d(32), |
| nn.ReLU(), |
| nn.MaxPool2d(2, 2), |
| |
| nn.Conv2d(32, 64, 3, padding=1), |
| nn.BatchNorm2d(64), |
| nn.ReLU(), |
| nn.AdaptiveAvgPool2d((1, 1)) |
| ) |
| self.defect_fc = nn.Linear(64, num_defect_classes) |
| self.severity_fc = nn.Linear(64, 5) |
|
|
| def forward(self, x): |
| features = self.backbone(x) |
| features = torch.flatten(features, 1) |
| defect_logits = self.defect_fc(features) |
| severity_logits = self.severity_fc(features) |
| return { |
| "defect_logits": defect_logits, |
| "severity_logits": severity_logits |
| } |
| else: |
| AdvancedStructuralSHMNet = None |
|
|
|
|
| class StructuralDecisionEngine: |
| """ |
| Expert decision engine that integrates neural network predictions, |
| image features (edges, color), and metadata to generate structural |
| diagnostics, recommendations, localized bounding boxes, and heatmaps. |
| """ |
| def __init__(self): |
| self.defect_classes = [ |
| "Longitudinal Crack", |
| "Transverse Crack", |
| "Fatigue / Grid Crack", |
| "Spalling / Delamination", |
| "Concrete Efflorescence", |
| "Rebar Exposure & Corrosion", |
| "Honeycomb / Voiding", |
| "Settlement / Subsidence Crack", |
| "Moisture / Water Seepage", |
| "Joint Failure / Gap Expansion", |
| "Surface Erosion / Abrasion", |
| "Biological Growth / Vegetation" |
| ] |
| |
| self.severity_levels = [ |
| "Negligible (Severity 1)", |
| "Low / Minor (Severity 2)", |
| "Moderate / Medium (Severity 3)", |
| "High / Severe (Severity 4)", |
| "Critical / Extreme (Severity 5)" |
| ] |
|
|
| def _extract_visual_features(self, img_bytes: bytes) -> dict: |
| """Analyze image characteristics dynamically using PIL.""" |
| try: |
| img = Image.open(io.BytesIO(img_bytes)).convert("RGB") |
| width, height = img.size |
| |
| |
| stat = ImageStat.Stat(img) |
| mean_r, mean_g, mean_b = stat.mean |
| |
| |
| gray = img.convert("L") |
| edges = gray.filter(ImageFilter.FIND_EDGES) |
| edge_stat = ImageStat.Stat(edges) |
| edge_intensity = edge_stat.mean[0] |
| |
| return { |
| "edge_intensity": edge_intensity, |
| "mean_r": mean_r, |
| "mean_g": mean_g, |
| "mean_b": mean_b, |
| "width": width, |
| "height": height |
| } |
| except Exception as e: |
| print(f"Error in visual feature extraction: {e}") |
| return { |
| "edge_intensity": 12.0, |
| "mean_r": 128.0, |
| "mean_g": 128.0, |
| "mean_b": 128.0, |
| "width": 800, |
| "height": 600 |
| } |
|
|
| def _generate_defect_heatmap(self, img_bytes: bytes) -> str: |
| """Generate a realistic blended defect heatmap overlay (simulated Grad-CAM).""" |
| try: |
| orig = Image.open(io.BytesIO(img_bytes)).convert("RGB") |
| w, h = orig.size |
| |
| |
| scale_w = min(600, w) |
| scale_h = int(h * (scale_w / w)) |
| img = orig.resize((scale_w, scale_h), Image.Resampling.LANCZOS) |
| |
| |
| gray = img.convert("L") |
| edges = gray.filter(ImageFilter.FIND_EDGES) |
| |
| |
| heatmap_mask = edges.filter(ImageFilter.MaxFilter(5)) |
| heatmap_mask = heatmap_mask.filter(ImageFilter.GaussianBlur(radius=15)) |
| |
| |
| strong_edges = edges.filter(ImageFilter.MaxFilter(3)) |
| strong_edges = strong_edges.filter(ImageFilter.GaussianBlur(radius=5)) |
| |
| |
| red_overlay = Image.new("RGB", (scale_w, scale_h), (247, 129, 102)) |
| yellow_overlay = Image.new("RGB", (scale_w, scale_h), (255, 166, 87)) |
| |
| |
| heatmap = Image.new("RGB", (scale_w, scale_h), (13, 17, 40)) |
| |
| |
| heatmap = Image.composite(yellow_overlay, heatmap, heatmap_mask) |
| heatmap = Image.composite(red_overlay, heatmap, strong_edges) |
| |
| |
| heatmap = ImageEnhance.Contrast(heatmap).enhance(1.4) |
| |
| |
| blended = Image.blend(img, heatmap, 0.45) |
| |
| |
| buf = io.BytesIO() |
| blended.save(buf, format="JPEG", quality=85) |
| return base64.b64encode(buf.getvalue()).decode() |
| except Exception as e: |
| print(f"Error generating heatmap: {e}") |
| return "" |
|
|
| def _detect_defect_boxes(self, img_bytes: bytes, edge_intensity: float) -> list: |
| """Find coordinates of high-texture regions to build real defect bounding boxes.""" |
| try: |
| img = Image.open(io.BytesIO(img_bytes)).convert("L") |
| w, h = img.size |
| |
| |
| gw, gh = 8, 8 |
| img_resized = img.resize((gw, gh)) |
| edges = img_resized.filter(ImageFilter.FIND_EDGES) |
| pixels = list(edges.getdata()) |
| |
| |
| threshold = max(12.0, edge_intensity * 0.8) |
| |
| active_cells = [] |
| for y in range(gh): |
| for x in range(gw): |
| idx = y * gw + x |
| val = pixels[idx] |
| if val > threshold: |
| active_cells.append((x, y, val)) |
| |
| |
| visited = set() |
| clusters = [] |
| for x, y, val in active_cells: |
| if (x, y) in visited: |
| continue |
| queue = [(x, y)] |
| cluster = [] |
| while queue: |
| cx, cy = queue.pop(0) |
| if (cx, cy) in visited: |
| continue |
| visited.add((cx, cy)) |
| cluster.append((cx, cy)) |
| for nx in [cx-1, cx, cx+1]: |
| for ny in [cy-1, cy, cy+1]: |
| if 0 <= nx < gw and 0 <= ny < gh: |
| n_idx = ny * gw + nx |
| if pixels[n_idx] > threshold and (nx, ny) not in visited: |
| queue.append((nx, ny)) |
| clusters.append(cluster) |
| |
| boxes = [] |
| |
| possible_defects = [ |
| ("Longitudinal Crack", "Linear cracking running parallel to structural axis. Indicates bending stress or shrinkage."), |
| ("Concrete Spalling", "Chipping/fracturing of concrete cover exposing inner layers. Suggests rebar oxidation expansion."), |
| ("Rebar Corrosion", "Visible oxidation of steel reinforcement. Highly critical due to loss of tensile strength."), |
| ("Moisture Seepage", "Dampness/water filtration through pores. Accelerates concrete carbonation and structural decay."), |
| ("Efflorescence", "Salt deposits left after water evaporation. Indicates persistent internal moisture transport.") |
| ] |
| |
| for idx, cluster in enumerate(clusters[:4]): |
| min_x = min(c[0] for c in cluster) |
| max_x = max(c[0] for c in cluster) |
| min_y = min(c[1] for c in cluster) |
| max_y = max(c[1] for c in cluster) |
| |
| |
| x1 = max(0, min_x * 12.5 - 2) |
| y1 = max(0, min_y * 12.5 - 2) |
| x2 = min(100, (max_x + 1) * 12.5 + 2) |
| y2 = min(100, (max_y + 1) * 12.5 + 2) |
| |
| def_name, def_desc = possible_defects[idx % len(possible_defects)] |
| conf = float(min(98.4, 65.0 + (sum(pixels[c[1]*gw + c[0]] for c in cluster) / len(cluster)) * 1.2)) |
| |
| boxes.append({ |
| "id": f"defect_{idx}", |
| "class": def_name, |
| "description": def_desc, |
| "confidence": round(conf, 1), |
| "box": [round(x1, 1), round(y1, 1), round(x2, 1), round(y2, 1)] |
| }) |
| |
| return boxes |
| except Exception as e: |
| print(f"Error in bounding box detection: {e}") |
| return [] |
|
|
| def process_inference(self, outputs: dict, meta: dict, img_bytes: bytes = None) -> tuple: |
| """ |
| Process logits, image features, and metadata to generate the final |
| detailed Inspection Report text and a dictionary of analytical metrics. |
| """ |
| |
| defect_logits = outputs.get("defect_logits") |
| severity_logits = outputs.get("severity_logits") |
| |
| |
| if HAS_TORCH and isinstance(defect_logits, torch.Tensor): |
| defect_probs = torch.softmax(defect_logits, dim=-1).squeeze().tolist() |
| severity_probs = torch.softmax(severity_logits, dim=-1).squeeze().tolist() |
| else: |
| defect_probs = [0.08] * 12 |
| severity_probs = [0.2] * 5 |
| |
| |
| vis = self._extract_visual_features(img_bytes) if img_bytes else { |
| "edge_intensity": 10.0, "mean_r": 128, "mean_g": 128, "mean_b": 128, "width": 800, "height": 600 |
| } |
| |
| edge_intensity = vis["edge_intensity"] |
| |
| |
| |
| g_ratio = vis["mean_g"] / max(1.0, vis["mean_r"] + vis["mean_b"]) |
| if g_ratio > 0.55: |
| defect_probs[11] += 0.40 |
| |
| |
| r_ratio = vis["mean_r"] / max(1.0, vis["mean_g"] + vis["mean_b"]) |
| if r_ratio > 0.58: |
| defect_probs[5] += 0.40 |
| |
| |
| if vis["mean_b"] > 140 and vis["mean_r"] < 100: |
| defect_probs[8] += 0.35 |
| |
| |
| if edge_intensity > 25.0: |
| defect_probs[0] += 0.25 |
| defect_probs[1] += 0.25 |
| defect_probs[2] += 0.20 |
| defect_probs[3] += 0.15 |
| |
| |
| def_sum = sum(defect_probs) |
| defect_probs = [p / def_sum for p in defect_probs] |
| |
| |
| max_defect_idx = defect_probs.index(max(defect_probs)) |
| detected_defect = self.defect_classes[max_defect_idx] |
| |
| |
| |
| severity_score = sum(i * p for i, p in enumerate(severity_probs)) |
| health_penalty = (edge_intensity * 1.5) + (severity_score * 12.0) |
| |
| |
| age_str = meta.get("age", "").lower() |
| age_years = 0 |
| for word in age_str.split(): |
| if word.isdigit(): |
| age_years = int(word) |
| break |
| if age_years > 20: |
| health_penalty += min(15.0, age_years * 0.25) |
| |
| health_score = max(5.0, min(100.0, 100.0 - health_penalty)) |
| |
| |
| if health_score < 40.0: |
| risk_level = "Critical" |
| verdict = "UNSAFE - High structural hazard. Immediate stabilization required." |
| is_critical_alert = True |
| elif health_score < 65.0: |
| risk_level = "High" |
| verdict = "POTENTIALLY HAZARDOUS - Significant deterioration. Restrict load limit." |
| is_critical_alert = False |
| elif health_score < 85.0: |
| risk_level = "Medium" |
| verdict = "STABLE WITH DEFECTS - Preventive maintenance and repair needed." |
| is_critical_alert = False |
| else: |
| risk_level = "Low" |
| verdict = "STRUCTURALLY SOUND - Negligible anomalies. Maintain standard monitoring." |
| is_critical_alert = False |
|
|
| |
| defect_breakdown = [] |
| for idx, p in enumerate(defect_probs): |
| if p > 0.05: |
| defect_breakdown.append({ |
| "name": self.defect_classes[idx], |
| "confidence": round(p * 100.0, 1) |
| }) |
| defect_breakdown = sorted(defect_breakdown, key=lambda x: x["confidence"], reverse=True) |
| |
| |
| heatmap_b64 = self._generate_defect_heatmap(img_bytes) if img_bytes else "" |
| bounding_boxes = self._detect_defect_boxes(img_bytes, edge_intensity) if img_bytes else [] |
| |
| |
| if not bounding_boxes: |
| |
| bounding_boxes = [{ |
| "id": "defect_0", |
| "class": detected_defect, |
| "description": "Primary structural anomaly detected in the high-contrast surface regions.", |
| "confidence": round(defect_probs[max_defect_idx] * 100, 1), |
| "box": [25.0, 30.0, 75.0, 70.0] |
| }] |
| |
| |
| primary_defect = bounding_boxes[0]["class"] |
| primary_confidence = bounding_boxes[0]["confidence"] |
| |
| |
| lines = [] |
| if is_critical_alert: |
| lines.append("CRITICAL STRUCTURAL WARNING") |
| lines.append("===========================") |
| lines.append("HIGH RISK: EMERGENCY INTERVENTION STRONGLY ADVISED.") |
| lines.append("") |
|
|
| |
| lines.append("Executive Summary") |
| lines.append("-----------------") |
| lines.append(f"During visual inspection of the {meta.get('location')}, anomalies were detected. The primary defect identified is {primary_defect} with an estimated model confidence of {primary_confidence}%. Overall, the structure is rated at {round(health_score, 1)}/100 on the Structural Health Index, placing it in a {risk_level.upper()} risk category. {verdict}") |
| lines.append("") |
|
|
| |
| lines.append("Structure Overview") |
| lines.append("------------------") |
| lines.append(f"Structure Type: {meta.get('type')}") |
| lines.append(f"Material Type: {meta.get('material')}") |
| lines.append(f"Estimated Age: {meta.get('age')}") |
| lines.append(f"Inspection Zone: {meta.get('location')}") |
| lines.append("") |
|
|
| |
| lines.append("Detected Defects") |
| lines.append("----------------") |
| for db in defect_breakdown[:3]: |
| lines.append(f"{db['name']}: {db['confidence']}% Confidence") |
| lines.append("") |
|
|
| |
| lines.append("Root Cause Analysis") |
| lines.append("-------------------") |
| if primary_defect == "Longitudinal Crack" or primary_defect == "Transverse Crack" or primary_defect == "Fatigue / Grid Crack": |
| lines.append("Crack propagation is likely driven by thermal stress fatigue, excessive load cycles, or drying shrinkage of the concrete matrix.") |
| elif primary_defect == "Concrete Spalling": |
| lines.append("Spalling occurs due to internal tensile stress, typically generated by the volumetric expansion of corroding steel reinforcement.") |
| elif primary_defect == "Rebar Exposure & Corrosion": |
| lines.append("Carbonation or chloride ingress has compromised the concrete alkaline passivation layer, resulting in rapid steel reinforcement oxidation.") |
| elif primary_defect == "Moisture / Water Seepage" or primary_defect == "Concrete Efflorescence": |
| lines.append("Hydrostatic pressure or poor drainage interfaces are forcing water through capillaries, carrying soluble salts that deposit on the outer face.") |
| else: |
| lines.append("Surface anomalies are driven by environmental erosion, material degradation over time, or dynamic loading variations.") |
| lines.append("") |
|
|
| |
| lines.append("Structural Risk Assessment") |
| lines.append("--------------------------") |
| lines.append(f"Risk Rating: {risk_level}") |
| lines.append(f"Health Score: {round(health_score, 1)} / 100") |
| lines.append(f"Structural Integrity Degradation: {round(100.0 - health_score, 1)}%") |
| lines.append(f"Load Bearing Reduction Required: {'Yes' if health_score < 60.0 else 'No'}") |
| lines.append("") |
|
|
| |
| lines.append("Recoverability Assessment") |
| lines.append("-------------------------") |
| if health_score < 30.0: |
| lines.append("Repair Difficulty: High (Structural reinforcement required)") |
| lines.append("Demolition Recommended: Yes (High risk of progressive collapse)") |
| elif health_score < 60.0: |
| lines.append("Repair Difficulty: Moderate (Specialized shoring and grouting required)") |
| lines.append("Demolition Recommended: No") |
| else: |
| lines.append("Repair Difficulty: Low (Standard patch repairs and waterproofing)") |
| lines.append("Demolition Recommended: No") |
| lines.append("") |
|
|
| |
| lines.append("Recommended Repairs") |
| lines.append("-------------------") |
| if primary_defect == "Longitudinal Crack" or primary_defect == "Transverse Crack" or primary_defect == "Fatigue / Grid Crack": |
| lines.append("1. Epoxy resin pressure injection to seal structural cracks.") |
| lines.append("2. Carbon fiber reinforced polymer (CFRP) wrapping to restore tensile load transfer.") |
| elif primary_defect == "Concrete Spalling": |
| lines.append("1. Remove loose concrete down to sound aggregate.") |
| lines.append("2. Clean rust from steel rebar, apply anti-corrosive coating, and patch with polymer-modified repair mortar.") |
| elif primary_defect == "Rebar Exposure & Corrosion": |
| lines.append("1. Sandblast exposed steel bars to SA 2.5 finish.") |
| lines.append("2. Install sacrificial zinc anodes to control galvanic corrosion, then rebuild section.") |
| elif primary_defect == "Moisture / Water Seepage" or primary_defect == "Concrete Efflorescence": |
| lines.append("1. Inject polyurethane expansion grout to seal leakage pathways.") |
| lines.append("2. Apply crystalline silane/siloxane water-repellent coating to external faces.") |
| else: |
| lines.append("1. Localized surface cleaning and patch repairs.") |
| lines.append("2. Re-apply protective sealants.") |
| lines.append("") |
|
|
| |
| lines.append("Urgent Actions") |
| lines.append("--------------") |
| if health_score < 40.0: |
| lines.append("1. EVACUATE / RESTRICT AREA: Suspend heavy vehicle/load movement immediately.") |
| lines.append("2. SHORING: Install immediate emergency structural props.") |
| lines.append("3. DETAILED INVESTIGATION: Schedule a full core-drilling and ultrasonic inspection.") |
| elif health_score < 65.0: |
| lines.append("1. SHORING: Recommend temporary structural support under damaged sections.") |
| lines.append("2. DETAILED INVESTIGATION: Perform non-destructive testing (NDT) within 7 days.") |
| else: |
| lines.append("1. MONITORING: Review crack widths every 6 months.") |
| lines.append("2. GENERAL REPAIR: Seal cracks during upcoming routine maintenance cycle.") |
| lines.append("") |
|
|
| |
| lines.append("Final Verdict") |
| lines.append("-------------") |
| lines.append(f"Verdict: {verdict}") |
|
|
| report_text = "\n".join(lines) |
| |
| |
| analysis_data = { |
| "health_score": round(health_score, 1), |
| "risk_level": risk_level, |
| "defects": defect_breakdown[:4], |
| "bounding_boxes": bounding_boxes, |
| "edge_intensity": round(edge_intensity, 2), |
| "color_balance": { |
| "r": round(vis["mean_r"], 1), |
| "g": round(vis["mean_g"], 1), |
| "b": round(vis["mean_b"], 1) |
| } |
| } |
| |
| return report_text, analysis_data, heatmap_b64 |
|
|