bitmapfilter: refine morph, add docs

This commit is contained in:
Jeff Epler 2024-01-06 13:34:42 -06:00
parent 75be426377
commit 7e23fac766
No known key found for this signature in database
GPG key ID: D5BF15AB975AB4DE
5 changed files with 243 additions and 71 deletions

View file

@ -30,25 +30,90 @@
#include "shared-bindings/displayio/Bitmap.h"
#include "shared-bindings/bitmapfilter/__init__.h"
//|
//| def morph(
//| bitmap: displayio.Bitmap,
//| weights: tuple[int],
//| mul: float = 1.0,
//| add: int = 0,
//| mask: displayio.Bitmap | None = None,
//| threshold=False,
//| offset: int = 0,
//| invert: bool = False,
//| ):
//| """Convolve an image with a kernel
//|
//| The ``bitmap``, which must be in RGB565_SWAPPED format, is modified
//| according to the ``weights``. Then a scaling factor ``m`` and an
//| offset factor ``b`` are applied.
//|
//| The ``weights`` must be a tuple of integers. The length of the tuple
//| must be the square of an odd number, usually 9 and sometimes 25.
//| Specific weights create different effects. For instance, these
//| weights represent a 3x3 gaussian blur:
//|
//| ``mul`` is number to multiply the convolution pixel results by. When
//| not set it defaults to a value that will prevent scaling in the
//| convolution output.
//|
//| ``add`` is a value to add to each convolution pixel result.
//|
//| ``mul`` basically allows you to do a global contrast adjustment and
//| add allows you to do a global brightness adjustment. Pixels that go
//| outside of the image mins and maxes for color channels will be
//| clipped.
//|
//| If youd like to adaptive threshold the image on the output of the
//| filter you can pass ``threshold=True`` which will enable adaptive
//| thresholding of the image which sets pixels to one or zero based on a
//| pixels brightness in relation to the brightness of the kernel of pixels
//| around them. A negative ``offset`` value sets more pixels to 1 as you make
//| it more negative while a positive value only sets the sharpest contrast
//| changes to 1. Set ``invert`` to invert the binary image resulting output.
//|
//| ``mask`` is another image to use as a pixel level mask for the operation.
//| The mask should be an image with just black or white pixels and should
//| be the same size as the image being operated on. Only pixels set in the
//| mask are modified.
//|
//| .. code-block:: python
//|
//| kernel_gauss_3 = [
//| 1, 2, 1,
//| 2, 4, 2,
//| 1, 2, 1]
//|
//| def blur(bitmap):
//| \"""Blur the bitmap with a 3x3 gaussian kernel\"""
//| bitmapfilter.morph(bitmap, kernel_gauss_3, 1/sum(kernel_gauss_3))
//| """
//|
STATIC mp_obj_t bitmapfilter_morph(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
enum { ARG_dest_bitmap, ARG_source_bitmap, ARG_weights, ARG_m, ARG_b };
enum { ARG_bitmap, ARG_weights, ARG_mul, ARG_add, ARG_threshold, ARG_offset, ARG_invert, ARG_mask };
static const mp_arg_t allowed_args[] = {
{ MP_QSTR_dest_bitmap, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } },
{ MP_QSTR_source_bitmap, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } },
{ MP_QSTR_bitmap, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } },
{ MP_QSTR_weights, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } },
{ MP_QSTR_m, MP_ARG_OBJ, { .u_obj = MP_ROM_INT(1) } },
{ MP_QSTR_b, MP_ARG_OBJ, { .u_obj = MP_ROM_INT(0) } },
{ MP_QSTR_mul, MP_ARG_OBJ, { .u_obj = MP_ROM_NONE } },
{ MP_QSTR_add, MP_ARG_OBJ, { .u_obj = MP_ROM_INT(0) } },
{ MP_QSTR_threshold, MP_ARG_BOOL, { .u_bool = false } },
{ MP_QSTR_offset, MP_ARG_INT, { .u_int = 0 } },
{ MP_QSTR_invert, MP_ARG_BOOL, { .u_bool = false } },
{ MP_QSTR_mask, MP_ARG_OBJ, { .u_obj = MP_ROM_NONE } },
};
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
mp_arg_validate_type(args[ARG_dest_bitmap].u_obj, &displayio_bitmap_type, MP_QSTR_dest_bitmap);
mp_arg_validate_type(args[ARG_source_bitmap].u_obj, &displayio_bitmap_type, MP_QSTR_source_bitmap);
displayio_bitmap_t *destination = MP_OBJ_TO_PTR(args[ARG_dest_bitmap].u_obj); // the destination bitmap
displayio_bitmap_t *source = MP_OBJ_TO_PTR(args[ARG_source_bitmap].u_obj); // the source bitmap
mp_arg_validate_type(args[ARG_bitmap].u_obj, &displayio_bitmap_type, MP_QSTR_bitmap);
displayio_bitmap_t *bitmap = args[ARG_bitmap].u_obj;
mp_float_t m = mp_obj_get_float(args[ARG_m].u_obj);
mp_int_t b = mp_obj_get_int(args[ARG_b].u_obj);
displayio_bitmap_t *mask = NULL; // the mask bitmap
if (args[ARG_mask].u_obj != mp_const_none) {
mp_arg_validate_type(args[ARG_mask].u_obj, &displayio_bitmap_type, MP_QSTR_mask);
mask = MP_OBJ_TO_PTR(args[ARG_mask].u_obj);
}
mp_int_t b = mp_obj_get_int(args[ARG_add].u_obj);
size_t n_weights;
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
}
int iweights[n_weights];
int weight_sum = 0;
for (size_t i = 0; i < n_weights; i++) {
iweights[i] = mp_obj_get_int(items[i]);
mp_int_t j = mp_obj_get_int(items[i]);
iweights[i] = j;
weight_sum += j;
}
shared_module_bitmapfilter_morph(source, destination, sq_n_weights / 2, iweights, (float)m, b, false, 0, false);
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;
shared_module_bitmapfilter_morph(bitmap, mask, sq_n_weights / 2, iweights, (float)m, b,
args[ARG_threshold].u_bool, args[ARG_offset].u_bool, args[ARG_invert].u_bool);
return mp_const_none;
}

