By the end of this module, you will be able to:
- Build a complete distance measurement application
- Implement real-time object detection and measurement
- Create a user-friendly interface
- Handle edge cases and error conditions
- Apply all concepts learned in Level 1
Goal: Create a RealSense-powered distance measurement tool that can:
- Measure distances to objects in real-time
- Display measurements on screen
- Save measurement data
- Provide a simple user interface
- Real-time distance measurement
- Visual feedback with crosshair
- Distance display in millimeters and inches
- Save measurements to file
- Keyboard controls for interaction
- Use pyrealsense2 for camera access
- Use OpenCV for image display
- Handle camera connection errors
- Provide clear user instructions
- Code should be well-commented
distance_measurement/
├── main.py # Main application
├── camera_handler.py # Camera management
├── measurement.py # Distance calculation
├── ui.py # User interface
├── data/
│ └── measurements.csv # Saved measurements
└── README.md # Project documentation
Create camera_handler.py:
import pyrealsense2 as rs
import numpy as np
import cv2
class RealSenseCamera:
"""RealSense camera handler class"""
def __init__(self, width=640, height=480, fps=30):
self.width = width
self.height = height
self.fps = fps
self.pipeline = None
self.config = None
self.is_streaming = False
def initialize(self):
"""Initialize the camera"""
try:
# Create pipeline
self.pipeline = rs.pipeline()
self.config = rs.config()
# Configure streams
self.config.enable_stream(
rs.stream.depth,
self.width,
self.height,
rs.format.z16,
self.fps
)
self.config.enable_stream(
rs.stream.color,
self.width,
self.height,
rs.format.bgr8,
self.fps
)
# Start streaming
self.pipeline.start(self.config)
self.is_streaming = True
print("✅ Camera initialized successfully")
return True
except Exception as e:
print(f"❌ Camera initialization failed: {e}")
return False
def get_frames(self):
"""Get current frames from camera"""
if not self.is_streaming:
return None, None
try:
# Wait for frames
frames = self.pipeline.wait_for_frames()
# Get individual frames
depth_frame = frames.get_depth_frame()
color_frame = frames.get_color_frame()
if depth_frame and color_frame:
# Convert to numpy arrays
depth_image = np.asanyarray(depth_frame.get_data())
color_image = np.asanyarray(color_frame.get_data())
return depth_image, color_image
else:
return None, None
except Exception as e:
print(f"❌ Error getting frames: {e}")
return None, None
def stop(self):
"""Stop camera streaming"""
if self.pipeline and self.is_streaming:
self.pipeline.stop()
self.is_streaming = False
print("✅ Camera stopped")
def get_camera_info(self):
"""Get camera information"""
if not self.is_streaming:
return None
try:
# Get device info
device = self.pipeline.get_active_profile().get_device()
return {
'name': device.get_info(rs.camera_info.name),
'serial': device.get_info(rs.camera_info.serial_number),
'firmware': device.get_info(rs.camera_info.firmware_version)
}
except Exception as e:
print(f"❌ Error getting camera info: {e}")
return NoneCreate measurement.py:
import numpy as np
import cv2
import csv
import datetime
from typing import Tuple, Optional
class DistanceMeasurer:
"""Distance measurement and data handling"""
def __init__(self, data_file='data/measurements.csv'):
self.data_file = data_file
self.measurements = []
def measure_distance(self, depth_image: np.ndarray, x: int, y: int) -> Optional[float]:
"""Measure distance at specific pixel coordinates"""
try:
# Check bounds
if x < 0 or x >= depth_image.shape[1] or y < 0 or y >= depth_image.shape[0]:
return None
# Get depth value
depth_value = depth_image[y, x]
# Check if depth is valid (not zero)
if depth_value == 0:
return None
# Convert to millimeters (assuming depth units are in mm)
distance_mm = depth_value
return distance_mm
except Exception as e:
print(f"❌ Error measuring distance: {e}")
return None
def mm_to_inches(self, mm: float) -> float:
"""Convert millimeters to inches"""
return mm / 25.4
def get_measurement_info(self, depth_image: np.ndarray, x: int, y: int) -> dict:
"""Get comprehensive measurement information"""
distance_mm = self.measure_distance(depth_image, x, y)
if distance_mm is None:
return {
'distance_mm': None,
'distance_inches': None,
'valid': False,
'message': 'No valid depth data'
}
distance_inches = self.mm_to_inches(distance_mm)
return {
'distance_mm': distance_mm,
'distance_inches': distance_inches,
'valid': True,
'message': f'{distance_mm:.1f}mm ({distance_inches:.2f}")'
}
def save_measurement(self, x: int, y: int, distance_mm: float, timestamp: str = None):
"""Save measurement to CSV file"""
if timestamp is None:
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
measurement = {
'timestamp': timestamp,
'x': x,
'y': y,
'distance_mm': distance_mm,
'distance_inches': self.mm_to_inches(distance_mm)
}
self.measurements.append(measurement)
# Save to CSV
try:
with open(self.data_file, 'a', newline='') as csvfile:
fieldnames = ['timestamp', 'x', 'y', 'distance_mm', 'distance_inches']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
# Write header if file is empty
if csvfile.tell() == 0:
writer.writeheader()
writer.writerow(measurement)
print(f"✅ Measurement saved: {measurement['message']}")
except Exception as e:
print(f"❌ Error saving measurement: {e}")
def load_measurements(self) -> list:
"""Load measurements from CSV file"""
try:
with open(self.data_file, 'r') as csvfile:
reader = csv.DictReader(csvfile)
return list(reader)
except FileNotFoundError:
print(f"📁 No existing measurements file found")
return []
except Exception as e:
print(f"❌ Error loading measurements: {e}")
return []Create ui.py:
import cv2
import numpy as np
from typing import Tuple, Optional
class MeasurementUI:
"""User interface for distance measurement"""
def __init__(self, window_name="RealSense Distance Measurement"):
self.window_name = window_name
self.crosshair_size = 20
self.crosshair_thickness = 2
self.crosshair_color = (0, 255, 0) # Green
self.text_color = (255, 255, 255) # White
self.text_bg_color = (0, 0, 0) # Black
def draw_crosshair(self, image: np.ndarray, x: int, y: int) -> np.ndarray:
"""Draw crosshair at specified coordinates"""
# Draw horizontal line
cv2.line(image,
(x - self.crosshair_size, y),
(x + self.crosshair_size, y),
self.crosshair_color,
self.crosshair_thickness)
# Draw vertical line
cv2.line(image,
(x, y - self.crosshair_size),
(x, y + self.crosshair_size),
self.crosshair_color,
self.crosshair_thickness)
return image
def draw_text_with_background(self, image: np.ndarray, text: str, position: Tuple[int, int]) -> np.ndarray:
"""Draw text with background for better visibility"""
x, y = position
# Get text size
font = cv2.FONT_HERSHEY_SIMPLEX
font_scale = 0.7
thickness = 2
(text_width, text_height), baseline = cv2.getTextSize(text, font, font_scale, thickness)
# Draw background rectangle
cv2.rectangle(image,
(x, y - text_height - baseline),
(x + text_width, y + baseline),
self.text_bg_color,
-1)
# Draw text
cv2.putText(image, text, (x, y), font, font_scale, self.text_color, thickness)
return image
def draw_measurement_info(self, image: np.ndarray, measurement_info: dict, x: int, y: int) -> np.ndarray:
"""Draw measurement information on image"""
if not measurement_info['valid']:
text = measurement_info['message']
self.draw_text_with_background(image, text, (10, 30))
return image
# Draw distance information
distance_text = f"Distance: {measurement_info['message']}"
self.draw_text_with_background(image, distance_text, (10, 30))
# Draw coordinates
coord_text = f"Position: ({x}, {y})"
self.draw_text_with_background(image, coord_text, (10, 60))
# Draw instructions
instructions = [
"Controls:",
"Mouse: Move crosshair",
"Click: Measure distance",
"S: Save measurement",
"Q: Quit"
]
for i, instruction in enumerate(instructions):
self.draw_text_with_background(image, instruction, (10, 90 + i * 25))
return image
def create_depth_colormap(self, depth_image: np.ndarray) -> np.ndarray:
"""Create colored depth map for visualization"""
# Normalize depth image
depth_normalized = cv2.convertScaleAbs(depth_image, alpha=0.03)
# Apply colormap
depth_colormap = cv2.applyColorMap(depth_normalized, cv2.COLORMAP_JET)
return depth_colormap
def display_images(self, color_image: np.ndarray, depth_image: np.ndarray,
measurement_info: dict, x: int, y: int) -> bool:
"""Display images with measurement information"""
# Create depth colormap
depth_colormap = self.create_depth_colormap(depth_image)
# Draw crosshair on both images
color_with_crosshair = self.draw_crosshair(color_image.copy(), x, y)
depth_with_crosshair = self.draw_crosshair(depth_colormap.copy(), x, y)
# Draw measurement info
color_with_info = self.draw_measurement_info(color_with_crosshair, measurement_info, x, y)
depth_with_info = self.draw_measurement_info(depth_with_crosshair, measurement_info, x, y)
# Display images
cv2.imshow('Color Stream', color_with_info)
cv2.imshow('Depth Stream', depth_with_info)
# Check for key press
key = cv2.waitKey(1) & 0xFF
return key == ord('q') # Return True if 'q' is pressedCreate main.py:
import cv2
import os
from camera_handler import RealSenseCamera
from measurement import DistanceMeasurer
from ui import MeasurementUI
def main():
"""Main application function"""
# Create data directory if it doesn't exist
os.makedirs('data', exist_ok=True)
# Initialize components
camera = RealSenseCamera()
measurer = DistanceMeasurer()
ui = MeasurementUI()
# Initialize camera
if not camera.initialize():
print("❌ Failed to initialize camera. Exiting.")
return
# Get camera info
camera_info = camera.get_camera_info()
if camera_info:
print(f"📷 Camera: {camera_info['name']}")
print(f"🔢 Serial: {camera_info['serial']}")
print(f"⚙️ Firmware: {camera_info['firmware']}")
# Load existing measurements
existing_measurements = measurer.load_measurements()
print(f"📊 Loaded {len(existing_measurements)} existing measurements")
# Mouse callback for crosshair movement
crosshair_x, crosshair_y = 320, 240 # Center of 640x480 image
def mouse_callback(event, x, y, flags, param):
nonlocal crosshair_x, crosshair_y
if event == cv2.EVENT_MOUSEMOVE:
crosshair_x, crosshair_y = x, y
# Set mouse callback
cv2.setMouseCallback('Color Stream', mouse_callback)
cv2.setMouseCallback('Depth Stream', mouse_callback)
print("\n🎯 Distance Measurement Tool Started!")
print("Move your mouse to position the crosshair, click to measure, press 's' to save, 'q' to quit")
try:
while True:
# Get frames from camera
depth_image, color_image = camera.get_frames()
if depth_image is None or color_image is None:
print("❌ Failed to get frames from camera")
break
# Get measurement at crosshair position
measurement_info = measurer.get_measurement_info(
depth_image, crosshair_x, crosshair_y
)
# Display images with UI
should_quit = ui.display_images(
color_image, depth_image, measurement_info, crosshair_x, crosshair_y
)
if should_quit:
break
# Handle key presses
key = cv2.waitKey(1) & 0xFF
if key == ord('s') and measurement_info['valid']:
# Save measurement
measurer.save_measurement(
crosshair_x, crosshair_y, measurement_info['distance_mm']
)
elif key == ord('q'):
break
except KeyboardInterrupt:
print("\n⏹️ Application interrupted by user")
except Exception as e:
print(f"❌ Unexpected error: {e}")
finally:
# Cleanup
camera.stop()
cv2.destroyAllWindows()
print("✅ Application closed successfully")
if __name__ == "__main__":
main()Create README.md:
# RealSense Distance Measurement Tool
A Python application for real-time distance measurement using RealSense cameras.
## Features
- Real-time distance measurement
- Visual crosshair for precise targeting
- Distance display in millimeters and inches
- Save measurements to CSV file
- Simple keyboard and mouse controls
## Requirements
- RealSense camera (D405, D415, D435, D455, D457, or D555)
- Python 3.7+
- Required packages: pyrealsense2, opencv-python, numpy
## Installation
```bash
pip install pyrealsense2 opencv-python numpypython main.py- Mouse: Move crosshair to target objects
- Click: Measure distance at crosshair position
- S: Save current measurement to file
- Q: Quit application
Measurements are saved to data/measurements.csv with the following format:
- Timestamp
- X, Y coordinates
- Distance in millimeters
- Distance in inches
- Ensure camera is connected via USB 3.0
- Check that RealSense SDK is properly installed
- Verify camera permissions on Linux/macOS
## 🧪 Testing Your Application
### Test Cases
1. **Basic Functionality**:
- Launch the application
- Verify camera connection
- Test crosshair movement
- Measure distances to various objects
2. **Edge Cases**:
- Test with no objects in view
- Test with reflective surfaces
- Test with very close objects
- Test with very far objects
3. **Data Persistence**:
- Save multiple measurements
- Verify CSV file creation
- Check data format and accuracy
4. **Error Handling**:
- Disconnect camera during operation
- Test with invalid coordinates
- Verify graceful shutdown
### Performance Testing
```python
# Add to main.py for performance testing
import time
def performance_test():
"""Test application performance"""
start_time = time.time()
frame_count = 0
# Run for 10 seconds
while time.time() - start_time < 10:
depth_image, color_image = camera.get_frames()
if depth_image is not None and color_image is not None:
frame_count += 1
fps = frame_count / 10
print(f"Average FPS: {fps:.1f}")
-
Multiple Measurement Points:
- Allow multiple crosshairs
- Measure distances between points
- Calculate object dimensions
-
Object Detection:
- Integrate with OpenCV object detection
- Automatically measure detected objects
- Track objects over time
-
3D Visualization:
- Display 3D point clouds
- Show measurement points in 3D
- Export 3D models
-
Data Analysis:
- Plot measurement trends
- Statistical analysis
- Export to different formats
-
What is the main advantage of using a class-based approach for the camera handler?
- A) Faster execution
- B) Better code organization
- C) Smaller file size
- D) Easier installation
-
Why is error handling important in the measurement application?
- A) To make the code faster
- B) To handle invalid depth data
- C) To reduce memory usage
- D) To improve image quality
-
What format is used for saving measurements?
- A) JSON
- B) XML
- C) CSV
- D) Binary
You've successfully completed Level 1 of RealSense University! You now have:
- A solid understanding of RealSense cameras
- Hands-on experience with the RealSense SDK
- A working distance measurement application
- Skills to build more complex RealSense applications
Ready to advance to the next level? Check out Level 2: Intermediate — Building Vision-Aware Apps to learn about:
- Point cloud processing
- ROS2 integration
- Advanced computer vision applications
- Cross-platform development