【android opencv学习笔记】Day 25: GrabCut 前景提取

GrabCut 前景提取算法

GrabCut 是 OpenCV 中基于图割(Graph Cuts)的交互式前景提取算法,通过用户简单标记(如矩形框或掩码),能自动迭代优化出物体与背景的精准分割边界,是静态图像抠图的经典方案。

本文将从原理讲解、API解析、Android完整源码实现三个维度,带你掌握这一算法的工程落地。


核心原理:迭代图割与颜色模型

1. 算法核心思想

GrabCut 基于图割算法,通过以下步骤实现前景/背景分割:

  1. 用户标记:用户提供初始标记(如包含前景的矩形框),矩形外像素标记为背景,矩形内像素标记为"可能前景"。
  2. 颜色建模:用高斯混合模型(GMM)分别对前景和背景的颜色分布建模,将像素划分为前景/背景组。
  3. 构建图模型:将图像像素构建为带权图,像素间的相似性(颜色、空间距离)作为边权,前景/背景标记作为终端节点。
  4. 图割优化:通过最小割算法找到前景与背景的最优分割边界,最小化能量函数(包含颜色相似性和边界平滑项)。
  5. 迭代优化:重复上述步骤,更新颜色模型和分割结果,直到收敛,最终得到精准的前景掩码。

2. 标记类型说明

OpenCV 定义了4种像素标记,用于描述像素的类别:

标记值 常量名 含义
0 GC_BGD 明确属于背景
1 GC_FGD 明确属于前景
2 GC_PR_BGD 可能属于背景
3 GC_PR_FGD 可能属于前景

OpenCV 核心 API 解析

1. GrabCut 核心函数

cpp 复制代码
void grabCut(
    InputArray img,               // 输入图像(8位3通道BGR)
    InputOutputArray mask,        // 输入/输出掩码(8位单通道,包含4种标记)
    Rect rect,                    // 包含前景的矩形框(仅在 GC_INIT_WITH_RECT 模式下有效)
    InputOutputArray bgdModel,    // 背景模型(算法内部使用,用户无需处理)
    InputOutputArray fgdModel,    // 前景模型(算法内部使用,用户无需处理)
    int iterCount,                // 迭代次数(通常5次足够)
    int mode = GC_INIT_WITH_RECT  // 初始化模式:矩形框/掩码
);

2. 关键参数说明

  • mask:既是输入也是输出,算法执行后会更新掩码的标记值,反映最终分割结果。
  • rect:矩形框仅在 GC_INIT_WITH_RECT 模式下有效,用于快速标记初始前景区域。
  • iterCount:迭代次数越多,分割结果越精准,但耗时也会增加,一般取 3-5 次即可。
  • mode:支持两种初始化模式:
    • GC_INIT_WITH_RECT:通过矩形框初始化,简单易用,适合快速分割。
    • GC_INIT_WITH_MASK:通过用户自定义掩码初始化,支持更精细的前景/背景标记。

3. 配套辅助 API

  • compare:根据掩码值筛选前景像素(如筛选 GC_PR_FGD 标记的像素)。
  • copyTo:根据掩码将前景像素复制到目标图像,生成抠图结果。

Android 完整项目实现

1. 布局文件:activity_main.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="8dp">

    <!-- 原图 -->
    <ImageView
        android:id="@+id/iv_original"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:scaleType="fitCenter"
        android:adjustViewBounds="true"/>

    <!-- 掩码图像 -->
    <ImageView
        android:id="@+id/iv_mask"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:layout_marginTop="4dp"
        android:scaleType="fitCenter"
        android:adjustViewBounds="true"/>

    <!-- 抠图结果 -->
    <ImageView
        android:id="@+id/iv_result"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:layout_marginTop="4dp"
        android:scaleType="fitCenter"
        android:adjustViewBounds="true"/>

</LinearLayout>

2. Kotlin 上层代码:MainActivity.kt

