diff --git a/src/omv/img/imlib.c b/src/omv/img/imlib.c index 9688561ee..2142e3d4c 100644 --- a/src/omv/img/imlib.c +++ b/src/omv/img/imlib.c @@ -126,36 +126,6 @@ static void imlib_read_pixels(FIL *fp, image_t *img, int line_start, int line_en } } -// Main Ram = 196,608 bytes -// - -// struct framebuffer = 20 bytes -// FB_JPEG_OFFS_SIZE = 1,024 bytes -// GRAYSCALE 320x240x1 image = 76,800 bytes -// fb_alloc = 4 bytes -// flash cache = 16,384 bytes -// = -// 102,376 bytes -// / -// GRAYSCALE 320x1 lines -// = -// 319 GRAYSCALE 320x1 lines -#define GS_LINE_BUFFER_SIZE (64) // double rgb565 - -// Main Ram = 196,608 bytes -// - -// struct framebuffer = 20 bytes -// FB_JPEG_OFFS_SIZE = 1,024 bytes -// RGB565 320x240x2 image = 153,600 bytes -// fb_alloc = 4 bytes -// flash cache = 16,384 bytes -// = -// 25,576 bytes -// / -// RGB565 320x2 lines -// = -// 39 RGB565 320x2 lines -#define RGB565_LINE_BUFFER_SIZE (32) - void imlib_image_operation(image_t *img, const char *path, image_t *other, line_op_t op) { if (path) { @@ -631,6 +601,102 @@ float imlib_orientation_degrees(image_t *img, int *sum, int *x_center, int *y_ce return (imlib_orientation_radians(img, sum, x_center, y_center, r) * 180.0) / M_PI; } +static void imlib_erode_dilate(image_t *img, int ksize, int threshold, int e_or_d) +{ + int brows = ksize + 1; + uint8_t *buffer = fb_alloc(img->w * brows * img->bpp); + if (IM_IS_GS(img)) { + for (int y=0; yh; y++) { + for (int x=0; xw; x++) { + // We're writing into the buffer like if it were a window. + int buffer_idx = ((y%brows)*img->w)+x; + buffer[buffer_idx] = IM_GET_GS_PIXEL(img, x, y); + if ((!!buffer[buffer_idx]) == e_or_d) { + continue; // short circuit (makes this very fast - usually) + } + int acc = 0; + for (int j=-ksize; j<=ksize; j++) { + for (int k=-ksize; k<=ksize; k++) { + if (IM_X_INSIDE(img, x+k) && IM_Y_INSIDE(img, y+j)) { + acc += !!IM_GET_GS_PIXEL(img, x+k, y+j); + } else { // outer pixels should not affect result. + acc += e_or_d ? 0 : 1; + // 1 for erode prevents acc from being lower. + // 0 for dilate prevents acc from being higher. + } + } + } + buffer[buffer_idx] = (acc >= threshold) ? 0xFF : 0; + } + if (y>=ksize) { + memcpy(img->pixels+((y-ksize)*img->w), + buffer+(((y-ksize)%brows)*img->w), + img->w * sizeof(uint8_t)); + } + } + for (int y=img->h-ksize; yh; y++) { + memcpy(img->pixels+(y*img->w), + buffer+((y%brows)*img->w), + img->w * sizeof(uint8_t)); + } + } else { + for (int y=0; yh; y++) { + for (int x=0; xw; x++) { + // We're writing into the buffer like if it were a window. + int buffer_idx = ((y%brows)*img->w)+x; + ((uint16_t *) buffer)[buffer_idx] = IM_GET_RGB565_PIXEL(img, x, y); + if ((!!((uint16_t *) buffer)[buffer_idx]) == e_or_d) { + continue; // short circuit (makes this very fast - usually) + } + int acc = 0; + for (int j=-ksize; j<=ksize; j++) { + for (int k=-ksize; k<=ksize; k++) { + if (IM_X_INSIDE(img, x+k) && IM_Y_INSIDE(img, y+j)) { + acc += !!IM_GET_RGB565_PIXEL(img, x+k, y+j); + } else { // outer pixels should not affect result. + acc += e_or_d ? 0 : 1; + // 1 for erode prevents acc from being lower. + // 0 for dilate prevents acc from being higher. + } + } + } + ((uint16_t *) buffer)[buffer_idx] = (acc >= threshold) ? 0xFFFF : 0; + } + if (y>=ksize) { + memcpy(((uint16_t *) img->pixels)+((y-ksize)*img->w), + ((uint16_t *) buffer)+(((y-ksize)%brows)*img->w), + img->w * sizeof(uint16_t)); + } + } + for (int y=img->h-ksize; yh; y++) { + memcpy(((uint16_t *) img->pixels)+(y*img->w), + ((uint16_t *) buffer)+((y%brows)*img->w), + img->w * sizeof(uint16_t)); + } + } + fb_free(); +} + +void imlib_erode(image_t *img, int ksize, int threshold) +{ + // Threshold should be equal to ((ksize*2)+1)*((ksize*2)+1) + // for normal operation. E.g. for ksize==3 -> threshold==9 + // Basically you're adjusting the number of pixels that + // must be set in the kernel for the output to be 1. + // Erode normally requires all pixels to be 1. + imlib_erode_dilate(img, ksize, threshold, 0); +} + +void imlib_dilate(image_t *img, int ksize, int threshold) +{ + // Threshold should be equal to 1 + // for normal operation. E.g. for ksize==3 -> threshold==1 + // Basically you're adjusting the number of pixels that + // must be set in the kernel for the output to be 1. + // Dilate normally requires one pixel to be 1. + imlib_erode_dilate(img, ksize, threshold, 1); +} + //////////////////////////////////////////////////////////////////////////////// void imlib_negate(image_t *img) @@ -782,70 +848,6 @@ void imlib_grayscale_to_rgb565(struct image *image) #endif } -void imlib_erode(image_t *src, int ksize) -{ - int c = ksize/2;// center pixel - int k_rows = (ksize+1)*2; - uint8_t *dst = fb_alloc0(src->w * k_rows); - - for (int y=1; yh-ksize; y++) { - for (int x=1; xw-ksize; x++) { - int i = y*src->w+x; - int di = (y%k_rows)*src->w+x; - if ((dst[di] = src->pixels[i])==0) { - continue; - } - - for (int j=-c; jpixels[(y+j)*src->w+x+k]==0) { - dst[di]=0; - goto done; - } - } - } - done:; - } - - if ((y+1)%k_rows==0) { - memcpy(src->pixels+((y/k_rows)*k_rows*src->w), dst, src->w*k_rows); - } - } - fb_free(); -} - -void imlib_dilate(image_t *src, int ksize) -{ - int c = ksize/2;// center pixel - int k_rows = (ksize+1)*2; - uint8_t *dst = fb_alloc0(src->w * k_rows); - - for (int y=1; yh-ksize; y++) { - for (int x=1; xw-ksize; x++) { - int i = y*src->w+x; - int di = (y%k_rows)*src->w+x; - if ((dst[di] = src->pixels[i])) { - continue; - } - - for (int j=-c; jpixels[(y+j)*src->w+x+k]) { - dst[di]=src->pixels[(y+j)*src->w+x+k]; - goto done; - } - } - } - done:; - } - - if ((y+1)%k_rows==0) { - memcpy(src->pixels+((y/k_rows)*k_rows*src->w), dst, src->w*k_rows); - } - } - fb_free(); -} - #ifdef OPENMV1 void imlib_threshold(image_t *src, image_t *dst, color_t *color, int color_size, int threshold) { diff --git a/src/omv/img/imlib.h b/src/omv/img/imlib.h index a9ba69478..f5b686fd4 100644 --- a/src/omv/img/imlib.h +++ b/src/omv/img/imlib.h @@ -146,6 +146,36 @@ extern const uint8_t g826_table[256]; __typeof__ (img1) _img1 = (img1); \ (_img0->w==_img1->w)&&(_img0->h==_img1->h)&&(_img0->bpp==_img1->bpp); }) +// Main Ram = 196,608 bytes +// - +// struct framebuffer = 20 bytes +// FB_JPEG_OFFS_SIZE = 1,024 bytes +// GRAYSCALE 320x240x1 image = 76,800 bytes +// fb_alloc = 4 bytes +// flash cache = 16,384 bytes +// = +// 102,376 bytes +// / +// GRAYSCALE 320x1 lines +// = +// 319 GRAYSCALE 320x1 lines +#define GS_LINE_BUFFER_SIZE (64) // double rgb565 + +// Main Ram = 196,608 bytes +// - +// struct framebuffer = 20 bytes +// FB_JPEG_OFFS_SIZE = 1,024 bytes +// RGB565 320x240x2 image = 153,600 bytes +// fb_alloc = 4 bytes +// flash cache = 16,384 bytes +// = +// 25,576 bytes +// / +// RGB565 320x2 lines +// = +// 39 RGB565 320x2 lines +#define RGB565_LINE_BUFFER_SIZE (32) + typedef struct size { int w; int h; @@ -370,6 +400,8 @@ int imlib_pixels(image_t *img, rectangle_t *r); int imlib_centroid(image_t *img, int *x_center, int *y_center, rectangle_t *r); float imlib_orientation_radians(image_t *img, int *sum, int *x_center, int *y_center, rectangle_t *r); float imlib_orientation_degrees(image_t *img, int *sum, int *x_center, int *y_center, rectangle_t *r); +void imlib_erode(image_t *img, int ksize, int threshold); +void imlib_dilate(image_t *img, int ksize, int threshold); /* Background Subtraction (Frame Differencing) functions */ void imlib_negate(image_t *img); @@ -391,8 +423,6 @@ void imlib_rgb_to_hsv(struct color *rgb, struct color *hsv); int imlib_image_mean(struct image *src); void imlib_histeq(struct image *src); void imlib_median_filter(image_t *src, int r); -void imlib_erode(image_t *src, int ksize); -void imlib_dilate(image_t *src, int ksize); void imlib_threshold(image_t *src, image_t *dst, color_t *color, int color_size, int threshold); void imlib_rainbow(image_t *src, struct image *dst); array_t *imlib_count_blobs(struct image *image); diff --git a/src/omv/py/py_image.c b/src/omv/py/py_image.c index 52cec8120..1edc0e865 100644 --- a/src/omv/py/py_image.c +++ b/src/omv/py/py_image.c @@ -575,6 +575,34 @@ static mp_obj_t py_image_orientation_degrees(uint n_args, const mp_obj_t *args, {mp_obj_new_int(sum), mp_obj_new_int(x), mp_obj_new_int(y), mp_obj_new_float(o)}); } +static mp_obj_t py_image_erode(uint n_args, const mp_obj_t *args, mp_map_t *kw_args) +{ + image_t *arg_img = py_image_cobj(args[0]); + PY_ASSERT_FALSE_MSG(IM_IS_JPEG(arg_img), + "Operation not supported on JPEG"); + + int ksize = mp_obj_get_int(args[1]); + PY_ASSERT_TRUE_MSG(ksize >= 0, "Kernel Size must be >= 0"); + imlib_erode(arg_img, ksize, + py_helper_lookup_int(kw_args, + MP_OBJ_NEW_QSTR(MP_QSTR_threshold), ((ksize*2)+1)*((ksize*2)+1))); + return mp_const_none; +} + +static mp_obj_t py_image_dilate(uint n_args, const mp_obj_t *args, mp_map_t *kw_args) +{ + image_t *arg_img = py_image_cobj(args[0]); + PY_ASSERT_FALSE_MSG(IM_IS_JPEG(arg_img), + "Operation not supported on JPEG"); + + int ksize = mp_obj_get_int(args[1]); + PY_ASSERT_TRUE_MSG(ksize >= 0, "Kernel Size must be >= 0"); + imlib_dilate(arg_img, ksize, + py_helper_lookup_int(kw_args, + MP_OBJ_NEW_QSTR(MP_QSTR_threshold), 1)); + return mp_const_none; +} + static mp_obj_t py_image_negate(mp_obj_t img_obj) { image_t *arg_img = py_image_cobj(img_obj); @@ -845,32 +873,6 @@ static mp_obj_t py_image_compress(mp_obj_t image_obj, mp_obj_t quality) return py_image_from_struct(&cimage); } -static mp_obj_t py_image_erode(mp_obj_t image_obj, mp_obj_t ksize_obj) -{ - image_t *image = NULL; - image = py_image_cobj(image_obj); - - /* sanity checks */ - PY_ASSERT_TRUE_MSG(image->bpp == 1, - "This function is only supported on GRAYSCALE images"); - - imlib_erode(image, mp_obj_get_int(ksize_obj)); - return mp_const_none; -} - -static mp_obj_t py_image_dilate(mp_obj_t image_obj, mp_obj_t ksize_obj) -{ - image_t *image = NULL; - image = py_image_cobj(image_obj); - - /* sanity checks */ - PY_ASSERT_TRUE_MSG(image->bpp == 1, - "This function is only supported on GRAYSCALE images"); - - imlib_dilate(image, mp_obj_get_int(ksize_obj)); - return mp_const_none; -} - static mp_obj_t py_image_find_blobs(mp_obj_t image_obj) { // Get image pointer @@ -1103,6 +1105,8 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_KW(py_image_pixels_obj, 1, py_image_pixels); STATIC MP_DEFINE_CONST_FUN_OBJ_KW(py_image_centroid_obj, 1, py_image_centroid); STATIC MP_DEFINE_CONST_FUN_OBJ_KW(py_image_orientation_radians_obj, 1, py_image_orientation_radians); STATIC MP_DEFINE_CONST_FUN_OBJ_KW(py_image_orientation_degrees_obj, 1, py_image_orientation_degrees); +STATIC MP_DEFINE_CONST_FUN_OBJ_KW(py_image_erode_obj, 2, py_image_erode); +STATIC MP_DEFINE_CONST_FUN_OBJ_KW(py_image_dilate_obj, 2, py_image_dilate); /* Background Subtraction (Frame Differencing) functions */ STATIC MP_DEFINE_CONST_FUN_OBJ_1(py_image_negate_obj, py_image_negate); STATIC MP_DEFINE_CONST_FUN_OBJ_2(py_image_difference_obj, py_image_difference); @@ -1116,8 +1120,6 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_1(py_image_histeq_obj, py_image_histeq); STATIC MP_DEFINE_CONST_FUN_OBJ_KW(py_image_median_obj, 1, py_image_median); STATIC MP_DEFINE_CONST_FUN_OBJ_3(py_image_threshold_obj, py_image_threshold); STATIC MP_DEFINE_CONST_FUN_OBJ_1(py_image_rainbow_obj, py_image_rainbow); -STATIC MP_DEFINE_CONST_FUN_OBJ_2(py_image_erode_obj, py_image_erode); -STATIC MP_DEFINE_CONST_FUN_OBJ_2(py_image_dilate_obj, py_image_dilate); STATIC MP_DEFINE_CONST_FUN_OBJ_2(py_image_compress_obj, py_image_compress); STATIC MP_DEFINE_CONST_FUN_OBJ_1(py_image_find_blobs_obj, py_image_find_blobs); @@ -1157,6 +1159,8 @@ static const mp_map_elem_t locals_dict_table[] = { {MP_OBJ_NEW_QSTR(MP_QSTR_centroid), (mp_obj_t)&py_image_centroid_obj}, {MP_OBJ_NEW_QSTR(MP_QSTR_orientation_radians), (mp_obj_t)&py_image_orientation_radians_obj}, {MP_OBJ_NEW_QSTR(MP_QSTR_orientation_degrees), (mp_obj_t)&py_image_orientation_degrees_obj}, + {MP_OBJ_NEW_QSTR(MP_QSTR_erode), (mp_obj_t)&py_image_erode_obj}, + {MP_OBJ_NEW_QSTR(MP_QSTR_dilate), (mp_obj_t)&py_image_dilate_obj}, /* Background Subtraction (Frame Differencing) functions */ {MP_OBJ_NEW_QSTR(MP_QSTR_negate), (mp_obj_t)&py_image_negate_obj}, {MP_OBJ_NEW_QSTR(MP_QSTR_difference), (mp_obj_t)&py_image_difference_obj}, @@ -1170,10 +1174,7 @@ static const mp_map_elem_t locals_dict_table[] = { {MP_OBJ_NEW_QSTR(MP_QSTR_median), (mp_obj_t)&py_image_median_obj}, {MP_OBJ_NEW_QSTR(MP_QSTR_threshold), (mp_obj_t)&py_image_threshold_obj}, {MP_OBJ_NEW_QSTR(MP_QSTR_rainbow), (mp_obj_t)&py_image_rainbow_obj}, - {MP_OBJ_NEW_QSTR(MP_QSTR_erode), (mp_obj_t)&py_image_erode_obj}, - {MP_OBJ_NEW_QSTR(MP_QSTR_dilate), (mp_obj_t)&py_image_dilate_obj}, {MP_OBJ_NEW_QSTR(MP_QSTR_compress), (mp_obj_t)&py_image_compress_obj}, - /* objects/feature detection */ {MP_OBJ_NEW_QSTR(MP_QSTR_find_blobs), (mp_obj_t)&py_image_find_blobs_obj}, {MP_OBJ_NEW_QSTR(MP_QSTR_find_template), (mp_obj_t)&py_image_find_template_obj}, diff --git a/usr/tests/test_erode_and_dilate.py b/usr/tests/test_erode_and_dilate.py new file mode 100644 index 000000000..4bcf7da85 --- /dev/null +++ b/usr/tests/test_erode_and_dilate.py @@ -0,0 +1,24 @@ +import pyb, sensor, image, math +sensor.reset() +sensor.set_framesize(sensor.QVGA) +grayscale_thres = (170, 255) +rgb565_thres = (70, 100, -128, 127, -128, 127) +while(True): + sensor.set_pixformat(sensor.GRAYSCALE) + for i in range(100): + img = sensor.snapshot() + img.binary([grayscale_thres]) + img.erode(2) + for i in range(100): + img = sensor.snapshot() + img.binary([grayscale_thres]) + img.dilate(2) + sensor.set_pixformat(sensor.RGB565) + for i in range(100): + img = sensor.snapshot() + img.binary([rgb565_thres]) + img.erode(2) + for i in range(100): + img = sensor.snapshot() + img.binary([rgb565_thres]) + img.dilate(2)