ChestSense / INTEGRATION_GUIDELINES.md
NoumanUsman's picture
Upload folder using huggingface_hub
52e8264 verified
|
Raw
History Blame Contribute Delete
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+