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

9.5 KiB

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

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:

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:

# 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

python3 calibrate_touch.py --output /path/to/calibration.json

Then specify the same path when initializing the touch driver:

touch = FT5xx6TouchDriver(
    calibration_file="/path/to/calibration.json"
)

Display Dimensions

If your display is not the default 800x1200:

python3 calibrate_touch.py --width 1024 --height 768

Testing Calibration

Use the test script to verify calibration quality:

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:

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:

# 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:

{
  "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:

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

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:

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:

# 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:

# 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 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