Skip to content

Commit 6e553e7

Browse files
committed
Add, test morph9
morph9 is a form of morph which performs 9 different convolutions, like a version of mix where each coefficient is a (2n+1)x(2n+1) matrix. Most use cases are covered by morph-then-mix, but some advanced operations may be more efficient to implement via morph9.
1 parent 902a36d commit 6e553e7

6 files changed

Lines changed: 673 additions & 3 deletions

File tree

locale/circuitpython.pot

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4328,6 +4328,12 @@ msgstr ""
43284328
msgid "weights must be a sequence of length 3, 6, 9, or 12"
43294329
msgstr ""
43304330

4331+
#: shared-bindings/bitmapfilter/__init__.c
4332+
msgid ""
4333+
"weights must be a sequence with 9 times an odd square number of elements "
4334+
"(usually 81 or 225)"
4335+
msgstr ""
4336+
43314337
#: shared-bindings/bitmapfilter/__init__.c
43324338
msgid ""
43334339
"weights must be a sequence with an odd square number of elements (usually 9 "

shared-bindings/bitmapfilter/__init__.c

Lines changed: 152 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
//| def morph(
3636
//| bitmap: displayio.Bitmap,
3737
//| weights: Sequence[int],
38-
//| mul: float = 1.0,
38+
//| mul: float | None = None,
3939
//| add: float = 0,
4040
//| mask: displayio.Bitmap | None = None,
4141
//| threshold=False,
@@ -89,6 +89,11 @@
8989
//| """
9090
//|
9191

