CircuitPython 9's new bitmaptools module includes a number of image filters: false color, lookup, mix, morph, and solarize.
If you have a different image processing task, and CircuitPython + ulab code is not fast enough, you can code a new algorithm in C. Note that this will require you to build your own custom CircuitPython firmware, so familiarize yourself with that process first. Then, get an overview of how to add a CircuitPython module coded in C.
There are 4 main parts you'll need:
- A declaration of the C function for processing the image
- The actual implementation of the image processing algorithm
- The function binding, which converts from CircuitPython arguments to C arguments
- The function's entry in the bitmaptools "globals table"
To illustrate each of these parts, the implementation of solarize will be used as an example.
In the file shared-bindings/bitmapfilter/__init__.h is a declaration of the image processing function. Solarize takes a bitmap (which it modifies in-place), an optional mask bitmap, and a threshold value from 0 to 1 as a float:
void shared_module_bitmapfilter_solarize( displayio_bitmap_t *bitmap, displayio_bitmap_t *mask, const mp_float_t threshold);
The C implementation of solarize in shared-module/bitmapfilter/__init__.c is shown below. Here are some key items to note:
- The threshold value is converted to a scaled integer just once. This is because on most microcontrollers, computations on integers are faster than computations on floating-point numbers.
- The bitmap depth is checked to determine if it's the right kind. In this example, only processing of 16-bit images is implemented. Furthermore, it is assumed (by the
IMAGE_GET_RGB565_PIXEL_FAST
andIMAGE_PUT_RGB565_PIXEL_FAST
functions) that 16-bit images are always in RGB565_SWAPPED format. - The image is processed by rows.
IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR
gets a pointer to the first pixel of a particular row (Y) value, while the GET/PUT macros take just a column (X) value. - If the optional mask bitmap is not NULL, it is checked before deciding whether to alter a given pixel.
- Other functions can take apart or put together pixel values. There are macros for YUV & RGB conversion, etc. They are near the top of the file.
- If your algorithm needs temporary space it can use
scratchpad_alloc
orscratch_bitmap16
. The morph algorithm does this, using small scratch bitmap so that it can process the image a row at a time.
void shared_module_bitmapfilter_solarize( displayio_bitmap_t *bitmap, displayio_bitmap_t *mask, const mp_float_t threshold) { int threshold_i = (int32_t)MICROPY_FLOAT_C_FUN(round)(256 * threshold); switch (bitmap->bits_per_value) { default: mp_raise_ValueError(MP_ERROR_TEXT("unsupported bitmap depth")); case 16: { for (int y = 0, yy = bitmap->height; y < yy; y++) { uint16_t *row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(bitmap, y); for (int x = 0, xx = bitmap->width; x < xx; x++) { if (mask && common_hal_displayio_bitmap_get_pixel(mask, x, y)) { continue; // Short circuit. } int pixel = IMAGE_GET_RGB565_PIXEL_FAST(row_ptr, x); int y = COLOR_RGB565_TO_Y(pixel); if (y > threshold_i) { y = MIN(255, MAX(0, 2 * threshold_i - y)); int u = COLOR_RGB565_TO_U(pixel); int v = COLOR_RGB565_TO_V(pixel); pixel = COLOR_YUV_TO_RGB565(y, u, v); IMAGE_PUT_RGB565_PIXEL_FAST(row_ptr, x, pixel); } } } break; } } }
In the file shared-bindings/bitmapfilter/__init__.c, is the adapter function from CircuitPython to C. It opens with the C function definition, which will always have the following form. Next, the possible Python function arguments are declared, first as an enum{}
and second as an allowed_args[]
. The args[]
array is created to have the same number of elements as allowed_args[]
. Each element in the enum
must match the element of allowed_args[]
and can be used to index into the args[]
array. The function mp_arg_parse_all
takes care of converting the input arguments into the args[]
array.
STATIC mp_obj_t bitmapfilter_solarize(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { enum { ARG_bitmap, ARG_threshold, ARG_mask }; static const mp_arg_t allowed_args[] = { { MP_QSTR_bitmap, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } }, { MP_QSTR_threshold, MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } }, { 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);
Next, the required processing of each object into the correct C type, such as mp_float_t
or displayio_bitmap_t *
, and dealing with the case where an optional argument was not specified:
// (continued from above) mp_float_t threshold = (args[ARG_threshold].u_obj == NULL) ? MICROPY_FLOAT_CONST(0.5) : mp_obj_get_float(args[ARG_threshold].u_obj); mp_arg_validate_type(args[ARG_bitmap].u_obj, &displayio_bitmap_type, MP_QSTR_bitmap); displayio_bitmap_t *bitmap = MP_OBJ_TO_PTR(args[ARG_bitmap].u_obj); displayio_bitmap_t *mask = NULL; 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); }
Finally, the converted arguments are passed to the function that implements the filter, and the modified bitmap is used as the CircuitPython return value. Immediately following the function definition is a line to create the CircuitPython function object.
If your algorithm needs to return something other than the modified bitmap, then you would need to add calls to build the CircuitPython object corresponding to the output or result of the C image processing function, and return this instead of the bitmap argument.
// (continued from above) shared_module_bitmapfilter_solarize(bitmap, mask, threshold); return args[ARG_bitmap].u_obj; } MP_DEFINE_CONST_FUN_OBJ_KW(bitmapfilter_solarize_obj, 0, bitmapfilter_solarize);
The final element is an entry in the bitmapfilter's module globals table for the function, near the bottom of shared-bindings/bitmapfilter/__init__.c:
STATIC const mp_rom_map_elem_t bitmapfilter_module_globals_table[] = { // ... { MP_ROM_QSTR(MP_QSTR_solarize), MP_ROM_PTR(&bitmapfilter_solarize_obj) }, // ... };
At this point, you can compile your code and address any build errors that occur.
If you're using a Linux or Mac based development environment, you can also test your filter on a host computer before uploading firmware to a board. You can do this by building in the ports/unix subdirectory with a commandline like make -j8 VARIANT=coverage. The created program build-coverage/micropython has importable bitmapfilter and displayio modules (among others). You can run this micropython (including under the gdb debugger) for testing your algorithm on a host computer. By setting MICROPYPATH=/complete/path/to/circuitpython/tests/testlib in your shell environment, you will be able to import some useful routines for getting bitmap test data (import blinka_image
) and for printing out representations of bitmaps on screen (import dump_bitmap
)
You can run the test suite, including image processing tests, with make -j8 VARIANT=coverage test. This will run the tests including those in tests/circuitpython. Have a look at an existing test such as tests/circuitpython/bitmapfilter_solar.py and its "expected output" file tests/circuitpython/bitmapfilter_solar.py.exp. After some preliminaries, it creates a test bitmap with several color ramps, and runs the solarize algorithm on it. It dumps the image using Unicode characters that represent 5 brightness levels, showing the R, G, and B image channels separately:
print("solarize (masked)") bitmapfilter.solarize(b, mask=q) dump_bitmap_rgb_swapped(b)
In a proper, wide terminal window the results can be seen, though they can be difficult to interpret.
Text editor powered by tinymce.