双线性插值算法:原理、实现、优化及在图像处理和多领域中的广泛应用与发展趋势(一)

发布于:2025-02-10 ⋅ 阅读:(38) ⋅ 点赞:(0)

一、头文件和命名空间

  • #include <opencv2/opencv.hpp>:这是 OpenCV 库的头文件,它包含了许多用于图像处理、计算机视觉任务的类和函数。OpenCV 是一个强大的开源计算机视觉库,提供了大量的工具和算法,可用于图像的读取、处理、分析、特征提取、目标检测等任务。
  • using namespace cv; 和 using namespace std;:这是使用 cv(OpenCV)和 std(C++ 标准库)的命名空间,这样可以直接使用其中的类和函数,而无需在每个调用前面添加 cv:: 或 std:: 前缀,不过使用 using namespace 可能会引起命名冲突的风险,在大型项目中通常不推荐,但在小型示例代码中可以提高代码的简洁性。

二、像素类型定义

typedef cv::Point3_<uint8_t> Pixel;

  • typedef cv::Point3_<uint8_t> Pixel;:这里使用 typedef 为 cv::Point3_<uint8_t> 类型定义了一个别名 Pixelcv::Point3_<uint8_t> 是一个三维点的数据结构,其中每个维度的数据类型是 uint8_t(无符号 8 位整数)。在图像处理中,这可能用于表示图像像素的颜色通道,例如对于 RGB 图像,它可以存储一个像素的红色、绿色和蓝色分量,因为这些颜色分量通常使用 8 位来表示其强度范围(0-255)。

三、双线性插值函数 bilinearInterpolation

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());

  • bilinearInterpolation(Mat& src, Mat& dst, double sx, double sy):这是双线性插值的函数,它接收四个参数:
    • src:输入的源图像矩阵,使用 Mat 类型表示,Mat 是 OpenCV 中用于存储图像的数据结构。
    • dst:输出的目标图像矩阵,也是 Mat 类型。
    • sx 和 sy:水平和垂直方向的缩放因子。
  • int dst_rows = static_cast<int>(src.rows * sy); 和 int dst_cols = static_cast<int>(src.cols * sx);:根据源图像的尺寸和缩放因子计算目标图像的行数和列数。使用 static_cast<int> 进行类型转换,将浮点数结果转换为整数,因为图像的行数和列数必须是整数。
  • dst = Mat::zeros(cv::Size(dst_cols, dst_rows), src.type());:创建一个与计算出的目标图像尺寸相同且类型与源图像相同的零矩阵作为目标图像。Mat::zeros 函数会创建一个指定尺寸和类型的矩阵,并将其元素初始化为零。
    dst.forEach<Pixel>([&](Pixel &p, const int * position) -> void {
        int row = position[0];
        int col = position[1];

  • dst.forEach<Pixel>([&](Pixel &p, const int * position) -> void:使用 forEach 函数遍历目标图像的每个像素。forEach 是 C++11 中的一个函数,允许使用 lambda 表达式对矩阵的每个元素进行操作。这里的 Pixel 是之前定义的像素类型,p 是当前像素的引用,position 是一个包含当前像素位置的数组,position[0] 表示行索引,position[1] 表示列索引。
        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 before_x = double(col + 0.5) / sx - 0.5f; 和 double before_y = double(row + 0.5) / sy - 0.5;:根据目标图像的当前像素位置和缩放因子计算其在源图像中的对应位置。这里加 0.5 是为了将像素中心作为参考位置,而不是像素的左上角,减 0.5 是为了得到精确的坐标映射。
  • int top_y = static_cast<int>(before_y); 等:计算源图像中对应位置的四个相邻像素的坐标。top_y 和 bottom_y 表示相邻的行,left_x 和 right_x 表示相邻的列。
        double u = before_x - left_x;
        double v = before_y - top_y;

  • double u = before_x - left_x; 和 double v = before_y - top_y;:计算源图像中对应位置的小数部分,u 和 v 是在 x 和 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];
            }
        }
    });
}

  • 这里根据源图像中计算得到的相邻像素位置进行不同情况的处理:
    • 如果计算出的源图像坐标超出了源图像的右下角(即 top_y >= src.rows - 1 且 left_x >= src.cols - 1),只使用右下角像素的值,根据其距离目标像素的比例进行加权。
    • 如果计算出的源图像坐标在最后一行(top_y >= src.rows - 1),使用最后一行的相邻两个像素(left_x 和 right_x)进行插值。
    • 如果计算出的源图像坐标在最后一列(left_x >= src.cols - 1),使用最后一列的相邻两个像素(top_y 和 bottom_y)进行插值。
    • 对于一般情况,使用双线性插值公式,根据四个相邻像素的加权平均值计算目标像素的值。对于多通道图像(如 RGB),使用 for 循环遍历每个通道,根据双线性插值公式 dst = (1 - u) * (1 - v) * f(x1, y1) + (1 - v) * u * f(x2, y1) + v * (1 - u) * f(x1, y2) + u * v * f(x2, y2) 计算每个通道的值,其中 f(x, y) 是源图像在 (x, y) 处的像素值。

