import gradio as gr import torch import json from PIL import Image from torchvision import transforms import time import pandas as pd from pathlib import Path import io import base64 from reportlab.lib.pagesizes import letter, A4 from reportlab.lib import colors from reportlab.lib.units import inch from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, PageBreak, Image as RLImage from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.enums import TA_CENTER, TA_LEFT from datetime import datetime print("āœ… Packages installed!\n") print("šŸš€ Creating Gradio Interface...\n") # ==================== LOAD MODEL & METADATA ==================== class BusClassifierInference: def __init__(self, model_path='deployment/bus_classifier_traced.pt', metadata_path='deployment/model_metadata.json'): """Initialize the inference model""" # Load metadata with open(metadata_path, 'r') as f: self.metadata = json.load(f) self.class_names = self.metadata['class_names'] self.device = 'cuda' if torch.cuda.is_available() else 'cpu' print(f"šŸ”§ Loading model on {self.device.upper()}...") # Try loading TorchScript first, fallback to PyTorch checkpoint try: self.model = torch.jit.load(model_path, map_location=self.device) print(f"āœ… TorchScript model loaded from {model_path}") except: print(f"āš ļø TorchScript not found, loading PyTorch checkpoint...") from torchvision import models # Load checkpoint checkpoint = torch.load('deployment/bus_classifier.pth', map_location=self.device) # Recreate model architecture self.model = models.efficientnet_b0(weights=None) num_features = self.model.classifier[1].in_features self.model.classifier[1] = torch.nn.Linear(num_features, len(self.class_names)) # Load weights self.model.load_state_dict(checkpoint['model_state_dict']) self.model = self.model.to(self.device) print(f"āœ… PyTorch checkpoint loaded") self.model.eval() # Define transform self.transform = transforms.Compose([ transforms.Resize((self.metadata['image_size'], self.metadata['image_size'])), transforms.ToTensor(), transforms.Normalize( mean=self.metadata['normalization']['mean'], std=self.metadata['normalization']['std'] ) ]) print(f"āœ… Model ready for inference!") print(f"šŸ“Š Classes: {', '.join(self.class_names)}\n") def predict_single(self, image): """Predict class for a single image""" start_time = time.time() # Load image if path provided if isinstance(image, (str, Path)): image = Image.open(image).convert('RGB') elif not isinstance(image, Image.Image): image = Image.fromarray(image).convert('RGB') # Preprocess input_tensor = self.transform(image).unsqueeze(0).to(self.device) # Inference with torch.no_grad(): logits = self.model(input_tensor) probs = torch.softmax(logits, dim=1) pred_class_idx = torch.argmax(probs, dim=1).item() confidence = probs[0][pred_class_idx].item() inference_time = time.time() - start_time # Get all probabilities all_probs = { self.class_names[i]: float(probs[0][i].item()) for i in range(len(self.class_names)) } # Sort by confidence sorted_probs = dict(sorted(all_probs.items(), key=lambda x: x[1], reverse=True)) return { 'predicted_class': self.class_names[pred_class_idx], 'confidence': confidence, 'all_probabilities': sorted_probs, 'inference_time_ms': inference_time * 1000 } def predict_batch(self, images): """Predict for multiple images""" results = [] total_start = time.time() for idx, image in enumerate(images): result = self.predict_single(image) result['image_index'] = idx + 1 results.append(result) total_time = time.time() - total_start return results, total_time # Initialize model print("="*80) predictor = BusClassifierInference() print("="*80) # ==================== PDF GENERATION FUNCTION ==================== def generate_pdf_report(results, images, total_time): """Generate a professional PDF report""" # Create temporary file pdf_filename = f"classification_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf" # Create PDF doc = SimpleDocTemplate(pdf_filename, pagesize=letter) story = [] styles = getSampleStyleSheet() # Custom styles title_style = ParagraphStyle( 'CustomTitle', parent=styles['Heading1'], fontSize=24, textColor=colors.HexColor('#667eea'), spaceAfter=30, alignment=TA_CENTER, fontName='Helvetica-Bold' ) heading_style = ParagraphStyle( 'CustomHeading', parent=styles['Heading2'], fontSize=16, textColor=colors.HexColor('#333333'), spaceAfter=12, spaceBefore=12, fontName='Helvetica-Bold' ) # Title story.append(Paragraph("🚌 Bus Component Classification Report", title_style)) story.append(Paragraph(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", styles['Normal'])) story.append(Spacer(1, 0.3*inch)) # Summary Section story.append(Paragraph("šŸ“Š Executive Summary", heading_style)) summary_data = [ ['Metric', 'Value'], ['Total Images Processed', str(len(images))], ['Total Processing Time', f'{total_time:.2f} seconds'], ['Average Time per Image', f'{total_time/len(images)*1000:.2f} ms'], ['Model Used', 'EfficientNet-B0'], ['Model Accuracy', '98.71%'], ['Device', predictor.device.upper()], ] summary_table = Table(summary_data, colWidths=[3*inch, 3*inch]) summary_table.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#667eea')), ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), ('ALIGN', (0, 0), (-1, -1), 'LEFT'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, 0), 12), ('BOTTOMPADDING', (0, 0), (-1, 0), 12), ('BACKGROUND', (0, 1), (-1, -1), colors.beige), ('GRID', (0, 0), (-1, -1), 1, colors.black), ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'), ('FONTSIZE', (0, 1), (-1, -1), 10), ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey]), ])) story.append(summary_table) story.append(Spacer(1, 0.3*inch)) # Performance Metrics story.append(Paragraph("šŸ“ˆ Performance Metrics", heading_style)) avg_confidence = sum([r['confidence'] for r in results]) / len(results) high_conf = sum([1 for r in results if r['confidence'] >= 0.95]) medium_conf = sum([1 for r in results if 0.80 <= r['confidence'] < 0.95]) low_conf = sum([1 for r in results if r['confidence'] < 0.80]) perf_data = [ ['Performance Metric', 'Value', 'Percentage'], ['Average Confidence', f'{avg_confidence*100:.2f}%', '-'], ['High Confidence (≄95%)', str(high_conf), f'{high_conf/len(images)*100:.1f}%'], ['Medium Confidence (80-95%)', str(medium_conf), f'{medium_conf/len(images)*100:.1f}%'], ['Low Confidence (<80%)', str(low_conf), f'{low_conf/len(images)*100:.1f}%'], ] perf_table = Table(perf_data, colWidths=[2.5*inch, 1.5*inch, 1.5*inch]) perf_table.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#4CAF50')), ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), ('ALIGN', (0, 0), (-1, -1), 'CENTER'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, 0), 11), ('BOTTOMPADDING', (0, 0), (-1, 0), 12), ('GRID', (0, 0), (-1, -1), 1, colors.black), ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey]), ])) story.append(perf_table) story.append(Spacer(1, 0.3*inch)) # Class Distribution story.append(Paragraph("šŸ“¦ Class Distribution", heading_style)) class_counts = {} for result in results: pred = result['predicted_class'] class_counts[pred] = class_counts.get(pred, 0) + 1 dist_data = [['Class Name', 'Count', 'Percentage']] for class_name, count in sorted(class_counts.items(), key=lambda x: x[1], reverse=True): dist_data.append([class_name, str(count), f'{count/len(images)*100:.1f}%']) dist_table = Table(dist_data, colWidths=[3*inch, 1.5*inch, 1.5*inch]) dist_table.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2196F3')), ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), ('ALIGN', (0, 0), (-1, -1), 'CENTER'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, 0), 11), ('BOTTOMPADDING', (0, 0), (-1, 0), 12), ('GRID', (0, 0), (-1, -1), 1, colors.black), ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey]), ])) story.append(dist_table) story.append(PageBreak()) # Detailed Results story.append(Paragraph("šŸ” Detailed Classification Results", heading_style)) story.append(Spacer(1, 0.2*inch)) # Create detailed table detail_data = [['#', 'Predicted Class', 'Confidence', 'Time (ms)', '2nd Best', '2nd Conf']] for result in results: second_best = list(result['all_probabilities'].keys())[1] second_conf = list(result['all_probabilities'].values())[1] detail_data.append([ str(result['image_index']), result['predicted_class'], f"{result['confidence']*100:.2f}%", f"{result['inference_time_ms']:.2f}", second_best, f"{second_conf*100:.2f}%" ]) detail_table = Table(detail_data, colWidths=[0.5*inch, 1.8*inch, 1*inch, 0.9*inch, 1.8*inch, 1*inch]) detail_table.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#764ba2')), ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), ('ALIGN', (0, 0), (-1, -1), 'CENTER'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, 0), 9), ('BOTTOMPADDING', (0, 0), (-1, 0), 12), ('GRID', (0, 0), (-1, -1), 1, colors.black), ('FONTSIZE', (0, 1), (-1, -1), 8), ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey]), ])) story.append(detail_table) story.append(Spacer(1, 0.3*inch)) # Footer story.append(Spacer(1, 0.5*inch)) footer_style = ParagraphStyle( 'Footer', parent=styles['Normal'], fontSize=9, textColor=colors.grey, alignment=TA_CENTER ) story.append(Paragraph("Bus Component Classification System v1.0 | Powered by EfficientNet-B0", footer_style)) story.append(Paragraph("This report is auto-generated and contains AI predictions.", footer_style)) # Build PDF doc.build(story) print(f"āœ… PDF Report generated: {pdf_filename}") return pdf_filename # ==================== GRADIO INTERFACE FUNCTIONS ==================== def predict_images(images): """Main prediction function for Gradio interface""" if images is None or len(images) == 0: return "

