Skip to content

Commit 943825f

Browse files
committed
Add bitmapfilter.blend
This can perform arbitrary channel mixing between two images. Alpha blend & maximum functions are demonstrated in the test. However, it should make most of the usual photo editing blends possible. (for dissolve, fill a mask bitmap with random values, which may be expensive to do from circuitpython code; we can specifically accelerate it if we need to)
1 parent ebe8390 commit 943825f

File tree

6 files changed

+320
-2
lines changed

6 files changed

+320
-2
lines changed

shared-bindings/bitmapfilter/__init__.c

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,8 +608,131 @@ STATIC mp_obj_t bitmapfilter_false_color(size_t n_args, const mp_obj_t *pos_args
608608
shared_module_bitmapfilter_false_color(bitmap, mask, palette->colors);
609609
return args[ARG_bitmap].u_obj;
610610
}
611-
612611
MP_DEFINE_CONST_FUN_OBJ_KW(bitmapfilter_false_color_obj, 0, bitmapfilter_false_color);
612+
613+
#define BLEND_TABLE_SIZE (4096)
614+
STATIC uint8_t *get_blend_table(mp_obj_t lookup, int mode) {
615+
mp_buffer_info_t lookup_buf;
616+
if (!mp_get_buffer(lookup, &lookup_buf, mode) || lookup_buf.len != BLEND_TABLE_SIZE) {
617+
return NULL;
618+
}
619+
return lookup_buf.buf;
620+
}
621+
//|
622+
//| BlendFunction = Callable[[float, float], float]
623+
//| """A function used to blend two images"""
624+
//|
625+
//| BlendTable = bytearray
626+
//| """A precomputed blend table
627+
//|
628+
//| There is not actually a BlendTable type. The real type is actually any
629+
//| buffer 4096 bytes in length."""
630+
//|
631+
//| def blend_precompute(lookup: BlendFunction, table: BlendTable | None = None) -> BlendTable:
632+
//| """Precompute a BlendTable from a BlendFunction
633+
//|
634+
//| If the optional ``table`` argument is provided, an existing `BlendTable` is updated
635+
//| with the new function values.
636+
//|
637+
//| The function's two arguments will range from 0 to 1. The returned value should also range from 0 to 1.
638+
//|
639+
//| A function to do a 33% blend of each source image could look like this:
640+
//|
641+
//| .. code-block:: python
642+
//|
643+
//| def blend_one_third(a, b):
644+
//| return a * .33 + b * .67
645+
//| """
646+
//|
647+
STATIC mp_obj_t blend_precompute(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
648+
enum { ARG_lookup, ARG_table };
649+
static const mp_arg_t allowed_args[] = {
650+
{ MP_QSTR_lookup, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } },
651+
{ MP_QSTR_table, MP_ARG_OBJ, { .u_obj = MP_ROM_NONE } },
652+
};
653+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
654+
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
655+
656+
mp_obj_t table = args[ARG_table].u_obj;
657+
if (table == mp_const_none) {
658+
table = mp_obj_new_bytearray_of_zeros(BLEND_TABLE_SIZE);
659+
}
660+
uint8_t *buf = get_blend_table(table, MP_BUFFER_WRITE);
661+
if (!buf) {
662+
mp_raise_TypeError_varg(MP_ERROR_TEXT("%q must be of type %q or %q, not %q"),
663+
MP_QSTR_table, MP_QSTR_NoneType, MP_QSTR_WritableBuffer,
664+
mp_obj_get_type_qstr(table));
665+
}
666+
shared_module_bitmapfilter_blend_precompute(args[ARG_lookup].u_obj, buf);
667+
return table;
668+
}
669+
MP_DEFINE_CONST_FUN_OBJ_KW(bitmapfilter_blend_precompute_obj, 0, blend_precompute);
670+
671+
//|
672+
//| def blend(
673+
//| dest: Bitmap,
674+
//| src1: Bitmap,
675+
//| src2: Bitmap,
676+
//| lookup: BlendFunction | BlendTable,
677+
//| mask: Bitmap = None,
678+
//| ) -> Bitmap:
679+
//| """Blend the 'src1' and 'src2' images according to lookup function or table 'lookup'
680+
//|
681+
//| If ``lookup`` is a function, it is converted to a `BlendTable` by
682+
//| internally calling blend_precompute. If a blend function is used repeatedly
683+
//| it can be more efficient to compute it once with `blend_precompute`.
684+
//|
685+
//| If the mask is supplied, pixels from ``src1`` are taken unchanged in masked areas.
686+
//|
687+
//| The source and destination bitmaps may be the same bitmap.
688+
//|
689+
//| The destination bitmap is returned.
690+
//| """
691+
//|
692+
693+
STATIC mp_obj_t bitmapfilter_blend(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
694+
enum { ARG_dest, ARG_src1, ARG_src2, ARG_lookup, ARG_mask };
695+
static const mp_arg_t allowed_args[] = {
696+
{ MP_QSTR_dest, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } },
697+
{ MP_QSTR_src1, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } },
698+
{ MP_QSTR_src2, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } },
699+
{ MP_QSTR_lookup, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } },
700+
{ MP_QSTR_mask, MP_ARG_OBJ, { .u_obj = MP_ROM_NONE } },
701+
};
702+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
703+
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
704+
705+
mp_arg_validate_type(args[ARG_dest].u_obj, &displayio_bitmap_type, MP_QSTR_dest);
706+
displayio_bitmap_t *dest = MP_OBJ_TO_PTR(args[ARG_dest].u_obj);
707+
708+
mp_arg_validate_type(args[ARG_src1].u_obj, &displayio_bitmap_type, MP_QSTR_src1);
709+
displayio_bitmap_t *src1 = MP_OBJ_TO_PTR(args[ARG_src1].u_obj);
710+
711+
mp_arg_validate_type(args[ARG_src2].u_obj, &displayio_bitmap_type, MP_QSTR_src2);
712+
displayio_bitmap_t *src2 = MP_OBJ_TO_PTR(args[ARG_src2].u_obj);
713+
714+
mp_obj_t lookup = args[ARG_lookup].u_obj;
715+
if (mp_obj_is_callable(lookup)) {
716+
lookup = mp_call_function_1(MP_OBJ_FROM_PTR(&bitmapfilter_blend_precompute_obj), lookup);
717+
}
718+
uint8_t *lookup_buf = get_blend_table(lookup, MP_BUFFER_READ);
719+
if (!lookup_buf) {
720+
mp_raise_TypeError_varg(MP_ERROR_TEXT("%q must be of type %q or %q, not %q"),
721+
MP_QSTR_lookup, MP_QSTR_callable, MP_QSTR_ReadableBuffer,
722+
mp_obj_get_type_qstr(lookup));
723+
}
724+
725+
displayio_bitmap_t *mask = NULL;
726+
if (args[ARG_mask].u_obj != mp_const_none) {
727+
mp_arg_validate_type(args[ARG_mask].u_obj, &displayio_bitmap_type, MP_QSTR_mask);
728+
mask = MP_OBJ_TO_PTR(args[ARG_mask].u_obj);
729+
}
730+
731+
shared_module_bitmapfilter_blend(dest, src1, src2, mask, lookup_buf);
732+
return args[ARG_dest].u_obj;
733+
}
734+
MP_DEFINE_CONST_FUN_OBJ_KW(bitmapfilter_blend_obj, 0, bitmapfilter_blend);
735+
613736
STATIC const mp_rom_map_elem_t bitmapfilter_module_globals_table[] = {
614737
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_bitmapfilter) },
615738
{ MP_ROM_QSTR(MP_QSTR_morph), MP_ROM_PTR(&bitmapfilter_morph_obj) },
@@ -621,6 +744,8 @@ STATIC const mp_rom_map_elem_t bitmapfilter_module_globals_table[] = {
621744
{ MP_ROM_QSTR(MP_QSTR_ChannelScaleOffset), MP_ROM_PTR(&bitmapfilter_channel_scale_offset_type) },
622745
{ MP_ROM_QSTR(MP_QSTR_ChannelMixer), MP_ROM_PTR(&bitmapfilter_channel_mixer_type) },
623746
{ MP_ROM_QSTR(MP_QSTR_ChannelMixerOffset), MP_ROM_PTR(&bitmapfilter_channel_mixer_offset_type) },
747+
{ MP_ROM_QSTR(MP_QSTR_blend), MP_ROM_PTR(&bitmapfilter_blend_obj) },
748+
{ MP_ROM_QSTR(MP_QSTR_blend_precompute), MP_ROM_PTR(&bitmapfilter_blend_precompute_obj) },
624749
};
625750
STATIC MP_DEFINE_CONST_DICT(bitmapfilter_module_globals, bitmapfilter_module_globals_table);
626751

