学习 Android (十八) 学习 OpenCV (三)

学习 Android (十八) 学习 OpenCV (三)

在上一章节,我们进一步的对 OpenCV SDK 中的 API 有了更近一步的,接下来我们继续学习

15. 椒盐噪声

15.1 什么是椒盐噪声

"椒盐噪声"这个名字非常形象地描述了这种噪声的特点:

  • 椒 (Pepper) : 代表黑色的噪点(像素值为0)。想象一下撒在图像上的黑胡椒粒。

  • 盐 (Salt) : 代表白色的噪点(像素值为255)。想象一下撒在图像上的盐粒。

因此,椒盐噪声表现为在图像上随机出现的、孤立的纯黑或纯白像素点。这种噪声通常由图像传感器、传输信道或解码处理错误引起。

15.2 如何 "制造" 椒盐噪声

思路:随机生成掩膜 → 小于阈值的像素置白(salt)、另一个掩膜置黑(pepper)。

要点:

  • 图像用 CV_8UC3(BGR)CV_8UC1(Gray)

  • Core.randu(mask, 0, 255) 生成均匀噪声;

  • Imgproc.threshold 把一定比例的随机值变为 255 的二值掩膜

  • Mat.setTo(Scalar, mask) 在掩膜处写入 255(白)或 0(黑)。

  • 对彩色图建议同一个掩膜作用于三通道(避免彩斑)。

其数学表达式可以表示为:

椒盐噪声的生成遵循一个简单的随机过程:

  1. 确定噪声密度:确定需要添加噪声的像素比例(例如,噪声密度为5%)。

  2. 随机选择像素点:在图像中随机选择相应数量的像素点。

  3. 分配噪声类型:为每个选中的像素点随机分配"椒"或"盐"噪声。

  4. 修改像素值:将选中的像素点值改为0(椒)或255(盐)。

其数学表达式可以表示为:

其中 I′(x,y)I'(x,y)I′(x,y) 是加噪后的像素值,I(x,y)I(x,y)I(x,y) 是原始像素值,PpepperP_{\text{pepper}}Ppepper 和 PsaltP_{\text{salt}}Psalt 分别是椒和盐噪声的概率。

15.3 如何去除椒盐噪声

  1. 中值滤波(首选)

    java 复制代码
    Imgproc.medianBlur(srcOrNoisy, dst, ksize);  // ksize 必须为奇数,3/5/7...
    • 优点:对脉冲(0/255)非常有效,边缘不易模糊。

    • 参数ksize=3/5/7;噪声更重用更大核,但细节会损失更多。

  2. 双边滤波(对椒盐作用较有限)

    java 复制代码
    Imgproc.bilateralFilter(src, dst, 9, 75, 75);
    • 更常用于高斯噪声场景;对椒盐不如中值。
  3. 形态学操作(可针对性清除盐/椒)

    java 复制代码
    Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3,3));
    // 清除白色小点(盐):开运算 = 先腐蚀后膨胀
    Imgproc.morphologyEx(src, dst, Imgproc.MORPH_OPEN, kernel);
    // 清除黑色小点(椒):闭运算 = 先膨胀后腐蚀
    Imgproc.morphologyEx(src, dst, Imgproc.MORPH_CLOSE, kernel);
    • 优点:针对小斑点,选择开/闭可定向清除。

    • 参数:核大小决定"斑点尺寸阈值"。

  4. 非局部均值(NLM)photo 模块)

    java 复制代码
    // 灰度:
    Photo.fastNlMeansDenoising(graySrc, dst, h /*10~30*/, 7, 21);
    // 彩色:
    Photo.fastNlMeansDenoisingColored(colorSrc, dst, h /*10~15*/, hColor /*10~15*/, 7, 21);
    • 优点:保边、保纹理;对低/中等椒盐也有效。

    • 参数h/hColor 为强度参数,越大去噪越强,细节损失越多。

15.4 应用场景

  1. 算法性能评估:在图像处理算法开发中,人工添加椒盐噪声可用于测试和评估去噪算法的性能。

  2. 图像增强预处理:在 OCR(光学字符识别)、二维码识别等应用中,预处理阶段可能需要去除椒盐噪声以提高识别准确率。

  3. 数据增强:在机器学习领域,可以通过添加椒盐噪声来扩充训练数据集,提高模型的鲁棒性和泛化能力。

  4. 模拟真实噪声:在图像处理教学和演示中,人工添加椒盐噪声可以模拟真实场景中的图像退化问题。

15.5 示例

接下来我们示例会实现如何给原图添加椒盐,并且对比中值滤波、形态学开+闭、非均布值各自处理后的效果

SaltPepperActivity.java

java 复制代码
public class SaltPepperActivity extends AppCompatActivity {

    private ActivitySaltPepperBinding mBinding;

    static {
        System.loadLibrary("opencv_java4");
    }

    private Mat srcBgr;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mBinding = ActivitySaltPepperBinding.inflate(getLayoutInflater());
        setContentView(mBinding.getRoot());

        try {
            // 1) 读入彩色图(BGR)
            srcBgr = Utils.loadResource(this, R.drawable.lena); // 换成你的图

            // 展示原图
            showMat(mBinding.ivOriginal, srcBgr);

            // 2) 加椒盐噪声(5%,盐椒各半)
            Mat noisy = addSaltPepperNoise(srcBgr, 0.05, 0.5);
            showMat(mBinding.ivNoisy, noisy);

            // 3) 中值滤波(彩色可直接用)
            Mat median = new Mat();
            Imgproc.medianBlur(noisy, median, 5);
            showMat(mBinding.ivMedian, median);

            // 4) 形态学操作(在灰度图上)
            Mat gray = new Mat();
            Imgproc.cvtColor(noisy, gray, Imgproc.COLOR_BGR2GRAY);

            Mat morphResult = new Mat();
            Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3));

            // 先开运算去白点,再闭运算去黑点
            Imgproc.morphologyEx(gray, morphResult, Imgproc.MORPH_OPEN, kernel);
            Imgproc.morphologyEx(morphResult, morphResult, Imgproc.MORPH_OPEN, kernel);

            showMat(mBinding.ivMorph, morphResult);

            // 5) NLM 彩色去噪
            Mat nlm = new Mat();
            Photo.fastNlMeansDenoisingColored(noisy, nlm, 10, 10, 7, 21);
            showMat(mBinding.ivNlm, nlm);

            // 释放
            noisy.release();
            median.release();
            gray.release();
            morphResult.release();
            nlm.release();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 生成椒盐噪声:amount = 噪声比例(0~1),saltRatio = 白点占比(0~1)
     * 对彩色图使用同一掩膜,避免彩色斑点。
     */
    public static Mat addSaltPepperNoise(Mat srcBgr, double amount, double saltRatio) {
        Mat noisy = srcBgr.clone();

        int rows = srcBgr.rows();
        int cols = srcBgr.cols();

        // 单通道噪声掩膜
        Mat rand = new Mat(rows, cols, CvType.CV_8UC1);
        Mat saltMask = new Mat();
        Mat pepperMask = new Mat();

        double pSalt = Math.max(0.0, Math.min(1.0, amount * saltRatio));
        double pPepper = Math.max(0.0, Math.min(1.0, amount * (1.0 - saltRatio)));

        // --- Salt:随机 < tSalt 的置为 255(掩膜)
        double tSalt = pSalt * 255.0;

        Core.randu(rand, 0, 255);
        Imgproc.threshold(rand, saltMask, tSalt, 255, Imgproc.THRESH_BINARY_INV);
        // 掩膜位置写入白色
        noisy.setTo(new Scalar(255, 255, 255), saltMask);

        // --- Pepper:随机 < tPepper 的置为 255(掩膜)
        double tPepper = pPepper * 255.0;
        Core.randu(rand, 0, 255);
        Imgproc.threshold(rand, pepperMask, tPepper, 255, Imgproc.THRESH_BINARY_INV);
        // 掩膜位置写入黑色
        noisy.setTo(new Scalar(0, 0, 0), pepperMask);

        rand.release();
        saltMask.release();
        pepperMask.release();

        return noisy;
    }

    /**
     * 将 Mat 显示到 ImageView(BGR/Gray 通用)
     */
    private void showMat(ImageView view, Mat mat) {
        Mat display = new Mat();
        if (mat.channels() == 1) {
            Imgproc.cvtColor(mat, display, Imgproc.COLOR_GRAY2RGBA);
        } else {
            Imgproc.cvtColor(mat, display, Imgproc.COLOR_BGR2RGBA);
        }
        android.graphics.Bitmap bmp = android.graphics.Bitmap.createBitmap(
                display.cols(), display.rows(), android.graphics.Bitmap.Config.ARGB_8888);
        Utils.matToBitmap(display, bmp);
        view.setImageBitmap(bmp);
        display.release();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (srcBgr != null) srcBgr.release();
    }
}

16. 高斯噪声

16.1 什么是高斯噪声

高斯噪声是图像处理中最常见的噪声类型之一,它是一种服从高斯分布(正态分布)的随机噪声。它与椒盐噪声不同,不是表现为极端的黑白点,内饰所有像素值都受到随机扰动,使图像看起来更像"雪花"电视信号或低光照条件下拍摄的图片,它的概率密度函数为:

  • μ:均值(通常为 0)

  • σ:标准差(控制噪声强度)

它的特点是:图像的每个像素值都被一个服从正态分布的随机值扰动。