kotlin 复制代码
package com.nicoli.grabcutdemo

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Bundle
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
    companion object {
        init {
            System.loadLibrary("native-lib")
        }
    }

    // JNI 接口声明
    private external fun grabCutSegment(src: Bitmap, outMask: Bitmap, outResult: Bitmap)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 加载原图(包含前景物体的图像)
        val originalBitmap = BitmapFactory.decodeResource(resources, R.drawable.animals)

        // 创建输出位图
        val maskBitmap = Bitmap.createBitmap(originalBitmap.width, originalBitmap.height, Bitmap.Config.ARGB_8888)
        val resultBitmap = Bitmap.createBitmap(originalBitmap.width, originalBitmap.height, Bitmap.Config.ARGB_8888)

        // 执行 GrabCut 前景提取
        grabCutSegment(originalBitmap, maskBitmap, resultBitmap)

        // 显示结果
        findViewById<ImageView>(R.id.iv_original).setImageBitmap(originalBitmap)
        findViewById<ImageView>(R.id.iv_mask).setImageBitmap(maskBitmap)
        findViewById<ImageView>(R.id.iv_result).setImageBitmap(resultBitmap)
    }
}

3. C++ 核心算法:native-lib.cpp(逐行注释)

cpp 复制代码
#include <jni.h>
#include <opencv2/opencv.hpp>
#include <android/bitmap.h>

using namespace cv;
using namespace std;

// ====================== 工具函数:Bitmap ↔ Mat 转换 ======================
Mat bitmapToMat(JNIEnv *env, jobject bitmap) {
    AndroidBitmapInfo info;
    void* pixels;
    AndroidBitmap_getInfo(env, bitmap, &info);
    AndroidBitmap_lockPixels(env, bitmap, &pixels);

    Mat rgba(info.height, info.width, CV_8UC4, pixels);
    Mat bgr;
    cvtColor(rgba, bgr, COLOR_RGBA2BGR); // 转为BGR格式(OpenCV默认)
    AndroidBitmap_unlockPixels(env, bitmap);
    return bgr;
}

void matToBitmap(JNIEnv *env, const Mat& srcMat, jobject dstBitmap) {
    AndroidBitmapInfo info;
    void* pixels;
    AndroidBitmap_getInfo(env, dstBitmap, &info);
    AndroidBitmap_lockPixels(env, dstBitmap, &pixels);

    Mat rgba;
    if (srcMat.channels() == 1) {
        cvtColor(srcMat, rgba, COLOR_GRAY2RGBA);
    } else {
        cvtColor(srcMat, rgba, COLOR_BGR2RGBA);
    }
    memcpy(pixels, rgba.data, info.width * info.height * 4);
    AndroidBitmap_unlockPixels(env, dstBitmap);
}

// ====================== GrabCut 前景提取核心函数 ======================
void grabCutForeground(const Mat& srcBgr, Mat& outMask, Mat& outResult) {
    // 1. 定义包含前景的矩形框(根据图像内容调整)
    Rect rect(50, 70, srcBgr.cols - 100, srcBgr.rows - 100);

    // 2. 初始化掩码和模型
    Mat mask(srcBgr.size(), CV_8UC1, Scalar(GC_BGD));
    mask(rect).setTo(GC_PR_FGD); // 矩形内标记为可能前景

    Mat bgModel, fgdModel;

    // 3. 执行 GrabCut 算法
    grabCut(srcBgr, mask, rect, bgModel, fgdModel, 5, GC_INIT_WITH_RECT);

    // 4. 提取前景掩码(GC_FGD 或 GC_PR_FGD)
    Mat foregroundMask;
    compare(mask, GC_PR_FGD, foregroundMask, CMP_EQ);
    foregroundMask |= (mask == GC_FGD);

    // 5. 生成抠图结果(白色背景)
    Mat foreground(srcBgr.size(), CV_8UC3, Scalar(255, 255, 255));
    srcBgr.copyTo(foreground, foregroundMask);

    // 6. 转换掩码为可视化灰度图
    outMask = mask * 85; // 标记值0→0,1→85,2→170,3→255,方便显示
    outResult = foreground;
}

