2
\$\begingroup\$

I've written this as an alternative to xinput_calibrator when it wasn't working for me. It's my first crack at several things: X.org, numpy, and tkinter.

This does indeed work (for me, although I've only tested it with one touchscreen and this has not been tested publicly).

Do you see any obvious bugs or gotchas, in general or specific to the libs being used?

I've just uploaded it to Github, although it's simple enough that it exists completely in one script, shown here:

#!/usr/bin/env python3

# requires python3-tk, python3-numpy

import re
import numpy as np
from subprocess import run, PIPE


prop_name = 'libinput Calibration Matrix'


def xinput(*args):
    return run(args=('/usr/bin/xinput', *args),
               stdout=PIPE, check=True,
               universal_newlines=True).stdout


def get_devs():
    devs = {int(groups[1]): groups[0] for groups in
            re.findall(r'↳ (\w.+\w)\s+id=(\d+)\D+slave *pointer',
                       xinput('--list', '--short'))}
    if not devs:
        print('No suitable input devices found')
        exit(1)
    return devs


def print_devs(devs):
    print('Pointer devices:')
    print('%4s %35s' % ('ID', 'Name'))
    for i, name in sorted(devs.items()):
        print('%4d %35s' % (i, name))
    print()


def choose_preferred(devs):
    preferred = [i for (i, n) in devs.items() if 'touch' in n.lower()]
    if preferred:
        return preferred[0]
    return next(iter(devs.keys()))


def choose_dev(devs, preferred):
    while True:
        devstr = input('Device to calibrate [%d]: ' % preferred)
        if not devstr:
            return preferred
        try:
            dev = int(devstr)
        except ValueError:
            continue
        if dev in devs.keys():
            return dev


def read_cal(dev):
    stdout = xinput('--list-props', str(dev))
    line = re.search(prop_name + r'.*:\s+(\S.+)', stdout)
    if not line:
        print('Cal property not set; is this an xinput device?')
        exit(1)
    vals = np.matrix(line.group(1)).reshape(3, 3)

    print('Old calibration:')
    print(vals)
    print()
    return vals, np.linalg.inv(vals)


def ask(q):
    do = input(q + ' [y]: ')
    return (do or 'y').lower() == 'y'


def choose_points():
    p_min, default = 3, 4

    while True:
        p_str = input('Point count (min %d) [%d]: ' %
                      (p_min, default))
        if not p_str:
            return default
        try:
            p = int(p_str)
        except ValueError:
            continue

        if p >= p_min:
            return p


def show_tk(n_points, old_cal_inv, new_cal=None):
    from tkinter import Tk, Canvas
    from math import ceil, sqrt

    root = Tk()
    X, Y = None, None
    root.attributes('-fullscreen', True)
    canvas = Canvas(root)

    def resize(event):
        nonlocal X, Y
        X, Y = event.width, event.height
        next_point()
    canvas.bind('<Configure>', resize)
    canvas.pack(expand=True, fill='both')

    point, points = {}, []
    index = -1
    n_cols = int(ceil(sqrt(n_points)))
    n_rows = int(ceil(n_points / n_cols))
    sensitive = False

    def next_point():
        nonlocal point, index, sensitive
        sensitive = True
        index += 1
        if index >= n_points:
            root.destroy()
        else:
            x = 0.1 + 0.8*(index % n_cols)/(n_cols - 1)
            y = 0.1 + 0.8*(index // n_cols)/(n_rows - 1)
            point = {'sx': x, 'sy': y}

            draw_target(point['sx'], point['sy'])

    def cross(px, py, colour):
        x, y = px*X, py*Y
        canvas.create_line(x-10, y, x+10, y, fill=colour)
        canvas.create_line(x, y-10, x, y+10, fill=colour)

    def draw_target(px, py):
        x, y = px*X, py*Y
        canvas.create_oval(x-10, y-10, x+10, y+10, outline='#F00', width=3)
        cross(px, py, '#F00')

    def cancel_cal(_):
        print('Calibration cancelled')
        points.clear()
        root.destroy()
    root.bind('<Escape>', cancel_cal)
    canvas.bind('<Escape>', cancel_cal)

    def transform(x, y, cal):
        p = np.matrix([[x], [y], [1]])
        out = np.matmul(cal, p)
        return out.item(0), out.item(1)

    def indicator(sx, sy, px, py, colour):
        canvas.create_line(X*sx, Y*sy, X*px, Y*py, fill=colour)
        cross(px, py, colour)

    def click(event):
        nonlocal sensitive
        if not sensitive:
            return
        sensitive = False

        sx, sy = point['sx'], point['sy']

        ox, oy = event.x/X, event.y/Y  # old-calibrated
        indicator(sx, sy, ox, oy, '#00F')

        ux, uy = transform(ox, oy, old_cal_inv)  # uncalibrated
        indicator(sx, sy, ux, uy, '#000')

        if new_cal is not None:
            nx, ny = transform(ux, uy, new_cal)  # new-calibrated (test only)
            indicator(sx, sy, nx, ny, '#0F0')

        point.update({'mx': ux, 'my': uy})
        points.append(point)

        canvas.after(500, next_point)
    canvas.bind('<Button-1>', click)

    root.mainloop()

    return points


def calibrate(points):
    from math import log10

    m_screen = np.matrix([[p['sx'], p['sy'], 1] for p in points])
    m_mouse = np.matrix([[p['mx'], p['my'], 1] for p in points])
    m_transform, residuals, rank, singular = np.linalg.lstsq(m_mouse, m_screen)
    quality = -log10(residuals.sum())

    m_transform = m_transform.getT()
    m_transform[2, :] = [0, 0, 1]
    return m_transform, quality


def use_cal(dev, new_cal):
    cal_array = [str(x)+',' for x in new_cal.flatten().tolist()[0]]
    xinput('--set-prop', str(dev), prop_name, *cal_array)


def main():
    devs = get_devs()
    print_devs(devs)
    preferred = choose_preferred(devs)
    dev = choose_dev(devs, preferred)
    print()

    old_cal, old_cal_inv = read_cal(dev)

    new_cal = None
    if ask('Calibrate?'):
        n_points = choose_points()
        print()

        points = show_tk(n_points, old_cal_inv)
        if points:
            new_cal, quality = calibrate(points)

            print('New calibration:')
            print(new_cal)
            print('Quality (should be at least 3): %.1f' % quality)
            print()

    if ask('Test?'):
        n_points = choose_points()
        show_tk(n_points, old_cal_inv, new_cal)

    if new_cal is not None and ask('Use calibration?'):
        use_cal(dev, new_cal)

main()
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.