【问题标题】:Blending does not remove seams in OpenCV混合不会消除 OpenCV 中的接缝
【发布时间】:2014-04-14 11:38:38
【问题描述】:

我正在尝试混合 2 个图像,以便它们之间的接缝消失。

第一张图片:

第二张图片:

如果混合应用:

如果应用了混合:

我使用了阿尔法混合NO 接缝被移除;事实上图像仍然相同,但更暗

这是我进行混合的部分

Mat warped1;
warpPerspective(left,warped1,perspectiveTransform,front.size());// Warping may be used for correcting image distortion
imshow("combined1",warped1/2+front/2);
            vector<Mat> imgs;
            imgs.push_back(warped1/2);
            imgs.push_back(front/2);
            double alpha = 0.5; 
            int min_x = ( imgs[0].cols - imgs[1].cols)/2 ;
            int min_y = ( imgs[0].rows -imgs[1].rows)/2 ;
            int width, height;
            if(min_x < 0) {
                min_x = 0; 
                width = (imgs).at(0).cols;
            }
            else         
                width = (imgs).at(1).cols;
            if(min_y < 0) {
                min_y = 0; 
                height = (imgs).at(0).rows - 1;
            }

            else         
                height = (imgs).at(1).rows - 1;
            Rect roi = cv::Rect(min_x, min_y, imgs[1].cols, imgs[1].rows);  
            Mat out_image = imgs[0].clone();
            Mat A_roi= imgs[0](roi);
            Mat out_image_roi = out_image(roi);
            addWeighted(A_roi,alpha,imgs[1],1-alpha,0.0,out_image_roi);
            imshow("foo",imgs[0](roi));

【问题讨论】:

  • 你能提供变形的图片吗?写了一个混合功能,但我确实需要扭曲/对齐的图像来演示。请提供相同尺寸的对齐图片。

标签: c++ opencv blending


【解决方案1】:

我选择根据到“物体中心”的距离来定义alpha值,离物体中心越远,alpha值越小。 “对象”由掩码定义。

我已将图像与 GIMP 对齐(类似于您的 warpPerspective)。它们需要在同一个坐标系中,并且两个图像必须具有相同的大小。

我的输入图像如下所示:

int main()
{

cv::Mat i1 = cv::imread("blending/i1_2.png");
cv::Mat i2 = cv::imread("blending/i2_2.png");

cv::Mat m1 = cv::imread("blending/i1_2.png",CV_LOAD_IMAGE_GRAYSCALE);
cv::Mat m2 = cv::imread("blending/i2_2.png",CV_LOAD_IMAGE_GRAYSCALE);

    // works too, for background near white
    //  m1 = m1 < 220;
    //  m2 = m2 < 220;

//    edited:  using OTSU thresholding. If not working you have to create your own masks with a better technique
cv::threshold(m1,m1,255,255,cv::THRESH_BINARY_INV|cv::THRESH_OTSU);
cv::threshold(m2,m2,255,255,cv::THRESH_BINARY_INV|cv::THRESH_OTSU);

cv::Mat out = computeAlphaBlending(i1,m1,i2,m2);

cv::waitKey(-1);
return 0;
}

具有混合功能: 我想需要一些 cmets 和优化,我稍后会添加它们。

