├── __init__.py ├── test ├── __init__.py ├── testBoxy.py ├── testFlattenLineSegment.py ├── testAngleMap.py ├── testFlattenAngle.py └── testLineSegment.py ├── requirements.txt ├── README.md ├── .gitignore ├── LICENSE ├── lineSegment.py └── checkBoxDetector.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.10.4 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## The problem. 2 | 3 | Detect checkboxes using openCV 4 | 5 | ## The solution. 6 | 7 | First we find all closed contours using findContours and then checking on hierarchy if it has a chield hierarchy[i][2] != -1 8 | After that we check if the child hierarchy has a sibling (otherwise it's just the internal contour and the contour is not filled) 9 | firtstChildHierarchy = hierarchy[hierarchy[i][2]] 10 | isFilled = firtstChildHierarchy[0]!=-1 11 | After that we check if it resembles a box by having 2 sets of parallel lines perpendicular with each other 12 | -------------------------------------------------------------------------------- /test/testBoxy.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from boxDetector.lineSegment import LineSegment 3 | 4 | from boxDetector.checkBoxDetector import( 5 | LineGroup, 6 | minRadAngle, 7 | is_boxy 8 | ) 9 | 10 | class TestBoxy(TestCase): 11 | def setUp(self): 12 | self.perfect_box = [(0,0),(0,10),(10,10),(10,0),(0,0)] 13 | self.box_with_noise = [(0,0),(1,1),(2,2),(0,2),(0,10),(10,10),(10,0),(0,0)] 14 | self.open = [(0,0),(1,1),(2,2),(0,10),(10,10)] 15 | 16 | def test_perfect_box(self): 17 | self.assertTrue(is_boxy(self.perfect_box)) 18 | 19 | def test_noise(self): 20 | self.assertTrue(is_boxy(self.box_with_noise)) 21 | 22 | def test_open(self): 23 | self.assertFalse(is_boxy(self.open)) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Fabio Oliveira Costa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/testFlattenLineSegment.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from boxDetector.lineSegment import LineSegment 3 | 4 | from boxDetector.checkBoxDetector import( 5 | LineGroup, 6 | flatten_line_segment 7 | ) 8 | 9 | 10 | class TestFlattenLineSegment(TestCase): 11 | 12 | def test_join_same_level(self): 13 | a = LineSegment((0, 0), (1, 1)) 14 | b = LineSegment((2, 2), (3, 3)) 15 | c = LineSegment((5, 5), (10, 10)) 16 | flatten = flatten_line_segment([a,b,c],a.unit_vector) 17 | joined = flatten[0.0] 18 | self.assertEqual(joined.a, (0, 0)) 19 | self.assertEqual(joined.b, (10, 10)) 20 | 21 | def test_join_varying_level(self): 22 | a = LineSegment((0, 0), (0, 1)) 23 | b = LineSegment((0, 2), (0, 5)) 24 | c = LineSegment((15, 2), (15, 10)) 25 | flatten = flatten_line_segment([a,b,c],a.unit_vector,gap_epsilon=6) 26 | # CHecking it respected the levels erasing the merged ones 27 | self.assertIn(0.0, flatten) 28 | self.assertNotIn(3.0, flatten) 29 | self.assertIn(15.0, flatten) 30 | joined = flatten[0.0] 31 | self.assertEqual(joined.a, a.a) 32 | self.assertEqual(joined.b, b.b) 33 | self.assertEqual(flatten[15.0], c) 34 | -------------------------------------------------------------------------------- /test/testAngleMap.py: -------------------------------------------------------------------------------- 1 | import math 2 | from unittest import TestCase 3 | from boxDetector.lineSegment import LineSegment 4 | from boxDetector.checkBoxDetector import build_segments_angle_map 5 | 6 | ninety_degrees = round(math.pi/2, 4) 7 | 8 | 9 | class TestAngleMap(TestCase): 10 | 11 | def setUp(self): 12 | self.perfect_5_box = [(0, 0), (5, 0), (5, 5), (0, 5), (0, 0)] 13 | 14 | def test_angle_indexing(self): 15 | angle_map = build_segments_angle_map(self.perfect_5_box) 16 | print(angle_map.keys()) 17 | self.assertEqual(len(angle_map.keys()), 2) 18 | self.assertIn(ninety_degrees, angle_map) 19 | self.assertIn(0.0, angle_map) 20 | 21 | def test_segment_indexing(self): 22 | points = self.perfect_5_box 23 | angle_map = build_segments_angle_map(points) 24 | 25 | zero_segments = angle_map[0.0] 26 | # Checking if the pivot point is the first vector 27 | segment = LineSegment(points[0], points[1]) 28 | self.assertEqual(zero_segments.pivotPoint, segment.vector) 29 | self.assertEqual(zero_segments.norm, segment.unit_vector) 30 | lines = zero_segments.lines 31 | 32 | self.assertIn(segment,lines) 33 | segment = LineSegment(points[2], points[3]) 34 | self.assertIn(segment,lines) 35 | 36 | ninety_segments = angle_map[ninety_degrees] 37 | segment = LineSegment(points[1], points[2]) 38 | self.assertEqual(ninety_segments.pivotPoint, segment.vector) 39 | self.assertEqual(ninety_segments.norm, segment.unit_vector) 40 | lines = ninety_segments.lines 41 | self.assertIn(segment,lines) 42 | segment = LineSegment(points[3], points[4]) 43 | self.assertIn(segment,lines) 44 | 45 | 46 | def test_segment_skiping(self): 47 | """ 48 | Check if segments less than min are skipped until min is reached 49 | """ 50 | points = [(0, 0), (2, 0), (5, 0)] 51 | angle_map = build_segments_angle_map(points, min_size=5) 52 | self.assertIn(0.0, angle_map) 53 | segment = LineSegment(points[0], points[2]) 54 | lines = angle_map[0.0].lines 55 | self.assertEqual(len(lines), 1) 56 | self.assertEqual(lines[0], segment) 57 | -------------------------------------------------------------------------------- /test/testFlattenAngle.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from boxDetector.lineSegment import LineSegment 3 | 4 | from boxDetector.checkBoxDetector import( 5 | LineGroup, 6 | minRadAngle, 7 | flatten_angles 8 | ) 9 | 10 | class TestFlattenAngle(TestCase): 11 | 12 | def test_angle_flatten(self): 13 | def check_seg_in_list(angle, flatten, to_check): 14 | segments = flatten[angle].lines 15 | projected = to_check.get_projected_segment(flatten[angle].norm) 16 | for segment in segments: 17 | if projected == segment: 18 | return True 19 | return False 20 | angleDict = {} 21 | segments = [] 22 | for i in range(0, 16, 2): 23 | segments.append(LineSegment((i, i), (i+1, i+1))) 24 | 25 | uv = segments[0].unit_vector 26 | lines = [segments[0],segments[1]] 27 | ok = LineGroup(uv, None, segments) 28 | angle_ok = 0.0 29 | angleDict[angle_ok] = ok 30 | 31 | uv = segments[2].unit_vector 32 | lines = [segments[2],segments[3]] 33 | to_flat = LineGroup(uv, None, lines) 34 | angle_flatten = angle_ok+minRadAngle 35 | angleDict[angle_flatten] = to_flat 36 | 37 | uv = segments[4].unit_vector 38 | lines = [segments[4],segments[5]] 39 | ok2 = LineGroup(uv, None, lines) 40 | angle_ok_2 = angle_flatten+0.1 41 | angleDict[angle_ok_2] = ok2 42 | 43 | uv = segments[6].unit_vector 44 | lines = [segments[6],segments[7]] 45 | not_flatten = LineGroup(uv, None, lines) 46 | 47 | angle_not_flatten = angle_ok_2+minRadAngle+0.2 48 | angleDict[angle_not_flatten] = not_flatten 49 | flatten = flatten_angles(angleDict) 50 | 51 | self.assertNotIn(angle_flatten, flatten) 52 | self.assertIn(angle_ok, flatten) 53 | self.assertIn(angle_ok_2, flatten) 54 | self.assertIn(angle_not_flatten, flatten) 55 | 56 | to_check = flatten[angle_ok].lines 57 | self.assertIn(ok.lines[0], to_check) 58 | 59 | segment = to_flat.lines[0] 60 | self.assertTrue(check_seg_in_list(angle_ok, flatten, segment)) 61 | self.assertIn(ok.lines[0], to_check) 62 | segment = to_flat.lines[1] 63 | self.assertTrue(check_seg_in_list(angle_ok,flatten, segment)) 64 | 65 | to_check = flatten[angle_ok_2].lines 66 | self.assertIn(ok2.lines[0], to_check) 67 | self.assertIn(ok2.lines[1], to_check) 68 | 69 | to_check = flatten[angle_not_flatten].lines 70 | self.assertIn(not_flatten.lines[0], to_check) 71 | self.assertIn(not_flatten.lines[1], to_check) 72 | -------------------------------------------------------------------------------- /test/testLineSegment.py: -------------------------------------------------------------------------------- 1 | import math 2 | from unittest import TestCase 3 | from boxDetector.lineSegment import LineSegment 4 | 5 | 6 | class TestLineSegment(TestCase): 7 | 8 | def test_right_order(self): 9 | """ 10 | Test that a line segment instantiate the original order if both A and B are on the right orer 11 | """ 12 | segment = LineSegment((0, 0), (5, 0)) 13 | self.assertEqual(segment.a, (0, 0)) 14 | self.assertEqual(segment.b, (5, 0)) 15 | 16 | def test_A_x_after(self): 17 | """ 18 | Test that a line segment instantiate the inverser order if A.x is after b 19 | """ 20 | a = (1, 0) 21 | b = (0, 0) 22 | segment = LineSegment(a, b) 23 | self.assertEqual(segment.a, b) 24 | self.assertEqual(segment.b, a) 25 | 26 | def test_A_y_after(self): 27 | """ 28 | Test that a line segment instantiate the inverser order if A.x is after b 29 | """ 30 | a = (0, 1) 31 | b = (0, 0) 32 | segment = LineSegment(a, b) 33 | self.assertEqual(segment.a, b) 34 | self.assertEqual(segment.b, a) 35 | 36 | def test_vector_creation(self): 37 | """ 38 | Test if the vetor is actualy created 39 | """ 40 | a = (1, 1) 41 | b = (10, 2) 42 | segment = LineSegment(a, b) 43 | vector = [b[0]-a[0], b[1]-a[1]] 44 | self.assertEqual(segment.x, vector[0]) 45 | self.assertEqual(segment.y, vector[1]) 46 | length = (vector[0]**2 + vector[1]**2)**0.5 47 | self.assertEqual(segment.length, length) 48 | 49 | def test_unit_vector(self): 50 | """ 51 | Test that a line segment makes the right unit vector 52 | """ 53 | a = (0, 5) 54 | b = (10, 10) 55 | segment = LineSegment(a, b) 56 | unit = [segment.x/segment.length, segment.y/segment.length] 57 | self.assertEqual(segment.unit_vector, unit) 58 | 59 | def test_default_angle(self): 60 | """ 61 | The angle shold by default test the vector with (1,0) 62 | """ 63 | a = (0, 0) 64 | b = (0, 10) 65 | segment = LineSegment(a, b) 66 | angle = segment.get_angle() 67 | expected_angle = round(math.pi/2, segment.angle_precision) 68 | self.assertEqual(angle, expected_angle) 69 | 70 | def test_add_segment_before(self): 71 | a = (5, 5) 72 | b = (10, 10) 73 | c = (0, 0) 74 | d = (4, 4) 75 | segment = LineSegment(a, b) 76 | to_add = LineSegment(c, d) 77 | segment.add_segment(to_add) 78 | self.assertEqual(segment.a, c) 79 | self.assertEqual(segment.b, b) 80 | 81 | def test_add_segment_after(self): 82 | c = (5, 5) 83 | d = (10, 10) 84 | a = (0, 0) 85 | b = (4, 4) 86 | segment = LineSegment(a, b) 87 | to_add = LineSegment(c, d) 88 | segment.add_segment(to_add) 89 | self.assertEqual(segment.a, a) 90 | self.assertEqual(segment.b, d) 91 | 92 | def test_add_segment_middle(self): 93 | a = (0, 0) 94 | b = (10, 10) 95 | c = (2, 2) 96 | d = (4, 4) 97 | segment = LineSegment(a, b) 98 | to_add = LineSegment(c, d) 99 | segment.add_segment(to_add) 100 | self.assertEqual(segment.a, a) 101 | self.assertEqual(segment.b, b) 102 | -------------------------------------------------------------------------------- /lineSegment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | import numpy as np 3 | 4 | def vector_length(vector): 5 | return (vector[0]**2+vector[1]**2)**0.5 6 | 7 | class LineSegment: 8 | 9 | """Represents a line segment on a pixel plane 10 | 11 | Since we are handling pixels there all coordinates are discrete(int) 12 | 13 | Attributes: 14 | a (Tuple(int,int)): start point nearest to the origin 15 | b (Tuple(int,int)): end point furthest to the origin 16 | vector (Tuple(int,int)): the vector itself b-a 17 | x (int): the vector starting point 18 | y (int): The vector end point 19 | unit_vector Tuple(int,int)): the unit vector 20 | length (int): the size of the vector sqrt(x²+y²) 21 | angle_precision (int): decimal point precision,defaults to 4 22 | """ 23 | 24 | __slots__ = ['a', 'b', 'x', 'y', 'vector', 25 | 'unit_vector', 'length', 'angle_precision','dist_to_origin'] 26 | 27 | def __init__(self, point_a, point_b, angle_precision=4): 28 | """ 29 | Args: 30 | point_a (Tuple(int,int)): Start point 31 | point_b (Tuple(int,int)): End point 32 | angle_precision (int): decimal point precision,defaults to 4 33 | """ 34 | if point_a[0] > point_b[0] or point_a[1] > point_b[1]: 35 | temp = point_a 36 | point_a = point_b 37 | point_b = temp 38 | self.a = point_a 39 | self.b = point_b 40 | self.angle_precision = angle_precision 41 | self._set_vector() 42 | self.unit_vector = LineSegment.get_unit_vector(self.x, self.y) 43 | 44 | def _set_vector(self): 45 | self.vector = [self.b[0]-self.a[0], self.b[1]-self.a[1]] 46 | self.x = self.vector[0] 47 | self.y = self.vector[1] 48 | self.length = vector_length(self.vector) 49 | self.dist_to_origin = vector_length(self.a) 50 | 51 | 52 | 53 | @staticmethod 54 | def get_unit_vector(x, y): 55 | """Gets the unit vector of a vector 56 | 57 | Args: 58 | x (int): x coordinate 59 | y (int): y coordinate 60 | """ 61 | vector = [x, y] 62 | length = np.linalg.norm(vector) 63 | if(length <= 0.0): 64 | return None 65 | norm = vector / length 66 | return [norm[0], norm[1]] 67 | 68 | def get_angle(self, relative_to=(1, 0)): 69 | """ get the angle between this segment and some vector 70 | Args: 71 | relative_to (ArrayLike): optional defaults to 1,0 72 | """ 73 | v2_u = relative_to 74 | if relative_to != (1, 0): 75 | v2_u = LineSegment.get_unit_vector(relative_to[0], relative_to[1]) 76 | # Not using the length since we are using both unit vectors 77 | angle = np.arccos(abs(np.dot(self.unit_vector, v2_u))) 78 | if np.isnan(angle): 79 | return 0.0 80 | return round(angle, self.angle_precision) 81 | 82 | def __repr__(self): 83 | text = "[(%d,%d),(%d,%d)] = (%d,%d) %dpx" 84 | vals = (self.a[0], self.a[1], self.b[0], 85 | self.b[1], self.x, self.y, self.length) 86 | return text % vals 87 | 88 | def __eq__(self, other): 89 | return self.a == other.a and self.b == other.b 90 | 91 | def __hash__(self): 92 | return (self.a[0], self.a[1], self.b[0], self.b[1]).__hash__() 93 | 94 | def __lt__(self, other): 95 | return self.dist_to_origin vector_length(self.b): 113 | self.b = segment.b 114 | self._set_vector() 115 | -------------------------------------------------------------------------------- /checkBoxDetector.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | """Box detector 3 | This module has functions to determine if a contour ressembles a box 4 | 5 | A box is considered something that has 2 sets of paralel line segments, 6 | perpendicular to each other and each paralel segment is roughly 7 | the same size 8 | """ 9 | from collections import namedtuple 10 | from boxDetector.lineSegment import LineSegment as Segment 11 | import numpy as np 12 | import cv2 13 | 14 | LineGroup = namedtuple("LineGroup", "norm pivotPoint lines") 15 | Checkbox = namedtuple("Checkbox", "a b c d") 16 | 17 | def build_segments_angle_map(points, angle_precision=4, min_size=5): 18 | """ Given a list of points build a line segment list 19 | 20 | All segments are compared to [0,1] and it's angle is used as key 21 | The vector of the 1st segment on a angle becames the pivotPoint 22 | all angles are indexed by distance with the pivot 23 | 24 | Args: 25 | points (List[Tuple(int,int)]): list of points on contour 26 | angle_precision (int): decimal point precision,defaults to 4 27 | min_size (int) : minimun segment size to consider 28 | """ 29 | 30 | def add_segment(segment,angle_map): 31 | angle = segment.get_angle() 32 | angle = round(angle, angle_precision) 33 | if angle in angle_map: 34 | angle_map[angle].lines.append(segment) 35 | else: 36 | norm = segment.unit_vector 37 | pivotPoint = segment.vector 38 | angle_map[angle] = LineGroup( 39 | norm, pivotPoint, [segment]) 40 | 41 | angle_segment_map = dict() 42 | ref_point = points[0] 43 | to_add = False 44 | segment = None 45 | # Indexing all line segments by angle in relation to a randle vector 46 | for i in range(1, len(points)): 47 | next_point = points[i] 48 | segment = Segment( 49 | ref_point, next_point, angle_precision=angle_precision) 50 | if segment.length < min_size: 51 | to_add = True 52 | continue 53 | to_add = False 54 | ref_point = next_point 55 | add_segment(segment,angle_segment_map) 56 | if to_add: 57 | add_segment(segment,angle_segment_map) 58 | return angle_segment_map 59 | 60 | 61 | def getRadAngle(degreeAngle): 62 | radAngle = (degreeAngle*np.pi)/180 63 | return round(radAngle, 4) 64 | 65 | def flatten_angles(angle_map, rad_angle_epsilon=8): 66 | """Concat similar angles together 67 | Args: 68 | angle_map (dict): list of something indexed by angles 69 | rad_angle_epsilon (float): how much of the difference to squash in rad 70 | """ 71 | ret = {} 72 | last_angle = None 73 | 74 | for angle in sorted(angle_map): 75 | if last_angle is None: 76 | last_angle = angle 77 | if angle - last_angle > rad_angle_epsilon: 78 | last_angle = angle 79 | if last_angle in ret: 80 | current_lines = angle_map[angle].lines 81 | norm = ret[last_angle].norm 82 | for segment in current_lines: 83 | projected = segment.get_projected_segment(norm) 84 | ret[last_angle].lines.append(projected) 85 | else: 86 | ret[last_angle] = angle_map[angle] 87 | return ret 88 | 89 | 90 | def flatten_line_segment(segments,norm, gap_epsilon=10): 91 | """Join multiple distance segments that are on the same gap_epsilon 92 | Args: 93 | segments (List): List of all segments 94 | gap_epsilon: what is the gap_epsilon to merge 95 | """ 96 | def get_distance(point,norm): 97 | return abs(np.cross(norm,point)) 98 | 99 | dist_dict = {} 100 | last_distance = None 101 | distance_segment_map = {} 102 | segments.sort() 103 | base = segments[0] 104 | for i in range(1, len(segments)): 105 | segment = segments[i] 106 | gap = [base.b[0]-segment.a[0], base.b[1]-segment.a[1]] 107 | dist = (gap[0]**2+gap[1]**2)**0.5 108 | if dist < gap_epsilon: 109 | base.add_segment(segment) 110 | else: 111 | distance = get_distance(base.a,norm) 112 | dist_dict[distance] = base 113 | base = segment 114 | distance = get_distance(base.a,norm) 115 | dist_dict[distance] = base 116 | return dist_dict 117 | 118 | def is_boxy(points, angle_epsilon=8, parallel_epsilon=0.5, min_size=5, gap_epsilon=10): 119 | """Wheter a point set resemble a box 120 | Args: 121 | angle_epsilon: angle diff that can be merged 122 | parallel_epsilon: how much can parallel lines differ 123 | min_size: min size that a segment must have 124 | gap_epsilon: min gap before merging segments 125 | """ 126 | angle_epsilon = getRadAngle(angle_epsilon) 127 | 128 | angle_map = build_segments_angle_map(points, min_size=min_size) 129 | flatted = flatten_angles(angle_map, rad_angle_epsilon=angle_epsilon) 130 | ninety_degree = np.pi/2 131 | angles_to_check = [] 132 | 133 | for angle in flatted: 134 | flated_lines = flatten_line_segment( 135 | flatted[angle].lines,flatted[angle].norm, gap_epsilon=gap_epsilon) 136 | has_parallel = False 137 | length_to_compare = None 138 | for distance in flated_lines: 139 | if flated_lines[distance].length < min_size: 140 | continue 141 | if not length_to_compare: 142 | length_to_compare = flated_lines[distance].length 143 | else: 144 | size_diff = 0.0 145 | if length_to_compare > flated_lines[distance].length: 146 | size_diff = flated_lines[distance].length/length_to_compare 147 | else: 148 | size_diff = length_to_compare/flated_lines[distance].length 149 | length_to_compare = flated_lines[distance].length 150 | if (1-size_diff) < parallel_epsilon: 151 | has_parallel = True 152 | break 153 | 154 | if has_parallel: 155 | angles_to_check.append(angle) 156 | for angle in angles_to_check: 157 | start = 1 158 | for i, next_angle in enumerate(angles_to_check, start): 159 | if (ninety_degree-angle_epsilon) <= abs(angle-next_angle) <= (ninety_degree+angle_epsilon): 160 | return True 161 | start += 1 162 | return False 163 | 164 | def formatBox(box): 165 | ret =[] 166 | for item in box: 167 | ret.append((item[0].item(),item[1].item())) 168 | return ret 169 | 170 | def get_filled_contours(img_path,min_rect_size = 30): 171 | filled_coordinates = [] 172 | img = cv2.imread(img_path) 173 | #Make it gray 174 | imgray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) 175 | #reducing noise 176 | ret,thresh = cv2.threshold(imgray,127,255,0) 177 | _,contours, hierarchy = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) 178 | #hierarchy fix and skipping outer container 179 | hierarchy = hierarchy[0][1:] 180 | #the first one is always the outer container skipping it 181 | contours = contours[1:] 182 | for i,cnt in enumerate(contours): 183 | #[0] = next contour at the same hierarchical level 184 | #[1] = previous contour at the same hierarchical level 185 | #[2] = denotes its first child contour 186 | #[3] = denotes index of its parent contour 187 | child_index = hierarchy[i][2] 188 | isClosedShape = child_index != -1 189 | if isClosedShape: 190 | # -1 because we skipped the outer contour 191 | firtstChildHierarchy = hierarchy[child_index-1] 192 | isFilled = firtstChildHierarchy[0]!=-1 193 | if isFilled: 194 | #Finds the minimun area rectangle for the contour 195 | rect = cv2.minAreaRect(cnt) 196 | box = cv2.boxPoints(rect) 197 | width = box[3][0] - box[0][0] 198 | if width< min_rect_size: 199 | continue 200 | contour_list=list(map(lambda x: x[0], cnt)) 201 | if not is_boxy(contour_list): 202 | continue 203 | filled_coordinates.append(formatBox(box)) 204 | return filled_coordinates --------------------------------------------------------------------------------