【短道速滑八】圆形半径的图像最大值和最小值算法的实现及其实时优化(非二值图)
在图像处理中,我们可以看到很多函数都是带有半径这个参数的,不过99%的情况下这个半径其实都是矩形的意思,在目前我所实现的算法中,也只有二值图像的最大值和最小值我实现了圆形半径的优化,可以参考: 一文,这里通过特征图实现了圆形半径算法的O(1)算法。
在实际的需求中,还有很多场合下需要圆形的最值算法,我们目前知道的有几个算法,比如在Photoshop中,选区的扩展和收缩,在图层样式的描边算法中等等,都不是普通的矩形半径。所以这个算法的优化也有着非常重要的意义。
在可以搜索到的资料中,我曾经在2个地方看到关于这个算法的优化实现,一个是ImageJ中,其UI界面下的功能如下所示:
我们尝试了下,在小半径下,这速度还是比较快的,\但是半径稍大时,就相对来说有点慢了。这个相关的代码可以在RankFilters.java中找到。
还有一个可以参考的代码是在GIMP中,这个也是我无意中寻得的,其代码路径分别在:
gimp-master\app\operations\gimpoperationgrow.c 和 gimp-master\app\operations\gimpoperationshrink.c文件中。
在GIMP中这个函数的主要作用也是对选区进行收缩和扩展。
原始选区 GIMP的扩展50像素 PS的扩展50像素
由以上图像看上去,似乎PS的扩展选区用的还是菱形半径,而不是圆形。
我们这里参考了GIMP的代码,以grow为例,其核心代码如下:
static void compute_border (gint16 *circ, guint16 xradius, guint16 yradius) { gint32 i; gint32 diameter = xradius * 2 + 1; gdouble tmp; for (i = 0; i < diameter; i++) { if (i > xradius) tmp = (i - xradius) - 0.5; else if (i < xradius) tmp = (xradius - i) - 0.5; else tmp = 0.0; circ[i] = RINT (yradius / (gdouble) xradius * sqrt (SQR (xradius) - SQR (tmp))); } } static inline void rotate_pointers (gfloat **p, guint32 n) { guint32 i; gfloat *tmp; tmp = p[0]; for (i = 0; i < n - 1; i++) p[i] = p[i + 1]; p[i] = tmp; } static gboolean gimp_operation_grow_process (GeglOperation *operation, GeglBuffer *input, GeglBuffer *output, const GeglRectangle *roi, gint level) { /* Any bugs in this function are probably also in thin_region. * Blame all bugs in this function on jaycox@gimp.org */ GimpOperationGrow *self = GIMP_OPERATION_GROW (operation); const Babl *input_format = gegl_operation_get_format (operation, "input"); const Babl *output_format = gegl_operation_get_format (operation, "output"); gint32 i, j, x, y; gfloat **buf; /* caches the region's pixel data */ gfloat *out; /* holds the new scan line we are computing */ gfloat **max; /* caches the largest values for each column */ gint16 *circ; /* holds the y coords of the filter's mask */ gfloat last_max; gint16 last_index; gfloat *buffer; max = g_new (gfloat *, roi->width + 2 * self->radius_x); buf = g_new (gfloat *, self->radius_y + 1); for (i = 0; i < self->radius_y + 1; i++) buf[i] = g_new (gfloat, roi->width); buffer = g_new (gfloat, (roi->width + 2 * self->radius_x) * (self->radius_y + 1)); for (i = 0; i < roi->width + 2 * self->radius_x; i++) { if (i < self->radius_x) max[i] = buffer; else if (i < roi->width + self->radius_x) max[i] = &buffer[(self->radius_y + 1) * (i - self->radius_x)]; else max[i] = &buffer[(self->radius_y + 1) * (roi->width + self->radius_x - 1)]; for (j = 0; j < self->radius_y + 1; j++) max[i][j] = 0.0; } /* offset the max pointer by self->radius_x so the range of the * array is [-self->radius_x] to [roi->width + self->radius_x] */ max += self->radius_x; out = g_new (gfloat, roi->width); circ = g_new (gint16, 2 * self->radius_x + 1); compute_border (circ, self->radius_x, self->radius_y); /* offset the circ pointer by self->radius_x so the range of the * array is [-self->radius_x] to [self->radius_x] */ circ += self->radius_x; memset (buf[0], 0, roi->width * sizeof (gfloat)); for (i = 0; i < self->radius_y && i < roi->height; i++) /* load top of image */ gegl_buffer_get (input, GEGL_RECTANGLE (roi->x, roi->y + i, roi->width, 1), 1.0, input_format, buf[i + 1], GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE); for (x = 0; x < roi->width; x++) /* set up max for top of image */ { max[x][0] = 0.0; /* buf[0][x] is always 0 */ max[x][1] = buf[1][x]; /* MAX (buf[1][x], max[x][0]) always = buf[1][x]*/ for (j = 2; j < self->radius_y + 1; j++) max[x][j] = MAX (buf[j][x], max[x][j - 1]); } for (y = 0; y < roi->height; y++) { rotate_pointers (buf, self->radius_y + 1); if (y < roi->height - (self->radius_y)) gegl_buffer_get (input, GEGL_RECTANGLE (roi->x, roi->y + y + self->radius_y, roi->width, 1), 1.0, input_format, buf[self->radius_y], GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE); else memset (buf[self->radius_y], 0, roi->width * sizeof (gfloat)); for (x = 0; x < roi->width; x++) /* update max array */ { for (i = self->radius_y; i > 0; i--) max[x][i] = MAX (MAX (max[x][i - 1], buf[i - 1][x]), buf[i][x]); max[x][0] = buf[0][x]; } last_max = max[0][circ[-1]]; last_index = 1; for (x = 0; x < roi->width; x++) /* render scan line */ { last_index--; if (last_index >= 0) { if (last_max >= 1.0) { out[x] = 1.0; } else { last_max = 0.0; for (i = self->radius_x; i >= 0; i--) if (last_max < max[x + i][circ[i]]) { last_max = max[x + i][circ[i]]; last_index = i; } out[x] = last_max; } } else { last_index = self->radius_x; last_max = max[x + self->radius_x][circ[self->radius_x]]; for (i = self->radius_x - 1; i >= -self->radius_x; i--) if (last_max < max[x + i][circ[i]]) { last_max = max[x + i][circ[i]]; last_index = i; } out[x] = last_max; } } gegl_buffer_set (output, GEGL_RECTANGLE (roi->x, roi->y + y, roi->width, 1), 0, output_format, out, GEGL_AUTO_ROWSTRIDE); } /* undo the offsets to the pointers so we can free the malloced memory */ circ -= self->radius_x; max -= self->radius_x; g_free (circ); g_free (buffer); g_free (max); for (i = 0; i < self->radius_y + 1; i++) g_free (buf[i]); g_free (buf); g_free (out); return TRUE; }
代码量不多,虽然不是纯C的代码,但是也是能看懂意思。
那么在主函数里,buf 内存的大小是(R + 1) * Width, 他实际上是动态保存了R+1行像素对应的最大值,如下所示
6 buf[0] = 0
4 buf[Width] = Max(6, 4)
7 buf[Width * 2] = Max(6, 4, 7)
如果第一列的数值为分别为6/4/7,则 buf[0] = 0, buf[Width] = Max(6, 4),即半径为1时的列最大值, buf[Width * 2] = Max(6, 4, 7),即半径为2时的列最大值。
如果计算了一整行的这种不同半径的最大值,那么对于一个圆形半径,我们只要计算沿着行方向上不同半径组合的最大值即可以得到圆半径内的最大值。
在代码中,compute_border就是计算圆形半径内每列或者每行的上下对称尺寸,这样,沿着行方向分别取不同半径的最大值做比较即可。
但是在上面代码里,没有直接这样,而是做了一定的优化,如下图所示:
如左图所示,在水平方向移动计算最大值时,当移动一个像素时,可以看到右侧红色的半圆(少一个像素)的左半部分完全在左侧的黄色圆形的内部,所以如果我们的黄色圆内的最大值已经在黄色圆的右侧,那么在计算红色圆内最大值的就没有必要遍历整个圆了,只需要计算右侧的半圆,那么这有50%的概率会发生这种事情,可以一定程度的降低计算量。
接着还有一个重要的优化措施,就是在更新每行的新的最值列表时,不是每行的都重新计算,而是基于上一行的结果进行简单的更新,原理如下所示:
// // 5 // 8 // 6 6 // 4 8 4 // 7 8 7 7 // 5 8 7 5 // 7 7 7 7 // 10 10 10 // 8 10
上述是某一列数的更新过程,注意半径为2的情况。
这个就是下面的代码所要表达的思想:
for (x = 0; x < roi->width; x++) /* update max array */ { for (i = self->radius_y; i > 0; i--) max[x][i] = MAX (MAX (max[x][i - 1], buf[i - 1][x]), buf[i][x]); max[x][0] = buf[0][x]; }
可能需要坐下来好好地想一想才能明白是咋回事的。
上面的GIMP代码写的很晦涩的,可以自己做点优化和美化吧。我测试了下,对于一副1000*1000的灰度图,半径为1时,耗时在2ms,半径为10时,耗时120ms,半径20时,耗时180ms,半径100时,耗时590ms。
当半径比较大时,速度还是有点慢的。
我们尝试用SIMD指令进行优化,这个有两点可以说的。一个是更新每行的新的最值列表时,这个代码很明显可以直接用简单的simd并行优化,那么接着就是根据列最值获得园内的最大值,这个时候就不要用上述半圆内优化的算法了,直接用simd优化最原始的算法即可。要知道_mm_max_epu8可以一次性处理16个字节的比较呢。
经过测试,同样的大小图,SIMD指令优化后,半径为1时,耗时在2ms,半径为10时,耗时8ms,半径20时,耗时10ms,半径100时,耗时28ms,半径越大时提速比几乎达到了20倍。
其实仔细思考啊,这个算法只要稍微改造下compute_border 函数还可以实现椭圆、菱形,平行四边形等对称形状的最值的。
Demo下载地址:https://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar
如果想时刻关注本人的最新文章,也可关注公众号: