dreader-hal/examples/calibrate_touch.py
2025-11-10 18:06:11 +01:00

356 lines
12 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Touchscreen Calibration Utility
This script guides the user through touchscreen calibration by:
1. Displaying calibration targets (circles) at known positions
2. Waiting for user to touch each target
3. Recording touch coordinates
4. Computing transformation matrix
5. Saving calibration data
Usage:
python3 calibrate_touch.py [--points N] [--output PATH]
Options:
--points N Number of calibration points (5 or 9, default 9)
--output PATH Calibration file path (default ~/.config/dreader/touch_calibration.json)
--virtual Use virtual display for testing
"""
import asyncio
import argparse
import sys
import os
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
# Add src to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../src'))
from dreader_hal.display.it8951 import IT8951DisplayDriver
from dreader_hal.touch.ft5xx6 import FT5xx6TouchDriver
from dreader_hal.calibration import TouchCalibration
from dreader_hal.types import GestureType, RefreshMode
class CalibrationUI:
"""
Calibration user interface.
Displays calibration targets and instructions on the e-ink display.
"""
def __init__(self, width: int, height: int, target_radius: int = 10):
self.width = width
self.height = height
self.target_radius = target_radius
def draw_target(self, image: Image.Image, x: int, y: int,
filled: bool = False) -> None:
"""
Draw a calibration target circle.
Args:
image: PIL Image to draw on
x: Target X position
y: Target Y position
filled: Whether target has been touched
"""
draw = ImageDraw.Draw(image)
# Draw concentric circles
r = self.target_radius
# Outer circle
draw.ellipse([x - r, y - r, x + r, y + r],
outline=0 if not filled else 128,
width=2)
# Middle circle
draw.ellipse([x - r//2, y - r//2, x + r//2, y + r//2],
outline=0 if not filled else 128,
width=2)
# Center dot
if filled:
draw.ellipse([x - 3, y - 3, x + 3, y + 3],
fill=128, outline=128)
else:
draw.ellipse([x - 3, y - 3, x + 3, y + 3],
fill=0, outline=0)
def create_calibration_screen(self, targets: list, current_idx: int,
completed: list) -> Image.Image:
"""
Create calibration screen with targets and instructions.
Args:
targets: List of (x, y) target positions
current_idx: Index of current target
completed: List of completed target indices
Returns:
PIL Image
"""
# Create white background
image = Image.new('L', (self.width, self.height), color=255)
draw = ImageDraw.Draw(image)
# Draw title
title = "Touchscreen Calibration"
try:
# Try to use a larger font if available
font_title = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 32)
font_text = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 24)
except:
# Fall back to default font
font_title = ImageFont.load_default()
font_text = ImageFont.load_default()
# Draw title centered
bbox = draw.textbbox((0, 0), title, font=font_title)
text_width = bbox[2] - bbox[0]
draw.text(((self.width - text_width) // 2, 30), title,
fill=0, font=font_title)
# Draw instructions
if current_idx < len(targets):
instruction = f"Touch target {current_idx + 1} of {len(targets)}"
else:
instruction = "Calibration complete!"
bbox = draw.textbbox((0, 0), instruction, font=font_text)
text_width = bbox[2] - bbox[0]
draw.text(((self.width - text_width) // 2, 80), instruction,
fill=0, font=font_text)
# Draw all targets
for idx, (tx, ty) in enumerate(targets):
if idx in completed:
# Completed target - filled
self.draw_target(image, tx, ty, filled=True)
elif idx == current_idx:
# Current target - highlighted
self.draw_target(image, tx, ty, filled=False)
# Draw arrow or indicator
draw.text((tx + self.target_radius + 10, ty - 10),
"", fill=0, font=font_text)
else:
# Future target - dimmed
self.draw_target(image, tx, ty, filled=False)
# Draw progress bar at bottom
progress_width = int((len(completed) / len(targets)) * (self.width - 100))
draw.rectangle([50, self.height - 50, 50 + progress_width, self.height - 30],
fill=0, outline=0)
draw.rectangle([50, self.height - 50, self.width - 50, self.height - 30],
outline=0, width=2)
return image
def create_results_screen(self, calibration: TouchCalibration) -> Image.Image:
"""
Create results screen showing calibration quality.
Args:
calibration: TouchCalibration instance
Returns:
PIL Image
"""
image = Image.new('L', (self.width, self.height), color=255)
draw = ImageDraw.Draw(image)
try:
font_title = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 32)
font_text = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 24)
except:
font_title = ImageFont.load_default()
font_text = ImageFont.load_default()
# Title
title = "Calibration Complete!"
bbox = draw.textbbox((0, 0), title, font=font_title)
text_width = bbox[2] - bbox[0]
draw.text(((self.width - text_width) // 2, 50), title,
fill=0, font=font_title)
# Quality
quality = calibration.get_calibration_quality()
rms_error = calibration.calibration_data.rms_error
quality_text = f"Quality: {quality}"
bbox = draw.textbbox((0, 0), quality_text, font=font_text)
text_width = bbox[2] - bbox[0]
draw.text(((self.width - text_width) // 2, 120), quality_text,
fill=0, font=font_text)
error_text = f"RMS Error: {rms_error:.2f} pixels"
bbox = draw.textbbox((0, 0), error_text, font=font_text)
text_width = bbox[2] - bbox[0]
draw.text(((self.width - text_width) // 2, 160), error_text,
fill=0, font=font_text)
# Instructions
info = "Calibration data has been saved."
bbox = draw.textbbox((0, 0), info, font=font_text)
text_width = bbox[2] - bbox[0]
draw.text(((self.width - text_width) // 2, 220), info,
fill=0, font=font_text)
return image
async def run_calibration(width: int, height: int, num_points: int,
output_path: str, virtual: bool = False):
"""
Run the calibration process.
Args:
width: Display width
height: Display height
num_points: Number of calibration points
output_path: Path to save calibration file
virtual: Use virtual display
"""
print(f"Starting touchscreen calibration...")
print(f"Display: {width}x{height}")
print(f"Calibration points: {num_points}")
print(f"Output: {output_path}")
# Initialize display
display = IT8951DisplayDriver(
width=width,
height=height,
virtual=virtual,
)
await display.initialize()
print("Display initialized")
# Initialize touch
touch = FT5xx6TouchDriver(
width=width,
height=height,
)
await touch.initialize()
print("Touch controller initialized")
# Create calibration instance
calibration = TouchCalibration(width, height, num_points)
ui = CalibrationUI(width, height, target_radius=20)
# Generate target positions
targets = calibration.generate_target_positions(margin=100, target_radius=20)
print(f"Generated {len(targets)} calibration targets")
completed = []
current_idx = 0
try:
# Calibration loop
while current_idx < len(targets):
# Draw calibration screen
screen = ui.create_calibration_screen(targets, current_idx, completed)
await display.show_image(screen, mode=RefreshMode.QUALITY)
target_x, target_y = targets[current_idx]
print(f"\nTarget {current_idx + 1}/{len(targets)}: Touch circle at ({target_x}, {target_y})")
# Wait for touch
touch_received = False
while not touch_received:
event = await touch.get_touch_event()
if event and event.gesture == GestureType.TAP:
# Record calibration point
calibration.add_calibration_point(
display_x=target_x,
display_y=target_y,
touch_x=event.x,
touch_y=event.y,
)
print(f" Recorded touch at ({event.x}, {event.y})")
# Mark as completed
completed.append(current_idx)
current_idx += 1
touch_received = True
# Small delay to prevent double-touches
await asyncio.sleep(0.5)
# Compute calibration matrix
print("\nComputing calibration matrix...")
success = calibration.compute_calibration()
if not success:
print("ERROR: Failed to compute calibration matrix")
return
print(f"Calibration computed successfully!")
print(f" Quality: {calibration.get_calibration_quality()}")
print(f" RMS Error: {calibration.calibration_data.rms_error:.2f} pixels")
print(f" Matrix: {calibration.calibration_data.matrix}")
# Save calibration
calibration.save(output_path)
print(f"\nCalibration saved to: {output_path}")
# Show results screen
results_screen = ui.create_results_screen(calibration)
await display.show_image(results_screen, mode=RefreshMode.QUALITY)
# Wait a bit so user can see results
await asyncio.sleep(3)
finally:
# Cleanup
await display.cleanup()
await touch.cleanup()
print("\nCalibration complete!")
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(
description="Touchscreen calibration utility for DReader HAL"
)
parser.add_argument(
'--points', type=int, default=9, choices=[5, 9],
help='Number of calibration points (default: 9)'
)
parser.add_argument(
'--output', type=str,
default=str(Path.home() / '.config' / 'dreader' / 'touch_calibration.json'),
help='Output calibration file path'
)
parser.add_argument(
'--width', type=int, default=800,
help='Display width in pixels (default: 800)'
)
parser.add_argument(
'--height', type=int, default=1200,
help='Display height in pixels (default: 1200)'
)
parser.add_argument(
'--virtual', action='store_true',
help='Use virtual display for testing'
)
args = parser.parse_args()
# Run calibration
asyncio.run(run_calibration(
width=args.width,
height=args.height,
num_points=args.points,
output_path=args.output,
virtual=args.virtual,
))
if __name__ == '__main__':
main()