OpenCV 笔记(22):图像的缩放——最近邻插值、双线性插值算法

1. 图像缩放

1.1 简介

图像缩放是指通过增加或减少像素来改变图像尺寸的过程,是图像处理中常见的操作。图像缩放会涉及效率和图像质量之间的权衡。

图像放大(也称为上采样插值)的主要目的是放大原图像,以便在更高分辨率的显示设备上显示。但是,放大图像并不能带来更多信息,因此图像质量会不可避免地受到影响。

图像缩小(也称为下采样)的主要目的是减小图像尺寸,以便更有效地存储或传输。缩小图像可以保留更多信息,但图像细节会丢失。

1.2 图像缩放方法分类

  • 空间域方法:直接在图像像素空间进行操作。常见的空间域缩放方法包括:
    • 最近邻插值:简单快速,但图像质量较差。
    • 双线性插值:图像质量比最近邻插值好,但计算量更大。
    • 立方插值:图像质量比双线性插值好,但计算量更大。

常见空间域缩放方法的比较:

方法 优点 缺点
最近邻插值 简单快速 容易产生锯齿
双线性插值 平滑图像 可能导致细节模糊
立方插值 效果更好 计算量较大
  • 频域方法:将图像转换为频域,然后在频域进行操作。常见的频域缩放方法包括:
    • 傅里叶插值:将图像转换为傅里叶频谱,然后根据缩放比例调整频谱大小,再将逆傅里叶变换回图像空间。傅里叶插值可以保持图像边缘锐度。图像质量较高,但计算量较大。
    • Lanczos 插值:一种改进的傅里叶插值算法,通过使用低通滤波器来消除频谱中的混叠现象,平衡了速度和质量,是常用频域算法之一。

2. 插值算法

图像插值算法是指在已知像素值的基础上,估计未知像素值的数学方法。OpenCV 提供了多种插值算法,用于图像缩放、旋转、仿射变换等操作。

在数学的数值分析领域中,内插,或称插值(英语:Interpolation),是一种通过已知的、离散的数据点,在范围内推求新数据点的过程或方法。

2.1 最近邻插值(Nearest Neighbor Interpolation)

最近邻插值通过找到目标像素在原图像中最近的像素值来赋值给目标像素。具体来说,根据原图像和目标图像的尺寸,计算缩放的比例,然后根据缩放比例计算目标像素所依据的原像素,并将该值赋给目标像素。

src_x = \frac{dst_x}{scale}

src_y = \frac{dst_y}{scale}

其中, src_xsrc_y 表示原图像中的坐标, dst_xdst_y 表示目标图像中的坐标,scale 表示放缩倍数。

最近邻插值的优点:

  • 算法简单,计算量小,速度快。
  • 不会产生新的像素值,保持原始图像的灰度值。

最近邻插值的缺点:

  • 容易产生锯齿现象,图像质量较低。
最近邻插值.png

下面的代码,展示了如何实现最近邻插值算法

#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"

using namespace std;
using namespace cv;

//最近邻插值算法
void nearestNeighbor(cv::Mat& src, cv::Mat& dst, float sx, float sy)
{
    // 由 scale 计算输出图像的尺寸(四舍五入)
    int dst_cols = round(src.cols * sx);
    int dst_rows = round(src.rows * sy);

    dst = cv::Mat(dst_rows,dst_cols,src.type());

    for (int i = 0; i < dst.rows; i++){
        for (int j = 0; j < dst.cols; j++){
            if (src.channels() == 1) {
                // 插值计算,输出图像的像素点由原图像对应的最近的像素点得到(四舍五入)
                int i_index = round(i / sy);
                int j_index = round(j / sx);
                if (i_index > src.rows - 1) i_index = src.rows - 1;//防止越界
                if (j_index > src.cols - 1) j_index = src.cols - 1;//防止越界

                dst.at<uchar>(i, j) = src.at<uchar>(i_index, j_index);
            } else {
                // 插值计算,输出图像的像素点由原图像对应的最近的像素点得到(四舍五入)
                int i_index = round(i / sy);
                int j_index = round(j / sx);
                if (i_index > src.rows - 1) i_index = src.rows - 1;//防止越界
                if (j_index > src.cols - 1) j_index = src.cols - 1;//防止越界

                dst.at<cv::Vec3b>(i, j)[0] = src.at<cv::Vec3b>(i_index, j_index)[0];
                dst.at<cv::Vec3b>(i, j)[1] = src.at<cv::Vec3b>(i_index, j_index)[1];
                dst.at<cv::Vec3b>(i, j)[2] = src.at<cv::Vec3b>(i_index, j_index)[2];
            }
        }
    }
}

