356 lines
12 KiB
Python
Executable File
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()
|