cv::Mat computeAlphaBlending(cv::Mat image1, cv::Mat mask1, cv::Mat image2, cv::Mat mask2)
{
// edited: find regions where no mask is set
// compute the region where no mask is set at all, to use those color values unblended
cv::Mat bothMasks = mask1 | mask2;
cv::imshow("maskOR",bothMasks);
cv::Mat noMask = 255-bothMasks;
// ------------------------------------------

// create an image with equal alpha values:
cv::Mat rawAlpha = cv::Mat(noMask.rows, noMask.cols, CV_32FC1);
rawAlpha = 1.0f;

// invert the border, so that border values are 0 ... this is needed for the distance transform
cv::Mat border1 = 255-border(mask1);
cv::Mat border2 = 255-border(mask2);

// show the immediate results for debugging and verification, should be an image where the border of the face is black, rest is white
cv::imshow("b1", border1);
cv::imshow("b2", border2);

// compute the distance to the object center
cv::Mat dist1;
cv::distanceTransform(border1,dist1,CV_DIST_L2, 3);

// scale distances to values between 0 and 1
double min, max; cv::Point minLoc, maxLoc;

// find min/max vals
cv::minMaxLoc(dist1,&min,&max, &minLoc, &maxLoc, mask1&(dist1>0));  // edited: find min values > 0
dist1 = dist1* 1.0/max; // values between 0 and 1 since min val should alwaysbe 0

// same for the 2nd image
cv::Mat dist2;
cv::distanceTransform(border2,dist2,CV_DIST_L2, 3);
cv::minMaxLoc(dist2,&min,&max, &minLoc, &maxLoc, mask2&(dist2>0));  // edited: find min values > 0
dist2 = dist2*1.0/max;  // values between 0 and 1


//TODO: now, the exact border has value 0 too... to fix that, enter very small values wherever border pixel is set...

// mask the distance values to reduce information to masked regions
cv::Mat dist1Masked;
rawAlpha.copyTo(dist1Masked,noMask);    // edited: where no mask is set, blend with equal values
dist1.copyTo(dist1Masked,mask1);
rawAlpha.copyTo(dist1Masked,mask1&(255-mask2)); //edited

cv::Mat dist2Masked;
rawAlpha.copyTo(dist2Masked,noMask);    // edited: where no mask is set, blend with equal values
dist2.copyTo(dist2Masked,mask2);
rawAlpha.copyTo(dist2Masked,mask2&(255-mask1)); //edited

cv::imshow("d1", dist1Masked);
cv::imshow("d2", dist2Masked);

// dist1Masked and dist2Masked now hold the "quality" of the pixel of the image, so the higher the value, the more of that pixels information should be kept after blending
// problem: these quality weights don't build a linear combination yet

// you want a linear combination of both image's pixel values, so at the end you have to divide by the sum of both weights
cv::Mat blendMaskSum = dist1Masked+dist2Masked;
//cv::imshow("blendmask==0",(blendMaskSum==0));

// you have to convert the images to float to multiply with the weight
cv::Mat im1Float;
image1.convertTo(im1Float,dist1Masked.type());
cv::imshow("im1Float", im1Float/255.0);

// TODO: you could replace those splitting and merging if you just duplicate the channel of dist1Masked and dist2Masked
// the splitting is just used here to use .mul later... which needs same number of channels
std::vector<cv::Mat> channels1;
cv::split(im1Float,channels1);
// multiply pixel value with the quality weights for image 1
cv::Mat im1AlphaB = dist1Masked.mul(channels1[0]);
cv::Mat im1AlphaG = dist1Masked.mul(channels1[1]);
cv::Mat im1AlphaR = dist1Masked.mul(channels1[2]);

std::vector<cv::Mat> alpha1;
alpha1.push_back(im1AlphaB);
alpha1.push_back(im1AlphaG);
alpha1.push_back(im1AlphaR);
cv::Mat im1Alpha;
cv::merge(alpha1,im1Alpha);
cv::imshow("alpha1", im1Alpha/255.0);

cv::Mat im2Float;
image2.convertTo(im2Float,dist2Masked.type());

std::vector<cv::Mat> channels2;
cv::split(im2Float,channels2);
// multiply pixel value with the quality weights for image 2
cv::Mat im2AlphaB = dist2Masked.mul(channels2[0]);
cv::Mat im2AlphaG = dist2Masked.mul(channels2[1]);
cv::Mat im2AlphaR = dist2Masked.mul(channels2[2]);

std::vector<cv::Mat> alpha2;
alpha2.push_back(im2AlphaB);
alpha2.push_back(im2AlphaG);
alpha2.push_back(im2AlphaR);
cv::Mat im2Alpha;
cv::merge(alpha2,im2Alpha);
cv::imshow("alpha2", im2Alpha/255.0);

// now sum both weighted images and divide by the sum of the weights (linear combination)
cv::Mat imBlendedB = (im1AlphaB + im2AlphaB)/blendMaskSum;
cv::Mat imBlendedG = (im1AlphaG + im2AlphaG)/blendMaskSum;
cv::Mat imBlendedR = (im1AlphaR + im2AlphaR)/blendMaskSum;
std::vector<cv::Mat> channelsBlended;
channelsBlended.push_back(imBlendedB);
channelsBlended.push_back(imBlendedG);
channelsBlended.push_back(imBlendedR);

// merge back to 3 channel image
cv::Mat merged;
cv::merge(channelsBlended,merged);

// convert to 8UC3
cv::Mat merged8U;
merged.convertTo(merged8U,CV_8UC3);

return merged8U;
}