16.2 高斯噪声如何产生

  • 遍历图像每个像素;

  • 为每个通道(R/G/B)添加一个随机值:

    • 使用正态分布生成器(如 Core.randn());
  • 限制像素值在 [0, 255] 范围。

16.3 如何去除高斯噪声

常用的去噪方法有:

  1. 高斯滤波 (Gaussian Filtering):最自然的方法,使用高斯核进行加权平均

  2. 均值滤波 (Mean Filtering):简单平均,效果一般但计算快

  3. 双边滤波 (Bilateral Filtering):保边去噪,效果较好但计算复杂

  4. 非局部均值去噪 (Non-Local Means Denoising):高级方法,效果最好但计算量大

16.4 应用场景

  1. 图像处理算法评估

    高斯噪声常用于评估图像处理算法的鲁棒性,如图像去噪、边缘检测、特征提取等算法的性能测试。

  2. 数据增强

    在机器学习和深度学习中,向训练数据添加高斯噪声是一种有效的数据增强技术,可以提高模型的泛化能力和鲁棒性。

  3. 模拟真实噪声

    许多真实世界的噪声(如传感器噪声、电子干扰)可以近似为高斯噪声,因此可用于模拟真实拍摄条件。

  4. 图像质量评估

    通过添加不同强度的高斯噪声并测试去噪效果,可以评估图像处理系统的性能极限。

  5. 隐私保护

    在需要共享图像但保护敏感信息时,添加适当强度的高斯噪声可以在保持图像可用性的同时保护隐私。

16.5 效果评估

使用峰值信噪比(PSNR) 作为客观评价指标:

textile 复制代码
PSNR = 10 × log₁₀(MAX² / MSE)

其中:

  • MAX 是像素最大值(255)

  • MSE 是均方误差,衡量原图与处理后的图像之间的差异

PSNR 值越高,表示图像质量越好。通常:

  • PSNR > 30 dB:质量较好

  • PSNR > 40 dB:质量很好

  • PSNR > 50 dB:质量极佳

16.6 示例

GaussianNoiseActivity.java

java 复制代码
public class GaussianNoiseActivity extends AppCompatActivity {
    private ActivityGaussianNoiseBinding mBinding;

    static {
        System.loadLibrary("opencv_java4");
    }

    private Mat originalMat;
    private Mat noisyMat;
    private Mat denoisedMat;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mBinding = ActivityGaussianNoiseBinding.inflate(getLayoutInflater());
        setContentView(mBinding.getRoot());


