Last active
January 11, 2018 13:58
-
-
Save s-shin/6409891 to your computer and use it in GitHub Desktop.
A tutrial in which square markers in an image are detected with OpenCV 2.4 in Python 2.7.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
"""Tutrial: Detect square markers with OpenCV 2.4 in Python 2.7. | |
(C) 2013 Shintaro Seki | |
MIT License | |
The marker used in this program can be generated by the following script. | |
cv2.imwrite("marker.png", cv2.resize(np.array([ | |
[0, 0, 0, 0], | |
[0, 0, 255, 0], | |
[0, 255, 255, 0], | |
[0, 0, 0, 0], | |
]), (512, 512), interpolation=cv2.INTER_NEAREST)) | |
""" | |
import sys | |
import copy | |
import cv2 | |
import numpy as np | |
# Filtered by this size. | |
MIN_SQUARE_SIZE = 20 # px | |
# Transformed marker size. | |
MARKER_SIZE = 200 # px | |
# Black frame size in marker. | |
MARKER_FRAME_SIZE = MARKER_SIZE * 0.25 | |
def convertImgToBit(img, threshold=0.8): | |
size = img.size | |
blackPixelNum = reduce(lambda m, c: m+1 if c == 0 else m, img.flat, 0) | |
whitePixelNum = size - blackPixelNum | |
if blackPixelNum > size * threshold: | |
return 1 | |
elif whitePixelNum > size * threshold: | |
return 0 | |
else: | |
return None | |
def areInClockwiseOrder(points): | |
"""Check whether `points` are in clockwise order. | |
See the website (Japanese) below for details. | |
http://www5d.biglobe.ne.jp/~noocyte/Programming/Geometry/PolygonMoment-jp.html | |
""" | |
s = 0 | |
for i in range(len(points)): | |
j = i + 1 if i != len(points) - 1 else 0 | |
s += (points[i][0] + points[j][0]) * (points[i][1] - points[j][1]) | |
return s < 0 | |
def __main(): | |
if len(sys.argv) < 2: | |
print "usage: %s FILENAME" % sys.argv[0] | |
return 0 | |
# Argument 1 is the target file name. | |
filename = sys.argv[1] | |
# [1] Load image. | |
srcImg = cv2.imread(filename) | |
if srcImg == None: | |
print "cannot read '%s'." % filename | |
return 1 | |
# [2] To grayscale. | |
gsImg = cv2.cvtColor(srcImg, cv2.COLOR_BGR2GRAY) | |
# [3] Noise reduction. If the input image is not compressed, | |
# this process can be skipped. | |
gsbImg = cv2.GaussianBlur(gsImg, (5, 5), 0) | |
# [4] Binarize. | |
threshVal, binImg = cv2.threshold( | |
gsbImg, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) | |
# [5] Find contours. The type of `contours` is needed to take care. | |
tmpImg = copy.deepcopy(binImg) | |
contours, hierarchy = cv2.findContours( | |
tmpImg, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) | |
#print type(contours), type(contours[0]), contours[0].shape | |
# [6] Pick up tetragons (that has 4 corners). If the corner points are | |
# connected in the order listed in the array, the lines don't intersect, | |
# but the rotation (cw/ccw) is not decided. | |
polygons = map(lambda c: cv2.approxPolyDP(c, 3, True), contours) | |
tetragons = filter(lambda p: len(p) == 4, polygons) | |
# [7] `tetragons` may have uncorrect ones, so these should be filtered | |
# by the size, the shape, and so on. | |
# Here, only filtered by the size. | |
minArea = MIN_SQUARE_SIZE ** 2 | |
tetragons = filter(lambda t: cv2.contourArea(t) >= minArea, tetragons) | |
# Fix too deeply nested array. | |
tetragons = map(lambda t: t.reshape(-1, 2), tetragons) | |
# Draw tetragons. | |
cornersImg = copy.deepcopy(srcImg) | |
cv2.drawContours(cornersImg, tetragons, -1, (0, 0, 255), 2) | |
# [8] Transform the tetragon regions. | |
squareImgs = [] | |
for tetragon in tetragons: | |
# Destination points are clockwise. | |
dst = np.array([ | |
[0, 0], | |
[MARKER_SIZE, 0], | |
[MARKER_SIZE, MARKER_SIZE], | |
[0, MARKER_SIZE] | |
], np.float32) | |
# If source points are not clockwise, the transformed image is mirrored. | |
if not areInClockwiseOrder(tetragon): | |
# np.array don't has the reverse function, but can be reversed | |
# by array slice. (http://stackoverflow.com/q/6771428) | |
tetragon = tetragon[::-1] | |
src = np.array(tetragon, np.float32) | |
matrix = cv2.getPerspectiveTransform(src, dst) | |
img = cv2.warpPerspective(binImg, matrix, (MARKER_SIZE, MARKER_SIZE)) | |
squareImgs.append(img) | |
# [9] Check each of the markers by using the template matching method. | |
# The rotation and the marker id can be got. | |
markerImgs = [] | |
for img in squareImgs: | |
# Get pattern region. | |
patternRegion = img[ | |
MARKER_FRAME_SIZE : MARKER_SIZE - MARKER_FRAME_SIZE, | |
MARKER_FRAME_SIZE : MARKER_SIZE - MARKER_FRAME_SIZE, | |
] | |
# The pattern region is divided to 4 parts and each of them is | |
# converted to a bit. | |
size = (MARKER_SIZE - MARKER_FRAME_SIZE * 2) * 0.5 | |
tl = convertImgToBit(patternRegion[:size,:size]) | |
tr = convertImgToBit(patternRegion[:size,size:]) | |
bl = convertImgToBit(patternRegion[size:,:size]) | |
br = convertImgToBit(patternRegion[size:,size:]) | |
bits = [tl, tr, br, bl] | |
if None in bits: | |
continue | |
if len(filter(lambda x: x == 1, bits)) != 1: | |
continue | |
rot90 = bits.index(1) | |
# draw information | |
markerImg = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) | |
text = "Rotation: %d[deg]" % (90 * rot90) | |
fontFace = cv2.FONT_HERSHEY_SIMPLEX | |
fontScale = 0.4 | |
thickness = 1 | |
size, baseLine = cv2.getTextSize(text, fontFace, fontScale, thickness) | |
cv2.putText(markerImg, text, (0, size[1]), fontFace, fontScale, | |
(0, 0, 255), thickness) | |
markerImgs.append(markerImg) | |
# Show windows and images. | |
wnds = [ | |
("src", srcImg), | |
("grayscale", gsImg), | |
("grayscale and blur", gsbImg), | |
("binarized (%d)" % threshVal, binImg), | |
("corners", cornersImg) | |
] | |
wnds.extend(map(lambda img, i: ("marker %d" % i, img), | |
markerImgs, range(len(markerImgs)))) | |
for w in wnds: | |
cv2.namedWindow(w[0]) | |
cv2.imshow(w[0], w[1]) | |
cv2.waitKey(0) | |
return 0 | |
if __name__ == "__main__": | |
sys.exit(__main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment