Skip to content

Commit 7e23fac

Browse files
committed
bitmapfilter: refine morph, add docs
1 parent 75be426 commit 7e23fac

File tree

5 files changed

+245
-73
lines changed

5 files changed

+245
-73
lines changed

shared-bindings/bitmapfilter/__init__.c

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,90 @@
3030
#include "shared-bindings/displayio/Bitmap.h"
3131
#include "shared-bindings/bitmapfilter/__init__.h"
3232

33+
//|
34+
//| def morph(
35+
//| bitmap: displayio.Bitmap,
36+
//| weights: tuple[int],
37+
//| mul: float = 1.0,
38+
//| add: int = 0,
39+
//| mask: displayio.Bitmap | None = None,
40+
//| threshold=False,
41+
//| offset: int = 0,
42+
//| invert: bool = False,
43+
//| ):
44+
//| """Convolve an image with a kernel
45+
//|
46+
//| The ``bitmap``, which must be in RGB565_SWAPPED format, is modified
47+
//| according to the ``weights``. Then a scaling factor ``m`` and an
48+
//| offset factor ``b`` are applied.
49+
//|
50+
//| The ``weights`` must be a tuple of integers. The length of the tuple
51+
//| must be the square of an odd number, usually 9 and sometimes 25.
52+
//| Specific weights create different effects. For instance, these
53+
//| weights represent a 3x3 gaussian blur:
54+
//|
55+
//| ``mul`` is number to multiply the convolution pixel results by. When
56+
//| not set it defaults to a value that will prevent scaling in the
57+
//| convolution output.
58+
//|
59+
//| ``add`` is a value to add to each convolution pixel result.
60+
//|
61+
//| ``mul`` basically allows you to do a global contrast adjustment and
62+
//| add allows you to do a global brightness adjustment. Pixels that go
63+
//| outside of the image mins and maxes for color channels will be
64+
//| clipped.
65+
//|
66+
//| If you’d like to adaptive threshold the image on the output of the
67+
//| filter you can pass ``threshold=True`` which will enable adaptive
68+
//| thresholding of the image which sets pixels to one or zero based on a
69+
//| pixel’s brightness in relation to the brightness of the kernel of pixels
70+
//| around them. A negative ``offset`` value sets more pixels to 1 as you make
71+
//| it more negative while a positive value only sets the sharpest contrast
72+
//| changes to 1. Set ``invert`` to invert the binary image resulting output.
73+
//|
74+
//| ``mask`` is another image to use as a pixel level mask for the operation.
75+
//| The mask should be an image with just black or white pixels and should
76+
//| be the same size as the image being operated on. Only pixels set in the
77+
//| mask are modified.
78+
//|
79+
//| .. code-block:: python
80+
//|
81+
//| kernel_gauss_3 = [
82+
//| 1, 2, 1,
83+
//| 2, 4, 2,
84+
//| 1, 2, 1]
85+
//|
86+
//| def blur(bitmap):
87+
//| \"""Blur the bitmap with a 3x3 gaussian kernel\"""
88+
//| bitmapfilter.morph(bitmap, kernel_gauss_3, 1/sum(kernel_gauss_3))
89+
//| """
90+
//|
91+
3392
STATIC mp_obj_t bitmapfilter_morph(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
34-
enum { ARG_dest_bitmap, ARG_source_bitmap, ARG_weights, ARG_m, ARG_b };
93+
enum { ARG_bitmap, ARG_weights, ARG_mul, ARG_add, ARG_threshold, ARG_offset, ARG_invert, ARG_mask };
3594
static const mp_arg_t allowed_args[] = {
36-
{ MP_QSTR_dest_bitmap, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } },
37-
{ MP_QSTR_source_bitmap, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } },
95+
{ MP_QSTR_bitmap, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } },
3896
{ MP_QSTR_weights, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } },
39-
{ MP_QSTR_m, MP_ARG_OBJ, { .u_obj = MP_ROM_INT(1) } },
40-
{ MP_QSTR_b, MP_ARG_OBJ, { .u_obj = MP_ROM_INT(0) } },
97+
{ MP_QSTR_mul, MP_ARG_OBJ, { .u_obj = MP_ROM_NONE } },
98+
{ MP_QSTR_add, MP_ARG_OBJ, { .u_obj = MP_ROM_INT(0) } },
99+
{ MP_QSTR_threshold, MP_ARG_BOOL, { .u_bool = false } },
100+
{ MP_QSTR_offset, MP_ARG_INT, { .u_int = 0 } },
101+
{ MP_QSTR_invert, MP_ARG_BOOL, { .u_bool = false } },
102+
{ MP_QSTR_mask, MP_ARG_OBJ, { .u_obj = MP_ROM_NONE } },
41103
};
42104
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
43105
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
44106