四、主函数 main

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

  • Mat src = imread(".../grass.jpg");:使用 imread 函数从文件系统中读取图像,将其存储在 src 矩阵中。imread 函数会根据文件路径尝试读取图像,如果成功,返回一个 Mat 类型的图像矩阵,否则返回一个空矩阵。
  • imshow("src", src);:使用 imshow 函数显示源图像,第一个参数是窗口名称,第二个参数是要显示的图像矩阵。
    double sx = 1.5;
    double sy = 1.5;
    Mat dst;
    bilinearInterpolation(src,dst, sx, sy);

  • double sx = 1.5; 和 double sy = 1.5;:定义水平和垂直缩放因子为 1.5。
  • Mat dst;:声明一个目标图像矩阵。
  • bilinearInterpolation(src,dst, sx, sy);:调用双线性插值函数对源图像进行缩放,将结果存储在 dst 矩阵中。
    imshow("dst", dst);
    waitKey(0);
    return 0;
}

  • imshow("dst", dst);:显示缩放后的目标图像。
  • waitKey(0);:等待用户按键,参数 0 表示无限期等待,直到用户按下按键。这用于保持窗口显示,否则程序会立即结束,图像窗口将一闪而过。
  • return 0;:主函数正常结束,返回 0 表示程序执行成功。

延申部分

一、双线性插值的数学原理

双线性插值是一种二维插值方法,其核心思想是根据目标图像中的像素位置,在源图像中找到其对应的位置,并根据该位置周围的四个相邻像素进行加权平均计算。假设我们在源图像中要找到位置 (x, y) 的像素值,而 x 和 y 是浮点数,其相邻的四个像素为 (x1, y1)(x1, y2)(x2, y1) 和 (x2, y2),其中 x1 = floor(x)x2 = ceil(x)y1 = floor(y)y2 = ceil(y),并且 u = x - x1v = y - y1。对于单通道图像,双线性插值公式为:
 

对于多通道图像(如 RGB),需要对每个通道分别进行上述计算。这种插值方法的优点是计算简单,结果相对平滑,能够在一定程度上保持图像的连续性,避免了最近邻插值产生的锯齿状效果。它在图像缩放、旋转、仿射变换等操作中广泛使用,因为这些操作通常会导致像素位置从整数坐标变为浮点数坐标,需要根据周围像素来估计新的像素值。

二、性能优化

  • 并行化:在上述代码中,使用了 forEach 函数进行像素遍历,但在性能要求较高的情况下,可以使用多线程或 GPU 加速。例如,OpenCV 提供了 parallel_for_ 函数,允许使用多线程并行处理图像像素。通过将图像分成多个区域,每个线程处理一个区域,可以充分利用多核 CPU 的性能。
#include <opencv2/opencv.hpp>
#include <opencv2/core/parallel.hpp>
using namespace cv;
using namespace std;

typedef cv::Point3_<uint8_t> Pixel;

class BilinearInterpolationBody : public cv::ParallelLoopBody {
private:
    Mat& src;
    Mat& dst;
    double sx;
    double sy;
public:
    BilinearInterpolationBody(Mat& _src, Mat& _dst, double _sx, double _sy) : src(_src), dst(_dst), sx(_sx), sy(_sy) {}

    void operator()(const cv::Range& range) const override {
        for (int r = range.start; r < range.end; ++r) {
            int row = r / dst.cols;
            int col = r % dst.cols;
            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];
                }
            }
        }
    }
};