        try {
            originalMat = Utils.loadResource(this, R.drawable.lena);
            showMat(mBinding.ivOriginal, originalMat);
            addGaussianNoise(0, 35);
            applyGaussianFilter();
            applyBilateralFilter();
            applyNlMeansFilter();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 添加高斯噪声
     *
     * @param mean   噪声均值
     * @param stdDev 噪声标准差
     */
    private void addGaussianNoise(double mean, double stdDev) {
        if (originalMat == null || originalMat.empty()) {
            Toast.makeText(this, "请先加载图像", Toast.LENGTH_SHORT).show();
            return;
        }

        // 创建噪声矩阵
        Mat noise = new Mat(originalMat.size(), originalMat.type());
        // 生成高斯噪声
        Core.randn(noise, mean, stdDev);
        // 添加噪声到原图
        noisyMat = new Mat();
        Core.add(originalMat, noise, noisyMat);
        // 确保像素值在有效范围内
        Core.normalize(noisyMat, noisyMat, 0, 255, Core.NORM_MINMAX);
        noisyMat.convertTo(noisyMat, CvType.CV_8U);
        // 显示带噪声的原图
        showMat(mBinding.ivGaussianNoise, noisyMat);
        // 计算并显示PSNR
        double psnr = calculatePSNR(originalMat, noisyMat);
        mBinding.ivPsnr.setText(String.format("PSNR: %.2f dB", psnr));

        noise.release();
        Log.e("GaussianNoiseActivity", String.format("已添加高斯噪声 (μ=%.1f, σ=%.1f)", mean, stdDev));

    }

    /**
     * 应用高斯滤波
     */
    private void applyGaussianFilter() {
        if (noisyMat == null || noisyMat.empty()) {
            Toast.makeText(this, "请先添加噪声", Toast.LENGTH_SHORT).show();
            return;
        }
        denoisedMat = new Mat();
        // 使用5x5高斯核,标准差自动计算
        Imgproc.GaussianBlur(noisyMat, denoisedMat, new Size(5, 5), 1.5);
        showMat(mBinding.ivGaussianFilter, denoisedMat);
    }

    /**
     * 应用双边滤波
     */
    private void applyBilateralFilter() {
        if (noisyMat == null || noisyMat.empty()) {
            Toast.makeText(this, "请先添加噪声", Toast.LENGTH_SHORT).show();
            return;
        }

        denoisedMat = new Mat();
        // 双边滤波参数:d=5, sigmaColor=75, sigmaSpace=75
        Imgproc.bilateralFilter(noisyMat, denoisedMat, 5, 75, 75);

        showMat(mBinding.ivBilateralFilter, denoisedMat);
    }

    /**
     * 应用非局部均值去噪
     */
    private void applyNlMeansFilter() {
        if (noisyMat == null || noisyMat.empty()) {
            Toast.makeText(this, "请先添加噪声", Toast.LENGTH_SHORT).show();
            return;
        }

        denoisedMat = new Mat();
        // 非局部均值去噪参数
        Photo.fastNlMeansDenoising(noisyMat, denoisedMat, 10, 7, 21);

        showMat(mBinding.ivNlMeansFilter, denoisedMat);
    }

    /**
     * 计算峰值信噪比 (PSNR)
     */
    private double calculatePSNR(Mat original, Mat processed) {
        Mat diff = new Mat();
        Core.absdiff(original, processed, diff);
        diff.convertTo(diff, CvType.CV_32F);
        Core.multiply(diff, diff, diff);

        double mse = Core.mean(diff).val[0];
        diff.release();

        if (mse <= 1e-10) {
            return 100; // 无限大,返回一个很大的值
        }

        return 10 * Math.log10(255 * 255 / mse);
    }


    /**
     * 将 Mat 显示到 ImageView(BGR/Gray 通用)
     */
    private void showMat(ImageView view, Mat mat) {
        Mat display = new Mat();
        if (mat.channels() == 1) {
            Imgproc.cvtColor(mat, display, Imgproc.COLOR_GRAY2RGBA);
        } else {
            Imgproc.cvtColor(mat, display, Imgproc.COLOR_BGR2RGBA);
        }
        android.graphics.Bitmap bmp = android.graphics.Bitmap.createBitmap(
                display.cols(), display.rows(), android.graphics.Bitmap.Config.ARGB_8888);
        Utils.matToBitmap(display, bmp);
        view.setImageBitmap(bmp);
        display.release();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (originalMat != null) originalMat.release();
        if (noisyMat != null) noisyMat.release();
        if (denoisedMat != null) denoisedMat.release();
    }
}

17. 均值滤波

17.1 什么是均值滤波

均值滤波 是图像处理中最简单、最直观的一种线性空间滤波技术,主要用于图像平滑噪声消除。其核心思想是用一个像素点周围邻域的平均灰度值来代替该像素点的灰度值,从而达到减少图像中"尖锐"变化、模糊图像的目的。

在 Android 开发中,借助 OpenCV 库的强大功能,我们可以轻松地实现均值滤波,用于处理从相机或图库中获取的图像。

均值滤波的过程在数学上等同于一个卷积操作。它使用一个称为"内核"或"滤波器"的矩阵,在原始图像上滑动。

对于一个大小为 m × n 的内核(通常为奇数,如 3x3, 5x5),输出图像中像素点 (x, y) 的值 g(x, y) 由输入图像 f 中对应邻域内所有像素值的平均值计算得出:

g(x,y)=1m×n∑i=−aa∑j=−bbf(x+i,y+j)g(x, y) = \frac{1}{m \times n} \sum_{i=-a}^{a} \sum_{j=-b}^{b} f(x+i, y+j)g(x,y)=m×n1∑i=−aa∑j=−bbf(x+i,y+j)

其中:

  • m = 2a + 1, n = 2b + 1 (内核的尺寸)。

  • f(x+i, y+j) 是原始图像在位置 (x+i, y+j) 的像素值。

  • 1m×n\frac{1}{m \times n}m×n1 是归一化系数,确保卷积后图像的整体亮度不变。

17.2 关键函数分析

核心函数:Imgproc.blur()

在 OpenCV for Android 中,均值滤波通过 Imgproc.blur() 静态方法实现。

java 复制代码
public static void blur(Mat src, Mat dst, Size ksize, Point anchor, int borderType)

参数详解:

  • src : 输入图像。可以是任何通道数的 Mat 对象(如灰度图 CV_8UC1 或彩色图 CV_8UC3)。函数会独立处理每个通道。

  • dst : 输出图像。其大小和类型与输入图像 src 相同。

  • ksize : 滤波内核的大小。使用 new Size(width, height) 创建。宽度和高度必须是正奇数

  • anchor : 内核的锚点。默认值为 new Point(-1, -1),表示锚点位于内核中心。通常不需要更改。

  • borderType : 边界像素外推模式。通常使用默认值即可,OpenCV 会自动选择。例如 Core.BORDER_DEFAULT

常用简化调用:

java 复制代码
// 最常用的形式,只需指定内核大小,锚点默认为中心,边界类型使用默认值
Imgproc.blur(inputMat, outputMat, new Size(3, 3));

内核大小对效果的影响

内核大小 ksize 是控制滤波强度的关键参数:

