27

I'm trying to simplify the following image using OpenCV:

enter image description here

What we have here are lots of red shapes. Some of them completely contain others. Some of them intersect their neighbors. My goal is to unify all intersecting shapes by replacing any two intersecting shapes with the bounding box of their union's polygon. (repeating until there are no more intersecting shapes).

By intersecting I mean also touching. Hope this makes it 100% clear:

enter image description here

I'm trying to do this efficiently using standard morphology operations; obviously it can be done naively in O(N^2), but that'll be too slow. Dilation doesn't help because some shapes are only 1px apart and I don't want them merged if they're not intersecting.

coffeewin
  • 182
  • 3
  • 6
  • 26
Assaf Lavie
  • 73,079
  • 34
  • 148
  • 203
  • What about using the function GroupRectangles (though it works in O(N^2)) ? – GilLevi Sep 30 '13 at 08:48
  • It wouldn't work for the last case - see http://stackoverflow.com/questions/21421070/opencv-grouprectangles-getting-grouped-and-ungrouped-rectangles – Ben Dowling Jan 29 '14 at 02:45

3 Answers3

16

UPDATE: I misunderstood the question earlier. We don't want to remove rectangles which are completely inside others. We only want to replace the intersecting rectangles. Therefore for the first case, we have to do nothing.

New api (2.4.9) supports & and | operators.

From opencv doc:

  • rect = rect1 & rect2 (rectangle intersection)
  • rect = rect1 | rect2 (minimum area rectangle containing rect2 and rect3 )

It also supports equality comparision (==)

  • rect == rect1

So it is now very easy to accomplish the task. For every pair of rectangle rect1 and rect2,

if((rect1 & rect2) == rect1) ... // rect1 is completely inside rect2; do nothing.
else if((rect1 & rect2).area() > 0) // they intersect; merge them.
    newrect = rect1 | rect2;
    ... // remove rect1 and rect2 from list and insert newrect.

UPDATE 2: (for translating in java)

I know java very little. I also never used the java API. I am giving here some psudo-code (which I think can be easily translated)

For & operator, we need a method which finds the intersect of two rectangles.

Method: Intersect (Rect A, Rect B)
left = max(A.x, B.x)
top  = max(A.y, B.y)
right = min(A.x + A.width, B.x + B.width)
bottom = min(A.y + A.height, B.y + B.height)
if(left <= right && top <= bottom) return Rect(left, top, right - left, bottom - top)
else return Rect()

For | operator, we need a similar method

Method: Merge (Rect A, Rect B)
left = min(A.x, B.x)
top  = min(A.y, B.y)
right = max(A.x + A.width, B.x + B.width)
bottom = max(A.y + A.height, B.y + B.height)
return Rect(left, top, right - left, bottom - top)

For == operator, we can use overloaded equals method.

