9

I'm trying to find a simple algorithm to crop (remove the black areas) of a panorama image created with the openCV Stitcher module.

My idea is to calculate the most inner black points in the image which will define the cropping area, as shown in the next image:

enter image description here

Expected cropped result:

enter image description here

I've tried the next two approaches, but they don't crop the image as expected:

First Approach:

void testCropA(cv::Mat& image)
{
    cv::Mat gray;
    cvtColor(image, gray, CV_BGR2GRAY);

    Size size = gray.size();
    int type = gray.type();
    int left = 0, top = 0, right = size.width, bottom = size.height;

    cv::Mat row_zeros = Mat::zeros(1, right, type);
    cv::Mat col_zeros = Mat::zeros(bottom, 1, type);

    while (countNonZero(gray.row(top) != row_zeros) == 0) { top++; }

    while (countNonZero(gray.col(left) != col_zeros) == 0) { left++; }

    while (countNonZero(gray.row(bottom-1) != row_zeros) == 0) { bottom--; }

    while (countNonZero(gray.col(right-1) != col_zeros) == 0) { right--;  }

    cv::Rect cropRect(left, top, right - left, bottom - top);
    image = image(cropRect);
}

Second Approach:

void testCropB(cv::Mat& image)
{
    cv::Mat gray;
    cvtColor(image, gray, CV_BGR2GRAY);

    int minCol = gray.cols;
    int minRow = gray.rows;
    int maxCol = 0;
    int maxRow = 0;

    for (int i = 0; i < gray.rows - 3; i++)
    {
        for (int j = 0; j < gray.cols; j++)
        {
            if (gray.at<char>(i, j) != 0)
            {
                if (i < minRow) {minRow = i;}
                if (j < minCol) {minCol = j;}
                if (i > maxRow) {maxRow = i;}
                if (j > maxCol) {maxCol = j;}
            }
        }
    }

    cv::Rect cropRect = Rect(minCol, minRow, maxCol - minCol, maxRow - minRow);
    image = image(cropRect);
}
PerracoLabs
  • 16,449
  • 15
  • 74
  • 127
  • You should give more description of the problem. It looks like it's cropping far too much, since you're going through each row until there is no black (yes?). You'll need to think of a new way to define the optimal area; there's no single correct answer (and as such, it's not a good fit for SO). – Dave May 24 '14 at 17:08
  • the problem is that is actually not cropping the image. I think my approaches are not finding the inner points correctly. – PerracoLabs May 24 '14 at 17:11
  • 3
    Have a look at my answer in http://stackoverflow.com/questions/21410449/how-do-i-crop-to-largest-interior-bounding-box-in-opencv/21479072#21479072 – Micka May 24 '14 at 17:12
  • I didn't know OpenCV had a Stitcher module, I'm glad I found this question :) – nietaki May 24 '14 at 17:13
  • Hi nietaki, yes it has, and the output images are really quite good. Only part left for me is the cropping which I'm trying to solve now. – PerracoLabs May 24 '14 at 17:15
  • 1
    Take a look at find contour! Specify that it only should search for the outermost and you will have a solution really fast. – Sebastian Schmitz May 26 '14 at 07:59
  • How would help to find the outermost contour? I'm trying to get the innermost points from the surrounding black areas. – PerracoLabs May 26 '14 at 17:39
  • Have you found a solution yet? – Andrej Adamenko Feb 20 '15 at 13:52

2 Answers2

2

This is my current solution. Hope it helps to others:

bool checkInteriorExterior(const cv::Mat &mask, const cv::Rect &croppingMask,
                                 int &top, int &bottom, int &left, int &right)
{
    // Return true if the rectangle is fine as it is
    bool result = true;

    cv::Mat sub = mask(croppingMask);
    int x = 0;
    int y = 0;

    // Count how many exterior pixels are, and choose that side for
    // reduction where mose exterior pixels occurred (that's the heuristic)

    int top_row = 0;
    int bottom_row = 0;
    int left_column = 0;
    int right_column = 0;

    for (y = 0, x = 0; x < sub.cols; ++x)
    {
        // If there is an exterior part in the interior we have
        // to move the top side of the rect a bit to the bottom
        if (sub.at<char>(y, x) == 0)
        {
            result = false;
            ++top_row;
        }
    }

    for (y = (sub.rows - 1), x = 0; x < sub.cols; ++x)
    {
        // If there is an exterior part in the interior we have
        // to move the bottom side of the rect a bit to the top
        if (sub.at<char>(y, x) == 0)
        {
            result = false;
            ++bottom_row;
        }
    }

    for (y = 0, x = 0; y < sub.rows; ++y)
    {
        // If there is an exterior part in the interior
        if (sub.at<char>(y, x) == 0)
        {
            result = false;
            ++left_column;
        }
    }

    for (x = (sub.cols - 1), y = 0; y < sub.rows; ++y)
    {
        // If there is an exterior part in the interior
        if (sub.at<char>(y, x) == 0)
        {
            result = false;
            ++right_column;
        }
    }

    // The idea is to set `top = 1` if it's better to reduce
    // the rect at the top than anywhere else.
    if (top_row > bottom_row)
    {
        if (top_row > left_column)
        {
            if (top_row > right_column)
            {
                top = 1;
            }
        }
    }
    else if (bottom_row > left_column)
    {
        if (bottom_row > right_column)
        {
            bottom = 1;
        }
    }

    if (left_column >= right_column)
    {
        if (left_column >= bottom_row)
        {
            if (left_column >= top_row)
            {
                left = 1;
            }
        }
    }
    else if (right_column >= top_row)
    {
        if (right_column >= bottom_row)
        {
            right = 1;
        }
    }

    return result;
}

bool compareX(cv::Point a, cv::Point b)
{
    return a.x < b.x;
}

bool compareY(cv::Point a, cv::Point b)
{
    return a.y < b.y;
}

void crop(cv::Mat &source)
{
    cv::Mat gray;
    source.convertTo(source, CV_8U);
    cvtColor(source, gray, cv::COLOR_RGB2GRAY);

    // Extract all the black background (and some interior parts maybe)

    cv::Mat mask = gray > 0;

    // now extract the outer contour
    std::vector<std::vector<cv::Point> > contours;
    std::vector<cv::Vec4i> hierarchy;

    cv::findContours(mask, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE, cv::Point(0, 0));
    cv::Mat contourImage = cv::Mat::zeros(source.size(), CV_8UC3);;

    // Find contour with max elements

    int maxSize = 0;
    int id = 0;

    for (int i = 0; i < contours.size(); ++i)
    {
        if (contours.at((unsigned long)i).size() > maxSize)
        {
            maxSize = (int)contours.at((unsigned long)i).size();
            id = i;
        }
    }

    // Draw filled contour to obtain a mask with interior parts

    cv::Mat contourMask = cv::Mat::zeros(source.size(), CV_8UC1);
    drawContours(contourMask, contours, id, cv::Scalar(255), -1, 8, hierarchy, 0, cv::Point());

    // Sort contour in x/y directions to easily find min/max and next

    std::vector<cv::Point> cSortedX = contours.at((unsigned long)id);
    std::sort(cSortedX.begin(), cSortedX.end(), compareX);
    std::vector<cv::Point> cSortedY = contours.at((unsigned long)id);
    std::sort(cSortedY.begin(), cSortedY.end(), compareY);

    int minXId = 0;
    int maxXId = (int)(cSortedX.size() - 1);
    int minYId = 0;
    int maxYId = (int)(cSortedY.size() - 1);

    cv::Rect croppingMask;

    while ((minXId < maxXId) && (minYId < maxYId))
    {
        cv::Point min(cSortedX[minXId].x, cSortedY[minYId].y);
        cv::Point max(cSortedX[maxXId].x, cSortedY[maxYId].y);
        croppingMask = cv::Rect(min.x, min.y, max.x - min.x, max.y - min.y);

        // Out-codes: if one of them is set, the rectangle size has to be reduced at that border

        int ocTop = 0;
        int ocBottom = 0;
        int ocLeft = 0;
        int ocRight = 0;

        bool finished = checkInteriorExterior(contourMask, croppingMask, ocTop, ocBottom, ocLeft, ocRight);

        if (finished == true)
        {
            break;
        }

        // Reduce rectangle at border if necessary

        if (ocLeft)
        { ++minXId; }
        if (ocRight)
        { --maxXId; }
        if (ocTop)
        { ++minYId; }
        if (ocBottom)
        { --maxYId; }
    }

    // Crop image with created mask

    source = source(croppingMask);
}
PerracoLabs
  • 16,449
  • 15
  • 74
  • 127
-1

I never used the stitcher calss, but I think that you may get the estimated homography matrix at each pair of images, if you could obtain it easily, then you can multiply it with the corners of the first original image and so for the corner of the last original one, you will get their stitched coordinate, then get the min of left and right x-coordinates and min of up and bottom y-coordinates of each images. You may get the coordinates of of each stitched image, what you need to do in some cases of cropping.

Y.AL
  • 1,808
  • 13
  • 27