  • 内核越小 (如 3x3): 模糊效果越弱,能去除微小的噪声,但保留更多原始图像的细节。

  • 内核越大 (如 15x15): 模糊效果越强,能去除更明显的噪声或更大的瑕疵,但也会导致图像细节严重丢失,变得非常模糊。

选择合适的内核大小需要在去噪效果细节保留之间取得平衡。

17.3 应用场景

适用场景

  1. 消除轻微随机噪声: 如高斯噪声,效果较好。

  2. 简单的图像预处理: 在复杂的图像分析(如边缘检测、目标识别)之前,先进行轻微的均值模糊,可以抑制微小的干扰纹理,使后续处理更稳定。

  3. 创造简单的模糊效果: 为图像添加柔化效果。

不适用场景 / 缺点

  1. 保护边缘能力差: 在模糊噪声的同时,也会同样地模糊我们关心的图像边缘和细节,这是其最大缺点。

  2. 处理椒盐噪声效果不佳 : 虽然能减弱椒盐噪声,但效果不如中值滤波。均值滤波会让噪声点扩散到周围区域,而不是彻底消除。

  3. 不适用于需要保留高频信息的任务: 如需要锐利边缘的视觉应用。

17.4 示例

BlurActivity.java

java 复制代码
public class BlurActivity extends AppCompatActivity {

    private ActivityBlurBinding mBinding;

    static {
        System.loadLibrary("opencv_java4");
    }

    private Mat mOriginalMat;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = ActivityBlurBinding.inflate(getLayoutInflater());
        setContentView(mBinding.getRoot());

        try {
            mOriginalMat = Utils.loadResource(this, R.drawable.lena);
            showMat(mBinding.ivOriginal, mOriginalMat);
            applyMeanBlur();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void applyMeanBlur() {
        if (mOriginalMat == null || mOriginalMat.empty()) {
            Toast.makeText(this, "请先加载原图", Toast.LENGTH_SHORT).show();
            return;
        }
        // 1. 创建一个 Mat 对象来存储模糊后的结果
        Mat blurredMat = new Mat();

        // 2. 应用均值滤波
        // 使用一个 15x15 的内核以获得明显的模糊效果
        Size kernelSize = new Size(15, 15);
        Imgproc.blur(mOriginalMat, blurredMat, kernelSize);

        showMat(mBinding.ivBlur, blurredMat);
        
        blurredMat.release();
    }

    /**
     * 将 Mat 显示到 ImageView(BGR/Gray 通用)
     */
    private void showMat(ImageView view, Mat mat) {
        Mat display = new Mat();
        if (mat.channels() == 1) {
            Imgproc.cvtColor(mat, display, Imgproc.COLOR_GRAY2RGBA);
        } else {
            Imgproc.cvtColor(mat, display, Imgproc.COLOR_BGR2RGBA);
        }
        android.graphics.Bitmap bmp = android.graphics.Bitmap.createBitmap(
                display.cols(), display.rows(), android.graphics.Bitmap.Config.ARGB_8888);
        Utils.matToBitmap(display, bmp);
        view.setImageBitmap(bmp);
        display.release();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mOriginalMat != null) mOriginalMat.release();
    }
}

我们可以看到原图变得非常模糊,所有细节都被平滑掉了。这直观地展示了均值滤波的强大平滑能力及其对图像细节的侵蚀作用。

当然我们也可以通过修改 applyMeanBlur() 方法中的 kernelSize(例如改为 new Size(5, 5))来观察不同强度的模糊效果。

18. 方框滤波

18.1 什么是方框滤波

方框滤波 是 OpenCV 提供的一种基本的线性空间滤波技术。它是均值滤波的更一般化形式。其核心思想是计算图像内核区域内所有像素值的总和或平均值,并用该值替换内核中心的像素值。

与均值滤波强制进行归一化不同,方框滤波提供了一个参数来选择是否进行归一化,这使其更加灵活,既可以实现模糊效果,也可以实现非归一化的求和操作,后者在某些图像处理场景中非常有用。

方框滤波同样通过卷积操作实现。对于一个大小为 m × n 的内核,输出图像中像素点 (x, y) 的值 g(x, y) 计算如下:

1. 不进行归一化 (normalize = false):
g(x,y)=∑i=−aa∑j=−bbf(x+i,y+j)g(x, y) = \sum_{i=-a}^{a} \sum_{j=-b}^{b} f(x+i, y+j)g(x,y)=∑i=−aa∑j=−bbf(x+i,y+j)

2. 进行归一化 (normalize = true):
g(x,y)=1m×n∑i=−aa∑j=−bbf(x+i,y+j)g(x, y) = \frac{1}{m \times n} \sum_{i=-a}^{a} \sum_{j=-b}^{b} f(x+i, y+j)g(x,y)=m×n1∑i=−aa∑j=−bbf(x+i,y+j)

与均值滤波的关系

normalize = true 时,方框滤波的计算公式完全等同于均值滤波 。事实上,OpenCV 中的 blur() 函数内部就是通过调用 boxFilter() 并设置 normalize=true 来实现的。

归一化的影响

  • normalize = true : 进行归一化,除以内核面积。这是默认行为,结果与均值滤波相同,用于图像模糊。它可以保证输出图像的像素值范围与输入一致(例如,对于 8-bit 图像,保持在 0-255 之间),避免图像整体变亮。

  • normalize = false : 不进行归一化 ,直接求和。这会导致输出图像的像素值急剧增大,远超原始范围(例如,3x3 内核求和会使像素值变为原来的 9 倍左右)。这通常会导致图像显示为一片白色(因为值被截断到 255)。非归一化的方框滤波主要用于计算邻域像素总和,是某些高级图像算法(如积分图像计算、自定义线性滤波)中的中间步骤。

18.2 关键函数分析

核心函数:Imgproc.boxFilter()

在 OpenCV for Android 中,方框滤波通过 Imgproc.boxFilter() 静态方法实现。

函数原型:

java 复制代码
public static void boxFilter(Mat src, Mat dst, int ddepth, Size ksize, Point anchor, boolean normalize, int borderType)

参数详解:

  • src: 输入图像。

  • dst : 输出图像。其大小与输入图像相同,但深度由 ddepth 参数指定。

  • ddepth : 输出图像的深度(-1 表示使用与输入图像相同的深度)。这是一个关键参数

    • normalize=true 时,通常设置为 -1CvType.CV_8U(与输入一致)。

    • normalize=false 时,由于像素值会变得很大,必须使用更深的深度来存储结果,例如 CvType.CV_32FCvType.CV_64F,否则会发生数值截断,导致图像信息丢失。

  • ksize : 模糊内核的大小。使用 new Size(width, height) 创建。宽度和高度必须是正奇数

  • anchor : 内核的锚点。默认值为 new Point(-1, -1),表示锚点位于内核中心。

  • normalize : 是否对目标图像进行归一化操作的标志。true 表示模糊(均值滤波),false 表示求和。

  • borderType: 边界像素外推模式。使用默认值即可。

blur() 函数的等价性

以下两行代码是等价的:

java 复制代码
// 使用 boxFilter 实现均值模糊
Imgproc.boxFilter(src, dst, -1, new Size(3, 3), new Point(-1, -1), true, Core.BORDER_DEFAULT);

// 使用 blur 函数实现均值模糊
Imgproc.blur(src, dst, new Size(3, 3));

18.3 应用场景

适用场景

  1. normalize = true (默认):

    • 与均值滤波完全相同:消除轻微随机噪声简单的图像预处理创造简单的模糊效果
  2. normalize = false:

    • 计算图像局部区域的和 :这是其独特优势。例如,它是计算积分图的基础。

    • 作为自定义线性滤波的构建模块:在某些特定的卷积操作中,可能需要先求和再乘以一个系数,而不是直接求平均。

    • 模板匹配等算法中的中间计算步骤

不适用场景 / 缺点

  • normalize = true : 与均值滤波相同,保护边缘能力差 ,处理椒盐噪声效果不佳

  • normalize = false: 直接求和的结果通常不能直接用于显示,需要后续处理,应用场景相对专业和狭窄。

18.4 示例

19. 高斯滤波

19.1 什么是方框滤波

19.2 关键函数分析

19.3 应用场景

19.4 示例

BoxFilterActivity.java

java 复制代码
public class BoxFilterActivity extends AppCompatActivity {
    private ActivityBoxFilterBinding mBinding;

    static {
        System.loadLibrary("opencv_java4");
    }

    private Mat mOriginalMat;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mBinding = ActivityBoxFilterBinding.inflate(getLayoutInflater());
        setContentView(mBinding.getRoot());

        try {
            mOriginalMat = Utils.loadResource(this, R.drawable.lena);

            showMat(mBinding.ivOriginal, mOriginalMat);
            applyBoxFilter(true); // 方框滤波归一化模糊处理
            applyBoxFilter(false); // 方框滤波归一化求和处理
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void applyBoxFilter(boolean normalize) {
        if (mOriginalMat.empty()) return;

        if (normalize) {
            // 模式 1: 归一化 (模糊)
            Mat resultMat = new Mat();
            Imgproc.boxFilter(mOriginalMat, resultMat, -1, new Size(3, 3));
            showMat(mBinding.ivBlur, resultMat);
            resultMat.release();
        } else {
            // 模式 2: 非归一化 (求和)
            Mat tempMat = new Mat();
            Imgproc.boxFilter(mOriginalMat, tempMat, -1, new Size(3, 3), new Point(-1, -1), false, Core.BORDER_DEFAULT);
            showMat(mBinding.ivBoxFilter, tempMat);
            tempMat.release();
        }
    }

    /**
     * 将 Mat 显示到 ImageView(BGR/Gray 通用)
     */
    private void showMat(ImageView view, Mat mat) {
        Mat display = new Mat();
        if (mat.channels() == 1) {
            Imgproc.cvtColor(mat, display, Imgproc.COLOR_GRAY2RGBA);
        } else {
            Imgproc.cvtColor(mat, display, Imgproc.COLOR_BGR2RGBA);
        }
        android.graphics.Bitmap bmp = android.graphics.Bitmap.createBitmap(
                display.cols(), display.rows(), android.graphics.Bitmap.Config.ARGB_8888);
        Utils.matToBitmap(display, bmp);
        view.setImageBitmap(bmp);
        display.release();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mOriginalMat != null) mOriginalMat.release();
    }
}

20. 双边滤波

20.1 什么是双边滤波

双边滤波 是OpenCV中一种非常强大且实用的非线性滤波技术。它能够在平滑图像、减少噪声的同时,很好地保留图像的边缘信息。这一特性使其在许多计算机视觉任务中比均值滤波、高斯滤波等传统线性滤波器更具优势。

双边滤波的核心思想是:不仅考虑像素在空间上的邻近性 (像高斯滤波那样),还考虑像素在亮度/颜色上的相似性 。这意味着只有那些在空间上接近颜色/亮度相似的像素才会对滤波结果产生显著贡献。

空间域权重 vs. 值域权重

双边滤波使用两个权重函数:

  1. 空间域权重 (Spatial Domain Weight)

    • 基于几何空间距离,距离中心像素越近的像素权重越大

    • 使用高斯函数计算:ws(i,j)=e−(i2+j2)2σs2w_s(i,j) = e^{-\frac{(i^2 + j^2)}{2\sigma_s^2}}ws(i,j)=e−2σs2(i2+j2)

    • 这与普通的高斯滤波完全相同

  2. 值域权重 (Range Domain Weight)

    • 基于像素值相似度,与中心像素值越相似的像素权重越大

    • 使用高斯函数计算:wr(i,j)=e−(I(i,j)−I(x,y))22σr2w_r(i,j) = e^{-\frac{(I(i,j) - I(x,y))^2}{2\sigma_r^2}}wr(i,j)=e−2σr2(I(i,j)−I(x,y))2

    • 这是双边滤波的关键创新点

最终的滤波结果是两个权重的乘积归一化后的值:

Ifiltered(x,y)=∑i,jI(i,j)⋅ws(i,j)⋅wr(i,j)∑i,jws(i,j)⋅wr(i,j)I_{\text{filtered}}(x,y) = \frac{\sum_{i,j} I(i,j) \cdot w_s(i,j) \cdot w_r(i,j)}{\sum_{i,j} w_s(i,j) \cdot w_r(i,j)}Ifiltered(x,y)=∑i,jws(i,j)⋅wr(i,j)∑i,jI(i,j)⋅ws(i,j)⋅wr(i,j)

其中:

  • I(x,y)I(x,y)I(x,y) 是原始图像

  • Ifiltered(x,y)I_{\text{filtered}}(x,y)Ifiltered(x,y) 是滤波后图像

  • wsw_sws 是空间域权重

  • wrw_rwr 是值域权重

20.2 关键函数分析

核心函数:Imgproc.bilateralFilter()

在 OpenCV for Android 中,双边滤波通过 Imgproc.bilateralFilter() 静态方法实现。

java 复制代码
public static void bilateralFilter(Mat src, Mat dst, int d, double sigmaColor, double sigmaSpace)

参数详解:

  • src: 输入图像,必须是 8-bit 或浮点型,1 通道或 3 通道

  • dst: 输出图像,大小和类型与输入图像相同

  • d : 滤波过程中每个像素邻域的直径。如果为非正数,则从 sigmaSpace 计算得出

  • sigmaColor: 值域滤波器的 σ 值。值越大,表明该像素邻域内有越多的颜色/亮度值会被混合在一起,产生较大的半相等颜色区域

  • sigmaSpace: 空间域滤波器的 σ 值。值越大,意味着越远的像素会相互影响,只要它们的颜色足够接近

参数选择策略

  1. 简单应用 :设置 d = 5, sigmaColor = 50, sigmaSpace = 50 作为起点

  2. 实时应用 :使用较小的 d 值(如 3-5)以提高性能

  3. 高质量滤波 :设置 d = 0,让 OpenCV 根据 sigmaSpace 自动计算合适的直径

  4. σ 值关系

  • 如果 sigmaColorsigmaSpace 都较小(<10),滤波效果较弱

  • 如果 sigmaColor 较大而 sigmaSpace 较小,只有颜色非常相似的区域被平滑

  • 如果 sigmaSpace 较大而 sigmaColor 较小,滤波行为接近高斯模糊

20.3 应用场景

适用场景

  1. 边缘保持平滑:需要减少噪声但同时保留清晰边缘的场景

  2. 人像美化:皮肤平滑处理(磨皮效果),减少皱纹和斑点同时保留五官轮廓

  3. 卡通化效果:与边缘检测结合创建卡通风格图像

  4. HDR色调映射:用于细节增强和噪声减少

  5. 预处理:在边缘检测、图像分割等任务前作为预处理步骤

不适用场景

  1. 实时视频处理:计算复杂度高,可能无法满足实时性要求

  2. 大尺寸图像处理:在没有优化的情况下处理大图像速度较慢

  3. 极端噪声去除:对于椒盐噪声等,中值滤波可能更有效

  4. 需要强烈模糊的场景:传统高斯滤波可能更合适

20.4 示例

BilateralFilterActivity.java

java 复制代码
public class BilateralFilterActivity extends AppCompatActivity {

    private ActivityBilateralFilterBinding mBinding;

    static {
        System.loadLibrary("opencv_java4");
    }

    private Mat mOriginalMat;

    private double sigmaColor = 75.0;
    private double sigmaSpace = 75.0;
    private int diameter = 15;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = ActivityBilateralFilterBinding.inflate(getLayoutInflater());
        setContentView(mBinding.getRoot());

        mBinding.tvSigmaColor.setText("颜色σ值: " + sigmaColor);
        mBinding.tvSigmaSpace.setText("空间σ值: " + sigmaSpace);
        mBinding.tvDiameter.setText("滤波直径: " + diameter);

        try {
            mOriginalMat = Utils.loadResource(this, R.drawable.lena);
            showMat(mBinding.imageViewOriginal, mOriginalMat);

            applyBilateralFilter();

            mBinding.sbSigmaColor.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
                @Override
                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                    sigmaColor = progress;
                    mBinding.tvSigmaColor.setText("颜色σ值: " + progress);
                }

                @Override
                public void onStartTrackingTouch(SeekBar seekBar) {}

                @Override
                public void onStopTrackingTouch(SeekBar seekBar) {}
            });

            mBinding.sbSigmaSpace.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
                @Override
                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                    sigmaSpace = progress;
                    mBinding.tvSigmaSpace.setText("空间σ值: " + progress);
                }

                @Override
                public void onStartTrackingTouch(SeekBar seekBar) {}

                @Override
                public void onStopTrackingTouch(SeekBar seekBar) {}
            });

            mBinding.sbDiameter.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
                @Override
                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                    diameter = progress;
                    mBinding.tvDiameter.setText("滤波直径: " + progress);
                }

                @Override
                public void onStartTrackingTouch(SeekBar seekBar) {}

                @Override
                public void onStopTrackingTouch(SeekBar seekBar) {}
            });

            mBinding.btnBilateral.setOnClickListener(view -> {
                applyBilateralFilter();
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void applyBilateralFilter() {
        if (mOriginalMat == null) return;

        Mat grayMat = new Mat();
        Mat resultMat = new Mat();
        Imgproc.cvtColor(mOriginalMat, grayMat, Imgproc.COLOR_RGBA2RGB);

        // 应用双边滤波
        Imgproc.bilateralFilter(grayMat, resultMat, diameter, sigmaColor, sigmaSpace);

        showMat(mBinding.imageViewFiltered, resultMat);

        grayMat.release();
        resultMat.release();
    }


    /**
     * 将 Mat 显示到 ImageView(BGR/Gray 通用)
     */
    private void showMat(ImageView view, Mat mat) {
        Mat display = new Mat();
        if (mat.channels() == 1) {
            Imgproc.cvtColor(mat, display, Imgproc.COLOR_GRAY2RGBA);
        } else {
            Imgproc.cvtColor(mat, display, Imgproc.COLOR_BGR2RGBA);
        }
        android.graphics.Bitmap bmp = android.graphics.Bitmap.createBitmap(
                display.cols(), display.rows(), android.graphics.Bitmap.Config.ARGB_8888);
        Utils.matToBitmap(display, bmp);
        view.setImageBitmap(bmp);
        display.release();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mOriginalMat != null) mOriginalMat.release();
    }
}
相关推荐
阿巴斯甜3 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker3 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95274 小时前
Andorid Google 登录接入文档
android
黄林晴6 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab18 小时前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿21 小时前
Android MediaPlayer 笔记
android
Jony_21 小时前
Android 启动优化方案
android
阿巴斯甜21 小时前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇21 小时前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android