92+
93+
STATIC mp_float_t get_m(mp_obj_t mul_obj, mp_float_t sum) {
94+
return mul_obj != mp_const_none ? mp_obj_get_float(mul_obj) : sum ? 1 / (mp_float_t)sum : 1;
95+
}
96+
9297
STATIC mp_obj_t bitmapfilter_morph(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
9398
enum { ARG_bitmap, ARG_weights, ARG_mul, ARG_add, ARG_threshold, ARG_offset, ARG_invert, ARG_mask };
9499
static const mp_arg_t allowed_args[] = {
@@ -136,18 +141,161 @@ STATIC mp_obj_t bitmapfilter_morph(size_t n_args, const mp_obj_t *pos_args, mp_m
136141
weight_sum += j;
137142
}
138143

139-
mp_float_t m = args[ARG_mul].u_obj != mp_const_none ? mp_obj_get_float(args[ARG_mul].u_obj) : 1 / (mp_float_t)weight_sum;
144+
mp_float_t m = get_m(args[ARG_mul].u_obj, weight_sum);
140145

141146
shared_module_bitmapfilter_morph(bitmap, mask, sq_n_weights / 2, iweights, m, b,
142147
args[ARG_threshold].u_bool, args[ARG_offset].u_bool, args[ARG_invert].u_bool);
143148
return args[ARG_bitmap].u_obj;
144149
}
145150
MP_DEFINE_CONST_FUN_OBJ_KW(bitmapfilter_morph_obj, 0, bitmapfilter_morph);
146151

152+
//|
153+
//| def morph9(
154+
//| bitmap: displayio.Bitmap,
155+
//| weights: Sequence[int],
156+
//| mul: Sequence[float] | None,
157+
//| add: Sequence[float] | None,
158+
//| mask: displayio.Bitmap | None = None,
159+
//| threshold=False,
160+
//| offset: int = 0,
161+
//| invert: bool = False,
162+
//| ) -> displayio.Bitmap:
163+
//| """Convolve an image with a kernel
164+
//|
165+
//| This is like a combination of 9 calls to morph plus one call to mix. It's
166+
//| so complicated and hard to explain that it must be good for something.
167+
//|
168+
//| The ``bitmap``, which must be in RGB565_SWAPPED format, is modified
169+
//| according to the ``weights``. Then a scaling factor ``m`` and an
170+
//| offset factor ``b`` are applied.
171+
//|
172+
//| The ``weights`` must be a tuple of integers. The length of the tuple
173+
//| must be 9 times the square of an odd number, such as 81 or 225. The weights
174+
//| are taken in groups of 9, with the first 3 giving the proportion of each of
175+
//| R, G, and B that are mixed into the output R, the next three the
176+
//| proportions mixed into the output G, and the last 3 the proportions that
177+
//| are mixed into the output blue.
178+
//|
179+
//| ``mul`` is a sequence of 3 numbers to multiply the convolution pixel
180+
//| results by. When not set it defaults to a value that will prevent scaling
181+
//| in the convolution output.
182+
//|
183+
//| ``add`` is a sequence of 3 numbers giving a value to add to each
184+
//| convolution pixel result. If unspecified or None, 0 is used for all 3 channels.
185+
//|
186+
//| ``mul`` basically allows you to do a global contrast adjustment and
187+
//| add allows you to do a global brightness adjustment. Pixels that go
188+
//| outside of the image mins and maxes for color channels will be
189+
//| clipped.
190+
//|
191+
//| If you’d like to adaptive threshold the image on the output of the
192+
//| filter you can pass ``threshold=True`` which will enable adaptive
193+
//| thresholding of the image which sets pixels to one or zero based on a
194+
//| pixel’s brightness in relation to the brightness of the kernel of pixels
195+
//| around them. A negative ``offset`` value sets more pixels to 1 as you make
196+
//| it more negative while a positive value only sets the sharpest contrast
197+
//| changes to 1. Set ``invert`` to invert the binary image resulting output.
198+
//|
199+
//| ``mask`` is another image to use as a pixel level mask for the operation.
200+
//| The mask should be an image the same size as the image being operated on.
201+
//| Only pixels set to a non-zero value in the mask are modified.
202+
//|
203+
//| .. code-block:: python
204+
//|
205+
//| kernel_gauss_3 = [
206+
//| 1, 2, 1,
207+
//| 2, 4, 2,
208+
//| 1, 2, 1]
209+
//|
210+
//| def blur(bitmap):
211+
//| \"""Blur the bitmap with a 3x3 gaussian kernel\"""
212+
//| bitmapfilter.morph(bitmap, kernel_gauss_3, 1/sum(kernel_gauss_3))
213+
//| """
214+
//|
215+
216+
static mp_obj_t subscr(mp_obj_t o, int i) {
217+
return mp_obj_subscr(o, MP_OBJ_NEW_SMALL_INT(i), MP_OBJ_SENTINEL);
218+
}
219+
220+
static mp_obj_t subscr_maybe(mp_obj_t o, int i) {
221+
return o == mp_const_none ? o : subscr(o, i);
222+
}
223+
147224
static mp_float_t float_subscr(mp_obj_t o, int i) {
148-
return mp_obj_get_float(mp_obj_subscr(o, MP_OBJ_NEW_SMALL_INT(i), MP_OBJ_SENTINEL));
225+
return mp_obj_get_float(subscr(o, i));
226+
}
227+
228+
STATIC mp_float_t float_subscr_maybe(mp_obj_t seq_maybe, int i, mp_float_t defval) {
229+
if (seq_maybe == mp_const_none) {
230+
return defval;
231+
}
232+
return float_subscr(seq_maybe, i);
233+
}
234+
235+
STATIC mp_obj_t bitmapfilter_morph9(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
236+
enum { ARG_bitmap, ARG_weights, ARG_mul, ARG_add, ARG_threshold, ARG_offset, ARG_invert, ARG_mask };
237+
static const mp_arg_t allowed_args[] = {
238+
{ MP_QSTR_bitmap, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } },
239+
{ MP_QSTR_weights, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } },
240+
{ MP_QSTR_mul, MP_ARG_OBJ, { .u_obj = MP_ROM_NONE } },
241+
{ MP_QSTR_add, MP_ARG_OBJ, { .u_obj = MP_ROM_NONE } },
242+
{ MP_QSTR_threshold, MP_ARG_BOOL, { .u_bool = false } },
243+
{ MP_QSTR_offset, MP_ARG_INT, { .u_int = 0 } },
244+
{ MP_QSTR_invert, MP_ARG_BOOL, { .u_bool = false } },
245+
{ MP_QSTR_mask, MP_ARG_OBJ, { .u_obj = MP_ROM_NONE } },
246+
};
247+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
248+
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
249+
250+
mp_arg_validate_type(args[ARG_bitmap].u_obj, &displayio_bitmap_type, MP_QSTR_bitmap);
251+
displayio_bitmap_t *bitmap = args[ARG_bitmap].u_obj;
252+
253+
displayio_bitmap_t *mask = NULL; // the mask bitmap
254+
if (args[ARG_mask].u_obj != mp_const_none) {
255+
mp_arg_validate_type(args[ARG_mask].u_obj, &displayio_bitmap_type, MP_QSTR_mask);
256+
mask = MP_OBJ_TO_PTR(args[ARG_mask].u_obj);
257+
}
258+
259+
mp_float_t b[3] = {
260+
float_subscr_maybe(args[ARG_add].u_obj, 0, 0),
261+
float_subscr_maybe(args[ARG_add].u_obj, 1, 0),
262+
float_subscr_maybe(args[ARG_add].u_obj, 2, 0),
263+
};
264+
265+
mp_obj_t weights = args[ARG_weights].u_obj;
266+
mp_obj_t obj_len = mp_obj_len(weights);
267+
if (obj_len == MP_OBJ_NULL || !mp_obj_is_small_int(obj_len)) {
268+
mp_raise_ValueError_varg(MP_ERROR_TEXT("%q must be of type %q, not %q"), MP_QSTR_weights, MP_QSTR_Sequence, mp_obj_get_type(weights)->name);
269+
}
270+
271+
size_t n_weights = MP_OBJ_SMALL_INT_VALUE(obj_len);
149272

273+
size_t sq_n_weights = (int)MICROPY_FLOAT_C_FUN(sqrt)(n_weights / 9);
274+
if (sq_n_weights % 2 == 0 || sq_n_weights * sq_n_weights * 9 != n_weights) {
275+
mp_raise_ValueError(MP_ERROR_TEXT("weights must be a sequence with 9 times an odd square number of elements (usually 81 or 225)"));
276+
}
277+
278+
int iweights[n_weights];
279+
int weight_sum[3] = {0, 0, 0};
280+
for (size_t i = 0; i < n_weights; i++) {
281+
mp_int_t j = mp_obj_get_int(mp_obj_subscr(weights, MP_OBJ_NEW_SMALL_INT(i), MP_OBJ_SENTINEL));
282+
iweights[i] = j;
283+
int target_channel = (i / 3) % 3;
284+
weight_sum[target_channel] += j;
285+
}
286+
287+
mp_float_t m[3] = {
288+
get_m(subscr_maybe(args[ARG_mul].u_obj, 0), weight_sum[0]),
289+
get_m(subscr_maybe(args[ARG_mul].u_obj, 1), weight_sum[1]),
290+
get_m(subscr_maybe(args[ARG_mul].u_obj, 2), weight_sum[2])
291+
};
292+
293+
shared_module_bitmapfilter_morph9(bitmap, mask, sq_n_weights / 2, iweights, m, b,
294+
args[ARG_threshold].u_bool, args[ARG_offset].u_bool, args[ARG_invert].u_bool);
295+
return args[ARG_bitmap].u_obj;
150296
}
297+
MP_DEFINE_CONST_FUN_OBJ_KW(bitmapfilter_morph9_obj, 0, bitmapfilter_morph9);
298+
151299
//| def mix(
152300
//| bitmap: displayio.Bitmap, weights: Sequence[int], mask: displayio.Bitmap | None = None
153301
//| ) -> displayio.Bitmap:
@@ -409,6 +557,7 @@ MP_DEFINE_CONST_FUN_OBJ_KW(bitmapfilter_false_color_obj, 0, bitmapfilter_false_c
409557
STATIC const mp_rom_map_elem_t bitmapfilter_module_globals_table[] = {
410558
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_bitmapfilter) },
411559
{ MP_ROM_QSTR(MP_QSTR_morph), MP_ROM_PTR(&bitmapfilter_morph_obj) },
560+
{ MP_ROM_QSTR(MP_QSTR_morph9), MP_ROM_PTR(&bitmapfilter_morph9_obj) },
412561
{ MP_ROM_QSTR(MP_QSTR_mix), MP_ROM_PTR(&bitmapfilter_mix_obj) },
413562
{ MP_ROM_QSTR(MP_QSTR_solarize), MP_ROM_PTR(&bitmapfilter_solarize_obj) },
414563
{ MP_ROM_QSTR(MP_QSTR_false_color), MP_ROM_PTR(&bitmapfilter_false_color_obj) },

shared-bindings/bitmapfilter/__init__.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ void shared_module_bitmapfilter_morph(
3939
int offset,
4040
bool invert);
4141

42+
void shared_module_bitmapfilter_morph9(
43+
displayio_bitmap_t *bitmap,
44+
displayio_bitmap_t *mask,
45+
const int ksize,
46+
const int *krn,
47+
const mp_float_t m[3],
48+
const mp_float_t b[3],
49+
bool threshold,
50+
int offset,
51+
bool invert);
52+
4253
void shared_module_bitmapfilter_mix(
4354
displayio_bitmap_t *bitmap,
4455
displayio_bitmap_t *mask,

shared-module/bitmapfilter/__init__.c

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,142 @@ STATIC uint16_t imlib_yuv_to_rgb(uint8_t y, int8_t u, int8_t v) {
177177
#define COLOR_YUV_TO_RGB565(y, u, v) imlib_yuv_to_rgb((y), u, v)
178178

179179

180+
void shared_module_bitmapfilter_morph9(
181+
displayio_bitmap_t *bitmap,
182+
displayio_bitmap_t *mask,
183+
const int ksize,
184+
const int *krn,
185+
const mp_float_t m[3],
186+
const mp_float_t b[3],
187+
bool threshold,
188+
int offset,
189+
bool invert) {
190+
191+
int brows = ksize + 1;
192+
193+
const int32_t m_int[3] = {
194+
(int32_t)MICROPY_FLOAT_C_FUN(round)(65536 * m[0]),
195+
(int32_t)MICROPY_FLOAT_C_FUN(round)(65536 * m[1]),
196+
(int32_t)MICROPY_FLOAT_C_FUN(round)(65536 * m[2])
197+
};
198+
const int32_t b_int[3] = {
199+
(int32_t)MICROPY_FLOAT_C_FUN(round)(65536 * COLOR_G6_MAX * b[0]),
200+
(int32_t)MICROPY_FLOAT_C_FUN(round)(2 * 65536 * COLOR_G6_MAX * b[1]),
201+
(int32_t)MICROPY_FLOAT_C_FUN(round)(65536 * COLOR_G6_MAX * b[2])
202+
};
203+
204+
check_matching_details(bitmap, bitmap);
205+
206+
switch (bitmap->bits_per_value) {
207+
default:
208+
mp_raise_ValueError(MP_ERROR_TEXT("unsupported bitmap depth"));
209+
case 16: {
210+
displayio_bitmap_t buf;
211+
scratch_bitmap16(&buf, brows, bitmap->width);
212+
213+
for (int y = 0, yy = bitmap->height; y < yy; y++) {
214+
uint16_t *row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(bitmap, y);
215+
uint16_t *buf_row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(&buf, (y % brows));
216+
217+
for (int x = 0, xx = bitmap->width; x < xx; x++) {
218+
if (mask && common_hal_displayio_bitmap_get_pixel(mask, x, y)) {
219+
IMAGE_PUT_RGB565_PIXEL_FAST(buf_row_ptr, x, IMAGE_GET_RGB565_PIXEL_FAST(row_ptr, x));
220+
continue; // Short circuit.
221+
222+
}
223+
int32_t r_acc = 0, g_acc = 0, b_acc = 0, ptr = 0;
224+
225+
if (x >= ksize && x < bitmap->width - ksize && y >= ksize && y < bitmap->height - ksize) {
226+
for (int j = -ksize; j <= ksize; j++) {
227+
uint16_t *k_row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(bitmap, y + j);
228+
for (int k = -ksize; k <= ksize; k++) {
229+
int pixel = IMAGE_GET_RGB565_PIXEL_FAST(k_row_ptr, x + k);
230+
int r = COLOR_RGB565_TO_R5(pixel);
231+
int g = COLOR_RGB565_TO_G6(pixel);
232+
int b = COLOR_RGB565_TO_B5(pixel);
233+
r_acc += krn[ptr++] * r;
234+
r_acc += (krn[ptr++] * g) / 2;
235+
r_acc += krn[ptr++] * b;
236+
g_acc += (krn[ptr++] * r) * 2;
237+
g_acc += krn[ptr++] * g;
238+
g_acc += (krn[ptr++] * b) * 2;
239+
b_acc += krn[ptr++] * r;
240+
b_acc += (krn[ptr++] * g) / 2;
241+
b_acc += krn[ptr++] * b;
242+
}
243+
}
244+
} else {
245+
for (int j = -ksize; j <= ksize; j++) {
246+
uint16_t *k_row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(bitmap,
247+
IM_MIN(IM_MAX(y + j, 0), (bitmap->height - 1)));
248+
for (int k = -ksize; k <= ksize; k++) {
249+
int pixel = IMAGE_GET_RGB565_PIXEL_FAST(k_row_ptr,
250+
IM_MIN(IM_MAX(x + k, 0), (bitmap->width - 1)));
251+
int r = COLOR_RGB565_TO_R5(pixel);
252+
int g = COLOR_RGB565_TO_G6(pixel);
253+
int b = COLOR_RGB565_TO_B5(pixel);
254+
r_acc += krn[ptr++] * r;
255+
r_acc += (krn[ptr++] * g) / 2;
256+
r_acc += krn[ptr++] * b;
257+
g_acc += (krn[ptr++] * r) * 2;
258+
g_acc += krn[ptr++] * g;
259+
g_acc += (krn[ptr++] * b) * 2;
260+
b_acc += krn[ptr++] * r;
261+
b_acc += (krn[ptr++] * g) / 2;
262+
b_acc += krn[ptr++] * b;
263+
}
264+
}
265+
}
266+
r_acc = (r_acc * m_int[0] + b_int[0]) >> 16;
267+
if (r_acc > COLOR_R5_MAX) {
268+
r_acc = COLOR_R5_MAX;
269+
} else if (r_acc < 0) {
270+
r_acc = 0;
271+
}
272+
g_acc = (g_acc * m_int[1] + b_int[1]) >> 16;
273+
if (g_acc > COLOR_G6_MAX) {
274+
g_acc = COLOR_G6_MAX;
275+
} else if (g_acc < 0) {
276+
g_acc = 0;
277+
}
278+
b_acc = (b_acc * m_int[2] + b_int[2]) >> 16;
279+
if (b_acc > COLOR_B5_MAX) {
280+
b_acc = COLOR_B5_MAX;
281+
} else if (b_acc < 0) {
282+
b_acc = 0;
283+
}
284+
285+
int pixel = COLOR_R5_G6_B5_TO_RGB565(r_acc, g_acc, b_acc);
286+
287+
if (threshold) {
288+
if (((COLOR_RGB565_TO_Y(pixel) - offset) < COLOR_RGB565_TO_Y(IMAGE_GET_RGB565_PIXEL_FAST(row_ptr, x))) ^ invert) {
289+
pixel = COLOR_RGB565_BINARY_MAX;
290+
} else {
291+
pixel = COLOR_RGB565_BINARY_MIN;
292+
}
293+
}
294+
295+
IMAGE_PUT_RGB565_PIXEL_FAST(buf_row_ptr, x, pixel);
296+
}
297+
298+
if (y >= ksize) { // Transfer buffer lines...
299+
memcpy(IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(bitmap, (y - ksize)),
300+
IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(&buf, ((y - ksize) % brows)),
301+
IMAGE_RGB565_LINE_LEN_BYTES(bitmap));
302+
}
303+
}
304+
305+
// Copy any remaining lines from the buffer image...
306+
for (int y = IM_MAX(bitmap->height - ksize, 0), yy = bitmap->height; y < yy; y++) {
307+
memcpy(IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(bitmap, y),
308+
IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(&buf, (y % brows)),
309+
IMAGE_RGB565_LINE_LEN_BYTES(bitmap));
310+
}
311+
312+
break;
313+
}
314+
}
315+
}
180316
void shared_module_bitmapfilter_morph(
181317
displayio_bitmap_t *bitmap,
182318
displayio_bitmap_t *mask,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from ulab import numpy as np
2+
from displayio import Bitmap
3+
import bitmapfilter
4+
from dump_bitmap import dump_bitmap_rgb_swapped
5+
from blinka_image import decode_resource
6+
7+
8+
def test_pattern():
9+
return decode_resource("testpattern", 2)
10+
11+
12+
blur = (1, 2, 1, 2, 4, 2, 1, 2, 1)
13+
14+
15+
def blur_i(i):
16+
result = np.zeros(9 * 9, dtype=np.int16)
17+
result[i::9] = blur
18+
print(list(result))
19+
return result
20+
21+
22+
b = test_pattern()
23+
dump_bitmap_rgb_swapped(b)
24+
25+
for i in range(9):
26+
b = test_pattern()
27+
bitmapfilter.morph9(b, weights=blur_i(i))
28+
# bitmapfilter.morph9(b, weights=blur_i(i), mul=(1/16, 1/16, 1/16))
29+
dump_bitmap_rgb_swapped(b)

0 commit comments

Comments
 (0)