2025, Nov 28 03:02

Исправляем TypeError при set_window_icon в pyGLFW: передаем RGBA массив или PIL Image

Разбираем ошибку TypeError в pyGLFW при set_window_icon: почему нельзя сплющивать буфер, как правильно передать RGBA 3D‑массив из OpenCV или объект PIL Image.

Установка значка окна в pyGLFW может неожиданно оказаться непростой задачей, если передать пиксельные данные в неверной форме. Частая ошибка — сплющивать буфер изображения перед вызовом glfw.set_window_icon(), из‑за чего возникает TypeError внутри собственной логики преобразования pyGLFW.

Проблема в контексте

Проблема проявляется, когда изображение читается, при необходимости дополняется альфа‑каналом, затем сплющивается и в конце передаётся в pyGLFW как кортеж. Ниже — пример, который демонстрирует неверный подход.

def apply_window_icon(self, icon_path: str):
    frame = cv2.imread(icon_path, cv2.IMREAD_UNCHANGED)
    if frame is None:
        return False
    hh, ww = frame.shape[:2]
    if frame.shape[2] == 3:
        a_layer = numpy.ones((hh, ww, 1), dtype=numpy.uint8) * 255
        frame = numpy.concatenate((frame, a_layer), axis=2)
    frame = frame.astype(numpy.uint8)
    flat_buf = frame.flatten()
    glfw_image = (ww, hh, flat_buf)
    glfw.set_window_icon(self.handle, 1, [glfw_image])

Это приводит к ошибке вроде «TypeError: 'int' object is not subscriptable», указывающей на внутренний доступ к pixels[i][j][k].

Что на самом деле происходит

Внутренняя реализация pyGLFW ожидает, что пиксельные данные будут структурированной коллекцией с тремя индексами: высота, ширина и канал. Иными словами, 3D‑массив формы H x W x 4. Соответствующий фрагмент в glfw/__init__.py явно индексирует pixels тремя индексами и итерируется по четырём каналам:

else:
    self.width, self.height, pixels = image
    array_type = ctypes.c_ubyte * 4 * self.width * self.height
    self.pixels_array = array_type()
    for i in range(self.height):
        for j in range(self.width):
            for k in range(4):
                self.pixels_array[i][j][k] = pixels[i][j][k]

Это означает, что сплющенный 1D‑буфер несовместим с ожидаемым шаблоном доступа. Это также подразумевает, что у значка должен быть альфа‑канал, поскольку внутренний цикл проходит по четырём каналам (RGBA).

Есть и другой поддерживаемый путь. Если объект, переданный в glfw.set_window_icon(), выглядит как изображение PIL, pyGLFW конвертирует его в RGBA и считывает данные напрямую. Логика проверяет наличие .size и .convert и обрабатывает это так:

if hasattr(image, 'size') and hasattr(image, 'convert'):
    # Рассматривать объект как изображение PIL/Pillow
    self.width, self.height = image.size
    array_type = ctypes.c_ubyte * 4 * (self.width * self.height)
    self.pixels_array = array_type()
    pixels = image.convert('RGBA').getdata()
    for i, pixel in enumerate(pixels):
        self.pixels_array[i] = pixel

Как исправить код

Есть два надёжных варианта. Первый — продолжать использовать OpenCV, но передавать корректный 3D‑массив в порядке RGBA и не вызывать flatten(). Второй — передать объект PIL Image и позволить pyGLFW выполнить конвертацию.

Ниже — исправленные варианты, которые сохраняют исходную логику, но приводят форму данных и порядок каналов к ожидаемым.

import cv2
import numpy
import glfw

# изображение создано с помощью ImageMagick:
#    convert -size 32x32 -define png:color-type=2 canvas:white empty.png
# или на Python с PIL
#    python3 -c 'from PIL import Image;Image.new("RGB", (32,32), color="white").save("empty.png")'

ICON_FILE = 'empty.png'

def attach_icon_via_pillow(win_handle, icon_path):
    from PIL import Image
    picture = Image.open(icon_path)
    glfw.set_window_icon(win_handle, 1, [picture])

def attach_icon_via_cv2(win_handle, icon_path):
    img_cv = cv2.imread(icon_path, cv2.IMREAD_UNCHANGED)
    img_cv = cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB)  # преобразование из BGR в RGB

    if img_cv is None:
        return False

    hh, ww = img_cv.shape[:2]

    if img_cv.shape[2] == 3:
        alpha_chan = numpy.ones((hh, ww, 1), dtype=numpy.uint8) * 255
        img_cv = numpy.concatenate((img_cv, alpha_chan), axis=2)

    img_cv = img_cv.astype(numpy.uint8)
    glfw_image = (ww, hh, img_cv)  # 3D‑массив, не сплющенный
    glfw.set_window_icon(win_handle, 1, [glfw_image])


def demo():
    if not glfw.init():
        return

    win = glfw.create_window(640, 480, "Hello World", None, None)
    if not win:
        glfw.terminate()
        return

    attach_icon_via_cv2(win, ICON_FILE)
    # attach_icon_via_pillow(win, ICON_FILE)

    glfw.make_context_current(win)

    while not glfw.window_should_close(win):
        glfw.swap_buffers(win)
        glfw.poll_events()

    glfw.terminate()

if __name__ == '__main__':
    demo()

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

API pyGLFW поддерживает две разные формы входных данных для значка: объект PIL Image или кортеж из ширины, высоты и 3D‑массива RGBA. Передача сплющенного буфера нарушает ожидаемый доступ pixels[i][j][k], что и приводит к увиденному TypeError. Понимание этого контракта экономит время на отладку и делает исправление очевидным. Когда документация скудна, чтение небольших, но понятных фрагментов исходного кода библиотеки помогает точно понять, какую форму и тип ожидает функция. Включение полного сообщения об ошибке тоже ускоряет поиск решения: проблемная строка часто прямо указывает на несоответствие.

Выводы

Держите пиксели в виде массива H x W x 4 при работе с OpenCV и добавляйте альфа‑канал, если его нет. Не сплющивайте буфер. Если нужен самый простой путь, передавайте объект PIL Image — pyGLFW преобразует его в RGBA сам. А если застряли или не находите документацию под Python, загляните в исходники библиотеки и приложите полный трейсбэк: строка и контекст обычно точно показывают, какую форму ожидает функция.