asif
  • 975
  • 8
  • 16
  • This is nice but I do not think he has the rect's coordinates, he would have to detect them first. – Rui Marques Jun 05 '14 at 15:09
  • @RuiMarques He actually has the Rect shapes. He mentioned it in a [comment](http://stackoverflow.com/questions/19079619/efficient-way-to-combine-intersecting-bounding-rectangles#comment28205660_19079805). – asif Jun 07 '14 at 07:11
  • I would love to know how to translate the same code in Java! Please help. Skimming through the API but nothing found. Worst case would be JNI-calls. – Tim Long Feb 01 '15 at 16:32
  • Thank @asif for updating the answer. What i'm now trying to do is exploit the OpenCV Java API. The operators theirself are not hard to implement in Java. Let see, how the JNI performance turns out. There are still a lot other functionalities to be ported from C++ library to Java. – Tim Long Feb 03 '15 at 11:55
11

To accomplish what you want we'll be using findContours. The key point here is to understand how it works when mode is set to CV_RETR_TREE. In this case, hierarchy is constructed in a way that every even depth level contains external contours, while odd depth levels contain internal contours. What we need here is to traverse the hierarchy tree printing the contours associated with even depth levels.

First we find the contours of an image called original

typedef std::vector<std::vector<cv::Point> > Contours;
typedef std::vector<cv::Vec4i> Hierarchy;

Contours contours;
Hierarchy hierarchy;
cv::findContours(original, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_NONE);

To print the external contours on an image called processed we need a recursive function.

void printExternalContours(cv::Mat img, Contours const& contours, Hierarchy const& hierarchy, int const idx)
{
    //for every contour of the same hierarchy level
    for(int i = idx; i >= 0; i = hierarchy[i][0])
    {
        //print it
        cv::drawContours(img, contours, i, cv::Scalar(255));

        //for every of its internal contours
        for(int j = hierarchy[i][2]; j >= 0; j = hierarchy[j][0])
        {
            //recursively print the external contours of its children
            printExternalContours(img, contours, hierarchy, hierarchy[j][2]);
        }
    }
}

printExternalContours(processed, contours, hierarchy, 0);

The result is shown bellow, where original and processed are displayed side by side.

original processed

If you absolutely need rectangular shapes, you just need to use boundingRect to get the minimum enclosing rectangle given a set of points (every single contour in this case) and use rectangle for the drawing. In other words, substitute

cv::drawContours(img, contours, i, cv::Scalar(255));

by

cv::rectangle(img, cv::boundingRect(contours[i]), cv::Scalar(255));

findContours expects a single 8-bit image, sou you could either make a gray image from your originals and then threshold it to get a perfect black background or, perhaps it would suffice to use the red channel in your case, just make sure the background is perfectly black.

Regarding the complexity of findContours, I can't attest it is any better than O(N^2), nor haven't I found any input on that after a quick google search, but I trust OpenCV implements the best known algorithm.

brunocodutra
  • 2,329
  • 17
  • 23
  • Thank you. I've tried it but it seems to eliminate contained (yet not intersected) shapes. e.g.: http://i.imgur.com/x5izyFc.png – Assaf Lavie Sep 29 '13 at 16:38
  • @AssafLavie it wasn't clear to me that only intersecting shapes should be unified, but now I see the bold text. Also, do you absolutely need rectangular shapes? Because the `findContours` approach will yield arbitrary poligonal shapes. – brunocodutra Sep 29 '13 at 16:45
  • The input is actually the result of a previous call to findContours; I then take the bounding box of each contour. – Assaf Lavie Sep 29 '13 at 16:49
  • 1
    @AssafLavie CV_RETR_LIST doesn't work either, let me work on that for a second. – brunocodutra Sep 29 '13 at 17:07
  • I wonder if a flood fill algorithm would help here. Use it to "walk" the connected shapes, while keeping track of the bounding box of each shape... (although it's not a filter-based solution) – Assaf Lavie Sep 29 '13 at 17:27
  • @AssafLavie I thought about it, but I can foresee several corner cases, I'm working on a solution based on what I initially proposed, I think it's going to work out, hold on for a moment. – brunocodutra Sep 29 '13 at 17:35
  • @AssafLavie I got it to work using `findContours` only, I'll update my answer. – brunocodutra Sep 29 '13 at 18:13
  • Awesome. I will port it to Python and try it out. Have you by any chance tested the touching && !intersecting case? – Assaf Lavie Sep 29 '13 at 18:46
  • @AssafLavie yes I'll upload new images – brunocodutra Sep 29 '13 at 18:54
  • @AssafLavie were you able to port this to Python? I am after the same thing. – Omnipresent Mar 18 '16 at 19:56
  • @brunocodutra Do you have the code that draws the original image in your answer? It'll be really helpful for that code to be here so someone can play around with `printExternalContours` – Omnipresent Mar 18 '16 at 19:59
  • @Omnipresent I'm sorry, but I don't have it anymore, that was almost 3 years ago today – brunocodutra Mar 18 '16 at 20:23
  • @brunocodutra I figured, but was worth asking. I'm having the same issue and came here from google. Thanks for your answer anyways. – Omnipresent Mar 18 '16 at 20:30
5

Given two bounding box contours in the form of (x,y,w,h), here's a function to create a single bounding box (assuming the boxes are touching or within each other). Returns (x,y,w,h) of the combined bounding box i.e, top-left x, top-left y, width, and height. Here's an illustration

(x1,y1)       w1                           (x3,y3)         w3
  ._____________________.                    .____________________________.
  |                     |                    |                            | 
  |                     |  h1                |                            |
  |   (x2,y2)           |                    |                            |
  |     ._______________|_______.      -->   |                            |
  |     |               |       |            |                            |  h3
  ._____|_______________.       |            |                            |
        |                       |  h2        |                            |
        |                       |            |                            |
        |           w2          |            |                            |
        ._______________________.            .____________________________.

Code

def combineBoundingBox(box1, box2):
    x = min(box1[0], box2[0])
    y = min(box1[1], box2[1])
    w = box2[0] + box2[2] - box1[0]
    h = max(box1[1] + box1[3], box2[1] + box2[3]) - y
    return (x, y, w, h)

Example

With these two bounding boxes,

>>> print(box1)
>>> print(box2)
(132, 85, 190, 231)
(264, 80, 121, 230)

>>> new = combineBoundingBox(box1, box2) 
>>> print(new)
(132, 80, 253, 236)

Here's the visual result: Before -> After

enter image description here enter image description here

nathancy
  • 42,661
  • 14
  • 115
  • 137