45-
mp_arg_validate_type(args[ARG_dest_bitmap].u_obj, &displayio_bitmap_type, MP_QSTR_dest_bitmap);
46-
mp_arg_validate_type(args[ARG_source_bitmap].u_obj, &displayio_bitmap_type, MP_QSTR_source_bitmap);
47-
displayio_bitmap_t *destination = MP_OBJ_TO_PTR(args[ARG_dest_bitmap].u_obj); // the destination bitmap
48-
displayio_bitmap_t *source = MP_OBJ_TO_PTR(args[ARG_source_bitmap].u_obj); // the source bitmap
107+
mp_arg_validate_type(args[ARG_bitmap].u_obj, &displayio_bitmap_type, MP_QSTR_bitmap);
108+
displayio_bitmap_t *bitmap = args[ARG_bitmap].u_obj;
109+
110+
displayio_bitmap_t *mask = NULL; // the mask bitmap
111+
if (args[ARG_mask].u_obj != mp_const_none) {
112+
mp_arg_validate_type(args[ARG_mask].u_obj, &displayio_bitmap_type, MP_QSTR_mask);
113+
mask = MP_OBJ_TO_PTR(args[ARG_mask].u_obj);
114+
}
49115

50-
mp_float_t m = mp_obj_get_float(args[ARG_m].u_obj);
51-
mp_int_t b = mp_obj_get_int(args[ARG_b].u_obj);
116+
mp_int_t b = mp_obj_get_int(args[ARG_add].u_obj);
52117

53118
size_t n_weights;
54119
mp_obj_t weights = mp_arg_validate_type(args[ARG_weights].u_obj, &mp_type_tuple, MP_QSTR_weights);
@@ -61,11 +126,17 @@ STATIC mp_obj_t bitmapfilter_morph(size_t n_args, const mp_obj_t *pos_args, mp_m
61126
}
62127

63128
int iweights[n_weights];
129+
int weight_sum = 0;
64130
for (size_t i = 0; i < n_weights; i++) {
65-
iweights[i] = mp_obj_get_int(items[i]);
131+
mp_int_t j = mp_obj_get_int(items[i]);
132+
iweights[i] = j;
133+
weight_sum += j;
66134
}
67135

68-
shared_module_bitmapfilter_morph(source, destination, sq_n_weights / 2, iweights, (float)m, b, false, 0, false);
136+
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;
137+
138+
shared_module_bitmapfilter_morph(bitmap, mask, sq_n_weights / 2, iweights, (float)m, b,
139+
args[ARG_threshold].u_bool, args[ARG_offset].u_bool, args[ARG_invert].u_bool);
69140
return mp_const_none;
70141
}
71142

shared-bindings/bitmapfilter/__init__.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
#include "shared-module/displayio/Bitmap.h"
3030

