39

Note: I am placing this question in both the MATLAB and Python tags as I am the most proficient in these languages. However, I welcome solutions in any language.


Question Preamble

I have taken an image with a fisheye lens. This image consists of a pattern with a bunch of square objects. What I want to do with this image is detect the centroid of each of these squares, then use these points to perform an undistortion of the image - specifically, I am seeking the right distortion model parameters. It should be noted that not all of the squares need to be detected. As long as a good majority of them are, then that's totally fine.... but that isn't the point of this post. The parameter estimation algorithm I have already written, but the problem is that it requires points that appear collinear in the image.

The base question I want to ask is given these points, what is the best way to group them together so that each group consists of a horizontal line or vertical line?

Background to my problem

This isn't really important with regards to the question I'm asking, but if you'd like to know where I got my data from and to further understand the question I'm asking, please read. If you're not interested, then you can skip right to the Problem setup section below.


An example of an image I am dealing with is shown below:

enter image description here

It is a 960 x 960 image. The image was originally higher resolution, but I subsample the image to facilitate faster processing time. As you can see, there are a bunch of square patterns that are dispersed in the image. Also, the centroids I have calculated are with respect to the above subsampled image.

The pipeline I have set up to retrieve the centroids is the following:

  1. Perform a Canny Edge Detection
  2. Focus on a region of interest that minimizes false positives. This region of interest is basically the squares without any of the black tape that covers one of their sides.
  3. Find all distinct closed contours
  4. For each distinct closed contour...

    a. Perform a Harris Corner Detection

    b. Determine if the result has 4 corner points

    c. If this does, then this contour belonged to a square and find the centroid of this shape

    d. If it doesn't, then skip this shape

  5. Place all detected centroids from Step #4 into a matrix for further examination.

Here's an example result with the above image. Each detected square has the four points colour coded according to the location of where it is with respect to the square itself. For each centroid that I have detected, I write an ID right where that centroid is in the image itself.

enter image description here

With the above image, there are 37 detected squares.

Problem Setup

Suppose I have some image pixel points stored in a N x 3 matrix. The first two columns are the x (horizontal) and y (vertical) coordinates where in image coordinate space, the y coordinate is inverted, which means that positive y moves downwards. The third column is an ID associated with the point.

Here is some code written in MATLAB that takes these points, plots them onto a 2D grid and labels each point with the third column of the matrix. If you read the above background, these are the points that were detected by my algorithm outlined above.

data = [ 475.  ,  605.75,    1.;
       571.  ,  586.5 ,    2.;
       233.  ,  558.5 ,    3.;
       669.5 ,  562.75,    4.;
       291.25,  546.25,    5.;
       759.  ,  536.25,    6.;
       362.5 ,  531.5 ,    7.;
       448.  ,  513.5 ,    8.;
       834.5 ,  510.  ,    9.;
       897.25,  486.  ,   10.;
       545.5 ,  491.25,   11.;
       214.5 ,  481.25,   12.;
       271.25,  463.  ,   13.;
       646.5 ,  466.75,   14.;
       739.  ,  442.75,   15.;
       340.5 ,  441.5 ,   16.;
       817.75,  421.5 ,   17.;
       423.75,  417.75,   18.;
       202.5 ,  406.  ,   19.;
       519.25,  392.25,   20.;
       257.5 ,  382.  ,   21.;
       619.25,  368.5 ,   22.;
       148.  ,  359.75,   23.;
       324.5 ,  356.  ,   24.;
       713.  ,  347.75,   25.;
       195.  ,  335.  ,   26.;
       793.5 ,  332.5 ,   27.;
       403.75,  328.  ,   28.;
       249.25,  308.  ,   29.;
       495.5 ,  300.75,   30.;
       314.  ,  279.  ,   31.;
       764.25,  249.5 ,   32.;
       389.5 ,  249.5 ,   33.;
       475.  ,  221.5 ,   34.;
       565.75,  199.  ,   35.;
       802.75,  173.75,   36.;
       733.  ,  176.25,   37.];

figure; hold on;
axis ij;
scatter(data(:,1), data(:,2),40, 'r.');
text(data(:,1)+10, data(:,2)+10, num2str(data(:,3)));

Similarly in Python, using numpy and matplotlib, we have:

import numpy as np
import matplotlib.pyplot as plt