int main()
{
    Mat src = imread(".../grass.jpg");
    imshow("src", src);

    Mat dst;
    nearestNeighbor(src, dst,1.5, 1.5);

    imshow("dst", dst);
    waitKey(0);

    return 0;
}
原图和最近邻插值实现的缩放.png

下面的代码,通过 Mat 的 forEach() 结合 C++11 lambda 表达式,实现对 Mat 对象快速像素遍历,进而重构了最近邻插值算法。

typedef cv::Point3_<uint8_t> Pixel;

//最近邻插值算法
void nearestNeighbor(cv::Mat& src, cv::Mat& dst, float sx, float sy)
{
    // 由 scale 计算输出图像的尺寸(四舍五入)
    int dst_cols = round(src.cols * sx);
    int dst_rows = round(src.rows * sy);

    dst = cv::Mat(dst_rows,dst_cols,src.type());
    dst.forEach<Pixel>([&](Pixel &p, const int * position) -> void {
        int row = position[0];
        int col = position[1];

        if (src.channels() == 1) {
            int i_index = round(row / sy);
            int j_index = round(col / sx);

            dst.at<uchar>(row, col) = src.at<uchar>(i_index, j_index);
        } else {
            int i_index = round(row/ sy);
            int j_index = round(col / sx);

            dst.at<cv::Vec3b>(row, col)[0] = src.at<cv::Vec3b>(i_index, j_index)[0];
            dst.at<cv::Vec3b>(row, col)[1] = src.at<cv::Vec3b>(i_index, j_index)[1];
            dst.at<cv::Vec3b>(row, col)[2] = src.at<cv::Vec3b>(i_index, j_index)[2];
        }
    });
}

2.2 双线性插值(Bilinear Interpolation)

先介绍一下线性插值,线性插值是一种估计两个已知数据点之间的值的方法。

线性插值.png

假设我们已知坐标 (x_0y_0) 与 (x_1y_1),要得到 [x_0x_1] 区间内某一位置 x 在直线上的值。由上图可得:

\frac{y-y_0}{x-x_0} = \frac{y_1-y_0}{x_1-x_0}

由于 x 已知,则 y:

y = \frac{x_1-x}{x_1-x_0}y_0 + \frac{x-x_0}{x_1-x_0}y_1

所以,这是在 x 方向上进行了一次线性插值。

双线性插值是对 x 方向和 y 方向分别进行插值,它根据原始图像中四个相邻像素的值来估计新位置处像素的值。它是一维线性插值的扩展。

双线性插值.png

在上图中,假设已知Q_{11}Q_{12}Q_{21}Q_{22}四个点,我们要估计由这四个点组成的矩形内的任意点(x,y)处像素值 f(x,y) 。

  • 对沿 y 轴的两对点 Q_{11}Q_{21}在 x 方向进行线性插值:

f(R_1)= \frac{x_2-x}{x_2-x_1}f(Q_{11}) + \frac{x-x_1}{x_2-x_1}f(Q_{21})

  • 对沿 y 轴的两对点 Q_{12}Q_{22}在 x 方向进行线性插值:

f(R_2)= \frac{x_2-x}{x_2-x_1}f(Q_{12}) + \frac{x-x_1}{x_2-x_1}f(Q_{22})

  • 对沿 x 轴的两对点 R_1R_2在 y 方向进行线性插值:

f(P)= \frac{y_2-y}{y_2-y_1}f(R_1) + \frac{y-y_1}{y_2-y_1}f(R_2)

此时,一共执行了三次线性插值,双线性插值只是对 x、y 方向进行插值,而不是进行两次插值。