3131
void shared_module_bitmapfilter_morph(
32-
displayio_bitmap_t *src,
33-
displayio_bitmap_t *dest,
32+
displayio_bitmap_t *bitmap,
33+
displayio_bitmap_t *mask,
3434
const int ksize,
3535
const int *krn,
3636
const float m,

shared-module/bitmapfilter/__init__.c

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
* Copyright (c) 2024 Jeff Epler for Adafruit Industries
55
*
66
* This work is licensed under the MIT license, see the file LICENSE for details.
7-
* Adapted from https://github.com/openmv/openmv/blob/master/src/omv/imlib/filter.c#L2083
7+
* Adapted from https://github.com/openmv/openmv/blob/master/bitmap/omv/imlib/filter.c#L2083
88
*/
99

1010
#include <stdbool.h>
1111
#include <math.h>
1212

1313
#include "py/runtime.h"
1414

15+
#include "shared-bindings/displayio/Bitmap.h"
1516
#include "shared-bindings/bitmapfilter/__init__.h"
1617
#include "shared-module/bitmapfilter/__init__.h"
1718

@@ -28,7 +29,7 @@
2829
#pragma GCC diagnostic ignored "-Wshadow"
2930

3031
static void check_matching_details(displayio_bitmap_t *b1, displayio_bitmap_t *b2) {
31-
if (b1->width != b2->width || b1->height != b2->height || b1->bits_per_value != b2->bits_per_value) {
32+
if (b1->width != b2->width || b1->height != b2->height) {
3233
mp_raise_ValueError(MP_ERROR_TEXT("bitmap size and depth must match"));
3334
}
3435
}
@@ -135,8 +136,8 @@ static void scratch_bitmap16(displayio_bitmap_t *buf, int rows, int cols) {
135136
})
136137

137138
void shared_module_bitmapfilter_morph(
138-
displayio_bitmap_t *src,
139-
displayio_bitmap_t *dest,
139+
displayio_bitmap_t *bitmap,
140+
displayio_bitmap_t *mask,
140141
const int ksize,
141142
const int *krn,
142143
const float m,
@@ -149,25 +150,30 @@ void shared_module_bitmapfilter_morph(
149150

150151
const int32_t m_int = (int32_t)MICROPY_FLOAT_C_FUN(round)(65536 * m);
151152

152-
check_matching_details(src, dest);
153+
check_matching_details(bitmap, bitmap);
153154

154-
switch (src->bits_per_value) {
155+
switch (bitmap->bits_per_value) {
155156
default:
156157
mp_raise_ValueError(MP_ERROR_TEXT("unsupported bitmap depth"));
157158
case 16: {
158159
displayio_bitmap_t buf;
159-
scratch_bitmap16(&buf, brows, src->width);
160+
scratch_bitmap16(&buf, brows, bitmap->width);
160161

161-
for (int y = 0, yy = src->height; y < yy; y++) {
162-
uint16_t *row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(src, y);
162+
for (int y = 0, yy = bitmap->height; y < yy; y++) {
163+
uint16_t *row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(bitmap, y);
163164
uint16_t *buf_row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(&buf, (y % brows));
164165

165-
for (int x = 0, xx = src->width; x < xx; x++) {
166+
for (int x = 0, xx = bitmap->width; x < xx; x++) {
167+
if (mask && common_hal_displayio_bitmap_get_pixel(mask, x, y)) {
168+
IMAGE_PUT_RGB565_PIXEL_FAST(buf_row_ptr, x, IMAGE_GET_RGB565_PIXEL_FAST(row_ptr, x));
169+
continue; // Short circuit.
170+
171+
}
166172
int32_t tmp, r_acc = 0, g_acc = 0, b_acc = 0, ptr = 0;
167173

168-
if (x >= ksize && x < src->width - ksize && y >= ksize && y < src->height - ksize) {
174+
if (x >= ksize && x < bitmap->width - ksize && y >= ksize && y < bitmap->height - ksize) {
169175
for (int j = -ksize; j <= ksize; j++) {
170-
uint16_t *k_row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(src, y + j);
176+
uint16_t *k_row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(bitmap, y + j);
171177
for (int k = -ksize; k <= ksize; k++) {
172178
int pixel = IMAGE_GET_RGB565_PIXEL_FAST(k_row_ptr, x + k);
173179
r_acc += krn[ptr] * COLOR_RGB565_TO_R5(pixel);
@@ -177,11 +183,11 @@ void shared_module_bitmapfilter_morph(
177183
}
178184
} else {
179185
for (int j = -ksize; j <= ksize; j++) {
180-
uint16_t *k_row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(src,
181-
IM_MIN(IM_MAX(y + j, 0), (src->height - 1)));
186+
uint16_t *k_row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(bitmap,
187+
IM_MIN(IM_MAX(y + j, 0), (bitmap->height - 1)));
182188
for (int k = -ksize; k <= ksize; k++) {
183189
int pixel = IMAGE_GET_RGB565_PIXEL_FAST(k_row_ptr,
184-
IM_MIN(IM_MAX(x + k, 0), (src->width - 1)));
190+
IM_MIN(IM_MAX(x + k, 0), (bitmap->width - 1)));
185191
r_acc += krn[ptr] * COLOR_RGB565_TO_R5(pixel);
186192
g_acc += krn[ptr] * COLOR_RGB565_TO_G6(pixel);
187193
b_acc += krn[ptr++] * COLOR_RGB565_TO_B5(pixel);
@@ -224,17 +230,17 @@ void shared_module_bitmapfilter_morph(
224230
}
225231

226232
if (y >= ksize) { // Transfer buffer lines...
227-
memcpy(IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(dest, (y - ksize)),
233+
memcpy(IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(bitmap, (y - ksize)),
228234
IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(&buf, ((y - ksize) % brows)),
229-
IMAGE_RGB565_LINE_LEN_BYTES(src));
235+
IMAGE_RGB565_LINE_LEN_BYTES(bitmap));
230236
}
231237
}
232238

233239
// Copy any remaining lines from the buffer image...
234-
for (int y = IM_MAX(src->height - ksize, 0), yy = src->height; y < yy; y++) {
235-
memcpy(IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(dest, y),
240+
for (int y = IM_MAX(bitmap->height - ksize, 0), yy = bitmap->height; y < yy; y++) {
241+
memcpy(IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(bitmap, y),
236242
IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(&buf, (y % brows)),
237-
IMAGE_RGB565_LINE_LEN_BYTES(src));
243+
IMAGE_RGB565_LINE_LEN_BYTES(bitmap));
238244
}
239245

240246
break;
Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,52 @@
1+
from displayio import Bitmap
12
import bitmapfilter
2-
import displayio
33

4-
b = displayio.Bitmap(5, 5, 65535)
5-
b[2, 2] = 0x1F00 # Blue in RGB565_SWAPPED
6-
b[0, 0] = 0xE007 # Green in RGB565_SWAPPED
7-
b[0, 4] = 0x00F8 # Red in RGB565_SWAPPED
8-
weights = (1, 1, 1, 1, 1, 1, 1, 1, 1)
4+
palette = list(" ░░▒▒▓▓█")
95

106

11-
def print_bitmap(bitmap):
12-
for i in range(bitmap.height):
13-
for j in range(bitmap.width):
14-
p = bitmap[j, i]
15-
p = ((p << 8) & 0xFF00) | (p >> 8)
16-
17-
r = (p >> 8) & 0xF8
18-
r |= r >> 5
19-
20-
g = (p >> 3) & 0xFC
21-
g |= g >> 6
22-
23-
b = (p << 3) & 0xF8
24-
b |= b >> 5
25-
print(f"{r:02x}{g:02x}{b:02x}", end=" ")
7+
def dump_bitmap(b):
8+
for i in range(b.height):
9+
for j in range(b.width):
10+
# Bit order is gggBBBBBRRRRRGGG" so this takes high order bits of G
11+
p = b[i, j] & 7
12+
print(end=palette[p])
2613
print()
2714
print()
2815

2916

30-
print(len(weights))
31-
32-
print_bitmap(b)
33-
bitmapfilter.morph(b, b, weights, m=1 / 9)
34-
print_bitmap(b)
17+
def make_circle_bitmap():
18+
b = Bitmap(17, 17, 65535)
19+
for i in range(b.height):
20+
y = i - 8
21+
for j in range(b.width):
22+
x = j - 8
23+
c = (x * x + y * y) > 64
24+
b[i, j] = 0xFFFF if c else 0
25+
return b
26+
27+
28+
def make_quadrant_bitmap():
29+
b = Bitmap(17, 17, 1)
30+
for i in range(b.height):
31+
for j in range(b.width):
32+
b[i, j] = (i < 8) ^ (j < 8)
33+
return b
34+
35+
36+
blur = (1, 2, 1, 2, 4, 2, 1, 2, 1)
37+
sharpen = (-1, -2, -1, -2, 4, -2, -1, -2, -1)
38+
b = make_circle_bitmap()
39+
dump_bitmap(b)
40+
bitmapfilter.morph(b, weights=blur)
41+
dump_bitmap(b)
42+
43+
b = make_circle_bitmap()
44+
q = make_quadrant_bitmap()
45+
dump_bitmap(q)
46+
bitmapfilter.morph(b, mask=q, weights=blur, add=32)
47+
dump_bitmap(b)
48+
49+
# This is a kind of edge filter
50+
b = make_circle_bitmap()
51+
bitmapfilter.morph(b, weights=sharpen, threshold=True, add=8, invert=True)
52+
dump_bitmap(b)

0 commit comments

Comments
 (0)