基于深度优先的图片联通域算法

 

需求

产品这边需要我能够根据用户画笔轨迹来生成Rect,比如用户在Mask上绘制了3笔,那就是3个区域,但是如果增加一画将3根轨迹连接起来那就是1个区域,如果用橡皮擦断一分为二,那就是2个区域

这个需求一开始听起来感觉挺简单的,和遍历位图之类的没差,后面才发现有点难做,第一个方案采用二遍扫描法,第一遍扫出所有的轨迹起点,第二遍根据起点进行扫描计算出多个Rect

但是两边扫描法耗时太久了,我也不知道我有没有写对,反正扫出来要遍历图片一千多次,我的天这谁能接受啊

方案

经过和iOS端同事的探讨,以及结合GPT,最终得出了这种情况下使用洪水填充算法最佳,洪水填充也是深度优先搜索(DFS)搜索算法的一种应用

代码

#include <jni.h>
#include <android/bitmap.h>
#include <android/log.h>
#include <vector>
#include <stack>
//宏定义,计算最大最小值
#define max(a, b) ((a) >= (b) ? (a) : (b))
#define min(a, b) ((a) <= (b) ? (a) : (b))
#define  LOG_TAG    "native-dev"
#define  LOGI(...)  __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...)  __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
extern "C"
JNIEXPORT void JNICALL
Java_com_gowow_bitmapnativehelp_BitmapNativeHelp_dfsConnectedComponent(JNIEnv *env, jobject thiz,
                                                                       jobject bitmap,
                                                                       jobject rect_list) {
    AndroidBitmapInfo bitmapInfo;
    memset(&bitmapInfo, 0, sizeof(bitmapInfo));
    // 获取bitmap的信息
    AndroidBitmap_getInfo(env, bitmap, &bitmapInfo);
    // 位图像素数据
    void *bitmapPixels;
    // 锁定内存
    AndroidBitmap_lockPixels(env, bitmap, &bitmapPixels);
    if (bitmapInfo.format == ANDROID_BITMAP_FORMAT_A_8) { // 透明通道图
        uint8_t *pixelsMarks = new uint8_t[bitmapInfo.width * bitmapInfo.height];
        memset(pixelsMarks, 0, bitmapInfo.width * bitmapInfo.height);
        // 存储矩形区域列表
        // 获取 ArrayList 对象的类和方法
        jclass arrayListClass = env->GetObjectClass(rect_list);
        jmethodID add_method = env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
        for (int y = 0; y < bitmapInfo.height; ++y) {
            for (int x = 0; x < bitmapInfo.width; ++x) {
                //计算当前像素在一维数组中的索引
                int index = y * bitmapInfo.width + x;
                //如果当前像素已经被处理过(标记为1),则跳过,继续下一个像素。
                if (pixelsMarks[index]) {
                    continue;
                }
                //取当前像素的透明通道值
                uint8_t *a8Value = ((uint8_t *) bitmapPixels) + index;
                // 找到一个透明通道值大于0的像素,进行洪水填充
                if (*a8Value > 0) {
                    //创建一个栈 pixelStack 用于实现深度优先搜索。
                    std::stack<std::pair<int, int>> pixelStack;
                    //将当前像素的坐标推入栈。
                    pixelStack.push({x, y});
                    //初始化最小和最大坐标值,用于记录洪水填充区域的边界。
                    int minX = x;
                    int maxX = x;
                    int minY = y;
                    int maxY = y;
                    //进入洪水填充的循环,直到栈为空。
                    while (!pixelStack.empty()) {
                        //弹出栈顶元素,表示当前要处理的像素。
                        auto current = pixelStack.top();
                        pixelStack.pop();
                        //获取当前像素的坐标和在一维数组中的索引
                        int currentX = current.first;
                        int currentY = current.second;
                        int currentIndex = currentY * bitmapInfo.width + currentX;
                        //如果当前像素已经被处理过,跳过。
                        if (pixelsMarks[currentIndex]) {
                            continue;
                        }
                        //标记当前像素为已处理。
                        pixelsMarks[currentIndex] = 1;
                        // 更新边界坐标。
                        minX = min(minX, currentX);
                        minY = min(minY, currentY);
                        maxX = max(maxX, currentX);
                        maxY = max(maxY, currentY);
                       // 定义相邻像素的偏移数组,表示左、右、上、下四个方向。
                        int offsets[4][2] = {
                                {-1, 0},
                                {1,  0},
                                {0,  -1},
                                {0,  1}
                        }; // left, right, top, bottom
                        
                        //遍历相邻像素的偏移数组
                        for (const auto &offset: offsets) {
                            //计算相邻像素的坐标。
                            int newX = currentX + offset[0];
                            int newY = currentY + offset[1];
                            //检查相邻像素是否在图像范围内。
                            if (newX >= 0 && newX < bitmapInfo.width && newY >= 0 &&
                                newY < bitmapInfo.height) {
                                //计算相邻像素在一维数组中的索引。
                                int newIndex = newY * bitmapInfo.width + newX;
                                //如果相邻像素未被处理且透明通道值大于0,将相邻像素的坐标推入栈。
                                if (!pixelsMarks[newIndex]) {
                                    uint8_t alpha = *((uint8_t *) bitmapPixels + newIndex);
                                    if (alpha > 0) {
                                        pixelStack.push({newX, newY});
                                    }
                                }
                            }
                        }
                    }
                    // 结束一轮洪水填充,添加一个Rect
                    jclass rectClass = env->FindClass("android/graphics/Rect");
                    jmethodID rectConstructor = env->GetMethodID(rectClass, "<init>", "(IIII)V");
                    jobject rectObject = env->NewObject(rectClass, rectConstructor,
                                                        minX, minY,
                                                        maxX + 1, maxY + 1);
                    env->CallBooleanMethod(rect_list, add_method, rectObject);
                }
            }
        }

        // 释放内存
        delete[] pixelsMarks;
    } else {
        LOGE("for performance reasons, only transparent channel images are supported");
    }
    // 解锁内存
    AndroidBitmap_unlockPixels(env, bitmap);
}

解释

这段代码只会处理透明通道图片,这样性能会好一点