和辅助函数:

cv::Mat border(cv::Mat mask)
{
cv::Mat gx;
cv::Mat gy;

cv::Sobel(mask,gx,CV_32F,1,0,3);
cv::Sobel(mask,gy,CV_32F,0,1,3);

cv::Mat border;
cv::magnitude(gx,gy,border);

return border > 100;
}

结果:

编辑:忘记了一个功能;) 编辑:现在保留原始背景

【讨论】:

  • 嗨@Micka。我有一个疑问:我尝试将上面相同的图像与其原始背景混合。他们的原始背景是一种白色,而不是纯白色。因此,结果就像问题中的原始结果一样 - 好像没有发生混合。我认为这与 m1 = m1 > 0/255 或 m2 = m2 > 0/255 有关,因为它们的背景既不是白色也不是黑色。你能告诉我吗?我想保留原来的背景
  • @Steph ,我调整了算法以保留背景(它没有)。但请记住,混合质量取决于输入掩码的质量,它应该看起来像 Haris 答案的前两个图像。如果使用添加的cv::threshold(以前的 m1 = m1>0)计算蒙版没有得到好的结果,您将不得不使用更好的函数从单个图像中提取目标对象(否则不清楚混合应该在什么/位置完成)。
  • 嗨@Micka。上面的背景不是原来的。我在主要i.imgur.com/We45tSZ.jpg 中设置阈值时得到这个,最终结果是i.imgur.com/mdxUSHe.jpg:/
  • 您是否尝试提取最外层的轮廓?也许那将是一个很好的蒙版边框......
  • 也许您必须阅读有关图像分割的文献。
【解决方案2】:

好的。这是一个新的尝试,它可能只适用于您的特定任务,将这些面部的 3 张图像准确混合,前、左、右。

我使用这些输入:

前(i1):

左(i2):

右(i3):

前掩码(m1):(可选):

这些图像的问题在于,正面图像仅覆盖一小部分,而左右重叠整个正面图像,这导致我的其他解决方案中的混合效果不佳。此外,图像的对齐不是很好(主要是由于透视效果),因此可能会出现混合伪影。

现在这个新方法的想法是,您肯定希望保留前面图像的部分,这些部分位于您的彩色“标记点”所跨越的区域内,这些部分不应混合。离该标记区域越远,应该使用来自左右图像的信息越多,因此我们创建一个带有 alpha 值的蒙版,它从 1(在标记区域内)线性降低到 0(在某个定义的距离处)从标记区域)。

所以标记所跨越的区域是这个:

既然我们知道左图基本上用在左标记三角形的左边区域,我们可以为左图和右图创建掩码,用于查找应该被前图另外覆盖的区域:

左:

右:

前标记区域以及不在左侧和右侧掩码中的所有内容:

这可以用可选的前置掩码输入来掩蔽,这样会更好,因为这个前置图像示例没有覆盖整个图像,但遗憾的是只覆盖了图像的一部分。

现在这是混合蒙版,alpha 值线性递减,直到与蒙版的距离为10 或更多像素:

现在我们首先创建仅覆盖左右图像的图像,复制大部分未混合的部分,但将左/右蒙版未覆盖的部分与0.5*left + 0.5*right混合在一起

blendLR:

最后我们通过计算将front 图像融合到blendLR 中:

blended = alpha*front + (1-alpha)*blendLR

一些改进可能包括从一些更高的信息(如重叠的大小或从标记三角形到面部边界的大小)计算 maxDist 值。

另一个改进是不计算0.5*left + 0.5*right,而是在这里也做一些阿尔法混合,从左边的图像中获取更多信息,我们在差距的左边。这将减少图像中间的接缝(前图像部分的顶部和底部)。

// idea: keep all the pixels from front image that are inside your 6 points area always unblended:
cv::Mat blendFrontAlpha(cv::Mat front, cv::Mat left, cv::Mat right, std::vector<cv::Point> sixPoints, cv::Mat frontForeground = cv::Mat())
{
// define some maximum distance. No information of the front image is used if it's further away than that maxDist.
// if you have some real masks, you can easily set the maxDist according to the dimension of that mask - dimension of the 6-point-mask
float maxDist = 10;

// to use the cv function to draw contours we must order it like this:
std::vector<std::vector<cv::Point> > contours;
contours.push_back(sixPoints);

// create the mask
cv::Mat frontMask = cv::Mat::zeros(front.rows, front.cols, CV_8UC1);

// draw those 6 points connected as a filled contour
cv::drawContours(frontMask,contours,0,cv::Scalar(255),-1);

// add "lines": everything left from the points 3-4-5 might be used from left image, everything from the points 0-1-2 might be used from the right image:
cv::Mat leftMask = cv::Mat::zeros(front.rows, front.cols, CV_8UC1);
{
    cv::Point2f center = cv::Point2f(sixPoints[3].x, sixPoints[3].y);

    float steigung = ((float)sixPoints[5].y - (float)sixPoints[3].y)/((float)sixPoints[5].x - (float)sixPoints[3].x);
    if(sixPoints[5].x - sixPoints[3].x == 0) steigung = 2*front.rows;

    float n = center.y - steigung*center.x;

    cv::Point2f top = cv::Point2f( (0-n)/steigung , 0);
    cv::Point2f bottom = cv::Point2f( (front.rows-1-n)/steigung , front.rows-1);

    // now create the contour of the left image:
    std::vector<cv::Point> leftMaskContour;
    leftMaskContour.push_back(top);
    leftMaskContour.push_back(bottom);
    leftMaskContour.push_back(cv::Point(0,front.rows-1));
    leftMaskContour.push_back(cv::Point(0,0));

    std::vector<std::vector<cv::Point> > leftMaskContours;
    leftMaskContours.push_back(leftMaskContour);
    cv::drawContours(leftMask,leftMaskContours,0,cv::Scalar(255),-1);

    cv::imshow("leftMask", leftMask);

    cv::imwrite("x_leftMask.png", leftMask);
}

// add "lines": everything left from the points 3-4-5 might be used from left image, everything from the points 0-1-2 might be used from the right image:
cv::Mat rightMask = cv::Mat::zeros(front.rows, front.cols, CV_8UC1);
{
    // add "lines": everything left from the points 3-4-5 might be used from left image, everything from the points 0-1-2 might be used from the right image:
    cv::Point2f center = cv::Point2f(sixPoints[2].x, sixPoints[2].y);

    float steigung = ((float)sixPoints[0].y - (float)sixPoints[2].y)/((float)sixPoints[0].x - (float)sixPoints[2].x);
    if(sixPoints[0].x - sixPoints[2].x == 0) steigung = 2*front.rows;

    float n = center.y - steigung*center.x;

    cv::Point2f top = cv::Point2f( (0-n)/steigung , 0);
    cv::Point2f bottom = cv::Point2f( (front.rows-1-n)/steigung , front.rows-1);

    std::cout << top << " - " << bottom << std::endl;

    // now create the contour of the left image:
    std::vector<cv::Point> rightMaskContour;
    rightMaskContour.push_back(cv::Point(front.cols-1,0));
    rightMaskContour.push_back(cv::Point(front.cols-1,front.rows-1));
    rightMaskContour.push_back(bottom);
    rightMaskContour.push_back(top);

    std::vector<std::vector<cv::Point> > rightMaskContours;
    rightMaskContours.push_back(rightMaskContour);
    cv::drawContours(rightMask,rightMaskContours,0,cv::Scalar(255),-1);

    cv::imshow("rightMask", rightMask);
    cv::imwrite("x_rightMask.png", rightMask);
}

// add everything that's not in the side masks to the front mask:
cv::Mat additionalFrontMask = (255-leftMask) & (255-rightMask);
// if we know more about the front face, use that information:
cv::imwrite("x_frontMaskIncreased1.png", frontMask + additionalFrontMask);
if(frontForeground.cols)
{
    // since the blending mask is blended for maxDist distance, we have to erode this mask here.
    cv::Mat tmp;
    cv::erode(frontForeground,tmp,cv::Mat(),cv::Point(),maxDist);
    // idea is to only use the additional front mask in those areas where the front image contains of face and not those background parts.
    additionalFrontMask = additionalFrontMask & tmp;
}
frontMask = frontMask + additionalFrontMask;
cv::imwrite("x_frontMaskIncreased2.png", frontMask);

//todo: add lines
cv::imshow("frontMask", frontMask);

// for visualization only:
cv::Mat frontMasked;
front.copyTo(frontMasked, frontMask);
cv::imshow("frontMasked", frontMasked);

cv::imwrite("x_frontMasked.png", frontMasked);

// compute inverse of mask to take it as input for distance transform:
cv::Mat inverseFrontMask = 255-frontMask;

// compute the distance to the mask, the further away from the mask, the less information from the front image should be used:
cv::Mat dist;
cv::distanceTransform(inverseFrontMask,dist,CV_DIST_L2, 3);

// scale wanted values between 0 and 1:
dist /= maxDist;
// remove all values > 1; those values are further away than maxDist pixel from the 6-point-mask
dist.setTo(cv::Scalar(1.0f), dist>1.0f);
// now invert the values so that they are == 1 inside the 6-point-area and go to 0 outside:
dist = 1.0f-dist;


cv::Mat alphaValues = dist;
//cv::Mat alphaNonZero = alphaValues > 0;
// now alphaValues contains your general blendingMask.
// but to use it on colored images, we need to duplicate the channels:
std::vector<cv::Mat> singleChannels;
singleChannels.push_back(alphaValues);
singleChannels.push_back(alphaValues);
singleChannels.push_back(alphaValues);
// merge all the channels:
cv::merge(singleChannels, alphaValues);

cv::imshow("alpha mask",alphaValues);
cv::imwrite("x_alpha_mask.png", alphaValues*255);

// convert all input mats to floating point mats:
front.convertTo(front,CV_32FC3);
left.convertTo(left,CV_32FC3);
right.convertTo(right,CV_32FC3);


cv::Mat result;
// first: blend left and right both with 0.5 to the result, this gives the correct results for the intersection of left and right equally weighted.
// TODO: these values could be blended from left to right, giving some finer results
cv::addWeighted(left,0.5,right,0.5,0, result);

// now copy all the elements that are included in only one of the masks (not blended, just 100% information)
left.copyTo(result,leftMask & (255-rightMask));
right.copyTo(result,rightMask & (255-leftMask));

cv::imshow("left+right", result/255.0f);
cv::imwrite("x_left_right.png", result);

// now blend the front image with it's alpha blending mask:
cv::Mat result2 = front.mul(alphaValues) + result.mul(cv::Scalar(1.0f,1.0f,1.0f)-alphaValues);

cv::imwrite("x_front_blend.png", front.mul(alphaValues));

cv::imshow("inv", cv::Scalar(1.0f,1.0f,1.0f)-alphaValues);
cv::imshow("f a", front.mul(alphaValues)/255.0f);
cv::imshow("f r", (result.mul(cv::Scalar(1.0f,1.0f,1.0f)-alphaValues))/255.0f);



result2.convertTo(result2, CV_8UC3);
return result2;


}

