├── data
├── 1.jpg
├── 1.png
└── 2.jpg
├── README.md
├── LICENSE
├── .gitignore
├── detection.py
└── 代码讲解.md
/data/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keefeWu/QRCode/HEAD/data/1.jpg
--------------------------------------------------------------------------------
/data/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keefeWu/QRCode/HEAD/data/1.png
--------------------------------------------------------------------------------
/data/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keefeWu/QRCode/HEAD/data/2.jpg
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # QRCode
2 | QR CODE detection
3 | 大家如果有什么新的想法可以一起完善这个项目,欢迎pull request
4 | 代码详解在
5 | https://blog.357573.com/2020/07/03/opencv实现二维码检测/
6 | 另外还有视频讲解
7 | https://www.bilibili.com/video/BV1yz411e7kQ
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 KeefeNG
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 |
--------------------------------------------------------------------------------
/.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 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
--------------------------------------------------------------------------------
/detection.py:
--------------------------------------------------------------------------------
1 | #coding:utf-8
2 | import cv2
3 | import numpy as np
4 | import math
5 | def show(img, name = 'img'):
6 | maxHeight = 540
7 | maxWidth = 960
8 | scaleX = maxWidth / img.shape[1]
9 | scaleY = maxHeight / img.shape[0]
10 | scale = min(scaleX, scaleY)
11 | if scale < 1:
12 | img = cv2.resize(img,(0,0),fx=scale, fy=scale)
13 | cv2.imshow(name,img)
14 | cv2.waitKey(0)
15 | cv2.destroyWindow(name)
16 |
17 |
18 | def convert_img_to_binary(img):
19 | gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
20 | binary_img = cv2.adaptiveThreshold(
21 | gray,
22 | 255, # Value to assign
23 | cv2.ADAPTIVE_THRESH_MEAN_C,# Mean threshold
24 | cv2.THRESH_BINARY,
25 | 11, # Block size of small area
26 | 2, # Const to substract
27 | )
28 | return binary_img
29 |
30 | def getContours(img):
31 | binary_img = convert_img_to_binary(img)
32 | kernel = np.ones((5, 5), np.uint8)
33 | binary_img = cv2.erode(binary_img, kernel)
34 | # thresholdImage = binary_img
35 | thresholdImage = cv2.Canny(binary_img, 100, 200) #Edges by canny edge detection
36 |
37 | _, contours, hierarchy = cv2.findContours(
38 | thresholdImage, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
39 | return thresholdImage, contours, hierarchy
40 |
41 | def checkRatioOfContours(index, contours, hierarchy):
42 | firstChildIndex = hierarchy[0][index][2]
43 | secondChildIndex = hierarchy[0][firstChildIndex][2]
44 | firstArea = cv2.contourArea(contours[index]) / (
45 | cv2.contourArea(contours[firstChildIndex]) + 1e-5)
46 | secondArea = cv2.contourArea(contours[firstChildIndex]) / (
47 | cv2.contourArea(contours[secondChildIndex]) + 1e-5)
48 | return ((firstArea / (secondArea+ 1e-5)) > 1 and \
49 | ((firstArea / (secondArea+ 1e-5)) < 10))
50 |
51 | def isPossibleCorner(contourIndex, levelsNum, contours, hierarchy):
52 | # if no chirld, return -1
53 | chirldIdx = hierarchy[0][contourIndex][2]
54 | level = 0
55 | while chirldIdx != -1:
56 | level += 1
57 | chirldIdx = hierarchy[0][chirldIdx][2]
58 | if level >= levelsNum:
59 | return checkRatioOfContours(contourIndex, contours, hierarchy)
60 | return False
61 |
62 | def getContourWithinLevel(levelsNum, contours, hierarchy):
63 | # find contours has 3 levels
64 | patterns = []
65 | patternsIndices = []
66 | for contourIndex in range(len(contours)):
67 | if isPossibleCorner(contourIndex, levelsNum, contours, hierarchy):
68 | patterns.append(contours[contourIndex])
69 | patternsIndices.append(contourIndex)
70 | return patterns, patternsIndices
71 |
72 | def isParentInList(intrestingPatternIdList, index, hierarchy):
73 | parentIdx = hierarchy[0][index][3]
74 | while (parentIdx != -1) and (parentIdx not in intrestingPatternIdList):
75 | parentIdx = hierarchy[0][parentIdx][3]
76 | return parentIdx != -1
77 |
78 | def getOrientation(contours, centerOfMassList):
79 | distance_AB = np.linalg.norm(centerOfMassList[0].flatten() - centerOfMassList[1].flatten(), axis = 0)
80 | distance_BC = np.linalg.norm(centerOfMassList[1].flatten() - centerOfMassList[2].flatten(), axis = 0)
81 | distance_AC = np.linalg.norm(centerOfMassList[0].flatten() - centerOfMassList[2].flatten(), axis = 0)
82 |
83 | largestLine = np.argmax(
84 | np.array([distance_AB, distance_BC, distance_AC]))
85 | bottomLeftIdx = 0
86 | topLeftIdx = 1
87 | topRightIdx = 2
88 | if largestLine == 0:
89 | bottomLeftIdx, topLeftIdx, topRightIdx = 0, 2, 1
90 | if largestLine == 1:
91 | bottomLeftIdx, topLeftIdx, topRightIdx = 1, 0, 2
92 | if largestLine == 2:
93 | bottomLeftIdx, topLeftIdx, topRightIdx = 0, 1, 2
94 |
95 | # distance between point to line:
96 | # abs(Ax0 + By0 + C)/sqrt(A^2+B^2)
97 | slope = (centerOfMassList[bottomLeftIdx][1] - centerOfMassList[topRightIdx][1]) / (centerOfMassList[bottomLeftIdx][0] - centerOfMassList[topRightIdx][0] + 1e-5)
98 | # y = kx + b => AX + BY +C = 0 => B = 1, A = -k, C = -b
99 | coefficientA = -slope
100 | coefficientB = 1
101 | constant = slope * centerOfMassList[bottomLeftIdx][0] - centerOfMassList[bottomLeftIdx][1]
102 | distance = (coefficientA * centerOfMassList[topLeftIdx][0] + coefficientB * centerOfMassList[topLeftIdx][1] + constant) / (
103 | np.sqrt(coefficientA ** 2 + coefficientB ** 2))
104 |
105 |
106 | pointList = np.zeros(shape=(3,2))
107 | # 回 回 tl bl
108 | if (slope >= 0) and (distance >= 0):
109 | # if slope and distance are positive A is bottom while B is right
110 | if (centerOfMassList[bottomLeftIdx][0] > centerOfMassList[topRightIdx][0]):
111 | pointList[1] = centerOfMassList[bottomLeftIdx]
112 | pointList[2] = centerOfMassList[topRightIdx]
113 | else:
114 | pointList[1] = centerOfMassList[topRightIdx]
115 | pointList[2] = centerOfMassList[bottomLeftIdx]
116 | # TopContour in the SouthWest of the picture
117 | ORIENTATION = "SouthWest"
118 |
119 | # 回 回 bl tl
120 | #
121 | # 回 tr
122 | elif (slope > 0) and (distance < 0):
123 | # if slope is positive and distance is negative then B is bottom
124 | # while A is right
125 | if (centerOfMassList[bottomLeftIdx][1] > centerOfMassList[topRightIdx][1]):
126 | pointList[2] = centerOfMassList[bottomLeftIdx]
127 | pointList[1] = centerOfMassList[topRightIdx]
128 | else:
129 | pointList[2] = centerOfMassList[topRightIdx]
130 | pointList[1] = centerOfMassList[bottomLeftIdx]
131 | ORIENTATION = "NorthEast"
132 |
133 |
134 | # 回 bl
135 | #
136 | # 回 回 tr tl
137 | elif (slope < 0) and (distance > 0):
138 | if (centerOfMassList[bottomLeftIdx][0] > centerOfMassList[topRightIdx][0]):
139 | pointList[1] = centerOfMassList[bottomLeftIdx]
140 | pointList[2] = centerOfMassList[topRightIdx]
141 | else:
142 | pointList[1] = centerOfMassList[topRightIdx]
143 | pointList[2] = centerOfMassList[bottomLeftIdx]
144 | ORIENTATION = "SouthEast"
145 | # 回 回 tl tr
146 | #
147 | # 回 bl
148 | elif (slope < 0) and (distance < 0):
149 |
150 | if (centerOfMassList[bottomLeftIdx][0] > centerOfMassList[topRightIdx][0]):
151 | pointList[2] = centerOfMassList[bottomLeftIdx]
152 | pointList[1] = centerOfMassList[topRightIdx]
153 | else:
154 | pointList[2] = centerOfMassList[topRightIdx]
155 | pointList[1] = centerOfMassList[bottomLeftIdx]
156 | pointList[0] = centerOfMassList[topLeftIdx]
157 | return pointList
158 |
159 | def getCenterOfMass(contours):
160 | pointList = []
161 | for i in range(len(contours)):
162 | moment = cv2.moments(contours[i])
163 | centreOfMassX = int(moment['m10'] / moment['m00'])
164 | centreOfMassY = int(moment['m01'] / moment['m00'])
165 | pointList.append([centreOfMassX, centreOfMassY])
166 | return pointList
167 |
168 | def lineAngle(line1, line2):
169 | return math.acos((line1[0] * line2[0] + line1[1] * line2[1]) /
170 | (np.linalg.norm(line1, axis = 0) * np.linalg.norm(line2, axis = 0)))
171 |
172 | def selectPatterns(pointList):
173 | lineList = []
174 | for i in range(len(pointList)):
175 | for j in range(i, len(pointList)):
176 | lineList.append([i, j])
177 | finalLineList = []
178 | finalResult = None
179 | minLengthDiff = -1
180 | for i in range(len(lineList)):
181 | for j in range(i, len(lineList)):
182 | line1 = np.array([pointList[lineList[i][0]][0] - pointList[lineList[i][1]][0],
183 | pointList[lineList[i][0]][1] - pointList[lineList[i][1]][1]])
184 | line2 = np.array([pointList[lineList[j][0]][0] - pointList[lineList[j][1]][0],
185 | pointList[lineList[j][0]][1] - pointList[lineList[j][1]][1]])
186 | pointIdxList = np.array([lineList[i][0], lineList[i][1], lineList[j][0], lineList[j][1]])
187 | pointIdxList = np.unique(pointIdxList)
188 | # print('****')
189 | if len(pointIdxList) == 3:
190 | theta = lineAngle(line1, line2)
191 | if abs(math.pi / 2 - theta) < math.pi / 6:
192 | lengthDiff = abs(np.linalg.norm(line1, axis = 0) - np.linalg.norm(line2, axis = 0))
193 | if lengthDiff < minLengthDiff or minLengthDiff < 0:
194 | minLengthDiff = abs(np.linalg.norm(line1, axis = 0) - np.linalg.norm(line2, axis = 0))
195 | finalResult = pointIdxList
196 |
197 |
198 | return finalResult
199 |
200 | def main():
201 | path = 'data/2.jpg'
202 |
203 | img = cv2.imread(path)
204 | show(img)
205 | thresholdImage, contours, hierarchy = getContours(img)
206 | img_show = cv2.cvtColor(thresholdImage, cv2.COLOR_GRAY2BGR)
207 | cv2.drawContours(img_show, contours, -1, (0,255,0), 3)
208 | show(img_show)
209 |
210 |
211 | # img_show = cv2.cvtColor(thresholdImage, cv2.COLOR_GRAY2BGR)
212 | # # cv2.drawContours(img_show, contours[areaIdList[-9]], -1, (0,255,0), 3)
213 | # cv2.drawContours(img_show, contours[2423], -1, (0,255,0), 3)
214 | # show(img_show)
215 |
216 |
217 | # qrcode corner has 3 levels
218 | levelsNum = 3
219 | patterns, patternsIndices = getContourWithinLevel(levelsNum, contours, hierarchy)
220 | img_show = cv2.cvtColor(thresholdImage, cv2.COLOR_GRAY2BGR)
221 | cv2.drawContours(img_show, patterns, -1, (0,255,0), 3)
222 | show(img_show)
223 | #in case not all the picture has clear pattern
224 | while len(patterns) < 3 and levelsNum > 0:
225 | levelsNum -= 1
226 | patterns, patternsIndices = getContourWithinLevel(levelsNum, contours, hierarchy)
227 |
228 | interstingPatternList = []
229 | if len(patterns) < 3 :
230 | print('no enough pattern')
231 | return False, []
232 | # return False
233 |
234 | elif len(patterns) == 3:
235 | for patternIndex in range(len(patterns)):
236 | x, y, w, h = cv2.boundingRect(patterns[patternIndex])
237 | interstingPatternList.append(patterns[patternIndex])
238 |
239 | cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
240 | show(img, 'qrcode')
241 | # return patterns
242 |
243 | elif len(patterns) > 3:
244 | # sort from small to large
245 | patternAreaList = np.array(
246 | [cv2.contourArea(pattern) for pattern in patterns])
247 | areaIdList = np.argsort(patternAreaList)
248 | # get patterns without parents
249 | intrestingPatternIdList = []
250 | for i in range(len(areaIdList) - 1, 0, -1):
251 | index = patternsIndices[areaIdList[i]]
252 | if hierarchy[0][index][3] == -1:
253 | intrestingPatternIdList.append(index)
254 | else:
255 | # We can make sure the parent must appear before chirld because we sorted the list by area
256 | if not isParentInList(intrestingPatternIdList, index, hierarchy):
257 | intrestingPatternIdList.append(index)
258 | img_show = cv2.cvtColor(thresholdImage, cv2.COLOR_GRAY2BGR)
259 | for intrestingPatternId in intrestingPatternIdList:
260 | x, y, w, h = cv2.boundingRect(contours[intrestingPatternId])
261 |
262 | cv2.rectangle(img_show, (x, y), (x + w, y + h), (0, 255, 0), 2)
263 | interstingPatternList.append(contours[intrestingPatternId])
264 | show(img_show, 'qrcode')
265 |
266 | img_show = cv2.cvtColor(thresholdImage, cv2.COLOR_GRAY2BGR)
267 | # cv2.drawContours(img_show, interstingPatternList, -1, (0,255,0), 3)
268 | # cv2.imwrite('/home/jiangzhiqi/Documents/blog/keefeWu.github.io/source/_posts/opencv实现二维码检测/contours3.jpg', img_show)
269 | centerOfMassList = getCenterOfMass(interstingPatternList)
270 | for centerOfMass in centerOfMassList:
271 | cv2.circle(img_show, tuple(centerOfMass), 3, (0, 255, 0))
272 | show(img_show, 'qrcode')
273 | id1, id2, id3 = 0, 1, 2
274 | if len(patterns) > 3:
275 | result = selectPatterns(centerOfMassList)
276 | if result is None:
277 | print('no correct pattern')
278 | return False, []
279 | id1, id2, id3 = result
280 | interstingPatternList = np.array(interstingPatternList)[[id1, id2, id3]]
281 | centerOfMassList = np.array(centerOfMassList)[[id1, id2, id3]]
282 | pointList = getOrientation(interstingPatternList, centerOfMassList)
283 | img_show = img.copy()
284 | for point in pointList:
285 | cv2.circle(img_show, tuple([int(point[0]), int(point[1])]), 10, (0, 255, 0), -1)
286 | # cv2.imwrite('/home/jiangzhiqi/Documents/blog/keefeWu.github.io/source/_posts/opencv实现二维码检测/result.jpg', img_show)
287 | point = pointList[0]
288 | cv2.circle(img_show, tuple([int(point[0]), int(point[1])]), 10, (0, 0, 255), -1)
289 |
290 | show(img_show)
291 | return True, pointList
292 | # contours
293 |
294 | main()
295 |
--------------------------------------------------------------------------------
/代码讲解.md:
--------------------------------------------------------------------------------
1 | 我用的是找轮廓的方法,根据轮廓层级,找到3层的轮廓,然后作为候选,就不去数点确认比例了.
2 |
3 | ### getContours
4 | 看我的步骤
5 | ```
6 | path = 'data/qrcode.jpg'
7 | img = cv2.imread(path)
8 | thresholdImage, contours, hierarchy = getContours(img)
9 | ```
10 | 首先把轮廓提取出来
11 | ```
12 | def convert_img_to_binary(img):
13 | gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
14 | binary_img = cv2.adaptiveThreshold(
15 | gray,
16 | 255, # Value to assign
17 | cv2.ADAPTIVE_THRESH_MEAN_C,# Mean threshold
18 | cv2.THRESH_BINARY,
19 | 11, # Block size of small area
20 | 2, # Const to substract
21 | )
22 | return binary_img
23 |
24 | def getContours(img):
25 | binary_img = convert_img_to_binary(img)
26 | thresholdImage = cv2.Canny(binary_img, 100, 200) #Edges by canny edge detection
27 | _, contours, hierarchy = cv2.findContours(
28 | thresholdImage, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
29 | return thresholdImage, contours, hierarchy
30 | ```
31 | 把图片预处理了一下,首先二值化,然后做一下canny提取边缘信息,就成了这样
32 | 
33 | ,然后调opencv的findContours函数,找到轮廓,
34 | 
35 | 结果发现全是轮廓,哈哈,不急,我们慢慢挑,最重要的是最后那个返回值,也就是层级关系.这个是我们算法的核心信息.
36 |
37 | 然后我们就要开始利用这个层级关系了,因为二维码轮廓是黑白黑三层的,所以我们搜索所有三层的轮廓
38 | ```
39 | # qrcode corner has 3 levels
40 | levelsNum = 3
41 | patterns, patternsIndices = getContourWithinLevel(levelsNum, contours, hierarchy)
42 | ```
43 | 看一下这个怎么搜索的
44 | ### getContourWithinLevel
45 | ```
46 | def isPossibleCorner(contourIndex, levelsNum, contours, hierarchy):
47 | # if no chirld, return -1
48 | chirldIdx = hierarchy[0][contourIndex][2]
49 | level = 0
50 | while chirldIdx != -1:
51 | level += 1
52 | chirldIdx = hierarchy[0][chirldIdx][2]
53 | if level >= levelsNum:
54 | return checkRatioOfContours(contourIndex, contours, hierarchy)
55 | return False
56 |
57 | def getContourWithinLevel(levelsNum, contours, hierarchy):
58 | # find contours has 3 levels
59 | patterns = []
60 | patternsIndices = []
61 | for contourIndex in range(len(contours)):
62 | if isPossibleCorner(contourIndex, levelsNum, contours, hierarchy):
63 | patterns.append(contours[contourIndex])
64 | patternsIndices.append(contourIndex)
65 | return patterns, patternsIndices
66 | ```
67 | 也就是找到有3层子轮廓的轮廓,把它返回回来,其中hierarchy这个对象,固定的shape是1,n,4其中n是轮廓的数量,例如我们这个例子就是(1, 1253, 4),最后那个4个维度代表下一个轮廓id,上一个轮廓id,子轮廓id,父轮廓id如果没有的话统统都是-1,所以我们这里获得hierarchy[0][contourIndex][2]就是获取了子轮廓的id,然后就数连着3级都有子轮廓就存下来,但是也不是都存下来了,因为最后还有一个checkRatioOfContours函数,这个是防止子轮廓大小比例异常
68 | ```
69 | def checkRatioOfContours(index, contours, hierarchy):
70 | firstChildIndex = hierarchy[0][index][2]
71 | secondChildIndex = hierarchy[0][firstChildIndex][2]
72 | firstArea = cv2.contourArea(contours[index]) / (
73 | cv2.contourArea(contours[firstChildIndex]) + 1e-5)
74 | secondArea = cv2.contourArea(contours[firstChildIndex]) / (
75 | cv2.contourArea(contours[secondChildIndex]) + 1e-5)
76 | return ((firstArea / (secondArea+ 1e-5)) > 1 and \
77 | ((firstArea / (secondArea+ 1e-5)) < 10))
78 | ```
79 | 所以我比较了一下,父轮廓的大小要在子轮廓的1-10倍之间,不然就算噪声排除掉了.
80 | 现在看看还剩下多少轮廓
81 | 
82 | 这样一挑,就只剩下8个轮廓了.
83 | 如果图片特别模糊,看不清是3个层级怎么办,我们后续也加了一步
84 | ```
85 | #in case not all the picture has clear pattern
86 | while len(patterns) < 3 and levelsNum > 0:
87 | levelsNum -= 1
88 | patterns, patternsIndices = getContourWithinLevel(levelsNum, contours, hierarchy)
89 | ```
90 | 如果找到的关键点还不够3个,那么我们就减小层级,这样假设太小了的,三层只能看见两层的我们也能找到,所以我们逐步缩小了层级,直到找够3个为止
91 | 找到之后就开始处理了,如果此时还不3个就直接宣告GG吧,也不浪费时间了
92 | ```
93 | interstingPatternList = []
94 | if len(patterns) < 3 :
95 | print('no enough pattern')
96 | return False, []
97 | # return False
98 | ```
99 | 如果刚好找到了3个,那就把这3个都作为感兴趣的轮廓加进去
100 | ```
101 | elif len(patterns) == 3:
102 | for patternIndex in range(len(patterns)):
103 | x, y, w, h = cv2.boundingRect(patterns[patternIndex])
104 | interstingPatternList.append(patterns[patternIndex])
105 |
106 | cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
107 | show(img, 'qrcode')
108 | # return patterns
109 | ```
110 | 如果比3个多,我们就要把父轮廓提取出来,刚才那8个都是一个父轮廓带着一个子轮廓的,现在我们要把这个父轮廓提取出来,子轮廓就不要了.
111 | 我们来挨个判断一下,把那些没有爸爸的加到感兴趣的里面,但是这样有个问题,如果有的有爸爸,但是他的爸爸在早期就被淘汰了,例如有的图整张图片有个超大的圈,所有图案都是外面那个圈的子轮廓,这时候我们就不能从全局去找爸爸了,那该怎么办呢?我们就从这8个待选的轮廓中找爸爸.
112 | ```
113 | elif len(patterns) > 3:
114 | # sort from small to large
115 | patternAreaList = np.array(
116 | [cv2.contourArea(pattern) for pattern in patterns])
117 | areaIdList = np.argsort(patternAreaList)
118 | # get patterns without parents
119 | intrestingPatternIdList = []
120 | for i in range(len(areaIdList) - 1, 0, -1):
121 | index = patternsIndices[areaIdList[i]]
122 | if hierarchy[0][index][3] == -1:
123 | intrestingPatternIdList.append(index)
124 | else:
125 | # We can make sure the parent must appear before chirld because we sorted the list by area
126 | if not isParentInList(intrestingPatternIdList, index, hierarchy):
127 | intrestingPatternIdList.append(index)
128 |
129 | for intrestingPatternId in intrestingPatternIdList:
130 | x, y, w, h = cv2.boundingRect(contours[intrestingPatternId])
131 | cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
132 | interstingPatternList.append(contours[intrestingPatternId])
133 | show(img, 'qrcode')
134 | ```
135 | 我们先给这些轮廓按照大小排个序,这样的话从大到小来判断,就可以优先提取出最大的了,剩下的绝对不可能是前一个的父轮廓,所以我们每个轮廓就判断一下它有没有爸爸已经被我们选中了即可.
136 | 这下我们搜集的所有轮廓都不互为父子了.
137 | 
138 | 我们再看现在剩下的轮廓,有一个在小电视上,那是我们不希望存在的,现在就要想办法把它除掉.
139 | 首先使用距离肯定是不合适的,因为小电视明显和最下面的那个最近,用全部距离的话会把右上角那个点去掉,把小电视保留.
140 | 那么我们就用角度好了,每三个点都找一遍,选出两条垂直并且长度也差不多的线
141 | 现在是轮廓,我们要把它变成点
142 | 首先每个轮廓找到他自己的重心
143 | ```
144 | centerOfMassList = getCenterOfMass(interstingPatternList)
145 | for centerOfMass in centerOfMassList:
146 | cv2.circle(img_show, tuple(centerOfMass), 3, (0, 255, 0))
147 | ```
148 | 结果如下
149 | 
150 | 找重心的函数是
151 | ```
152 | def getCenterOfMass(contours):
153 | pointList = []
154 | for i in range(len(contours)):
155 | moment = cv2.moments(contours[i])
156 | centreOfMassX = int(moment['m10'] / moment['m00'])
157 | centreOfMassY = int(moment['m01'] / moment['m00'])
158 | pointList.append([centreOfMassX, centreOfMassY])
159 | return pointList
160 | ```
161 |
162 | 我是通过计算图像的矩来找的重心,说白了也就是哪边点多就往哪边偏移,这符合重心的原理.当然直接用轮廓找个重心也是可以的,但是这样噪声影响比较大.
163 | 找到重心之后我们要挑出最终的三个点
164 | ```
165 | id1, id2, id3 = 0, 1, 2
166 | if len(patterns) > 3:
167 | result = selectPatterns(centerOfMassList)
168 | if result is None:
169 | print('no correct pattern')
170 | return False, []
171 | id1, id2, id3 = result
172 | ```
173 | ### selectPatterns
174 | ```
175 | def selectPatterns(pointList):
176 | lineList = []
177 | for i in range(len(pointList)):
178 | for j in range(i, len(pointList)):
179 | lineList.append([i, j])
180 | finalLineList = []
181 | finalResult = None
182 | minLengthDiff = -1
183 | for i in range(len(lineList)):
184 | for j in range(i, len(lineList)):
185 | line1 = np.array([pointList[lineList[i][0]][0] - pointList[lineList[i][1]][0],
186 | pointList[lineList[i][0]][1] - pointList[lineList[i][1]][1]])
187 | line2 = np.array([pointList[lineList[j][0]][0] - pointList[lineList[j][1]][0],
188 | pointList[lineList[j][0]][1] - pointList[lineList[j][1]][1]])
189 | pointIdxList = np.array([lineList[i][0], lineList[i][1], lineList[j][0], lineList[j][1]])
190 | pointIdxList = np.unique(pointIdxList)
191 | # print('****')
192 | if len(pointIdxList) == 3:
193 | theta = lineAngle(line1, line2)
194 | if abs(math.pi / 2 - theta) < math.pi / 6:
195 | lengthDiff = abs(np.linalg.norm(line1, axis = 0) - np.linalg.norm(line2, axis = 0))
196 | if lengthDiff < minLengthDiff or minLengthDiff < 0:
197 | minLengthDiff = abs(np.linalg.norm(line1, axis = 0) - np.linalg.norm(line2, axis = 0))
198 | finalResult = pointIdxList
199 |
200 |
201 | return finalResult
202 | ```
203 | 我的做法是每3个点做两条线段,然后判断线段如果差不多90度,我这里的范围比较宽松,可以浮动30度,也就是说你夹角只要在60-120度之间的都可以进入下一步,下一步就是从所有夹角符合的情况中选出线段长度最接近的,也就是minLengthDiff这一组,恭喜这三个点两条线获胜,就是我们想要的轮廓.
204 |
205 | 最后就是来计算旋转角度了,也就是看这三个点哪个是左下,哪个是左上,哪个是右上
206 | ```
207 | interstingPatternList = np.array(interstingPatternList)[[id1, id2, id3]]
208 | centerOfMassList = np.array(centerOfMassList)[[id1, id2, id3]]
209 | pointList = getOrientation(interstingPatternList, centerOfMassList)
210 | ```
211 | ### getOrientation
212 | ```
213 | def getOrientation(contours, centerOfMassList):
214 | distance_AB = np.linalg.norm(centerOfMassList[0].flatten() - centerOfMassList[1].flatten(), axis = 0)
215 | distance_BC = np.linalg.norm(centerOfMassList[1].flatten() - centerOfMassList[2].flatten(), axis = 0)
216 | distance_AC = np.linalg.norm(centerOfMassList[0].flatten() - centerOfMassList[2].flatten(), axis = 0)
217 |
218 | largestLine = np.argmax(
219 | np.array([distance_AB, distance_BC, distance_AC]))
220 | bottomLeftIdx = 0
221 | topLeftIdx = 1
222 | topRightIdx = 2
223 | if largestLine == 0:
224 | bottomLeftIdx, topLeftIdx, topRightIdx = 0, 2, 1
225 | if largestLine == 1:
226 | bottomLeftIdx, topLeftIdx, topRightIdx = 1, 0, 2
227 | if largestLine == 2:
228 | bottomLeftIdx, topLeftIdx, topRightIdx = 0, 1, 2
229 |
230 | # distance between point to line:
231 | # abs(Ax0 + By0 + C)/sqrt(A^2+B^2)
232 | slope = (centerOfMassList[bottomLeftIdx][1] - centerOfMassList[topRightIdx][1]) / (centerOfMassList[bottomLeftIdx][0] - centerOfMassList[topRightIdx][0] + 1e-5)
233 | # y = kx + b => AX + BY +C = 0 => B = 1, A = -k, C = -b
234 | coefficientA = -slope
235 | coefficientB = 1
236 | constant = slope * centerOfMassList[bottomLeftIdx][0] - centerOfMassList[bottomLeftIdx][1]
237 | distance = (coefficientA * centerOfMassList[topLeftIdx][0] + coefficientB * centerOfMassList[topLeftIdx][1] + constant) / (
238 | np.sqrt(coefficientA ** 2 + coefficientB ** 2))
239 |
240 |
241 | pointList = np.zeros(shape=(3,2))
242 | # 回 回 tl bl
243 | if (slope >= 0) and (distance >= 0):
244 | # if slope and distance are positive A is bottom while B is right
245 | if (centerOfMassList[bottomLeftIdx][0] > centerOfMassList[topRightIdx][0]):
246 | pointList[1] = centerOfMassList[bottomLeftIdx]
247 | pointList[2] = centerOfMassList[topRightIdx]
248 | else:
249 | pointList[1] = centerOfMassList[topRightIdx]
250 | pointList[2] = centerOfMassList[bottomLeftIdx]
251 | # TopContour in the SouthWest of the picture
252 | ORIENTATION = "SouthWest"
253 |
254 | # 回 回 bl tl
255 | #
256 | # 回 tr
257 | elif (slope > 0) and (distance < 0):
258 | # if slope is positive and distance is negative then B is bottom
259 | # while A is right
260 | if (centerOfMassList[bottomLeftIdx][1] > centerOfMassList[topRightIdx][1]):
261 | pointList[2] = centerOfMassList[bottomLeftIdx]
262 | pointList[1] = centerOfMassList[topRightIdx]
263 | else:
264 | pointList[2] = centerOfMassList[topRightIdx]
265 | pointList[1] = centerOfMassList[bottomLeftIdx]
266 | ORIENTATION = "NorthEast"
267 |
268 |
269 | # 回 bl
270 | #
271 | # 回 回 tr tl
272 | elif (slope < 0) and (distance > 0):
273 | if (centerOfMassList[bottomLeftIdx][0] > centerOfMassList[topRightIdx][0]):
274 | pointList[1] = centerOfMassList[bottomLeftIdx]
275 | pointList[2] = centerOfMassList[topRightIdx]
276 | else:
277 | pointList[1] = centerOfMassList[topRightIdx]
278 | pointList[2] = centerOfMassList[bottomLeftIdx]
279 | ORIENTATION = "SouthEast"
280 | # 回 回 tl tr
281 | #
282 | # 回 bl
283 | elif (slope < 0) and (distance < 0):
284 |
285 | if (centerOfMassList[bottomLeftIdx][0] > centerOfMassList[topRightIdx][0]):
286 | pointList[2] = centerOfMassList[bottomLeftIdx]
287 | pointList[1] = centerOfMassList[topRightIdx]
288 | else:
289 | pointList[2] = centerOfMassList[topRightIdx]
290 | pointList[1] = centerOfMassList[bottomLeftIdx]
291 | pointList[0] = centerOfMassList[topLeftIdx]
292 | return pointList
293 | ```
294 | 我这里其实就是先用距离找出离两个距离最远的点,剩下一个就是直角点了,然后算出斜边的斜率,再算出直角点在斜边的哪一侧,就可以得出这个图形的旋转方式.
295 | 最终就可以得到我们想要的三个点了,我返回时候是按照左上角,左下角,右上角的这个顺序返回的.
296 | 看看识别结果
297 | 
298 | 
299 |
--------------------------------------------------------------------------------