I have been working on creating a simple AR Tag classifier, for detecting a simple AR Tag glyph. Spitting out an image that looks like this:With the red circle indicating the corner closest to the orientation square.  My program is structured as follows:AR.py (where all the magic happens)import cv2import numpy as npfrom artag import ARtagdef display_scaled_image(name, image, scale):    Function to display a scaled cv2 image    :param name:        Window name    :type name:        basestring    :param image:        Image as numpy array    :type image:        numpy.ndarray    :param scale:        Scale factor applied to image    :type scale:        float        height, width = image.shape[:2]    cv2.imshow(name, cv2.resize(image,            (int(scale * width), int(scale * height)),            interpolation=cv2.INTER_CUBIC))def mask_black(img):    Function to mask for black boxes in an BGR image    :param img:        Numpy image to process    :type img:        numpy.ndarray    :return:        the masked image    :rtype:        numpy.ndarray        return cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,                        cv2.THRESH_BINARY, 511, 10)def find_tag(img):        Identify AR tags in an image    :param img:        The image to be processed, may be pre-converted to greyscale    :type img:        numpy.ndarray    :return:        Tuple[List[artag], numpy.ndarray]        # Convert the image to greyscale if it is not    if len(img.shape) > 2:        img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)    dims = img.shape[:2]    # Normalize image size    img, _ = rescale_img(img)    kernel = np.ones((3, 3), np.uint8)    img = mask_black(img)    img = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)    _, contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)    # Clean up contours to regular shapes    culled_conts = []    for c, h in zip(contours, hierarchy[0]):        c = cv2.convexHull(c)        epsilon = 0.01 * cv2.arcLength(c, True)        approx = cv2.approxPolyDP(c, epsilon, True)        culled_conts.append((approx, h))    img, scale = rescale_img(img, dims)    # Identify nested shapes as possible AR tags    ar_tags = []    for c, h in culled_conts:        # 4 sided tags        if len(c) == 4:            # The tag contains a smaller contour            if h[2] != -1:                tag = ARtag(c, culled_conts[h[2]][0], scale)                if tag.valid():                    ar_tags.append(tag)    img = cv2.merge((img, img, img))    return ar_tags, imgdef rescale_img(img, dims=None, width=1024):    Rescale image, either to the dimensions provided or to the width    provided, returning the image and it's dimensions    :param img:        The input image    :type img:        numpy.ndarray    :param dims:        Provide this parameter to fit the image to a specific size    :type dims:        Tuple[int, int]    :param width:        The width to resize the image to, (overridden by dims) deafualts to 1024    :type width:        int    :return:        Tuple of image and its dimensions    :rtype:        Tuple[numpy.ndarray, Tuple[int, int]]        grey = img    if dims is None:        r = float(width) / grey.shape[1]        dims = (width, int(grey.shape[0] * r))    else:        dims = dims[::-1]    scale = (dims[1] / float(grey.shape[0]), dims[0] / float(grey.shape[1]))    return cv2.resize(grey, dims, interpolation=cv2.INTER_AREA), scaleif __name__ == __main__:    # Display results for all test images    import os    for f in list(os.walk(Test imges))[0][2]:        if jpg in f:            in_img = cv2.imread(Test imges/ + f)            grey = cv2.cvtColor(in_img, cv2.COLOR_BGR2GRAY)            ar_tags, proc_img = find_tag(grey)            for i in ar_tags:                proc_img = i.draw(proc_img)                in_img = i.draw(in_img)            display_scaled_image(Image - {}.format(f), proc_img, .25)            display_scaled_image(Orig Image - {}.format(f), in_img, .25)            cv2.waitKey(0)            cv2.destroyAllWindows()artag.pyimport mathimport cv2class ARtag(object):    def __init__(self, outer, inner, scale):        Create a ARTag object, based on its inner contour,        outer contour and the scale of the image processed as        related to the image that was processed to extract them        :param outer:            The outer contour (Assumed to have 4 sides)        :param inner:            The outer contour (Assumed to have at least 4 sides)        :param scale:            The scale of the image that was processed to create this tag        :type scale:            Tuple[int, int]                self.outer_cont = outer        for i, c in enumerate(self.outer_cont):            self.outer_cont[i][0][0] = c[0][0] * scale[1]            self.outer_cont[i][0][1] = c[0][1] * scale[0]        self.inner_cont = inner        for i, c in enumerate(self.inner_cont):            self.inner_cont[i][0][0] = c[0][0] * scale[1]            self.inner_cont[i][0][1] = c[0][1] * scale[0]        self.outer_tup = [tuple(i[0]) for i in outer]        self.inner_tup = [tuple(i[0]) for i in inner]        x, y = zip(*self.inner_tup)        x = sum(x) / len(x)        y = sum(y) / len(y)        self.outer_tup = sorted(self.outer_tup, key=lambda p: math.sqrt((p[0] - x)**2 + (p[1] - y)**2))        # Points of interest labeled clockwise from marked corner        self.p0 = self.outer_tup[0]        self.p1 = self.outer_tup[1]        self.p2 = self.outer_tup[3]        # Furthest point must be the 3rd point        self.p3 = self.outer_tup[2]    def draw(self, img):        Draw the tag on a 3 channel image        :param img:            The 3 channel image        :type img:            numpy.ndarray        :return:            The input image        :rtype:            numpy.ndarray                img = cv2.drawContours(img, [self.outer_cont], -1, (0, 255, 0), 10)        img = cv2.drawContours(img, [self.inner_cont], -1, (255, 0, 0), 10)        img = cv2.circle(img, tuple(self.p0), 20, (0, 0, 255), 10)        return img    def ratio(self, img):        Ratio of the size of the Tag vs the input image size        :param img:            The input image        :type img:            numpy.ndarray        :return:            The processed image        :rtype:            numpy.ndarray                return cv2.contourArea(self.outer_cont) / float(img.shape[0] * img.shape[1])    def in_outer(self):        return cv2.contourArea(self.inner_cont) / cv2.contourArea(self.outer_cont)    @staticmethod    def dist(p0, p1):         Return the distance between two tuples in the format (x, y)        :param p0:            The first point        :type p0:            Tuple[int, int]        :param p1:            The second point        :type p1:            Tuple[int, int]        :return:            The distance        :rtype:            float                return math.sqrt((p0[0] - p1[0])**2 + (p0[1] - p1[1])**2)    def valid(self):        Check the validity of this artag        Check the following items:         - Any line distance is more than 1.8 times the average length         - Any line distance is less than .2 times the average length         - The inner contour takes up more than 10% of the tag         - The inner contour takes up less than 1% of the tag        :return:            The validity of the tag        :rtype:            bool                lengths = [            self.dist(self.p0, self.p1),            self.dist(self.p1, self.p2),            self.dist(self.p2, self.p3),            self.dist(self.p3, self.p0)        ]        # check aspect ratio        if any([i > 1.8 * sum(lengths) / len(lengths) for i in lengths]):            return False        if any([i < .2 * sum(lengths) / len(lengths) for i in lengths]):            return False        # Check area ratios        if cv2.contourArea(self.inner_cont) / cv2.contourArea(self.outer_cont) > 0.1:            return False        if cv2.contourArea(self.inner_cont) / cv2.contourArea(self.outer_cont) < 0.01:            return False        return TrueIt seems to work great, except for image 11 seems to fail to detect the Tag, this has somthing to do with the nesting of the contours. What I would like to do to fix this image, would be to do somthing like find large 4-sided contours that have smaller 4 sided contours inside, that are not nested and have 4 sides. The problem is the structure of the hierarchy object returned by opencv. It has the indexes of the contours, but if you modify the list it makes this useless. I would love to have a data structure for the contours, that have the data baked in, but the problem occurs if I start removing contours, becouse they are invalid or somthing I get problems referring to contours that are nonexistent.Any input on solving this problem elegantly would be a great in addition to a general review of the code.My code is hosted on github here: AR-TestingYou can download all the code with the test images here: AR-Testing.zip
Simple AR Tag classifier
python;python 2.7;image;opencv