dreader-hal/CALIBRATION.md
2025-11-10 18:06:11 +01:00

348 lines
9.5 KiB
Markdown

# Touchscreen Calibration
The DReader HAL includes touchscreen calibration support to accurately align touch coordinates with display pixels. This is essential for precise touch interaction on e-ink devices.
## Why Calibration is Needed
Touchscreen controllers and display controllers are separate components with their own coordinate systems. Without calibration:
- Touch coordinates may not align precisely with display pixels
- Touches may register at offset positions
- The offset may vary across different areas of the screen
- Linear scaling alone may not account for rotation, skew, or non-linear distortion
Calibration solves this by computing an **affine transformation matrix** that maps touch coordinates to display coordinates with high precision.
## Quick Start
### 1. Run Calibration
```bash
cd examples
python3 calibrate_touch.py
```
This will:
1. Display calibration targets (circles) at known positions
2. Wait for you to touch each target
3. Compute the transformation matrix
4. Save calibration data to `~/.config/dreader/touch_calibration.json`
### 2. Use Calibrated Touch
Once calibration is complete, the touch driver automatically loads and applies the calibration:
```python
from dreader_hal.touch.ft5xx6 import FT5xx6TouchDriver
touch = FT5xx6TouchDriver(width=800, height=1200)
await touch.initialize() # Automatically loads calibration
# All touch events are now calibrated
event = await touch.get_touch_event()
print(f"Touch at ({event.x}, {event.y})") # Calibrated coordinates
```
## Calibration Options
### Number of Calibration Points
You can choose between 5-point or 9-point calibration:
```bash
# 5-point calibration (corners + center) - faster
python3 calibrate_touch.py --points 5
# 9-point calibration (3x3 grid) - more accurate (default)
python3 calibrate_touch.py --points 9
```
**Recommendation:** Use 9-point calibration for best accuracy.
### Custom Calibration File Location
```bash
python3 calibrate_touch.py --output /path/to/calibration.json
```
Then specify the same path when initializing the touch driver:
```python
touch = FT5xx6TouchDriver(
calibration_file="/path/to/calibration.json"
)
```
### Display Dimensions
If your display is not the default 800x1200:
```bash
python3 calibrate_touch.py --width 1024 --height 768
```
## Testing Calibration
Use the test script to verify calibration quality:
```bash
python3 test_calibration.py
```
This displays an interactive UI where you can:
- Tap anywhere on the screen
- See calibrated coordinates with crosshairs
- View calibration offset and error
- Verify calibration quality
## Calibration Quality
The calibration system computes **RMS (Root Mean Square) error** to assess quality:
| Quality | RMS Error | Description |
|---------|-----------|-------------|
| Excellent | < 5 pixels | Professional-grade accuracy |
| Good | 5-10 pixels | Suitable for most applications |
| Fair | 10-20 pixels | Acceptable for basic touch |
| Poor | > 20 pixels | Re-calibration recommended |
Quality is displayed during calibration and can be checked programmatically:
```python
quality = touch.calibration.get_calibration_quality()
error = touch.calibration.calibration_data.rms_error
print(f"Calibration: {quality} ({error:.2f}px RMS error)")
```
## How Calibration Works
### 1. Calibration Point Collection
The calibration process displays targets at known display coordinates and records the raw touch coordinates when you tap each target:
```
Display Coordinates Touch Coordinates
(100, 100) → (95, 103)
(400, 100) → (392, 105)
(700, 100) → (689, 107)
...
```
### 2. Affine Transformation
An affine transformation maps touch coordinates to display coordinates:
```
x_display = a * x_touch + b * y_touch + c
y_display = d * x_touch + e * y_touch + f
```
This handles:
- **Translation** (offset)
- **Scaling** (different resolutions)
- **Rotation** (if display is rotated)
- **Skew** (non-perpendicular axes)
### 3. Least-Squares Fitting
The calibration algorithm uses **least-squares fitting** to find the best transformation matrix that minimizes error across all calibration points.
With N calibration points, the system is:
- **Over-determined** (N > 3 points for 6 unknowns)
- **Robust** to individual touch errors
- **Optimal** in the least-squares sense
### 4. Coordinate Transformation
Once calibrated, all touch coordinates are automatically transformed:
```python
# Raw touch from sensor
raw_x, raw_y = 392, 105
# Apply calibration
calibrated_x, calibrated_y = calibration.transform(raw_x, raw_y)
# Result: (400, 100) - matches display target!
```
## Calibration Data Format
Calibration is saved as JSON:
```json
{
"points": [
{"display_x": 100, "display_y": 100, "touch_x": 95, "touch_y": 103},
{"display_x": 400, "display_y": 100, "touch_x": 392, "touch_y": 105},
...
],
"matrix": [1.05, -0.02, -3.5, 0.01, 0.98, 2.1],
"width": 800,
"height": 1200,
"rms_error": 3.42
}
```
- **points**: List of calibration point pairs
- **matrix**: Affine transformation `[a, b, c, d, e, f]`
- **width/height**: Display dimensions
- **rms_error**: Quality metric in pixels
## Programmatic Usage
### Manual Calibration
You can implement custom calibration UI:
```python
from dreader_hal.calibration import TouchCalibration
# Create calibration instance
calibration = TouchCalibration(width=800, height=1200, num_points=9)
# Generate target positions
targets = calibration.generate_target_positions(margin=100, target_radius=20)
# For each target
for display_x, display_y in targets:
# Show target on display
# Wait for touch
touch_x, touch_y = get_touch() # Your touch reading code
# Add calibration point
calibration.add_calibration_point(display_x, display_y, touch_x, touch_y)
# Compute transformation
success = calibration.compute_calibration()
if success:
# Save calibration
calibration.save("~/.config/dreader/touch_calibration.json")
print(f"Quality: {calibration.get_calibration_quality()}")
print(f"RMS Error: {calibration.calibration_data.rms_error:.2f}px")
```
### Using Calibration
```python
from dreader_hal.calibration import TouchCalibration
# Load existing calibration
calibration = TouchCalibration(width=800, height=1200)
calibration.load("~/.config/dreader/touch_calibration.json")
# Transform coordinates
display_x, display_y = calibration.transform(raw_x, raw_y)
# Check if calibrated
if calibration.is_calibrated():
print("Calibration active")
```
## When to Re-Calibrate
You should re-calibrate if:
- **Initial setup**: First time using the device
- **Hardware changes**: Replaced touchscreen or display
- **Poor accuracy**: RMS error > 20 pixels
- **Display rotation**: Changed from portrait to landscape
- **Physical damage**: Screen damage or loose connections
## Troubleshooting
### Calibration Fails to Compute
**Problem:** `compute_calibration()` returns `False`
**Solutions:**
- Ensure at least 3 calibration points were collected
- Check that points are not all collinear
- Verify touch coordinates are valid
### Poor Calibration Quality
**Problem:** High RMS error or "Poor" quality rating
**Solutions:**
- Re-run calibration, touching targets more precisely
- Use 9-point calibration instead of 5-point
- Check for hardware issues (loose connections, damaged screen)
- Ensure targets are clearly visible on e-ink display
### Calibration Not Loading
**Problem:** "No calibration file found" message
**Solutions:**
- Check calibration file exists at expected path
- Verify file permissions are readable
- Ensure display dimensions match calibration data
### Touches Still Offset After Calibration
**Problem:** Calibrated touches don't align with targets
**Solutions:**
- Check calibration quality with `test_calibration.py`
- Re-run calibration
- Verify calibration file is being loaded (check console output)
- Ensure touch driver is using calibration (not bypassed)
## Advanced Topics
### Custom Transformation Algorithms
The default calibration uses affine transformation with least-squares fitting. For advanced use cases, you can extend `TouchCalibration`:
```python
class CustomCalibration(TouchCalibration):
def compute_calibration(self) -> bool:
# Implement custom algorithm
# e.g., polynomial transformation, neural network, etc.
pass
```
### Multi-Display Support
For devices with multiple displays:
```python
# Calibration per display
calibration_main = TouchCalibration(800, 1200)
calibration_main.load("main_display_cal.json")
calibration_secondary = TouchCalibration(400, 600)
calibration_secondary.load("secondary_display_cal.json")
```
### Runtime Calibration Adjustment
You can update calibration without full re-calibration:
```python
# Add new calibration points to existing calibration
calibration.add_calibration_point(x_display, y_display, x_touch, y_touch)
calibration.compute_calibration() # Re-compute with new points
calibration.save(calibration_file)
```
## API Reference
See [calibration.py](src/dreader_hal/calibration.py) for full API documentation.
Key classes:
- **`TouchCalibration`**: Main calibration class
- **`CalibrationData`**: Calibration dataset and matrix
- **`CalibrationPoint`**: Single calibration point pair
Key methods:
- `generate_target_positions()`: Create calibration target grid
- `add_calibration_point()`: Record calibration point
- `compute_calibration()`: Calculate transformation matrix
- `transform()`: Apply calibration to coordinates
- `save()` / `load()`: Persist calibration data
- `is_calibrated()`: Check if calibration is loaded
- `get_calibration_quality()`: Get quality assessment