├── 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 | ![canny](https://blog.357573.com/2020/07/03/opencv实现二维码检测/canny.jpg) 33 | ,然后调opencv的findContours函数,找到轮廓, 34 | ![轮廓](https://blog.357573.com/2020/07/03/opencv实现二维码检测/contours.jpg) 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 | ![轮廓](https://blog.357573.com/2020/07/03/opencv实现二维码检测/contours2.jpg) 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 | ![轮廓](https://blog.357573.com/2020/07/03/opencv实现二维码检测/contours3.jpg) 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 | ![重心](https://blog.357573.com/2020/07/03/opencv实现二维码检测/mass.jpg) 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 | ![结果](https://blog.357573.com/2020/07/03/opencv实现二维码检测/result.jpg) 298 | ![结果](https://blog.357573.com/2020/07/03/opencv实现二维码检测/result2.jpg) 299 | --------------------------------------------------------------------------------