2025, Oct 31 14:47

Как правильно вычислять координаты текста в PIL и tkinter

Разбираем, почему подписи в PIL и tkinter «слипаются»: ошибка в арифметике шагов, а не в координатах. Показываем деление на (n+1) и код для сетки 2×2.

Точное размещение текста на изображении PIL, отображаемом через tkinter, кажется обманчиво простым — пока не вмешается арифметика. Типичная ситуация: вы хотите нанести подписи через равные промежутки на картинку 900×900 и рассчитываете получить точки 300×300, 300×600, 600×300 и 600×600. Вместо этого всё слипается возле центра. Причина не в различии систем координат PIL и ImageTk, а в том, как сгруппированы вычисления.

Постановка задачи

Ниже — упрощённый пример, который вычисляет шаг от размеров изображения и рисует один и тот же текст в раскладке 2×2. Имена переменных обычные, а логика — та самая, что приводит к неверным позициям.

global painter, tk_img
painter = ImageDraw.Draw(pil_img)
face = ImageFont.load_default()
for row in range(1, int(2)+1):
    print(tk_img.height() / int(2) + 1)
    y_step = (tk_img.height() / int(2) + 1) * row
    for col in range(1, int(2)+1):
        x_step = (tk_img.width() / int(2) + 1) * col
        print(x_step)
        painter.text(xy=(x_step, y_step), text="TESTER", fill=(255,255,255), font=face)
tk_img = ImageTk.PhotoImage(pil_img)
image_holder.image = tk_img
image_holder.config(image=tk_img)

Что именно идёт не так

Проблема чисто арифметическая. В выражениях для x и y шаг формируется как деление на 2, а затем прибавляется 1. То есть шаг считается как (размер / 2) + 1. Для изображения 900×900 это примерно 451. Когда циклы умножают этот шаг на 1 и 2, координаты по каждой оси получаются 451 и 902. В итоге предпринимаются попытки поставить надписи в точках (451,451), (451,902), (902,451), (902,902). Внутри изображения оказывается только первая; остальные лежат на нижней/правой границе или за её пределами и потому не видны. Это не несовпадение единиц между PIL и ImageTk — дело в расстановке скобок.

Задумывалось другое: разбить область на три равные части и поставить текст в позициях 1/3 и 2/3. Значит, шаг должен быть равен размеру / 3, а не (размер / 2) + 1. Плюс один относится не к результату деления, а к знаменателю, задающему число отрезков.

Решение

Скобки меняют смысл. Делите на нужное число сегментов, а не на 2 с последующим прибавлением 1. В исправленном варианте деление идёт на (2 + 1), то есть на треть размера.

global painter, tk_img
painter = ImageDraw.Draw(pil_img)
face = ImageFont.load_default()
for row in range(1, int(2)+1):
    y_step = (tk_img.height() / (int(2) + 1)) * row
    for col in range(1, int(2)+1):
        x_step = (tk_img.width() / (int(2) + 1)) * col
        painter.text(xy=(x_step, y_step), text="TESTER", fill=(255,255,255), font=face)
tk_img = ImageTk.PhotoImage(pil_img)
image_holder.image = tk_img
image_holder.config(image=tk_img)

Для изображения 900×900 шаг равен 300. Цикл ставит подписи на 300 и 600 по обеим осям, получая ожидаемые точки 300×300, 300×600, 600×300 и 600×600.

Почему это важно

При вычислении координат для графики приоритет операций и группировка выражений напрямую влияют на результат. Незаметная перестановка скобок способна увести элементы за пределы области рисования без единого сообщения об ошибке. Легко отвлечься на стек GUI и заподозрить разнобой единиц между tkinter, ImageTk и PIL. В таких ситуациях библиотеки используют одни и те же пиксели; внимание нужно уделить арифметике.

Ещё один практический момент — контроль итераций. Если цель — четыре элемента в сетке 2×2, циклы должны давать ровно два шага по каждой оси. Расширение диапазонов увеличит число позиций и легко «вышвырнет» часть из них за границы. Связывайте делитель с задуманным разбиением, чтобы не вылезти за холст.

Выводы

Распределяя элементы по изображению, мыслите отрезками. Нужны позиции в 1/3 и 2/3 — берите шаг как размер, делённый на число сегментов, и умножайте на индекс. Держите делитель внутри скобок, чтобы делить на общее число частей, а не на частичное значение с прибавкой. И согласуйте количество итераций с тем, сколько точек реально требуется на каждой оси.

Статья основана на вопросе на StackOverflow от Aadvik и ответе автора Aadvik.