int main()
{
// front image
cv::Mat i1 = cv::imread("blending/new/front.jpg");
// left image
cv::Mat i2 = cv::imread("blending/new/left.jpg");
// right image
cv::Mat i3 = cv::imread("blending/new/right.jpg");

// optional: mask of front image
cv::Mat m1 = cv::imread("blending/new/mask_front.png",CV_LOAD_IMAGE_GRAYSCALE);

cv::imwrite("x_M1.png", m1);

// these are the marker points you detect in the front image.
// the order is important. the first three pushed points are the right points (left part of the face) in order from top to bottom
// the second three points are the ones from the left image half, in order from bottom to top
// check coordinates for those input images to understand the ordering!
std::vector<cv::Point> frontArea;
frontArea.push_back(cv::Point(169,92));
frontArea.push_back(cv::Point(198,112));
frontArea.push_back(cv::Point(169,162));
frontArea.push_back(cv::Point(147,162));
frontArea.push_back(cv::Point(122,112));
frontArea.push_back(cv::Point(147,91));

// first parameter is the front image, then left (right face half), then right (left half of face), then the image polygon and optional the front image mask (which contains all facial parts of the front image)
cv::Mat result = blendFrontAlpha(i1,i2,i3, frontArea, m1);


cv::imshow("RESULT", result);
cv::imwrite("x_Result.png", result);

cv::waitKey(-1);

return 0;

}

