Image and Texture

Image

An image class for loading, saving, and manipulating pixel data.

Uses Pillow for file I/O and stores pixel data as a numpy uint8 array.

Source code in ncca/ngl/image.py
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
class Image:
    """An image class for loading, saving, and manipulating pixel data.

    Uses Pillow for file I/O and stores pixel data as a numpy uint8 array.
    """

    def __init__(
        self,
        filename: str | None = None,
        width: int = 0,
        height: int = 0,
        mode: ImageModes | None = None,
    ) -> None:
        """Create an image, either from a file or as a blank canvas.

        Args:
            filename: Path of an image to load; takes precedence if given.
            width: Width of the blank image when no filename is given.
            height: Height of the blank image when no filename is given.
            mode: Colour mode for the blank image; data is None if omitted.
        """
        if filename:
            self.load(filename)
            logger.debug(f"Creating Image from file {filename} ")
        else:
            self._width = width
            self._height = height
            self._mode = mode
            if mode:
                if mode == ImageModes.GRAY:
                    self._data = np.zeros((height, width), dtype=np.uint8)
                else:
                    self._data = np.zeros(
                        (height, width, len(mode.value)), dtype=np.uint8
                    )
            else:
                self._data = None

    def set_pixel(self, x: int, y: int, r: int, g: int, b: int, a: int = 255) -> None:
        """Set the pixel at (x, y) to the given colour.

        Args:
            x: Pixel x coordinate.
            y: Pixel y coordinate.
            r: Red component (0-255).
            g: Green component (0-255).
            b: Blue component (0-255).
            a: Alpha component (0-255), used only in RGBA mode.

        Raises:
            ValueError: If the coordinates are out of bounds.
        """
        if x < 0 or x >= self._width or y < 0 or y >= self._height:
            raise ValueError("Pixel coordinates out of bounds")
        if self._mode == ImageModes.RGBA:
            self._data[y, x] = [r, g, b, a]
        else:
            self._data[y, x] = [r, g, b]

    def load(self, filename: str) -> bool:
        """Load an image from file, converting unsupported modes.

        Returns:
            bool: True on success, False if loading failed.
        """
        try:
            with PILImage.open(filename) as img:
                self._width = img.width
                self._height = img.height
                try:
                    self._mode = ImageModes(img.mode)
                except ValueError:
                    logger.warning(f"Image mode {img.mode} not supported, converting")
                    if img.mode == "I;16":
                        img = img.convert("L")
                    else:
                        img = img.convert("RGB")
                    self._mode = ImageModes(img.mode)

                self._data = np.array(img)
            return True
        except Exception as e:
            logger.error(f"Error loading image {filename}: {e}")
            return False

    def save(self, filename: str) -> bool:
        """Save the image to file, format inferred from the extension.

        Returns:
            bool: True on success, False if saving failed.
        """
        try:
            img = PILImage.fromarray(self._data).convert(self._mode.value)
            img.save(filename)
            return True
        except Exception as e:
            logger.error(f"Error saving image {filename}: {e}")
            return False

    @property
    def width(self) -> int:
        """The image width in pixels."""
        return self._width

    @property
    def height(self) -> int:
        """The image height in pixels."""
        return self._height

    @property
    def mode(self) -> ImageModes:
        """The image colour mode."""
        return self._mode

    def get_pixels(self) -> np.ndarray:
        """Return the raw pixel data array."""
        return self._data

height property

The image height in pixels.

mode property

The image colour mode.

width property

The image width in pixels.

__init__(filename=None, width=0, height=0, mode=None)

Create an image, either from a file or as a blank canvas.

Parameters:
  • filename (str | None, default: None ) –

    Path of an image to load; takes precedence if given.

  • width (int, default: 0 ) –

    Width of the blank image when no filename is given.

  • height (int, default: 0 ) –

    Height of the blank image when no filename is given.

  • mode (ImageModes | None, default: None ) –

    Colour mode for the blank image; data is None if omitted.

Source code in ncca/ngl/image.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def __init__(
    self,
    filename: str | None = None,
    width: int = 0,
    height: int = 0,
    mode: ImageModes | None = None,
) -> None:
    """Create an image, either from a file or as a blank canvas.

    Args:
        filename: Path of an image to load; takes precedence if given.
        width: Width of the blank image when no filename is given.
        height: Height of the blank image when no filename is given.
        mode: Colour mode for the blank image; data is None if omitted.
    """
    if filename:
        self.load(filename)
        logger.debug(f"Creating Image from file {filename} ")
    else:
        self._width = width
        self._height = height
        self._mode = mode
        if mode:
            if mode == ImageModes.GRAY:
                self._data = np.zeros((height, width), dtype=np.uint8)
            else:
                self._data = np.zeros(
                    (height, width, len(mode.value)), dtype=np.uint8
                )
        else:
            self._data = None

get_pixels()

Return the raw pixel data array.

Source code in ncca/ngl/image.py
136
137
138
def get_pixels(self) -> np.ndarray:
    """Return the raw pixel data array."""
    return self._data

load(filename)

Load an image from file, converting unsupported modes.

Returns:
  • bool( bool ) –

    True on success, False if loading failed.

Source code in ncca/ngl/image.py
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def load(self, filename: str) -> bool:
    """Load an image from file, converting unsupported modes.

    Returns:
        bool: True on success, False if loading failed.
    """
    try:
        with PILImage.open(filename) as img:
            self._width = img.width
            self._height = img.height
            try:
                self._mode = ImageModes(img.mode)
            except ValueError:
                logger.warning(f"Image mode {img.mode} not supported, converting")
                if img.mode == "I;16":
                    img = img.convert("L")
                else:
                    img = img.convert("RGB")
                self._mode = ImageModes(img.mode)

            self._data = np.array(img)
        return True
    except Exception as e:
        logger.error(f"Error loading image {filename}: {e}")
        return False

save(filename)

Save the image to file, format inferred from the extension.

Returns:
  • bool( bool ) –

    True on success, False if saving failed.

Source code in ncca/ngl/image.py
107
108
109
110
111
112
113
114
115
116
117
118
119
def save(self, filename: str) -> bool:
    """Save the image to file, format inferred from the extension.

    Returns:
        bool: True on success, False if saving failed.
    """
    try:
        img = PILImage.fromarray(self._data).convert(self._mode.value)
        img.save(filename)
        return True
    except Exception as e:
        logger.error(f"Error saving image {filename}: {e}")
        return False

set_pixel(x, y, r, g, b, a=255)

Set the pixel at (x, y) to the given colour.

Parameters:
  • x (int) –

    Pixel x coordinate.

  • y (int) –

    Pixel y coordinate.

  • r (int) –

    Red component (0-255).

  • g (int) –

    Green component (0-255).

  • b (int) –

    Blue component (0-255).

  • a (int, default: 255 ) –

    Alpha component (0-255), used only in RGBA mode.

Raises:
  • ValueError

    If the coordinates are out of bounds.

Source code in ncca/ngl/image.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def set_pixel(self, x: int, y: int, r: int, g: int, b: int, a: int = 255) -> None:
    """Set the pixel at (x, y) to the given colour.

    Args:
        x: Pixel x coordinate.
        y: Pixel y coordinate.
        r: Red component (0-255).
        g: Green component (0-255).
        b: Blue component (0-255).
        a: Alpha component (0-255), used only in RGBA mode.

    Raises:
        ValueError: If the coordinates are out of bounds.
    """
    if x < 0 or x >= self._width or y < 0 or y >= self._height:
        raise ValueError("Pixel coordinates out of bounds")
    if self._mode == ImageModes.RGBA:
        self._data[y, x] = [r, g, b, a]
    else:
        self._data[y, x] = [r, g, b]

Texture

A texture class to load and create OpenGL textures.

Source code in ncca/ngl/opengl/texture.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
class Texture:
    """A texture class to load and create OpenGL textures."""

    def __init__(self, filename: str | None = None) -> None:
        """Create a texture, optionally loading an image file immediately."""
        self._image = Image(filename)
        self._texture_id = 0
        self._multi_texture_id = 0

    @property
    def width(self) -> int:
        """The texture image width in pixels."""
        return self._image.width

    @property
    def height(self) -> int:
        """The texture image height in pixels."""
        return self._image.height

    @property
    def format(self) -> int:
        """The OpenGL pixel format for the image mode, or 0 if unknown."""
        if self._image.mode:
            if self._image.mode.value == "RGB":
                return gl.GL_RGB
            elif self._image.mode.value == "RGBA":
                return gl.GL_RGBA
            elif self._image.mode.value == "L":
                return gl.GL_RED
        return 0

    @property
    def internal_format(self) -> int:
        """The OpenGL internal format for the image mode, or 0 if unknown."""
        if self._image.mode:
            if self._image.mode.value == "RGB":
                return gl.GL_RGB8
            elif self._image.mode.value == "RGBA":
                return gl.GL_RGBA8
            elif self._image.mode.value == "L":
                return gl.GL_R8
        return 0

    def load_image(self, filename: str) -> bool:
        """Load an image file; returns True on success."""
        return self._image.load(filename)

    def get_pixels(self) -> bytes:
        """Return the raw pixel data as bytes."""
        return self._image.get_pixels().tobytes()

    def set_texture_gl(self) -> int:
        """Generate a texture ID and set the texture parameters.

        Returns 0 (the OpenGL "not active" default) if the image is invalid.
        """
        if self._image.width > 0 and self._image.height > 0:
            self._texture_id = gl.glGenTextures(1)
            gl.glActiveTexture(gl.GL_TEXTURE0 + self._multi_texture_id)
            gl.glBindTexture(gl.GL_TEXTURE_2D, self._texture_id)
            gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
            gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
            gl.glTexImage2D(
                gl.GL_TEXTURE_2D,
                0,
                self.internal_format,
                self.width,
                self.height,
                0,
                self.format,
                gl.GL_UNSIGNED_BYTE,
                self.get_pixels(),
            )
            gl.glGenerateMipmap(gl.GL_TEXTURE_2D)
        return self._texture_id

    def set_multi_texture(self, id: int) -> None:
        """Set the texture unit offset used when binding."""
        self._multi_texture_id = id

format property

The OpenGL pixel format for the image mode, or 0 if unknown.

height property

The texture image height in pixels.

internal_format property

The OpenGL internal format for the image mode, or 0 if unknown.

width property

The texture image width in pixels.

__init__(filename=None)

Create a texture, optionally loading an image file immediately.

Source code in ncca/ngl/opengl/texture.py
13
14
15
16
17
def __init__(self, filename: str | None = None) -> None:
    """Create a texture, optionally loading an image file immediately."""
    self._image = Image(filename)
    self._texture_id = 0
    self._multi_texture_id = 0

get_pixels()

Return the raw pixel data as bytes.

Source code in ncca/ngl/opengl/texture.py
57
58
59
def get_pixels(self) -> bytes:
    """Return the raw pixel data as bytes."""
    return self._image.get_pixels().tobytes()

load_image(filename)

Load an image file; returns True on success.

Source code in ncca/ngl/opengl/texture.py
53
54
55
def load_image(self, filename: str) -> bool:
    """Load an image file; returns True on success."""
    return self._image.load(filename)

set_multi_texture(id)

Set the texture unit offset used when binding.

Source code in ncca/ngl/opengl/texture.py
86
87
88
def set_multi_texture(self, id: int) -> None:
    """Set the texture unit offset used when binding."""
    self._multi_texture_id = id

set_texture_gl()

Generate a texture ID and set the texture parameters.

Returns 0 (the OpenGL "not active" default) if the image is invalid.

Source code in ncca/ngl/opengl/texture.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def set_texture_gl(self) -> int:
    """Generate a texture ID and set the texture parameters.

    Returns 0 (the OpenGL "not active" default) if the image is invalid.
    """
    if self._image.width > 0 and self._image.height > 0:
        self._texture_id = gl.glGenTextures(1)
        gl.glActiveTexture(gl.GL_TEXTURE0 + self._multi_texture_id)
        gl.glBindTexture(gl.GL_TEXTURE_2D, self._texture_id)
        gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
        gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
        gl.glTexImage2D(
            gl.GL_TEXTURE_2D,
            0,
            self.internal_format,
            self.width,
            self.height,
            0,
            self.format,
            gl.GL_UNSIGNED_BYTE,
            self.get_pixels(),
        )
        gl.glGenerateMipmap(gl.GL_TEXTURE_2D)
    return self._texture_id

ImageModes

Bases: Enum

Supported image colour modes, matching PIL mode strings.

Source code in ncca/ngl/image.py
14
15
16
17
18
19
class ImageModes(Enum):
    """Supported image colour modes, matching PIL mode strings."""

    RGB = "RGB"
    RGBA = "RGBA"
    GRAY = "L"