| import os |
| import cv2 |
| import numpy as np |
| import gradio as gr |
| import tempfile |
| import urllib.request |
| from datetime import datetime |
| import matplotlib |
| matplotlib.use("Agg") |
| import matplotlib.pyplot as plt |
| from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas |
| from matplotlib.figure import Figure |
| from reportlab.lib.pagesizes import A4 |
| from reportlab.lib import colors |
| from reportlab.lib.units import cm |
| from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image, Table, TableStyle, HRFlowable |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle |
| from reportlab.lib.enums import TA_CENTER, TA_LEFT |
|
|
|
|
| |
| |
| |
| |
| def detect_cracks_hybrid(frame, sensitivity=0.25): |
| h, w = frame.shape[:2] |
| annotated = frame.copy() |
|
|
| |
| hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) |
|
|
| |
| mask_orange = cv2.inRange(hsv, np.array([5, 100, 150]), np.array([25, 255, 255])) |
| |
| mask_red1 = cv2.inRange(hsv, np.array([0, 120, 150]), np.array([5, 255, 255])) |
| mask_red2 = cv2.inRange(hsv, np.array([170,120, 150]), np.array([180,255, 255])) |
| |
| mask_yellow = cv2.inRange(hsv, np.array([20, 100, 150]), np.array([35, 255, 255])) |
|
|
| thermal_mask = cv2.bitwise_or(mask_orange, mask_red1) |
| thermal_mask = cv2.bitwise_or(thermal_mask, mask_red2) |
| thermal_mask = cv2.bitwise_or(thermal_mask, mask_yellow) |
|
|
| |
| gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) |
| blur = cv2.GaussianBlur(gray, (5, 5), 0) |
|
|
| |
| low_thresh = int(30 + (1 - sensitivity) * 70) |
| high_thresh = int(100 + (1 - sensitivity) * 150) |
| edges = cv2.Canny(blur, low_thresh, high_thresh) |
|
|
| |
| kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) |
| edges = cv2.dilate(edges, kernel, iterations=1) |
|
|
| |
| |
| thermal_pixels = np.count_nonzero(thermal_mask) |
| edge_pixels = np.count_nonzero(edges) |
| total_pixels = w * h |
|
|
| |
| crack_ratio = min( |
| (thermal_pixels * 0.7 + edge_pixels * 0.3) / total_pixels, |
| 1.0 |
| ) |
|
|
| |
| |
| if thermal_pixels > 0: |
| overlay = annotated.copy() |
| overlay[thermal_mask > 0] = [0, 80, 255] |
| annotated = cv2.addWeighted(annotated, 0.55, overlay, 0.45, 0) |
|
|
| |
| edge_colored = np.zeros_like(annotated) |
| edge_colored[edges > 0] = [0, 220, 255] |
| annotated = cv2.addWeighted(annotated, 0.85, edge_colored, 0.15, 0) |
|
|
| |
| combined_mask = cv2.bitwise_or(thermal_mask, edges) |
| contours, _ = cv2.findContours(combined_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) |
| num_detections = 0 |
| for cnt in contours: |
| area = cv2.contourArea(cnt) |
| if area > 200: |
| num_detections += 1 |
| x, y, cw, ch = cv2.boundingRect(cnt) |
| cv2.rectangle(annotated, (x, y), (x+cw, y+ch), (0, 0, 255), 1) |
|
|
| safe_ratio = 1 - crack_ratio |
| return annotated, crack_ratio, safe_ratio, num_detections |
|
|
|
|
| |
| |
| |
| def generate_histogram(crack_ratios): |
| frames = list(range(1, len(crack_ratios) + 1)) |
| safe_ratios = [1 - r for r in crack_ratios] |
|
|
| fig, axes = plt.subplots(1, 2, figsize=(14, 5), facecolor='#0f0f1a') |
|
|
| ax1 = axes[0] |
| ax1.set_facecolor('#1a1a2e') |
| ax1.plot(frames, [r*100 for r in crack_ratios], color='#ff4d4d', linewidth=2, label='Cracked %') |
| ax1.plot(frames, [r*100 for r in safe_ratios], color='#00c897', linewidth=2, label='Safe %') |
| ax1.fill_between(frames, [r*100 for r in crack_ratios], alpha=0.3, color='#ff4d4d') |
| ax1.fill_between(frames, [r*100 for r in safe_ratios], alpha=0.1, color='#00c897') |
| ax1.set_ylim(0, 100) |
| ax1.set_xlabel("Frame Number", color='#aaa', fontsize=11) |
| ax1.set_ylabel("Percentage %", color='#aaa', fontsize=11) |
| ax1.set_title("Crack Detection Over Time", color='white', fontsize=14, fontweight='bold', pad=15) |
| ax1.tick_params(colors='#aaa') |
| ax1.legend(facecolor='#1a1a2e', labelcolor='white', fontsize=10) |
| for spine in ax1.spines.values(): spine.set_edgecolor('#333') |
|
|
| avg_crack = np.mean(crack_ratios) * 100 |
| avg_safe = 100 - avg_crack |
| ax2 = axes[1] |
| ax2.set_facecolor('#1a1a2e') |
| bars = ax2.bar(["Safe Ground", "Cracked Ground"], [avg_safe, avg_crack], |
| color=["#00c897", "#ff4d4d"], edgecolor="#333", linewidth=1, width=0.45) |
| ax2.set_ylim(0, 110) |
| ax2.set_ylabel("Average %", color='#aaa', fontsize=11) |
| ax2.set_title("Overall Analysis Summary", color='white', fontsize=14, fontweight='bold', pad=15) |
| ax2.tick_params(colors='#aaa') |
| for spine in ax2.spines.values(): spine.set_edgecolor('#333') |
| for bar, val in zip(bars, [avg_safe, avg_crack]): |
| ax2.text(bar.get_x()+bar.get_width()/2, bar.get_height()+2, |
| f"{val:.1f}%", ha='center', va='bottom', color='white', fontsize=13, fontweight='bold') |
|
|
| plt.tight_layout(pad=3) |
| hist_path = os.path.join(tempfile.gettempdir(), "GeoGuard_Histogram.png") |
| plt.savefig(hist_path, facecolor='#0f0f1a', dpi=150, bbox_inches='tight') |
| plt.close(fig) |
| return hist_path |
|
|
|
|
| |
| |
| |
| def generate_pdf_report(crack_ratios, histogram_path, total_frames, fps, sensitivity): |
| pdf_path = os.path.join(tempfile.gettempdir(), "GeoGuard_Report.pdf") |
| doc = SimpleDocTemplate(pdf_path, pagesize=A4, |
| leftMargin=2*cm, rightMargin=2*cm, |
| topMargin=2*cm, bottomMargin=2*cm) |
| styles = getSampleStyleSheet() |
| story = [] |
|
|
| DARK = colors.HexColor("#0f0f1a") |
| RED = colors.HexColor("#ff4d4d") |
| GREEN = colors.HexColor("#00c897") |
| GRAY = colors.HexColor("#888888") |
| LIGHT = colors.HexColor("#f5f5f5") |
|
|
| title_style = ParagraphStyle("T", fontName="Helvetica-Bold", fontSize=26, |
| textColor=RED, alignment=TA_CENTER, spaceAfter=4) |
| sub_style = ParagraphStyle("S", fontName="Helvetica", fontSize=11, |
| textColor=GRAY, alignment=TA_CENTER, spaceAfter=2) |
| sec_style = ParagraphStyle("Se", fontName="Helvetica-Bold", fontSize=14, |
| textColor=DARK, spaceBefore=16, spaceAfter=8) |
| body_style = ParagraphStyle("B", fontName="Helvetica", fontSize=11, |
| textColor=colors.HexColor("#333"), spaceAfter=6, leading=18) |
|
|
| story.append(Spacer(1, 0.5*cm)) |
| story.append(Paragraph("GEO GUARD", title_style)) |
| story.append(Paragraph("AI Thermal Crack Detection – Analysis Report", sub_style)) |
| story.append(Paragraph(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", sub_style)) |
| story.append(Spacer(1, 0.3*cm)) |
| story.append(HRFlowable(width="100%", thickness=2, color=RED)) |
| story.append(Spacer(1, 0.5*cm)) |
|
|
| avg_crack = np.mean(crack_ratios) * 100 |
| avg_safe = 100 - avg_crack |
| max_crack = max(crack_ratios) * 100 |
| min_crack = min(crack_ratios) * 100 |
| duration = total_frames / fps if fps > 0 else 0 |
| status = "CRITICAL" if avg_crack > 30 else ("WARNING" if avg_crack > 10 else "SAFE") |
| s_color = RED if avg_crack > 10 else GREEN |
|
|
| story.append(Paragraph("Analysis Summary", sec_style)) |
| tbl_data = [ |
| ["Metric", "Value"], |
| ["Overall Status", status], |
| ["Total Frames Analyzed", str(total_frames)], |
| ["Video Duration", f"{duration:.1f} sec"], |
| ["Frame Rate (FPS)", f"{fps:.1f}"], |
| ["Sensitivity", f"{sensitivity:.0%}"], |
| ["Average Crack Ratio", f"{avg_crack:.2f}%"], |
| ["Average Safe Ratio", f"{avg_safe:.2f}%"], |
| ["Maximum Crack in Frame", f"{max_crack:.2f}%"], |
| ["Minimum Crack in Frame", f"{min_crack:.2f}%"], |
| ] |
| tbl = Table(tbl_data, colWidths=[8*cm, 8*cm]) |
| tbl.setStyle(TableStyle([ |
| ("BACKGROUND", (0,0), (-1,0), DARK), |
| ("TEXTCOLOR", (0,0), (-1,0), colors.white), |
| ("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"), |
| ("FONTSIZE", (0,0), (-1,-1), 11), |
| ("ALIGN", (0,0), (-1,-1), "CENTER"), |
| ("VALIGN", (0,0), (-1,-1), "MIDDLE"), |
| ("ROWBACKGROUNDS", (0,1), (-1,-1), [LIGHT, colors.white]), |
| ("GRID", (0,0), (-1,-1), 0.5, colors.HexColor("#ccc")), |
| ("ROWHEIGHT", (0,0), (-1,-1), 0.7*cm), |
| ("TEXTCOLOR", (1,1), (1,1), s_color), |
| ("FONTNAME", (1,1), (1,1), "Helvetica-Bold"), |
| ])) |
| story.append(tbl) |
| story.append(Spacer(1, 0.7*cm)) |
| story.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor("#ddd"))) |
| story.append(Spacer(1, 0.3*cm)) |
| story.append(Paragraph("Crack Analysis Charts", sec_style)) |
| story.append(Image(histogram_path, width=16*cm, height=6*cm)) |
| story.append(Spacer(1, 0.7*cm)) |
| story.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor("#ddd"))) |
| story.append(Spacer(1, 0.3*cm)) |
| story.append(Paragraph("Recommendations", sec_style)) |
|
|
| if avg_crack > 30: |
| recs = ["CRITICAL: Immediate structural inspection required.", |
| "Area should be cordoned off until safety assessment is complete.", |
| "Contact a licensed structural engineer within 24 hours.", |
| "Document all visible cracks with photographs for insurance purposes."] |
| elif avg_crack > 10: |
| recs = ["WARNING: Schedule a professional inspection within 2 weeks.", |
| "Monitor crack progression with periodic thermal scans.", |
| "Apply temporary sealing to prevent water ingress.", |
| "Review maintenance history for recurring issues."] |
| else: |
| recs = ["SAFE: No immediate action required.", |
| "Continue routine monitoring on a quarterly basis.", |
| "Maintain regular thermal scanning schedule.", |
| "Keep records for future comparison."] |
|
|
| for rec in recs: |
| story.append(Paragraph(f"• {rec}", body_style)) |
|
|
| story.append(Spacer(1, 1*cm)) |
| story.append(HRFlowable(width="100%", thickness=2, color=RED)) |
| story.append(Spacer(1, 0.2*cm)) |
| footer = ParagraphStyle("F", fontName="Helvetica", fontSize=9, |
| textColor=GRAY, alignment=TA_CENTER) |
| story.append(Paragraph("GeoGuard AI System • Hybrid Thermal + Edge Detection • Confidential Report", footer)) |
|
|
| doc.build(story) |
| return pdf_path |
|
|
|
|
| |
| |
| |
| def analyze_thermal_video(video_path, sensitivity=0.25): |
| cap = cv2.VideoCapture(video_path) |
| if not cap.isOpened(): |
| raise ValueError("تعذّر فتح الفيديو") |
|
|
| fps = cap.get(cv2.CAP_PROP_FPS) or 25 |
| w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) |
| h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) |
|
|
| output_path = os.path.join(tempfile.gettempdir(), "GeoGuard_Result.mp4") |
| out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w*2, h)) |
|
|
| fig = Figure(figsize=(4, 4), facecolor='#0f0f1a') |
| canvas = FigureCanvas(fig) |
| ax = fig.add_subplot(111) |
| ax.set_facecolor('#1a1a2e') |
|
|
| frame_count = 0 |
| crack_ratios = [] |
|
|
| while True: |
| ret, frame = cap.read() |
| if not ret: |
| break |
| frame_count += 1 |
|
|
| annotated, crack_ratio, safe_ratio, n_det = detect_cracks_hybrid(frame, sensitivity) |
| crack_ratios.append(crack_ratio) |
|
|
| status = "WARNING: CRACK DETECTED" if crack_ratio > 0.05 else "SAFE" |
| color = (0, 0, 255) if crack_ratio > 0.05 else (0, 200, 0) |
| cv2.putText(annotated, status, (10, 35), cv2.FONT_HERSHEY_SIMPLEX, 0.85, color, 2) |
| cv2.putText(annotated, f"Cracks: {n_det}", (10, 65), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,255,0), 1) |
| cv2.putText(annotated, f"Frame: {frame_count}", (10, h-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200,200,200), 1) |
|
|
| ax.cla() |
| bars = ax.bar(["Safe\nGround", "Cracked\nGround"], |
| [safe_ratio*100, crack_ratio*100], |
| color=["#00c897","#ff4d4d"], edgecolor="white", linewidth=0.8) |
| ax.set_ylim(0, 100) |
| ax.set_ylabel("Percentage %", color="white", fontsize=9) |
| ax.set_title("Live Crack Analysis", color="white", fontsize=10, fontweight='bold') |
| ax.tick_params(colors="white") |
| for spine in ax.spines.values(): spine.set_edgecolor("#333") |
| for bar, val in zip(bars, [safe_ratio*100, crack_ratio*100]): |
| ax.text(bar.get_x()+bar.get_width()/2, bar.get_height()+1, |
| f"{val:.1f}%", ha='center', va='bottom', color='white', fontsize=9) |
|
|
| canvas.draw() |
| hist_img = np.frombuffer(canvas.buffer_rgba(), dtype=np.uint8) |
| hist_img = hist_img.reshape(fig.canvas.get_width_height()[::-1] + (4,)) |
| hist_img = cv2.cvtColor(hist_img, cv2.COLOR_RGBA2BGR) |
| hist_img = cv2.resize(hist_img, (w, h)) |
| out.write(np.hstack((annotated, hist_img))) |
|
|
| cap.release() |
| out.release() |
|
|
| histogram_path = generate_histogram(crack_ratios) |
| pdf_path = generate_pdf_report(crack_ratios, histogram_path, frame_count, fps, sensitivity) |
|
|
| return output_path, histogram_path, pdf_path |
|
|
|
|
| |
| |
| |
| custom_css = """ |
| @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@700;900&family=Inter:wght@300;400;500&display=swap'); |
| |
| body, .gradio-container { |
| background: #0a0a14 !important; |
| font-family: 'Inter', sans-serif !important; |
| } |
| .gradio-container { max-width: 1100px !important; margin: 0 auto !important; } |
| |
| #header-box { |
| background: linear-gradient(135deg, #0f0f1a 0%, #1a0a0a 100%); |
| border: 1px solid #ff4d4d33; |
| border-radius: 16px; |
| padding: 32px; |
| margin-bottom: 20px; |
| text-align: center; |
| box-shadow: 0 0 40px #ff4d4d22; |
| } |
| #header-box h1 { |
| font-family: 'Orbitron', monospace !important; |
| font-size: 2.4rem !important; |
| background: linear-gradient(90deg, #ff4d4d, #ff8c42); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| margin: 0 0 8px 0 !important; |
| letter-spacing: 3px; |
| } |
| #header-box p { color: #888 !important; font-size: 0.95rem !important; margin: 0 !important; } |
| |
| label span { |
| color: #aaa !important; |
| font-size: 0.85rem !important; |
| letter-spacing: 1px !important; |
| text-transform: uppercase !important; |
| } |
| #run-btn { |
| background: linear-gradient(135deg, #ff4d4d, #c0392b) !important; |
| border: none !important; |
| border-radius: 10px !important; |
| color: white !important; |
| font-family: 'Orbitron', monospace !important; |
| font-size: 1rem !important; |
| letter-spacing: 2px !important; |
| padding: 14px !important; |
| margin-top: 12px !important; |
| box-shadow: 0 4px 20px #ff4d4d44 !important; |
| transition: all 0.3s ease !important; |
| } |
| #run-btn:hover { transform: translateY(-2px) !important; box-shadow: 0 8px 30px #ff4d4d66 !important; } |
| input[type=range] { accent-color: #ff4d4d !important; } |
| .section-title { |
| color: #ff4d4d; |
| font-family: 'Orbitron', monospace; |
| font-size: 1rem; |
| letter-spacing: 2px; |
| text-transform: uppercase; |
| margin: 24px 0 12px 0; |
| padding-bottom: 8px; |
| border-bottom: 1px solid #ff4d4d33; |
| } |
| """ |
|
|
| |
| |
| |
| with gr.Blocks(css=custom_css, title="GeoGuard – AI Crack Detection") as demo: |
|
|
| gr.HTML(""" |
| <div id="header-box"> |
| <h1>🛰 GEO GUARD</h1> |
| <p>AI-Powered Thermal Crack Detection | Hybrid Thermal + Edge Detection</p> |
| </div> |
| """) |
|
|
| gr.HTML('<div class="section-title">⬆ Input & Configuration</div>') |
| with gr.Row(): |
| with gr.Column(scale=1): |
| video_input = gr.Video(label="Thermal Video") |
| sens_slider = gr.Slider( |
| minimum=0.1, maximum=0.9, value=0.25, step=0.05, |
| label="Detection Sensitivity", |
| info="Higher = detects more cracks (may include noise)" |
| ) |
| run_btn = gr.Button("▶ RUN ANALYSIS", elem_id="run-btn") |
| with gr.Column(scale=2): |
| video_output = gr.Video(label="Analyzed Output Video") |
|
|
| gr.HTML('<div class="section-title">📊 Analysis Results</div>') |
| histogram_output = gr.Image(label="Crack Detection Histogram", type="filepath") |
|
|
| gr.HTML('<div class="section-title">📄 PDF Report</div>') |
| pdf_output = gr.File(label="Download Full Report (PDF)") |
|
|
| run_btn.click( |
| fn=analyze_thermal_video, |
| inputs=[video_input, sens_slider], |
| outputs=[video_output, histogram_output, pdf_output] |
| ) |
|
|
| demo.launch() |
|
|