shared-bindings/bitmapfilter/__init__.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,12 @@ void shared_module_bitmapfilter_false_color(
7373
displayio_bitmap_t *bitmap,
7474
displayio_bitmap_t *mask,
7575
_displayio_color_t palette[256]);
76+
77+
void shared_module_bitmapfilter_blend_precompute(mp_obj_t fun, uint8_t lookup[4096]);
78+
79+
void shared_module_bitmapfilter_blend(
80+
displayio_bitmap_t *dest,
81+
displayio_bitmap_t *src1,
82+
displayio_bitmap_t *src2,
83+
displayio_bitmap_t *mask,
84+
const uint8_t lookup[4096]);

shared-module/bitmapfilter/__init__.c

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
#pragma GCC diagnostic ignored "-Wshadow"
3131

3232
static void check_matching_details(displayio_bitmap_t *b1, displayio_bitmap_t *b2) {
33-
if (b1->width != b2->width || b1->height != b2->height) {
33+
if (b1->width != b2->width || b1->height != b2->height || b1->bits_per_value != b2->bits_per_value) {
3434
mp_raise_ValueError(MP_ERROR_TEXT("bitmap size and depth must match"));
3535
}
3636
}
@@ -456,3 +456,64 @@ void shared_module_bitmapfilter_false_color(
456456
}
457457
}
458458
}
459+
460+
void shared_module_bitmapfilter_blend_precompute(mp_obj_t fun, uint8_t lookup[4096]) {
461+
uint8_t *ptr = lookup;
462+
for (int i = 0; i < 64; i++) {
463+
mp_obj_t fi = mp_obj_new_float(i * (1 / MICROPY_FLOAT_CONST(63.)));
464+
for (int j = 0; j < 64; j++) {
465+
mp_obj_t fj = mp_obj_new_float(j * (1 / MICROPY_FLOAT_CONST(63.)));
466+
mp_float_t res = mp_obj_get_float(mp_call_function_2(fun, fi, fj));
467+
*ptr++ = res < 0 ? 0 : res > 1 ? 1 : (uint8_t)MICROPY_FLOAT_C_FUN(round)(63 * res);
468+
}
469+
}
470+
}
471+
472+
#define FIVE_TO_SIX(x) ({ int tmp = (x); (tmp << 1) | (tmp & 1); })
473+
#define SIX_TO_FIVE(x) ((x) >> 1)
474+
475+
void shared_module_bitmapfilter_blend(
476+
displayio_bitmap_t *bitmap,
477+
displayio_bitmap_t *src1,
478+
displayio_bitmap_t *src2,
479+
displayio_bitmap_t *mask,
480+
const uint8_t lookup[4096]) {
481+
482+
check_matching_details(bitmap, src1);
483+
check_matching_details(bitmap, src2);
484+
485+
switch (bitmap->bits_per_value) {
486+
default:
487+
mp_raise_ValueError(MP_ERROR_TEXT("unsupported bitmap depth"));
488+
case 16: {
489+
for (int y = 0, yy = bitmap->height; y < yy; y++) {
490+
uint16_t *dest_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(bitmap, y);
491+
uint16_t *src1_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(src1, y);
492+
uint16_t *src2_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(src2, y);
493+
for (int x = 0, xx = bitmap->width; x < xx; x++) {
494+
int pixel1 = IMAGE_GET_RGB565_PIXEL_FAST(src1_ptr, x);
495+
if (mask && common_hal_displayio_bitmap_get_pixel(mask, x, y)) {
496+
IMAGE_PUT_RGB565_PIXEL_FAST(dest_ptr, x, pixel1);
497+
continue; // Short circuit.
498+
}
499+
int pixel2 = IMAGE_GET_RGB565_PIXEL_FAST(src2_ptr, x);
500+
501+
int r1 = FIVE_TO_SIX(COLOR_RGB565_TO_R5(pixel1));
502+
int r2 = FIVE_TO_SIX(COLOR_RGB565_TO_R5(pixel2));
503+
int r = SIX_TO_FIVE(lookup[r1 * 64 + r2]);
504+
505+
int g1 = COLOR_RGB565_TO_R5(pixel1);
506+
int g2 = COLOR_RGB565_TO_R5(pixel2);
507+
int g = lookup[g1 * 64 + g2];
508+
509+
int b1 = FIVE_TO_SIX(COLOR_RGB565_TO_B5(pixel1));
510+
int b2 = FIVE_TO_SIX(COLOR_RGB565_TO_B5(pixel2));
511+
int b = SIX_TO_FIVE(lookup[b1 * 64 + b2]);
512+
513+
int pixel = COLOR_R5_G6_B5_TO_RGB565(r, g, b);
514+
IMAGE_PUT_RGB565_PIXEL_FAST(dest_ptr, x, pixel);
515+
}
516+
}
517+
}
518+
}
519+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from displayio import Bitmap
2+
import bitmapfilter
3+
from dump_bitmap import dump_bitmap_rgb_swapped
4+
from blinka_image import decode_resource
5+
6+
7+
def test_pattern():
8+
return decode_resource("testpattern", 2)
9+
10+
11+
def blinka():
12+
return decode_resource("blinka_32x32", 0)
13+
14+
15+
def blendfunc(frac):
16+
nfrac = 1 - frac
17+
18+
def inner(x, y):
19+
return x * frac + y * nfrac
20+
21+
return inner
22+
23+
24+
def make_quadrant_bitmap():
25+
b = Bitmap(17, 17, 1)
26+
for i in range(b.height):
27+
for j in range(b.width):
28+
b[i, j] = (i < 8) ^ (j < 8)
29+
return b
30+
31+
32+
b = Bitmap(32, 32, 65535)
33+
print(test_pattern().width)
34+
print(blinka().width)
35+
print(b.width)
36+
print(test_pattern().height)
37+
print(blinka().height)
38+
print(b.height)
39+
40+
mask = make_quadrant_bitmap()
41+
blend_table = bitmapfilter.blend_precompute(blendfunc(0.1))
42+
bitmapfilter.blend(b, test_pattern(), blinka(), blend_table, mask)
43+
dump_bitmap_rgb_swapped(b)
44+
45+
bitmapfilter.blend(b, test_pattern(), blinka(), blendfunc(0.5), mask)
46+
dump_bitmap_rgb_swapped(b)
47+
48+
bitmapfilter.blend(b, test_pattern(), blinka(), max, mask)
49+
dump_bitmap_rgb_swapped(b)
50+
bitmapfilter.blend(b, test_pattern(), blinka(), min)
51+
dump_bitmap_rgb_swapped(b)

0 commit comments

Comments
 (0)