Lab 3 An Image Class
Aims
The aim of this lab is to continue our exploration of TDD in python by developing an Image Class
- Developing a Simple Image Class
- Understand and Use python packages and uv
- Develop some simple Image algorithms.
Getting Started
We are going to start a new project but this time it will be in the new repository we created so we can upload to GitHub later. We will use uv to create a new project for us. In this case we are going to create a python package for our image class.
uv init --package Image
cd Image
find .
.
./pyproject.toml
./README.md
./src
./src/image
./src/image/__init__.py
You can see uv has created a template project file which now has a src folder as well as the pyproject.toml file and README.md. We can run this package via uv using
uv run image
Hello from image!
To get started we are going to create a folder for tests and add our dependencies.
mkdir tests
uv add --dev pytest
uv add pillow
RGBA class
In most image manipulation API’s image data is stored using a single unsigned integer with 32bit of precision, this allows for better cache and alignment with CPU word sizes (32 or 64 bits).
A 32-bit integer lets you pack RGBA—8 bits per channel—into one compact unit. Bitwise shifts and masks can quickly extract or modify channels this c++ code is quite common in API’s including things like Vulkan and OpenGL.
uint32_t pixel ; // packing is AARRGGBB
uint8_t r = (pixel >> 16) & 0xFF;
uint8_t g = (pixel >> 8) & 0xFF;
uint8_t b = (pixel >> 0) & 0xFF;
uint8_t a = (pixel >> 24) & 0xFF;
In python we don’t need to do this however we are going to use a python
@dataclass to represent RGBA values.
An Image Class
We are going to develop an image class and the RGBA class using a TDD approach.
classDiagram
class rgba {
<<dataclass>>
+int r
+int g
+int b
+int a
+__post_init__()
+as_tuple() tuple[int, int, int, int]
+__iter__()
}
class Image {
-int _width
-int _height
-np.ndarray _rgba_data
+width: int
+height: int
+pixels: np.ndarray
+shape: tuple[int, ...]
+__init__(width: int, height: int, fill_colour: Union[rgba, tuple, None])
+set_pixel(x: int, y: int, colour: rgba)
+get_pixel(x: int, y: int) Tuple[int, int, int, int]
+clear(colour: rgba)
+save(name: str)
+line(sx: int, sy: int, ex: int, ey: int, colour: rgba)
+rectangle(tx: int, ty: int, bx: int, by: int, colour: rgba)
+__getitem__(key: tuple[int, int]) rgba
+__setitem__(key: tuple[int, int], colour: rgba)
}
Image "1" -- "*" rgba : uses
We will use a TDD approach and write the following tests in sequence. This will be done interactively in the labs, however a full initial solution can be found here
# tests/test_Image.py
test_ctor_defaults()
test_ctor_values()
# tests/test_Image.py
def test_image_ctor_and_clear():
def test_set_get_pixel():
def test_line_horizontal():
def test_rectangle():
def test_save():
Building a package
In the pyproject.toml file you will notice it has a section as follows
[build-system]
[build-system]
requires = ["uv_build>=0.9.0,<0.10.0"]
build-backend = "uv_build"
It will exclude the tests from our package as they are not needed.
This is generated so we can package things using the uv_build build system. At it’s simplest level we can generate a python wheel and then install it into our own projects. To do this we can use the uv build command.
uv build
Building source distribution...
Building wheel from source distribution...
Successfully built dist/image-0.1.0.tar.gz
Successfully built dist/image-0.1.0-py3-none-any.whl
The file will contain everything in the project including the tests, if we wish to exclude these when we distribute we can add the following to the pyproject.toml
[tool.uv.build-backend]
module-root = "src"
exclude = ["tests"]
To test our project we can do the following in a new folder (I suggest in the folder below our current Image one).
uv init ImageTest
cd ImageTest
uv pip install ../Image/dist/image-0.1.0-py3-none-any.whl
uv run python
Python 3.13.3 (main, May 17 2025, 13:30:59) [Clang 20.1.4 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from image import Image
>>> i=Image(20,20)
>>> i.get_pixel(10,10)
RGBA(r=0, g=0, b=0, a=255)
This is a really useful way of building and testing our own packages before distributing.
Exercises
Using the Image class created above add a new method called
def line(self, sx: int, sy: int, ex: int, ey: int, color: RGBA):
Which uses the Bresenham line drawing algorithm outlined here to draw to the Image buffer. Note the following C++ functions may be of use std::swap and std::abs
A sample unit test for this method would be as follows, however this only tests a horizontal line. What other tests could be easily written?
def test_line_horizontal():
img = Image(5, 5)
blue = RGBA(0, 0, 255, 255)
img.line(0, 2, 4, 2, blue)
for x in range(5):
assert img.get_pixel(x, 2) == blue
Also we can write a method called rectangle which takes in two _x,_y values for Top Left and Bottom Right and fills the area with a colour.
def rectangle(self, tx: int, ty: int, bx: int, by: int, color: RGBA):
With both of these methods we should think about what happens for values out of the image range.