// ====================== JNI 接口 ======================
extern "C" JNIEXPORT void JNICALL
Java_com_nicoli_grabcutdemo_MainActivity_grabCutSegment
(JNIEnv *env, jobject thiz, jobject srcBitmap, jobject outMask, jobject outResult) {
    // 1. 转换Bitmap为OpenCV Mat
    Mat srcBgr = bitmapToMat(env, srcBitmap);

    // 2. 执行GrabCut前景提取
    Mat maskMat, resultMat;
    grabCutForeground(srcBgr, maskMat, resultMat);

    // 3. 转换结果回Bitmap
    matToBitmap(env, maskMat, outMask);
    matToBitmap(env, resultMat, outResult);
}

效果与参数详解

1. 运行效果

  • 原图:包含前景物体和背景的彩色图像。
  • 掩码图像:不同灰度值表示像素标记,黑色为背景、白色为前景,中间灰度为不确定区域。
  • 抠图结果:前景物体被完整提取,背景替换为白色,边界清晰自然。

2. 关键参数说明

  • 矩形框位置:必须完全包含前景物体,且尽量靠近物体边缘,避免过多背景像素影响分割效果。
  • 迭代次数:一般取 3-5 次即可,对于复杂场景(如前景背景颜色相似),可增加到 8-10 次。
  • 掩码模式 :如果需要更精细的控制,可使用 GC_INIT_WITH_MASK 模式,手动标记部分前景/背景像素,提升分割精度。

扩展应用与优化

1. 交互式标记优化

在实际项目中,可通过用户触摸交互,让用户在图像上绘制前景/背景标记,再调用 GrabCut 算法分割物体,实现更精准的抠图效果。

2. 后处理优化

  • 边缘平滑:分割后可对掩码进行形态学开/闭运算,去除边缘毛刺,填充内部孔洞。
  • 颜色校正:提取前景后,可调整前景物体的亮度、对比度,使其与新背景更融合。

3. 性能优化

  • 降采样处理:对大图像先降采样再执行 GrabCut,提升处理速度,分割完成后再将掩码上采样回原图尺寸。
  • ROI 裁剪:根据用户标记的矩形框,裁剪出包含前景的局部区域,仅对局部区域执行分割,减少计算量。

总结

GrabCut 算法通过用户简单标记和迭代图割优化,实现了高精度的前景/背景分割,是静态图像抠图的经典方案。

核心在于初始标记的设置,合理的矩形框或掩码标记能显著提升分割效果。在文本识别、图像编辑、目标检测等场景中,GrabCut 可作为强大的图像预处理工具,提取目标物体,去除背景干扰。

相关推荐
承渊政道14 小时前
我的创作纪念日写在创作第256天:从第一篇C语言博客,到一路向前的自己!
c语言·开发语言·笔记·学习·学习方法
脑子不好真君14 小时前
build-your-own-x学习笔记——Updating
笔记·学习
Cat_Rocky14 小时前
CICD-DevOps简单学习
运维·学习·devops
nashane14 小时前
HarmonyOS 6学习:解决非媒体文件下载后用户不可见的问题
学习·华为·harmonyos
知识分享小能手14 小时前
Flask入门学习教程,从入门到精通,Flask智能租房——详情页完整知识点详解(8)
python·学习·flask
吃好睡好便好14 小时前
矩阵的求幂运算
人工智能·学习·线性代数·算法·matlab·矩阵
Kapaseker14 小时前
Kotlin 的扩展没有你看上去的那么简单
android·kotlin
一颗宁檬不酸14 小时前
Android多线程实现方式
android
weixin_4280053014 小时前
C#调用 AI学习从0开始-第2阶段(Function Calling+工具调用智能体)-第8天Function Calling原理
人工智能·学习·c#·functioncalling