View file

@ -29,8 +29,8 @@
#include "shared-module/displayio/Bitmap.h"
void shared_module_bitmapfilter_morph(
displayio_bitmap_t *src,
displayio_bitmap_t *dest,
displayio_bitmap_t *bitmap,
displayio_bitmap_t *mask,
const int ksize,
const int *krn,
const float m,

View file

@ -4,7 +4,7 @@
* Copyright (c) 2024 Jeff Epler for Adafruit Industries
*
* This work is licensed under the MIT license, see the file LICENSE for details.
* Adapted from https://github.com/openmv/openmv/blob/master/src/omv/imlib/filter.c#L2083
* Adapted from https://github.com/openmv/openmv/blob/master/bitmap/omv/imlib/filter.c#L2083
*/
#include <stdbool.h>
@ -12,6 +12,7 @@
#include "py/runtime.h"
#include "shared-bindings/displayio/Bitmap.h"
#include "shared-bindings/bitmapfilter/__init__.h"
#include "shared-module/bitmapfilter/__init__.h"
@ -28,7 +29,7 @@
#pragma GCC diagnostic ignored "-Wshadow"
static void check_matching_details(displayio_bitmap_t *b1, displayio_bitmap_t *b2) {
if (b1->width != b2->width || b1->height != b2->height || b1->bits_per_value != b2->bits_per_value) {
if (b1->width != b2->width || b1->height != b2->height) {
mp_raise_ValueError(MP_ERROR_TEXT("bitmap size and depth must match"));
}
}
@ -135,8 +136,8 @@ static void scratch_bitmap16(displayio_bitmap_t *buf, int rows, int cols) {
})
void shared_module_bitmapfilter_morph(
displayio_bitmap_t *src,
displayio_bitmap_t *dest,
displayio_bitmap_t *bitmap,
displayio_bitmap_t *mask,
const int ksize,
const int *krn,
const float m,
@ -149,25 +150,30 @@ void shared_module_bitmapfilter_morph(
const int32_t m_int = (int32_t)MICROPY_FLOAT_C_FUN(round)(65536 * m);
check_matching_details(src, dest);
check_matching_details(bitmap, bitmap);
switch (src->bits_per_value) {
switch (bitmap->bits_per_value) {
default:
mp_raise_ValueError(MP_ERROR_TEXT("unsupported bitmap depth"));
case 16: {
displayio_bitmap_t buf;
scratch_bitmap16(&buf, brows, src->width);
scratch_bitmap16(&buf, brows, bitmap->width);
for (int y = 0, yy = src->height; y < yy; y++) {
uint16_t *row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(src, y);
for (int y = 0, yy = bitmap->height; y < yy; y++) {
uint16_t *row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(bitmap, y);
uint16_t *buf_row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(&buf, (y % brows));
for (int x = 0, xx = src->width; x < xx; x++) {
for (int x = 0, xx = bitmap->width; x < xx; x++) {
if (mask && common_hal_displayio_bitmap_get_pixel(mask, x, y)) {
IMAGE_PUT_RGB565_PIXEL_FAST(buf_row_ptr, x, IMAGE_GET_RGB565_PIXEL_FAST(row_ptr, x));
continue; // Short circuit.
}
int32_t tmp, r_acc = 0, g_acc = 0, b_acc = 0, ptr = 0;
if (x >= ksize && x < src->width - ksize && y >= ksize && y < src->height - ksize) {
if (x >= ksize && x < bitmap->width - ksize && y >= ksize && y < bitmap->height - ksize) {
for (int j = -ksize; j <= ksize; j++) {
uint16_t *k_row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(src, y + j);
uint16_t *k_row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(bitmap, y + j);
for (int k = -ksize; k <= ksize; k++) {
int pixel = IMAGE_GET_RGB565_PIXEL_FAST(k_row_ptr, x + k);
r_acc += krn[ptr] * COLOR_RGB565_TO_R5(pixel);
@ -177,11 +183,11 @@ void shared_module_bitmapfilter_morph(
}
} else {
for (int j = -ksize; j <= ksize; j++) {
uint16_t *k_row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(src,
IM_MIN(IM_MAX(y + j, 0), (src->height - 1)));
uint16_t *k_row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(bitmap,
IM_MIN(IM_MAX(y + j, 0), (bitmap->height - 1)));
for (int k = -ksize; k <= ksize; k++) {
int pixel = IMAGE_GET_RGB565_PIXEL_FAST(k_row_ptr,
IM_MIN(IM_MAX(x + k, 0), (src->width - 1)));
IM_MIN(IM_MAX(x + k, 0), (bitmap->width - 1)));
r_acc += krn[ptr] * COLOR_RGB565_TO_R5(pixel);
g_acc += krn[ptr] * COLOR_RGB565_TO_G6(pixel);
b_acc += krn[ptr++] * COLOR_RGB565_TO_B5(pixel);
@ -224,17 +230,17 @@ void shared_module_bitmapfilter_morph(
}
if (y >= ksize) { // Transfer buffer lines...
memcpy(IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(dest, (y - ksize)),
memcpy(IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(bitmap, (y - ksize)),
IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(&buf, ((y - ksize) % brows)),
IMAGE_RGB565_LINE_LEN_BYTES(src));
IMAGE_RGB565_LINE_LEN_BYTES(bitmap));
}
}
// Copy any remaining lines from the buffer image...
for (int y = IM_MAX(src->height - ksize, 0), yy = src->height; y < yy; y++) {
memcpy(IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(dest, y),
for (int y = IM_MAX(bitmap->height - ksize, 0), yy = bitmap->height; y < yy; y++) {
memcpy(IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(bitmap, y),
IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(&buf, (y % brows)),
IMAGE_RGB565_LINE_LEN_BYTES(src));
IMAGE_RGB565_LINE_LEN_BYTES(bitmap));
}
break;

View file

@ -1,34 +1,52 @@
from displayio import Bitmap
import bitmapfilter
import displayio
b = displayio.Bitmap(5, 5, 65535)
b[2, 2] = 0x1F00 # Blue in RGB565_SWAPPED
b[0, 0] = 0xE007 # Green in RGB565_SWAPPED
b[0, 4] = 0x00F8 # Red in RGB565_SWAPPED
weights = (1, 1, 1, 1, 1, 1, 1, 1, 1)
palette = list(" ░░▒▒▓▓█")
def print_bitmap(bitmap):
for i in range(bitmap.height):
for j in range(bitmap.width):
p = bitmap[j, i]
p = ((p << 8) & 0xFF00) | (p >> 8)
r = (p >> 8) & 0xF8
r |= r >> 5
g = (p >> 3) & 0xFC
g |= g >> 6
b = (p << 3) & 0xF8
b |= b >> 5
print(f"{r:02x}{g:02x}{b:02x}", end=" ")
def dump_bitmap(b):
for i in range(b.height):
for j in range(b.width):
# Bit order is gggBBBBBRRRRRGGG" so this takes high order bits of G
p = b[i, j] & 7
print(end=palette[p])
print()
print()
print(len(weights))
def make_circle_bitmap():
b = Bitmap(17, 17, 65535)
for i in range(b.height):
y = i - 8
for j in range(b.width):
x = j - 8
c = (x * x + y * y) > 64
b[i, j] = 0xFFFF if c else 0
return b
print_bitmap(b)
bitmapfilter.morph(b, b, weights, m=1 / 9)
print_bitmap(b)
def make_quadrant_bitmap():
b = Bitmap(17, 17, 1)
for i in range(b.height):
for j in range(b.width):
b[i, j] = (i < 8) ^ (j < 8)
return b
blur = (1, 2, 1, 2, 4, 2, 1, 2, 1)
sharpen = (-1, -2, -1, -2, 4, -2, -1, -2, -1)
b = make_circle_bitmap()
dump_bitmap(b)
bitmapfilter.morph(b, weights=blur)
dump_bitmap(b)
b = make_circle_bitmap()
q = make_quadrant_bitmap()
dump_bitmap(q)
bitmapfilter.morph(b, mask=q, weights=blur, add=32)
dump_bitmap(b)
# This is a kind of edge filter
b = make_circle_bitmap()
bitmapfilter.morph(b, weights=sharpen, threshold=True, add=8, invert=True)
dump_bitmap(b)

View file

@ -1,13 +1,90 @@
9
00ff00 000000 000000 000000 000000
000000 000000 000000 000000 000000
000000 000000 0000ff 000000 000000
000000 000000 000000 000000 000000
ff0000 000000 000000 000000 000000
████████ ████████
█████ █████
███ ███
██ ██
██ ██
█ █
█ █
█ █
█ █
█ █
█ █
██ ██
██ ██
███ ███
█████ █████
████████ ████████
007100 003800 000000 000000 000000
003800 001c18 000018 000018 000000
000000 000018 000018 000018 000000
310000 180018 000018 000018 000000
6b0000 310000 000000 000000 000000
█████▓▓▒░▒▓▓█████
███▓▒░░░ ░░░▒▓███
██▓░░ ░░▓██
█▓░ ░▓█
█▒░ ░▒█
▓░ ░▓
▓░ ░▓
▒░ ░▒
░ ░
▒░ ░▒
▓░ ░▓
▓░ ░▓
█▒░ ░▒█
█▓░ ░▓█
██▓░░ ░░▓██
███▓▒░░░ ░░░▒▓███
█████▓▓▒░▒▓▓█████
░░░░░░░░░
░░░░░░░░░
░░░░░░░░░
░░░░░░░░░
░░░░░░░░░
░░░░░░░░░
░░░░░░░░░
░░░░░░░░░
░░░░░░░░
░░░░░░░░
░░░░░░░░
░░░░░░░░
░░░░░░░░
░░░░░░░░
░░░░░░░░
░░░░░░░░
░░░░░░░░
████████ ████████
█████▓▓▓ █████
███▓▓▒▒▒ ███
██▓▒▒▒▒▒ ██
██▓▒▒▒▒▒ ██
█▓▒▒▒▒▒▒ █
█▓▒▒▒▒▒▒ █
█▓▒▒▒▒▒▒ █
▒▒▒▒▒▒▒▒▓
█ ▒▒▒▒▒▒▒▓█
█ ▒▒▒▒▒▒▒▓█
█ ▒▒▒▒▒▒▒▓█
██ ▒▒▒▒▒▒▓██
██ ▒▒▒▒▒▒▓██
███ ▒▒▒▒▓▓███
█████ ▒▓▓▓█████
████████▓████████
█████ █ █████
███ ███████ ███
██ ███████████ ██
█ █████████████ █
█ █████████████ █
███████████████
███████████████
███████████████
█████████████████
███████████████
███████████████
███████████████
█ █████████████ █
█ █████████████ █
██ ███████████ ██
███ ███████ ███
█████ █ █████