update project structure and improve scripts
This commit is contained in:
+7
-1
@@ -33,6 +33,7 @@ static/results/
|
|||||||
*.jpg
|
*.jpg
|
||||||
*.jpeg
|
*.jpeg
|
||||||
*.png
|
*.png
|
||||||
|
images/
|
||||||
|
|
||||||
# Log files
|
# Log files
|
||||||
*.log
|
*.log
|
||||||
@@ -54,4 +55,9 @@ local_settings.py
|
|||||||
# Large files
|
# Large files
|
||||||
*.zip
|
*.zip
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.pth
|
*.pth
|
||||||
|
*.csv
|
||||||
|
*.pt
|
||||||
|
|
||||||
|
# Test files
|
||||||
|
tests/
|
||||||
+211
-51
@@ -1,77 +1,237 @@
|
|||||||
from flask import Flask, request, jsonify, send_file
|
|
||||||
from pathlib import Path
|
|
||||||
from ultralytics import YOLO
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
import logging
|
import logging
|
||||||
from io import BytesIO
|
import random
|
||||||
|
from pathlib import Path
|
||||||
|
from flask import Flask, request, jsonify, send_file, render_template
|
||||||
|
import cv2
|
||||||
|
|
||||||
|
from config import Config
|
||||||
|
from detector import MemoryDetector
|
||||||
|
from exceptions import DetectionError, FileUploadError, ValidationError, VideoProcessingError
|
||||||
|
from utils import allowed_file, setup_logging
|
||||||
|
from video_processor import VideoProcessor
|
||||||
|
|
||||||
|
# Initialize Flask app
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
logging.basicConfig(level=logging.INFO)
|
app.config['MAX_CONTENT_LENGTH'] = Config.MAX_FILE_SIZE
|
||||||
|
|
||||||
# Initialize detector
|
# Setup logging
|
||||||
MODEL_PATH = str(Path(__file__).parent.parent / "runs" / "detect" / "train" / "weights" / "best.pt")
|
setup_logging()
|
||||||
model = YOLO(MODEL_PATH)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Initialize detector and video processor
|
||||||
|
detector = None
|
||||||
|
video_processor = None
|
||||||
|
|
||||||
|
|
||||||
|
def init_app():
|
||||||
|
"""Initialize application."""
|
||||||
|
global detector, video_processor
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create directories
|
||||||
|
Config.create_directories()
|
||||||
|
|
||||||
|
# Validate model exists
|
||||||
|
Config.validate_model()
|
||||||
|
|
||||||
|
# Initialize detector
|
||||||
|
detector = MemoryDetector(
|
||||||
|
model_path=Config.MODEL_PATH,
|
||||||
|
confidence_threshold=Config.CONFIDENCE_THRESHOLD,
|
||||||
|
image_size=Config.IMAGE_SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize video processor
|
||||||
|
video_processor = VideoProcessor(detector)
|
||||||
|
|
||||||
|
logger.info("Application initialized successfully")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Application initialization failed: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
return send_file('test.html')
|
"""Serve the frontend interface."""
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/detect', methods=['POST'])
|
@app.route('/api/health')
|
||||||
def detect():
|
def health_check():
|
||||||
if 'image' not in request.files:
|
"""Health check endpoint."""
|
||||||
return jsonify({'error': 'No image provided'}), 400
|
return jsonify({
|
||||||
|
'status': 'healthy',
|
||||||
|
'service': 'Memory Detection API',
|
||||||
|
'model_loaded': detector is not None
|
||||||
|
})
|
||||||
|
|
||||||
file = request.files['image']
|
|
||||||
if file.filename == '':
|
|
||||||
return jsonify({'error': 'No selected file'}), 400
|
|
||||||
|
|
||||||
|
@app.route('/api/v1/detect', methods=['POST'])
|
||||||
|
def detect_memory():
|
||||||
|
"""Detect memory modules in uploaded image."""
|
||||||
try:
|
try:
|
||||||
# Read image directly from memory
|
# Validate file upload
|
||||||
img_bytes = file.read()
|
if 'image' not in request.files:
|
||||||
img = cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_COLOR)
|
raise FileUploadError("No image file provided")
|
||||||
|
|
||||||
# Run prediction
|
|
||||||
results = model.predict(img, imgsz=416, conf=0.3)
|
|
||||||
|
|
||||||
# Generate annotated image
|
file = request.files['image']
|
||||||
annotated = results[0].plot(line_width=2, font_size=0.5)
|
if file.filename == '':
|
||||||
|
raise FileUploadError("No file selected")
|
||||||
|
|
||||||
# Save to results folder
|
if not allowed_file(file.filename, Config.ALLOWED_EXTENSIONS):
|
||||||
output_dir = Path("static/results")
|
raise ValidationError(f"Invalid file type. Allowed: {Config.ALLOWED_EXTENSIONS}")
|
||||||
output_dir.mkdir(exist_ok=True)
|
|
||||||
|
# Process image
|
||||||
|
logger.info(f"Processing uploaded file: {file.filename}")
|
||||||
|
|
||||||
|
# Read image data
|
||||||
|
image_bytes = file.read()
|
||||||
|
|
||||||
|
# Perform detection
|
||||||
|
result = detector.detect_from_bytes(image_bytes)
|
||||||
|
|
||||||
|
# Save result image
|
||||||
filename = f"{uuid.uuid4()}.jpg"
|
filename = f"{uuid.uuid4()}.jpg"
|
||||||
output_path = output_dir / filename
|
output_path = Path(Config.RESULT_FOLDER) / filename
|
||||||
cv2.imwrite(str(output_path), annotated)
|
cv2.imwrite(str(output_path), result['annotated_image'])
|
||||||
|
|
||||||
# Extract detections
|
|
||||||
detections = []
|
|
||||||
for box in results[0].boxes:
|
|
||||||
detections.append({
|
|
||||||
'box': box.xyxy[0].tolist(),
|
|
||||||
'confidence': float(box.conf[0]),
|
|
||||||
'class': int(box.cls[0])
|
|
||||||
})
|
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'detections': detections,
|
'success': True,
|
||||||
'result_image': f"/results/{filename}"
|
'data': {
|
||||||
|
'detections': result['detections'],
|
||||||
|
'detection_count': result['detection_count'],
|
||||||
|
'result_image_url': f"/api/v1/results/{filename}"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except (DetectionError, FileUploadError, ValidationError) as e:
|
||||||
|
logger.error(f"Detection request failed: {e}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error in detection: {e}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Internal server error'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/v1/results/<filename>')
|
||||||
|
def get_result_image(filename):
|
||||||
|
"""Get result image."""
|
||||||
|
try:
|
||||||
|
file_path = Path(Config.RESULT_FOLDER) / filename
|
||||||
|
if not file_path.exists():
|
||||||
|
return jsonify({'error': 'Image not found'}), 404
|
||||||
|
|
||||||
|
return send_file(file_path)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error serving result image: {e}")
|
||||||
|
return jsonify({'error': 'Internal server error'}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/v1/detect/video', methods=['POST'])
|
||||||
|
def detect_video():
|
||||||
|
"""Process video for memory module detection."""
|
||||||
|
try:
|
||||||
|
# Validate video upload
|
||||||
|
if 'video' not in request.files:
|
||||||
|
raise FileUploadError("No video file provided")
|
||||||
|
|
||||||
|
file = request.files['video']
|
||||||
|
if file.filename == '':
|
||||||
|
raise FileUploadError("No file selected")
|
||||||
|
|
||||||
|
# Check video format
|
||||||
|
video_extensions = {'mp4', 'avi', 'mov', 'mkv'}
|
||||||
|
if not allowed_file(file.filename, video_extensions):
|
||||||
|
raise ValidationError(f"Invalid video format. Allowed: {video_extensions}")
|
||||||
|
|
||||||
|
# Save uploaded video temporarily
|
||||||
|
video_filename = f"temp_{uuid.uuid4()}.mp4"
|
||||||
|
video_path = Path(Config.UPLOAD_FOLDER) / video_filename
|
||||||
|
file.save(str(video_path))
|
||||||
|
|
||||||
|
# Get processing parameters
|
||||||
|
fps = request.form.get('fps', 1, type=int)
|
||||||
|
max_frames = request.form.get('max_frames', 50, type=int)
|
||||||
|
|
||||||
|
logger.info(f"Processing video: {file.filename}, fps={fps}, max_frames={max_frames}")
|
||||||
|
|
||||||
|
# Process video
|
||||||
|
result = video_processor.process_video(
|
||||||
|
str(video_path),
|
||||||
|
fps=fps,
|
||||||
|
max_frames=max_frames
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean up temporary file
|
||||||
|
video_path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'data': {
|
||||||
|
'video_filename': file.filename,
|
||||||
|
'processing_info': result['processing_info'],
|
||||||
|
'detections': result['detections'],
|
||||||
|
'summary': result['summary']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except (VideoProcessingError, FileUploadError, ValidationError) as e:
|
||||||
|
logger.error(f"Video processing failed: {e}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error in video processing: {e}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Internal server error'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/v1/test-images')
|
||||||
|
def list_test_images():
|
||||||
|
"""List available test images."""
|
||||||
|
try:
|
||||||
|
test_images = Config.get_test_images()
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'test_images': [img.name for img in test_images]
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Detection error: {str(e)}")
|
logger.error(f"Error listing test images: {e}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': 'Internal server error'}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(413)
|
||||||
|
def file_too_large(e):
|
||||||
|
"""Handle file too large error."""
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': f'File too large. Maximum size: {Config.MAX_FILE_SIZE} bytes'
|
||||||
|
}), 413
|
||||||
|
|
||||||
@app.route('/results/<filename>')
|
|
||||||
def get_result(filename):
|
|
||||||
return send_file(Path("static/results") / filename)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Create directories
|
# Initialize application
|
||||||
Path("static/uploads").mkdir(parents=True, exist_ok=True)
|
init_app()
|
||||||
Path("static/results").mkdir(parents=True, exist_ok=True)
|
|
||||||
app.run(host='0.0.0.0', port=5000)
|
# Start server
|
||||||
|
logger.info(f"Starting server on {Config.HOST}:{Config.PORT}")
|
||||||
|
app.run(
|
||||||
|
host=Config.HOST,
|
||||||
|
port=Config.PORT,
|
||||||
|
debug=Config.DEBUG
|
||||||
|
)
|
||||||
+54
-5
@@ -1,12 +1,61 @@
|
|||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
# Server settings
|
||||||
|
HOST = os.environ.get('HOST', '0.0.0.0')
|
||||||
|
PORT = int(os.environ.get('PORT', 5000))
|
||||||
|
DEBUG = os.environ.get('FLASK_ENV') == 'development'
|
||||||
|
|
||||||
|
# File settings
|
||||||
UPLOAD_FOLDER = 'static/uploads'
|
UPLOAD_FOLDER = 'static/uploads'
|
||||||
RESULT_FOLDER = 'static/results'
|
RESULT_FOLDER = 'static/results'
|
||||||
HARDCODED_IMAGES = 'training/memory'
|
MAX_FILE_SIZE = 16 * 1024 * 1024 # 16MB
|
||||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg'}
|
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'bmp'}
|
||||||
|
|
||||||
# Model configuration
|
# Model settings - pre-trained model
|
||||||
MODEL_PATH = 'models/memory_detector.pt'
|
MODEL_PATH = os.environ.get('MODEL_PATH', 'yolov8n.pt') # Auto-downloads if not exists
|
||||||
CONFIDENCE_THRESHOLD = 0.5
|
CONFIDENCE_THRESHOLD = float(os.environ.get('CONFIDENCE_THRESHOLD', 0.3))
|
||||||
|
IMAGE_SIZE = 416
|
||||||
|
|
||||||
|
# Hardcoded test images
|
||||||
|
TEST_IMAGES_PATH = 'training/memory'
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO')
|
||||||
|
LOG_FILE = 'logs/app.log'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_directories(cls):
|
||||||
|
"""Create required directories."""
|
||||||
|
directories = [
|
||||||
|
cls.UPLOAD_FOLDER,
|
||||||
|
cls.RESULT_FOLDER,
|
||||||
|
Path(cls.LOG_FILE).parent,
|
||||||
|
]
|
||||||
|
|
||||||
|
for directory in directories:
|
||||||
|
Path(directory).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate_model(cls):
|
||||||
|
"""Check if model file exists or can be downloaded."""
|
||||||
|
# For pre-trained models
|
||||||
|
if cls.MODEL_PATH in ['yolov8n.pt', 'yolov8s.pt', 'yolov8m.pt', 'yolov8l.pt', 'yolov8x.pt']:
|
||||||
|
print(f"Using pre-trained model: {cls.MODEL_PATH} (will auto-download if needed)")
|
||||||
|
return
|
||||||
|
|
||||||
|
# For custom models, check if file exists
|
||||||
|
if not Path(cls.MODEL_PATH).exists():
|
||||||
|
raise FileNotFoundError(f"Model file not found: {cls.MODEL_PATH}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_test_images(cls):
|
||||||
|
"""Get list of test images."""
|
||||||
|
test_path = Path(cls.TEST_IMAGES_PATH)
|
||||||
|
if not test_path.exists():
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [f for f in test_path.iterdir()
|
||||||
|
if f.suffix.lower()[1:] in cls.ALLOWED_EXTENSIONS]
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import pandas as pd
|
|
||||||
from pathlib import Path
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
def csv_to_yolo(csv_path, output_dir):
|
|
||||||
# Create output directory if it doesn't exist
|
|
||||||
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
df = pd.read_csv(csv_path)
|
|
||||||
|
|
||||||
for filename in df['filename'].unique():
|
|
||||||
img_data = df[df['filename'] == filename].iloc[0]
|
|
||||||
img_w, img_h = img_data['img_width'], img_data['img_height']
|
|
||||||
|
|
||||||
yolo_lines = []
|
|
||||||
for _, row in df[df['filename'] == filename].iterrows():
|
|
||||||
# Convert absolute to normalized coordinates
|
|
||||||
x_center = ((row['x1'] + row['x2']) / 2) / img_w
|
|
||||||
y_center = ((row['y1'] + row['y2']) / 2) / img_h
|
|
||||||
width = abs(row['x2'] - row['x1']) / img_w
|
|
||||||
height = abs(row['y2'] - row['y1']) / img_h
|
|
||||||
|
|
||||||
yolo_lines.append(f"0 {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}")
|
|
||||||
|
|
||||||
# Save as YOLO .txt file
|
|
||||||
txt_path = Path(output_dir) / f"{Path(filename).stem}.txt"
|
|
||||||
with open(txt_path, 'w') as f:
|
|
||||||
f.write("\n".join(yolo_lines))
|
|
||||||
print(f"Successfully converted CSV to YOLO format in {output_dir}")
|
|
||||||
|
|
||||||
# Error handling
|
|
||||||
try:
|
|
||||||
csv_to_yolo("annotations.csv", "yolo_labels")
|
|
||||||
except FileNotFoundError:
|
|
||||||
print("Error: annotations.csv not found. Please check the file path.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"An error occurred: {str(e)}")
|
|
||||||
+116
-35
@@ -1,55 +1,136 @@
|
|||||||
from ultralytics import YOLO
|
|
||||||
import cv2
|
import cv2
|
||||||
from pathlib import Path
|
import numpy as np
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from ultralytics import YOLO
|
||||||
|
from exceptions import ModelLoadError, DetectionError, ImageProcessingError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MemoryDetector:
|
class MemoryDetector:
|
||||||
def __init__(self, model_path):
|
def __init__(self, model_path, confidence_threshold=0.3, image_size=416):
|
||||||
|
self.model_path = model_path
|
||||||
|
self.confidence_threshold = confidence_threshold
|
||||||
|
self.image_size = image_size
|
||||||
|
self.model = None
|
||||||
|
self._load_model()
|
||||||
|
|
||||||
|
def _load_model(self):
|
||||||
|
"""Load YOLO model with error handling."""
|
||||||
try:
|
try:
|
||||||
self.model = YOLO(model_path)
|
if not Path(self.model_path).exists():
|
||||||
logger.info(f"Loaded model from {model_path}")
|
raise FileNotFoundError(f"Model file not found: {self.model_path}")
|
||||||
|
|
||||||
|
logger.info(f"Loading model from {self.model_path}")
|
||||||
|
self.model = YOLO(self.model_path)
|
||||||
|
logger.info("Model loaded successfully")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Model loading failed: {str(e)}")
|
logger.error(f"Failed to load model: {e}")
|
||||||
raise
|
raise ModelLoadError(f"Model loading failed: {str(e)}")
|
||||||
|
|
||||||
def detect(self, image_path):
|
def detect_from_bytes(self, image_bytes):
|
||||||
|
"""Detect memory modules from image bytes."""
|
||||||
try:
|
try:
|
||||||
|
# Decode image
|
||||||
|
nparr = np.frombuffer(image_bytes, np.uint8)
|
||||||
|
image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
||||||
|
|
||||||
|
if image is None:
|
||||||
|
raise ImageProcessingError("Could not decode image")
|
||||||
|
|
||||||
|
return self._perform_detection(image)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Detection from bytes failed: {e}")
|
||||||
|
raise DetectionError(f"Detection failed: {str(e)}")
|
||||||
|
|
||||||
|
def detect_from_file(self, file_path):
|
||||||
|
"""Detect memory modules from image file."""
|
||||||
|
try:
|
||||||
|
if not Path(file_path).exists():
|
||||||
|
raise FileNotFoundError(f"Image file not found: {file_path}")
|
||||||
|
|
||||||
|
image = cv2.imread(str(file_path))
|
||||||
|
if image is None:
|
||||||
|
raise ImageProcessingError("Could not load image file")
|
||||||
|
|
||||||
|
return self._perform_detection(image)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Detection from file failed: {e}")
|
||||||
|
raise DetectionError(f"Detection failed: {str(e)}")
|
||||||
|
|
||||||
|
def _perform_detection(self, image):
|
||||||
|
"""Perform detection on image."""
|
||||||
|
try:
|
||||||
|
logger.info("Running detection")
|
||||||
|
|
||||||
# Run inference
|
# Run inference
|
||||||
results = self.model.predict(image_path, imgsz=416, conf=0.5)
|
results = self.model.predict(
|
||||||
|
image,
|
||||||
|
imgsz=self.image_size,
|
||||||
|
conf=self.confidence_threshold,
|
||||||
|
verbose=False
|
||||||
|
)
|
||||||
|
|
||||||
# Extract results
|
# Extract detections
|
||||||
boxes = results[0].boxes.xyxy.cpu().numpy()
|
detections = self._extract_detections(results[0])
|
||||||
confidences = results[0].boxes.conf.cpu().numpy()
|
|
||||||
|
|
||||||
# Convert to list of [x1, y1, x2, y2, confidence]
|
# Create annotated image
|
||||||
detections = []
|
annotated_image = self._draw_boxes(image, detections)
|
||||||
for box, conf in zip(boxes, confidences):
|
|
||||||
detections.append({
|
|
||||||
'box': [int(x) for x in box],
|
|
||||||
'confidence': float(conf)
|
|
||||||
})
|
|
||||||
|
|
||||||
# Annotate image
|
logger.info(f"Detection completed: {len(detections)} objects found")
|
||||||
annotated_img = self._draw_boxes(image_path, detections)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'detections': detections,
|
'detections': detections,
|
||||||
'annotated_image': annotated_img
|
'annotated_image': annotated_image,
|
||||||
|
'detection_count': len(detections)
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Detection failed: {str(e)}")
|
logger.error(f"Detection processing failed: {e}")
|
||||||
raise
|
raise DetectionError(f"Detection processing failed: {str(e)}")
|
||||||
|
|
||||||
def _draw_boxes(self, image_path, detections):
|
def _extract_detections(self, result):
|
||||||
img = cv2.imread(str(image_path))
|
"""Extract detection results."""
|
||||||
for det in detections:
|
detections = []
|
||||||
x1, y1, x2, y2 = det['box']
|
|
||||||
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
|
if result.boxes is not None:
|
||||||
cv2.putText(img, f"{det['confidence']:.2f}",
|
boxes = result.boxes.xyxy.cpu().numpy()
|
||||||
(x1, y1-10),
|
confidences = result.boxes.conf.cpu().numpy()
|
||||||
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,0), 1)
|
classes = result.boxes.cls.cpu().numpy() if result.boxes.cls is not None else None
|
||||||
return img
|
|
||||||
|
for i, (box, conf) in enumerate(zip(boxes, confidences)):
|
||||||
|
detection = {
|
||||||
|
'box': [float(coord) for coord in box], # [x1, y1, x2, y2]
|
||||||
|
'confidence': float(conf),
|
||||||
|
'class': int(classes[i]) if classes is not None else 0
|
||||||
|
}
|
||||||
|
detections.append(detection)
|
||||||
|
|
||||||
|
return detections
|
||||||
|
|
||||||
|
def _draw_boxes(self, image, detections):
|
||||||
|
"""Draw bounding boxes on image."""
|
||||||
|
annotated = image.copy()
|
||||||
|
|
||||||
|
for detection in detections:
|
||||||
|
box = detection['box']
|
||||||
|
confidence = detection['confidence']
|
||||||
|
|
||||||
|
# Extract coordinates
|
||||||
|
x1, y1, x2, y2 = map(int, box)
|
||||||
|
|
||||||
|
# Draw bounding box
|
||||||
|
cv2.rectangle(annotated, (x1, y1), (x2, y2), (0, 255, 0), 2)
|
||||||
|
|
||||||
|
# Draw confidence score
|
||||||
|
label = f"Memory: {confidence:.2f}"
|
||||||
|
cv2.putText(
|
||||||
|
annotated, label, (x1, y1 - 10),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1
|
||||||
|
)
|
||||||
|
|
||||||
|
return annotated
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""Custom exceptions for memory detection."""
|
||||||
|
|
||||||
|
|
||||||
|
class DetectionError(Exception):
|
||||||
|
"""Raised when detection fails."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ModelLoadError(Exception):
|
||||||
|
"""Raised when model loading fails."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ImageProcessingError(Exception):
|
||||||
|
"""Raised when image processing fails."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FileUploadError(Exception):
|
||||||
|
"""Raised when file upload fails."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationError(Exception):
|
||||||
|
"""Raised when validation fails."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class VideoProcessingError(Exception):
|
||||||
|
"""Raised when video processing fails."""
|
||||||
|
pass
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
from app import app
|
|
||||||
from config import Config
|
|
||||||
from training import TrainingDataManager
|
|
||||||
|
|
||||||
# Ensuring the backend directory is in the system path
|
|
||||||
backend_dir = Path(__file__).parent
|
|
||||||
sys.path.insert(0, str(backend_dir))
|
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
logging.basicConfig(
|
|
||||||
level=getattr(logging, Config.LOG_LEVEL),
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def setup_environment():
|
|
||||||
"""Set up the application environment."""
|
|
||||||
try:
|
|
||||||
# Validate and create required directories
|
|
||||||
Config.validate_paths()
|
|
||||||
|
|
||||||
# Initialize training data manager
|
|
||||||
training_manager = TrainingDataManager()
|
|
||||||
|
|
||||||
# Log training data statistics
|
|
||||||
stats = training_manager.get_training_statistics()
|
|
||||||
logger.info(f"Training data loaded: {stats}")
|
|
||||||
|
|
||||||
# Validate training data
|
|
||||||
validation = training_manager.validate_training_data()
|
|
||||||
logger.info(f"Training data validation: {validation}")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error setting up environment: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main application entry point."""
|
|
||||||
logger.info("Starting Memory Module Detection API")
|
|
||||||
|
|
||||||
# Set up environment
|
|
||||||
if not setup_environment():
|
|
||||||
logger.error("Failed to set up environment")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Display configuration
|
|
||||||
logger.info(f"Server configuration:")
|
|
||||||
logger.info(f" - Host: {Config.HOST}")
|
|
||||||
logger.info(f" - Port: {Config.PORT}")
|
|
||||||
logger.info(f" - Debug: {Config.DEBUG}")
|
|
||||||
logger.info(f" - Algorithm: {Config.ALGORITHM}")
|
|
||||||
logger.info(f" - Max file size: {Config.MAX_CONTENT_LENGTH} bytes")
|
|
||||||
|
|
||||||
# Start the Flask application
|
|
||||||
try:
|
|
||||||
app.run(
|
|
||||||
host=Config.HOST,
|
|
||||||
port=Config.PORT,
|
|
||||||
debug=Config.DEBUG,
|
|
||||||
threaded=True
|
|
||||||
)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
logger.info("Application stopped by user")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Application error: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Memory Module Detection</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
background: #f9f9f9;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
.section h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background: #45a049;
|
||||||
|
}
|
||||||
|
input[type="file"] {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.result-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.detection-image {
|
||||||
|
max-width: 100%;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.detection-info {
|
||||||
|
background: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: #d32f2f;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.success {
|
||||||
|
color: #388e3c;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
table, th, td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Memory Module Detection</h1>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>Health Check</h2>
|
||||||
|
<button id="healthCheckBtn">Check API Status</button>
|
||||||
|
<div id="healthStatus"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>Image Detection</h2>
|
||||||
|
<input type="file" id="imageUpload" accept=".jpg,.jpeg,.png">
|
||||||
|
<button id="uploadBtn">Detect Memory Modules</button>
|
||||||
|
<div class="result-container" id="imageResult"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>Video Detection</h2>
|
||||||
|
<input type="file" id="videoUpload" accept=".mp4,.avi,.mov,.mkv">
|
||||||
|
<div class="video-params">
|
||||||
|
<label for="fpsInput">Frames per second:</label>
|
||||||
|
<input type="number" id="fpsInput" min="1" max="30" value="1">
|
||||||
|
|
||||||
|
<label for="maxFramesInput">Max frames:</label>
|
||||||
|
<input type="number" id="maxFramesInput" min="1" max="100" value="50">
|
||||||
|
</div>
|
||||||
|
<button id="videoBtn">Process Video</button>
|
||||||
|
<div class="result-container" id="videoResult"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Health Check
|
||||||
|
document.getElementById('healthCheckBtn').addEventListener('click', async () => {
|
||||||
|
const statusDiv = document.getElementById('healthStatus');
|
||||||
|
statusDiv.innerHTML = 'Checking...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("{{ url_for('health_check') }}");
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
statusDiv.innerHTML = `
|
||||||
|
<div class="success">
|
||||||
|
<p><strong>Status:</strong> ${data.status}</p>
|
||||||
|
<p><strong>Service:</strong> ${data.service}</p>
|
||||||
|
<p><strong>Model Loaded:</strong> ${data.model_loaded ? 'Yes' : 'No'}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
statusDiv.innerHTML = `<div class="error">API Error: ${data.error || 'Unknown error'}</div>`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
statusDiv.innerHTML = `<div class="error">Failed to connect to API: ${error.message}</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Image upload detection
|
||||||
|
document.getElementById('uploadBtn').addEventListener('click', async () => {
|
||||||
|
const fileInput = document.getElementById('imageUpload');
|
||||||
|
const resultDiv = document.getElementById('imageResult');
|
||||||
|
|
||||||
|
if (!fileInput.files || fileInput.files.length === 0) {
|
||||||
|
resultDiv.innerHTML = '<div class="error">Please select an image file</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
resultDiv.innerHTML = 'Processing...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('image', file);
|
||||||
|
|
||||||
|
const response = await fetch("{{ url_for('detect_memory') }}", {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && data.success) {
|
||||||
|
displayDetectionResult(resultDiv, data.data);
|
||||||
|
} else {
|
||||||
|
resultDiv.innerHTML = `<div class="error">${data.error || 'Detection failed'}</div>`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
resultDiv.innerHTML = `<div class="error">Error: ${error.message}</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Video processing
|
||||||
|
document.getElementById('videoBtn').addEventListener('click', async () => {
|
||||||
|
const fileInput = document.getElementById('videoUpload');
|
||||||
|
const resultDiv = document.getElementById('videoResult');
|
||||||
|
const fps = document.getElementById('fpsInput').value;
|
||||||
|
const maxFrames = document.getElementById('maxFramesInput').value;
|
||||||
|
|
||||||
|
if (!fileInput.files || fileInput.files.length === 0) {
|
||||||
|
resultDiv.innerHTML = '<div class="error">Please select a video file</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
resultDiv.innerHTML = 'Processing... (This may take a while)';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('video', file);
|
||||||
|
formData.append('fps', fps);
|
||||||
|
formData.append('max_frames', maxFrames);
|
||||||
|
|
||||||
|
const response = await fetch("{{ url_for('detect_video') }}", {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && data.success) {
|
||||||
|
displayVideoResult(resultDiv, data.data);
|
||||||
|
} else {
|
||||||
|
resultDiv.innerHTML = `<div class="error">${data.error || 'Video processing failed'}</div>`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
resultDiv.innerHTML = `<div class="error">Error: ${error.message}</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper function to display detection results
|
||||||
|
function displayDetectionResult(container, data) {
|
||||||
|
const detections = data.detections || [];
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="success">
|
||||||
|
<p>Detected ${data.detection_count} memory modules</p>
|
||||||
|
<img class="detection-image" src="{{ url_for('get_result_image', filename='') }}${data.result_image_url.split('/').pop()}" alt="Detection result">
|
||||||
|
</div>
|
||||||
|
<div class="detection-info">
|
||||||
|
<h4>Detection Details:</h4>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (detections.length > 0) {
|
||||||
|
html += '<table><tr><th>Class</th><th>Confidence</th><th>Position (x1,y1,x2,y2)</th></tr>';
|
||||||
|
|
||||||
|
detections.forEach(det => {
|
||||||
|
const className = 'Memory Module';
|
||||||
|
const confidence = (det.confidence * 100).toFixed(2);
|
||||||
|
const [x1, y1, x2, y2] = det.box.map(coord => coord.toFixed(1));
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<tr>
|
||||||
|
<td>${className}</td>
|
||||||
|
<td>${confidence}%</td>
|
||||||
|
<td>${x1}, ${y1}, ${x2}, ${y2}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</table>';
|
||||||
|
} else {
|
||||||
|
html += '<p>No memory modules detected</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to display video results
|
||||||
|
function displayVideoResult(container, data) {
|
||||||
|
const detections = data.detections || [];
|
||||||
|
const summary = data.summary || {};
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="success">
|
||||||
|
<h3>Video Processing Results</h3>
|
||||||
|
<p><strong>Video:</strong> ${data.video_filename}</p>
|
||||||
|
<p><strong>Frames Processed:</strong> ${data.processing_info.frames_processed}</p>
|
||||||
|
<p><strong>Processing Time:</strong> ${data.processing_info.processing_time.toFixed(2)} seconds</p>
|
||||||
|
<p><strong>Average FPS:</strong> ${data.processing_info.average_fps.toFixed(2)}</p>
|
||||||
|
|
||||||
|
<h4>Summary:</h4>
|
||||||
|
<p>Total Detections: ${summary.total_detections || 0}</p>
|
||||||
|
<p>Average Confidence: ${summary.average_confidence ? (summary.average_confidence * 100).toFixed(2) + '%' : 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detection-info">
|
||||||
|
<h4>Frame-by-Frame Detections:</h4>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (detections.length > 0) {
|
||||||
|
html += '<table><tr><th>Frame</th><th>Time (s)</th><th>Detections</th><th>Details</th></tr>';
|
||||||
|
|
||||||
|
detections.forEach(frame => {
|
||||||
|
html += `
|
||||||
|
<tr>
|
||||||
|
<td>${frame.frame_number}</td>
|
||||||
|
<td>${frame.timestamp.toFixed(2)}</td>
|
||||||
|
<td>${frame.detection_count}</td>
|
||||||
|
<td>
|
||||||
|
${frame.detections.map(d => `
|
||||||
|
Memory (${(d.confidence * 100).toFixed(2)}%) at ${d.box.map(coord => coord.toFixed(1)).join(',')}
|
||||||
|
`).join('; ')}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</table>';
|
||||||
|
} else {
|
||||||
|
html += '<p>No memory modules detected in any frames</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<h2>Memory Module Detection</h2>
|
|
||||||
<form action="/detect" method="post" enctype="multipart/form-data">
|
|
||||||
<input type="file" name="image" accept="image/*">
|
|
||||||
<input type="submit" value="Upload">
|
|
||||||
</form>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def allowed_file(filename, allowed_extensions):
|
||||||
|
"""Check if file extension is allowed."""
|
||||||
|
return '.' in filename and \
|
||||||
|
filename.rsplit('.', 1)[1].lower() in allowed_extensions
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging():
|
||||||
|
"""Setup simple logging configuration."""
|
||||||
|
# Create logs directory
|
||||||
|
log_dir = Path('logs')
|
||||||
|
log_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler('logs/app.log'),
|
||||||
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set levels for noisy libraries
|
||||||
|
logging.getLogger('werkzeug').setLevel(logging.WARNING)
|
||||||
|
logging.getLogger('ultralytics').setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_image_size(file_size, max_size):
|
||||||
|
"""Validate image file size."""
|
||||||
|
return file_size <= max_size
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_extension(filename):
|
||||||
|
"""Get file extension from filename."""
|
||||||
|
return filename.rsplit('.', 1)[1].lower() if '.' in filename else ''
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
"""Simple video processing for memory module detection."""
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
from exceptions import VideoProcessingError
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class VideoProcessor:
|
||||||
|
"""Simple video processor for memory module detection."""
|
||||||
|
|
||||||
|
def __init__(self, detector):
|
||||||
|
self.detector = detector
|
||||||
|
|
||||||
|
def process_video(self, video_path: str, fps: int = 1, max_frames: int = 100) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Process video file and detect memory modules in frames.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
video_path: Path to video file
|
||||||
|
fps: Frames per second to process (1 = every second)
|
||||||
|
max_frames: Maximum frames to process
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with detection results
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not Path(video_path).exists():
|
||||||
|
raise FileNotFoundError(f"Video file not found: {video_path}")
|
||||||
|
|
||||||
|
logger.info(f"Processing video: {video_path}")
|
||||||
|
|
||||||
|
# Open video
|
||||||
|
cap = cv2.VideoCapture(video_path)
|
||||||
|
if not cap.isOpened():
|
||||||
|
raise VideoProcessingError("Could not open video file")
|
||||||
|
|
||||||
|
# Get video properties
|
||||||
|
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||||
|
video_fps = cap.get(cv2.CAP_PROP_FPS)
|
||||||
|
duration = total_frames / video_fps if video_fps > 0 else 0
|
||||||
|
|
||||||
|
# Calculate frame interval
|
||||||
|
frame_interval = max(1, int(video_fps / fps)) if video_fps > 0 else 1
|
||||||
|
|
||||||
|
logger.info(f"Video properties: {total_frames} frames, {video_fps:.2f}fps, {duration:.2f}s")
|
||||||
|
|
||||||
|
# Process frames
|
||||||
|
detections = []
|
||||||
|
frame_count = 0
|
||||||
|
processed_frames = 0
|
||||||
|
|
||||||
|
while cap.isOpened() and processed_frames < max_frames:
|
||||||
|
ret, frame = cap.read()
|
||||||
|
if not ret:
|
||||||
|
break
|
||||||
|
|
||||||
|
if frame_count % frame_interval == 0:
|
||||||
|
try:
|
||||||
|
# Get timestamp
|
||||||
|
timestamp = frame_count / video_fps if video_fps > 0 else frame_count
|
||||||
|
|
||||||
|
# Detect memory modules in frame
|
||||||
|
result = self.detector._perform_detection(frame)
|
||||||
|
|
||||||
|
# Store frame detection
|
||||||
|
frame_detection = {
|
||||||
|
'frame_number': frame_count,
|
||||||
|
'timestamp': round(timestamp, 2),
|
||||||
|
'detections': result['detections'],
|
||||||
|
'detection_count': result['detection_count']
|
||||||
|
}
|
||||||
|
detections.append(frame_detection)
|
||||||
|
processed_frames += 1
|
||||||
|
|
||||||
|
logger.info(f"Frame {frame_count}: {result['detection_count']} detections")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to process frame {frame_count}: {e}")
|
||||||
|
|
||||||
|
frame_count += 1
|
||||||
|
|
||||||
|
cap.release()
|
||||||
|
|
||||||
|
# Generate summary
|
||||||
|
total_detections = sum(d['detection_count'] for d in detections)
|
||||||
|
avg_detections = total_detections / len(detections) if detections else 0
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'video_info': {
|
||||||
|
'path': video_path,
|
||||||
|
'total_frames': total_frames,
|
||||||
|
'fps': video_fps,
|
||||||
|
'duration': duration
|
||||||
|
},
|
||||||
|
'processing_info': {
|
||||||
|
'frames_processed': processed_frames,
|
||||||
|
'frame_interval': frame_interval,
|
||||||
|
'target_fps': fps
|
||||||
|
},
|
||||||
|
'detections': detections,
|
||||||
|
'summary': {
|
||||||
|
'total_detections': total_detections,
|
||||||
|
'avg_detections_per_frame': round(avg_detections, 2),
|
||||||
|
'frames_with_detections': len([d for d in detections if d['detection_count'] > 0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"Video processing completed: {processed_frames} frames, {total_detections} total detections")
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Video processing failed: {e}")
|
||||||
|
raise VideoProcessingError(f"Video processing failed: {str(e)}")
|
||||||
|
|
||||||
|
def create_annotated_video(self, video_path: str, output_path: str, fps: int = 1) -> str:
|
||||||
|
"""
|
||||||
|
Create annotated video with bounding boxes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
video_path: Input video path
|
||||||
|
output_path: Output video path
|
||||||
|
fps: Processing fps
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to annotated video
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info(f"Creating annotated video: {video_path} -> {output_path}")
|
||||||
|
|
||||||
|
# Open input video
|
||||||
|
cap = cv2.VideoCapture(video_path)
|
||||||
|
if not cap.isOpened():
|
||||||
|
raise VideoProcessingError("Could not open input video")
|
||||||
|
|
||||||
|
# Get video properties
|
||||||
|
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||||
|
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||||
|
video_fps = cap.get(cv2.CAP_PROP_FPS)
|
||||||
|
|
||||||
|
# Create output video writer
|
||||||
|
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
||||||
|
out = cv2.VideoWriter(output_path, fourcc, video_fps, (width, height))
|
||||||
|
|
||||||
|
frame_interval = max(1, int(video_fps / fps)) if video_fps > 0 else 1
|
||||||
|
frame_count = 0
|
||||||
|
|
||||||
|
while cap.isOpened():
|
||||||
|
ret, frame = cap.read()
|
||||||
|
if not ret:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Process every nth frame for detection
|
||||||
|
if frame_count % frame_interval == 0:
|
||||||
|
try:
|
||||||
|
result = self.detector._perform_detection(frame)
|
||||||
|
annotated_frame = result['annotated_image']
|
||||||
|
out.write(annotated_frame)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to annotate frame {frame_count}: {e}")
|
||||||
|
out.write(frame) # Write original frame if annotation fails
|
||||||
|
else:
|
||||||
|
out.write(frame) # Write original frame
|
||||||
|
|
||||||
|
frame_count += 1
|
||||||
|
|
||||||
|
cap.release()
|
||||||
|
out.release()
|
||||||
|
|
||||||
|
logger.info(f"Annotated video created: {output_path}")
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Annotated video creation failed: {e}")
|
||||||
|
raise VideoProcessingError(f"Annotated video creation failed: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def extract_frames(video_path: str, output_dir: str, fps: int = 1) -> List[str]:
|
||||||
|
"""
|
||||||
|
Extract frames from video for individual processing.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
video_path: Path to video file
|
||||||
|
output_dir: Directory to save frames
|
||||||
|
fps: Frames per second to extract
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of extracted frame paths
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
output_path = Path(output_dir)
|
||||||
|
output_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
cap = cv2.VideoCapture(video_path)
|
||||||
|
video_fps = cap.get(cv2.CAP_PROP_FPS)
|
||||||
|
frame_interval = max(1, int(video_fps / fps))
|
||||||
|
|
||||||
|
frame_paths = []
|
||||||
|
frame_count = 0
|
||||||
|
saved_count = 0
|
||||||
|
|
||||||
|
while cap.isOpened():
|
||||||
|
ret, frame = cap.read()
|
||||||
|
if not ret:
|
||||||
|
break
|
||||||
|
|
||||||
|
if frame_count % frame_interval == 0:
|
||||||
|
frame_filename = f"frame_{saved_count:04d}.jpg"
|
||||||
|
frame_path = output_path / frame_filename
|
||||||
|
cv2.imwrite(str(frame_path), frame)
|
||||||
|
frame_paths.append(str(frame_path))
|
||||||
|
saved_count += 1
|
||||||
|
|
||||||
|
frame_count += 1
|
||||||
|
|
||||||
|
cap.release()
|
||||||
|
logger.info(f"Extracted {len(frame_paths)} frames to {output_dir}")
|
||||||
|
return frame_paths
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Frame extraction failed: {e}")
|
||||||
|
raise VideoProcessingError(f"Frame extraction failed: {str(e)}")
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import cv2
|
|
||||||
|
|
||||||
|
|
||||||
def draw_boxes(image, boxes):
|
|
||||||
"""
|
|
||||||
Draw bounding boxes on image
|
|
||||||
:param image: Input image
|
|
||||||
:param boxes: List of bounding boxes in format [(x1, y1, x2, y2), ...]
|
|
||||||
:return: Image with boxes drawn
|
|
||||||
"""
|
|
||||||
for box in boxes:
|
|
||||||
x1, y1, x2, y2 = box
|
|
||||||
cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2)
|
|
||||||
return image
|
|
||||||
|
|
||||||
|
|
||||||
def allowed_file(filename, allowed_extensions):
|
|
||||||
return '.' in filename and \
|
|
||||||
filename.rsplit('.', 1)[1].lower() in allowed_extensions
|
|
||||||
+131
-64
@@ -1,75 +1,142 @@
|
|||||||
# Memory Module Detection API Documentation
|
# Memory Module Detection System
|
||||||
|
|
||||||
## Overview
|
A computer vision system for detecting memory modules in images and videos using YOLO object detection.
|
||||||
Flask API for detecting memory modules on motherboard images using YOLOv8. Processes uploaded images and returns bounding box coordinates with confidence scores.
|
|
||||||
|
|
||||||
## Base URL
|
## Features
|
||||||
`http://localhost:5000`
|
|
||||||
|
|
||||||
## Endpoints
|
- **Image Detection**: Upload images to detect memory modules
|
||||||
|
- **Video Processing**: Process video files frame-by-frame
|
||||||
|
- **Bounding Box Visualization**: See detected objects with confidence scores
|
||||||
|
- **Detailed Analytics**: Get detection statistics and metrics
|
||||||
|
- **REST API**: Fully documented API endpoints
|
||||||
|
- **Web Interface**: User-friendly browser interface
|
||||||
|
|
||||||
### 1. Root Endpoint
|
## Requirements
|
||||||
**GET** `/`
|
|
||||||
- Returns the test interface HTML page
|
|
||||||
- Response: `test.html`
|
|
||||||
|
|
||||||
### 2. Image Detection
|
- Python 3.8+
|
||||||
**POST** `/detect`
|
- CUDA-enabled GPU (recommended for best performance)
|
||||||
- Accepts image uploads for processing
|
- Docker (optional)
|
||||||
- **Request:**
|
|
||||||
```bash
|
|
||||||
curl -X POST -F "image=@motherboard.jpg" http://localhost:5000/detect
|
|
||||||
```
|
|
||||||
- **Successful Response (200):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"detections": [
|
|
||||||
{
|
|
||||||
"box": [x1,y1,x2,y2],
|
|
||||||
"confidence": 0.95,
|
|
||||||
"class": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"result_image": "/results/filename.jpg"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **Error Responses:**
|
|
||||||
- `400 Bad Request`: Missing/invalid image file
|
|
||||||
- `500 Server Error`: Processing failure
|
|
||||||
|
|
||||||
### 3. Result Retrieval
|
## Installation
|
||||||
**GET** `/results/<filename>`
|
|
||||||
- Returns annotated image with bounding boxes
|
|
||||||
- Example: `http://localhost:5000/results/out1.jpg`
|
|
||||||
|
|
||||||
## Request/Response Examples
|
### 1. Clone the repository
|
||||||
**Sample Request:**
|
```bash
|
||||||
```python
|
git clone https://github.com/yourusername/memory-detection.git
|
||||||
import requests
|
cd memory-detection
|
||||||
response = requests.post(
|
|
||||||
'http://localhost:5000/detect',
|
|
||||||
files={'image': open('motherboard.jpg', 'rb')}
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Sample Response:**
|
### 2. Set up Python environment
|
||||||
```json
|
```bash
|
||||||
{
|
python -m venv venv
|
||||||
"detections": [
|
source venv/bin/activate # Linux/MacOS
|
||||||
{
|
venv\Scripts\activate # Windows
|
||||||
"box": [541,567,661,265],
|
|
||||||
"confidence": 0.98,
|
|
||||||
"class": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"result_image": "/results/out1.jpg"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Technical Specifications
|
### 3. Install dependencies
|
||||||
| Parameter | Value |
|
```bash
|
||||||
|--------------------|---------------------------|
|
pip install -r requirements.txt
|
||||||
| Model | YOLOv8n (custom-trained) |
|
```
|
||||||
| Input Formats | JPG/PNG |
|
|
||||||
| Recommended Resolution | 416px |
|
### 4. Download YOLO model
|
||||||
| Processing Time (CPU) | 200-500ms per image |
|
Place your `.pt` model file in the `models/` directory and update `config.py` with the correct path.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Edit `config.py` to customize:
|
||||||
|
|
||||||
|
- Model paths
|
||||||
|
- Confidence thresholds
|
||||||
|
- File size limits
|
||||||
|
- Server host/port settings
|
||||||
|
|
||||||
|
## Running the Application
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
```bash
|
||||||
|
python backend/app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Mode (with Gunicorn)
|
||||||
|
```bash
|
||||||
|
gunicorn --bind 0.0.0.0:5000 backend.app:app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
```bash
|
||||||
|
docker build -t memory-detection .
|
||||||
|
docker run -p 5000:5000 memory-detection
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
### Endpoints
|
||||||
|
|
||||||
|
| Endpoint | Method | Description |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| `/` | GET | Web interface |
|
||||||
|
| `/api/health` | GET | Service health check |
|
||||||
|
| `/api/v1/detect` | POST | Detect memory modules in image |
|
||||||
|
| `/api/v1/detect/video` | POST | Process video for detections |
|
||||||
|
| `/api/v1/results/<filename>` | GET | Retrieve result images |
|
||||||
|
|
||||||
|
### Example Requests
|
||||||
|
|
||||||
|
**Image Detection**:
|
||||||
|
```bash
|
||||||
|
curl -X POST -F "image=@test.jpg" http://localhost:5000/api/v1/detect
|
||||||
|
```
|
||||||
|
|
||||||
|
**Video Processing**:
|
||||||
|
```bash
|
||||||
|
curl -X POST -F "video=@test.mp4" -F "fps=2" -F "max_frames=100" http://localhost:5000/api/v1/detect/video
|
||||||
|
```
|
||||||
|
|
||||||
|
## Web Interface
|
||||||
|
|
||||||
|
Access the web interface at `http://localhost:5000`:
|
||||||
|
|
||||||
|
1. **Upload Images**:
|
||||||
|
- Supported formats: JPG, PNG
|
||||||
|
- Max file size: 10MB (configurable)
|
||||||
|
|
||||||
|
2. **Process Videos**:
|
||||||
|
- Supported formats: MP4, AVI, MOV
|
||||||
|
- Adjustable FPS and frame limits
|
||||||
|
- Detailed frame-by-frame results
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
memory-detection/
|
||||||
|
├── backend/ # Core application
|
||||||
|
│ ├── app.py # Flask application
|
||||||
|
│ ├── config.py # Configuration
|
||||||
|
│ ├── detector.py # YOLO detection logic
|
||||||
|
│ ├── exceptions.py # Custom exceptions
|
||||||
|
│ ├── utils.py # Helper functions
|
||||||
|
│ ├── video_processor.py # Video handling
|
||||||
|
│ └── templates/ # Frontend templates
|
||||||
|
│ └── index.html # Web interface
|
||||||
|
├── models/ # YOLO model files
|
||||||
|
├── uploads/ # Temporary upload storage
|
||||||
|
├── results/ # Detection result images
|
||||||
|
├── tests/ # Unit tests
|
||||||
|
├── requirements.txt # Python dependencies
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Common Issues**:
|
||||||
|
|
||||||
|
1. **Model not loading**:
|
||||||
|
- Verify model path in `config.py`
|
||||||
|
- Check file permissions
|
||||||
|
|
||||||
|
2. **CUDA errors**:
|
||||||
|
- Ensure compatible CUDA version is installed
|
||||||
|
- Try CPU-only mode by modifying detector.py
|
||||||
|
|
||||||
|
3. **File upload issues**:
|
||||||
|
- Check `MAX_FILE_SIZE` in config
|
||||||
|
- Verify file extensions are allowed
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ def train_model():
|
|||||||
patience=20, # Early stopping if no improvement
|
patience=20, # Early stopping if no improvement
|
||||||
lr0=0.001, # Learning rate
|
lr0=0.001, # Learning rate
|
||||||
cos_lr=True, # Cosine learning rate scheduler
|
cos_lr=True, # Cosine learning rate scheduler
|
||||||
workers=1, # Reduce if memory errors
|
workers=1, # Reduce memory errors
|
||||||
cache=False, # Disable cache if low on disk space
|
cache=False, # Disable cache if low on disk space
|
||||||
single_cls=True,
|
single_cls=True,
|
||||||
optimizer='AdamW', # For small datasets
|
optimizer='AdamW', # For small datasets
|
||||||
Reference in New Issue
Block a user