【讨论】:

  • @steph 我认为对于这些输入图像以及这种对齐方式,您无法期望通过简单的 alpha 混合技术获得更好的自动化结果。
  • 非常感谢 micka!我会尝试这个并且一定会尝试改进它:)
  • @Micka 我正在尝试做同样的事情,你能帮我告诉我如何进行多重混合吗?就像我们在 Photoshop 中所做的一样
  • 对不起,我不使用 Photoshop,所以不知道你想要实现什么。能举个例子吗?
【解决方案3】:

为了避免使面部在交叉点之外变得透明,您不能对整个图像使用单个 alpha

比如你需要在img[0]img[1]的交集处使用alpha=0.5,在img[1]=0所在的区域使用alpha=1,在img[0]=0所在的区域使用alpha=0

此示例是一种简单的方法,但它不会完全消除接缝。如果你想这样,你必须根据图像内容更智能地调整alpha。您可以查看有关该主题的大量研究文章,但这不是一项简单的任务:

  • “梯度域中的无缝图像拼接”,作者 Levin、Zomet Peleg 和 Weiss,ECCV 2004 (link)

  • “使用多视角平面扫描的无缝拼接”​​,作者 Kang、Szeliski 和 Uyttendaele,2004 年 (link)

【讨论】:

    【解决方案4】:
    1. 首先从您的输入图像创建一个遮罩图像,这可以通过对源图像进行阈值处理并在它们之间执行 bitwise_and 来完成。

    2. 现在使用上面的掩码将加权结果复制到新的垫子上。


    在下面的代码中,我没有使用 warpPerspective,而是在两个图像上使用了 ROI 来正确对齐。

    Mat left=imread("left.jpg");
    Mat front=imread("front.jpg");
    int x=30, y=10, w=240, h=200, offset_x=20, offset_y=6;
    Mat leftROI=left(Rect(x,y,w,h));
    Mat frontROI=front(Rect(x-offset_x,y+offset_y,w,h)); 
    
    //create mask
    Mat gray1,thr1;
    cvtColor(leftROI,gray1,CV_BGR2GRAY);
    threshold( gray1, thr1,190, 255,CV_THRESH_BINARY_INV );
    Mat gray2,thr2;
    cvtColor(frontROI,gray2,CV_BGR2GRAY);
    threshold( gray2, thr2,190, 255,CV_THRESH_BINARY_INV );
    Mat mask;
    bitwise_and(thr1,thr2,mask);
    
    //perform add weighted and copy using mask
    Mat add;
    double alpha=.5;
    double beta=.5;
    addWeighted(frontROI,alpha,leftROI,beta,0.0,add,-1);
    Mat dst(add.rows,add.cols,add.type(),Scalar::all(255));
    add.copyTo(dst,mask);
    imshow("dst",dst);
    

    【讨论】:

    • 嗨@Haris。我有一个疑问:您在 x,y,h,offset_x 和 offset_y 中输入的值仅适用于右上方的面吗?对于其他面,尺寸不会相同。如何在不每次都更改这些值的情况下为不同的人生成掩码?
    • @Steph 您可以在 ROI 之前创建蒙版,但对于最终混合,您需要正确对齐蒙版,这里我将绿点作为公共点手动完成。您可以通过在两个图像上找到绿点(轮廓质心)的中心点并将该值作为偏移量来手动完成,但是您的两个输入图像都应该包含一些如上所述的标记。
    • 嗨哈里斯!实际上我已经根据标记的质心对齐了图像。我只需要找到他们的面具。我该怎么做?
    • 找到他们的面具意味着,找到正确对齐的面部面具?
    • 在上面接受的答案中,函数 computeAlphaBlending() 将对齐的图像及其各自的掩码作为参数。对齐完成后,我需要找到他们的面具。只有他们的面具
    猜你喜欢
    • 1970-01-01
    • 2013-01-23
    • 1970-01-01
    • 2012-08-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-03-25
    • 1970-01-01
    相关资源
    最近更新 更多