仿射变换的实现由两种方式:一种是 前向映射(Forward Mapping):直接采用利用原图像坐标 \((v,w)\) 通过 \(\begin{bmatrix} x & y & 1\end{bmatrix}=\begin{bmatrix} v & w & 1\end{bmatrix} \textbf{T}\) 得到变换后的坐标 \((x,y)\),使用前向映射会导致一些问题:可能会有多个像素坐标映射到输出图像的同一位置,也可能输出图像的某些位置完全没有相应的输入图像像素与它匹配,也就是没有被映射到,造成有规律的空洞(黑色的花纹状);更好的一种方式是采用 反向映射(Inverse Mapping):扫描输出图像的位置 \((x,y)\),通过 \(\begin{bmatrix} v & w & 1\end{bmatrix}= \begin{bmatrix} x & y & 1\end{bmatrix}\textbf{T}^{-1}\)(其中 \(\textbf{T}^{-1}\) 为 \(\textbf{T}\) 的逆矩阵)计算输入图像对应的位置 \((v,w)\),通过插值方法决定输出图像该位置的灰度值。
本文这里采取冈萨雷斯的《数字图像处理_第三版》的变换矩阵方式,毕竟所学的矩阵论也是将变换矩阵放在后面作为第二个因子。虽然仿射变换都有现成的 API 可以调用,而且速度一般要比自己写的要快,但是知其然终究也要知其所以然。
if x1 <= 1 W = 1 - (a+3)*x2 + (a+2)*x3; elseif x1>1 && x1<=2 W = -4*a + 8*a*x1 - 5*a*x2 + a*x3; else W = 0; end
旋转变换中感觉插值的作用没体现出来,以肉眼来看感觉三种插值方法的效果差不多,可能是 Shaun 选取的示例不好,为了体现插值效果,应该采用尺度变换(缩放变换)的。以上代码改为尺度变换也简单,自定义图像缩放后的宽高,以两倍为例,H = R * 2; W = C * 2;,再将旋转变换矩阵改为尺度变换矩阵,尺度变换矩阵中 \(c_x=W/C;c_y=H/R\)。
for (int y = 0; y < H; y++) { for (int x = 0; x < W; x++) { cv::Mat point = (cv::Mat_<double>(1, 3) << x, y, 1); cv::Mat original_coordinate = point * inv_T; // 矩阵乘法 double v = original_coordinate.at<double>(0, 0); // 原图像的横坐标,列,宽 double w = original_coordinate.at<double>(0, 1); // 原图像的纵坐标,行,高
// 变换后的位置判断是否越界 if (v >= 0 && w >= 0 && v <= C - 1 && w <= R - 1) { res.at<uchar>(y, x) = img.at<uchar>(round(w), round(v)); // 用原图像对应坐标的像素值填充变换后的图像(最邻近插值) // ------------ - 双线性插值(bilinear interpolation)---------------- - int left = floor(v), right = ceil(v), top = floor(w), bottom = ceil(w); double dC = v - left; // 列偏差 double dR = w - top; // 行偏差 res.at<uchar>(y, x) = (1 - dC)*(1 - dR)*img.at<uchar>(top, left) + dC*(1 - dR)*img.at<uchar>(top, right) + (1 - dC)*dR*img.at<uchar>(bottom, left) + dC*dR*img.at<uchar>(bottom, right); // ------------ - 双三次插值(bicubic interpolation)------------------------ - if (left >= 1 && top >= 1 && left <= (C - 3) && top <= (R - 3)) { cv::Mat MA = (cv::Mat_<double>(1, 4) << bicubic(1 + dC), bicubic(dC), bicubic(1 - dC), bicubic(2 - dC)); cv::Mat MB = img(cv::Rect(left - 1, top - 1, 4, 4)); // 提取当前相邻区域16个像素点做插值 MB.convertTo(MB, CV_64FC1); // 变换为浮点型数据 MB = MB.t(); // 求转置矩阵 cv::Mat MC = (cv::Mat_<double>(4, 1) << bicubic(1 + dR), bicubic(dR), bicubic(1 - dR), bicubic(2 - dR)); cv::Mat result = MA*MB*MC; res.at<uchar>(y, x) = static_cast<uchar>(result.at<double>(0, 0)); } } } }
import cv2 import numpy as np import tensorflow as tf from object_detection.utils import label_map_util from object_detection.utils import visualization_utils as vis_util
因为当时还在用 opencv-2.4.11,所以本文所实现的代码是基于 opencv-2.4.11,不过应该只要在 opencv-2.0 版本及以上只要有 Mat 数据结构的都能用,毕竟 Shaun 只用到了 OpenCV 中的 Mat 数据结构。Talk is cheap, show you the code(代码很乱,估计也只用这么一次,所以就没怎么注意了 :-P)。具体 C++ 实现代码为:
// ---------------- 鼠标事件回调函数 --------------------------------- static cv::Mat src_img; // 原始图像全局变量 staticvoidmouseCallback(int event, int x, int y, int flags, void *) { bool selected = false; static cv::Point left_top_vertex, right_down_vertex; // 左上角顶点和右下角顶点 // When the left mouse button is pressed, record its position and save it in corner1 if (event == CV_EVENT_LBUTTONDOWN) // 左键按下 { left_top_vertex.x = x; left_top_vertex.y = y; std::cout << "Corner 1 recorded at " << left_top_vertex << std::endl; } // When the left mouse button is released, record its position and save it in corner2 if (event == cv::EVENT_LBUTTONUP) // 左键弹起 { // Also check if user selection is bigger than 20 pixels (jut for fun!) if (abs(x - left_top_vertex.x) > 10 && abs(y - left_top_vertex.y) > 10) { right_down_vertex.x = x; right_down_vertex.y = y; std::cout << "Corner 2 recorded at " << right_down_vertex << std::endl << std::endl; selected = true; } else { std::cout << "Please select a bigger region" << std::endl; } } // Update the box showing the selected region as the user drags the mouse if (flags == CV_EVENT_FLAG_LBUTTON) // 左键拖拽 { cv::Point pt; pt.x = x; pt.y = y; cv::Mat local_img = src_img.clone(); rectangle(local_img, left_top_vertex, pt, cv::Scalar(0, 0, 255)); imshow("Cropping app", local_img); } // Define ROI and crop it out when both corners have been selected if (selected) { cv::Rect box; box.width = abs(left_top_vertex.x - right_down_vertex.x); box.height = abs(left_top_vertex.y - right_down_vertex.y); box.x = cv::min(left_top_vertex.x, right_down_vertex.x); box.y = cv::min(left_top_vertex.y, right_down_vertex.y); // Make an image out of just the selected ROI and display it in a new window cv::Mat crop(src_img, box); cv::namedWindow("Crop"); imshow("Crop", crop); } }
// ---------- 响应鼠标事件 ------------------------------------ voidsetMouseCallbackTest() { src_img = cv::imread("../data/lena.jpg", CV_LOAD_IMAGE_ANYDEPTH | CV_LOAD_IMAGE_ANYCOLOR); cv::namedWindow("Cropping app"); imshow("Cropping app", src_img); // Set the mouse event callback function cv::setMouseCallback("Cropping app", mouseCallback);