āš ļø Please upload at least one image!

", None if len(images) > 50: return f"

āš ļø Maximum 50 images allowed! You uploaded {len(images)} images.

", None print(f"\nšŸ” Processing {len(images)} image(s)...") # Get predictions results, total_time = predictor.predict_batch(images) # Generate PDF Report pdf_file = generate_pdf_report(results, images, total_time) # Calculate class distribution class_counts = {} for result in results: pred = result['predicted_class'] class_counts[pred] = class_counts.get(pred, 0) + 1 # ==================== BUILD COMPACT GRID OUTPUT ==================== html_output = f"""
šŸ“Š Images: {len(images)}
ā±ļø Total Time: {total_time:.2f}s
⚔ Avg Time: {total_time/len(images)*1000:.0f}ms
šŸŽÆ High Confidence: {sum([1 for r in results if r['confidence'] >= 0.95])}/{len(images)}

šŸ“¦ Class Distribution

""" # Add class distribution bars for class_name, count in sorted(class_counts.items(), key=lambda x: x[1], reverse=True): percentage = (count / len(images)) * 100 html_output += f"""
{class_name} {count}
{percentage:.1f}%
""" html_output += """

šŸ” Detailed Results

""" # Individual predictions in grid for idx, result in enumerate(results): pred_class = result['predicted_class'] confidence = result['confidence'] inf_time = result['inference_time_ms'] # Color based on confidence if confidence >= 0.95: border_color = "#4CAF50" badge_color = "#4CAF50" elif confidence >= 0.80: border_color = "#FF9800" badge_color = "#FF9800" else: border_color = "#F44336" badge_color = "#F44336" # Get the actual image img = images[idx] if isinstance(img, str): with open(img, 'rb') as f: img_data = f.read() else: img_pil = Image.open(img).convert('RGB') buffer = io.BytesIO() img_pil.save(buffer, format='JPEG') img_data = buffer.getvalue() img_base64 = base64.b64encode(img_data).decode() html_output += f"""
Image {idx+1}
#{idx+1}
{pred_class}
{confidence*100:.1f}%
ā±ļø {inf_time:.1f}ms
""" html_output += """
""" print(f"āœ… Complete! Processed {len(images)} images in {total_time:.2f}s\n") return html_output, pdf_file # ==================== CREATE MINIMAL GRADIO INTERFACE ==================== custom_css = """ .gradio-container { max-width: 1200px !important; margin: auto !important; } /* Upload button styling */ .upload-button { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; color: white !important; font-size: 16px !important; font-weight: bold !important; padding: 25px 40px !important; border-radius: 12px !important; border: 3px dashed rgba(255, 255, 255, 0.5) !important; cursor: pointer !important; transition: all 0.3s ease !important; } .upload-button:hover { transform: translateY(-2px) !important; box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4) !important; border-color: white !important; } details summary { cursor: pointer; padding: 10px 15px; background: #f0f0f0; border-radius: 6px; font-weight: bold; color: #333; border: 1px solid #ddd; user-select: none; } details[open] summary { background: #667eea; color: white; border-color: #667eea; } details { margin-bottom: 15px; } details div { padding: 10px 15px; background: white; border: 1px solid #ddd; border-top: none; border-radius: 0 0 6px 6px; max-height: 200px; overflow-y: auto; } """ with gr.Blocks(title="🚌 Bus Classifier", css=custom_css) as demo: # Header gr.HTML("""

🚌 Bus Component Classifier

EfficientNet-B0 | Accuracy: 98.71% | Real-time Classification

""") # Collapsible System Info with gr.Accordion("šŸ“‹ System Information", open=False): gr.HTML(f"""
Model: EfficientNet-B0
Classes: {len(predictor.class_names)}
Accuracy: 98.71%
Device: {predictor.device.upper()}
Max Images: 50 per batch
šŸ“¦ Supported Classes:
{' '.join([f'{cls}' for cls in predictor.class_names])}
""") # Upload Section with clear button gr.HTML("""

šŸ“¤ Upload Images

Click the button below to select images (JPG, PNG | Max: 50 images)

""") with gr.Row(): with gr.Column(): image_input = gr.File( file_count="multiple", file_types=["image"], label="", show_label=False, elem_classes=["upload-button"] ) # File count and collapsible list file_list_html = gr.HTML() def update_file_list(files): if not files or len(files) == 0: return "" file_count = len(files) # Show first 5 files visible_files = files[:5] if file_count > 5 else files html = f"""
šŸ“ {file_count} image{'s' if file_count != 1 else ''} selected
""" # Show first 5 files for idx, file in enumerate(visible_files): filename = file.name if hasattr(file, 'name') else str(file).split('/')[-1] html += f"""
{idx + 1}. {filename}
""" # If more than 5, show collapsible if file_count > 5: html += f"""
āž• Show {file_count - 5} more files
""" for idx, file in enumerate(files[5:], start=6): filename = file.name if hasattr(file, 'name') else str(file).split('/')[-1] html += f"""
{idx}. {filename}
""" html += """
""" html += "
" return html image_input.change( fn=update_file_list, inputs=[image_input], outputs=[file_list_html] ) # Buttons with gr.Row(): predict_btn = gr.Button( "šŸ” Classify Images", variant="primary", size="lg" ) clear_btn = gr.Button( "šŸ—‘ļø Clear All", size="lg" ) # Results Section gr.HTML("""

šŸ“Š Classification Results

""") results_output = gr.HTML() # PDF Download Section gr.HTML("""

šŸ“„ Download Report

""") pdf_output = gr.File(label="", show_label=False) # Footer Info (Collapsible) with gr.Accordion("ā„¹ļø How to Interpret Results", open=False): gr.HTML("""
ā— Green (≄95%): High confidence - Very reliable prediction
ā— Orange (80-95%): Medium confidence - Generally reliable
ā— Red (<80%): Low confidence - Manual review recommended
""") # Button actions predict_btn.click( fn=predict_images, inputs=[image_input], outputs=[results_output, pdf_output] ) def clear_all(): return None, None, None, "" clear_btn.click( fn=clear_all, inputs=[], outputs=[image_input, results_output, pdf_output, file_list_html] ) # ==================== LAUNCH ==================== print("\n" + "="*80) print("šŸš€ LAUNCHING GRADIO INTERFACE (LOCAL)") print("="*80) print(f"Model: EfficientNet-B0") print(f"Classes: {len(predictor.class_names)}") print(f"Device: {predictor.device.upper()}") print(f"{'='*80}\n") demo.launch()