data = np.array([[ 475.  ,  605.75,    1.  ],
   [ 571.  ,  586.5 ,    2.  ],
   [ 233.  ,  558.5 ,    3.  ],
   [ 669.5 ,  562.75,    4.  ],
   [ 291.25,  546.25,    5.  ],
   [ 759.  ,  536.25,    6.  ],
   [ 362.5 ,  531.5 ,    7.  ],
   [ 448.  ,  513.5 ,    8.  ],
   [ 834.5 ,  510.  ,    9.  ],
   [ 897.25,  486.  ,   10.  ],
   [ 545.5 ,  491.25,   11.  ],
   [ 214.5 ,  481.25,   12.  ],
   [ 271.25,  463.  ,   13.  ],
   [ 646.5 ,  466.75,   14.  ],
   [ 739.  ,  442.75,   15.  ],
   [ 340.5 ,  441.5 ,   16.  ],
   [ 817.75,  421.5 ,   17.  ],
   [ 423.75,  417.75,   18.  ],
   [ 202.5 ,  406.  ,   19.  ],
   [ 519.25,  392.25,   20.  ],
   [ 257.5 ,  382.  ,   21.  ],
   [ 619.25,  368.5 ,   22.  ],
   [ 148.  ,  359.75,   23.  ],
   [ 324.5 ,  356.  ,   24.  ],
   [ 713.  ,  347.75,   25.  ],
   [ 195.  ,  335.  ,   26.  ],
   [ 793.5 ,  332.5 ,   27.  ],
   [ 403.75,  328.  ,   28.  ],
   [ 249.25,  308.  ,   29.  ],
   [ 495.5 ,  300.75,   30.  ],
   [ 314.  ,  279.  ,   31.  ],
   [ 764.25,  249.5 ,   32.  ],
   [ 389.5 ,  249.5 ,   33.  ],
   [ 475.  ,  221.5 ,   34.  ],
   [ 565.75,  199.  ,   35.  ],
   [ 802.75,  173.75,   36.  ],
   [ 733.  ,  176.25,   37.  ]])

plt.figure()
plt.gca().invert_yaxis()

plt.plot(data[:,0], data[:,1], 'r.', markersize=14)

for idx in np.arange(data.shape[0]):
    plt.text(data[idx,0]+10, data[idx,1]+10, str(int(data[idx,2])), size='large')

plt.show()

We get:

enter image description here


Back to the question

As you can see, these points are more or less in a grid pattern and you can see that we can form lines between the points. Specifically, you can see that there are lines that can be formed horizontally and vertically.

For example, if you reference the image in the background section of my problem, we can see that there are 5 groups of points that can be grouped in a horizontal manner. For example, points 23, 26, 29, 31, 33, 34, 35, 37 and 36 form one group. Points 19, 21, 24, 28, 30 and 32 form another group and so on and so forth. Similarly in a vertical sense, we can see that points 26, 19, 12 and 3 form one group, points 29, 21, 13 and 5 form another group and so on.


Question to ask

My question is this: What is a method that can successfully group points in horizontal groupings and vertical groupings separately, given that the points could be in any orientation?

Conditions

  1. There must be at least three points per line. If there is anything less than that, then this does not qualify as a segment. Therefore, the points 36 and 10 don't qualify as a vertical line, and similarly the isolated point 23 shouldn't quality as a vertical line, but it is part of the first horizontal grouping.

  2. The above calibration pattern can be in any orientation. However, for what I'm dealing with, the worst kind of orientation you can get is what you see above in the background section.


Expected Output

The output would be a pair of lists where the first list has elements where each element gives you a sequence of point IDs that form a horizontal line. Similarly, the second list has elements where each element gives you a sequence of point IDs that form a vertical line.

Therefore, the expected output for the horizontal sequences would look something like this:

MATLAB

horiz_list = {[23, 26, 29, 31, 33, 34, 35, 37, 36], [19, 21, 24, 28, 30, 32], ...};
vert_list = {[26, 19, 12, 3], [29, 21, 13, 5], ....};

Python

horiz_list = [[23, 26, 29, 31, 33, 34, 35, 37, 36], [19, 21, 24, 28, 30, 32], ....]
vert_list = [[26, 19, 12, 3], [29, 21, 13, 5], ...]

What I have tried

Algorithmically, what I have tried is to undo the rotation that is experienced at these points. I've performed Principal Components Analysis and I tried projecting the points with respect to the computed orthogonal basis vectors so that the points would more or less be on a straight rectangular grid.

Once I have that, it's just a simple matter of doing some scanline processing where you could group points based on a differential change on either the horizontal or vertical coordinates. You'd sort the coordinates by either the x or y values, then examine these sorted coordinates and look for a large change. Once you encounter this change, then you can group points in between the changes together to form your lines. Doing this with respect to each dimension would give you either the horizontal or vertical groupings.

With regards to PCA, here's what I did in MATLAB and Python:

MATLAB

%# Step #1 - Get just the data - no IDs
data_raw = data(:,1:2);

%# Decentralize mean
data_nomean = bsxfun(@minus, data_raw, mean(data_raw,1));

%# Step #2 - Determine covariance matrix
%# This already decentralizes the mean
cov_data = cov(data_raw);

%# Step #3 - Determine right singular vectors
[~,~,V] = svd(cov_data);

%# Step #4 - Transform data with respect to basis
F = V.'*data_nomean.';

