Skip to content

Commit 3641120

Browse files
committed
Add bitmapfilter.mix
This allows operations between channels in an image. It can be used for the following use cases: * Conversion to B&W or sepia * Adding color casts * Mixing or swapping arbitrary channels * Inverting or scaling arbitrary channels
1 parent 214ebc3 commit 3641120

11 files changed

Lines changed: 456 additions & 1 deletion

File tree

locale/circuitpython.pot

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4324,6 +4324,10 @@ msgstr ""
43244324
msgid "wbits"
43254325
msgstr ""
43264326

4327+
#: shared-bindings/bitmapfilter/__init__.c
4328+
msgid "weights must be a sequence of length 3, 9, or 12"
4329+
msgstr ""
4330+
43274331
#: shared-bindings/bitmapfilter/__init__.c
43284332
msgid ""
43294333
"weights must be a sequence with an odd square number of elements (usually 9 "

shared-bindings/bitmapfilter/__init__.c

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,101 @@ STATIC mp_obj_t bitmapfilter_morph(size_t n_args, const mp_obj_t *pos_args, mp_m
141141
args[ARG_threshold].u_bool, args[ARG_offset].u_bool, args[ARG_invert].u_bool);
142142
return mp_const_none;
143143
}
144-
145144
MP_DEFINE_CONST_FUN_OBJ_KW(bitmapfilter_morph_obj, 0, bitmapfilter_morph);
146145

146+
static mp_float_t float_subscr(mp_obj_t o, int i) {
147+
return mp_obj_get_float(mp_obj_subscr(o, MP_OBJ_NEW_SMALL_INT(i), MP_OBJ_SENTINEL));
148+
149+
}
150+
//| def mix(
151+
//| bitmap: displayio.Bitmap, weights: Sequence[int], mask: displayio.Bitmap | None = None
152+
//| ):
153+
//| """Perform a channel mixing operation on the bitmap
154+
//|
155+
//| The ``bitmap``, which must be in RGB565_SWAPPED format, is modified
156+
//| according to the ``weights``.
157+
//|
158+
//| If ``weights`` is a list of length 3, then each channel is scaled independently:
159+
//| The numbers are the red, green, and blue channel scales.
160+
//|
161+
//| If ``weights`` is a list of length 9, then channels are mixed. The first three
162+
//| numbers are the fraction of red, green and blue input channels mixed into the
163+
//| red output channel. The next 3 numbers are for green, and the final 3 are for blue.
164+
//|
165+
//| If ``weights`` is a list of length 12, then channels are mixed with an offset.
166+
//| Every fourth value is the offset value.
167+
//|
168+
//| For example, to perform a sepia conversion on an input image,
169+
//|
170+
//| .. code-block:: python
171+
//|
172+
//| sepia_weights = [
173+
//| .393, .769, .189,
174+
//| .349, .686, .168,
175+
//| .272, .534, .131]
176+
//|
177+
//| def sepia(bitmap):
178+
//| \"""Convert the bitmap to sepia\"""
179+
//| bitmapfilter.mix(bitmap, sepia_weights)
180+
//| """
181+
//|
182+
STATIC mp_obj_t bitmapfilter_mix(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
183+
enum { ARG_bitmap, ARG_weights, ARG_mask };
184+
static const mp_arg_t allowed_args[] = {
185+
{ MP_QSTR_bitmap, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } },
186+
{ MP_QSTR_weights, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } },
187+
{ MP_QSTR_mask, MP_ARG_OBJ, { .u_obj = MP_ROM_NONE } },
188+
};
189+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
190+
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
191+
192+
mp_arg_validate_type(args[ARG_bitmap].u_obj, &displayio_bitmap_type, MP_QSTR_bitmap);
193+
displayio_bitmap_t *bitmap = MP_OBJ_TO_PTR(args[ARG_bitmap].u_obj);
194+
195+
mp_float_t weights[12];
196+
memset(weights, 0, sizeof(weights));
197+
198+
mp_obj_t weights_obj = args[ARG_weights].u_obj;
199+
mp_int_t len = mp_obj_get_int(mp_obj_len(weights_obj));
200+
201+
switch (len) {
202+
case 3:
203+
for (int i = 0; i < 3; i++) {
204+
weights[5 * i] = float_subscr(weights_obj, i);
205+
}
206+
break;
207+
case 9:
208+
for (int i = 0; i < 9; i++) {
209+
weights[i + i / 3] = float_subscr(weights_obj, i);
210+
}
211+
break;
212+
case 12:
213+
for (int i = 0; i < 12; i++) {
214+
weights[i] = float_subscr(weights_obj, i);
215+
}
216+
break;
217+
default:
218+
mp_raise_ValueError(
219+
MP_ERROR_TEXT("weights must be a sequence of length 3, 9, or 12"));
220+
}
221+
222+
223+
displayio_bitmap_t *mask = NULL;
224+
if (args[ARG_mask].u_obj != mp_const_none) {
225+
mp_arg_validate_type(args[ARG_mask].u_obj, &displayio_bitmap_type, MP_QSTR_mask);
226+
mask = MP_OBJ_TO_PTR(args[ARG_mask].u_obj);
227+
}
228+
229+
shared_module_bitmapfilter_mix(bitmap, mask, weights);
230+
return mp_const_none;
231+
}
232+
233+
MP_DEFINE_CONST_FUN_OBJ_KW(bitmapfilter_mix_obj, 0, bitmapfilter_mix);
234+
147235
STATIC const mp_rom_map_elem_t bitmapfilter_module_globals_table[] = {
148236
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_bitmapfilter) },
149237
{ MP_ROM_QSTR(MP_QSTR_morph), MP_ROM_PTR(&bitmapfilter_morph_obj) },
238+
{ MP_ROM_QSTR(MP_QSTR_mix), MP_ROM_PTR(&bitmapfilter_mix_obj) },
150239
};
151240
STATIC MP_DEFINE_CONST_DICT(bitmapfilter_module_globals, bitmapfilter_module_globals_table);
152241

