mirror of
https://github.com/blender/blender
synced 2026-09-29 04:37:17 +03:00
PyAPI: add ImBuf buffer methods & context manager for pixel data access
Provide buffer access to pixel data, as well as methods to ensure/clear float/byte buffers as previously it wasn't possible to create new float images. - ImBuf.with_buffer(): context manager yielding a memoryview of pixel data (byte or float, with optional region and write support). - ImBuf.ensure_buffer(), has_buffer(), clear_buffer(): manage byte/float pixel buffers. - Export blocking: resize/crop/free raise BufferError while pixel buffers are active. - PyC_ParseRectI, PyC_ParseOptionalRectI: reusable O& converters for rcti argument parsing. - Additional tests. Ref !155734
This commit is contained in:
parent
0cc55f2c9b
commit
e0f91aed38
4 changed files with 856 additions and 0 deletions
|
|
@ -19,6 +19,14 @@ from imbuf.types import ImBuf
|
|||
|
||||
DEFAULT_SIZE = 32, 32
|
||||
|
||||
# Test color in linear float, sRGB byte, and the expected conversions between them.
|
||||
COLOR_FLOAT = (1.0, 0.5, 0.25, 1.0)
|
||||
COLOR_BYTE = (255, 128, 196, 255)
|
||||
# sRGB byte -> linear float.
|
||||
COLOR_BYTE_AS_FLOAT = (1.0, 0.2159, 0.5520, 1.0)
|
||||
# Linear float -> sRGB byte.
|
||||
COLOR_FLOAT_AS_BYTE = (255, 188, 137, 255)
|
||||
|
||||
# NOTE: an invalid file format & missing files is aborting on the build-bot.
|
||||
# Disable as it makes tests fail, although we could consider demoting this to a warning
|
||||
# since it prevents valid code-paths from being tested.
|
||||
|
|
@ -407,6 +415,290 @@ class TestImBufPlanes(unittest.TestCase):
|
|||
imbuf.new((2, 2), planes=p)
|
||||
|
||||
|
||||
class TestImBufPixelsBytes(unittest.TestCase):
|
||||
|
||||
def test_read_only_by_default(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
with ibuf.with_buffer('BYTE') as buf:
|
||||
self.assertEqual(buf.readonly, True)
|
||||
with self.assertRaises(TypeError):
|
||||
buf[0] = 0xFF
|
||||
ibuf.free()
|
||||
|
||||
def test_write_mode(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
with ibuf.with_buffer('BYTE', write=True) as buf:
|
||||
self.assertEqual(buf.readonly, False)
|
||||
buf[0] = 42
|
||||
self.assertEqual(buf[0], 42)
|
||||
ibuf.free()
|
||||
|
||||
def test_buffer_length(self):
|
||||
w, h = 8, 6
|
||||
ibuf = imbuf.new((w, h))
|
||||
with ibuf.with_buffer('BYTE') as buf:
|
||||
self.assertEqual(len(buf), w * h * 4)
|
||||
ibuf.free()
|
||||
|
||||
def test_freed_image(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ibuf.free()
|
||||
with self.assertRaises(ReferenceError):
|
||||
ibuf.with_buffer('BYTE')
|
||||
|
||||
def test_exports_block_resize(self):
|
||||
ibuf = imbuf.new((4, 4))
|
||||
with ibuf.with_buffer('BYTE') as buf:
|
||||
with self.assertRaises(BufferError):
|
||||
ibuf.resize((2, 2))
|
||||
# After context exit, resize works.
|
||||
ibuf.resize((2, 2))
|
||||
self.assertEqual(ibuf.size, (2, 2))
|
||||
ibuf.free()
|
||||
|
||||
def test_exports_block_crop(self):
|
||||
ibuf = imbuf.new((4, 4))
|
||||
with ibuf.with_buffer('BYTE') as buf:
|
||||
with self.assertRaises(BufferError):
|
||||
ibuf.crop((0, 0), (1, 1))
|
||||
ibuf.free()
|
||||
|
||||
def test_exports_block_free(self):
|
||||
ibuf = imbuf.new((4, 4))
|
||||
with ibuf.with_buffer('BYTE') as buf:
|
||||
with self.assertRaises(BufferError):
|
||||
ibuf.free()
|
||||
ibuf.free()
|
||||
|
||||
def test_double_enter_blocked(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ctx = ibuf.with_buffer('BYTE')
|
||||
ctx.__enter__()
|
||||
with self.assertRaises(RuntimeError):
|
||||
ctx.__enter__()
|
||||
ctx.__exit__(None, None, None)
|
||||
ibuf.free()
|
||||
|
||||
def test_reentry_after_exit(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ctx = ibuf.with_buffer('BYTE', write=True)
|
||||
buf1 = ctx.__enter__()
|
||||
buf1[0] = 10
|
||||
ctx.__exit__(None, None, None)
|
||||
buf2 = ctx.__enter__()
|
||||
self.assertEqual(buf2[0], 10)
|
||||
ctx.__exit__(None, None, None)
|
||||
ibuf.free()
|
||||
|
||||
|
||||
class TestImBufPixelsRegion(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.ibuf = imbuf.new((8, 6))
|
||||
with self.ibuf.with_buffer('BYTE', write=True) as buf:
|
||||
for i in range(8 * 6):
|
||||
buf[i * 4] = i % 256
|
||||
|
||||
def tearDown(self):
|
||||
self.ibuf.free()
|
||||
|
||||
def test_full_image_1d(self):
|
||||
with self.ibuf.with_buffer('BYTE') as buf:
|
||||
self.assertEqual(buf.ndim, 1)
|
||||
self.assertEqual(len(buf), 8 * 6 * 4)
|
||||
|
||||
def test_region_none_is_1d(self):
|
||||
with self.ibuf.with_buffer('BYTE', region=None) as buf:
|
||||
self.assertEqual(buf.ndim, 1)
|
||||
|
||||
def test_sub_region_2d(self):
|
||||
with self.ibuf.with_buffer('BYTE', region=((2, 1), (6, 4))) as buf:
|
||||
self.assertEqual(buf.ndim, 2)
|
||||
self.assertEqual(buf.shape, (3, 16))
|
||||
|
||||
def test_region_pixel_values(self):
|
||||
with self.ibuf.with_buffer('BYTE', region=((2, 1), (6, 4))) as buf:
|
||||
# Pixel (2, 1) has index 1*8+2 = 10.
|
||||
self.assertEqual(buf[0, 0], 10)
|
||||
# Pixel (2, 2) has index 2*8+2 = 18.
|
||||
self.assertEqual(buf[1, 0], 18)
|
||||
|
||||
def test_full_image_region_collapses_to_1d(self):
|
||||
with self.ibuf.with_buffer('BYTE', region=((0, 0), (8, 6))) as buf:
|
||||
self.assertEqual(buf.ndim, 1)
|
||||
|
||||
def test_oversized_region_collapses_to_1d(self):
|
||||
with self.ibuf.with_buffer('BYTE', region=((-5, -5), (100, 100))) as buf:
|
||||
self.assertEqual(buf.ndim, 1)
|
||||
|
||||
def test_zero_area_region(self):
|
||||
# Fully outside image bounds.
|
||||
with self.ibuf.with_buffer('BYTE', region=((100, 100), (200, 200))) as buf:
|
||||
self.assertEqual(buf.ndim, 2)
|
||||
self.assertEqual(buf.shape[0] * buf.shape[1], 0)
|
||||
|
||||
# Start == end (zero-size).
|
||||
with self.ibuf.with_buffer('BYTE', region=((3, 3), (3, 3))) as buf:
|
||||
self.assertEqual(buf.shape[0] * buf.shape[1], 0)
|
||||
|
||||
def test_inverted_region_sanitized(self):
|
||||
# Inverted region is sanitized (min/max swapped), not treated as empty.
|
||||
with self.ibuf.with_buffer('BYTE', region=((5, 5), (2, 2))) as buf:
|
||||
self.assertEqual(buf.ndim, 2)
|
||||
self.assertEqual(buf.shape, (3, 12))
|
||||
|
||||
def test_region_write_does_not_corrupt_neighbors(self):
|
||||
with self.ibuf.with_buffer('BYTE', write=True, region=((1, 1), (3, 3))) as buf:
|
||||
for r in range(buf.shape[0]):
|
||||
for c in range(buf.shape[1]):
|
||||
buf[r, c] = 0xFF
|
||||
with self.ibuf.with_buffer('BYTE') as buf:
|
||||
self.assertEqual(buf[0], 0)
|
||||
inside = (1 * 8 + 1) * 4
|
||||
self.assertEqual(buf[inside], 0xFF)
|
||||
|
||||
|
||||
class TestImBufPixelsFloat(unittest.TestCase):
|
||||
|
||||
def test_no_float_buffer_error(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
with self.assertRaises(RuntimeError):
|
||||
ibuf.with_buffer('FLOAT')
|
||||
ibuf.free()
|
||||
|
||||
def test_ensure_and_access(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
with ibuf.with_buffer('FLOAT') as buf:
|
||||
self.assertEqual(buf.format, "f")
|
||||
ibuf.free()
|
||||
|
||||
def test_float_write(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
with ibuf.with_buffer('FLOAT', write=True) as buf:
|
||||
buf[0] = 0.5
|
||||
self.assertAlmostEqual(buf[0], 0.5, places=5)
|
||||
ibuf.free()
|
||||
|
||||
|
||||
class TestImBufEnsureBuffers(unittest.TestCase):
|
||||
|
||||
def test_ensure_float_from_byte(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
with ibuf.with_buffer('BYTE', write=True) as buf:
|
||||
buf[0:4] = bytes(COLOR_BYTE)
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
with ibuf.with_buffer('FLOAT') as buf:
|
||||
for i, expected in enumerate(COLOR_BYTE_AS_FLOAT):
|
||||
self.assertAlmostEqual(buf[i], expected, places=3)
|
||||
ibuf.free()
|
||||
|
||||
def test_ensure_byte_from_float(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
with ibuf.with_buffer('FLOAT', write=True) as buf:
|
||||
for i, v in enumerate(COLOR_FLOAT):
|
||||
buf[i] = v
|
||||
ibuf.clear_buffer('BYTE')
|
||||
self.assertFalse(ibuf.has_buffer('BYTE'))
|
||||
ibuf.ensure_buffer('BYTE')
|
||||
self.assertTrue(ibuf.has_buffer('BYTE'))
|
||||
with ibuf.with_buffer('BYTE') as buf:
|
||||
for i, expected in enumerate(COLOR_FLOAT_AS_BYTE):
|
||||
self.assertEqual(buf[i], expected)
|
||||
ibuf.free()
|
||||
|
||||
def test_ensure_noop_when_present(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ibuf.ensure_buffer('BYTE')
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
ibuf.free()
|
||||
|
||||
def test_ensure_blocked_while_exported(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
with ibuf.with_buffer('BYTE') as buf:
|
||||
with self.assertRaises(BufferError):
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
ibuf.free()
|
||||
|
||||
|
||||
class TestImBufHasAndClearBuffer(unittest.TestCase):
|
||||
|
||||
def test_has_buffer_byte(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
self.assertTrue(ibuf.has_buffer('BYTE'))
|
||||
self.assertFalse(ibuf.has_buffer('FLOAT'))
|
||||
ibuf.free()
|
||||
|
||||
def test_clear_buffer_byte(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
self.assertTrue(ibuf.has_buffer('BYTE'))
|
||||
ibuf.clear_buffer('BYTE')
|
||||
self.assertFalse(ibuf.has_buffer('BYTE'))
|
||||
ibuf.free()
|
||||
|
||||
def test_clear_buffer_float(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
self.assertTrue(ibuf.has_buffer('FLOAT'))
|
||||
ibuf.clear_buffer('FLOAT')
|
||||
self.assertFalse(ibuf.has_buffer('FLOAT'))
|
||||
ibuf.free()
|
||||
|
||||
def test_clear_buffer_blocked_while_exported(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
with ibuf.with_buffer('BYTE') as buf:
|
||||
with self.assertRaises(BufferError):
|
||||
ibuf.clear_buffer('BYTE')
|
||||
ibuf.free()
|
||||
|
||||
def test_clear_already_absent(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ibuf.clear_buffer('FLOAT')
|
||||
ibuf.free()
|
||||
|
||||
|
||||
class TestImBufExitSync(unittest.TestCase):
|
||||
|
||||
def test_byte_write_syncs_to_float(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
with ibuf.with_buffer('BYTE', write=True) as buf:
|
||||
for i, v in enumerate(COLOR_BYTE):
|
||||
buf[i] = v
|
||||
with ibuf.with_buffer('FLOAT') as buf:
|
||||
for i, expected in enumerate(COLOR_BYTE_AS_FLOAT):
|
||||
self.assertAlmostEqual(buf[i], expected, places=3)
|
||||
ibuf.free()
|
||||
|
||||
def test_float_write_syncs_to_byte(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
with ibuf.with_buffer('FLOAT', write=True) as buf:
|
||||
for i, v in enumerate(COLOR_FLOAT):
|
||||
buf[i] = v
|
||||
with ibuf.with_buffer('BYTE') as buf:
|
||||
for i, expected in enumerate(COLOR_FLOAT_AS_BYTE):
|
||||
self.assertEqual(buf[i], expected)
|
||||
ibuf.free()
|
||||
|
||||
def test_read_only_does_not_sync(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
with ibuf.with_buffer('FLOAT', write=True) as buf:
|
||||
for i, v in enumerate(COLOR_FLOAT):
|
||||
buf[i] = v
|
||||
# Read-only byte access should NOT overwrite the float buffer.
|
||||
with ibuf.with_buffer('BYTE') as buf:
|
||||
_ = buf[0]
|
||||
with ibuf.with_buffer('FLOAT') as buf:
|
||||
for i, expected in enumerate(COLOR_FLOAT):
|
||||
self.assertAlmostEqual(buf[i], expected, places=3)
|
||||
ibuf.free()
|
||||
|
||||
|
||||
def main():
|
||||
import sys
|
||||
sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue