# 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