|
Download INTEGRATION_GUIDELINES.md from NoumanUsman/ChestSense: direct link, hf CLI and curl.
- Browser
- Download file 24.4 kB
-
https://huggingface.co/NoumanUsman/ChestSense/resolve/main/INTEGRATION_GUIDELINES.md
- Command line
-
hf download hf://NoumanUsman/ChestSense/INTEGRATION_GUIDELINES.md
-
curl -L -o INTEGRATION_GUIDELINES.md https://huggingface.co/NoumanUsman/ChestSense/resolve/main/INTEGRATION_GUIDELINES.md
24.4 kB
| # 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: <image_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: <image_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<bool> 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<ValidateCTResponse> validateCT(List<int> 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<AnalysisResponse> analyze(List<int> 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<String, dynamic> 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<TumorDetection> 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<String, dynamic> 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<int> bbox; | |
| final List<int> bboxWithPadding; | |
| final String prediction; | |
| final double confidence; | |
| final Map<String, double> 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<String, dynamic> json) { | |
| return TumorDetection( | |
| tumorId: json['tumor_id'] ?? 0, | |
| bbox: List<int>.from(json['bbox'] ?? []), | |
| bboxWithPadding: List<int>.from(json['bbox_with_padding'] ?? []), | |
| prediction: json['prediction'] ?? 'Unknown', | |
| confidence: (json['confidence'] ?? 0).toDouble(), | |
| allConfidences: Map<String, double>.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<CancerAnalysisScreen> { | |
| final service = LungCancerAnalysisService(); | |
| bool isLoading = false; | |
| AnalysisResponse? result; | |
| String? errorMessage; | |
| void analyzeImage(List<int> 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<AnalysisResponse> analyzeWithTimeout(List<int> 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<int> 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<String, AnalysisResponse> _cache = {}; | |
| static String hashImage(List<int> imageBytes) { | |
| return sha256.convert(imageBytes).toString(); | |
| } | |
| static void store(List<int> imageBytes, AnalysisResponse response) { | |
| _cache[hashImage(imageBytes)] = response; | |
| } | |
| static AnalysisResponse? retrieve(List<int> imageBytes) { | |
| return _cache[hashImage(imageBytes)]; | |
| } | |
| } | |
| ``` | |
| ### 4. Permission Handling | |
| ```dart | |
| import 'package:permission_handler/permission_handler.dart'; | |
| Future<bool> requestCameraPermission() async { | |
| final status = await Permission.camera.request(); | |
| return status.isGranted; | |
| } | |
| Future<bool> 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<int> compressImage(List<int> 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+ | |