0

I'm trying to locate the maze location from a photo.

What I'd like to get is the (x,y) points of the corners of the maze.

As you could see I applied cv2.Canny() to the picture and got a really nice clean image as a start.

enter image description here

So the next step is to locate the maze.

I have searched for a while, all SOF questions are asking for finding locations of "perfect" rectangles, e.g. this one and this one But in my case, the rectangle doesn't have a closed contour, so codes for them won't work in my case.

Also have had a look at the OpenCV code, they all try to find Contours and draw those contours onto the image, but it doesn't work for me. I just got 1 big contour which goes alone the borders of my photo.

cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)      
cnts = imutils.grab_contours(cnts)

Update 1

Original Photo: enter image description here

Code:

import cv2
from PIL import Image
from matplotlib import pyplot as plt
import numpy as np
import imutils

img = cv2.imread('maze.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

edges = cv2.Canny(img,100,200)


f,axarr = plt.subplots(1,2,figsize=(15,15))

axarr[0].imshow(img)
axarr[1].imshow(edges)

plt.show()
Franva
  • 6,565
  • 23
  • 79
  • 144
  • Please provide the original image without being plotted inside `matplotlib`. As a starter: Grayscale the image, binary threshold using some relatively large threshold value (since the paper is quite white-ish), some morphology to get a nice contour, find contours. – HansHirse Apr 12 '21 at 12:27
  • hi @HansHirse thanks for your comment. Showing image inside Jupyter Notebook is quite common, may I know why I should not do it? The right side image is a grayscale image and has been binary already (0 or 255). Yes, I have tried to find its contour as I described in my question, but none of them worked as I explained in my question. – Franva Apr 12 '21 at 12:49
  • You shall provide the original image here, such that people can download it, and try something on their own. Nobody wants to manually cut out the (approximate) image from the saved plot image, especially since it'll give different results compared to working on the original image. Also, without seeing some more code in terms of a [mre], helping is difficult anyway. – HansHirse Apr 12 '21 at 12:53
  • @HansHirse make sense. I have uploaded the original photo and my code. – Franva Apr 12 '21 at 13:27

1 Answers1

5

One approach could be using morphological reconstruction, although it would probably be an ad-hoc solution. Assuming the maze is always in the center of the photo, you could use a "window" of the central portion of the maze as seed/mark for a morphological reconstruction of the maze given that all borders are connected. As a result you would have isolated the maze out of the photo. The (x,y) of the corners would be "easy" to get by obtaining the bounding box of the isolated maze.

For example:

from skimage.morphology import reconstruction
from skimage.io import imread, imshow
import matplotlib.pyplot as plt
import numpy as np


maze = imread('/home/user/.../maze.jpg', as_gray=True)
h, w = maze.shape

#creating the seed 
seed = np.zeros_like(maze)
size = 40

#we create a window in the center of the image
seed[h//2-size:h//2+size, w//2-size:w//2+size] = maze[h//2-size:h//2+size, w//2-size:w//2+size]

seed would contain a centered portion of the maze with a black frame. Keep in mind, seed must be the same size as maze, thus the existence of the frame.

first seed for reconstruction

We apply the reconstruction for the first time and binarize the result. By default, reconstruction uses a structural element with (3,3) shape. The values for the threshold can be adjusted depending the image.

rec_1 = reconstruction(seed, maze)
rec_1[rec_1 < 0.70] = 0.
rec_1[rec_1 >= 0.70] = 1.

This is rec_1: first reconstruction attempt

It's good but we can make it a little bit better. Let's apply reconstruction again, but this time using a bigger window and erosion instead of dilation as the reconstruction method:

seed_2 = np.ones_like(rec_1)
size_2 = 240
seed_2[h//2-size_2:h//2+size_2, w//2-size_2:w//2+size_2] = recon[h//2-size_2:h//2+size_2, w//2-size_2:w//2+size_2]
rec_2 = reconstruction(seed_2, rec_1, method='erosion', selem=np.ones((11,11)))

Notice I'm using a bigger structural element too, with a shape of (11,11).

The final reconstruction step gives us this result:

final reoconstruction

The following step would be to use a bounding box method to get the top left and bottom right (x, y) coordinates.

Those results will be an approximation given that in the original image, the maze is not perfectly flat and we would be relying the fact that the maze's entry and exit are exactly in those positions and not anywhere else in the maze.

Also, this can probably be done with fewer steps.

UPDATE: As suggested, the exact coordinates can be obtained by using convex hull, binary erosion and corner detection techniques instead of a bounding box method.

First we invert rec_2, then we get the convex hull, apply erosion to shrink the size of the rectangle and calculate the corner peaks coordinates.

from skimage.util import invert
from skimage.morphology import binary_erosion
from skimage.morphology.convex_hull import convex_hull_image
from skimage.feature import corner_harris, corner_subpix

rec_2_inv = invert(rec_2)
hull = convex_hull_image(rec_2_inv)
hull_eroded = binary_erosion(hull, selem=np.ones(30,30))

coords = corner_peaks(corner_harris(hull_eroded), min_distance=5, threshold_rel=0.02)
coords_subpix = corner_subpix(rec_2_inv, coords, window_size=13)[2:4]

This is the eroded convex hull: Eroded convex hull

Finally we plot the final image with the coordinates over it:

fig, ax = plt.subplots()
ax.imshow(rec_2_inv, cmap=plt.cm.gray)
ax.plot(coords_subpix[:, 1], coords_subpix[:, 0], '+r', markersize=15)
plt.savefig('maze_with_coordinates.png', bbox_tight=True)

Annotated entry and exit on the maze

Where coords_subpix holds the numerical values of the coordinates and I purposely assign two values instead of four. Those values are:

[[1611.48876404  104.50561798]
 [2695.07777778 1679.67222222]]

Most of the update code was done using scikit-image example's parameters with minimal tweaking.

pazitos10
  • 1,641
  • 16
  • 25
  • 1
    What a nice solution! Don't use a bounding box here, but rather invert the last image, do some morphological closing, get the convex hull, and apply some corner detection, e.g. [Shi-Tomasi](https://docs.opencv.org/4.5.2/d8/dd8/tutorial_good_features_to_track.html). That way, you'll get the "exact" corner points instead of some approximations from the bounding box. – HansHirse Apr 12 '21 at 14:39
  • Thanks for the suggestion, I will implement it and update the answer. – pazitos10 Apr 12 '21 at 14:52
  • @pazitos10 this is most well written and best all-around answer I have ever received on SOF! I feel so lucky to have your answer here! Thank you so much ~! I just got off from my classes, will try your beautiful approach tmrw the 1st thing~! – Franva Apr 13 '21 at 14:50
  • hi @HansHirse why using a bounding box is not good here? I'm new to Image Processing, would like to know all details. thanks – Franva Apr 13 '21 at 22:48
  • hi @pazitos10 I am having trouble to understand the concept of corner_subpix() and how exactly the morphology reconstruction works. I have read both documents. Also I checked coords, I have 4 points in it, but when checking coords_subpix, I got this ```array([[ nan, nan], [2696.76966292, 1679.86516854]])``` – Franva Apr 14 '21 at 00:27
  • why do we need the coords_subpix after all we already have all 4 coords? – Franva Apr 14 '21 at 00:27
  • 1
    Hi @Franva, I don't know the specifics of `corner_subpix()` but you can read more about it [here](https://scikit-image.org/docs/dev/api/skimage.feature.html#skimage.feature.corner_subpix) and [here](https://en.wikipedia.org/wiki/Corner_detection#The_F.C3.B6rstner_corner_detector). When I did the experiments, it returned `nan` when I used a selem shape that was too big. To "fix it", I just turned the selem shape a notch until it returned numerical values. – pazitos10 Apr 14 '21 at 00:58
  • 1
    @Franva ... On why we need to use `corner_subpix()` when we already have 4 corners, I I could be wrong but I think it could be a safety measure to ensure those coordinates are really representing corners and aren't a false positive. Besides it provides precision to those coordinates which was required for the problem. – pazitos10 Apr 14 '21 at 01:03
  • 1
    @Franva Finally, on how morphology reconstruction works I strongly suggest you to read the cited papers in the [docs](https://scikit-image.org/docs/dev/api/skimage.morphology.html?highlight=reconstruct#reconstruction). Essentially it relies on dilation to make the seed "grow" and intersecting it with the original image to create a bigger representation. Repeating this step until there are no differences between the original image and the intersected portion and making it grow does not change the result. But that's one kind of morphological reconstruction. – pazitos10 Apr 14 '21 at 01:10
  • thanks @pazitos10 after reading more articles, I now have understood the concept of subpixel. I am trying all sorts of different combinations for `selem` in `hull_eroded`(hope this is the one you referred to), since I don't understand it, I still cannot get the correct start entrance and end exist. Do you know some websites which have visual videos to show hos these image processing steps work? I have masked your gorgeous post as my answer as your approach has perfectly worked out the location of the maze. once again, thank you :) – Franva Apr 14 '21 at 02:03
  • 1
    @Franva thanks for your kind words! I do refer to `selem` in `hull_eroded`. Basically, the "bigger" the selem the faster you erode the pixels but it will also have an impact on how the corner coordinates are calculated. We apply erosion to shrink the convex hull enough to make it "fit" inside the maze outer borders. Once it fits, the upper left and bottom right corner are the coordinates for the entry and exit of the maze. Feel free to try with other methods for [corner detection](https://scikit-image.org/docs/dev/api/skimage.feature.html#module-skimage.feature) too. – pazitos10 Apr 14 '21 at 03:02
  • hi @pazitos10 appreciate your explanation, it much more easier to understand in your plain language than those I saw on its document with full of jargon. I hope there could be a website which teaches all these techniques with simple and plain language to enable the average people to understand and learn well the image processing. – Franva Apr 14 '21 at 03:28
  • 1
    @Franva some time ago I created a [repo](https://github.com/Pazitos10/math-morph) with my implementation from scratch of some basic morphology operations including reconstruction. There's a course from [Udemy](https://www.youtube.com/watch?v=uUweXBmm978) that teaches the basics of computer vision and includes concepts of mathematical morphology. – pazitos10 Apr 14 '21 at 04:01
  • 1
    @pazitos10 thanks for sharing and introducing these! have started the repo and followed the course :)!! – Franva Apr 14 '21 at 07:13
  • @Franva Thank you and good luck on your journey! – pazitos10 Apr 14 '21 at 13:38
  • hi @pazitos10 I know this question has been answered and closed, but just wondering is it possible to locate the 2 entry points(start and end point) by imaging processing without relying on the black arrows? If yes, I will create a separated question for it. – Franva Apr 26 '21 at 13:53
  • @Franva, Although my answer here doesn't rely on the black arrows (as the 2nd reconstruction removes them) but in the fact that both entry and exit points are in the corners of the maze, I think it will be better if you create a new question to explain in detail your case and what you have tried. – pazitos10 Apr 27 '21 at 21:16
  • hi @pazitos10, I found the piece of code only works for 1 particular photo, I have created a new question for image processing, https://1drv.ms/u/s!Asflam6BEzhjgbIh8_tQqdv-V4RBgA?e=3nYbg6 just in case you are interested. thanks – Franva May 11 '21 at 13:14