shared-bindings/bitmapfilter/__init__.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,8 @@ void shared_module_bitmapfilter_morph(
3838
bool threshold,
3939
int offset,
4040
bool invert);
41+
42+
void shared_module_bitmapfilter_mix(
43+
displayio_bitmap_t *bitmap,
44+
displayio_bitmap_t *mask,
45+
const mp_float_t weights[12]);

shared-module/bitmapfilter/__init__.c

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,3 +247,73 @@ void shared_module_bitmapfilter_morph(
247247
}
248248
}
249249
}
250+
251+
void shared_module_bitmapfilter_mix(
252+
displayio_bitmap_t *bitmap,
253+
displayio_bitmap_t *mask,
254+
const mp_float_t weights[12]) {
255+
256+
int wt[12];
257+
for (int i = 0; i < 12; i++) {
258+
// The different scale factors correct for G having 6 bits while R, G have 5
259+
// by doubling the scale for R/B->G and halving the scale for G->R/B.
260+
// As well, the final value in each row has to be scaled up by the
261+
// component's maxval.
262+
int scale =
263+
(i == 1 || i == 9) ? 32768 :
264+
(i == 4 || i == 6) ? 131072 :
265+
(i == 3 || i == 11) ? 65535 * COLOR_B5_MAX :
266+
(i == 7) ? 65535 * COLOR_G6_MAX :
267+
65536;
268+
wt[i] = (int32_t)MICROPY_FLOAT_C_FUN(round)(scale * weights[i]);
269+
}
270+
271+
check_matching_details(bitmap, bitmap);
272+
273+
switch (bitmap->bits_per_value) {
274+
default:
275+
mp_raise_ValueError(MP_ERROR_TEXT("unsupported bitmap depth"));
276+
case 16: {
277+
for (int y = 0, yy = bitmap->height; y < yy; y++) {
278+
uint16_t *row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(bitmap, y);
279+
for (int x = 0, xx = bitmap->width; x < xx; x++) {
280+
if (mask && common_hal_displayio_bitmap_get_pixel(mask, x, y)) {
281+
continue; // Short circuit.
282+
}
283+
int pixel = IMAGE_GET_RGB565_PIXEL_FAST(row_ptr, x);
284+
int32_t r_acc = 0, g_acc = 0, b_acc = 0;
285+
int r = COLOR_RGB565_TO_R5(pixel);
286+
int g = COLOR_RGB565_TO_G6(pixel);
287+
int b = COLOR_RGB565_TO_B5(pixel);
288+
r_acc = r * wt[0] + g * wt[1] + b * wt[2] + wt[3];
289+
r_acc >>= 16;
290+
if (r_acc < 0) {
291+
r_acc = 0;
292+
} else if (r_acc > COLOR_R5_MAX) {
293+
r_acc = COLOR_R5_MAX;
294+
}
295+
296+
g_acc = r * wt[4] + g * wt[5] + b * wt[6] + wt[7];
297+
g_acc >>= 16;
298+
if (g_acc < 0) {
299+
g_acc = 0;
300+
} else if (g_acc > COLOR_G6_MAX) {
301+
g_acc = COLOR_G6_MAX;
302+
}
303+
304+
b_acc = r * wt[8] + g * wt[9] + b * wt[10] + wt[11];
305+
b_acc >>= 16;
306+
if (b_acc < 0) {
307+
b_acc = 0;
308+
} else if (b_acc > COLOR_B5_MAX) {
309+
b_acc = COLOR_B5_MAX;
310+
}
311+
312+
pixel = COLOR_R5_G6_B5_TO_RGB565(r_acc, g_acc, b_acc);
313+
IMAGE_PUT_RGB565_PIXEL_FAST(row_ptr, x, pixel);
314+
}
315+
}
316+
break;
317+
}
318+
}
319+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from displayio import Bitmap
2+
import bitmapfilter
3+
import ulab
4+
from dump_bitmap import dump_bitmap_rgb_swapped
5+
from blinka_image import decode_blinka
6+
7+
8+
def make_quadrant_bitmap():
9+
b = Bitmap(17, 17, 1)
10+
for i in range(b.height):
11+
for j in range(b.width):
12+
b[i, j] = (i < 8) ^ (j < 8)
13+
return b
14+
15+
16+
q = make_quadrant_bitmap()
17+
b = decode_blinka(3)
18+
dump_bitmap_rgb_swapped(b)
19+
20+
sepia_weights = [0.393, 0.769, 0.189, 0.349, 0.686, 0.168, 0.272, 0.534, 0.131]
21+
22+
print("sepia")
23+
bitmapfilter.mix(b, sepia_weights)
24+
dump_bitmap_rgb_swapped(b)
25+
26+
# Red channel only
27+
print("red channel only (note: masked)")
28+
b = decode_blinka(3)
29+
bitmapfilter.mix(b, [1, 0, 0], mask=q)
30+
dump_bitmap_rgb_swapped(b)
31+
32+
# Scale green channel
33+
print("scale green channel (note: masked)")
34+
b = decode_blinka(3)
35+
bitmapfilter.mix(b, [1, 2, 0], mask=q)
36+
dump_bitmap_rgb_swapped(b)
37+
38+
# Swap R & G channels, invert B channel
39+
print("swap R&G, invert B")
40+
b = decode_blinka(3)
41+
bitmapfilter.mix(b, [0, 1, 0, 0, 1, 0, 0, 0, 0, 0, -1, 1])
42+
dump_bitmap_rgb_swapped(b)

0 commit comments

Comments
 (0)