双线性插值用于根据原始图像中的已知值来估计调整大小的图像中像素的强度或颜色值。 与最近邻插值相比,这种方法可以产生更平滑的结果,后者可能会导致可见的伪影或锯齿状边缘。

下面的代码,展示了如何实现双线性插值算法。

#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

typedef cv::Point3_<uint8_t> Pixel;

// 双线性插值算法
void bilinearInterpolation(Mat& src, Mat& dst, double sx, double sy) {
    int dst_rows = static_cast<int>(src.rows * sy);
    int dst_cols = static_cast<int>(src.cols * sx);
    dst = Mat::zeros(cv::Size(dst_cols, dst_rows), src.type());

    dst.forEach<Pixel>([&](Pixel &p, const int * position) -> void {

        int row = position[0];
        int col = position[1];

        // (col,row)为目标图像坐标
        // (before_x,before_y)原图坐标
        double before_x = double(col + 0.5) / sx - 0.5f;
        double before_y = double(row + 0.5) / sy - 0.5;
        // 原图像坐标四个相邻点
        // 获得变换前最近的四个顶点,取整
        int top_y = static_cast<int>(before_y);
        int bottom_y = top_y + 1;
        int left_x = static_cast<int>(before_x);
        int right_x = left_x + 1;

        //计算变换前坐标的小数部分
        double u = before_x - left_x;
        double v = before_y - top_y;

        // 如果计算的原始图像的像素大于真实原始图像尺寸
        if ((top_y >= src.rows - 1) && (left_x >= src.cols - 1)) {//右下角
            for (size_t k = 0; k < src.channels(); k++) {
                dst.at<Vec3b>(row, col)[k] = (1\. - u) * (1\. - v) * src.at<Vec3b>(top_y, left_x)[k];
            }
        } else if (top_y >= src.rows - 1) { //最后一行
            for (size_t k = 0; k < src.channels(); k++) {
                dst.at<Vec3b>(row, col)[k]
                        = (1\. - u) * (1\. - v) * src.at<Vec3b>(top_y, left_x)[k]
                          + (1\. - v) * u * src.at<Vec3b>(top_y, right_x)[k];
            }
        } else if (left_x >= src.cols - 1) {//最后一列
            for (size_t k = 0; k < src.channels(); k++) {
                dst.at<Vec3b>(row, col)[k]
                        = (1\. - u) * (1\. - v) * src.at<Vec3b>(top_y, left_x)[k]
                          + (v) * (1\. - u) * src.at<Vec3b>(bottom_y, left_x)[k];
            }
        } else {
            for (size_t k = 0; k < src.channels(); k++) {
                dst.at<Vec3b>(row, col)[k]
                        = (1\. - u) * (1\. - v) * src.at<Vec3b>(top_y, left_x)[k]
                          + (1\. - v) * (u) * src.at<Vec3b>(top_y, right_x)[k]
                          + (v) * (1\. - u) * src.at<Vec3b>(bottom_y, left_x)[k]
                          + (u) * (v) * src.at<Vec3b>(bottom_y, right_x)[k];
            }
        }
    });
}

int main() {
    Mat src = imread(".../grass.jpg");
    imshow("src", src);

    double sx = 1.5;
    double sy = 1.5;

    Mat dst;
    bilinearInterpolation(src,dst, sx, sy);

    imshow("dst", dst);

    waitKey(0);
    return 0;
}
原图和双线性插值实现的缩放.png

3. 总结

图像缩放是图像处理中一项重要的技术,具有广泛的应用场景。

本文介绍了两种比较简单的插值算法:最近邻插值、双线性插值。最近邻插值适合于需要保持图像原始灰度值或边缘清晰度的场景。双线性插值适合于需要平滑图像的场景。如果需要更高的图像质量,可以考虑使用其他插值算法,例如立方插值或 Lanczos 插值,后续的文章也会介绍它们。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,630评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,405评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,382评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,548评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,853评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,276评论 1 209
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,638评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,350评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,015评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,309评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,836评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,206评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,767评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,972评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,715评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,319评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,222评论 2 258

推荐阅读更多精彩内容