pilbox / tests /test_pilbox.py
John Ho
Add Crop Video tab: tracking-window video crop from a per-frame box JSON
8ebf17a
Raw
History Blame Contribute Delete
9.8 kB
"""Unit tests for the lite pilbox bounding-box annotation module."""
import base64
import io
import numpy as np
import pytest
from PIL import Image, ImageColor
import pilbox
def _b64_mask(mask_bool):
"""Encode a boolean numpy mask as a base64 PNG string (as stored in the JSON)."""
im = Image.fromarray(mask_bool.astype(np.uint8) * 255).convert("1")
buf = io.BytesIO()
im.save(buf, format="PNG")
return base64.b64encode(buf.getvalue()).decode("ascii")
def _sample_objects():
return [
{"object_id": 0, "boundingBox": {"x0": 10, "y0": 10, "x1": 40, "y1": 60}},
{"object_id": 1, "boundingBox": {"x0": 50, "y0": 20, "x1": 90, "y1": 80}},
]
def test_annotate_returns_same_size_copy_without_mutating_input():
im = Image.new("RGB", (100, 100), "white")
before = np.array(im).copy()
out = pilbox.annotate(im, _sample_objects())
assert isinstance(out, Image.Image)
assert out.size == im.size
# input is untouched
assert np.array_equal(np.array(im), before)
# output actually drew something (differs from a blank image)
assert not np.array_equal(np.array(out), before)
def test_color_for_is_stable_and_distinct():
mapping = {}
a1 = pilbox.color_for("a", mapping)
b = pilbox.color_for("b", mapping)
a2 = pilbox.color_for("a", mapping)
assert a1 == a2 # stable across calls
assert a1 != b # distinct keys -> distinct colors
assert a1.startswith("#") and len(a1) == 7
def test_palette_color_never_repeats_consecutively():
colors = [pilbox.palette_color(i) for i in range(50)]
assert all(c.startswith("#") and len(c) == 7 for c in colors)
# consecutive golden-angle hues are always far apart
assert all(colors[i] != colors[i + 1] for i in range(len(colors) - 1))
def test_im_draw_bbox_coerces_float_coords():
im = Image.new("RGB", (100, 100), "white")
before = np.array(im).copy()
# floats must not raise and must draw
pilbox.im_draw_bbox(im, 10.5, 10.9, 40.2, 60.7, color="red", caption="x")
assert not np.array_equal(np.array(im), before)
def test_annotate_shared_color_map_stable_across_calls():
# A shared color_map must give a key the SAME color regardless of the object
# order it appears in — the property that keeps a track id one color across
# video frames. object_id 1 appears first in call A and second in call B.
im = Image.new("RGB", (60, 60), "white")
shared = {}
a = [{"object_id": 1, "boundingBox": {"x0": 2, "y0": 2, "x1": 10, "y1": 10}}]
b = [
{"object_id": 0, "boundingBox": {"x0": 2, "y0": 2, "x1": 10, "y1": 10}},
{"object_id": 1, "boundingBox": {"x0": 20, "y0": 20, "x1": 30, "y1": 30}},
]
pilbox.annotate(im, a, color_map=shared)
pilbox.annotate(im, b, color_map=shared)
# object_id 1 was seen first -> palette index 0 in BOTH calls; 0 -> index 1.
assert shared[1] == 0
assert shared[0] == 1
def test_annotate_default_color_map_is_per_call():
# Without a shared map each call colors independently (backward compatible).
im = Image.new("RGB", (60, 60), "white")
objs = [{"object_id": 7, "boundingBox": {"x0": 2, "y0": 2, "x1": 10, "y1": 10}}]
out = pilbox.annotate(im, objs) # no color_map arg
assert out.size == (60, 60)
def test_annotate_custom_keys():
im = Image.new("RGB", (100, 100), "white")
objs = [{"cls": "player", "box": {"x0": 5, "y0": 5, "x1": 30, "y1": 30}}]
out = pilbox.annotate(im, objs, label_key="cls", color_key="cls", bbox_key="box")
assert out.size == (100, 100)
def test_letterbox_size_aspect_and_bars():
# a 100x50 (2:1) image fit into an 80x80 (1:1) canvas -> letterboxed
im = Image.new("RGB", (100, 50), (255, 255, 255))
out = pilbox.letterbox(im, 80, 80)
assert isinstance(out, Image.Image)
assert out.size == (80, 80) # exact target size
# width fills (80), height scales to 40 -> 20px black bars top and bottom
assert out.getpixel((40, 2)) == (0, 0, 0) # top bar black
assert out.getpixel((40, 78)) == (0, 0, 0) # bottom bar black
assert out.getpixel((40, 40)) == (255, 255, 255) # center holds the image
def test_letterbox_pillarbox_and_custom_fill():
# a 50x100 (tall) image into 80x80 -> pillarbox (side bars); custom fill color
im = Image.new("RGB", (50, 100), (255, 255, 255))
out = pilbox.letterbox(im, 80, 80, fill=(0, 0, 255))
assert out.size == (80, 80)
assert out.getpixel((2, 40)) == (0, 0, 255) # left bar is the fill color
assert out.getpixel((40, 40)) == (255, 255, 255) # center holds the image
def test_crop_returns_expected_region():
im = Image.new("RGB", (100, 100), "white")
# paint a red patch so we can confirm the crop grabs the right pixels
im.paste((255, 0, 0), (10, 20, 40, 80))
out = pilbox.crop(im, 10, 20, 40, 80)
assert isinstance(out, Image.Image)
assert out.size == (30, 60) # (x1 - x0, y1 - y0)
assert np.array_equal(np.array(out), np.full((60, 30, 3), (255, 0, 0), dtype=np.uint8))
def test_crop_does_not_mutate_input():
im = Image.new("RGB", (100, 100), "white")
before = np.array(im).copy()
out = pilbox.crop(im, 10, 20, 40, 80)
assert out is not im
assert np.array_equal(np.array(im), before)
def test_crop_rejects_invalid_box():
im = Image.new("RGB", (100, 100), "white")
with pytest.raises(ValueError):
pilbox.crop(im, 40, 20, 10, 80) # inverted x (x1 <= x0)
with pytest.raises(ValueError):
pilbox.crop(im, 10, 20, 40, 20) # empty y (y1 <= y0)
with pytest.raises(ValueError):
pilbox.crop(im, 10, 20, 200, 80) # x1 exceeds image width
def test_im_color_mask_blends_masked_pixels_only():
img = np.zeros((4, 4, 3), dtype=np.uint8) # black
mask = np.zeros((4, 4), dtype=bool)
mask[0, 0] = True
out = pilbox.im_color_mask(img, mask, rgb_tup=(255, 0, 0), alpha=0.5)
assert out.dtype == np.uint8 and out.shape == (4, 4, 3)
assert tuple(int(v) for v in out[0, 0]) == (127, 0, 0) # masked -> halfway to red
assert tuple(int(v) for v in out[1, 1]) == (0, 0, 0) # unmasked untouched
def test_im_color_mask_rejects_shape_mismatch():
img = np.zeros((4, 4, 3), dtype=np.uint8)
bad_mask = np.zeros((4, 2), dtype=bool) # same height, wrong width
with pytest.raises(ValueError):
pilbox.im_color_mask(img, bad_mask)
def test_annotate_mask_and_box_share_color():
im = Image.new("RGB", (40, 40), "white")
# object 1's mask covers a patch clear of every box outline
mask1 = np.zeros((40, 40), dtype=bool)
mask1[25:35, 25:35] = True
objs = [
{"object_id": 0, "boundingBox": {"x0": 2, "y0": 2, "x1": 10, "y1": 10}},
{
"object_id": 1,
"boundingBox": {"x0": 15, "y0": 15, "x1": 22, "y1": 22},
"b64_mask": _b64_mask(mask1),
},
]
out = pilbox.annotate(im, objs, mask_alpha=1.0)
arr = np.array(out)
# object 1's palette color (index 1, since it's the 2nd distinct object_id)
expected = ImageColor.getrgb(pilbox.palette_color(1))
# a pixel well inside object 1's mask region is filled with that exact color
assert tuple(int(v) for v in arr[30, 30]) == expected
def test_annotate_empty_mask_key_skips_masks():
im = Image.new("RGB", (40, 40), "white")
mask = np.ones((40, 40), dtype=bool)
objs = [
{
"object_id": 0,
"boundingBox": {"x0": 2, "y0": 2, "x1": 10, "y1": 10},
"b64_mask": _b64_mask(mask),
}
]
# a full-image mask would repaint the corners if drawn; with masks disabled
# the far corner stays white.
out = pilbox.annotate(im, objs, mask_key="", mask_alpha=1.0)
assert tuple(int(v) for v in np.array(out)[38, 38]) == (255, 255, 255)
def _fg_bg_image_and_mask():
"""A 4x4 solid image plus a center 2x2 foreground mask."""
img = np.zeros((4, 4, 3), dtype=np.uint8)
img[:, :] = (200, 100, 50)
mask = np.zeros((4, 4), dtype=bool)
mask[1:3, 1:3] = True
return img, mask
def test_im_apply_mask_color_background_keeps_foreground():
img, mask = _fg_bg_image_and_mask()
out = pilbox.im_apply_mask(img, mask, bg_rgb_tup=(0, 0, 0))
assert out.shape == (4, 4, 3)
assert tuple(int(v) for v in out[1, 1]) == (200, 100, 50) # foreground kept
assert tuple(int(v) for v in out[0, 0]) == (0, 0, 0) # background blacked out
def test_im_apply_mask_rejects_shape_mismatch():
img, mask = _fg_bg_image_and_mask()
with pytest.raises(ValueError):
pilbox.im_apply_mask(img, mask[:, :2], bg_rgb_tup=(0, 0, 0))
def test_im_apply_mask_bg_options_do_not_raise():
# regression guard: these paths use ImageFilter / ImageOps, which must be imported.
img, mask = _fg_bg_image_and_mask()
assert pilbox.im_apply_mask(img, mask, bg_blur_radius=2).shape == (4, 4, 3)
assert pilbox.im_apply_mask(img, mask, bg_greyscale=True).shape == (4, 4, 3)
assert pilbox.im_apply_mask(
img, mask, bg_rgb_tup=(0, 0, 0), mask_gblur_radius=2
).shape == (4, 4, 3)
# no bg_* option -> transparent (RGBA) result
assert pilbox.im_apply_mask(img, mask).shape == (4, 4, 4)
def test_apply_mask_wrapper_masks_background():
im = Image.new("RGB", (4, 4), (200, 100, 50))
_, mask = _fg_bg_image_and_mask()
out = pilbox.apply_mask(im, _b64_mask(mask), bg_rgb_tup=(0, 0, 0))
assert isinstance(out, Image.Image) and out.mode == "RGB"
assert out.getpixel((1, 1)) == (200, 100, 50) # foreground kept
assert out.getpixel((0, 0)) == (0, 0, 0) # background blacked out
def test_apply_mask_rejects_bad_base64():
im = Image.new("RGB", (4, 4), "white")
with pytest.raises(ValueError):
pilbox.apply_mask(im, "not-valid-base64-png!!", bg_rgb_tup=(0, 0, 0))