%# Visualize both the original data points and transformed data
figure;
plot(F(1,:), F(2,:), 'b.', 'MarkerSize', 14);
axis ij;
hold on;
plot(data(:,1), data(:,2), 'r.', 'MarkerSize', 14);

Python

import numpy as np
import numpy.linalg as la

# Step #1 and Step #2 - Decentralize mean
centroids_raw = data[:,:2]
mean_data = np.mean(centroids_raw, axis=0)

# Transpose for covariance calculation
data_nomean = (centroids_raw - mean_data).T

# Step #3 - Determine covariance matrix
# Doesn't matter if you do this on the decentralized result
# or the normal result - cov subtracts the mean off anyway
cov_data = np.cov(data_nomean)

# Step #4 - Determine right singular vectors via SVD
# Note - This is already V^T, so there's no need to transpose
_,_,V = la.svd(cov_data)

# Step #5 - Transform data with respect to basis
data_transform = np.dot(V, data_nomean).T

plt.figure()
plt.gca().invert_yaxis()

plt.plot(data[:,0], data[:,1], 'b.', markersize=14)
plt.plot(data_transform[:,0], data_transform[:,1], 'r.', markersize=14)

plt.show()

The above code not only reprojects the data, but it also plots both the original points and the projected points together in a single figure. However, when I tried reprojecting my data, this is the plot I get:

enter image description here

The points in red are the original image coordinates while the points in blue are reprojected onto the basis vectors to try and remove the rotation. It still doesn't quite do the job. There is still some orientation with respect to the points so if I tried to do my scanline algorithm, points from the lines below for horizontal tracing or to the side for vertical tracing would be inadvertently grouped and this isn't correct.


Perhaps I'm overthinking the problem, but any insights you have regarding this would be greatly appreciated. If the answer is indeed superb, I would be inclined to award a high bounty as I've been stuck on this problem for quite some time.

I hope this question wasn't long winded. If you don't have an idea of how to solve this, then I thank you for your time in reading my question regardless.

Looking forward to any insights that you may have. Thanks very much!

rayryeng
  • 102,964
  • 22
  • 184
  • 193
  • 1
    I'm not sure, but perhaps you could achieve some kind of reduction by computing the convex hull and working on its borders first... – Leandro Caniglia Apr 09 '15 at 23:52
  • @LeandroCaniglia - That's a good idea. I'll muck around with that and I'll see how it goes. Thanks for the tip! – rayryeng Apr 09 '15 at 23:53
  • @knedlsepp thanks for reading! yes the squares are also rotated. I didn't mention this because I didn't think it was important, but some are rotated for getting other parameters with the lens. – rayryeng Apr 10 '15 at 00:15
  • What I also don't understand is why you need this grouping. Wouldn't the parameter search simply consist of: Search the parameter space of your model to minimize the error of the best affine mapping of the transformed (using said parameters) grid to your computed centroids? – knedlsepp Apr 10 '15 at 00:17
  • @knedlsepp I am actually not trying to find an affine mapping. What I actually need is a particular parameter from the parameter estimation algorithm I've already written, and to get that, I need to determine points that are collinear in the image. This parameter I'm retrieving is important for the product I'm developing where I'm employed. Still, thank you very much for your questions :) – rayryeng Apr 10 '15 at 00:20
  • 2
    I was suspecting it had something to do with bublcam. ;-) I am aware that an affine mapping won't suffice here (and I just realized *projective* is what I should have said), but what I meant is to use an arbitrary optimization algorithm in which step you distort the rectangular grid using the parameters you are trying to find and minimize the error of the best projective fit of the parameter-distorted points and the centroids using some RANSAC method. (I guess you could also use the inverse mapping on the recorded points and projective-fit them to the rectangular grid.) – knedlsepp Apr 10 '15 at 00:28
  • @knedlsepp Ahhh... Now that's an interesting viewpoint. I wouldn't know what objective function I would use to undistort or undo the rotation. Usually with projective mappings and from what I have seen, you have a set of source and target points. I wouldn't know what the target points would be. Could you provide some insight on this? – rayryeng Apr 10 '15 at 00:31
  • Hey @rayryeng very interesting problem! I can't help you however haha but I couldn't resist telling you that the `for` loop used to plot the points and display the text (MATLAB part) can be replaced by a call to `scatter` and another to `text` which handles many indices at once. Eg: `scatter(data(:,1), data(:,2),40, 'r.'); text(data(:,1)+10, data(:,2)+10, num2str(data(:,3)));` works great. It may not matter much but that could be useful...Sorry to bother and good luck! – Benoit_11 Apr 10 '15 at 00:55
  • 1
    @Benoit_11 - Hmm! Thanks! I always forget about `scatter` :) I'll keep that in mind for the future. Thanks for reading btw! – rayryeng Apr 10 '15 at 01:02
  • In terms of target and source points I was thinking of randomly sampling those. Similar to what they use e.g. [here](http://de.mathworks.com/help/vision/ref/estimategeometrictransformation.html#zmw57dd0e24673). I don't know if this could be made reasonably fast considering that doing RANSACs inside an optimization step will cost lots of cycles. – knedlsepp Apr 10 '15 at 01:02
  • Other ideas more tailored to your problem of finding lines: You could assume those are collinear up to 'lens error' and use something like [this](http://stackoverflow.com/a/29233720/3139711) (Code is for fitting planes to point sets, but should be general enough to work for fitting lines as well). Or you could choose every pair of points, generate an image by drawing semi-transparent lines through those points so the lines add up and finally use the standard image processing-approach of the Hough transform. – knedlsepp Apr 10 '15 at 01:03
  • @knedlsepp - This algorithm needs to be reasonably fast... putting RANSAC in there may slow things down.... depending on what it is that needs to be estimated. I'll have to think about that some more. I'll have a look at your other post (looks interesting, just skimmed through it) and the Hough transform idea is promising. I'll have to try that out too. Thanks for the commentary! – rayryeng Apr 10 '15 at 01:17
  • What do you consider as "horizontal" (or "verticle"). If there is a line which passes through all the points, making a 60 degree angle to the origin. Should that line belong to vertical or horizontal group? Another question, why aren't your centroids in the center of each square (rather the 4 detected harris corners)? That is how it is supposed to be right? – Autonomous Apr 10 '15 at 01:51
  • @ParagS.Chandakkar - To minimize processing, I just made the centroid based on the 4 corner points, but they can certainly be with respect to the entire square. With regards to your question about the 60 degrees, that circumstance should never happen. If you look at the above image, in actuality, that pattern is a rectangular grid of squares - some are slightly rotated but there is a clear order. I am trying to ascertain this same order under fisheye lens distortion and when the image is taken at possibly different orientations. However, I may not be understanding you. Can you clarify? – rayryeng Apr 10 '15 at 02:07
  • I was just looking at the plot and forgot about the fisheye lens! Never mind. That helped me to update my thoughts as well! If all of your images are similar (can be rotated, but structure should be similar, rigid is another word), then you may *start* with the following. To find all the points on a vertical line, project first (can be any) centroid on a horizontal line. When you project, you are going to get a scalar. Project next centroid, if it lies near to the earlier project centroid (you define what is "near"), then you let it be, otherwise skip. You can maintain a table so that (contd.) – Autonomous Apr 10 '15 at 02:25
  • if centroid 12 is collinear with 23, then don't process 23 again (i.e. don't start the process from 23). Centroid 23 can lie on other line, e.g. started by 26. This sounds me like exhaustive search, but probably the nature of the problem demands it. You can do the above process even in 2-D, but I think running a least-square fitting many times and then getting `gof` would be slower. About the range of horizontal line, you can first detect the angle of black line in your image, and the horizontal line would lie in +/- 10 degrees search zone (say). – Autonomous Apr 10 '15 at 02:25
  • @ParagS.Chandakkar Oh :) hehe. Yes that makes sense now with the 60 degree comment. Yes, the pattern is going to be the same. The image may be taken at slightly different orientations but the pattern is guaranteed to be the same. I'm unclear on what you mean by projecting the centroid on a horizontal line. How can you project this onto a line that is unknown? Also, the black line may prove to be useful. I've disregarded it because I didn't think I would need it. However, the fisheye distortion messes with the detection of its orientation. – rayryeng Apr 10 '15 at 02:37
  • Let's say your black line is angled at an angle `ang`. Is it not possible project a point on to a line which is passing through origin (or any point for that matter) and angled at `ang +/- 10`? So there will be 5 lines if you take a precision of 5 degrees. – Autonomous Apr 10 '15 at 02:48
  • @ParagS.Chandakkar - ahhhh yes. I understand what you're saying now. Interesting. That's also an idea that I'll have to try. Thanks! However, that looks like that would only work for the vertical lines. How would you do that for the horizontal lines? I don't have a guiding black line on the sides of the pattern like at the top and the bottom of the pattern to facilitate the projection. – rayryeng Apr 10 '15 at 03:01
  • I would suggest to use an orthogonal vector to the black lines to get an angle that you can compare to your horizontal lines. This is basically what you get when you would rotate the whole image, like i suggested in my answer before reading this :) – H W Apr 10 '15 at 08:36
  • 2
    Out of curiosity, looking at the very top image, is there any reason why "Brendon sucks"? :-) – kkuilla Apr 10 '15 at 11:38
  • @rayryeng: BTW: In projective space the centroids are actually not the `mean`s of the corner-coordinates: [illustration](http://ptgmedia.pearsoncmg.com/images/chap15_9780321399526/elementLinks/15fig13.jpg) – knedlsepp Apr 10 '15 at 12:36
  • @knedlsepp I'm aware. The algorithm I designed is through least squares so even if the exact centroids are off by a bit, it's fine. BTW thank you for your insight so far. – rayryeng Apr 10 '15 at 13:41
  • @kkuilla - lol. A joke that one of my coworkers left for one of my other coworkers. What we are doing is trying to calibrate each fisheye lens that's part of our product. For now, we're doing it in-house and every time that happens, you'll see that message. – rayryeng Apr 10 '15 at 13:41

3 Answers3

15

Note 1: It has a number of settings -> which for other images may need to altered to get the result you want see % Settings - play around with these values

Note 2: It doesn't find all of the lines you want -> but its a starting point....

To call this function, invoke this in the command prompt:

>> [h, v] = testLines;

We get:

>> celldisp(h)

h{1} =
     1     2     4     6     9    10
h{2} =
     3     5     7     8    11    14    15    17
h{3} =
     1     2     4     6     9    10
h{4} =
     3     5     7     8    11    14    15    17
h{5} =
     1     2     4     6     9    10
h{6} =
     3     5     7     8    11    14    15    17
h{7} =
     3     5     7     8    11    14    15    17
h{8} =
     1     2     4     6     9    10
h{9} =
     1     2     4     6     9    10
h{10} =
    12    13    16    18    20    22    25    27
h{11} =
    13    16    18    20    22    25    27
h{12} =
     3     5     7     8    11    14    15    17
h{13} =
     3     5     7     8    11    14    15
h{14} =
    12    13    16    18    20    22    25    27
h{15} =
     3     5     7     8    11    14    15    17
h{16} =
    12    13    16    18    20    22    25    27
h{17} =
    19    21    24    28    30
h{18} =
    21    24    28    30
h{19} =
    12    13    16    18    20    22    25    27
h{20} =
    19    21    24    28    30
h{21} =
    12    13    16    18    20    22    24    25
h{22} =
    12    13    16    18    20    22    24    25    27
h{23} =
    23    26    29    31    33    34    35
h{24} =
    23    26    29    31    33    34    35    37
h{25} =
    23    26    29    31    33    34    35    36    37
h{26} =
    33    34    35    37    36
h{27} =
    31    33    34    35    37

>> celldisp(v)
v{1} =
    33    28    18     8     1
v{2} =
    34    30    20    11     2
v{3} =
    26    19    12     3
v{4} =
    35    22    14     4
v{5} =
    29    21    13     5
v{6} =
    25    15     6
v{7} =
    31    24    16     7
v{8} =
    37    32    27    17     9

A figure is also generated that draws the lines through each proper set of points:

enter image description here

function [horiz_list, vert_list] = testLines

global counter;
global colours; 
close all;

data = [ 475.  ,  605.75,    1.;
       571.  ,  586.5 ,    2.;
       233.  ,  558.5 ,    3.;
       669.5 ,  562.75,    4.;
       291.25,  546.25,    5.;
       759.  ,  536.25,    6.;
       362.5 ,  531.5 ,    7.;
       448.  ,  513.5 ,    8.;
       834.5 ,  510.  ,    9.;
       897.25,  486.  ,   10.;
       545.5 ,  491.25,   11.;
       214.5 ,  481.25,   12.;
       271.25,  463.  ,   13.;
       646.5 ,  466.75,   14.;
       739.  ,  442.75,   15.;
       340.5 ,  441.5 ,   16.;
       817.75,  421.5 ,   17.;
       423.75,  417.75,   18.;
       202.5 ,  406.  ,   19.;
       519.25,  392.25,   20.;
       257.5 ,  382.  ,   21.;
       619.25,  368.5 ,   22.;
       148.  ,  359.75,   23.;
       324.5 ,  356.  ,   24.;
       713.  ,  347.75,   25.;
       195.  ,  335.  ,   26.;
       793.5 ,  332.5 ,   27.;
       403.75,  328.  ,   28.;
       249.25,  308.  ,   29.;
       495.5 ,  300.75,   30.;
       314.  ,  279.  ,   31.;
       764.25,  249.5 ,   32.;
       389.5 ,  249.5 ,   33.;
       475.  ,  221.5 ,   34.;
       565.75,  199.  ,   35.;
       802.75,  173.75,   36.;
       733.  ,  176.25,   37.];

figure; hold on;
axis ij;

% Change due to Benoit_11
scatter(data(:,1), data(:,2),40, 'r.'); text(data(:,1)+10, data(:,2)+10, num2str(data(:,3)));
text(data(:,1)+10, data(:,2)+10, num2str(data(:,3)));

% Process your data as above then run the function below(note it has sub functions)
counter = 0;
colours = 'bgrcmy';
[horiz_list, vert_list] = findClosestPoints ( data(:,1), data(:,2) );


function [horiz_list, vert_list] = findClosestPoints ( x, y )
  % calc length of points
  nX = length(x);
  % set up place holder flags
  modelledH = false(nX,1);
  modelledV = false(nX,1);
  horiz_list = {};
  vert_list = {};

  % loop for all points
  for p=1:nX
    % have we already modelled a horizontal line through these?
    % second last param - true - horizontal, false - vertical
    if modelledH(p)==false
      [modelledH, index] = ModelPoints ( p, x, y, modelledH, true, true );
      horiz_list = [horiz_list index];
    else
      [~, index] = ModelPoints ( p, x, y, modelledH, true, false );
      horiz_list = [horiz_list index];
    end

    % make a temp copy of the x and y and remove any of the points modelled 
    %  from the horizontal -> this  is to avoid them being found in the 
    %  second call.
    tempX = x;
    tempY = y;
    tempX(index) = NaN;
    tempY(index) = NaN;
    tempX(p) = x(p);
    tempY(p) = y(p);
    % Have we found a vertial line?
    if modelledV(p)==false
      [modelledV, index] = ModelPoints ( p, tempX, tempY, modelledV, false, true );
      vert_list = [vert_list index];
    end
  end
end
function [modelled, index] = ModelPoints ( p, x, y, modelled, method, fullRun )
  % p - row in your original data matrix
  % x - data(:,1)
  % y - data(:,2)
  % modelled - array of flags to whether rows have been modelled
  % method   - horizontal or vertical (used to calc graadients)
  % fullRun  - full calc or just to get indexes 
  %            this could be made better by storing the indexes of each horizontal in the method above

  % Settings - play around with these values 
  gradDelta = 0.2;  % find points where gradient is less than this value
  gradLimit = 0.45; % if mean gradient of line is above this ignore
  numberOfPointsToCheck = 7; % number of points to check when look along the line
                        % to find other points (this reduces chance of it
                        % finding other points far away
                        %  I optimised this for your example to be 7
                        %  Try varying it and you will see how it effect the result.

  % Find the index of points which are inline.
  [index, grad] = CalcIndex ( x, y, p, gradDelta, method );
  % check gradient of line
  if abs(mean(grad))>gradLimit
    index = [];
    return
  end
  % add point of interest to index
  index = [p index];

  % loop through all points found above to find any other points which are in
  %  line with these points (this allows for slight curvature
  combineIndex = [];
  for ii=2:length(index)
    % Find inedex of the points found above (find points on curve)
    [index2] = CalcIndex ( x, y, index(ii), gradDelta, method, numberOfPointsToCheck, grad(ii-1) );

    % Check that the point on this line are on the original (i.e. inline -> not at large angle
    if any(ismember(index,index2))
      % store points found
      combineIndex = unique([index2 combineIndex]);
    end
  end

  % copy to index
  index = combineIndex;
  if fullRun
    %  do some plotting
    %  TODO: here you would need to calculate your arrays to output.
    xx = x(index);
    [sX,sOrder] = sort(xx);
    % Check its found at least 3 points
    if length ( index(sOrder) ) > 2
      % flag the modelled on the points found
      modelled(index(sOrder)) = true;
      % plot the data
      plot ( x(index(sOrder)), y(index(sOrder)), colours(mod(counter,numel(colours)) + 1));
      counter = counter + 1;
    end
    index = index(sOrder);
  end
end  
function [index, gradCheck] = CalcIndex ( x, y, p, gradLimit, method, nPoints2Consider, refGrad )
  % x - data(:,1)
  % y - data(:,2)
  % p - point of interest
  % method (x/y) or (y\x)
  % nPoints2Consider - only look at N points (options)
  % refgrad          - rather than looking for gradient of closest point -> use this
  %                  - reference gradient to find similar points (finds points on curve)
  nX = length(x);
  % calculate gradient
  for g=1:nX
    if method
      grad(g) = (x(g)-x(p))\(y(g)-y(p));
    else
      grad(g) = (y(g)-y(p))\(x(g)-x(p));
    end
  end
  % find distance to all other points
  delta = sqrt ( (x-x(p)).^2 + (y-y(p)).^2 );
  % set its self = NaN
  delta(delta==min(delta)) = NaN;
  % find the closest points
  [m,order] = sort(delta);

  if nargin == 7
    % for finding along curve
    % set any far away points to be NaN
    grad(order(nPoints2Consider+1:end)) = NaN;
    % find the closest points to the reference gradient within the allowable limit
    index = find(abs(grad-refGrad)<gradLimit==1);
    % store output
    gradCheck = grad(index);
  else
    % find the points which are closes to the gradient of the closest point
    index = find(abs(grad-grad(order(1)))<gradLimit==1);
    % store gradients to output
    gradCheck = grad(index);
  end
end
end
rayryeng
  • 102,964
  • 22
  • 184
  • 193
matlabgui
  • 5,642
  • 1
  • 12
  • 15
  • Was about to post something similar! BTW: You can do most of the manual work by using: `knnsearch` https://gist.github.com/knedlsepp/5a62e16d22d01ab38f56 I would then have suggested to find connected edges where the angles at the connection points are low. This way the wrong diagonal edges will be removed. – knedlsepp Apr 10 '15 at 12:23
  • @knedlsepp - I'm still interested in seeing what you propose. Please go ahead and post this. – rayryeng Apr 10 '15 at 13:42
  • @matlabgui - Thank you very much. I will examine your solution in detail to see if it works for me. Stay tuned. – rayryeng Apr 10 '15 at 13:42
  • @matlabgui - Given your results figure, those lines that you found are perfectly acceptable... so long as they span a sufficient amount across the image. In hindsight, your results are what I desire. I need to do some testing first, but I will be inclined to accept this answer. When I'm able to, I will also award you a bounty. Thanks very much! – rayryeng Apr 10 '15 at 14:38
  • @knedlsepp - cheers and I didn't have access to knnsearch while writing it. – matlabgui Apr 10 '15 at 14:42
  • 1
    @rayryeng Cheers! :) Glad to be of help! I thought it was an interesting problem to solve and had some available time to work on it away from consultancy and my GUI toolbox – matlabgui Apr 10 '15 at 14:42
  • @matlabgui - I appreciate that very much :)... especially since you weren't compensated in any other way... other than my sincere thanks and of course a forthcoming bounty. I'll do some testing and I'll get back to you, but for the most part, the results are what I desire :). – rayryeng Apr 10 '15 at 14:44
  • @matlabgui - I tried running your code and it isn't giving me the plot that was generated in your post. Am I doing something wrong? It only produces one segment. – rayryeng Apr 10 '15 at 15:13
  • @rayreng I wouldn't have thought so... I just ran it again and have the same result - I simply copied your data in the question. I doubt that the Matlab version would influence the results but I was using R2014b FYI. – matlabgui Apr 10 '15 at 15:17
  • @matlabgui - I don't think so either... I'm using MATLAB R2013a. Do I need to have the plot with the points and the IDs visible first before calling your code? – rayryeng Apr 10 '15 at 15:31
  • @matlabgui - Yup it was that lol. *face palm*. Thanks :) I don't get the different coloured segments like you do though. – rayryeng Apr 10 '15 at 15:32
  • @matlabgui - I've modified the code so that the lists are saved. BTW, the output looks great. I still need to do testing, but now your code is complete. I've also modified it so that plotting can be done in multiple colours per line. This doesn't happen for me in MATLAB R2013a. I'm going to modify your post with this information. Hope you don't mind! – rayryeng Apr 10 '15 at 16:08
  • @matlabgui - I'm confident this works. Answer is accepted. When I am able to, I will award you a +200 bounty. Thanks so much! – rayryeng Apr 10 '15 at 19:15
  • Cool!! :) FYI: One advancement on the algorithm that I would make (if it was mine or a consultancy job) would be to update the 2nd call to CalcIndex to include a max distance setting to find points along the curve as well as have it check the closest N points... Good luck. – matlabgui Apr 10 '15 at 19:24
  • @matlabgui - Bounty given! Thanks so much for your patience. Take care! – rayryeng Apr 27 '15 at 19:29
  • @matlabgui do you think It would be possible to do the same with Python? I don't know MATLAB and try to figure it out, whether it generally possible with Python – Julia Koncha May 30 '22 at 11:46
  • 1
    @JuliaKoncha Well this is from a long time ago!! Yes I would imagine so, the algorithm I wrote is matlab without any toolboxes (if I recall correctly) so I would imagine you should be able to do it in any other language. – matlabgui May 30 '22 at 14:56
  • @rayryeng I have a similar problem and try to convert solution suggested by matlabgui into python. But my problem is that I don't know MATLAB:) That's why I stuck at some places. Is there a possibility to have a look at your python version of this solution? I would be very thankful:) – Julia Koncha May 31 '22 at 12:58
  • @JuliaKoncha That code is protected by IP and I wrote it for the startup I worked at about 10 years ago. That code unfortunately is lost as the startup was dissolved and I no longer have a copy of it, but this means I could rewrite it again and share it. Maybe when I have time later, I can write a Python equivalent but I make no promises. However, if you are familiar with NumPy, this should be quite simple to convert. Other suggestions I have are that cell arrays in MATLAB (i.e. `some_var = {}`) are lists in Python. Left-leaning slashes (\\) mean to divide the left and multiply by the right. – rayryeng May 31 '22 at 14:08
  • @rayryeng thank you very much for your response! I will probably better try to post a question with a specific problem where I stuck. Anyway, thanks a lot! – Julia Koncha May 31 '22 at 18:26
  • 1
    @JuliaKoncha No problem at all. I think I may be able to find time this weekend to do it. It should be very simple to convert. When that's done, I will probably put it on GIthub which you can use and modify where necessary. Good luck in the meanwhile! – rayryeng Jun 01 '22 at 19:46
5

While I can not suggest a better approach to group any given list of centroid points than the one you already tried, I hope the following idea might help you out:

Since you are very specific about the content of your image (containing a field of squares) I was wondering if you in fact need to group the centroid points from the data given in your problem setup, or if you can use the data described in Background to the problem as well. Since you already determined the corners of each detected square as well as their position in that given square it seems to me like it would be very accurate to determine a neighbour of a given square by comparing the corner-coordinates.

So for finding any candidate for a right neighbour of any square, i would suggest you compare the upper right and lower right corner of that square with the upper left and lower left corner of any other square (or any square within a certain distance). Allowing for only small vertical differences and slightly greater horizontal differences, you can "match" two squares, if both their corresponding corner-points are close enough together.

By using an upper limit to the allowed vertical/horizontal difference between corners, you might even be able to just assign the best matching square within these boundaries as neighbour

A problem might be that you don't detect all the squares, so there is a rather large space between square 30 and 32. Since you said you need 'at least' 3 squares per row, it might be viable for you to simply ignore square 32 in that horizontal line. If that is not an option for you, you could try to match as many squares as possible and afterwards assign the "missing" squares to a point in your grid by using the previously calculated data:

In the example about square 32 you would've detected that it has upper and lower neighbours 27 and 37. Also you should've been able to determine that square 27 lies within row 1 and 37 lies within row 3, so you can assign square 32 to the "best matching" row in between, which is obviously 2 in this case.

This general approach is basically the approach you have tried already, but should hopefully be a lot more accurate since you are now comparing orientation and distance of two lines instead of simply comparing the location of two points in a grid.

Also as a sidenode on your previous attempts - can you use the black cornerlines to correct the initial rotation of your image a bit? This might make further distortion algorithms (like the ones that you discussed with knedlsepp in the comments) a lot more accurate. (EDIT: I did read the comments of Parag just now - comparing the points by the angle of the lines is of course basically the same as rotating the Image beforehand)

H W
  • 2,556
  • 3
  • 21
  • 45
  • 1
    1. I don't have to just consider the centroids. I can certainly use any data that is available in the image, so you are certainly open to use that information. 2. I can certainly avoid using square 32 for a horizontal line. The other centroids along that same row are a good enough distribution to be deemed collinear. 3. Thank you for your interest :) I will most likely use your ideas in combination with what matlabgui has provided for me. Take care! – rayryeng Apr 10 '15 at 14:43
5

I'm using a cropped version of the posted image as the input. Here I'm only addressing the case where the orientation of the grid can be thought of as near horizontal/vertical. This may not fully address your scope, but I think it may give you some pointers.

enter image description here

Binarize the image so that the distorted squares are filled. Here I use a simple Otsu thresholding. Then take the distance transform of this binary image.

enter image description here

In the distance transformed image we see the gaps between the squares as peaks.

To get horizontally oriented lines, take the local maxima of each of the columns of the distance image and then find connected components.

To get vertically oriented lines, take the local maxima of each of the rows of the distance image and then find connected components.

Images below show the horizontal and vertical lines thus found with corner points as circles.

horizontal lines and corners

vertical lines and corners

For reasonably long connected components, you can fit a curve (a line or a polynomial) and then classify the corner points, say based on the distance to the curve, on which side of the curve the point is, etc.

I did this in Matlab. I didn't try the curve fitting and classification parts.

clear all;
close all;

im = imread('0RqUd-1.jpg');
gr = rgb2gray(im);
% any preprocessing to get a binary image that fills the distorted squares
bw = ~im2bw(gr, graythresh(gr));

di = bwdist(bw);                % distance transform
di2 = imdilate(di, ones(3));    % propagate max

corners = corner(gr);           % simple corners

% find regional max for each column of dist image
regmxh = zeros(size(di2));
for c = 1:size(di2, 2)
    regmxh(:, c) = imregionalmax(di2(:, c));
end
% label connected components
ccomph = bwlabel(regmxh, 8);

% find regional max for each row of dist image
regmxv = zeros(size(di2));
for r = 1:size(di2, 1)
    regmxv(r, :) = imregionalmax(di2(r, :));
end
% label connected components
ccompv = bwlabel(regmxv, 8);

figure, imshow(gr, [])
hold on
plot(corners(:, 1), corners(:, 2), 'ro')
figure, imshow(di, [])
figure, imshow(label2rgb(ccomph), [])
hold on
plot(corners(:, 1), corners(:, 2), 'ro')
figure, imshow(label2rgb(ccompv), [])
hold on
plot(corners(:, 1), corners(:, 2), 'ro')

To get these lines for arbitrary oriented grids, you can think of the distance image as a graph and find optimal paths. See here for a nice graph based approach.

Community
  • 1
  • 1
dhanushka
  • 10,492
  • 2
  • 37
  • 47
  • 1
    Thank you for this approach. +1. However, I will be going with matlabgui's approach instead as it doesn't require anything form the image processing toolbox. Also, the algorithm is ultimately going to be performed in Python instead so his solution is more portable. Thank you for your efforts nonetheless! – rayryeng Apr 10 '15 at 17:35