Skip to content

Instantly share code, notes, and snippets.

@s-shin
Last active January 11, 2018 13:58
Show Gist options
  • Save s-shin/6409891 to your computer and use it in GitHub Desktop.
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.
# -*- 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