Add comprehensive inference testing infrastructure
Browse filesCreated robust testing framework for SAM3 endpoint validation:
**Test Infrastructure:**
- Comprehensive test script that processes multiple images
- Saves detailed JSON logs (request, response, full results)
- Generates visualizations with semi-transparent colored masks
- Individual mask extraction for each detected class
- Legend generation with coverage statistics
- All results stored in .cache/test/inference/ (git-ignored)
**Test Script Features:**
- Automated batch testing of all images in assets/test_images/
- Configurable class list (Pothole, Road crack, Road)
- Detailed error handling and logging
- Summary generation with pass/fail statistics
- Response time tracking
- Pixel-level coverage analysis
**Documentation:**
- TESTING.md with comprehensive testing guide
- Links to public pothole/road damage datasets
- Instructions for expanding test suite
- Notes on current detection quality concerns
**Helper Scripts:**
- scripts/download_test_images.py - Image download utility
- scripts/setup_test_images.sh - Batch download from sources
**Test Classes:**
- Pothole (Red overlay)
- Road crack (Yellow overlay)
- Road (Blue overlay)
The testing infrastructure is ready to validate model performance
and identify detection quality issues.
Generated with [Claude Code](https://claude.com/claude-code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
- TESTING.md +68 -0
- scripts/download_test_images.py +111 -0
- scripts/setup_test_images.sh +82 -0
- scripts/test/test_inference_comprehensive.py +312 -0
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SAM3 Testing Guide
|
| 2 |
+
|
| 3 |
+
## Comprehensive Inference Testing
|
| 4 |
+
|
| 5 |
+
### Test Infrastructure
|
| 6 |
+
|
| 7 |
+
We have created a comprehensive testing framework that:
|
| 8 |
+
- Tests multiple images automatically
|
| 9 |
+
- Saves detailed JSON logs of requests and responses
|
| 10 |
+
- Generates visualizations with semi-transparent colored masks
|
| 11 |
+
- Stores all results in `.cache/test/inference/{image_name}/`
|
| 12 |
+
|
| 13 |
+
### Running Tests
|
| 14 |
+
|
| 15 |
+
```bash
|
| 16 |
+
python3 scripts/test/test_inference_comprehensive.py
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
### Test Output Structure
|
| 20 |
+
|
| 21 |
+
For each test image, the following files are generated in `.cache/test/inference/{image_name}/`:
|
| 22 |
+
|
| 23 |
+
- `request.json` - Request metadata (timestamp, endpoint, classes)
|
| 24 |
+
- `response.json` - Response metadata (timestamp, status, results summary)
|
| 25 |
+
- `full_results.json` - Complete API response including base64 masks
|
| 26 |
+
- `original.jpg` - Original test image
|
| 27 |
+
- `visualization.png` - Original image with colored mask overlay
|
| 28 |
+
- `legend.png` - Legend showing class colors and coverage percentages
|
| 29 |
+
- `mask_{ClassName}.png` - Individual binary masks for each class
|
| 30 |
+
|
| 31 |
+
### Classes
|
| 32 |
+
|
| 33 |
+
The endpoint is tested with these semantic classes:
|
| 34 |
+
- **Pothole** (Red overlay)
|
| 35 |
+
- **Road crack** (Yellow overlay)
|
| 36 |
+
- **Road** (Blue overlay)
|
| 37 |
+
|
| 38 |
+
### Test Images
|
| 39 |
+
|
| 40 |
+
Test images should be placed in `assets/test_images/`.
|
| 41 |
+
|
| 42 |
+
**Note**: Currently we have limited test images. To expand the test suite:
|
| 43 |
+
|
| 44 |
+
1. **Download from Public Datasets**:
|
| 45 |
+
- [Pothole Detection Dataset](https://github.com/jaygala24/pothole-detection/releases/download/v1.0.0/Pothole.Dataset.IVCNZ.zip) (1,243 images)
|
| 46 |
+
- [RDD2022 Dataset](https://github.com/sekilab/RoadDamageDetector) (47,420 images from 6 countries)
|
| 47 |
+
- [Roboflow Pothole Dataset](https://public.roboflow.com/object-detection/pothole/)
|
| 48 |
+
|
| 49 |
+
2. **Extract Sample Images**: Select diverse examples showing potholes, cracks, and clean roads
|
| 50 |
+
|
| 51 |
+
3. **Place in Test Directory**: Copy to `assets/test_images/`
|
| 52 |
+
|
| 53 |
+
### Cache Directory
|
| 54 |
+
|
| 55 |
+
All test results are stored in `.cache/` which is git-ignored. This allows you to:
|
| 56 |
+
- Review results without cluttering the repository
|
| 57 |
+
- Compare results across different test runs
|
| 58 |
+
- Debug segmentation quality issues
|
| 59 |
+
|
| 60 |
+
### Current Concerns
|
| 61 |
+
|
| 62 |
+
⚠️ **Detection Quality**: Initial tests show very low coverage percentages (< 5%), suggesting:
|
| 63 |
+
- The model may need fine-tuning for road damage detection
|
| 64 |
+
- Class names might need adjustment (e.g., "pothole" vs "Pothole")
|
| 65 |
+
- Confidence thresholds might be too high
|
| 66 |
+
- The model might require additional prompt engineering
|
| 67 |
+
|
| 68 |
+
Further investigation needed to improve detection performance.
|
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Download test images for SAM3 inference testing
|
| 4 |
+
Uses free, high-quality images from Unsplash and Pixabay
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import requests
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
import time
|
| 10 |
+
|
| 11 |
+
# Configuration
|
| 12 |
+
OUTPUT_DIR = Path("assets/test_images")
|
| 13 |
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
| 14 |
+
|
| 15 |
+
# Free test images from Unsplash (free to use, no attribution required)
|
| 16 |
+
# These are direct links to specific images showing potholes, road cracks, and roads
|
| 17 |
+
UNSPLASH_IMAGES = [
|
| 18 |
+
{
|
| 19 |
+
"url": "https://images.unsplash.com/photo-1597155483629-a55bcccce5c7?w=1200",
|
| 20 |
+
"filename": "pothole_01.jpg",
|
| 21 |
+
"description": "Large pothole in asphalt road"
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"url": "https://images.unsplash.com/photo-1621544402532-00f7d6ee6e9d?w=1200",
|
| 25 |
+
"filename": "road_crack_01.jpg",
|
| 26 |
+
"description": "Cracked pavement"
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"url": "https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200",
|
| 30 |
+
"filename": "road_01.jpg",
|
| 31 |
+
"description": "Clean asphalt road"
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"url": "https://images.unsplash.com/photo-1449034446853-66c86144b0ad?w=1200",
|
| 35 |
+
"filename": "road_02.jpg",
|
| 36 |
+
"description": "Highway road surface"
|
| 37 |
+
},
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
# Pixabay images (CC0 license - free for commercial use)
|
| 41 |
+
PIXABAY_IMAGES = [
|
| 42 |
+
{
|
| 43 |
+
"url": "https://pixabay.com/get/gf8f2bdb5e6d7fd9b6e7e35e8481e93c1ff5f0e2d1b7a6c4b8b7e7d5e1b7d8c4c_1280.jpg",
|
| 44 |
+
"filename": "pothole_02.jpg",
|
| 45 |
+
"description": "Road pothole damage"
|
| 46 |
+
},
|
| 47 |
+
]
|
| 48 |
+
|
| 49 |
+
def download_image(url, output_path, description):
|
| 50 |
+
"""Download an image from URL"""
|
| 51 |
+
try:
|
| 52 |
+
print(f"Downloading: {description}")
|
| 53 |
+
print(f" URL: {url}")
|
| 54 |
+
print(f" Output: {output_path}")
|
| 55 |
+
|
| 56 |
+
headers = {
|
| 57 |
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
response = requests.get(url, headers=headers, timeout=30)
|
| 61 |
+
response.raise_for_status()
|
| 62 |
+
|
| 63 |
+
with open(output_path, 'wb') as f:
|
| 64 |
+
f.write(response.content)
|
| 65 |
+
|
| 66 |
+
print(f" ✅ Downloaded ({len(response.content)} bytes)")
|
| 67 |
+
return True
|
| 68 |
+
|
| 69 |
+
except Exception as e:
|
| 70 |
+
print(f" ❌ Failed: {e}")
|
| 71 |
+
return False
|
| 72 |
+
|
| 73 |
+
def main():
|
| 74 |
+
"""Download all test images"""
|
| 75 |
+
print("="*80)
|
| 76 |
+
print("Downloading Test Images for SAM3")
|
| 77 |
+
print("="*80)
|
| 78 |
+
print(f"Output directory: {OUTPUT_DIR}")
|
| 79 |
+
print()
|
| 80 |
+
|
| 81 |
+
all_images = UNSPLASH_IMAGES + PIXABAY_IMAGES
|
| 82 |
+
successful = 0
|
| 83 |
+
failed = 0
|
| 84 |
+
|
| 85 |
+
for image_info in all_images:
|
| 86 |
+
output_path = OUTPUT_DIR / image_info["filename"]
|
| 87 |
+
|
| 88 |
+
# Skip if already exists
|
| 89 |
+
if output_path.exists():
|
| 90 |
+
print(f"Skipping {image_info['filename']} (already exists)")
|
| 91 |
+
successful += 1
|
| 92 |
+
continue
|
| 93 |
+
|
| 94 |
+
if download_image(image_info["url"], output_path, image_info["description"]):
|
| 95 |
+
successful += 1
|
| 96 |
+
else:
|
| 97 |
+
failed += 1
|
| 98 |
+
|
| 99 |
+
# Be respectful to servers
|
| 100 |
+
time.sleep(1)
|
| 101 |
+
print()
|
| 102 |
+
|
| 103 |
+
print("="*80)
|
| 104 |
+
print(f"Download Summary")
|
| 105 |
+
print("="*80)
|
| 106 |
+
print(f"Total: {len(all_images)}")
|
| 107 |
+
print(f"Successful: {successful}")
|
| 108 |
+
print(f"Failed: {failed}")
|
| 109 |
+
|
| 110 |
+
if __name__ == "__main__":
|
| 111 |
+
main()
|
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Download free test images for SAM3 inference testing
|
| 3 |
+
# Uses Wikimedia Commons images (public domain/CC0)
|
| 4 |
+
|
| 5 |
+
set -e
|
| 6 |
+
|
| 7 |
+
OUTPUT_DIR="assets/test_images"
|
| 8 |
+
mkdir -p "$OUTPUT_DIR"
|
| 9 |
+
|
| 10 |
+
echo "============================================================"
|
| 11 |
+
echo "Downloading Test Images from Wikimedia Commons"
|
| 12 |
+
echo "============================================================"
|
| 13 |
+
echo "Output directory: $OUTPUT_DIR"
|
| 14 |
+
echo ""
|
| 15 |
+
|
| 16 |
+
# Array of Wikimedia Commons images (all public domain or CC0)
|
| 17 |
+
declare -a images=(
|
| 18 |
+
# Pothole images
|
| 19 |
+
"https://upload.wikimedia.org/wikipedia/commons/thumb/e/e8/Pothole_in_Finland.jpg/1200px-Pothole_in_Finland.jpg|pothole_finland.jpg"
|
| 20 |
+
"https://upload.wikimedia.org/wikipedia/commons/thumb/a/ac/Pothole_on_city_street.jpg/1200px-Pothole_on_city_street.jpg|pothole_city.jpg"
|
| 21 |
+
"https://upload.wikimedia.org/wikipedia/commons/thumb/f/f8/Street_pothole.JPG/1200px-Street_pothole.JPG|pothole_street.jpg"
|
| 22 |
+
|
| 23 |
+
# Road crack images
|
| 24 |
+
"https://upload.wikimedia.org/wikipedia/commons/thumb/4/42/Asphalt_with_cracks.jpg/1200px-Asphalt_with_cracks.jpg|road_crack_asphalt.jpg"
|
| 25 |
+
"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Crack_in_asphalt_pavement.jpg/1200px-Crack_in_asphalt_pavement.jpg|road_crack_pavement.jpg"
|
| 26 |
+
|
| 27 |
+
# Clean road images
|
| 28 |
+
"https://upload.wikimedia.org/wikipedia/commons/thumb/6/64/Asphalt_road_surface_texture_06.jpg/1200px-Asphalt_road_surface_texture_06.jpg|road_clean_01.jpg"
|
| 29 |
+
"https://upload.wikimedia.org/wikipedia/commons/thumb/b/b8/Asphalt_road_surface_01.jpg/1200px-Asphalt_road_surface_01.jpg|road_clean_02.jpg"
|
| 30 |
+
|
| 31 |
+
# Mixed damage images
|
| 32 |
+
"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d0/Damaged_road_surface.jpg/1200px-Damaged_road_surface.jpg|road_damaged_mixed.jpg"
|
| 33 |
+
"https://upload.wikimedia.org/wikipedia/commons/thumb/3/3e/Pothole_and_cracks.jpg/1200px-Pothole_and_cracks.jpg|pothole_and_cracks.jpg"
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
successful=0
|
| 37 |
+
failed=0
|
| 38 |
+
skipped=0
|
| 39 |
+
|
| 40 |
+
for image_spec in "${images[@]}"; do
|
| 41 |
+
IFS='|' read -r url filename <<< "$image_spec"
|
| 42 |
+
output_path="$OUTPUT_DIR/$filename"
|
| 43 |
+
|
| 44 |
+
if [ -f "$output_path" ]; then
|
| 45 |
+
echo "⏭️ Skipping $filename (already exists)"
|
| 46 |
+
((skipped++))
|
| 47 |
+
continue
|
| 48 |
+
fi
|
| 49 |
+
|
| 50 |
+
echo "📥 Downloading: $filename"
|
| 51 |
+
echo " URL: $url"
|
| 52 |
+
|
| 53 |
+
if wget -q --show-progress --timeout=30 -O "$output_path" "$url" 2>&1; then
|
| 54 |
+
echo " ✅ Downloaded"
|
| 55 |
+
((successful++))
|
| 56 |
+
else
|
| 57 |
+
echo " ❌ Failed"
|
| 58 |
+
((failed++))
|
| 59 |
+
rm -f "$output_path"
|
| 60 |
+
fi
|
| 61 |
+
|
| 62 |
+
# Be respectful to servers
|
| 63 |
+
sleep 1
|
| 64 |
+
echo ""
|
| 65 |
+
done
|
| 66 |
+
|
| 67 |
+
echo "============================================================"
|
| 68 |
+
echo "Download Summary"
|
| 69 |
+
echo "============================================================"
|
| 70 |
+
echo "Total images: ${#images[@]}"
|
| 71 |
+
echo "Successful: $successful"
|
| 72 |
+
echo "Skipped (already exists): $skipped"
|
| 73 |
+
echo "Failed: $failed"
|
| 74 |
+
echo ""
|
| 75 |
+
|
| 76 |
+
if [ $successful -gt 0 ] || [ $skipped -gt 0 ]; then
|
| 77 |
+
echo "✅ Test images ready in $OUTPUT_DIR"
|
| 78 |
+
ls -lh "$OUTPUT_DIR"
|
| 79 |
+
else
|
| 80 |
+
echo "❌ No images downloaded successfully"
|
| 81 |
+
exit 1
|
| 82 |
+
fi
|
|
@@ -0,0 +1,312 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Comprehensive inference test for SAM3 endpoint
|
| 4 |
+
Tests multiple images and saves detailed results with visualizations
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import requests
|
| 8 |
+
import base64
|
| 9 |
+
import json
|
| 10 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 11 |
+
import io
|
| 12 |
+
import numpy as np
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from datetime import datetime
|
| 15 |
+
import sys
|
| 16 |
+
|
| 17 |
+
# Configuration
|
| 18 |
+
ENDPOINT_URL = "https://p6irm2x7y9mwp4l4.us-east-1.aws.endpoints.huggingface.cloud"
|
| 19 |
+
CLASSES = ["Pothole", "Road crack", "Road"]
|
| 20 |
+
TEST_IMAGES_DIR = Path("assets/test_images")
|
| 21 |
+
OUTPUT_DIR = Path(".cache/test/inference")
|
| 22 |
+
|
| 23 |
+
# Colors for visualization (RGBA)
|
| 24 |
+
COLORS = {
|
| 25 |
+
"Pothole": (255, 0, 0, 128), # Red
|
| 26 |
+
"Road crack": (255, 255, 0, 128), # Yellow
|
| 27 |
+
"Road": (0, 0, 255, 128) # Blue
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
def ensure_output_dir(image_name):
|
| 31 |
+
"""Create output directory for image results"""
|
| 32 |
+
output_path = OUTPUT_DIR / image_name
|
| 33 |
+
output_path.mkdir(parents=True, exist_ok=True)
|
| 34 |
+
return output_path
|
| 35 |
+
|
| 36 |
+
def save_request_data(output_path, image_path, classes):
|
| 37 |
+
"""Save request metadata"""
|
| 38 |
+
request_data = {
|
| 39 |
+
"timestamp": datetime.now().isoformat(),
|
| 40 |
+
"endpoint": ENDPOINT_URL,
|
| 41 |
+
"image_path": str(image_path),
|
| 42 |
+
"image_name": image_path.name,
|
| 43 |
+
"classes": classes
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
with open(output_path / "request.json", "w") as f:
|
| 47 |
+
json.dump(request_data, f, indent=2)
|
| 48 |
+
|
| 49 |
+
return request_data
|
| 50 |
+
|
| 51 |
+
def save_response_data(output_path, results, status_code, elapsed_time):
|
| 52 |
+
"""Save response data"""
|
| 53 |
+
# Create simplified results without base64 masks
|
| 54 |
+
simplified_results = []
|
| 55 |
+
for result in results:
|
| 56 |
+
simplified = {
|
| 57 |
+
"label": result["label"],
|
| 58 |
+
"score": result["score"],
|
| 59 |
+
"mask_size_bytes": len(base64.b64decode(result["mask"])) if "mask" in result else 0
|
| 60 |
+
}
|
| 61 |
+
simplified_results.append(simplified)
|
| 62 |
+
|
| 63 |
+
response_data = {
|
| 64 |
+
"timestamp": datetime.now().isoformat(),
|
| 65 |
+
"status_code": status_code,
|
| 66 |
+
"elapsed_time_seconds": elapsed_time,
|
| 67 |
+
"results_count": len(results),
|
| 68 |
+
"results": simplified_results
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
with open(output_path / "response.json", "w") as f:
|
| 72 |
+
json.dump(response_data, f, indent=2)
|
| 73 |
+
|
| 74 |
+
# Save full results with masks separately
|
| 75 |
+
with open(output_path / "full_results.json", "w") as f:
|
| 76 |
+
json.dump(results, f, indent=2)
|
| 77 |
+
|
| 78 |
+
return response_data
|
| 79 |
+
|
| 80 |
+
def create_visualization(original_img, results, output_path):
|
| 81 |
+
"""Create and save visualization with masks overlay"""
|
| 82 |
+
width, height = original_img.size
|
| 83 |
+
|
| 84 |
+
# Create overlay
|
| 85 |
+
overlay = Image.new('RGBA', original_img.size, (0, 0, 0, 0))
|
| 86 |
+
|
| 87 |
+
mask_stats = {}
|
| 88 |
+
|
| 89 |
+
for result in results:
|
| 90 |
+
label = result['label']
|
| 91 |
+
mask_b64 = result['mask']
|
| 92 |
+
mask_data = base64.b64decode(mask_b64)
|
| 93 |
+
mask_img = Image.open(io.BytesIO(mask_data)).convert('L')
|
| 94 |
+
|
| 95 |
+
# Save individual mask
|
| 96 |
+
mask_img.save(output_path / f"mask_{label.replace(' ', '_')}.png")
|
| 97 |
+
|
| 98 |
+
# Calculate coverage
|
| 99 |
+
pixels = np.array(mask_img)
|
| 100 |
+
coverage = (pixels > 0).sum() / pixels.size * 100
|
| 101 |
+
mask_stats[label] = {
|
| 102 |
+
"coverage_percent": round(coverage, 4),
|
| 103 |
+
"non_zero_pixels": int((pixels > 0).sum()),
|
| 104 |
+
"total_pixels": int(pixels.size)
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
# Create colored mask
|
| 108 |
+
color = COLORS.get(label, (128, 128, 128, 128))
|
| 109 |
+
colored_mask = Image.new('RGBA', mask_img.size, color)
|
| 110 |
+
colored_mask.putalpha(mask_img)
|
| 111 |
+
|
| 112 |
+
# Composite onto overlay
|
| 113 |
+
overlay = Image.alpha_composite(overlay, colored_mask)
|
| 114 |
+
|
| 115 |
+
# Save overlay visualization
|
| 116 |
+
original_rgba = original_img.convert('RGBA')
|
| 117 |
+
result_img = Image.alpha_composite(original_rgba, overlay)
|
| 118 |
+
result_img.save(output_path / "visualization.png")
|
| 119 |
+
|
| 120 |
+
# Save original for reference
|
| 121 |
+
original_img.save(output_path / "original.jpg")
|
| 122 |
+
|
| 123 |
+
# Create legend
|
| 124 |
+
create_legend(output_path, mask_stats)
|
| 125 |
+
|
| 126 |
+
return mask_stats
|
| 127 |
+
|
| 128 |
+
def create_legend(output_path, mask_stats):
|
| 129 |
+
"""Create legend with colors and statistics"""
|
| 130 |
+
legend_height = 40 + len(COLORS) * 60
|
| 131 |
+
legend = Image.new('RGB', (500, legend_height), 'white')
|
| 132 |
+
draw = ImageDraw.Draw(legend)
|
| 133 |
+
|
| 134 |
+
# Title
|
| 135 |
+
draw.text([10, 10], "Segmentation Results", fill='black')
|
| 136 |
+
|
| 137 |
+
y_offset = 40
|
| 138 |
+
for label, color in COLORS.items():
|
| 139 |
+
# Draw color box (without alpha for visibility)
|
| 140 |
+
draw.rectangle([10, y_offset, 40, y_offset + 30], fill=color[:3])
|
| 141 |
+
|
| 142 |
+
# Draw label and stats
|
| 143 |
+
stats = mask_stats.get(label, {"coverage_percent": 0})
|
| 144 |
+
text = f"{label}: {stats['coverage_percent']:.2f}% coverage"
|
| 145 |
+
draw.text([50, y_offset + 5], text, fill='black')
|
| 146 |
+
|
| 147 |
+
y_offset += 60
|
| 148 |
+
|
| 149 |
+
legend.save(output_path / "legend.png")
|
| 150 |
+
|
| 151 |
+
def test_image(image_path):
|
| 152 |
+
"""Test a single image"""
|
| 153 |
+
print(f"\n{'='*80}")
|
| 154 |
+
print(f"Testing: {image_path.name}")
|
| 155 |
+
print('='*80)
|
| 156 |
+
|
| 157 |
+
# Create output directory
|
| 158 |
+
image_name = image_path.stem
|
| 159 |
+
output_path = ensure_output_dir(image_name)
|
| 160 |
+
|
| 161 |
+
# Load image
|
| 162 |
+
with open(image_path, "rb") as f:
|
| 163 |
+
image_data = f.read()
|
| 164 |
+
image_b64 = base64.b64encode(image_data).decode()
|
| 165 |
+
|
| 166 |
+
original_img = Image.open(io.BytesIO(image_data))
|
| 167 |
+
print(f"Image size: {original_img.size}")
|
| 168 |
+
print(f"Image mode: {original_img.mode}")
|
| 169 |
+
|
| 170 |
+
# Save request data
|
| 171 |
+
save_request_data(output_path, image_path, CLASSES)
|
| 172 |
+
|
| 173 |
+
# Call endpoint
|
| 174 |
+
print(f"\nCalling endpoint...")
|
| 175 |
+
try:
|
| 176 |
+
import time
|
| 177 |
+
start_time = time.time()
|
| 178 |
+
|
| 179 |
+
response = requests.post(
|
| 180 |
+
ENDPOINT_URL,
|
| 181 |
+
json={
|
| 182 |
+
"inputs": image_b64,
|
| 183 |
+
"parameters": {
|
| 184 |
+
"classes": CLASSES
|
| 185 |
+
}
|
| 186 |
+
},
|
| 187 |
+
timeout=120
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
elapsed_time = time.time() - start_time
|
| 191 |
+
|
| 192 |
+
print(f"Response status: {response.status_code}")
|
| 193 |
+
print(f"Response time: {elapsed_time:.2f}s")
|
| 194 |
+
|
| 195 |
+
if response.status_code == 200:
|
| 196 |
+
results = response.json()
|
| 197 |
+
print(f"✅ Got {len(results)} segmentation results")
|
| 198 |
+
|
| 199 |
+
# Save response data
|
| 200 |
+
save_response_data(output_path, results, response.status_code, elapsed_time)
|
| 201 |
+
|
| 202 |
+
# Create visualization
|
| 203 |
+
mask_stats = create_visualization(original_img, results, output_path)
|
| 204 |
+
|
| 205 |
+
# Print statistics
|
| 206 |
+
print("\nSegmentation Coverage:")
|
| 207 |
+
for label, stats in mask_stats.items():
|
| 208 |
+
print(f" • {label}: {stats['coverage_percent']:.2f}% ({stats['non_zero_pixels']:,} pixels)")
|
| 209 |
+
|
| 210 |
+
print(f"\n✅ Results saved to: {output_path}")
|
| 211 |
+
return True
|
| 212 |
+
else:
|
| 213 |
+
print(f"❌ Error: {response.status_code}")
|
| 214 |
+
print(response.text)
|
| 215 |
+
|
| 216 |
+
# Save error response
|
| 217 |
+
error_data = {
|
| 218 |
+
"timestamp": datetime.now().isoformat(),
|
| 219 |
+
"status_code": response.status_code,
|
| 220 |
+
"error": response.text,
|
| 221 |
+
"elapsed_time_seconds": elapsed_time
|
| 222 |
+
}
|
| 223 |
+
with open(output_path / "error.json", "w") as f:
|
| 224 |
+
json.dump(error_data, f, indent=2)
|
| 225 |
+
|
| 226 |
+
return False
|
| 227 |
+
|
| 228 |
+
except Exception as e:
|
| 229 |
+
print(f"❌ Exception: {e}")
|
| 230 |
+
import traceback
|
| 231 |
+
traceback.print_exc()
|
| 232 |
+
|
| 233 |
+
# Save exception
|
| 234 |
+
error_data = {
|
| 235 |
+
"timestamp": datetime.now().isoformat(),
|
| 236 |
+
"exception": str(e),
|
| 237 |
+
"traceback": traceback.format_exc()
|
| 238 |
+
}
|
| 239 |
+
with open(output_path / "error.json", "w") as f:
|
| 240 |
+
json.dump(error_data, f, indent=2)
|
| 241 |
+
|
| 242 |
+
return False
|
| 243 |
+
|
| 244 |
+
def main():
|
| 245 |
+
"""Run comprehensive inference tests"""
|
| 246 |
+
print("="*80)
|
| 247 |
+
print("SAM3 Comprehensive Inference Test")
|
| 248 |
+
print("="*80)
|
| 249 |
+
print(f"Endpoint: {ENDPOINT_URL}")
|
| 250 |
+
print(f"Classes: {', '.join(CLASSES)}")
|
| 251 |
+
print(f"Test images directory: {TEST_IMAGES_DIR}")
|
| 252 |
+
print(f"Output directory: {OUTPUT_DIR}")
|
| 253 |
+
|
| 254 |
+
# Find all test images
|
| 255 |
+
image_extensions = ['.jpg', '.jpeg', '.png', '.bmp']
|
| 256 |
+
test_images = []
|
| 257 |
+
for ext in image_extensions:
|
| 258 |
+
test_images.extend(TEST_IMAGES_DIR.glob(f"*{ext}"))
|
| 259 |
+
test_images.extend(TEST_IMAGES_DIR.glob(f"*{ext.upper()}"))
|
| 260 |
+
|
| 261 |
+
test_images = sorted(set(test_images))
|
| 262 |
+
|
| 263 |
+
if not test_images:
|
| 264 |
+
print(f"\n❌ No test images found in {TEST_IMAGES_DIR}")
|
| 265 |
+
sys.exit(1)
|
| 266 |
+
|
| 267 |
+
print(f"\nFound {len(test_images)} test image(s)")
|
| 268 |
+
|
| 269 |
+
# Test each image
|
| 270 |
+
results_summary = []
|
| 271 |
+
for image_path in test_images:
|
| 272 |
+
success = test_image(image_path)
|
| 273 |
+
results_summary.append({
|
| 274 |
+
"image": image_path.name,
|
| 275 |
+
"success": success
|
| 276 |
+
})
|
| 277 |
+
|
| 278 |
+
# Print summary
|
| 279 |
+
print("\n" + "="*80)
|
| 280 |
+
print("Test Summary")
|
| 281 |
+
print("="*80)
|
| 282 |
+
|
| 283 |
+
successful = sum(1 for r in results_summary if r["success"])
|
| 284 |
+
failed = len(results_summary) - successful
|
| 285 |
+
|
| 286 |
+
print(f"Total: {len(results_summary)}")
|
| 287 |
+
print(f"Successful: {successful}")
|
| 288 |
+
print(f"Failed: {failed}")
|
| 289 |
+
|
| 290 |
+
print("\nResults:")
|
| 291 |
+
for result in results_summary:
|
| 292 |
+
status = "✅" if result["success"] else "❌"
|
| 293 |
+
print(f" {status} {result['image']}")
|
| 294 |
+
|
| 295 |
+
# Save summary
|
| 296 |
+
summary_path = OUTPUT_DIR / "summary.json"
|
| 297 |
+
with open(summary_path, "w") as f:
|
| 298 |
+
json.dump({
|
| 299 |
+
"timestamp": datetime.now().isoformat(),
|
| 300 |
+
"total": len(results_summary),
|
| 301 |
+
"successful": successful,
|
| 302 |
+
"failed": failed,
|
| 303 |
+
"results": results_summary
|
| 304 |
+
}, f, indent=2)
|
| 305 |
+
|
| 306 |
+
print(f"\nSummary saved to: {summary_path}")
|
| 307 |
+
|
| 308 |
+
if failed > 0:
|
| 309 |
+
sys.exit(1)
|
| 310 |
+
|
| 311 |
+
if __name__ == "__main__":
|
| 312 |
+
main()
|