【发布时间】:2011-08-30 13:02:17
【问题描述】:
由于某种原因,每当我使用 OpenCV 的 warpPerspective() 函数时,最终的变形图像不包含原始图像中的所有内容。图像的左侧部分似乎被切断了。我认为发生这种情况的原因是因为扭曲的图像是在画布的最左侧位置为 warpPerspective() 创建的。有没有办法解决这个问题?谢谢
【问题讨论】:
标签: c++ image image-processing opencv
由于某种原因,每当我使用 OpenCV 的 warpPerspective() 函数时,最终的变形图像不包含原始图像中的所有内容。图像的左侧部分似乎被切断了。我认为发生这种情况的原因是因为扭曲的图像是在画布的最左侧位置为 warpPerspective() 创建的。有没有办法解决这个问题?谢谢
【问题讨论】:
标签: c++ image image-processing opencv
warpPerspective() 工作正常。无需重写。 你可能用错了。
记住以下提示:
【讨论】:
Mat tmp; cv::resize( imageList[image1], tmp, Size(), scaleFactor, scaleFactor ); warpPerspective( tmp, transformedImage, homography, Size( 2*tmp.cols, 2*tmp.rows ) );
秘密分为两部分:变换矩阵(单应性)和生成的图像大小。
使用 getPerspectiveTransform() 计算正确的变换。从原始图像中取4个点,计算它们在目的地的正确位置,以相同的顺序放入两个向量中,并用它们计算透视变换矩阵。
确保目标图像大小(warpPerspective() 的第三个参数)正是您想要的。将其定义为 Size(myWidth, myHeight)。
【讨论】:
出现问题是因为单应性将图像的一部分映射到图像区域之外的负 x,y 值,因此无法绘制。 我们希望做的是将扭曲的输出偏移一些像素,以将整个扭曲的图像“分流”到正坐标(因此在图像区域内)。
可以使用矩阵乘法组合单应性(这就是它们如此强大的原因)。如果A和B是单应性,则AB表示先应用B,再应用A的单应性。
因此,为了偏移输出,我们需要做的就是创建一个平移的单应矩阵,然后将其预乘以我们的原始单应矩阵
二维单应矩阵如下所示:
[R11,R12,T1]
[R21,R22,T2]
[ P , P , 1]
其中R代表旋转矩阵,T代表平移,P代表透视扭曲。 所以一个纯平移的单应性看起来像这样:
[ 1 , 0 , x_offset]
[ 0 , 1 , y_offset]
[ 0 , 0 , 1 ]
因此,只需将单应性预乘以与上述类似的矩阵,您的输出图像就会发生偏移。
(确保使用矩阵乘法,而不是元素乘法!)
【讨论】:
我已经做了一种方法... 它正在工作。
perspectiveTransform(obj_corners,scene_corners,H);
int maxCols(0),maxRows(0);
for(int i=0;i<scene_corners.size();i++)
{
if(maxRows < scene_corners.at(i).y)
maxRows = scene_corners.at(i).y;
if(maxCols < scene_corners.at(i).x)
maxCols = scene_corners.at(i).x;
}
我只是分别找到x点和y点的最大值并放在上面
warpPerspective( tmp, transformedImage, homography, Size( maxCols, maxRows ) );
【讨论】:
试试下面的homography_warp。
void homography_warp(const cv::Mat& src, const cv::Mat& H, cv::Mat& dst);
src 是源图片。
H 是你的单应性。
dst 是扭曲的图像。
homography_warp 按照https://stackoverflow.com/users/1060066/matt-freeman 在他的回答https://stackoverflow.com/a/8229116/15485 中的描述调整你的单应性
// Convert a vector of non-homogeneous 2D points to a vector of homogenehous 2D points.
void to_homogeneous(const std::vector< cv::Point2f >& non_homogeneous, std::vector< cv::Point3f >& homogeneous)
{
homogeneous.resize(non_homogeneous.size());
for (size_t i = 0; i < non_homogeneous.size(); i++) {
homogeneous[i].x = non_homogeneous[i].x;
homogeneous[i].y = non_homogeneous[i].y;
homogeneous[i].z = 1.0;
}
}
// Convert a vector of homogeneous 2D points to a vector of non-homogenehous 2D points.
void from_homogeneous(const std::vector< cv::Point3f >& homogeneous, std::vector< cv::Point2f >& non_homogeneous)
{
non_homogeneous.resize(homogeneous.size());
for (size_t i = 0; i < non_homogeneous.size(); i++) {
non_homogeneous[i].x = homogeneous[i].x / homogeneous[i].z;
non_homogeneous[i].y = homogeneous[i].y / homogeneous[i].z;
}
}
// Transform a vector of 2D non-homogeneous points via an homography.
std::vector<cv::Point2f> transform_via_homography(const std::vector<cv::Point2f>& points, const cv::Matx33f& homography)
{
std::vector<cv::Point3f> ph;
to_homogeneous(points, ph);
for (size_t i = 0; i < ph.size(); i++) {
ph[i] = homography*ph[i];
}
std::vector<cv::Point2f> r;
from_homogeneous(ph, r);
return r;
}
// Find the bounding box of a vector of 2D non-homogeneous points.
cv::Rect_<float> bounding_box(const std::vector<cv::Point2f>& p)
{
cv::Rect_<float> r;
float x_min = std::min_element(p.begin(), p.end(), [](const cv::Point2f& lhs, const cv::Point2f& rhs) {return lhs.x < rhs.x; })->x;
float x_max = std::max_element(p.begin(), p.end(), [](const cv::Point2f& lhs, const cv::Point2f& rhs) {return lhs.x < rhs.x; })->x;
float y_min = std::min_element(p.begin(), p.end(), [](const cv::Point2f& lhs, const cv::Point2f& rhs) {return lhs.y < rhs.y; })->y;
float y_max = std::max_element(p.begin(), p.end(), [](const cv::Point2f& lhs, const cv::Point2f& rhs) {return lhs.y < rhs.y; })->y;
return cv::Rect_<float>(x_min, y_min, x_max - x_min, y_max - y_min);
}
// Warp the image src into the image dst through the homography H.
// The resulting dst image contains the entire warped image, this
// behaviour is the same of Octave's imperspectivewarp (in the 'image'
// package) behaviour when the argument bbox is equal to 'loose'.
// See http://octave.sourceforge.net/image/function/imperspectivewarp.html
void homography_warp(const cv::Mat& src, const cv::Mat& H, cv::Mat& dst)
{
std::vector< cv::Point2f > corners;
corners.push_back(cv::Point2f(0, 0));
corners.push_back(cv::Point2f(src.cols, 0));
corners.push_back(cv::Point2f(0, src.rows));
corners.push_back(cv::Point2f(src.cols, src.rows));
std::vector< cv::Point2f > projected = transform_via_homography(corners, H);
cv::Rect_<float> bb = bounding_box(projected);
cv::Mat_<double> translation = (cv::Mat_<double>(3, 3) << 1, 0, -bb.tl().x, 0, 1, -bb.tl().y, 0, 0, 1);
cv::warpPerspective(src, dst, translation*H, bb.size());
}
【讨论】:
这是我的解决方案
由于“warpPerspective()”中的第三个参数是一个变换矩阵,
我们可以制作一个变换矩阵, 它先向后移动图像,然后旋转图像,最后向前移动图像。
就我而言,我有一个高度为 160 像素、宽度为 160 像素的图像。 我想围绕 [80,80] 而不是 [0,0] 旋转图像
首先,将图像向后移动(即 T1)
然后旋转图像(即 R)
最终将图像向前移动(即 T2)
void rotateImage(Mat &src_img,int degree)
{
float radian=(degree/180.0)*M_PI;
Mat R(3,3,CV_32FC1,Scalar(0));
R.at<float>(0,0)=cos(radian);R.at<float>(0,1)=-sin(radian);
R.at<float>(1,0)=sin(radian);R.at<float>(1,1)=cos(radian);
R.at<float>(2,2)=1;
Mat T1(3,3,CV_32FC1,Scalar(0));
T1.at<float>(0,2)=-80;
T1.at<float>(1,2)=-80;
T1.at<float>(0,0)=1;
T1.at<float>(1,1)=1;
T1.at<float>(2,2)=1;
Mat T2(3,3,CV_32FC1,Scalar(0));
T2.at<float>(0,2)=80;
T2.at<float>(1,2)=80;
T2.at<float>(0,0)=1;
T2.at<float>(1,1)=1;
T2.at<float>(2,2)=1;
std::cerr<<T1<<std::endl;
std::cerr<<R<<std::endl;
std::cerr<<T2<<std::endl;
std::cerr<<T2*R*T1<<"\n"<<std::endl;
cv::warpPerspective(src_img, src_img, T2*R*T1, src_img.size(), cv::INTER_LINEAR);
}
【讨论】:
这里有一个opencv-python解决你的问题,我放在github上:https://github.com/Sanster/notes/blob/master/opencv/warpPerspective.md
关键点如user3094631所说,得到两个平移矩阵(T1,T2)并应用于旋转矩阵(M)T2*M*T1
在我给出的代码中,T1 是从原点图像的中心点开始的,T2 是从变换后的boundingBox 的左上点开始的。转换后的boundingBox来自原点:
height = img.shape[0]
width = img.shape[1]
#..get T1
#..get M
pnts = np.asarray([
[0, 0],
[width, 0],
[width, height],
[0, height]
], dtype=np.float32)
pnts = np.array([pnts])
dst_pnts = cv2.perspectiveTransform(pnts, M * T1)[0]
dst_pnts = np.asarray(dst_pnts, dtype=np.float32)
bbox = cv2.boundingRect(dst_pnts)
T2 = np.matrix([[1., 0., 0 - bbox[0]],
[0., 1., 0 - bbox[1]],
[0., 0., 1.]])
【讨论】:
马特的回答是一个好的开始,他说你需要将单应性乘以是正确的
[ 1 , 0 , x_offset]
[ 0 , 1 , y_offset]
[ 0 , 0 , 1 ]
但他没有具体说明 x_offset 和 y_offset 是什么。其他答案说只是进行透视变换,但这是不正确的。您想进行 INVERSE 透视变换。
仅仅因为点 0,0 转换为 -10,-10,并不意味着将图像移动 10,10 会导致图像未裁剪。这是因为点 10,10 不一定映射到 0,0。
您要做的是找出将映射到0,0的点,并将图像移动这么多。为此,您采用单应性的逆 (cv2.invert) 并应用 perspectiveTransform。
您需要应用反向变换来找到正确的点。
这将获得正确的 x_offset 和 y_offset 以对齐左上角。从那里找到正确的边界框并完美地拟合整个图像,您需要计算出倾斜度(在正常的非逆转换之后图像向左或向上倾斜多少)并将该量添加到您的 x_offset 和 y_offset也是。
编辑:这都是理论。在我的测试中,图像有几个像素的偏差,我不知道为什么。
【讨论】:
解决翘曲图像投影到翘曲输出之外的问题的一种简单方法是将翘曲图像转换到正确的位置。主要挑战在于找到正确的平移偏移量。
此处给出的其他答案中已经讨论了翻译的概念,因此我将解释如何获得正确的偏移量。想法是两个图像中的匹配特征在最终拼接的图像中应该具有相同的坐标。
假设我们按如下方式引用图像:
si):需要变形的图片di): 'source image' 的视角将被扭曲的图像wsi): 源图像
将其扭曲到目标图像透视图后为了计算平移偏移量,您需要执行以下操作:
在您对好的匹配进行采样并从单应性中找到掩码后,将最佳匹配的关键点(一个具有最小距离且是内点的关键点(应该从单应性计算获得的掩码中获得 1 的值))存储在 @987654324 @ 和 di。让我们分别在si and diisbm_siandbm_di`中说最佳匹配的关键点。
bm_si = [x1, y1,1]
bm_di = [x2, y2, 1]
只需将 bm_si 与单应矩阵 (H) 相乘,即可找到 wsi 中的位置。
bm_wsi = np.dot(H,bm_si)
bm_wsi = [x/bm_wsi[2] for x in bm_wsi]
根据您将在 si 变形 (=wsi) 的输出上放置 di 的位置,调整 bm_di
假设您要从左图像变形到右图像(例如左图像是si,右图像是di),那么您将把di 放在右侧wsi 上,因此bm_di[0] += si.shape[0]
经过以上步骤
x_offset = bm_di[0] - bm_si[0]
y_offset = bm_di[1] - bm_si[1]
使用计算的偏移量找到新的单应矩阵并扭曲si。
T = np.array([[1, 0, x_offset], [0, 1, y_offset], [0, 0, 1]])
translated_H = np.dot(T.H)
wsi_frame_size = tuple(2*x for x in si.shape)
stitched = cv2.warpPerspective(si, translated_H, wsi_frame_size)
stitched[0:si.shape[0],si.shape[1]:] = di
【讨论】: