双线性插值算法:原理、实现、优化及在图像处理和多范畴中的广泛应用与发展 ...

打印 上一主题 下一主题

主题 1725|帖子 1725|积分 5175

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
一、头文件和命名空间



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

  1. typedef cv::Point3_<uint8_t> Pixel;
复制代码



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

  1. void bilinearInterpolation(Mat& src, Mat& dst, double sx, double sy) {
  2.     int dst_rows = static_cast<int>(src.rows * sy);
  3.     int dst_cols = static_cast<int>(src.cols * sx);
  4.     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 函数会创建一个指定尺寸和类型的矩阵,并将其元素初始化为零。
  1.     dst.forEach<Pixel>([&](Pixel &p, const int * position) -> void {
  2.         int row = position[0];
  3.         int col = position[1];
复制代码



  • dst.forEach<ixel>([&](Pixel &p, const int * position) -> void:使用 forEach 函数遍历目标图像的每个像素。forEach 是 C++11 中的一个函数,允许使用 lambda 表达式对矩阵的每个元素进行操作。这里的 Pixel 是之前定义的像素类型,p 是当前像素的引用,position 是一个包含当前像素位置的数组,position[0] 表示行索引,position[1] 表示列索引。
  1.         double before_x = double(col + 0.5) / sx - 0.5f;
  2.         double before_y = double(row + 0.5) / sy - 0.5;
  3.         int top_y = static_cast<int>(before_y);
  4.         int bottom_y = top_y + 1;
  5.         int left_x = static_cast<int>(before_x);
  6.         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 表示相邻的列。
  1.         double u = before_x - left_x;
  2.         double v = before_y - top_y;
复制代码



  • double u = before_x - left_x; 和 double v = before_y - top_y;:盘算源图像中对应位置的小数部门,u 和 v 是在 x 和 y 方向上距离左上角相邻像素的比例。
  1.         if ((top_y >= src.rows - 1) && (left_x >= src.cols - 1)) {//右下角
  2.             for (size_t k = 0; k < src.channels(); k++) {
  3.                 dst.at<Vec3b>(row, col)[k] = (1. - u) * (1. - v) * src.at<Vec3b>(top_y, left_x)[k];
  4.             }
  5.         } else if (top_y >= src.rows - 1) { //最后一行
  6.             for (size_t k = 0; k < src.channels(); k++) {
  7.                 dst.at<Vec3b>(row, col)[k]
  8.                         = (1. - u) * (1. - v) * src.at<Vec3b>(top_y, left_x)[k]
  9.                           + (1. - v) * u * src.at<Vec3b>(top_y, right_x)[k];
  10.             }
  11.         } else if (left_x >= src.cols - 1) {//最后一列
  12.             for (size_t k = 0; k < src.channels(); k++) {
  13.                 dst.at<Vec3b>(row, col)[k]
  14.                         = (1. - u) * (1. - v) * src.at<Vec3b>(top_y, left_x)[k]
  15.                           + (v) * (1. - u) * src.at<Vec3b>(bottom_y, left_x)[k];
  16.             }
  17.         } else {
  18.             for (size_t k = 0; k < src.channels(); k++) {
  19.                 dst.at<Vec3b>(row, col)[k]
  20.                         = (1. - u) * (1. - v) * src.at<Vec3b>(top_y, left_x)[k]
  21.                           + (1. - v) * (u) * src.at<Vec3b>(top_y, right_x)[k]
  22.                           + (v) * (1. - u) * src.at<Vec3b>(bottom_y, left_x)[k]
  23.                           + (u) * (v) * src.at<Vec3b>(bottom_y, right_x)[k];
  24.             }
  25.         }
  26.     });
  27. }
复制代码



  • 这里根据源图像中盘算得到的相邻像素位置进行不同情况的处理:

    • 如果盘算出的源图像坐标超出了源图像的右下角(即 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
  1. int main() {
  2.     Mat src = imread(".../grass.jpg");
  3.     imshow("src", src);
复制代码



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



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



  • 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 - x1、v = y - y1。对于单通道图像,双线性插值公式为:
 
对于多通道图像(如 RGB),必要对每个通道分别进行上述盘算。这种插值方法的优点是盘算简单,结果相对平滑,能够在一定程度上保持图像的连续性,避免了近来邻插值产生的锯齿状结果。它在图像缩放、旋转、仿射变换等操作中广泛使用,由于这些操作通常会导致像素位置从整数坐标变为浮点数坐标,必要根据周围像素来估计新的像素值。
二、性能优化



  • 并行化:在上述代码中,使用了 forEach 函数进行像素遍历,但在性能要求较高的情况下,可以使用多线程或 GPU 加快。例如,OpenCV 提供了 parallel_for_ 函数,允许使用多线程并行处理图像像素。通过将图像分成多个地区,每个线程处理一个地区,可以充分利用多核 CPU 的性能。
  1. #include <opencv2/opencv.hpp>#include <opencv2/core/parallel.hpp>using namespace cv;using namespace std;typedef cv::Point3_<uint8_t> Pixel;
  2. 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) {
  3.     int dst_rows = static_cast<int>(src.rows * sy);
  4.     int dst_cols = static_cast<int>(src.cols * sx);
  5.     dst = Mat::zeros(cv::Size(dst_cols, dst_rows), src.type());
  6.     BilinearInterpolationBody body(src, dst, sx, sy);    cv::parallel_for_(cv::Range(0, dst.rows * dst.cols), body);}int main() {
  7.     Mat src = imread(".../grass.jpg");
  8.     imshow("src", src);
  9.     double sx = 1.5;
  10.     double sy = 1.5;
  11.     Mat dst;
  12.     bilinearInterpolation(src,dst, sx, sy);
  13.     imshow("dst", dst);
  14.     waitKey(0);
  15.     return 0;
  16. }
复制代码

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


  • 使用 OpenCV 内置函数:现实上,OpenCV 已经提供了双线性插值的函数,如 resize 函数:
  1. #include <opencv2/opencv.hpp>using namespace cv;using namespace std;int main() {
  2.     Mat src = imread(".../grass.jpg");
  3.     imshow("src", src);
  4.     double sx = 1.5;    double sy = 1.5;    Mat dst;    resize(src, dst, Size(), sx, sy, INTER_LINEAR);    imshow("dst", dst);
  5.     waitKey(0);
  6.     return 0;
  7. }
复制代码

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



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



  • 近来邻插值:近来邻插值是一种简单的插值方法,它直接将目标像素的值设置为源图像中最接近的像素的值。代码如下:
  1. #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() {
  2.     Mat src = imread(".../grass.jpg");
  3.     imshow("src", src);
  4.     double sx = 1.5;    double sy = 1.5;    Mat dst;    nearestNeighborInterpolation(src, dst, sx, sy);    imshow("dst", dst);
  5.     waitKey(0);
  6.     return 0;
  7. }
复制代码

近来邻插值的优点是盘算速率快,但会导致图像产生锯齿状结果,由于它没有思量周围像素的信息,只是简单复制近来像素的值。


  • 双三次插值:双三次插值使用了更多的邻域像素(通常是 16 个),使用更高阶的多项式进行插值,能够得到更平滑的结果,尤其在图像放大时,能保存更多细节。OpenCV 中的 resize 函数可以使用 INTER_CUBIC 参数实现双三次插值:
  1. #include <opencv2/opencv.hpp>using namespace cv;using namespace std;int main() {
  2.     Mat src = imread(".../grass.jpg");
  3.     imshow("src", src);
  4.     double sx = 1.5;    double sy = 1.5;    Mat dst;    resize(src, dst, Size(), sx, sy, INTER_CUBIC);    imshow("dst", dst);
  5.     waitKey(0);
  6.     return 0;
  7. }
复制代码
双三次插值通常比双线性插值产生更好的结果,尤其是对于图像的放大操作。它的原理是使用一个三次多项式函数对源图像中周围 16 个像素进行加权盘算,以得到目标像素的值。该多项式函数的计划思量了像素之间的距离和梯度信息,使得天生的图像更加平滑,细节更加丰富,但相应的盘算本钱也更高。



 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

惊落一身雪

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表