// 双线性插值算法
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());
    BilinearInterpolationBody body(src, dst, sx, sy);
    cv::parallel_for_(cv::Range(0, dst.rows * dst.cols), body);
}


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;
}

在上述代码中,我们创建了一个 BilinearInterpolationBody 类,它继承自 cv::ParallelLoopBody,并重写了 operator() 方法。然后使用 cv::parallel_for_ 函数并行执行 BilinearInterpolationBody 对象的 operator() 方法,将图像分成多个区域,每个线程处理一个区域,提高了处理速度。

  • 使用 OpenCV 内置函数:实际上,OpenCV 已经提供了双线性插值的函数,如 resize 函数:
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;


int main() {
    Mat src = imread(".../grass.jpg");
    imshow("src", src);
    double sx = 1.5;
    double sy = 1.5;
    Mat dst;
    resize(src, dst, Size(), sx, sy, INTER_LINEAR);
    imshow("dst", dst);
    waitKey(0);
    return 0;
}

resize 函数中的 INTER_LINEAR 参数表示使用双线性插值。使用内置函数可以使代码更加简洁,同时利用了 OpenCV 的优化,性能可能更好。

三、应用场景和局限性

  • 图像缩放:这是双线性插值最常见的应用场景,通过调整缩放因子可以将图像放大或缩小。但对于大幅缩放,双线性插值可能会导致图像模糊,因为它只是简单地根据周围像素进行加权平均,对于放大操作,不能恢复出更多细节,对于缩小操作,可能会丢失一些细节。
  • 图像旋转和仿射变换:在图像旋转和仿射变换中,会导致像素位置的变化,双线性插值可以用来计算变换后图像的像素值。但对于旋转等操作,双线性插值可能会导致旋转后的图像出现一定程度的模糊,尤其是在旋转角度较大时。
  • 图像拼接和图像融合:在图像拼接中,可能需要对拼接区域的图像进行插值处理,以实现平滑过渡。双线性插值可以作为一种简单的方法,但对于高质量的图像拼接,可能需要更复杂的算法,如基于梯度的融合算法。

四、与其他插值方法的比较

  • 最近邻插值:最近邻插值是一种简单的插值方法,它直接将目标像素的值设置为源图像中最接近的像素的值。代码如下:
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;


// 最近邻插值算法
void nearestNeighborInterpolation(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());


    for (int row = 0; row < dst.rows; ++row) {
        for (int col = 0; col < dst.cols; ++col) {
            int src_row = static_cast<int>(row / sy);
            int src_col = static_cast<int>(col / sx);
            if (src_row >= src.rows) src_row = src.rows - 1;
            if (src_col >= src.cols) src_col = src.cols - 1;
            for (size_t k = 0; k < src.channels(); ++k) {
                dst.at<Vec3b>(row, col)[k] = src.at<Vec3b>(src_row, src_col)[k];
            }
        }
    }
}


int main() {
    Mat src = imread(".../grass.jpg");
    imshow("src", src);
    double sx = 1.5;
    double sy = 1.5;
    Mat dst;
    nearestNeighborInterpolation(src, dst, sx, sy);
    imshow("dst", dst);
    waitKey(0);
    return 0;
}

最近邻插值的优点是计算速度快,但会导致图像产生锯齿状效果,因为它没有考虑周围像素的信息,只是简单复制最近像素的值。

  • 双三次插值:双三次插值使用了更多的邻域像素(通常是 16 个),使用更高阶的多项式进行插值,能够得到更平滑的结果,尤其在图像放大时,能保留更多细节。OpenCV 中的 resize 函数可以使用 INTER_CUBIC 参数实现双三次插值:
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;


int main() {
    Mat src = imread(".../grass.jpg");
    imshow("src", src);
    double sx = 1.5;
    double sy = 1.5;
    Mat dst;
    resize(src, dst, Size(), sx, sy, INTER_CUBIC);
    imshow("dst", dst);
    waitKey(0);
    return 0;
}

双三次插值通常比双线性插值产生更好的效果,尤其是对于图像的放大操作。它的原理是使用一个三次多项式函数对源图像中周围 16 个像素进行加权计算,以得到目标像素的值。该多项式函数的设计考虑了像素之间的距离和梯度信息,使得生成的图像更加平滑,细节更加丰富,但相应的计算成本也更高。

 


网站公告

今日签到

点亮在社区的每一天
去签到