# Lung Cancer Classification API - Flutter Integration Guidelines Complete guide for integrating the Grad-CAM backend API into your Flutter application. --- ## Table of Contents 1. [API Overview](#api-overview) 2. [Base URL & Configuration](#base-url--configuration) 3. [Endpoints](#endpoints) 4. [Request/Response Formats](#requestresponse-formats) 5. [Image Handling](#image-handling) 6. [Error Handling](#error-handling) 7. [Code Examples](#code-examples) 8. [UI Integration](#ui-integration) 9. [Best Practices](#best-practices) 10. [Troubleshooting](#troubleshooting) --- ## API Overview This API provides: - **YOLO Tumor Detection**: Detects lung tumors in CT scan images - **DenseNet121 Classification**: Classifies detected tumors into 4 cancer types - **Grad-CAM Visualization**: Explains model predictions with attention heatmaps - **CT Scan Validation**: Validates input is a medical image (not a color photo) ### Supported Cancer Classes ``` 0. Adenocarcinoma (Class A) 1. Small Cell (Class B) 2. Large Cell (Class E) 3. Squamous Cell (Class G) ``` --- ## Base URL & Configuration ### Development ``` Base URL: http://localhost:5001 ``` ### Production (Railway Deployment) ``` Base URL: https://your-railway-app.up.railway.app ``` ### Environment Configuration ```dart class ApiConfig { static const String baseUrl = 'http://localhost:5001'; // Change for production static const int timeout = 120; // seconds (image processing takes time) static const String apiVersion = 'v1.0.0'; } ``` --- ## Endpoints ### 1. Health Check **GET** `/health` Check if API is running and models are loaded. **Response:** ```json { "status": "healthy", "model_loaded": true } ``` --- ### 2. Validate CT Scan **POST** `/validate-ct` Validate if uploaded image is a CT scan (grayscale check). **Request:** ``` Content-Type: multipart/form-data Body: - file: ``` **Response (Valid CT):** ```json { "is_ct_scan": true, "color_score": 3.52, "message": "Valid CT scan - grayscale image detected" } ``` **Response (Invalid - Color Image):** ```json { "is_ct_scan": false, "color_score": 45.2, "message": "NOT a CT scan - color image detected (color_score=45.2)" } ``` **Color Score Interpretation:** - `< 6.0`: Valid CT scan (grayscale) - `>= 6.0`: Not a CT scan (color image detected) --- ### 3. Analyze (YOLO + Classification + Grad-CAM) **POST** `/analyze` Complete pipeline: detect tumors → classify → generate heatmaps. **Request:** ``` Content-Type: multipart/form-data Body: - file: ``` **Response (With Tumors):** ```json { "success": true, "tumors_detected": 1, "detection_image": "base64_encoded_image_with_boxes", "heatmap_image": "base64_encoded_heatmap", "detections": [ { "tumor_id": 1, "bbox": [161, 319, 225, 390], "bbox_with_padding": [149, 305, 237, 404], "prediction": "Small Cell (Class B)", "confidence": 44.62, "all_confidences": { "Adenocarcinoma (Class A)": 11.07, "Small Cell (Class B)": 44.62, "Large Cell (Class E)": 25.52, "Squamous Cell (Class G)": 18.79 }, "crop_image": "base64_encoded_crop", "heatmap_image": "base64_encoded_heatmap_of_crop" } ] } ``` **Response (No Tumors):** ```json { "success": true, "tumors_detected": 0, "detections": [], "detection_image": "base64_encoded_original_image", "message": "No tumors detected in this image" } ``` **Response (Error):** ```json { "error": "Could not load image" } ``` --- ## Request/Response Formats ### Multipart Form Data (Image Upload) ```dart // Example using http package var request = http.MultipartRequest('POST', Uri.parse('$baseUrl/analyze')); request.files.add( http.MultipartFile.fromBytes( 'file', imageBytes, filename: 'scan.jpg', ), ); ``` ### Response Image Decoding All images in responses are **base64-encoded JPEG**. ```dart // Decode base64 to Image widget Image.memory( base64Decode(response['detection_image']), fit: BoxFit.contain, ) ``` --- ## Image Handling ### Supported Formats - JPEG (.jpg, .jpeg) - PNG (.png) - Recommended: **JPEG** (faster processing) ### Image Size Recommendations - **Minimum**: 256x256 pixels - **Optimal**: 512x512 pixels - **Maximum**: 2048x2048 pixels (will be resized internally) ### Image Acquisition #### From Camera ```dart import 'package:image_picker/image_picker.dart'; final picker = ImagePicker(); final pickedFile = await picker.pickImage(source: ImageSource.camera); if (pickedFile != null) { final bytes = await pickedFile.readAsBytes(); // Use bytes for upload } ``` #### From Gallery ```dart final pickedFile = await picker.pickImage(source: ImageSource.gallery); if (pickedFile != null) { final bytes = await pickedFile.readAsBytes(); // Use bytes for upload } ``` #### From File ```dart import 'dart:io'; File imageFile = File('/path/to/image.jpg'); final bytes = await imageFile.readAsBytes(); // Use bytes for upload ``` ### Base64 Image Display ```dart import 'dart:convert'; // Decode and display Widget displayBase64Image(String base64String) { return Image.memory( base64Decode(base64String), fit: BoxFit.contain, ); } // Or with error handling Widget safeDisplayImage(String base64String) { try { return Image.memory( base64Decode(base64String), fit: BoxFit.contain, errorBuilder: (context, error, stackTrace) { return Center(child: Text('Failed to load image')); }, ); } catch (e) { return Center(child: Text('Invalid image data')); } } ``` --- ## Error Handling ### HTTP Status Codes | Status | Meaning | Action | |--------|---------|--------| | 200 | Success | Process response | | 400 | Bad Request | Check file format/size | | 500 | Server Error | Retry or check logs | ### Common Errors #### "No file uploaded" ```json {"error": "No file uploaded"} ``` **Cause**: File not attached to request **Fix**: Ensure `file` field is included in multipart request #### "Could not load image" ```json {"error": "Could not load image"} ``` **Cause**: Corrupted or unsupported image format **Fix**: Use JPEG/PNG, verify file integrity #### "YOLO model not loaded" ```json {"error": "YOLO model not loaded"} ``` **Cause**: Backend not initialized properly **Fix**: Restart backend, check `models/best.pt` exists #### Timeout **Cause**: Image processing takes >120 seconds **Fix**: Reduce image size, check server performance --- ## Code Examples ### Complete Service Class ```dart import 'package:http/http.dart' as http; import 'dart:convert'; class LungCancerAnalysisService { final String baseUrl = 'http://localhost:5001'; final int timeout = Duration(seconds: 120).inSeconds; // Health check Future healthCheck() async { try { final response = await http .get(Uri.parse('$baseUrl/health')) .timeout(Duration(seconds: 10)); return response.statusCode == 200; } catch (e) { print('Health check failed: $e'); return false; } } // Validate CT scan Future validateCT(List imageBytes) async { try { var request = http.MultipartRequest('POST', Uri.parse('$baseUrl/validate-ct')); request.files.add( http.MultipartFile.fromBytes('file', imageBytes, filename: 'scan.jpg'), ); request.headers['Accept'] = 'application/json'; final streamedResponse = await request.send() .timeout(Duration(seconds: this.timeout)); final response = await http.Response.fromStream(streamedResponse); if (response.statusCode == 200) { final json = jsonDecode(response.body); return ValidateCTResponse.fromJson(json); } else { throw Exception('Validation failed: ${response.body}'); } } catch (e) { throw Exception('CT validation error: $e'); } } // Full analysis with YOLO + Classification Future analyze(List imageBytes) async { try { var request = http.MultipartRequest('POST', Uri.parse('$baseUrl/analyze')); request.files.add( http.MultipartFile.fromBytes('file', imageBytes, filename: 'scan.jpg'), ); request.headers['Accept'] = 'application/json'; final streamedResponse = await request.send() .timeout(Duration(seconds: this.timeout)); final response = await http.Response.fromStream(streamedResponse); if (response.statusCode == 200) { final json = jsonDecode(response.body); return AnalysisResponse.fromJson(json); } else { throw Exception('Analysis failed: ${response.body}'); } } catch (e) { throw Exception('Analysis error: $e'); } } } // Response models class ValidateCTResponse { final bool isCTScan; final double colorScore; final String message; ValidateCTResponse({ required this.isCTScan, required this.colorScore, required this.message, }); factory ValidateCTResponse.fromJson(Map json) { return ValidateCTResponse( isCTScan: json['is_ct_scan'] ?? false, colorScore: (json['color_score'] ?? 0).toDouble(), message: json['message'] ?? '', ); } } class AnalysisResponse { final bool success; final int tumorsDetected; final List detections; final String detectionImage; // base64 final String heatmapImage; // base64 final String? message; AnalysisResponse({ required this.success, required this.tumorsDetected, required this.detections, required this.detectionImage, required this.heatmapImage, this.message, }); factory AnalysisResponse.fromJson(Map json) { return AnalysisResponse( success: json['success'] ?? false, tumorsDetected: json['tumors_detected'] ?? 0, detections: (json['detections'] as List) .map((d) => TumorDetection.fromJson(d)) .toList(), detectionImage: json['detection_image'] ?? '', heatmapImage: json['heatmap_image'] ?? '', message: json['message'], ); } } class TumorDetection { final int tumorId; final List bbox; final List bboxWithPadding; final String prediction; final double confidence; final Map allConfidences; final String cropImage; // base64 final String heatmapImage; // base64 TumorDetection({ required this.tumorId, required this.bbox, required this.bboxWithPadding, required this.prediction, required this.confidence, required this.allConfidences, required this.cropImage, required this.heatmapImage, }); factory TumorDetection.fromJson(Map json) { return TumorDetection( tumorId: json['tumor_id'] ?? 0, bbox: List.from(json['bbox'] ?? []), bboxWithPadding: List.from(json['bbox_with_padding'] ?? []), prediction: json['prediction'] ?? 'Unknown', confidence: (json['confidence'] ?? 0).toDouble(), allConfidences: Map.from( (json['all_confidences'] as Map).map( (k, v) => MapEntry(k, (v as num).toDouble()), ), ), cropImage: json['crop_image'] ?? '', heatmapImage: json['heatmap_image'] ?? '', ); } } ``` ### Usage in Widget ```dart class CancerAnalysisScreen extends StatefulWidget { @override _CancerAnalysisScreenState createState() => _CancerAnalysisScreenState(); } class _CancerAnalysisScreenState extends State { final service = LungCancerAnalysisService(); bool isLoading = false; AnalysisResponse? result; String? errorMessage; void analyzeImage(List imageBytes) async { setState(() { isLoading = true; errorMessage = null; }); try { // Step 1: Validate CT scan final validation = await service.validateCT(imageBytes); if (!validation.isCTScan) { setState(() { errorMessage = 'Not a CT scan (score: ${validation.colorScore})'; isLoading = false; }); return; } // Step 2: Full analysis final analysis = await service.analyze(imageBytes); setState(() { result = analysis; isLoading = false; }); } catch (e) { setState(() { errorMessage = 'Error: $e'; isLoading = false; }); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Lung Cancer Analysis')), body: SingleChildScrollView( child: Padding( padding: EdgeInsets.all(16), child: Column( children: [ if (isLoading) Center( child: Column( children: [ CircularProgressIndicator(), SizedBox(height: 16), Text('Analyzing image...'), ], ), ), if (errorMessage != null) Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.red.shade100, borderRadius: BorderRadius.circular(8), ), child: Text( errorMessage!, style: TextStyle(color: Colors.red.shade900), ), ), if (result != null) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Card( child: Padding( padding: EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Tumors Detected: ${result!.tumorsDetected}', style: Theme.of(context).textTheme.titleLarge, ), SizedBox(height: 16), // Detection image Text('Detection Image:'), Image.memory( base64Decode(result!.detectionImage), fit: BoxFit.contain, ), SizedBox(height: 16), // Heatmap image Text('Heatmap (Highest Confidence):'), Image.memory( base64Decode(result!.heatmapImage), fit: BoxFit.contain, ), ], ), ), ), SizedBox(height: 16), // Individual tumor details ...result!.detections.map((tumor) { return Card( child: Padding( padding: EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Tumor #${tumor.tumorId}', style: Theme.of(context).textTheme.titleMedium, ), Text('Classification: ${tumor.prediction}'), Text('Confidence: ${tumor.confidence.toStringAsFixed(2)}%'), SizedBox(height: 12), Text('All Confidences:'), ...tumor.allConfidences.entries.map((e) { return Text( ' ${e.key}: ${e.value.toStringAsFixed(2)}%', ); }).toList(), SizedBox(height: 12), Text('Bounding Box: ${tumor.bbox}'), ], ), ), ); }).toList(), ], ), ], ), ), ), ); } } ``` --- ## UI Integration ### Recommended UI Flow ``` ┌─────────────────────┐ │ Home Screen │ │ - Upload Image │ │ - Take Photo │ │ - Gallery │ └──────────┬──────────┘ │ ▼ ┌─────────────────────┐ │ Validation Screen │ │ - Show Color Score │ │ - CT Scan Check │ │ - Retry/Continue │ └──────────┬──────────┘ │ ▼ ┌─────────────────────┐ │ Loading Screen │ │ - Progress Indicator│ │ - "Analyzing..." │ │ - Timeout Handling │ └──────────┬──────────┘ │ ▼ ┌──────────────────────┐ │ Results Screen │ │ ┌──────────────────┐ │ │ │ Detection Image │ │ │ ├──────────────────┤ │ │ │ Heatmap Image │ │ │ ├──────────────────┤ │ │ │ Tumor Details │ │ │ │ - Confidence │ │ │ │ - Classification │ │ │ │ - Per-class % │ │ │ └──────────────────┘ │ └──────────────────────┘ ``` ### Confidence Visualization ```dart Widget buildConfidenceBar(String className, double confidence) { final color = confidence > 0.4 ? Colors.red : confidence > 0.2 ? Colors.orange : Colors.green; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(className), LinearProgressIndicator( value: confidence / 100, minHeight: 8, backgroundColor: Colors.grey.shade300, valueColor: AlwaysStoppedAnimation(color), ), Text('${confidence.toStringAsFixed(2)}%'), ], ); } ``` ### Image Display with Zoom ```dart import 'package:photo_view/photo_view.dart'; Widget zoomableImage(String base64String) { return PhotoView( imageProvider: MemoryImage(base64Decode(base64String)), minScale: PhotoViewComputedScale.contained * 0.8, maxScale: PhotoViewComputedScale.covered * 2, ); } ``` --- ## Best Practices ### 1. Handle Timeouts Gracefully ```dart Future analyzeWithTimeout(List imageBytes) async { try { return await service.analyze(imageBytes).timeout( Duration(seconds: 120), onTimeout: () => throw TimeoutException('Analysis took too long'), ); } on TimeoutException { // Show user-friendly message // Suggest reducing image size } } ``` ### 2. Validate Before Upload ```dart bool validateImage(List imageBytes) { // Check file size (limit to 10MB) const maxSizeBytes = 10 * 1024 * 1024; // 10 MB if (imageBytes.length > maxSizeBytes) { return false; } // Check JPEG/PNG signature if (imageBytes[0] == 0xFF && imageBytes[1] == 0xD8) { return true; // JPEG } if (imageBytes[0] == 0x89 && imageBytes[1] == 0x50) { return true; // PNG } return false; } ``` ### 3. Cache Results ```dart class AnalysisCache { static final Map _cache = {}; static String hashImage(List imageBytes) { return sha256.convert(imageBytes).toString(); } static void store(List imageBytes, AnalysisResponse response) { _cache[hashImage(imageBytes)] = response; } static AnalysisResponse? retrieve(List imageBytes) { return _cache[hashImage(imageBytes)]; } } ``` ### 4. Permission Handling ```dart import 'package:permission_handler/permission_handler.dart'; Future requestCameraPermission() async { final status = await Permission.camera.request(); return status.isGranted; } Future requestGalleryPermission() async { final status = await Permission.photos.request(); return status.isGranted; } ``` ### 5. Network State Awareness ```dart import 'package:connectivity_plus/connectivity_plus.dart'; bool isNetworkAvailable() { final connectivity = Connectivity(); return connectivity.checkConnectivity().then((result) { return result != ConnectivityResult.none; }); } // Show offline message if needed ``` --- ## Troubleshooting ### Issue: "Connection refused" **Cause**: Backend not running **Solution**: ```bash cd /path/to/backend source .venv/bin/activate python main.py ``` ### Issue: "Image too large" **Cause**: File size > 10MB **Solution**: Compress before upload ```dart import 'package:flutter_image_compress/flutter_image_compress.dart'; List compressImage(List imageBytes) { return XFile.fromData(imageBytes) .compress(quality: 85) .then((file) => file.readAsBytes()); } ``` ### Issue: "CORS Error" **Cause**: Frontend URLs not whitelisted **Solution**: Backend already has CORS enabled via Flask-CORS ### Issue: "Timeout after 120s" **Cause**: Large image or slow server **Solution**: - Reduce image resolution - Check server CPU/memory - Increase timeout threshold ### Issue: "Invalid base64 image" **Cause**: Corrupted response data **Solution**: - Log full response: `print(response.body)` - Check server error logs - Retry with different image ### Issue: Model not loaded **Error**: `"YOLO model not loaded"` **Cause**: `models/best.pt` missing **Solution**: Verify model files exist in backend --- ## Environment-Specific Configuration ### Development ```dart const String API_BASE = 'http://localhost:5001'; const bool DEBUG = true; const int TIMEOUT = 120; ``` ### Staging ```dart const String API_BASE = 'https://staging-api.railway.app'; const bool DEBUG = true; const int TIMEOUT = 120; ``` ### Production ```dart const String API_BASE = 'https://your-production-api.railway.app'; const bool DEBUG = false; const int TIMEOUT = 120; ``` ### Switch at Runtime ```dart String getBaseUrl() { if (kDebugMode) { return 'http://localhost:5001'; } return 'https://your-production-api.railway.app'; } ``` --- ## API Testing ### cURL Examples **Validation:** ```bash curl -X POST http://localhost:5001/validate-ct \ -F "file=@path/to/scan.jpg" ``` **Analysis:** ```bash curl -X POST http://localhost:5001/analyze \ -F "file=@path/to/scan.jpg" ``` **Health Check:** ```bash curl http://localhost:5001/health ``` ### Postman Setup 1. Create POST request to `http://localhost:5001/analyze` 2. Go to "Body" tab → Select "form-data" 3. Add key "file" (type: File) 4. Select image file 5. Send & view response --- ## Dependencies for Flutter Add to `pubspec.yaml`: ```yaml dependencies: flutter: sdk: flutter http: ^1.1.0 image_picker: ^1.0.0 photo_view: ^0.14.0 flutter_image_compress: ^2.1.0 permission_handler: ^11.4.0 connectivity_plus: ^5.0.0 crypto: ^3.0.0 ``` --- ## Performance Metrics | Operation | Time | Notes | |-----------|------|-------| | Image Upload | 1-5s | Depends on image size & network | | YOLO Detection | 5-15s | GPU faster if available | | DenseNet Classification | 2-5s | Per tumor detected | | Grad-CAM Heatmap | 2-3s | Per tumor | | **Total** | **10-30s** | End-to-end (single tumor) | --- ## Support & Contact For issues or questions: 1. Check [Troubleshooting](#troubleshooting) section 2. Review API logs: `python main.py` console output 3. Test with cURL first: ensure backend works 4. Check Flutter app logs: `flutter logs` 5. Verify network connectivity & firewall rules --- **API Version**: 1.0.0 **Last Updated**: March 8, 2026 **Compatibility**: Flutter 3.0+ , Dart 3.0+