├── static ├── .nojekyll └── img │ ├── favicon.ico │ ├── docusaurus.png │ ├── logo.svg │ ├── undraw_docusaurus_tree.svg │ └── undraw_docusaurus_mountain.svg ├── .dockerignore ├── babel.config.js ├── .vscode └── settings.json ├── Dockerfile ├── docs ├── start │ ├── _category_.json │ ├── extra-03-trackbar.md │ ├── extra-01-code-optimization.md │ ├── 02-basic-element-image.md │ ├── 04-basic-operations.md │ ├── extra-06-drawing-with-mouse.md │ ├── 07-image-geometric-transformation.md │ ├── 05-changing-colorspaces.md │ ├── 03-open-camera.md │ ├── 01-introduction-and-installation.md │ ├── extra-02-high-quality-save-and-matplotlib.md │ ├── 06-image-thresholding.md │ ├── extra-04-otsu-thresholding.md │ ├── 08-drawing-function.md │ ├── challenge-01-draw-dynamic-clock.md │ ├── extra-05-warpaffine-warpperspective.md │ └── challenge-02-create-gui-with-pyqt5.md ├── basic │ ├── _category_.json │ ├── extra-07-contrast-and-brightness.md │ ├── 13-contours.md │ ├── extra-08-padding-and-convolution.md │ ├── 09-image-blending.md │ ├── extra-11-convex-hull.md │ ├── 11-edge-detection.md │ ├── 16-template-matching.md │ ├── 12-erode-and-dilate.md │ ├── extra-10-contours-hierarchy.md │ ├── 17-hough-transform.md │ ├── 15-histograms.md │ ├── extra-09-image-gradients.md │ ├── 14-contour-features.md │ ├── 10-smoothing-images.md │ └── challenge-03-lane-road-detection.md └── index.md ├── tsconfig.json ├── sidebars.js ├── .gitignore ├── src ├── components │ └── HomepageFeatures │ │ ├── styles.module.css │ │ └── index.tsx ├── pages │ ├── index.module.css │ └── index.tsx └── css │ └── custom.css ├── package.json ├── docusaurus.config.js └── README.md /static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | 3 | node_modules -------------------------------------------------------------------------------- /static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodecWang/opencv-python-tutorial/HEAD/static/img/favicon.ico -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodecWang/opencv-python-tutorial/HEAD/static/img/docusaurus.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-slim as builder 2 | COPY . . 3 | RUN npm install 4 | RUN npm run build 5 | 6 | FROM nginx 7 | COPY --from=builder build /usr/share/nginx/html 8 | -------------------------------------------------------------------------------- /docs/start/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "入门篇", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "安装并了解 OpenCV-Python 的基本使用方法" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/basic/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "基础篇", 3 | "position": 3, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "学习 OpenCV-Python 在常见图像处理算法中的实践" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@tsconfig/docusaurus/tsconfig.json", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /sidebars.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 2 | const sidebars = { 3 | // By default, Docusaurus generates a sidebar from the docs folder structure 4 | tutorialSidebar: [{ type: "autogenerated", dirName: "." }], 5 | }; 6 | 7 | module.exports = sidebars; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | 13 | .featureItem { 14 | cursor: pointer; 15 | } 16 | 17 | .featureItem:hover { 18 | background-color: var(--ifm-color-emphasis-200); 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #2e8555; 10 | --ifm-color-primary-dark: #29784c; 11 | --ifm-color-primary-darker: #277148; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #33925d; 14 | --ifm-color-primary-lighter: #359962; 15 | --ifm-color-primary-lightest: #3cad6e; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme='dark'] { 22 | --ifm-color-primary: #25c2a0; 23 | --ifm-color-primary-dark: #21af90; 24 | --ifm-color-primary-darker: #1fa588; 25 | --ifm-color-primary-darkest: #1a8870; 26 | --ifm-color-primary-light: #29d5b0; 27 | --ifm-color-primary-lighter: #32d8b4; 28 | --ifm-color-primary-lightest: #4fddbf; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clsx from "clsx"; 3 | import Link from "@docusaurus/Link"; 4 | import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; 5 | import Layout from "@theme/Layout"; 6 | import HomepageFeatures from "@site/src/components/HomepageFeatures"; 7 | 8 | import styles from "./index.module.css"; 9 | 10 | function HomepageHeader() { 11 | const { siteConfig } = useDocusaurusContext(); 12 | return ( 13 |
14 |
15 |

{siteConfig.title}

16 |

{siteConfig.tagline}

17 |
18 | 19 | 开始学习 20 | 21 |
22 |
23 |
24 | ); 25 | } 26 | 27 | export default function Home(): JSX.Element { 28 | const { siteConfig } = useDocusaurusContext(); 29 | return ( 30 | 34 | 35 |
36 | 37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-website", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "2.0.0-beta.21", 19 | "@docusaurus/preset-classic": "2.0.0-beta.21", 20 | "@mdx-js/react": "^1.6.22", 21 | "clsx": "^1.1.1", 22 | "hast-util-is-element": "^1.1.0", 23 | "prism-react-renderer": "^1.3.3", 24 | "react": "^17.0.2", 25 | "react-dom": "^17.0.2", 26 | "rehype-katex": "^5.0.0", 27 | "remark-math": "^3.0.1" 28 | }, 29 | "devDependencies": { 30 | "@docusaurus/module-type-aliases": "2.0.0-beta.21", 31 | "@tsconfig/docusaurus": "^1.0.5", 32 | "typescript": "^4.6.4" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.5%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docs/basic/extra-07-contrast-and-brightness.md: -------------------------------------------------------------------------------- 1 | # 番外篇:亮度与对比度 2 | 3 | ![](http://cos.codec.wang/cv2_contrast_brightness.jpg) 4 | 5 | 学习如何调整图片的亮度和对比度。图片等可到文末引用处下载。 6 | 7 | ## 亮度与对比度 8 | 9 | 亮度调整是将图像像素的强度整体变大/变小,对比度调整指的是图像暗处的像素强度变低,亮出的变高,从而拓宽某个区域内的显示精度。 10 | 11 | OpenCV 中亮度和对比度应用这个公式来计算:$g(x)=αf(x)+β$,其中:$α$、$β$常称为增益与偏置值,分别控制图片的对比度和亮度。 12 | 13 | :::tip 14 | 此处对 α/β 控制对比度和亮度有争议,具体请参考:[OpenCV 关于对比度和亮度的误解](http://blog.csdn.net/abc20002929/article/details/40474807)。 15 | ::: 16 | 17 | ```python 18 | import cv2 19 | import numpy as np 20 | 21 | img = cv2.imread('lena.jpg') 22 | # 此处需注意,请参考后面的解释 23 | res = np.uint8(np.clip((1.5 * img + 10), 0, 255)) 24 | tmp = np.hstack((img, res)) # 两张图片横向合并(便于对比显示) 25 | 26 | cv2.imshow('image', tmp) 27 | cv2.waitKey(0) 28 | ``` 29 | 30 | 还记得图像混合那一节中 numpy 对数据溢出的取模处理吗?`250+10 = 260 => 260%256=4`,它并不适用于我们的图像处理,所以用 [np.clip\(\)](https://docs.scipy.org/doc/numpy/reference/generated/numpy.clip.html#numpy.clip) 函数将数据限定:`a<0 => a=0, a>255 => a=255`。 31 | 32 | ![亮度与对比度调整](http://cos.codec.wang/cv2_contrast_brightness.jpg) 33 | 34 | ## 练习 35 | 36 | 1. 创建两个滑动条分别调整对比度和亮度(对比度范围:0~0.3,亮度:0~100)。提示:因为滑动条没有小数,所以可以设置为 0~300,然后乘以 0.01。 37 | 2. 亮度/对比度用 C++实现也很有趣,推荐阅读:[OpenCV 改变图像亮度和对比度以及优化](http://blog.csdn.net/u013139259/article/details/52145377)。 38 | 39 | ## 引用 40 | 41 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/Extra-07-Contrast-and-Brightness) 42 | - [numpy.clip()](https://docs.scipy.org/doc/numpy/reference/generated/numpy.clip.html#numpy.clip) 43 | - [OpenCV 关于对比度和亮度的误解](http://blog.csdn.net/abc20002929/article/details/40474807) 44 | - [OpenCV 改变图像亮度和对比度以及优化](http://blog.csdn.net/u013139259/article/details/52145377) 45 | - [Mat::convertTo](https://docs.opencv.org/3.1.0/d3/d63/classcv_1_1Mat.html#a3f356665bb0ca452e7d7723ccac9a810) 46 | -------------------------------------------------------------------------------- /docs/start/extra-03-trackbar.md: -------------------------------------------------------------------------------- 1 | # 番外篇:滑动条 2 | 3 | ![](http://cos.codec.wang/cv2_track_bar_rgb.jpg) 4 | 5 | 学习使用滑动条动态调整参数。图片等可到文末引用处下载。 6 | 7 | ## 滑动条的使用 8 | 9 | 首先我们需要创建一个滑动条,如`cv2.createTrackbar('R','image',0,255,call_back)`,其中 10 | 11 | - 参数 1:滑动条的名称 12 | - 参数 2:所在窗口的名称 13 | - 参数 3:当前的值 14 | - 参数 4:最大值 15 | - 参数 5:回调函数名称,回调函数默认有一个表示当前值的参数 16 | 17 | 创建好之后,可以在回调函数中获取滑动条的值,也可以用:`cv2.getTrackbarPos()`得到,其中,参数 1 是滑动条的名称,参数 2 是窗口的名称。 18 | 19 | ## RGB 调色板 20 | 21 | 下面我们实现一个 RGB 的调色板,理解下滑动条的用法: 22 | 23 | ```python 24 | import cv2 25 | import numpy as np 26 | 27 | # 回调函数,x 表示滑块的位置,本例暂不使用 28 | def nothing(x): 29 | pass 30 | 31 | img = np.zeros((300, 512, 3), np.uint8) 32 | cv2.namedWindow('image') 33 | 34 | # 创建 RGB 三个滑动条 35 | cv2.createTrackbar('R', 'image', 0, 255, nothing) 36 | cv2.createTrackbar('G', 'image', 0, 255, nothing) 37 | cv2.createTrackbar('B', 'image', 0, 255, nothing) 38 | 39 | while(True): 40 | cv2.imshow('image', img) 41 | if cv2.waitKey(1) == 27: 42 | break 43 | 44 | # 获取滑块的值 45 | r = cv2.getTrackbarPos('R', 'image') 46 | g = cv2.getTrackbarPos('G', 'image') 47 | b = cv2.getTrackbarPos('B', 'image') 48 | # 设定 img 的颜色 49 | img[:] = [b, g, r] 50 | ``` 51 | 52 | ![](http://cos.codec.wang/cv2_track_bar_rgb.jpg) 53 | 54 | ## 小结 55 | 56 | - `cv2.createTrackbar()`用来创建滑动条,可以在回调函数中或使用`cv2.getTrackbarPos()`得到滑块的位置 57 | 58 | ## 接口文档 59 | 60 | - [cv2.createTrackbar\(\)](https://docs.opencv.org/4.0.0/d7/dfc/group__highgui.html#gaf78d2155d30b728fc413803745b67a9b) 61 | - [cv2.getTrackbarPos\(\)](https://docs.opencv.org/4.0.0/d7/dfc/group__highgui.html#ga122632e9e91b9ec06943472c55d9cda8) 62 | 63 | ## 引用 64 | 65 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/Extra-03-Trackbar) 66 | - [Trackbar as the Color Palette](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_gui/py_trackbar/py_trackbar.html) 67 | -------------------------------------------------------------------------------- /src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clsx from "clsx"; 3 | import styles from "./styles.module.css"; 4 | import { useHistory } from "@docusaurus/router"; 5 | 6 | type FeatureItem = { 7 | title: string; 8 | link: string; 9 | Svg: React.ComponentType>; 10 | description: JSX.Element; 11 | }; 12 | 13 | const FeatureList: FeatureItem[] = [ 14 | { 15 | title: "入门篇", 16 | link: "/docs/category/入门篇", 17 | Svg: require("@site/static/img/undraw_docusaurus_mountain.svg").default, 18 | description: <>安装并了解 OpenCV-Python 的基本使用方法。, 19 | }, 20 | { 21 | title: "基础篇", 22 | link: "/docs/category/基础篇", 23 | Svg: require("@site/static/img/undraw_docusaurus_tree.svg").default, 24 | description: <>学习 OpenCV-Python 在常见图像处理算法中的实践。, 25 | }, 26 | { 27 | title: "More...", 28 | link: "/", 29 | Svg: require("@site/static/img/undraw_docusaurus_react.svg").default, 30 | description: <>敬请期待..., 31 | }, 32 | ]; 33 | 34 | function Feature({ title, link, Svg, description }: FeatureItem) { 35 | const history = useHistory(); 36 | return ( 37 |
history.push(link)} 40 | > 41 |
42 | 43 |
44 |
45 |

{title}

46 |

{description}

47 |
48 |
49 | ); 50 | } 51 | 52 | export default function HomepageFeatures(): JSX.Element { 53 | return ( 54 |
55 |
56 |
57 | {FeatureList.map((props, idx) => ( 58 | 59 | ))} 60 |
61 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /docs/start/extra-01-code-optimization.md: -------------------------------------------------------------------------------- 1 | # 番外篇:代码性能优化 2 | 3 | 学习如何评估和优化代码性能。 4 | 5 | 完成一项任务很重要,高效地完成更重要。图像处理是对矩阵的操作,数据量巨大。如果代码写的不好,性能差距将很大,所以这节我们来了解下如何评估和提升代码性能。 6 | 7 | ## 评估代码运行时间 8 | 9 | ```python 10 | import cv2 11 | 12 | start = cv2.getTickCount() 13 | # 这里写测试代码... 14 | end = cv2.getTickCount() 15 | print((end - start) / cv2.getTickFrequency()) 16 | ``` 17 | 18 | 这段代码就是用来测量程序运行时间的(单位:s),其中`cv2.getTickCount()`函数得到电脑启动以来的时钟周期数,`cv2.getTickFrequency()`返回你电脑的主频,前后相减再除以主频就是你代码的运行时间(这样解释并不完全准确,但能理解就行)。另外,也可以用 Python 中的 time 模块计时: 19 | 20 | ```python 21 | import time 22 | 23 | start = time.clock() 24 | # 这里写测试代码... 25 | end = time.clock() 26 | print(end - start) 27 | ``` 28 | 29 | :::tip 30 | 如果你使用的是 [IPython](https://baike.baidu.com/item/ipython) 或 [Jupyter Notebook](https://baike.baidu.com/item/Jupyter) 开发环境,性能分析将会非常方便,详情请参考:[Timing and Profiling in IPython](http://pynash.org/2013/03/06/timing-and-profiling/) 31 | ::: 32 | 33 | ## 优化原则 34 | 35 | - 数据元素少时用 Python 语法,数据元素多时用 Numpy: 36 | 37 | ```python 38 | x = 10 39 | z = np.uint8([10]) 40 | 41 | # 尝试比较下面三句话各自的运行时间 42 | y = x * x * x # (1.6410249677846285e-06) 43 | y = x**3 # (2.461537451676943e-06) 44 | y = z * z * z # 最慢 (3.1179474387907945e-05) 45 | ``` 46 | 47 | 所以 Numpy 的运行速度并不一定比 Python 本身语法快,元素数量较少时,请用 Python 本身格式。 48 | 49 | - 尽量避免使用循环,尤其嵌套循环,因为极其慢!!! 50 | - 优先使用 OpenCV/Numpy 中封装好的函数 51 | - 尽量将数据向量化,变成 Numpy 的数据格式 52 | - 尽量避免数组的复制操作 53 | 54 | ## 接口文档 55 | 56 | - [cv2.getTickCount\(\)](https://docs.opencv.org/4.0.0/db/de0/group__core__utils.html#gae73f58000611a1af25dd36d496bf4487) 57 | - [cv2.getTickFrequency\(\)](https://docs.opencv.org/4.0.0/db/de0/group__core__utils.html#ga705441a9ef01f47acdc55d87fbe5090c) 58 | 59 | ## 引用 60 | 61 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/Extra-01-Code-Optimization) 62 | - [Python Optimization Techniques](https://wiki.python.org/moin/PythonSpeed/PerformanceTips) 63 | - [Timing and Profiling in IPython](http://pynash.org/2013/03/06/timing-and-profiling/) 64 | - [Advanced Numpy](http://www.scipy-lectures.org/advanced/advanced_numpy/index.html#advanced-numpy) 65 | -------------------------------------------------------------------------------- /docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Note: type annotations allow type checking and IDEs autocompletion 3 | 4 | const math = require("remark-math"); 5 | const katex = require("rehype-katex"); 6 | 7 | const lightCodeTheme = require("prism-react-renderer/themes/github"); 8 | const darkCodeTheme = require("prism-react-renderer/themes/dracula"); 9 | 10 | /** @type {import('@docusaurus/types').Config} */ 11 | const config = { 12 | title: "OpenCV-Python Tutorial", 13 | tagline: "面向初学者的 OpenCV-Python 教程", 14 | url: "https://codec.wang", 15 | baseUrl: "/", 16 | onBrokenLinks: "throw", 17 | onBrokenMarkdownLinks: "warn", 18 | favicon: "img/favicon.ico", 19 | 20 | // GitHub pages deployment config. 21 | // If you aren't using GitHub pages, you don't need these. 22 | organizationName: "CodecWang", 23 | projectName: "opencv-python-tutorial", 24 | 25 | i18n: { 26 | defaultLocale: "zh-Hans", 27 | locales: ["zh-Hans"], 28 | }, 29 | 30 | presets: [ 31 | [ 32 | "classic", 33 | /** @type {import('@docusaurus/preset-classic').Options} */ 34 | ({ 35 | docs: { 36 | sidebarPath: require.resolve("./sidebars.js"), 37 | editUrl: 38 | "https://github.com/CodecWang/opencv-python-tutorial/tree/master/", 39 | remarkPlugins: [math], 40 | rehypePlugins: [katex], 41 | }, 42 | theme: { 43 | customCss: require.resolve("./src/css/custom.css"), 44 | }, 45 | }), 46 | ], 47 | ], 48 | 49 | stylesheets: [ 50 | { 51 | href: "https://cdn.jsdelivr.net/npm/katex@0.13.24/dist/katex.min.css", 52 | type: "text/css", 53 | integrity: 54 | "sha384-odtC+0UGzzFL/6PNoE8rX/SPcQDXBJ+uRepguP4QkPCm2LBxH3FA3y+fKSiJ+AmM", 55 | crossorigin: "anonymous", 56 | }, 57 | ], 58 | 59 | themeConfig: 60 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 61 | ({ 62 | navbar: { 63 | title: "OpenCV-Python Tutorial", 64 | logo: { 65 | alt: "My Site Logo", 66 | src: "img/logo.svg", 67 | }, 68 | items: [ 69 | { 70 | href: "http://codec.wang", 71 | label: "CodecWang's Blog", 72 | position: "right", 73 | }, 74 | { 75 | href: "https://github.com/CodecWang/opencv-python-tutorial", 76 | label: "GitHub", 77 | position: "right", 78 | }, 79 | ], 80 | }, 81 | footer: { 82 | style: "dark", 83 | copyright: `Copyright © ${new Date().getFullYear()} CodecWang. Built with Docusaurus.`, 84 | }, 85 | prism: { 86 | theme: lightCodeTheme, 87 | darkTheme: darkCodeTheme, 88 | }, 89 | }), 90 | }; 91 | 92 | module.exports = config; 93 | -------------------------------------------------------------------------------- /docs/start/02-basic-element-image.md: -------------------------------------------------------------------------------- 1 | # 02: 基本元素 - 图片 2 | 3 | ![](http://cos.codec.wang/cv2_image_coordinate_channels.jpg) 4 | 5 | 学习如何加载图片,显示并保存图片。图片等可到文末引用处下载。 6 | 7 | ## 目标 8 | 9 | - 加载图片,显示图片,保存图片 10 | - OpenCV 函数:`cv2.imread()`, `cv2.imshow()`, `cv2.imwrite()` 11 | 12 | ## 教程 13 | 14 | 大部分人可能都知道电脑上的彩色图是以 RGB(Red/Green/Blue: 红/绿/蓝) 颜色模式显示的,但 OpenCV 中彩色图是以 BGR 通道顺序存储的,灰度图只有一个通道。 15 | 16 | 图像坐标的起始点是在左上角,所以行对应的是 y,列对应的是 x: 17 | 18 | ![](http://cos.codec.wang/cv2_image_coordinate_channels.jpg) 19 | 20 | ### 加载图片 21 | 22 | 使用`cv2.imread()`来读入一张图片: 23 | 24 | ```python 25 | import cv2 26 | 27 | # 加载灰度图 28 | img = cv2.imread('lena.jpg', 0) 29 | ``` 30 | 31 | - 参数 1:图片的文件名 32 | - 如果图片放在当前文件夹下,直接写文件名就行,如'lena.jpg' 33 | - 否则需要给出绝对路径,如'D:\OpenCVSamples\lena.jpg' 34 | - 参数 2:读入方式,省略即采用默认值 35 | - `cv2.IMREAD_COLOR`:彩色图,默认值 (1) 36 | - `cv2.IMREAD_GRAYSCALE`:灰度图 (0) 37 | - `cv2.IMREAD_UNCHANGED`:包含透明通道的彩色图 (-1) 38 | 39 | :::tip 40 | 41 | 路径中不能有中文噢,并且没有加载成功的话是不会报错的,`print(img)`的结果为 None,后面处理才会报错,算是个小坑。 42 | 43 | ::: 44 | 45 | ### 显示图片 46 | 47 | 使用`cv2.imshow()`显示图片,窗口会自适应图片的大小: 48 | 49 | ```python 50 | cv2.imshow('lena', img) 51 | cv2.waitKey(0) 52 | ``` 53 | 54 | 参数 1 是窗口的名字,参数 2 是要显示的图片。不同窗口之间用窗口名区分,所以窗口名相同就表示是同一个窗口,显示结果如下: 55 | 56 | ![](http://cos.codec.wang/cv2_show_lena_gray.jpg) 57 | 58 | `cv2.waitKey()`是让程序暂停的意思,参数是等待时间,单位毫秒 ms。时间一到,会继续执行接下来的程序,传入 0 的话表示一直等待。等待期间也可以获取用户的按键输入:`k = cv2.waitKey(0)`([练习 1](#练习))。 59 | 60 | 我们也可以先用`cv2.namedWindow()`创建一个窗口,之后再显示图片: 61 | 62 | ```python 63 | # 先定义窗口,后显示图片 64 | cv2.namedWindow('lena2', cv2.WINDOW_NORMAL) 65 | cv2.imshow('lena2', img) 66 | cv2.waitKey(0) 67 | ``` 68 | 69 | 参数 1 依旧是窗口的名字,参数 2 默认是`cv2.WINDOW_AUTOSIZE`,表示窗口大小自适应图片,也可以设置为`cv2.WINDOW_NORMAL`,表示窗口大小可调整。图片比较大的时候,可以考虑用后者。 70 | 71 | ### 保存图片 72 | 73 | 使用`cv2.imwrite()`保存图片,参数 1 是包含后缀名的文件名: 74 | 75 | ```python 76 | cv2.imwrite('lena_gray.jpg', img) 77 | ``` 78 | 79 | Nice,是不是很简单呐,再接再厉噢\(●'◡'●\) 80 | 81 | ## 小结 82 | 83 | - `cv2.imread()`读入图片、`cv2.imshow()`显示图片、`cv2.imwrite()`保存图片。 84 | 85 | ## 练习 86 | 87 | 1. 打开 lena.jpg 并显示,如果按下's',就保存图片为'lena_save.bmp',否则就结束程序。 88 | 2. Matplotlib 是 Python 中常用的一个绘图库,请学习[番外篇:无损保存和 Matplotlib 使用](./extra-02-high-quality-save-and-matplotlib)。 89 | 90 | ## 接口文档 91 | 92 | - [Mat Object](https://docs.opencv.org/4.0.0/d3/d63/classcv_1_1Mat.html) 93 | - [cv2.imread\(\)](https://docs.opencv.org/4.0.0/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56) 94 | - [cv2.imshow\(\)](https://docs.opencv.org/4.0.0/d7/dfc/group__highgui.html#ga453d42fe4cb60e5723281a89973ee563) 95 | - [cv2.imwrite\(\)](https://docs.opencv.org/4.0.0/d4/da8/group__imgcodecs.html#gabbc7ef1aa2edfaa87772f1202d67e0ce) 96 | - [cv.namedWindow\(\)](https://docs.opencv.org/4.0.0/d7/dfc/group__highgui.html#ga5afdf8410934fd099df85c75b2e0888b) 97 | 98 | ## 引用 99 | 100 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/02-Basic-Element-Image) 101 | - [Getting Started with Images](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_gui/py_image_display/py_image_display.html) 102 | -------------------------------------------------------------------------------- /docs/basic/13-contours.md: -------------------------------------------------------------------------------- 1 | # 13: 轮廓 2 | 3 | ![](http://cos.codec.wang/cv2_understand_contours.jpg) 4 | 5 | 学习如何寻找并绘制轮廓。图片等可到文末引用处下载。 6 | 7 | ## 目标 8 | 9 | - 了解轮廓概念 10 | - 寻找并绘制轮廓 11 | - OpenCV 函数:`cv2.findContours()`, `cv2.drawContours()` 12 | 13 | ## 教程 14 | 15 | ### 啥叫轮廓 16 | 17 | 轮廓是一系列相连的点组成的曲线,代表了物体的基本外形。 18 | 19 | 谈起轮廓不免想到边缘,它们确实很像。简单的说,**轮廓是连续的,边缘并不全都连续**(下图)。其实边缘主要是作为图像的特征使用,比如可以用边缘特征可以区分脸和手,而轮廓主要用来分析物体的形态,比如物体的周长和面积等,可以说边缘包括轮廓。 20 | 21 | ![边缘和轮廓的区别](http://cos.codec.wang/cv2_understand_contours.jpg) 22 | 23 | 寻找轮廓的操作一般用于二值化图,所以通常会使用阈值分割或 Canny 边缘检测先得到二值图。 24 | 25 | :::tip 26 | **寻找轮廓是针对白色物体的**,一定要保证物体是白色,而背景是黑色,**不然很多人在寻找轮廓时会找到图片最外面的一个框**。 27 | ::: 28 | 29 | ### 寻找轮廓 30 | 31 | 使用`cv2.findContours()`寻找轮廓: 32 | 33 | ```python 34 | import cv2 35 | 36 | img = cv2.imread('handwriting.jpg') 37 | img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 38 | ret, thresh = cv2.threshold(img_gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) 39 | 40 | # 寻找二值化图中的轮廓 41 | image, contours, hierarchy = cv2.findContours( 42 | thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) 43 | print(len(contours)) # 结果应该为 2 44 | ``` 45 | 46 | - 参数 2:轮廓的查找方式,一般使用 cv2.RETR_TREE,表示提取所有的轮廓并建立轮廓间的层级。更多请参考:[RetrievalModes](https://docs.opencv.org/4.0.0/d3/dc0/group__imgproc__shape.html#ga819779b9857cc2f8601e6526a3a5bc71) 47 | - 参数 3:轮廓的近似方法。比如对于一条直线,我们可以存储该直线的所有像素点,也可以只存储起点和终点。使用 cv2.CHAIN_APPROX_SIMPLE 就表示用尽可能少的像素点表示轮廓。更多请参考:[ContourApproximationModes](https://docs.opencv.org/4.0.0/d3/dc0/group__imgproc__shape.html#ga4303f45752694956374734a03c54d5ff) 48 | - 简便起见,这两个参数也可以直接用真值 3 和 2 表示。 49 | 50 | 函数有 3 个返回值,image 还是原来的二值化图片,hierarchy 是轮廓间的层级关系([番外篇:轮廓层级](./extra-10-contours-hierarchy/)),这两个暂时不用理会。我们主要看 contours,它就是找到的轮廓了,以数组形式存储,记录了每条轮廓的所有像素点的坐标\(x,y\)。 51 | 52 | ![](http://cos.codec.wang/cv2_find_contours_contours.jpg) 53 | 54 | ### 绘制轮廓 55 | 56 | 轮廓找出来后,为了方便观看,可以像前面图中那样用红色画出来:`cv2.drawContours()` 57 | 58 | ```python 59 | cv2.drawContours(img, contours, -1, (0, 0, 255), 2) 60 | ``` 61 | 62 | 其中参数 2 就是得到的 contours,参数 3 表示要绘制哪一条轮廓,-1 表示绘制所有轮廓,参数 4 是颜色(B/G/R 通道,所以\(0,0,255\) 表示红色),参数 5 是线宽,之前在绘制图形中介绍过。 63 | 64 | :::tip 65 | 很多人画图时明明用了彩色,但没有效果,请检查你是在哪个图上画,画在灰度图和二值图上显然是没有彩色的\(⊙o⊙\)。 66 | ::: 67 | 68 | 一般情况下,我们会首先获得要操作的轮廓,再进行轮廓绘制及分析: 69 | 70 | ```python 71 | cnt = contours[1] 72 | cv2.drawContours(img, [cnt], 0, (0, 0, 255), 2) 73 | ``` 74 | 75 | ## 小结 76 | 77 | - 轮廓特征非常有用,使用`cv2.findContours()`寻找轮廓,`cv2.drawContours()`绘制轮廓。 78 | 79 | ## 接口文档 80 | 81 | - [cv2.findContours()](https://docs.opencv.org/4.0.0/d3/dc0/group__imgproc__shape.html#gadf1ad6a0b82947fa1fe3c3d497f260e0) 82 | - [cv2.RetrievalModes](https://docs.opencv.org/4.0.0/d3/dc0/group__imgproc__shape.html#ga819779b9857cc2f8601e6526a3a5bc71) 83 | - [cv2.ContourApproximationModes](https://docs.opencv.org/4.0.0/d3/dc0/group__imgproc__shape.html#ga4303f45752694956374734a03c54d5ff) 84 | - [cv2.drawContours()](https://docs.opencv.org/4.0.0/d6/d6e/group__imgproc__draw.html#ga746c0625f1781f1ffc9056259103edbc) 85 | 86 | ## 引用 87 | 88 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/13-Contours) 89 | - [Contours : Getting Started](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contours_begin/py_contours_begin.html) 90 | -------------------------------------------------------------------------------- /docs/basic/extra-08-padding-and-convolution.md: -------------------------------------------------------------------------------- 1 | # 番外篇:卷积基础 - 图片边框 2 | 3 | ![](http://cos.codec.wang/cv2_understand_padding.jpg) 4 | 5 | 了解卷积/滤波的基础知识,给图片添加边框。 6 | 7 | 卷积的概念其实很好理解,下面我就给大家做个最简单的解释,绝对轻松加愉快的辣 o\(_ ̄ ▽  ̄_\)o 8 | 9 | ## 卷积 10 | 11 | 什么是二维卷积呢?看下面一张图就一目了然: 12 | 13 | ![](http://cos.codec.wang/cv2_understand_convolution.jpg) 14 | 15 | 卷积就是循环对**图像跟一个核逐个元素相乘再求和得到另外一副图像的操作**,比如结果图中第一个元素 5 是怎么算的呢?原图中 3×3 的区域与 3×3 的核逐个元素相乘再相加: 16 | 17 | $$ 18 | 5=1\times1+2\times0+1\times0+0\times0+1\times0+1\times0+3\times0+0\times0+2\times2 19 | $$ 20 | 21 | 算完之后,整个框再往右移一步继续计算,横向计算完后,再往下移一步继续计算……网上有一副很经典的动态图,方便我们理解卷积: 22 | 23 | ![](http://cos.codec.wang/cv2_understand_cnn.gif) 24 | 25 | ## padding 26 | 27 | 不难发现,前面我们用 3×3 的核对一副 6×6 的图像进行卷积,得到的是 4×4 的图,图片缩小了!那怎么办呢?我们可以**把原图扩充一圈,再卷积,这个操作叫填充 padding**。 28 | 29 | > 事实上,原图为 n×n,卷积核为 f×f,最终结果图大小为\(n-f+1\) × \(n-f+1\)。 30 | 31 | ![](http://cos.codec.wang/cv2_understand_padding.jpg) 32 | 33 | 那么扩展的这一层应该填充什么值呢?OpenCV 中有好几种填充方式,都使用`cv2.copyMakeBorder()`函数实现,一起来看看。 34 | 35 | ## 添加边框 36 | 37 | `cv2.copyMakeBorder()`用来给图片添加边框,它有下面几个参数: 38 | 39 | - src:要处理的原图 40 | - top, bottom, left, right:上下左右要扩展的像素数 41 | - **borderType**:边框类型,这个就是需要关注的填充方式,详情请参考:[BorderTypes](https://docs.opencv.org/3.3.1/d2/de8/group__core__array.html#ga209f2f4869e304c82d07739337eae7c5) 42 | 43 | 其中默认方式和固定值方式最常用,我们详细说明一下: 44 | 45 | ### 固定值填充 46 | 47 | 顾名思义,`cv2.BORDER_CONSTANT`这种方式就是边框都填充成一个固定的值,比如下面的程序都填充 0: 48 | 49 | ```python 50 | img = cv2.imread('6_by_6.bmp', 0) 51 | print(img) 52 | 53 | # 固定值边框,统一都填充 0 也称为 zero padding 54 | cons = cv2.copyMakeBorder(img, 1, 1, 1, 1, cv2.BORDER_CONSTANT, value=0) 55 | print(cons) 56 | ``` 57 | 58 | ![](http://cos.codec.wang/cv2_zero_padding_output.jpg) 59 | 60 | ### 默认边框类型 61 | 62 | 默认边框`cv2.BORDER_DEFAULT`其实是取镜像对称的像素填充,比较拗口,一步步解释: 63 | 64 | ```python 65 | default = cv2.copyMakeBorder(img, 1, 1, 1, 1, cv2.BORDER_DEFAULT) 66 | print(default) 67 | ``` 68 | 69 | 首先进行上下填充,填充成与原图像边界对称的值,如下图: 70 | 71 | ![](http://cos.codec.wang/cv2_up_down_padding_first.jpg) 72 | 73 | 同理再进行左右两边的填充,最后把四个顶点补充上就好了: 74 | 75 | ![](http://cos.codec.wang/cv2_right_left_padding_second2.jpg) 76 | 77 | :::tip 78 | 一般情况下默认方式更加合理,因为边界的像素值更加接近。具体应视场合而定。 79 | ::: 80 | 81 | ## OpenCV 进行卷积 82 | 83 | OpenCV 中用`cv2.filter2D()`实现卷积操作,比如我们的核是下面这样(3×3 区域像素的和除以 10): 84 | 85 | $$ 86 | M = \frac{1}{10}\left[ 87 | \begin{matrix} 88 | 1 & 1 & 1 \newline 89 | 1 & 1 & 1 \newline 90 | 1 & 1 & 1 91 | \end{matrix} 92 | \right] \tag{3} 93 | $$ 94 | 95 | ```python 96 | img = cv2.imread('lena.jpg') 97 | # 定义卷积核 98 | kernel = np.ones((3, 3), np.float32) / 10 99 | # 卷积操作,-1 表示通道数与原图相同 100 | dst = cv2.filter2D(img, -1, kernel) 101 | ``` 102 | 103 | ![](http://cos.codec.wang/cv2_convolution_kernel_3_3.jpg) 104 | 105 | 可以看到这个核对图像进行了模糊处理,这是卷积的众多功能之一。当然卷积还有很多知识没有学到,后面我们再继续深入。 106 | 107 | ## 练习 108 | 109 | 1. 尝试给"lena.jpg"添加几种不同的边框类型,对比下效果。 110 | 111 | ## 引用 112 | 113 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/Extra-08-Padding-and-Convolution) 114 | - [Basic Operations on Images](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_core/py_basic_ops/py_basic_ops.html) 115 | - [图像卷积与滤波的一些知识点](http://blog.csdn.net/zouxy09/article/details/49080029) 116 | -------------------------------------------------------------------------------- /docs/start/04-basic-operations.md: -------------------------------------------------------------------------------- 1 | # 04: 图像基本操作 2 | 3 | ![](http://cos.codec.wang/cv2_lena_face_roi_crop.jpg) 4 | 5 | 学习获取和修改像素点的值,ROI 感兴趣区域,通道分离合并等基本操作。图片等可到文末引用处下载。 6 | 7 | ## 目标 8 | 9 | - 访问和修改图片像素点的值 10 | - 获取图片的宽、高、通道数等属性 11 | - 了解感兴趣区域 ROI 12 | - 分离和合并图像通道 13 | 14 | ## 教程 15 | 16 | ### 获取和修改像素点值 17 | 18 | 我们先读入一张图片: 19 | 20 | ```python 21 | import cv2 22 | 23 | img = cv2.imread('lena.jpg') 24 | ``` 25 | 26 | 通过行列的坐标来获取某像素点的值,对于彩色图,结果是 B,G,R 三个值的列表,对于灰度图或单通道图,只有一个值: 27 | 28 | ```python 29 | px = img[100, 90] 30 | print(px) # [103 98 197] 31 | 32 | # 只获取蓝色 blue 通道的值 33 | px_blue = img[100, 90, 0] 34 | print(px_blue) # 103 35 | ``` 36 | 37 | 还记得吗?行对应 y,列对应 x,所以其实是`img[y, x]`,需要注意噢\(●ˇ∀ˇ●\)。容易混淆的话,可以只记行和列,行在前,列在后。 38 | 39 | 修改像素的值也是同样的方式: 40 | 41 | ```python 42 | img[100, 90] = [255, 255, 255] 43 | print(img[100, 90]) # [255 255 255] 44 | ``` 45 | 46 | :::tip 47 | 还有一种性能更好的方式,获取:`img.item(100,100,0)`,修改:`img.itemset((100,100,0),255)`,但这种方式只能 B,G,R 逐一进行。 48 | ::: 49 | 50 | 注意:这步操作只是内存中的 img 像素点值变了,因为没有保存,所以原图并没有更改。 51 | 52 | ### 图片属性 53 | 54 | `img.shape`获取图像的形状,图片是彩色的话,返回一个包含**行数(高度)、列数(宽度)和通道数**的元组,灰度图只返回行数和列数: 55 | 56 | ```python 57 | print(img.shape) # (263, 247, 3) 58 | # 形状中包括行数、列数和通道数 59 | height, width, channels = img.shape 60 | # img 是灰度图的话:height, width = img.shape 61 | ``` 62 | 63 | `img.dtype`获取图像数据类型: 64 | 65 | ```python 66 | print(img.dtype) # uint8 67 | ``` 68 | 69 | :::tip 70 | 多数错误是因为数据类型不对导致的,所以健壮的代码应该对这个属性加以判断。 71 | ::: 72 | 73 | `img.size`获取图像总像素数: 74 | 75 | ```python 76 | print(img.size) # 263*247*3=194883 77 | ``` 78 | 79 | ### ROI 80 | 81 | [ROI](https://baike.baidu.com/item/ROI/1125333#viewPageContent):Region of Interest,感兴趣区域。什么意思呢?比如我们要检测眼睛,因为眼睛肯定在脸上,所以我们感兴趣的只有脸这部分,其他都不 care,所以可以单独把脸截取出来,这样就可以大大节省计算量,提高运行速度。 82 | 83 | ![只关心脸 ( ╯□╰ )](http://cos.codec.wang/cv2_lena_face_roi_crop.jpg) 84 | 85 | 截取 ROI 非常简单,指定图片的范围即可(后面我们学了特征后,就可以自动截取辣,\(ง •\_•\)ง): 86 | 87 | ```python 88 | # 截取脸部 ROI 89 | face = img[100:200, 115:188] 90 | cv2.imshow('face', face) 91 | cv2.waitKey(0) 92 | ``` 93 | 94 | ### 通道分割与合并 95 | 96 | 彩色图的 BGR 三个通道是可以分开单独访问的,也可以将单独的三个通道合并成一副图像。分别使用`cv2.split()`和`cv2.merge()`: 97 | 98 | ```python 99 | b, g, r = cv2.split(img) 100 | img = cv2.merge((b, g, r)) 101 | ``` 102 | 103 | `split()`函数比较耗时,**更高效的方式是用 numpy 中的索引**,如提取 B 通道: 104 | 105 | ```python 106 | b = img[:, :, 0] 107 | cv2.imshow('blue', b) 108 | cv2.waitKey(0) 109 | ``` 110 | 111 | ## 小结 112 | 113 | - `img[y,x]`获取/设置像素点值,`img.shape`:图片的形状(行数、列数、通道数),`img.dtype`:图像的数据类型。 114 | - `img[y1:y2,x1:x2]`进行 ROI 截取,`cv2.split()/cv2.merge()`通道分割/合并。更推荐的获取单通道方式:`b = img[:, :, 0]`。 115 | 116 | ## 练习 117 | 118 | 1. 打开 lena.jpg,将帽子部分(高:25~120,宽:50~220)的红色通道截取出来并显示。 119 | 120 | ## 接口文档 121 | 122 | - [cv2.split()](https://docs.opencv.org/4.0.0/d2/de8/group__core__array.html#ga0547c7fed86152d7e9d0096029c8518a) 123 | - [cv2.merge()](https://docs.opencv.org/4.0.0/d2/de8/group__core__array.html#ga7d7b4d6c6ee504b30a20b1680029c7b4) 124 | 125 | ## 引用 126 | 127 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/04-Basic-Operations) 128 | - [Basic Operations on Images](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_core/py_basic_ops/py_basic_ops.html#basic-ops) 129 | -------------------------------------------------------------------------------- /docs/basic/09-image-blending.md: -------------------------------------------------------------------------------- 1 | # 09: 图像混合 2 | 3 | ![](http://cos.codec.wang/cv2_image_blending_6_4.jpg) 4 | 5 | 学习图片间的数学运算,图像混合。图片等可到文末引用处下载。 6 | 7 | ## 目标 8 | 9 | - 图片间的数学运算,如相加、按位运算等 10 | - OpenCV 函数:`cv2.add()`, `cv2.addWeighted()`, `cv2.bitwise_and()` 11 | 12 | ## 教程 13 | 14 | > 首先恭喜你已经完成了入门篇的学习噢,接下来我们学习一些 OpenCV 的基础内容,加油\(ง •\_•\)ง 15 | 16 | ### 图片相加 17 | 18 | 要叠加两张图片,可以用`cv2.add()`函数,相加两幅图片的形状(高度/宽度/通道数)必须相同。numpy 中可以直接用 res = img + img1 相加,但这两者的结果并不相同: 19 | 20 | ```python 21 | x = np.uint8([250]) 22 | y = np.uint8([10]) 23 | print(cv2.add(x, y)) # 250+10 = 260 => 255 24 | print(x + y) # 250+10 = 260 % 256 = 4 25 | ``` 26 | 27 | 如果是二值化图片(只有 0 和 255 两种值),两者结果是一样的(用 numpy 的方式更简便一些)。 28 | 29 | ### 图像混合 30 | 31 | 图像混合`cv2.addWeighted()`也是一种图片相加的操作,只不过两幅图片的权重不一样,γ 相当于一个修正值: 32 | 33 | $$ 34 | dst = \alpha\times img1+\beta\times img2 + \gamma 35 | $$ 36 | 37 | ```python 38 | img1 = cv2.imread('lena_small.jpg') 39 | img2 = cv2.imread('opencv-logo-white.png') 40 | res = cv2.addWeighted(img1, 0.6, img2, 0.4, 0) 41 | ``` 42 | 43 | ![图像混合](http://cos.codec.wang/cv2_image_blending_6_4.jpg) 44 | 45 | :::tip 46 | α 和 β 都等于 1 时,就相当于图片相加。 47 | ::: 48 | 49 | ### 按位操作 50 | 51 | 按位操作包括按位与/或/非/异或操作,有什么用途呢?比如说我们要实现下图的效果: 52 | 53 | ![](http://cos.codec.wang/cv2_bitwise_operations_demo.jpg) 54 | 55 | 如果将两幅图片直接相加会改变图片的颜色,如果用图像混合,则会改变图片的透明度,所以我们需要用按位操作。首先来了解一下[掩膜](https://baike.baidu.com/item/%E6%8E%A9%E8%86%9C/8544392?fr=aladdin)(mask)的概念:掩膜是用一副二值化图片对另外一幅图片进行局部的遮挡,看下图就一目了然了: 56 | 57 | ![掩膜概念](http://cos.codec.wang/cv2_understand_mask.jpg) 58 | 59 | 所以我们的思路就是把原图中要放 logo 的区域抠出来,再把 logo 放进去就行了: 60 | 61 | ```python 62 | img1 = cv2.imread('lena.jpg') 63 | img2 = cv2.imread('opencv-logo-white.png') 64 | 65 | # 把 logo 放在左上角,所以我们只关心这一块区域 66 | rows, cols = img2.shape[:2] 67 | roi = img1[:rows, :cols] 68 | 69 | # 创建掩膜 70 | img2gray = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY) 71 | ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY) 72 | mask_inv = cv2.bitwise_not(mask) 73 | 74 | # 保留除 logo 外的背景 75 | img1_bg = cv2.bitwise_and(roi, roi, mask=mask_inv) 76 | dst = cv2.add(img1_bg, img2) # 进行融合 77 | img1[:rows, :cols] = dst # 融合后放在原图上 78 | ``` 79 | 80 | :::tip 81 | 掩膜的概念在图像混合/叠加的场景下使用较多,可以多多练习噢! 82 | ::: 83 | 84 | ## 小结 85 | 86 | - `cv2.add()`用来叠加两幅图片,`cv2.addWeighted()`也是叠加两幅图片,但两幅图片的权重不一样。 87 | - `cv2.bitwise_and()`, `cv2.bitwise_not()`, `cv2.bitwise_or()`, `cv2.bitwise_xor()`分别执行按位与/或/非/异或运算。掩膜就是用来对图片进行全局或局部的遮挡。 88 | 89 | ## 接口文档 90 | 91 | - [cv2.add\(\)](https://docs.opencv.org/4.0.0/d2/de8/group__core__array.html#ga10ac1bfb180e2cfda1701d06c24fdbd6) 92 | - [cv2.addWeighted\(\)](https://docs.opencv.org/4.0.0/d2/de8/group__core__array.html#gafafb2513349db3bcff51f54ee5592a19) 93 | - [cv2.bitwise_and\(\)](https://docs.opencv.org/4.0.0/d2/de8/group__core__array.html#ga60b4d04b251ba5eb1392c34425497e14) 94 | - [cv2.bitwise_not\(\)](https://docs.opencv.org/4.0.0/d2/de8/group__core__array.html#ga0002cf8b418479f4cb49a75442baee2f) 95 | 96 | ## 引用 97 | 98 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/09-Image-Blending) 99 | - [掩膜](https://baike.baidu.com/item/%E6%8E%A9%E8%86%9C/8544392?fr=aladdin) 100 | - [Arithmetic Operations on Images](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_core/py_image_arithmetics/py_image_arithmetics.html) 101 | -------------------------------------------------------------------------------- /docs/start/extra-06-drawing-with-mouse.md: -------------------------------------------------------------------------------- 1 | # 番外篇:鼠标绘图 2 | 3 | ![](http://cos.codec.wang/cv2_live_draw_rectangle.gif) 4 | 5 | 学习如何用鼠标实时绘图。图片等可到文末引用处下载。 6 | 7 | ## 目标 8 | 9 | - 捕获鼠标事件 10 | - OpenCV 函数:`cv2.setMouseCallback()` 11 | 12 | ## 教程 13 | 14 | ### 知道鼠标在哪儿 15 | 16 | OpenCV 中,我们需要创建一个鼠标的回调函数来获取鼠标当前的位置、当前的事件如左键按下/左键释放或是右键单击等等,然后执行相应的功能。 17 | 18 | 使用`cv2.setMouseCallback()`来创建鼠标的回调函数,比如我们在左键单击的时候,打印出当前鼠标的位置: 19 | 20 | ```python 21 | import cv2 22 | import numpy as np 23 | 24 | # 鼠标的回调函数 25 | def mouse_event(event, x, y, flags, param): 26 | # 通过 event 判断具体是什么事件,这里是左键按下 27 | if event == cv2.EVENT_LBUTTONDOWN: 28 | print((x, y)) 29 | 30 | img = np.zeros((512, 512, 3), np.uint8) 31 | cv2.namedWindow('image') 32 | # 定义鼠标的回调函数 33 | cv2.setMouseCallback('image', mouse_event) 34 | 35 | while(True): 36 | cv2.imshow('image', img) 37 | # 按下 ESC 键退出 38 | if cv2.waitKey(20) == 27: 39 | break 40 | ``` 41 | 42 | 上面的代码先定义鼠标的回调函数`mouse_event()`,然后在回调函数中判断是否是左键单击事件 `EVENT_LBUTTONDOWN`,是的话就打印出坐标。需要注意的是,回调函数的参数格式是固定的,不要随意更改。 43 | 44 | 那除了左键单击之外,还有哪些事件呢?可以用下面的代码打印出来: 45 | 46 | ```python 47 | # 获取所有的事件 48 | events = [i for i in dir(cv2) if 'EVENT' in i] 49 | print(events) 50 | ``` 51 | 52 | ### 综合实例 53 | 54 | 现在我们来实现一个综合的例子,这个实例会帮助你理解图像交互的一些思想: 55 | 56 | 在图像上用鼠标画图,可以画圆或矩形,按 m 键在两种模式下切换。左键按下时开始画图,移动到哪儿画到哪儿,左键释放时结束画图。听上去很复杂,是吗?一步步来看: 57 | 58 | - 用鼠标画图:需要定义鼠标的回调函数 mouse_event 59 | - 画圆或矩形:需要定义一个画图的模式 mode 60 | - 左键单击、移动、释放:需要捕获三个不同的事件 61 | - 开始画图,结束画图:需要定义一个画图的标记位 drawing 62 | 63 | 好,开始 coding 吧: 64 | 65 | ```python 66 | import cv2 67 | import numpy as np 68 | 69 | drawing = False # 是否开始画图 70 | mode = True # True:画矩形,False:画圆 71 | start = (-1, -1) 72 | 73 | def mouse_event(event, x, y, flags, param): 74 | global start, drawing, mode 75 | 76 | # 左键按下:开始画图 77 | if event == cv2.EVENT_LBUTTONDOWN: 78 | drawing = True 79 | start = (x, y) 80 | # 鼠标移动,画图 81 | elif event == cv2.EVENT_MOUSEMOVE: 82 | if drawing: 83 | if mode: 84 | cv2.rectangle(img, start, (x, y), (0, 255, 0), 1) 85 | else: 86 | cv2.circle(img, (x, y), 5, (0, 0, 255), -1) 87 | # 左键释放:结束画图 88 | elif event == cv2.EVENT_LBUTTONUP: 89 | drawing = False 90 | if mode: 91 | cv2.rectangle(img, start, (x, y), (0, 255, 0), 1) 92 | else: 93 | cv2.circle(img, (x, y), 5, (0, 0, 255), -1) 94 | 95 | 96 | img = np.zeros((512, 512, 3), np.uint8) 97 | cv2.namedWindow('image') 98 | cv2.setMouseCallback('image', mouse_event) 99 | 100 | while(True): 101 | cv2.imshow('image', img) 102 | # 按下 m 切换模式 103 | if cv2.waitKey(1) == ord('m'): 104 | mode = not mode 105 | elif cv2.waitKey(1) == 27: 106 | break 107 | ``` 108 | 109 | 效果应该如下图所示: 110 | 111 | ![](http://cos.codec.wang/cv2_mouse_drawing_rectangle_circle.jpg) 112 | 113 | ## 小结 114 | 115 | - 要用鼠标绘图,需要用`cv2.setMouseCallback()`定义回调函数,然后在回调函数中根据不同的 event 事件,执行不同的功能。 116 | 117 | ## 练习 118 | 119 | 1.(选做)实现用鼠标画矩形,跟实例差不多,但只实时画一个,类似下面动图: 120 | 121 | ![实时画一个矩形](http://cos.codec.wang/cv2_live_draw_rectangle.gif) 122 | 123 | 2.(选做)做一个在白色面板上绘图的简单程序,可用滑动条调整颜色和笔刷大小。 124 | 125 | ## 引用 126 | 127 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/Extra-06-Drawing-with-Mouse) 128 | - [Mouse as a Paint-Brush](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_gui/py_mouse_handling/py_mouse_handling.html) 129 | -------------------------------------------------------------------------------- /docs/start/07-image-geometric-transformation.md: -------------------------------------------------------------------------------- 1 | # 07: 图像几何变换 2 | 3 | ![](http://cos.codec.wang/cv2_perspective_transformations_inm.jpg) 4 | 5 | 学习如何旋转、平移、缩放和翻转图片。图片等可到文末引用处下载。 6 | 7 | ## 目标 8 | 9 | - 实现旋转、平移和缩放图片 10 | - OpenCV 函数:`cv2.resize()`, `cv2.flip()`, `cv2.warpAffine()` 11 | 12 | ## 教程 13 | 14 | > 图像的几何变换从原理上看主要包括两种:基于 2×3 矩阵的仿射变换(平移、缩放、旋转和翻转等)、基于 3×3 矩阵的透视变换,感兴趣的小伙伴可参考 [番外篇:仿射变换与透视变换](./extra-05-warpaffine-warpperspective/)。 15 | 16 | ### 缩放图片 17 | 18 | 缩放就是调整图片的大小,使用`cv2.resize()`函数实现缩放。可以按照比例缩放,也可以按照指定的大小缩放: 19 | 20 | ```python 21 | import cv2 22 | 23 | img = cv2.imread('drawing.jpg') 24 | 25 | # 按照指定的宽度、高度缩放图片 26 | res = cv2.resize(img, (132, 150)) 27 | # 按照比例缩放,如 x,y 轴均放大一倍 28 | res2 = cv2.resize(img, None, fx=2, fy=2, interpolation=cv2.INTER_LINEAR) 29 | 30 | cv2.imshow('shrink', res), cv2.imshow('zoom', res2) 31 | cv2.waitKey(0) 32 | ``` 33 | 34 | 我们也可以指定缩放方法`interpolation`,更专业点叫插值方法,默认是`INTER_LINEAR`,全部可以参考:[InterpolationFlags](https://docs.opencv.org/4.0.0/da/d54/group__imgproc__transform.html#ga5bb5a1fea74ea38e1a5445ca803ff121) 35 | 36 | ### 翻转图片 37 | 38 | 镜像翻转图片,可以用`cv2.flip()`函数: 39 | 40 | ```python 41 | dst = cv2.flip(img, 1) 42 | ``` 43 | 44 | 其中,参数 2 = 0:垂直翻转 (沿 x 轴),参数 2 > 0: 水平翻转 (沿 y 轴),参数 2 < 0: 水平垂直翻转。 45 | 46 | ![](http://cos.codec.wang/cv2_flip_image_sample.jpg) 47 | 48 | ### 平移图片 49 | 50 | 要平移图片,我们需要定义下面这样一个矩阵,tx,ty 是向 x 和 y 方向平移的距离: 51 | 52 | $$ 53 | M = \left[ 54 | \begin{matrix} 55 | 1 & 0 & t_x \newline 56 | 0 & 1 & t_y 57 | \end{matrix} 58 | \right] 59 | $$ 60 | 61 | 平移是用仿射变换函数`cv2.warpAffine()`实现的: 62 | 63 | ```python 64 | # 平移图片 65 | import numpy as np 66 | 67 | rows, cols = img.shape[:2] 68 | 69 | # 定义平移矩阵,需要是 numpy 的 float32 类型 70 | # x 轴平移 100,y 轴平移 50 71 | M = np.float32([[1, 0, 100], [0, 1, 50]]) 72 | # 用仿射变换实现平移 73 | dst = cv2.warpAffine(img, M, (cols, rows)) 74 | 75 | cv2.imshow('shift', dst) 76 | cv2.waitKey(0) 77 | ``` 78 | 79 | ![](http://cos.codec.wang/cv2_translation_100_50.jpg) 80 | 81 | ### 旋转图片 82 | 83 | 旋转同平移一样,也是用仿射变换实现的,因此也需要定义一个变换矩阵。OpenCV 直接提供了 `cv2.getRotationMatrix2D()`函数来生成这个矩阵,该函数有三个参数: 84 | 85 | - 参数 1:图片的旋转中心 86 | - 参数 2:旋转角度 (正:逆时针,负:顺时针) 87 | - 参数 3:缩放比例,0.5 表示缩小一半 88 | 89 | ```python 90 | # 45°旋转图片并缩小一半 91 | M = cv2.getRotationMatrix2D((cols / 2, rows / 2), 45, 0.5) 92 | dst = cv2.warpAffine(img, M, (cols, rows)) 93 | 94 | cv2.imshow('rotation', dst) 95 | cv2.waitKey(0) 96 | ``` 97 | 98 | ![逆时针旋转 45°并缩放](http://cos.codec.wang/cv2_rotation_45_degree.jpg) 99 | 100 | ## 小结 101 | 102 | - `cv2.resize()`缩放图片,可以按指定大小缩放,也可以按比例缩放。 103 | - `cv2.flip()`翻转图片,可以指定水平/垂直/水平垂直翻转三种方式。 104 | - 平移/旋转是靠仿射变换`cv2.warpAffine()`实现的。 105 | 106 | ## 接口文档 107 | 108 | - [cv2.resize\(\)](https://docs.opencv.org/4.0.0/da/d54/group__imgproc__transform.html#ga47a974309e9102f5f08231edc7e7529d) 109 | - [cv2.filp\(\)](https://docs.opencv.org/4.0.0/d2/de8/group__core__array.html#gaca7be533e3dac7feb70fc60635adf441) 110 | - [cv2.warpAffine\(\)](https://docs.opencv.org/4.0.0/da/d54/group__imgproc__transform.html#ga0203d9ee5fcd28d40dbc4a1ea4451983) 111 | - [cv2.getRotationMatrix2D\(\)](https://docs.opencv.org/4.0.0/da/d54/group__imgproc__transform.html#gafbbc470ce83812914a70abfb604f4326) 112 | 113 | ## 引用 114 | 115 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/07-Image-Geometric-Transformation) 116 | - [Geometric Transformations of Images](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_geometric_transformations/py_geometric_transformations.html) 117 | -------------------------------------------------------------------------------- /docs/basic/extra-11-convex-hull.md: -------------------------------------------------------------------------------- 1 | # 番外篇:凸包及更多轮廓特征 2 | 3 | ![](http://cos.codec.wang/cv2_understand_convex.jpg) 4 | 5 | 计算凸包及更多轮廓特征。图片等可到文末引用处下载。 6 | 7 | ## 多边形逼近 8 | 9 | 前面我们学习过最小外接矩和最小外接圆,那么可以用一个最小的多边形包围物体吗?当然可以: 10 | 11 | ```python 12 | import cv2 13 | import numpy as np 14 | 15 | # 1.先找到轮廓 16 | img = cv2.imread('unregular.jpg', 0) 17 | _, thresh = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) 18 | image, contours, hierarchy = cv2.findContours(thresh, 3, 2) 19 | cnt = contours[0] 20 | 21 | # 2.进行多边形逼近,得到多边形的角点 22 | approx = cv2.approxPolyDP(cnt, 3, True) 23 | 24 | # 3.画出多边形 25 | image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) 26 | cv2.polylines(image, [approx], True, (0, 255, 0), 2) 27 | ``` 28 | 29 | 其中`cv2.approxPolyDP()`的参数 2(epsilon) 是一个距离值,表示多边形的轮廓接近实际轮廓的程度,值越小,越精确;参数 3 表示是否闭合。 30 | 31 | ![](http://cos.codec.wang/cv2_understand_approxpoly.jpg) 32 | 33 | ## 凸包 34 | 35 | [凸包](https://baike.baidu.com/item/%E5%87%B8%E5%8C%85/179150?fr=aladdin)跟多边形逼近很像,只不过它是物体最外层的"凸"多边形:集合 A 内连接任意两个点的直线都在 A 的内部,则称集合 A 是凸形的。如下图,红色的部分为手掌的凸包,双箭头部分表示凸缺陷 (Convexity Defects),凸缺陷常用来进行手势识别等: 36 | 37 | ![](http://cos.codec.wang/cv2_understand_convex.jpg) 38 | 39 | ```python 40 | # 1.先找到轮廓 41 | img = cv2.imread('convex.jpg', 0) 42 | _, thresh = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) 43 | image, contours, hierarchy = cv2.findContours(thresh, 3, 2) 44 | cnt = contours[0] 45 | 46 | # 2.寻找凸包,得到凸包的角点 47 | hull = cv2.convexHull(cnt) 48 | 49 | # 3.绘制凸包 50 | image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) 51 | cv2.polylines(image, [hull], True, (0, 255, 0), 2) 52 | ``` 53 | 54 | ![](http://cos.codec.wang/cv2_convex_hull.jpg) 55 | 56 | 其中函数`cv2.convexHull()`有个可选参数 returnPoints,默认是 True,代表返回角点的 x/y 坐标;如果为 False 的话,表示返回轮廓中是凸包角点的索引,比如说: 57 | 58 | ```python 59 | print(hull[0]) # [[362 184]](坐标) 60 | hull2 = cv2.convexHull(cnt, returnPoints=False) 61 | print(hull2[0]) # [510](cnt 中的索引) 62 | print(cnt[510]) # [[362 184]] 63 | ``` 64 | 65 | 当使用`cv2.convexityDefects()`计算凸包缺陷时,returnPoints 需为 False,详情可参考:[Convexity Defects](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contours_more_functions/py_contours_more_functions.html#contours-more-functions) 66 | 67 | 另外可以用下面的语句来判断轮廓是否是凸形的: 68 | 69 | ```python 70 | print(cv2.isContourConvex(hull)) # True 71 | ``` 72 | 73 | ## 点到轮廓距离 74 | 75 | `cv2.pointPolygonTest()`函数计算点到轮廓的最短距离(也就是垂线),又称多边形测试: 76 | 77 | ```python 78 | dist = cv2.pointPolygonTest(cnt, (100, 100), True) # -3.53 79 | ``` 80 | 81 | 其中参数 3 为 True 时表示计算距离值:点在轮廓外面值为负,点在轮廓上值为 0,点在轮廓里面值为正;参数 3 为 False 时,只返回-1/0/1 表示点相对轮廓的位置,不计算距离。 82 | 83 | 更多轮廓特征,如当量直径、平均强度等,我目前也没用到过,以后用到再写吧,感兴趣的可以参看:[Contour Properties](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contour_properties/py_contour_properties.html)、[Contours Hierarchy](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contours_hierarchy/py_contours_hierarchy.html) 84 | 85 | ## 引用 86 | 87 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/Extra-11-Convex-Hull) 88 | - [Convexity Defects](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contours_more_functions/py_contours_more_functions.html#contours-more-functions) 89 | - [Contour Properties](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contour_properties/py_contour_properties.html) 90 | - [Contours Hierarchy](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contours_hierarchy/py_contours_hierarchy.html) 91 | -------------------------------------------------------------------------------- /docs/start/05-changing-colorspaces.md: -------------------------------------------------------------------------------- 1 | # 05: 颜色空间转换 2 | 3 | ![](http://cos.codec.wang/cv2_exercise_tracking_three_colors.jpg) 4 | 5 | 学习如何进行图片的颜色空间转换,视频中追踪特定颜色的物体。图片等可到文末引用处下载。 6 | 7 | ## 目标 8 | 9 | - 颜色空间转换,如 BGR↔Gray,BGR↔HSV 等 10 | - 追踪视频中特定颜色的物体 11 | - OpenCV 函数:`cv2.cvtColor()`, `cv2.inRange()` 12 | 13 | ## 教程 14 | 15 | ### 颜色空间转换 16 | 17 | ```python 18 | import cv2 19 | 20 | img = cv2.imread('lena.jpg') 21 | # 转换为灰度图 22 | img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 23 | 24 | cv2.imshow('img', img) 25 | cv2.imshow('gray', img_gray) 26 | cv2.waitKey(0) 27 | ``` 28 | 29 | `cv2.cvtColor()`用来进行颜色模型转换,参数 1 是要转换的图片,参数 2 是转换模式, `COLOR_BGR2GRAY`表示 BGR→Gray,可用下面的代码显示所有的转换模式: 30 | 31 | ```python 32 | flags = [i for i in dir(cv2) if i.startswith('COLOR_')] 33 | print(flags) 34 | ``` 35 | 36 | :::tip 37 | 颜色转换其实是数学运算,如灰度化最常用的是:`gray=R*0.299+G*0.587+B*0.114`。 38 | ::: 39 | 40 | ### 视频中特定颜色物体追踪 41 | 42 | [HSV](https://baike.baidu.com/item/HSV/547122)是一个常用于颜色识别的模型,相比 BGR 更易区分颜色,转换模式用`COLOR_BGR2HSV`表示。 43 | 44 | :::tip 45 | OpenCV 中色调 H 范围为\[0,179\],饱和度 S 是\[0,255\],明度 V 是\[0,255\]。虽然 H 的理论数值是 0°~360°,但 8 位图像像素点的最大值是 255,所以 OpenCV 中除以了 2,某些软件可能使用不同的尺度表示,所以同其他软件混用时,记得归一化。 46 | ::: 47 | 48 | 现在,我们实现一个使用 HSV 来只显示视频中蓝色物体的例子,步骤如下: 49 | 50 | 1. 捕获视频中的一帧 51 | 2. 从 BGR 转换到 HSV 52 | 3. 提取蓝色范围的物体 53 | 4. 只显示蓝色物体 54 | 55 | ![](http://cos.codec.wang/cv2_blue_object_tracking.jpg) 56 | 57 | ```python 58 | import numpy as np 59 | 60 | capture = cv2.VideoCapture(0) 61 | 62 | # 蓝色的范围,不同光照条件下不一样,可灵活调整 63 | lower_blue = np.array([100, 110, 110]) 64 | upper_blue = np.array([130, 255, 255]) 65 | 66 | while(True): 67 | # 1.捕获视频中的一帧 68 | ret, frame = capture.read() 69 | 70 | # 2.从 BGR 转换到 HSV 71 | hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) 72 | 73 | # 3.inRange():介于 lower/upper 之间的为白色,其余黑色 74 | mask = cv2.inRange(hsv, lower_blue, upper_blue) 75 | 76 | # 4.只保留原图中的蓝色部分 77 | res = cv2.bitwise_and(frame, frame, mask=mask) 78 | 79 | cv2.imshow('frame', frame) 80 | cv2.imshow('mask', mask) 81 | cv2.imshow('res', res) 82 | 83 | if cv2.waitKey(1) == ord('q'): 84 | break 85 | ``` 86 | 87 | 其中,`bitwise_and()`函数暂时不用管,后面会讲到。那蓝色的 HSV 值的上下限 lower 和 upper 范围是怎么得到的呢?其实很简单,我们先把标准蓝色的 BGR 值用`cvtColor()`转换下: 88 | 89 | ```python 90 | blue = np.uint8([[[255, 0, 0]]]) 91 | hsv_blue = cv2.cvtColor(blue, cv2.COLOR_BGR2HSV) 92 | print(hsv_blue) # [[[120 255 255]]] 93 | ``` 94 | 95 | 结果是\[120, 255, 255\],所以,我们把蓝色的范围调整成了上面代码那样。 96 | 97 | :::tip 98 | [Lab](https://baike.baidu.com/item/Lab/1514615) 颜色空间也经常用来做颜色识别,有兴趣的同学可以了解下。 99 | ::: 100 | 101 | ## 小结 102 | 103 | - `cv2.cvtColor()`函数用来进行颜色空间转换,常用 BGR↔Gray,BGR↔HSV。 104 | - HSV 颜色模型常用于颜色识别。要想知道某种颜色在 HSV 下的值,可以将它的 BGR 值用`cvtColor()`转换得到。 105 | 106 | ## 练习 107 | 108 | 1. 尝试在视频中同时提取红色、蓝色、绿色的物体。(效果如下) 109 | 110 | ![](http://cos.codec.wang/cv2_exercise_tracking_three_colors.jpg) 111 | 112 | ## 接口文档 113 | 114 | - [cv2.cvtColor\(\)](https://docs.opencv.org/4.0.0/d8/d01/group__imgproc__color__conversions.html#ga397ae87e1288a81d2363b61574eb8cab) 115 | - [cv2.inRange\(\)](https://docs.opencv.org/4.0.0/d2/de8/group__core__array.html#ga48af0ab51e36436c5d04340e036ce981) 116 | - [cv2.bitwise_and\(\)](https://docs.opencv.org/4.0.0/d2/de8/group__core__array.html#ga60b4d04b251ba5eb1392c34425497e14) 117 | 118 | ## 引用 119 | 120 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/05-Changing-Colorspaces) 121 | - [Changing Colorspaces](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_colorspaces/py_colorspaces.html) 122 | -------------------------------------------------------------------------------- /docs/basic/11-edge-detection.md: -------------------------------------------------------------------------------- 1 | # 11: 边缘检测 2 | 3 | ![](http://cos.codec.wang/cv2_canny_edge_detection_threshold.jpg) 4 | 5 | 学习使用 Canny 获取图像的边缘。图片等可到文末引用处下载。 6 | 7 | > [Canny J . A Computational Approach To Edge Detection\[J\]. IEEE Transactions on Pattern Analysis and Machine Intelligence, 1986, PAMI-8\(6\):679-698.](https://www.computer.org/cms/Computer.org/Transactions%20Home%20Pages/TPAMI/PDFs/top_ten_6.pdf) 8 | 9 | ## 目标 10 | 11 | - Canny 边缘检测的简单概念 12 | - OpenCV 函数:`cv2.Canny()` 13 | 14 | ## 教程 15 | 16 | Canny 边缘检测方法常被誉为边缘检测的最优方法,废话不多说,先看个例子: 17 | 18 | ```python 19 | import cv2 20 | import numpy as np 21 | 22 | img = cv2.imread('handwriting.jpg', 0) 23 | edges = cv2.Canny(img, 30, 70) # canny 边缘检测 24 | 25 | cv2.imshow('canny', np.hstack((img, edges))) 26 | cv2.waitKey(0) 27 | ``` 28 | 29 | ![](http://cos.codec.wang/cv2_canny_edge_detection.jpg) 30 | 31 | `cv2.Canny()`进行边缘检测,参数 2、3 表示最低、高阈值,下面来解释下具体原理。 32 | 33 | :::tip 34 | 之前我们用低通滤波的方式模糊了图片,那反过来,想得到物体的边缘,就需要用到高通滤波。推荐先阅读:[番外篇:图像梯度](./extra-09-image-gradients/)。 35 | ::: 36 | 37 | ### Canny 边缘检测 38 | 39 | Canny 边缘提取的具体步骤如下: 40 | 41 | 1,使用 5×5 高斯滤波消除噪声: 42 | 43 | 边缘检测本身属于锐化操作,对噪点比较敏感,所以需要进行平滑处理。高斯滤波的具体内容参考前一篇:[平滑图像](./smoothing-images) 44 | 45 | $$ 46 | K=\frac{1}{256}\left[ 47 | \begin{matrix} 48 | 1 & 4 & 6 & 4 & 1 \newline 49 | 4 & 16 & 24 & 16 & 4 \newline 50 | 6 & 24 & 36 & 24 & 6 \newline 51 | 4 & 16 & 24 & 16 & 4 \newline 52 | 1 & 4 & 6 & 4 & 1 53 | \end{matrix} 54 | \right] 55 | $$ 56 | 57 | 2,计算图像梯度的方向: 58 | 59 | 首先使用 Sobel 算子计算两个方向上的梯度$G_x$和$G_y$,然后算出梯度的方向: 60 | 61 | $$ 62 | \theta=\arctan(\frac{G_y}{G_x}) 63 | $$ 64 | 65 | 保留这四个方向的梯度:0°/45°/90°/135°,有什么用呢?我们接着看。 66 | 67 | 3,取局部极大值: 68 | 69 | 梯度其实已经表示了轮廓,但为了进一步筛选,可以在上面的四个角度方向上再取局部极大值: 70 | 71 | ![](http://cos.codec.wang/cv2_understand_canny_direction.jpg) 72 | 73 | 比如,A 点在 45° 方向上大于 B/C 点,那就保留它,把 B/C 设置为 0。 74 | 75 | 4,滞后阈值: 76 | 77 | 经过前面三步,就只剩下 0 和可能的边缘梯度值了,为了最终确定下来,需要设定高低阈值: 78 | 79 | ![](http://cos.codec.wang/cv2_understand_canny_max_min_val.jpg) 80 | 81 | - 像素点的值大于最高阈值,那肯定是边缘(上图 A) 82 | - 同理像素值小于最低阈值,那肯定不是边缘 83 | - 像素值介于两者之间,如果与高于最高阈值的点连接,也算边缘,所以上图中 C 算,B 不算 84 | 85 | Canny 推荐的高低阈值比在 2:1 到 3:1 之间。 86 | 87 | ### 先阈值分割后检测 88 | 89 | 其实很多情况下,阈值分割后再检测边缘,效果会更好: 90 | 91 | ```python 92 | _, thresh = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) 93 | edges = cv2.Canny(thresh, 30, 70) 94 | 95 | cv2.imshow('canny', np.hstack((img, thresh, edges))) 96 | cv2.waitKey(0) 97 | ``` 98 | 99 | 代码中我用了[番外篇:Otsu 阈值法](../start/extra-04-otsu-thresholding/)中的自动阈值分割,如果你不太了解,大可以使用传统的方法,不过如果是下面这种图片,推荐用 Otsu 阈值法。另外 Python 中某个值不用的话,就写个下划线'\_'。 100 | 101 | ![](http://cos.codec.wang/cv2_canny_edge_detection_threshold.jpg) 102 | 103 | ## 练习 104 | 105 | 1. (选做)如果你不太理解高低阈值的效果,创建两个滑动条来调节它们的值看看: 106 | 107 | ![](http://cos.codec.wang/cv2_trackbar_maxval_minval_canny.gif) 108 | 109 | ## 小结 110 | 111 | - Canny 是用的最多的边缘检测算法,用`cv2.Canny()`实现。 112 | 113 | ## 接口文档 114 | 115 | - [cv2.Canny()](https://docs.opencv.org/4.0.0/dd/d1a/group__imgproc__feature.html#ga04723e007ed888ddf11d9ba04e2232de) 116 | 117 | ## 引用 118 | 119 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/11-Edge-Detection) 120 | - [Canny Edge Detection](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_canny/py_canny.html) 121 | - [Canny 边缘检测](http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/imgproc/imgtrans/canny_detector/canny_detector.html) 122 | - [Canny J . A Computational Approach To Edge Detection\[J\]. IEEE Transactions on Pattern Analysis and Machine Intelligence, 1986, PAMI-8\(6\):679-698.](https://www.computer.org/cms/Computer.org/Transactions%20Home%20Pages/TPAMI/PDFs/top_ten_6.pdf) 123 | -------------------------------------------------------------------------------- /docs/start/03-open-camera.md: -------------------------------------------------------------------------------- 1 | # 03: 打开摄像头 2 | 3 | 学习打开摄像头捕获照片、播放本地视频、录制视频等。图片/视频等可到文末引用处下载。 4 | 5 | ## 目标 6 | 7 | - 打开摄像头并捕获照片 8 | - 播放本地视频,录制视频 9 | - OpenCV 函数:`cv2.VideoCapture()`, `cv2.VideoWriter()` 10 | 11 | ## 教程 12 | 13 | ### 打开摄像头 14 | 15 | 要使用摄像头,需要使用`cv2.VideoCapture(0)`创建 VideoCapture 对象,参数 0 指的是摄像头的编号,如果你电脑上有两个摄像头的话,访问第 2 个摄像头就可以传入 1,依此类推。 16 | 17 | ```python 18 | # 打开摄像头并灰度化显示 19 | import cv2 20 | 21 | capture = cv2.VideoCapture(0) 22 | 23 | while(True): 24 | # 获取一帧 25 | ret, frame = capture.read() 26 | # 将这帧转换为灰度图 27 | gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) 28 | 29 | cv2.imshow('frame', gray) 30 | if cv2.waitKey(1) == ord('q'): 31 | break 32 | ``` 33 | 34 | `capture.read()`函数返回的第 1 个参数 ret(return value) 是一个布尔值,表示当前这一帧是否获取正确。`cv2.cvtColor()`用来转换颜色,这里将彩色图转成灰度图。 35 | 36 | 另外,通过`cap.get(propId)`可以获取摄像头的一些属性,比如捕获的分辨率,亮度和对比度等。propId 是从 0~18 的数字,代表不同的属性,完整的属性列表可以参考:[VideoCaptureProperties](https://docs.opencv.org/4.0.0/d4/d15/group__videoio__flags__base.html#gaeb8dd9c89c10a5c63c139bf7c4f5704d)。也可以使用`cap.set(propId,value)`来修改属性值。比如说,我们在 while 之前添加下面的代码: 37 | 38 | ```python 39 | # 获取捕获的分辨率 40 | # propId 可以直接写数字,也可以用 OpenCV 的符号表示 41 | width, height = capture.get(3), capture.get(4) 42 | print(width, height) 43 | 44 | # 以原分辨率的一倍来捕获 45 | capture.set(cv2.CAP_PROP_FRAME_WIDTH, width * 2) 46 | capture.set(cv2.CAP_PROP_FRAME_HEIGHT, height * 2) 47 | ``` 48 | 49 | :::tip 50 | 51 | 某些摄像头设定分辨率等参数时会无效,因为它有固定的分辨率大小支持,一般可在摄像头的资料页中找到。 52 | 53 | ::: 54 | 55 | ### 播放本地视频 56 | 57 | 跟打开摄像头一样,如果把摄像头的编号换成视频的路径就可以播放本地视频了。回想一下`cv2.waitKey()`,它的参数表示暂停时间,所以这个值越大,视频播放速度越慢,反之,播放速度越快,通常设置为 25 或 30。 58 | 59 | ```python 60 | # 播放本地视频 61 | capture = cv2.VideoCapture('demo_video.mp4') 62 | 63 | while(capture.isOpened()): 64 | ret, frame = capture.read() 65 | gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) 66 | 67 | cv2.imshow('frame', gray) 68 | if cv2.waitKey(30) == ord('q'): 69 | break 70 | ``` 71 | 72 | ### 录制视频 73 | 74 | 之前我们保存图片用的是`cv2.imwrite()`,要保存视频,我们需要创建一个`VideoWriter`的对象,需要给它传入四个参数: 75 | 76 | - 输出的文件名,如'output.avi' 77 | - 编码方式 [FourCC](https://baike.baidu.com/item/fourcc/6168470?fr=aladdin) 码 78 | - 帧率 [FPS](https://baike.baidu.com/item/FPS/3227416) 79 | - 要保存的分辨率大小 80 | 81 | FourCC 是用来指定视频编码方式的四字节码,所有的编码可参考[Video Codecs](http://www.fourcc.org/codecs.php)。如 MJPG 编码可以这样写: `cv2.VideoWriter_fourcc(*'MJPG')`或`cv2.VideoWriter_fourcc('M','J','P','G')` 82 | 83 | ```python 84 | capture = cv2.VideoCapture(0) 85 | 86 | # 定义编码方式并创建 VideoWriter 对象 87 | fourcc = cv2.VideoWriter_fourcc(*'MJPG') 88 | outfile = cv2.VideoWriter('output.avi', fourcc, 25., (640, 480)) 89 | 90 | while(capture.isOpened()): 91 | ret, frame = capture.read() 92 | 93 | if ret: 94 | outfile.write(frame) # 写入文件 95 | cv2.imshow('frame', frame) 96 | if cv2.waitKey(1) == ord('q'): 97 | break 98 | else: 99 | break 100 | ``` 101 | 102 | ## 小结 103 | 104 | - 使用`cv2.VideoCapture()`创建视频对象,然后在循环中一帧帧显示图像。参数传入数字时,代表打开摄像头,传入本地视频路径时,表示播放本地视频。 105 | - `cap.get(propId)`获取视频属性,`cap.set(propId,value)`设置视频属性。 106 | - `cv2.VideoWriter()`创建视频写入对象,用来录制/保存视频。 107 | 108 | ## 练习 109 | 110 | 1. 请先阅读[番外篇:滑动条](./extra-03-trackbar/),然后实现一个可以拖动滑块播放视频的功能。(提示:需要用到 `cv2.CAP_PROP_FRAME_COUNT`和`cv2.CAP_PROP_POS_FRAMES`两个属性)。 111 | 112 | ## 接口文档 113 | 114 | - [VideoCapture Object](https://docs.opencv.org/4.0.0/d8/dfe/classcv_1_1VideoCapture.html>) 115 | - [VideoWriter Object](https://docs.opencv.org/4.0.0/dd/d9e/classcv_1_1VideoWriter.html>) 116 | - [cv2.cvtColor()](https://docs.opencv.org/4.0.0/d8/d01/group__imgproc__color__conversions.html#ga397ae87e1288a81d2363b61574eb8cab) 117 | 118 | ## 引用 119 | 120 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/03-Open-Camera) 121 | - [Video Codecs by FOURCC](http://www.fourcc.org/codecs.php) 122 | - [Getting Started with Videos](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_gui/py_video_display/py_video_display.html) 123 | -------------------------------------------------------------------------------- /docs/basic/16-template-matching.md: -------------------------------------------------------------------------------- 1 | # 16: 模板匹配 2 | 3 | ![](http://cos.codec.wang/cv2_understand_template_matching.jpg) 4 | 5 | 学习使用模板匹配在图像中寻找物体。图片等可到文末引用处下载。 6 | 7 | ## 目标 8 | 9 | - 使用模板匹配在图像中寻找物体 10 | - OpenCV 函数:`cv2.matchTemplate()`, `cv2.minMaxLoc()` 11 | 12 | ## 教程 13 | 14 | ### 模板匹配 15 | 16 | [模板匹配](https://baike.baidu.com/item/模板匹配)就是用来在大图中找小图,也就是说在一副图像中寻找另外一张模板图像的位置: 17 | 18 | ![](http://cos.codec.wang/cv2_understand_template_matching.jpg) 19 | 20 | 用`cv2.matchTemplate()`实现模板匹配。首先我们来读入图片和模板: 21 | 22 | ```python 23 | import cv2 24 | import numpy as np 25 | from matplotlib import pyplot as plt 26 | 27 | img = cv2.imread('lena.jpg', 0) 28 | template = cv2.imread('face.jpg', 0) 29 | h, w = template.shape[:2] # rows->h, cols->w 30 | ``` 31 | 32 | 匹配函数返回的是一副灰度图,最白的地方表示最大的匹配。使用`cv2.minMaxLoc()`函数可以得到最大匹配值的坐标,以这个点为左上角角点,模板的宽和高画矩形就是匹配的位置了: 33 | 34 | ```python 35 | # 相关系数匹配方法:cv2.TM_CCOEFF 36 | res = cv2.matchTemplate(img, template, cv2.TM_CCOEFF) 37 | min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) 38 | 39 | left_top = max_loc # 左上角 40 | right_bottom = (left_top[0] + w, left_top[1] + h) # 右下角 41 | cv2.rectangle(img, left_top, right_bottom, 255, 2) # 画出矩形位置 42 | ``` 43 | 44 | ![](http://cos.codec.wang/cv2_ccoeff_matching_template.jpg) 45 | 46 | ### 原理 47 | 48 | > 这部分可看可不看,不太理解也没关系,还记得前面的方法吗?不懂得就划掉\(✿◕‿◕✿\) 49 | 50 | 模板匹配的原理其实很简单,就是不断地在原图中移动模板图像去比较,有 6 种不同的比较方法,详情可参考:[TemplateMatchModes](https://docs.opencv.org/3.3.1/df/dfb/group__imgproc__object.html#ga3a7850640f1fe1f58fe91a2d7583695d) 51 | 52 | - 平方差匹配 CV_TM_SQDIFF:用两者的平方差来匹配,最好的匹配值为 0 53 | - 归一化平方差匹配 CV_TM_SQDIFF_NORMED 54 | - 相关匹配 CV_TM_CCORR:用两者的乘积匹配,数值越大表明匹配程度越好 55 | - 归一化相关匹配 CV_TM_CCORR_NORMED 56 | - 相关系数匹配 CV_TM_CCOEFF:用两者的相关系数匹配,1 表示完美的匹配,-1 表示最差的匹配 57 | - 归一化相关系数匹配 CV_TM_CCOEFF_NORMED 58 | 59 | 归一化的意思就是将值统一到 0~1,这些方法的对比代码可到[源码处](#引用)查看。模板匹配也是应用卷积来实现的:假设原图大小为 W×H,模板图大小为 w×h,那么生成图大小是\(W-w+1\)×\(H-h+1\),生成图中的每个像素值表示原图与模板的匹配程度。 60 | 61 | ### 匹配多个物体 62 | 63 | 前面我们是找最大匹配的点,所以只能匹配一次。我们可以设定一个匹配阈值来匹配多次: 64 | 65 | ```python 66 | # 1.读入原图和模板 67 | img_rgb = cv2.imread('mario.jpg') 68 | img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY) 69 | template = cv2.imread('mario_coin.jpg', 0) 70 | h, w = template.shape[:2] 71 | 72 | # 2.标准相关模板匹配 73 | res = cv2.matchTemplate(img_gray, template, cv2.TM_CCOEFF_NORMED) 74 | threshold = 0.8 75 | 76 | # 3.这边是 Python/Numpy 的知识,后面解释 77 | loc = np.where(res >= threshold) # 匹配程度大于%80 的坐标 y,x 78 | for pt in zip(*loc[::-1]): # *号表示可选参数 79 | right_bottom = (pt[0] + w, pt[1] + h) 80 | cv2.rectangle(img_rgb, pt, right_bottom, (0, 0, 255), 2) 81 | ``` 82 | 83 | ![](http://cos.codec.wang/cv2_template_matching_multi.jpg) 84 | 85 | 第 3 步有几个 Python/Numpy 的重要知识,来大致看下: 86 | 87 | - [np.where\(\)](https://docs.scipy.org/doc/numpy/reference/generated/numpy.where.html)在这里返回 res 中值大于 0.8 的所有坐标,如: 88 | 89 | ```python 90 | x = np.arange(9.).reshape(3, 3) 91 | print(np.where(x > 5)) 92 | # 结果 (先 y 坐标,再 x 坐标):(array([2, 2, 2]), array([0, 1, 2])) 93 | ``` 94 | 95 | ![](http://cos.codec.wang/cv2_np_where_function.jpg) 96 | 97 | - [zip\(\)](https://docs.python.org/3/library/functions.html#zip)函数,功能强大到难以解释,举个简单例子就知道了: 98 | 99 | ```python 100 | x = [1, 2, 3] 101 | y = [4, 5, 6] 102 | print(list(zip(x, y))) # [(1, 4), (2, 5), (3, 6)] 103 | ``` 104 | 105 | 这样大家就能理解前面代码的用法了吧:因为 loc 是先 y 坐标再 x 坐标,所以用 loc\[::-1\] 翻转一下,然后再用 zip 函数拼接在一起。 106 | 107 | ## 练习 108 | 109 | 1. 之前我们有学过形状匹配,不论形状旋转/缩放都可以匹配到。思考一下,图片旋转或缩放的话模板匹配还有作用吗? 110 | 111 | ## 小结 112 | 113 | - 模板匹配用来在大图中找小图。 114 | - `cv2.matchTemplate()`用来进行模板匹配。 115 | 116 | ## 引用 117 | 118 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/16-Template-Matching) 119 | - [Template Matching](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_template_matching/py_template_matching.html) 120 | - [模板匹配](http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/imgproc/histograms/template_matching/template_matching.html#template-matching) 121 | - [TemplateMatchModes](https://docs.opencv.org/3.3.1/df/dfb/group__imgproc__object.html#ga3a7850640f1fe1f58fe91a2d7583695d) 122 | -------------------------------------------------------------------------------- /docs/basic/12-erode-and-dilate.md: -------------------------------------------------------------------------------- 1 | # 12: 腐蚀与膨胀 2 | 3 | ![](http://cos.codec.wang/cv2_understand_morphological.jpg) 4 | 5 | 学习常用形态学操作:腐蚀膨胀,开运算和闭运算。图片等可到文末引用处下载。 6 | 7 | ## 目标 8 | 9 | - 了解形态学操作的概念 10 | - 学习膨胀、腐蚀、开运算和闭运算等形态学操作 11 | - OpenCV 函数:`cv2.erode()`, `cv2.dilate()`, `cv2.morphologyEx()` 12 | 13 | ## 教程 14 | 15 | ### 啥叫形态学操作 16 | 17 | 形态学操作其实就是**改变物体的形状**,比如腐蚀就是"变瘦",膨胀就是"变胖",看下图就明白了: 18 | 19 | ![](http://cos.codec.wang/cv2_understand_morphological.jpg) 20 | 21 | :::tip 22 | 形态学操作一般作用于二值化图,来连接相邻的元素或分离成独立的元素。**腐蚀和膨胀是针对图片中的白色部分!** 23 | ::: 24 | 25 | ### 腐蚀 26 | 27 | 腐蚀的效果是把图片"变瘦",其原理是在原图的小区域内取局部最小值。因为是二值化图,只有 0 和 255,所以小区域内有一个是 0 该像素点就为 0: 28 | 29 | ![](http://cos.codec.wang/cv2_understand_erosion.jpg) 30 | 31 | 这样原图中边缘地方就会变成 0,达到了瘦身目的(小胖福利\(●ˇ∀ˇ●\)) 32 | 33 | OpenCV 中用`cv2.erode()`函数进行腐蚀,只需要指定核的大小就行: 34 | 35 | ```python 36 | import cv2 37 | import numpy as np 38 | 39 | img = cv2.imread('j.bmp', 0) 40 | kernel = np.ones((5, 5), np.uint8) 41 | erosion = cv2.erode(img, kernel) # 腐蚀 42 | ``` 43 | 44 | > 这个核也叫结构元素,因为形态学操作其实也是应用卷积来实现的。结构元素可以是矩形/椭圆/十字形,可以用`cv2.getStructuringElement()`来生成不同形状的结构元素,比如: 45 | 46 | ```python 47 | kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) # 矩形结构 48 | kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) # 椭圆结构 49 | kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5)) # 十字形结构 50 | ``` 51 | 52 | ![](http://cos.codec.wang/cv2_morphological_struct_element.jpg) 53 | 54 | ### 膨胀 55 | 56 | 膨胀与腐蚀相反,取的是局部最大值,效果是把图片"变胖": 57 | 58 | ```python 59 | dilation = cv2.dilate(img, kernel) # 膨胀 60 | ``` 61 | 62 | ### 开/闭运算 63 | 64 | 先腐蚀后膨胀叫开运算(因为先腐蚀会分开物体,这样容易记住),其作用是:分离物体,消除小区域。这类形态学操作用`cv2.morphologyEx()`函数实现: 65 | 66 | ```python 67 | kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) # 定义结构元素 68 | 69 | img = cv2.imread('j_noise_out.bmp', 0) 70 | opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel) # 开运算 71 | ``` 72 | 73 | 闭运算则相反:先膨胀后腐蚀(先膨胀会使白色的部分扩张,以至于消除/"闭合"物体里面的小黑洞,所以叫闭运算) 74 | 75 | ```python 76 | img = cv2.imread('j_noise_in.bmp', 0) 77 | closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel) # 闭运算 78 | ``` 79 | 80 | ![](http://cos.codec.wang/cv2_morphological_opening_closing.jpg) 81 | 82 | :::tip 83 | 很多人对开闭运算的作用不是很清楚(好吧,其实是比较容易混 ◑﹏◐),但看上图 ↑,不用怕:如果我们的目标物体外面有很多无关的小区域,就用开运算去除掉;如果物体内部有很多小黑洞,就用闭运算填充掉。 84 | ::: 85 | 86 | 接下来的 3 种形态学操作并不常用,大家有兴趣可以看看(因为较短,没有做成番外篇): 87 | 88 | ### 其他形态学操作 89 | 90 | - 形态学梯度:膨胀图减去腐蚀图,`dilation - erosion`,这样会得到物体的轮廓: 91 | 92 | ```python 93 | img = cv2.imread('school.bmp', 0) 94 | gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel) 95 | ``` 96 | 97 | ![](http://cos.codec.wang/cv2_morphological_gradient.jpg) 98 | 99 | - 顶帽:原图减去开运算后的图:`src - opening` 100 | 101 | ```python 102 | tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel) 103 | ``` 104 | 105 | - 黑帽:闭运算后的图减去原图:`closing - src` 106 | 107 | ```python 108 | blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel) 109 | ``` 110 | 111 | ## 小结 112 | 113 | - 形态学操作就是改变物体的形状,如腐蚀使物体"变瘦",膨胀使物体"变胖"。 114 | - 先腐蚀后膨胀会分离物体,所以叫开运算,常用来去除小区域物体。 115 | - 先膨胀后腐蚀会消除物体内的小洞,所以叫闭运算。开/闭理解了之后很容易记忆噢\(⊙o⊙\)。 116 | 117 | ## 接口文档 118 | 119 | - [cv2.erode()](https://docs.opencv.org/4.0.0/d4/d86/group__imgproc__filter.html#gaeb1e0c1033e3f6b891a25d0511362aeb) 120 | - [cv2.getStructuringElement()](https://docs.opencv.org/4.0.0/d4/d86/group__imgproc__filter.html#gac342a1bb6eabf6f55c803b09268e36dc) 121 | - [cv2.dilate()](https://docs.opencv.org/4.0.0/d4/d86/group__imgproc__filter.html#ga4ff0f3318642c4f469d0e11f242f3b6c) 122 | - [cv2.MorphShapes](https://docs.opencv.org/4.0.0/d4/d86/group__imgproc__filter.html#gac2db39b56866583a95a5680313c314ad) 123 | - [cv2.morphologyEx()](https://docs.opencv.org/4.0.0/d4/d86/group__imgproc__filter.html#ga67493776e3ad1a3df63883829375201f) 124 | - [cv2.MorphTypes](https://docs.opencv.org/4.0.0/d4/d86/group__imgproc__filter.html#ga7be549266bad7b2e6a04db49827f9f32) 125 | 126 | ## 引用 127 | 128 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/12-Erode-and-Dilate) 129 | - [Morphological Operations](http://homepages.inf.ed.ac.uk/rbf/HIPR2/morops.htm) 130 | - [Computer Vision: Algorithms and Applications](http://szeliski.org/Book/) 131 | -------------------------------------------------------------------------------- /docs/start/01-introduction-and-installation.md: -------------------------------------------------------------------------------- 1 | # 01: 简介与安装 2 | 3 | 相信大部分人知道的 OpenCV 都是用 C++ 来开发的,那为什么我推荐使用 Python 呢? 4 | 5 | :::tip 6 | 7 | 本教程基础内容来自 [OpenCV 官方英文教程](https://docs.opencv.org/),我按照使用度和难易度翻译,重新编写了大量原创内容,将不常用和较难的部分写成番外篇,浅显易懂,很 easy 的辣。每节的源码、图片和练习题答案均可在引用处找到噢\(⊙o⊙\) 8 | 9 | ::: 10 | 11 | ## Python 照样快! 12 | 13 | 众所周知,虽然 Python 语法简洁、编写高效,但相比 C/C++运行慢很多。然而 Python 还有个重要的特性:它是一门胶水语言!Python 可以很容易地扩展 C/C++。OpenCV-Python 就是用 Python 包装了 C++ 的实现,背后实际就是 C++ 的代码在跑,运行速度非常接近原生。 14 | 15 | 比如我分别用 Python 和 C++实现读入图片和调整图片的亮度对比度,结果如下: 16 | 17 | ![](http://cos.codec.wang/cv2_python_vs_cplus_speed.jpg) 18 | 19 | **可以看到某些情况下 Python 的运行速度甚至好于 C++,代码行数也直接少一半多!** 20 | 21 | 另外,图像是矩阵数据,OpenCV-Python 原生支持 [Numpy](https://baike.baidu.com/item/numpy),相当于 Python 中的 Matlab,为矩阵运算、科学计算提供了极大的便利性。 22 | 23 | ## 人工智能浪潮 24 | 25 | 近些年,人工智能 AI 相关技术的快速发展大家有目共睹。在编程语言方面,更多人希望的是具备高效开发效率、跨平台、高度扩展性的语言,尤其是一些 AI 巨头优先推出支持 Python 语言的深度学习框架,如 Facebook 的[PyTorch](https://pytorch.org/)、Google 的[Tensorflow](https://tensorflow.google.cn/)等,可以说 Python 是名副其实的“网红语言”了。 26 | 27 | ![](http://cos.codec.wang/cv2_ai_ml_dl2.jpg) 28 | 29 | 从[TIOBE 编程语言排行榜](https://www.tiobe.com/tiobe-index/)也可以看到,Python 发展迅猛,已经逼近 C++的份额。这个排行榜每月更新,就不截图了,我编写时的 TOP5:Java/C/C++/Python/C\#。 30 | 31 | ## 人生苦短,我用 Python 32 | 33 | - 如果你搞科研用,果断放弃 C++(Matlab?出门左拐) 34 | - 如果你是快速原型开发,验证方案,果断放弃 C++ 35 | - 如果你懒的配置 OpenCV 环境,果断放弃 C++ 36 | - 如果你的程序是在支持 Python 的较高硬件环境下运行,果断放弃 C++ 37 | - 如果你担心 Python 写不了界面,那是你的问题 o_o .... 38 | - 除非你的程序是 MFC 或已经用 C++编写其他模块或是嵌入式设备,那就用 C++吧 39 | 40 | **"人生苦短,我用 Python!!!"** 41 | 42 | ## 安装 43 | 44 | > 本教程编写时使用的相关版本是:OpenCV 4.x,Python 3.x。 45 | 46 | ### opencv-python 47 | 48 | 只需终端下的一条指令: 49 | 50 | ```bash 51 | pip install opencv-python 52 | ``` 53 | 54 | pip 是 Python 的包管理器,如果你还没安装 Python,强烈推荐安装[Anaconda](https://www.anaconda.com/download/),它包含了大量的科学计算包,不用后期一个个安装。 55 | 56 | ### Anaconda 安装 57 | 58 | 进入 Anaconda[官网](https://www.anaconda.com/download/),下载最新版本的安装文件,速度比较慢的话,可以去[清华开源镜像站](https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/)。 59 | 60 | - Windows 版是 exe 文件,双击直接安装,安装时记得勾选 `Add Anaconda to my PATH environment variable`,添加到环境变量。 61 | - Linux 版是 sh 文件,执行`bash Anaconda3-xx.sh`,Linux 版也会提示添加到环境变量,记得输 yes 就行。 62 | - MAC 版是 pkg 文件,同样直接双击安装即可。 63 | 64 | ### 安装测试 65 | 66 | Python 的版本可以在终端中输入`python --version`来查看。对于 OpenCV,打开 Python 的开发环境,输入`import cv2`,运行没有报错说明一切正常。要查看 OpenCV 的版本,可以: 67 | 68 | ```bash 69 | print(cv2.__version__) 70 | ``` 71 | 72 | > 编辑器我习惯用 [Visual Studio Code](http://code.visualstudio.com/),也可以用 [PyCharm](http://www.jetbrains.com/pycharm/)/[Atom](https://atom.io/)/Jupyter Notebook\(Anaconda 自带\)。 73 | 74 | ### 常见问题 75 | 76 | 1. pip 识别不了:pip 的目录没有添加到环境变量中,添加到用户\(或系统\) 变量的 path 中。 77 | 2. 下载速度很慢:可到[此处](https://pypi.org/search/?q=opencv-python)下载离线版,完成后在终端输入`pip install 文件名`安装。 78 | 79 | ## 学习软件 80 | 81 | 为了便于学习 OpenCV,我编写了一款 Windows 平台的教学软件[LearnOpenCVEdu](https://github.com/codecwang/LearnOpenCVEdu),目前只开发了一部分功能,欢迎 Star 支持:smiley:。 82 | 83 | ![](http://cos.codec.wang/cv2_learn_opencv_edu_soft_screenshot.jpg) 84 | 85 | :::tip 86 | 虽然我推荐大家使用 OpenCV-Python 进行图像处理,但想要深入理解 OpenCV,C++是必须的,尤其是**OpenCV 源码**! 87 | ::: 88 | 89 | ## 引用 90 | 91 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/01-Introduction-and-Installation) 92 | 93 | ### 网络资料 94 | 95 | - [OpenCV Docs 官方文档](https://docs.opencv.org/) 96 | - [OpenCV 源码](https://github.com/opencv/opencv) 97 | - [官方英文教程:OpenCV-Python Tutorials](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_tutorials.html) 98 | - [LearnOpenCV](http://www.learnopencv.com)、[LearnOpenCV Github](https://github.com/spmallick/learnopencv) 99 | - [OpenCV 中文教程](http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/tutorials.html) 100 | 101 | ### 书籍 102 | 103 | - [Programming Computer Vision with Python](http://programmingcomputervision.com/)、[中文书](https://www.amazon.cn/dp/B00L3Y3NEM/ref=sr_1_1?ie=UTF8&qid=1543929834&sr=8-1&keywords=Python+%E8%AE%A1%E7%AE%97%E6%9C%BA%E8%A7%86%E8%A7%89) 104 | - [Practical Python and OpenCV](https://www.pyimagesearch.com/practical-python-opencv/) 105 | 106 | ### 名校视觉研究所/课程 107 | 108 | - [卡内基梅隆大学](http://graphics.cs.cmu.edu/) 109 | - [多伦多大学](https://www.cs.toronto.edu/~guerzhoy/320/) 110 | -------------------------------------------------------------------------------- /docs/basic/extra-10-contours-hierarchy.md: -------------------------------------------------------------------------------- 1 | # 番外篇:轮廓层级 2 | 3 | ![](http://cos.codec.wang/cv2_understand_hierarchy.jpg) 4 | 5 | 了解轮廓间的层级关系。图片等可到文末引用处[文末引用出](#引用)下载。 6 | 7 | 前面我们使用`cv2.findContours()`寻找轮廓时,参数 3 表示轮廓的寻找方式\(RetrievalModes\),当时我们传入的是 cv2.RETR_TREE,它表示什么意思呢?另外,函数返回值 hierarchy 有什么用途呢?下面我们就来研究下这两个问题。 8 | 9 | ## 理解轮廓层级 10 | 11 | 很多情况下,图像中的形状之间是有关联的,比如说下图: 12 | 13 | ![](http://cos.codec.wang/cv2_understand_hierarchy.jpg) 14 | 15 | 图中总共有 8 条轮廓,2 和 2a 分别表示外层和里层的轮廓,3 和 3a 也是一样。从图中看得出来: 16 | 17 | - 轮廓 0/1/2 是最外层的轮廓,我们可以说它们处于同一轮廓等级:0 级 18 | - 轮廓 2a 是轮廓 2 的子轮廓,反过来说 2 是 2a 的父轮廓,轮廓 2a 算一个等级:1 级 19 | - 同样 3 是 2a 的子轮廓,轮廓 3 处于一个等级:2 级 20 | - 类似的,3a 是 3 的子轮廓,等等………… 21 | 22 | 这里面 OpenCV 关注的就是两个概念:同一轮廓等级和轮廓间的子属关系。 23 | 24 | ## OpenCV 中轮廓等级的表示 25 | 26 | 如果我们打印出`cv2.findContours()`函数的返回值 hierarchy,会发现它是一个包含 4 个值的数组:**\[Next, Previous, First Child, Parent\]** 27 | 28 | - _Next:与当前轮廓处于同一层级的下一条轮廓_ 29 | 30 | 举例来说,前面图中跟 0 处于同一层级的下一条轮廓是 1,所以 Next=1;同理,对轮廓 1 来说,Next=2;那么对于轮廓 2 呢?没有与它同一层级的下一条轮廓了,此时 Next=-1。 31 | 32 | - _Previous:与当前轮廓处于同一层级的上一条轮廓_ 33 | 34 | 跟前面一样,对于轮廓 1 来说,Previous=0;对于轮廓 2,Previous=1;对于轮廓 1,没有上一条轮廓了,所以 Previous=-1。 35 | 36 | - _First Child:当前轮廓的第一条子轮廓_ 37 | 38 | 比如对于轮廓 2,第一条子轮廓就是轮廓 2a,所以 First Child=2a;对轮廓 3a,First Child=4。 39 | 40 | - _Parent:当前轮廓的父轮廓_ 41 | 42 | 比如 2a 的父轮廓是 2,Parent=2;轮廓 2 没有父轮廓,所以 Parent=-1。 43 | 44 | 下面我们通过代码验证一下: 45 | 46 | ```python 47 | import cv2 48 | 49 | # 1.读入图片 50 | img = cv2.imread('hierarchy.jpg') 51 | img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 52 | _, thresh = cv2.threshold(img_gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) 53 | 54 | # 2.寻找轮廓 55 | image, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, 2) 56 | 57 | # 3.绘制轮廓 58 | print(len(contours),hierarchy) # 8 条 59 | cv2.drawContours(img, contours, -1, (0, 0, 255), 2) 60 | ``` 61 | 62 | :::tip 63 | OpenCV 中找到的轮廓序号跟前面讲的不同噢,如下图: 64 | ::: 65 | 66 | ![](http://cos.codec.wang/cv2_hierarchy_RETR_TREE.jpg) 67 | 68 | 现在既然我们了解了层级的概念,那么类似 cv2.RETR_TREE 的轮廓寻找方式又是啥意思呢? 69 | 70 | ## 轮廓寻找方式 71 | 72 | OpenCV 中有四种轮廓寻找方式[RetrievalModes](https://docs.opencv.org/3.3.1/d3/dc0/group__imgproc__shape.html#ga819779b9857cc2f8601e6526a3a5bc71),下面分别来看下: 73 | 74 | ### 1. RETR_LIST 75 | 76 | 这是最简单的一种寻找方式,它不建立轮廓间的子属关系,也就是所有轮廓都属于同一层级。这样,hierarchy 中的后两个值\[First Child, Parent\] 都为-1。比如同样的图,我们使用 cv2.RETR_LIST 来寻找轮廓: 77 | 78 | ```python 79 | _, _, hierarchy = cv2.findContours(thresh, cv2.RETR_LIST, 2) 80 | print(hierarchy) 81 | # 结果如下 82 | [[[ 1 -1 -1 -1] 83 | [ 2 0 -1 -1] 84 | [ 3 1 -1 -1] 85 | [ 4 2 -1 -1] 86 | [ 5 3 -1 -1] 87 | [ 6 4 -1 -1] 88 | [ 7 5 -1 -1] 89 | [-1 6 -1 -1]]] 90 | ``` 91 | 92 | 因为没有从属关系,所以轮廓 0 的下一条是 1,1 的下一条是 2…… 93 | 94 | :::tip 95 | 如果你不需要轮廓层级信息的话,cv2.RETR_LIST 更推荐使用,因为性能更好。 96 | ::: 97 | 98 | ### 2. RETR_TREE 99 | 100 | cv2.RETR_TREE 就是之前我们一直在使用的方式,它会完整建立轮廓的层级从属关系,前面已经详细说明过了。 101 | 102 | ### 3. RETR_EXTERNAL 103 | 104 | 这种方式只寻找最高层级的轮廓,也就是它只会找到前面我们所说的 3 条 0 级轮廓: 105 | 106 | ```python 107 | _, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, 2) 108 | print(len(contours), hierarchy, sep='\n') 109 | # 结果如下 110 | 3 111 | [[[ 1 -1 -1 -1] 112 | [ 2 0 -1 -1] 113 | [-1 1 -1 -1]]] 114 | ``` 115 | 116 | ![](http://cos.codec.wang/cv2_hierarchy_RETR_EXTERNAL.jpg) 117 | 118 | ### 4. RETR_CCOMP 119 | 120 | 相比之下 cv2.RETR_CCOMP 比较难理解,但其实也很简单:它把所有的轮廓只分为 2 个层级,不是外层的就是里层的。结合代码和图片,我们来理解下: 121 | 122 | ```python 123 | _, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_CCOMP, 2) 124 | print(hierarchy) 125 | # 结果如下 126 | [[[ 1 -1 -1 -1] 127 | [ 2 0 -1 -1] 128 | [ 4 1 3 -1] 129 | [-1 -1 -1 2] 130 | [ 6 2 5 -1] 131 | [-1 -1 -1 4] 132 | [ 7 4 -1 -1] 133 | [-1 6 -1 -1]]] 134 | ``` 135 | 136 | ![](http://cos.codec.wang/cv2_hierarchy_RETR_CCOMP.jpg) 137 | 138 | :::caution 139 | 使用这个参数找到的轮廓序号与之前不同。 140 | ::: 141 | 142 | 图中括号里面 1 代表外层轮廓,2 代表里层轮廓。比如说对于轮廓 2,Next 就是 4,Previous 是 1,它有里层的轮廓 3,所以 First Child=3,但因为只有两个层级,它本身就是外层轮廓,所以 Parent=-1。大家可以针对其他的轮廓自己验证一下。 143 | 144 | ## 练习 145 | 146 | 1. 如下图,找到 3 个圆环的内环,然后填充成\(180,215,215\) 这种颜色: 147 | 148 | ![](http://cos.codec.wang/cv2_hierarchy_fill_holes.jpg) 149 | 150 | ## 引用 151 | 152 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/Extra-10-Contours-Hierarchy) 153 | - [Contours Hierarchy](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contours_hierarchy/py_contours_hierarchy.html#contours-hierarchy) 154 | -------------------------------------------------------------------------------- /docs/start/extra-02-high-quality-save-and-matplotlib.md: -------------------------------------------------------------------------------- 1 | # 番外篇:无损保存和 Matplotlib 2 | 3 | ![](http://cos.codec.wang/cv2_matplotlib_show_gray_image.jpg) 4 | 5 | 了解常用图片格式和 OpenCV 高质量保存图片的方式,学习如何使用 Matplotlib 显示 OpenCV 图像。 6 | 7 | ## 无损保存 8 | 9 | 事实上,我们日常看到的大部分图片都是压缩过的,那么都有哪些常见的图片格式呢? 10 | 11 | ### 常用图片格式 12 | 13 | - [bmp](https://baike.baidu.com/item/BMP/35116) 14 | - 全称:Bitmap 15 | - **不压缩** 16 | - [jpg](https://baike.baidu.com/item/JPEG) 17 | - 全称:Joint Photographic Experts Group 18 | - **有损压缩方式** 19 | - [png](https://baike.baidu.com/item/PNG) 20 | - 全称:Portable Network Graphics 21 | - **无损压缩方式** 22 | 23 | 简单来说,同一个文件保存成不同的格式后,文件大小上 bmp 肯定是最大的,而 png 和 jpg,不同的压缩比结果会有所不同。可以用画图工具新建一副 100×100 的图像,分别保存成这三种格式来验证: 24 | 25 | ![](http://cos.codec.wang/cv2_high_save_mspaint_format.jpg) 26 | 27 | ### 高质量保存 28 | 29 | 用 cv2.imwrite() 保存图片时,可以传入第三个参数,用于控制保存质量: 30 | 31 | - `cv2.IMWRITE_JPEG_QUALITY`:jpg 质量控制,取值 0~100,值越大,质量越好,默认为 95 32 | - `cv2.IMWRITE_PNG_COMPRESSION`:png 质量控制,取值 0~9,值越大,压缩比越高,默认为 1 33 | 34 | 还有诸如`CV_IMWRITE_WEBP_QUALITY`的参量,不常用,请参考:[ImwriteFlags](https://docs.opencv.org/4.0.0/d4/da8/group__imgcodecs.html#ga292d81be8d76901bff7988d18d2b42ac>)。 35 | 36 | 举例来说,原图 lena.jpg 的分辨率是 350×350,大小 49.7KB。我们把它转成不同格式看下: 37 | 38 | ```python 39 | import cv2 40 | 41 | new_img = cv2.imread('lena.jpg') 42 | 43 | # bmp 44 | cv2.imwrite('img_bmp.bmp',new_img) # 文件大小:359KB 45 | 46 | # jpg 默认 95% 质量 47 | cv2.imwrite('img_jpg95.jpg',new_img) # 文件大小:52.3KB 48 | # jpg 20% 质量 49 | cv2.imwrite('img_jpg20.jpg',new_img,[int(cv2.IMWRITE_JPEG_QUALITY),20]) # 文件大小:8.01KB 50 | # jpg 100% 质量 51 | cv2.imwrite('img_jpg100.jpg',new_img,[int(cv2.IMWRITE_JPEG_QUALITY),100]) # 文件大小:82.5KB 52 | 53 | # png 默认 1 压缩比 54 | cv2.imwrite('img_png1.png',new_img) # 文件大小:240KB 55 | # png 9 压缩比 56 | cv2.imwrite('img_png9.png',new_img,[int(cv2.IMWRITE_PNG_COMPRESSION),9]) # 文件大小:207KB 57 | ``` 58 | 59 | 可以看到: 60 | 61 | - bmp 文件是最大的,没有任何压缩(1 个像素点 1byte,3 通道的彩色图总大小:350×350×3/1024 ≈ 359 KB) 62 | - jpg/png 本身就有压缩的,所以就算是 100% 的质量保存,体积也比 bmp 小很多 63 | - jpg 的容量优势很明显,这也是它为什么如此流行的原因 64 | 65 | > 思考:为什么原图 49.7KB,保存成 bmp 或其他格式反而大了呢? 66 | 67 | 这是个很有趣的问题,很多童鞋都问过我。这里需要明确的是保存新格式时,**容量大小跟原图的容量没有直接关系,而是取决于原图的分辨率大小和原图本身的内容(压缩方式)**,所以 lena.jpg 保存成不压缩的 bmp 格式时,容量大小就是固定的 350×350×3/1024 ≈ 359 KB;另外,容量变大不代表画质提升噢,不然就逆天了~~~ 68 | 69 | ## Matplotlib 70 | 71 | Matplotlib 是 Python 的一个很常用的绘图库,有兴趣的可以去[官网](http://www.matplotlib.org/)学习更多内容。 72 | 73 | ### 显示灰度图 74 | 75 | ```python 76 | import cv2 77 | import matplotlib.pyplot as plt 78 | 79 | img = cv2.imread('lena.jpg', 0) 80 | 81 | # 灰度图显示,cmap(color map) 设置为 gray 82 | plt.imshow(img, cmap='gray') 83 | plt.show() 84 | ``` 85 | 86 | 结果如下: 87 | 88 | ![](http://cos.codec.wang/cv2_matplotlib_show_gray_image.jpg) 89 | 90 | ### 显示彩色图 91 | 92 | **OpenCV 中的图像是以 BGR 的通道顺序存储的**,但 Matplotlib 是以 RGB 模式显示的,所以直接在 Matplotlib 中显示 OpenCV 图像会出现问题,因此需要转换一下: 93 | 94 | ```python 95 | import cv2 96 | import matplotlib.pyplot as plt 97 | 98 | img = cv2.imread('lena.jpg') 99 | img2 = img[:, :, ::-1] 100 | # 或使用 101 | # img2 = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) 102 | 103 | # 显示不正确的图 104 | plt.subplot(121),plt.imshow(img) 105 | 106 | # 显示正确的图 107 | plt.subplot(122) 108 | plt.xticks([]), plt.yticks([]) # 隐藏 x 和 y 轴 109 | plt.imshow(img2) 110 | 111 | plt.show() 112 | ``` 113 | 114 | > `img[:,:,0]`表示图片的蓝色通道,`img[:,:,::-1]`就表示 BGR 翻转,变成 RGB,说明一下: 115 | 116 | 熟悉 Python 的童鞋应该知道,对一个字符串 s 翻转可以这样写:`s[::-1]`,'abc'变成'cba',-1 表示逆序。图片是二维的,所以完整地复制一副图像就是: 117 | 118 | ```python 119 | img2 = img[:,:] # 写全就是:img2 = img[0:height,0:width] 120 | ``` 121 | 122 | 而图片是有三个通道,相当于一个长度为 3 的字符串,所以通道翻转与图片复制组合起来便是`img[:,:,::-1]`。 123 | 124 | 结果如下: 125 | 126 | ![](http://cos.codec.wang/cv2_matplotlib_show_color_image.jpg) 127 | 128 | ### 加载和保存图片 129 | 130 | 不使用 OpenCV,Matplotlib 也可以加载和保存图片: 131 | 132 | ```python 133 | import matplotlib.image as pli 134 | 135 | img = pli.imread('lena.jpg') 136 | plt.imshow(img) 137 | 138 | # 保存图片,需放在 show() 函数之前 139 | plt.savefig('lena2.jpg') 140 | plt.show() 141 | ``` 142 | 143 | ## 接口文档 144 | 145 | - [cv2.imwrite()](https://docs.opencv.org/4.0.0/d4/da8/group__imgcodecs.html#gabbc7ef1aa2edfaa87772f1202d67e0ce) 146 | - [ImwriteFlags](https://docs.opencv.org/4.0.0/d4/da8/group__imgcodecs.html#ga292d81be8d76901bff7988d18d2b42ac) 147 | 148 | ## 引用 149 | 150 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/Extra-02-High-Quality-Save-and-Matplotlib) 151 | - [聊一聊几种常用 web 图片格式](https://segmentfault.com/a/1190000013589397) 152 | - [Matplotlib 官网](http://www.matplotlib.org/) 153 | -------------------------------------------------------------------------------- /docs/basic/17-hough-transform.md: -------------------------------------------------------------------------------- 1 | # 17: 霍夫变换 2 | 3 | ![](http://cos.codec.wang/cv2_understand_hough_transform.jpg) 4 | 5 | 学习使用霍夫变换识别出图像中的直线和圆。图片等可到文末引用处下载。 6 | 7 | ## 目标 8 | 9 | - 理解霍夫变换的实现 10 | - 分别使用霍夫线变换和圆变换检测图像中的直线和圆 11 | - OpenCV 函数:`cv2.HoughLines()`, `cv2.HoughLinesP()`, `cv2.HoughCircles()` 12 | 13 | ## 教程 14 | 15 | ### 理解霍夫变换 16 | 17 | 霍夫变换常用来在图像中提取直线和圆等几何形状,我来做个简易的解释: 18 | 19 | ![](http://cos.codec.wang/cv2_understand_hough_transform.jpg) 20 | 21 | 学过几何的都知道,直线可以分别用直角坐标系和极坐标系来表示: 22 | 23 | ![](http://cos.codec.wang/cv2_line_expression_in_coordinate.jpg) 24 | 25 | 那么经过某个点$(x_0, y_0)$的所有直线都可以用这个式子来表示: 26 | 27 | $$ 28 | r_\theta=x_0\cdot\cos \theta+y_0\cdot\sin \theta 29 | $$ 30 | 31 | 也就是说每一个$(r, θ)$都表示一条经过$(x_0, y_0)$直线,那么同一条直线上的点必然会有同样的$(r, θ)$。如果将某个点所有的$(r, θ)$绘制成下面的曲线,那么同一条直线上的点的$(r, θ)$曲线会相交于一点: 32 | 33 | ![](http://cos.codec.wang/cv2_curve_of_r_theta.jpg) 34 | 35 | OpenCV 中首先计算$(r, θ)$累加数,累加数超过一定值后就认为在同一直线上。 36 | 37 | ### 霍夫直线变换 38 | 39 | OpenCV 中用`cv2.HoughLines()`在二值图上实现霍夫变换,函数返回的是一组直线的$(r, θ)$数据: 40 | 41 | ```python 42 | import cv2 43 | import numpy as np 44 | 45 | # 1.加载图片,转为二值图 46 | img = cv2.imread('shapes.jpg') 47 | drawing = np.zeros(img.shape[:], dtype=np.uint8) 48 | gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 49 | edges = cv2.Canny(gray, 50, 150) 50 | 51 | # 2.霍夫直线变换 52 | lines = cv2.HoughLines(edges, 0.8, np.pi / 180, 90) 53 | ``` 54 | 55 | 函数中: 56 | 57 | - 参数 1:要检测的二值图(一般是阈值分割或边缘检测后的图) 58 | - 参数 2:距离 r 的精度,值越大,考虑越多的线 59 | - 参数 3:角度 θ 的精度,值越小,考虑越多的线 60 | - 参数 4:累加数阈值,值越小,考虑越多的线 61 | 62 | ```python 63 | # 3.将检测的线画出来(注意是极坐标噢) 64 | for line in lines: 65 | rho, theta = line[0] 66 | a = np.cos(theta) 67 | b = np.sin(theta) 68 | x0 = a * rho 69 | y0 = b * rho 70 | x1 = int(x0 + 1000 * (-b)) 71 | y1 = int(y0 + 1000 * (a)) 72 | x2 = int(x0 - 1000 * (-b)) 73 | y2 = int(y0 - 1000 * (a)) 74 | 75 | cv2.line(drawing, (x1, y1), (x2, y2), (0, 0, 255)) 76 | ``` 77 | 78 | ![](http://cos.codec.wang/cv2_hough_line_function.jpg) 79 | 80 | ### 统计概率霍夫直线变换 81 | 82 | 前面的方法又称为标准霍夫变换,它会计算图像中的每一个点,计算量比较大,另外它得到的是整一条线(r 和 θ),并不知道原图中直线的端点。所以提出了统计概率霍夫直线变换\(Probabilistic Hough Transform\),是一种改进的霍夫变换: 83 | 84 | ```python 85 | drawing = np.zeros(img.shape[:], dtype=np.uint8) 86 | # 3.统计概率霍夫线变换 87 | lines = cv2.HoughLinesP(edges, 0.8, np.pi / 180, 90, 88 | minLineLength=50, maxLineGap=10) 89 | ``` 90 | 91 | 前面几个参数跟之前的一样,有两个可选参数: 92 | 93 | - `minLineLength`:最短长度阈值,比这个长度短的线会被排除 94 | - `maxLineGap`:同一直线两点之间的最大距离 95 | 96 | ```python 97 | # 3.将检测的线画出来 98 | for line in lines: 99 | x1, y1, x2, y2 = line[0] 100 | cv2.line(drawing, (x1, y1), (x2, y2), (0, 255, 0), 1, lineType=cv2.LINE_AA) 101 | ``` 102 | 103 | `cv2.LINE_AA`在之前绘图功能中讲解过,表示抗锯齿线型。 104 | 105 | ![](http://cos.codec.wang/cv2_hough_lines_p_function.jpg) 106 | 107 | ### 霍夫圆变换 108 | 109 | 霍夫圆变换跟直线变换类似,只不过线是用$(r, θ)$表示,圆是用$(x_center, y_center, r)$来表示,从二维变成了三维,数据量变大了很多;所以一般使用霍夫梯度法减少计算量,对该算法感兴趣的同学可参考:[Circle Hough Transform](https://en.wikipedia.org/wiki/Circle_Hough_Transform) 110 | 111 | ```python 112 | drawing = np.zeros(img.shape[:], dtype=np.uint8) 113 | # 2.霍夫圆变换 114 | circles = cv2.HoughCircles(edges, cv2.HOUGH_GRADIENT, 1, 20, param2=30) 115 | circles = np.int0(np.around(circles)) 116 | ``` 117 | 118 | 其中, 119 | 120 | - 参数 2:变换方法,一般使用霍夫梯度法,详情:[HoughModes](https://docs.opencv.org/3.3.1/d7/dbd/group__imgproc.html#ga073687a5b96ac7a3ab5802eb5510fe65) 121 | - 参数 3 dp=1:表示霍夫梯度法中累加器图像的分辨率与原图一致 122 | - 参数 4:两个不同圆圆心的最短距离 123 | - 参数 5:param2 跟霍夫直线变换中的累加数阈值一样 124 | 125 | ```python 126 | # 将检测的圆画出来 127 | for i in circles[0, :]: 128 | cv2.circle(drawing, (i[0], i[1]), i[2], (0, 255, 0), 2) # 画出外圆 129 | cv2.circle(drawing, (i[0], i[1]), 2, (0, 0, 255), 3) # 画出圆心 130 | ``` 131 | 132 | ![](http://cos.codec.wang/cv2_hough_circles_function.jpg) 133 | 134 | ## 小结 135 | 136 | - 霍夫变换用来提取图像中的直线和圆等几何形状。 137 | - 霍夫直线变换:`cv2.HoughLines()`(整条直线), `cv2.HoughLinesP()`。 138 | - 霍夫圆变换:`cv2.HoughCircles()`。 139 | 140 | ## 引用 141 | 142 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/17-Hough-Transform) 143 | - [Hough Line Transform](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_houghlines/py_houghlines.html) 144 | - [Hough Circle Transform](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_houghcircles/py_houghcircles.html) 145 | - [Hough transform](https://en.wikipedia.org/wiki/Hough_transform) 146 | - [经典霍夫变换(Hough Transform)](https://blog.csdn.net/YuYunTan/article/details/80141392) 147 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: "目录" 3 | sidebar_position: 1 4 | --- 5 | 6 | # 面向初学者的 OpenCV-Python 教程 7 | 8 | [![GitHub Repo stars](https://img.shields.io/github/stars/CodecWang/opencv-python-tutorial?style=social)](https://github.com/CodecWang/opencv-python-tutorial) 9 | 10 | ![](http://cos.codec.wang/opencv-python-tutorial-amend-new-cover.png) 11 | 12 | ### 入门篇 13 | 14 | | 标题 | 简介 | 15 | | :------------------------------------------------------------------------------------------ | :--------------------------------------- | 16 | | [简介与安装](/docs/start/introduction-and-installation) | 了解和安装 OpenCV-Python | 17 | | [番外篇:代码性能优化](/docs/start/extra-01-code-optimization) | 度量运行时间/提升效率的几种方式 | 18 | | [基本元素:图片](/docs/start/basic-element-image) | 图片的载入/显示和保存 | 19 | | [番外篇:无损保存和 Matplotlib 使用](/docs/start/extra-02-high-quality-save-and-matplotlib) | 高保真保存图片、Matplotlib 库的简单使用 | 20 | | [打开摄像头](/docs/start/open-camera) | 打开摄像头捕获图片/录制视频/播放本地视频 | 21 | | [番外篇:滑动条](/docs/start/extra-03-trackbar) | 滑动条的使用 | 22 | | [图像基本操作](/docs/start/basic-operations) | 访问像素点/ROI/通道分离合并/图片属性 | 23 | | [颜色空间转换](/docs/start/changing-colorspaces) | 颜色空间转换/追踪特定颜色物体 | 24 | | [阈值分割](/docs/start/image-thresholding) | 阈值分割/二值化 | 25 | | [番外篇:Otsu 阈值法](/docs/start/extra-04-otsu-thresholding) | 双峰图片/Otsu 自动阈值法 | 26 | | [图像几何变换](/docs/start/image-geometric-transformation) | 旋转/平移/缩放/翻转 | 27 | | [番外篇:仿射变换与透视变换](/docs/start/extra-05-warpaffine-warpperspective) | 基于 2×3 的仿射变换/基于 3×3 的透视变换 | 28 | | [绘图功能](/docs/start/drawing-function) | 画线/画圆/画矩形/添加文字 | 29 | | [番外篇:鼠标绘图](/docs/start/extra-06-drawing-with-mouse) | 用鼠标实时绘图 | 30 | | [挑战篇:画动态时钟](/docs/start/challenge-01-draw-dynamic-clock) | / | 31 | | [挑战篇:PyQt5 编写 GUI 界面](/docs/start/challenge-02-create-gui-with-pyqt5) | / | 32 | 33 | ### 基础篇 34 | 35 | | 标题 | 简介 | 36 | | :-------------------------------------------------------------------------- | :------------------------------------- | 37 | | [图像混合](/docs/basic/image-blending) | 算数运算/混合/按位运算 | 38 | | [番外篇:亮度与对比度](/docs/basic/extra-07-contrast-and-brightness) | 调整图片的亮度和对比度 | 39 | | [平滑图像](/docs/basic/smoothing-images) | 卷积/滤波/模糊/降噪 | 40 | | [番外篇:卷积基础 - 图片边框](/docs/basic/extra-08-padding-and-convolution) | 了解卷积/滤波的基础知识/给图片添加边框 | 41 | | [边缘检测](/docs/basic/edge-detection) | Canny/Sobel 算子 | 42 | | [番外篇:图像梯度](/docs/basic/extra-09-image-gradients) | 了解图像梯度和边缘检测的相关概念 | 43 | | [腐蚀与膨胀](/docs/basic/erode-and-dilate) | 形态学操作/腐蚀/膨胀/开运算/闭运算 | 44 | | [轮廓](/docs/basic/contours) | 寻找/绘制轮廓 | 45 | | [番外篇:轮廓层级](/docs/basic/extra-10-contours-hierarchy) | 了解轮廓间的层级关系 | 46 | | [轮廓特征](/docs/basic/contour-features) | 面积/周长/最小外接矩\(圆\)/形状匹配 | 47 | | [番外篇:凸包及更多轮廓特征](/docs/basic/extra-11-convex-hull) | 计算凸包/了解更多轮廓特征 | 48 | | [直方图](/docs/basic/histograms) | 计算绘制直方图/均衡化 | 49 | | [模板匹配](/docs/basic/template-matching) | 图中找小图 | 50 | | [霍夫变换](/docs/basic/hough-transform) | 提取直线/圆 | 51 | | [挑战任务:车道检测](/docs/basic/challenge-03-lane-road-detection) | / | 52 | 53 | > 如果您觉得写的不错的话,欢迎打赏,我会努力写出更好的内容!✊🤟 54 | 55 | ![](http://cos.codec.wang/wechat_alipay_pay_pic.png) 56 | -------------------------------------------------------------------------------- /docs/start/06-image-thresholding.md: -------------------------------------------------------------------------------- 1 | # 06: 阈值分割 2 | 3 | ![](http://cos.codec.wang/cv2_threshold_binary_demo.jpg) 4 | 5 | 学习使用不同的阈值方法"二值化"图像。图片等可到文末引用处下载。 6 | 7 | ## 目标 8 | 9 | - 使用固定阈值、自适应阈值和 Otsu 阈值法"二值化"图像 10 | - OpenCV 函数:`cv2.threshold()`, `cv2.adaptiveThreshold()` 11 | 12 | ## 教程 13 | 14 | ### 固定阈值分割 15 | 16 | 固定阈值分割很直接,一句话说就是像素点值大于阈值变成一类值,小于阈值变成另一类值。 17 | 18 | ![](http://cos.codec.wang/cv2_threshold_binary_demo.jpg) 19 | 20 | ```python 21 | import cv2 22 | 23 | # 灰度图读入 24 | img = cv2.imread('gradient.jpg', 0) 25 | 26 | # 阈值分割 27 | ret, th = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) 28 | cv2.imshow('thresh', th) 29 | cv2.waitKey(0) 30 | ``` 31 | 32 | `cv2.threshold()`用来实现阈值分割,ret 代表当前的阈值,暂时不用理会。函数有 4 个参数: 33 | 34 | - 参数 1:要处理的原图,**一般是灰度图** 35 | - 参数 2:设定的阈值 36 | - 参数 3:对于`THRESH_BINARY`、`THRESH_BINARY_INV`阈值方法所选用的最大阈值,一般为 255 37 | - 参数 4:阈值的方式,主要有 5 种,详情:[ThresholdTypes](https://docs.opencv.org/4.0.0/d7/d1b/group__imgproc__misc.html#gaa9e58d2860d4afa658ef70a9b1115576) 38 | 39 | 下面结合代码理解下这 5 种阈值方式: 40 | 41 | ```python 42 | import matplotlib.pyplot as plt 43 | 44 | # 应用 5 种不同的阈值方法 45 | ret, th1 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) 46 | ret, th2 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY_INV) 47 | ret, th3 = cv2.threshold(img, 127, 255, cv2.THRESH_TRUNC) 48 | ret, th4 = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO) 49 | ret, th5 = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO_INV) 50 | 51 | titles = ['Original', 'BINARY', 'BINARY_INV', 'TRUNC', 'TOZERO', 'TOZERO_INV'] 52 | images = [img, th1, th2, th3, th4, th5] 53 | 54 | # 使用 Matplotlib 显示 55 | for i in range(6): 56 | plt.subplot(2, 3, i + 1) 57 | plt.imshow(images[i], 'gray') 58 | plt.title(titles[i], fontsize=8) 59 | plt.xticks([]), plt.yticks([]) # 隐藏坐标轴 60 | 61 | plt.show() 62 | ``` 63 | 64 | ![5 种不同的阈值方式结果](http://cos.codec.wang/cv2_different_threshold_demo.jpg) 65 | 66 | :::tip 67 | 很多人误以为阈值分割就是 [二值化](https://baike.baidu.com/item/%E4%BA%8C%E5%80%BC%E5%8C%96)。从上图中可以发现,两者并不等同,阈值分割结果是两类值,而不是两个值,所以教程开头我把二值化加了引号。 68 | ::: 69 | 70 | ![](http://cos.codec.wang/cv2_different_thresholds_theory.jpg) 71 | 72 | ### 自适应阈值 73 | 74 | 看得出来固定阈值是在整幅图片上应用一个阈值进行分割,_它并不适用于明暗分布不均的图片_。 `cv2.adaptiveThreshold()`自适应阈值会每次取图片的一小部分计算阈值,这样图片不同区域的阈值就不尽相同。它有 5 个参数,其实很好理解,先看下效果: 75 | 76 | ```python 77 | # 自适应阈值对比固定阈值 78 | img = cv2.imread('sudoku.jpg', 0) 79 | 80 | # 固定阈值 81 | ret, th1 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) 82 | # 自适应阈值 83 | th2 = cv2.adaptiveThreshold( 84 | img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 4) 85 | th3 = cv2.adaptiveThreshold( 86 | img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 17, 6) 87 | 88 | titles = ['Original', 'Global(v = 127)', 'Adaptive Mean', 'Adaptive Gaussian'] 89 | images = [img, th1, th2, th3] 90 | 91 | for i in range(4): 92 | plt.subplot(2, 2, i + 1), plt.imshow(images[i], 'gray') 93 | plt.title(titles[i], fontsize=8) 94 | plt.xticks([]), plt.yticks([]) 95 | plt.show() 96 | ``` 97 | 98 | ![自适应阈值对比固定阈值](http://cos.codec.wang/cv2_adaptive_vs_global_thresholding.jpg) 99 | 100 | - 参数 1:要处理的原图 101 | - 参数 2:最大阈值,一般为 255 102 | - 参数 3:小区域阈值的计算方式 103 | - `ADAPTIVE_THRESH_MEAN_C`:小区域内取均值 104 | - `ADAPTIVE_THRESH_GAUSSIAN_C`:小区域内加权求和,权重是个高斯核 105 | - 参数 4:阈值方法,只能使用`THRESH_BINARY`、`THRESH_BINARY_INV`,具体见前面所讲的阈值方法 106 | - 参数 5:小区域的面积,如 11 就是 11\*11 的小块 107 | - 参数 6:最终阈值等于小区域计算出的阈值再减去此值 108 | 109 | 如果你没看懂上面的参数也不要紧,暂时会用就行,当然我建议你调整下参数看看不同的结果。 110 | 111 | ### Otsu 阈值 112 | 113 | 在前面固定阈值中,我们是随便选了一个阈值如 127,那如何知道我们选的这个阈值效果好不好呢?答案是:不断尝试,所以这种方法在很多文献中都被称为经验阈值。[Otsu 阈值法](https://baike.baidu.com/item/otsu/16252828)就提供了一种自动高效的二值化方法,不过我们直方图还没学,这里暂时略过。 114 | 115 | 好吧,我知道我激起了你的兴趣,~ o\(_ ̄ ▽  ̄_\)o,有能力的童鞋可以看下[练习题](#练习)。 116 | 117 | ## 小结 118 | 119 | - `cv2.threshold()`用来进行固定阈值分割。固定阈值不适用于光线不均匀的图片,所以用 `cv2.adaptiveThreshold()`进行自适应阈值分割。 120 | - 二值化跟阈值分割并不等同。针对不同的图片,可以采用不同的阈值方法。 121 | 122 | ## 练习 123 | 124 | 1. Otsu 阈值是一种高效的二值化算法,请阅读[番外篇:Otsu 阈值法](./extra-04-otsu-thresholding/)。 125 | 126 | ## 接口文档 127 | 128 | - [cv2.threshold\(\)](https://docs.opencv.org/4.0.0/d7/d1b/group__imgproc__misc.html#gae8a4a146d1ca78c626a53577199e9c57) 129 | - [cv2.adaptiveThreshold\(\)](https://docs.opencv.org/4.0.0/d7/d1b/group__imgproc__misc.html#ga72b913f352e4a1b1b397736707afcde3) 130 | - [cv2.ThresholdTypes\(\)](https://docs.opencv.org/4.0.0/d7/d1b/group__imgproc__misc.html#gaa9e58d2860d4afa658ef70a9b1115576) 131 | 132 | ## 引用 133 | 134 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/06-Image-Thresholding) 135 | - [Image Thresholding](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_thresholding/py_thresholding.html) 136 | -------------------------------------------------------------------------------- /docs/start/extra-04-otsu-thresholding.md: -------------------------------------------------------------------------------- 1 | # 番外篇:Otsu 阈值法 2 | 3 | ![](http://cos.codec.wang/cv2_bimodal_image_two_peaks.jpg) 4 | 5 | 大部分图像处理任务都需要先进行二值化操作,阈值的选取很关键,Otsu 阈值法会自动计算阈值。 6 | 7 | [Otsu 阈值法](https://baike.baidu.com/item/otsu/16252828)(日本人大津展之提出的,也可称大津算法)非常适用于双峰图片,啥意思呢? 8 | 9 | > [Otsu N. A threshold selection method from gray-level histograms\[J\]. IEEE transactions on systems, man, and cybernetics, 1979, 9\(1\): 62-66.](https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=4310076) 10 | 11 | ## 什么是双峰图片? 12 | 13 | 双峰图片就是指图片的灰度直方图上有两个峰值,直方图就是每个值(0~255)的像素点个数统计,后面会详细介绍。 14 | 15 | ![](http://cos.codec.wang/cv2_bimodal_image_two_peaks.jpg) 16 | 17 | Otsu 算法假设这副图片由前景色和背景色组成,通过统计学方法(最大类间方差)选取一个阈值,将前景和背景尽可能分开,我们先来看下代码,然后详细说明下算法原理。 18 | 19 | ## 代码示例 20 | 21 | 下面这段代码对比了使用固定阈值和 Otsu 阈值后的不同结果: 22 | 23 | 另外,对含噪点的图像,先进行滤波操作效果会更好。 24 | 25 | ```python 26 | import cv2 27 | from matplotlib import pyplot as plt 28 | 29 | img = cv2.imread('noisy.jpg', 0) 30 | 31 | # 固定阈值法 32 | ret1, th1 = cv2.threshold(img, 100, 255, cv2.THRESH_BINARY) 33 | 34 | # Otsu 阈值法 35 | ret2, th2 = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) 36 | 37 | # 先进行高斯滤波,再使用 Otsu 阈值法 38 | blur = cv2.GaussianBlur(img, (5, 5), 0) 39 | ret3, th3 = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) 40 | ``` 41 | 42 | 下面我们用 Matplotlib 把原图、直方图和阈值图都显示出来: 43 | 44 | ```python 45 | images = [img, 0, th1, img, 0, th2, blur, 0, th3] 46 | titles = ['Original', 'Histogram', 'Global(v=100)', 47 | 'Original', 'Histogram', "Otsu's", 48 | 'Gaussian filtered Image', 'Histogram', "Otsu's"] 49 | 50 | for i in range(3): 51 | # 绘制原图 52 | plt.subplot(3, 3, i * 3 + 1) 53 | plt.imshow(images[i * 3], 'gray') 54 | plt.title(titles[i * 3], fontsize=8) 55 | plt.xticks([]), plt.yticks([]) 56 | 57 | # 绘制直方图 plt.hist,ravel 函数将数组降成一维 58 | plt.subplot(3, 3, i * 3 + 2) 59 | plt.hist(images[i * 3].ravel(), 256) 60 | plt.title(titles[i * 3 + 1], fontsize=8) 61 | plt.xticks([]), plt.yticks([]) 62 | 63 | # 绘制阈值图 64 | plt.subplot(3, 3, i * 3 + 3) 65 | plt.imshow(images[i * 3 + 2], 'gray') 66 | plt.title(titles[i * 3 + 2], fontsize=8) 67 | plt.xticks([]), plt.yticks([]) 68 | plt.show() 69 | ``` 70 | 71 | ![固定阈值 vs Otsu 阈值](http://cos.codec.wang/cv2_otsu_vs_simple_thresholding.jpg) 72 | 73 | 可以看到,Otsu 阈值明显优于固定阈值,省去了不断尝试阈值判断效果好坏的过程。其中,绘制直方图时,使用了 numpy 中的[ravel\(\)](https://docs.scipy.org/doc/numpy/reference/generated/numpy.ravel.html)函数,它会将原矩阵压缩成一维数组,便于画直方图。 74 | 75 | ## Otsu 算法详解 76 | 77 | Otsu 阈值法将整幅图分为前景(目标)和背景,以下是一些符号规定: 78 | 79 | - $T$:分割阈值 80 | - $N_0$:前景像素点数 81 | - $N_1$:背景像素点数 82 | - $\omega_0$:前景的像素点数占整幅图像的比例 83 | - $\omega_1$:背景的像素点数占整幅图像的比例 84 | - $\mu_0$:前景的平均像素值 85 | - $\mu_1$:背景的平均像素值 86 | - $\mu$:整幅图的平均像素值 87 | - $rows\times cols$:图像的行数和列数 88 | 89 | 结合下图会更容易理解一些,有一副大小为 4×4 的图片,假设阈值 T 为 1,那么: 90 | 91 | ![](http://cos.codec.wang/cv2_otsu_theory_sample.jpg) 92 | 93 | 其实很好理解,$N_0+N_1$就是总的像素点个数,也就是行数乘列数: 94 | 95 | $$ 96 | N_0+N_1=rows\times cols 97 | $$ 98 | 99 | $\omega_0$和$\omega_1$是前/背景所占的比例,也就是: 100 | 101 | $$ 102 | \omega_0=\frac{N_0}{rows\times cols} 103 | $$ 104 | 105 | $$ 106 | \omega_1=\frac{N_1}{rows\times cols} 107 | $$ 108 | 109 | $$ 110 | \omega_0+\omega_1=1 \tag{1} 111 | $$ 112 | 113 | 整幅图的平均像素值就是: 114 | 115 | $$ 116 | \mu=\omega_0\times \mu_0+\omega_1\times \mu_1 \tag{2} 117 | $$ 118 | 119 | 此时,我们定义一个前景$\mu_0$与背景$\mu_1$的方差$g$: 120 | 121 | $$ 122 | g=\omega_0(\mu_0-\mu)^2+\omega_1(\mu_1-\mu)^2 \tag{3} 123 | $$ 124 | 125 | 将前述的 1/2/3 公式整合在一起,便是: 126 | 127 | $$ 128 | g=\omega_0\omega_1(\mu_0-\mu_1)^2 129 | $$ 130 | 131 | **$g$就是前景与背景两类之间的方差,这个值越大,说明前景和背景的差别也就越大,效果越好。Otsu 算法便是遍历阈值 T,使得$g$最大,所以又称为最大类间方差法。**基本上双峰图片的阈值 T 在两峰之间的谷底。 132 | 133 | ## 接口文档 134 | 135 | - [cv2.ThresholdTypes](https://docs.opencv.org/4.0.0/d7/d1b/group__imgproc__misc.html#gaa9e58d2860d4afa658ef70a9b1115576) 136 | - [cv2.GaussianBlur()](https://docs.opencv.org/4.0.0/d4/d86/group__imgproc__filter.html#gaabe8c836e97159a9193fb0b11ac52cf1) 137 | 138 | ## 引用 139 | 140 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/Extra-04-Otsu-Thresholding) 141 | - [numpy.ravel](https://docs.scipy.org/doc/numpy/reference/generated/numpy.ravel.html) 142 | - [Otsu's Method\(wikipedia\)](https://en.wikipedia.org/wiki/Otsu%27s_method) 143 | - [Image Thresholding](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_thresholding/py_thresholding.html) 144 | - [一维 OTSU 法、最小交叉熵法、二维 OTSU 法及 C++源码](https://blog.csdn.net/u011776903/article/details/73274802) 145 | - [Otsu N. A threshold selection method from gray-level histograms\[J\]. IEEE transactions on systems, man, and cybernetics, 1979, 9\(1\): 62-66.](https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=4310076) 146 | -------------------------------------------------------------------------------- /docs/basic/15-histograms.md: -------------------------------------------------------------------------------- 1 | # 15: 直方图 2 | 3 | ![](http://cos.codec.wang/cv2_understand_histogram.jpg) 4 | 5 | 学习计算并绘制直方图,直方图均衡化等。图片等可到文末引用处下载。 6 | 7 | ## 目标 8 | 9 | - 计算并绘制直方图 10 | - (自适应)直方图均衡化 11 | - OpenCV 函数:`cv2.calcHist()`, `cv2.equalizeHist()` 12 | 13 | ## 教程 14 | 15 | ### 啥叫直方图 16 | 17 | 简单来说,直方图就是图像中每个像素值的个数统计,比如说一副灰度图中像素值为 0 的有多少个,1 的有多少个…… 18 | 19 | ![](http://cos.codec.wang/cv2_understand_histogram.jpg) 20 | 21 | 在计算直方图之前,有几个术语先来了解一下: 22 | 23 | - dims: 要计算的通道数,对于灰度图 dims=1,普通彩色图 dims=3 24 | - range: 要计算的像素值范围,一般为\[0,256\) 25 | - bins: 子区段数目,如果我们统计 0`~`255 每个像素值,bins=256;如果划分区间,比如 0`~`15, 16`~`31…240`~`255 这样 16 个区间,bins=16 26 | 27 | ### 计算直方图 28 | 29 | OpenCV 和 Numpy 中都提供了计算直方图的函数,我们对比下它们的性能。 30 | 31 | #### OpenCV 中直方图计算 32 | 33 | 使用`cv2.calcHist(images, channels, mask, histSize, ranges)`计算,其中: 34 | 35 | - 参数 1:要计算的原图,以方括号的传入,如:\[img\] 36 | - 参数 2:类似前面提到的 dims,灰度图写\[0\] 就行,彩色图 B/G/R 分别传入\[0\]/\[1\]/\[2\] 37 | - 参数 3:要计算的区域,计算整幅图的话,写 None 38 | - 参数 4:前面提到的 bins 39 | - 参数 5:前面提到的 range 40 | 41 | ```python 42 | import cv2 43 | import numpy as np 44 | import matplotlib.pyplot as plt 45 | 46 | img = cv2.imread('hist.jpg', 0) 47 | hist = cv2.calcHist([img], [0], None, [256], [0, 256]) # 性能:0.025288 s 48 | ``` 49 | 50 | #### Numpy 中直方图计算 51 | 52 | 也可用 Numpy 的函数计算,其中[ravel\(\)](https://docs.scipy.org/doc/numpy/reference/generated/numpy.ravel.html)函数将二维矩阵展平变成一维数组,之前有提到过: 53 | 54 | ```python 55 | hist, bins = np.histogram(img.ravel(), 256, [0, 256]) # 性能:0.020628 s 56 | ``` 57 | 58 | :::tip 59 | Numpy 中还有一种更高效的方式:(还记得怎么评估性能吗:[番外篇:代码性能优化](../start/extra-01-code-optimization/)) 60 | ::: 61 | 62 | ```python 63 | hist = np.bincount(img.ravel(), minlength=256) # 性能:0.003163 s 64 | ``` 65 | 66 | 计算出直方图之后,怎么把它画出来呢? 67 | 68 | ### 绘制直方图 69 | 70 | 其实 Matplotlib 自带了一个计算并绘制直方图的功能,不需要用到上面的函数: 71 | 72 | ```python 73 | plt.hist(img.ravel(), 256, [0, 256]) 74 | plt.show() 75 | ``` 76 | 77 | 当然,也可以用前面计算出来的结果绘制: 78 | 79 | ```python 80 | plt.plot(hist) 81 | plt.show() 82 | ``` 83 | 84 | ![](http://cos.codec.wang/cv2_calc_draw_histogram.jpg) 85 | 86 | 从直方图上可以看到图片的大部分区域集中在 150 偏白的附近,这其实并不是很好的效果,下面我们来看看如何改善它。 87 | 88 | > 使用 OpenCV 的画线功能也可以画直方图,不过太麻烦了,有兴趣的可以看下官方示例:[hist.py](https://github.com/opencv/opencv/blob/master/samples/python/hist.py)。 89 | 90 | ### 直方图均衡化 91 | 92 | 一副效果好的图像通常在直方图上的分布比较均匀,直方图均衡化就是用来改善图像的全局亮度和对比度。其实从观感上就可以发现,前面那幅图对比度不高,偏灰白。对均衡化算法感兴趣的同学可参考:[维基百科:直方图均衡化](https://zh.wikipedia.org/wiki/%E7%9B%B4%E6%96%B9%E5%9B%BE%E5%9D%87%E8%A1%A1%E5%8C%96) 93 | 94 | ![](http://cos.codec.wang/cv2_understand_histogram_equalization.jpg) 95 | 96 | ```python 97 | equ = cv2.equalizeHist(img) 98 | ``` 99 | 100 | OpenCV 中用`cv2.equalizeHist()`实现均衡化。我们把两张图片并排显示,对比一下: 101 | 102 | ```python 103 | cv2.imshow('equalization', np.hstack((img, equ))) # 并排显示 104 | cv2.waitKey(0) 105 | ``` 106 | 107 | ![](http://cos.codec.wang/cv2_before_after_equalization.jpg) 108 | 109 | ![均衡化前后的直方图对比](http://cos.codec.wang/cv2_before_after_equalization_histogram.jpg) 110 | 111 | 可以看到均衡化后图片的亮度和对比度效果明显好于原图。 112 | 113 | ### 自适应均衡化 114 | 115 | 不难看出来,直方图均衡化是应用于整幅图片的,会有什么问题呢?看下图: 116 | 117 | ![](http://cos.codec.wang/cv2_understand_adaptive_histogram.jpg) 118 | 119 | 很明显,因为全局调整亮度和对比度的原因,脸部太亮,大部分细节都丢失了。 120 | 121 | 自适应均衡化就是用来解决这一问题的:它在每一个小区域内(默认 8×8)进行直方图均衡化。当然,如果有噪点的话,噪点会被放大,需要对小区域内的对比度进行了限制,所以这个算法全称叫:**对比度受限的自适应直方图均衡化**CLAHE\([Contrast Limited Adaptive Histogram Equalization](https://en.wikipedia.org/wiki/Adaptive_histogram_equalization)\)。 122 | 123 | ```python 124 | # 自适应均衡化,参数可选 125 | clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) 126 | cl1 = clahe.apply(img) 127 | ``` 128 | 129 | ![](http://cos.codec.wang/cv2_adaptive_histogram.jpg) 130 | 131 | ## 练习 132 | 133 | 1. `cv2.calcHist()`函数中的参数 3 是指要计算的区域\(mask:目标区域白色,其余黑色\),编写一个只计算图片左上角 200×200 区域直方图的程序。 134 | 135 | ![](http://cos.codec.wang/cv2_histogram_mask.jpg) 136 | 137 | ## 小结 138 | 139 | - 直方图是一种分析图像的手段。 140 | - `cv2.calcHist()`和`numpy.bincount()`均可用来计算直方图,使用 Matplotlib 绘制直方图。 141 | - 均衡化用来使图像的直方图分布更加均匀,提升亮度和对比度。 142 | 143 | ## 引用 144 | 145 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/15-Histograms) 146 | - [Histograms - 1 : Find, Plot, Analyze !!!](https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_histograms/py_histogram_begins/py_histogram_begins.html#histograms-getting-started) 147 | - [Histograms - 2: Histogram Equalization](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_histograms/py_histogram_equalization/py_histogram_equalization.html#histogram-equalization) 148 | - [维基百科:直方图均衡化](https://zh.wikipedia.org/wiki/%E7%9B%B4%E6%96%B9%E5%9B%BE%E5%9D%87%E8%A1%A1%E5%8C%96) 149 | - [维基百科:自适应直方图均衡化](https://en.wikipedia.org/wiki/Adaptive_histogram_equalization) 150 | - [Cambridge in Color website](http://www.cambridgeincolour.com/tutorials/histograms1.htm) 151 | -------------------------------------------------------------------------------- /docs/start/08-drawing-function.md: -------------------------------------------------------------------------------- 1 | # 08: 绘图功能 2 | 3 | ![](http://cos.codec.wang/cv2_drawing_functions.jpg) 4 | 5 | 学习画线、圆和矩形等多种几何形状,给图片添加文字。图片等可到文末引用处下载。 6 | 7 | ## 目标 8 | 9 | - 绘制各种几何形状、添加文字 10 | - OpenCV 函数:`cv2.line()`, `cv2.circle()`, `cv2.rectangle()`, `cv2.ellipse()`, `cv2.putText()` 11 | 12 | ## 教程 13 | 14 | ### 参数说明 15 | 16 | 绘制形状的函数有一些共同的参数,提前在此说明一下: 17 | 18 | - img:要绘制形状的图片 19 | - color:绘制的颜色 20 | - 彩色图就传入 BGR 的一组值,如蓝色就是 (255, 0, 0) 21 | - 灰度图,传入一个灰度值就行 22 | - thickness:线宽,默认为 1;**对于矩形/圆之类的封闭形状而言,传入 -1 表示填充形状** 23 | 24 | 需要导入的模块和显示图片的通用代码: 25 | 26 | ```python 27 | import cv2 28 | import numpy as np 29 | import matplotlib.pyplot as plt 30 | 31 | cv2.imshow('img', img) 32 | cv2.waitKey(0) 33 | ``` 34 | 35 | ![绘制各种几何形状](http://cos.codec.wang/cv2_drawing_functions.jpg) 36 | 37 | 上图就是本教程绘制的最终效果,下面一步步来看: 38 | 39 | ### 画线 40 | 41 | 画直线只需指定起点和终点的坐标就行: 42 | 43 | ```python 44 | # 创建一副黑色的图片 45 | img = np.zeros((512, 512, 3), np.uint8) 46 | # 画一条线宽为 5 的蓝色直线,参数 2:起点,参数 3:终点 47 | cv2.line(img, (0, 0), (512, 512), (255, 0, 0), 5) 48 | ``` 49 | 50 | :::tip 51 | 所有绘图函数均会直接影响原图片,这点要注意。 52 | ::: 53 | 54 | ### 画矩形 55 | 56 | 画矩形需要知道左上角和右下角的坐标: 57 | 58 | ```python 59 | # 画一个绿色边框的矩形,参数 2:左上角坐标,参数 3:右下角坐标 60 | cv2.rectangle(img, (384, 0), (510, 128), (0, 255, 0), 3) 61 | ``` 62 | 63 | ### 画圆 64 | 65 | 画圆需要指定圆心和半径,注意下面的例子中线宽=-1 代表填充: 66 | 67 | ```python 68 | # 画一个填充红色的圆,参数 2:圆心坐标,参数 3:半径 69 | cv2.circle(img, (447, 63), 63, (0, 0, 255), -1) 70 | ``` 71 | 72 | ### 画椭圆 73 | 74 | 画椭圆需要的参数比较多,请对照后面的代码理解这几个参数: 75 | 76 | - 参数 2:椭圆中心 (x,y) 77 | - 参数 3:x/y 轴的长度 78 | - 参数 4:angle - 椭圆的旋转角度 79 | - 参数 5:startAngle - 椭圆的起始角度 80 | - 参数 6:endAngle - 椭圆的结束角度 81 | 82 | :::tip 83 | OpenCV 中原点在左上角,所以这里的角度是以顺时针方向计算的。 84 | ::: 85 | 86 | ```python 87 | # 在图中心画一个填充的半圆 88 | cv2.ellipse(img, (256, 256), (100, 50), 0, 0, 180, (255, 0, 0), -1) 89 | ``` 90 | 91 | ### 画多边形 92 | 93 | 画多边形需要指定一系列多边形的顶点坐标,相当于从第一个点到第二个点画直线,再从第二个点到第三个点画直线.... 94 | 95 | OpenCV 中需要先将多边形的顶点坐标需要变成顶点数 ×1×2 维的矩阵,再来绘制: 96 | 97 | ```python 98 | # 定义四个顶点坐标 99 | pts = np.array([[10, 5], [50, 10], [70, 20], [20, 30]], np.int32) 100 | # 顶点个数:4,矩阵变成 4*1*2 维 101 | pts = pts.reshape((-1, 1, 2)) 102 | cv2.polylines(img, [pts], True, (0, 255, 255)) 103 | ``` 104 | 105 | `cv2.polylines()`的参数 3 如果是 False 的话,多边形就不闭合。 106 | 107 | :::tip 108 | 如果需要绘制多条直线,使用 cv2.polylines\(\) 要比 cv2.line\(\) 高效很多,例如: 109 | ::: 110 | 111 | ```python 112 | # 使用 cv2.polylines() 画多条直线 113 | line1 = np.array([[100, 20], [300, 20]], np.int32).reshape((-1, 1, 2)) 114 | line2 = np.array([[100, 60], [300, 60]], np.int32).reshape((-1, 1, 2)) 115 | line3 = np.array([[100, 100], [300, 100]], np.int32).reshape((-1, 1, 2)) 116 | cv2.polylines(img, [line1, line2, line3], True, (0, 255, 255)) 117 | ``` 118 | 119 | ### 添加文字 120 | 121 | 使用`cv2.putText()`添加文字,它的参数也比较多,同样请对照后面的代码理解这几个参数: 122 | 123 | - 参数 2:要添加的文本 124 | - 参数 3:文字的起始坐标(左下角为起点) 125 | - 参数 4:字体 126 | - 参数 5:文字大小(缩放比例) 127 | 128 | ```python 129 | # 添加文字 130 | font = cv2.FONT_HERSHEY_SIMPLEX 131 | cv2.putText(img, 'ex2tron', (10, 500), font, 132 | 4, (255, 255, 255), 2, lineType=cv2.LINE_AA) 133 | ``` 134 | 135 | 字体可参考:[HersheyFonts](https://docs.opencv.org/4.0.0/d6/d6e/group__imgproc__draw.html#ga0f9314ea6e35f99bb23f29567fc16e11)。另外,这里有个线型 lineType 参数,LINE_AA 表示抗锯齿线型,具体可见[LineTypes](https://docs.opencv.org/3.3.1/d0/de1/group__core.html#gaf076ef45de481ac96e0ab3dc2c29a777) 136 | 137 | ## 小结 138 | 139 | - `cv2.line()`画直线,`cv2.circle()`画圆,`cv2.rectangle()`画矩形,`cv2.ellipse()`画椭圆,`cv2.polylines()`画多边形,`cv2.putText()`添加文字。 140 | - 画多条直线时,`cv2.polylines()`要比`cv2.line()`高效很多。 141 | 142 | ## 练习 143 | 144 | 1. 你能用已学的绘图功能画出 OpenCV 的 logo 吗?\(提示:椭圆和圆\) 145 | 146 | ![OpenCV logo](http://cos.codec.wang/cv2_draw_opencv_logo.jpg) 147 | 148 | ## 接口文档 149 | 150 | - [cv2.line\(\)](https://docs.opencv.org/4.0.0/d6/d6e/group__imgproc__draw.html#ga7078a9fae8c7e7d13d24dac2520ae4a2) 151 | - [cv2.circle\(\)](https://docs.opencv.org/4.0.0/d6/d6e/group__imgproc__draw.html#gaf10604b069374903dbd0f0488cb43670) 152 | - [cv2.rectangle\(\)](https://docs.opencv.org/4.0.0/d6/d6e/group__imgproc__draw.html#ga07d2f74cadcf8e305e810ce8eed13bc9) 153 | - [cv2.ellipse\(\)](https://docs.opencv.org/4.0.0/d6/d6e/group__imgproc__draw.html#ga28b2267d35786f5f890ca167236cbc69) 154 | - [cv2.putText\(\)](https://docs.opencv.org/4.0.0/d6/d6e/group__imgproc__draw.html#ga5126f47f883d730f633d74f07456c576) 155 | - [cv2.polylines\(\)](https://docs.opencv.org/4.0.0/d6/d6e/group__imgproc__draw.html#ga1ea127ffbbb7e0bfc4fd6fd2eb64263c) 156 | 157 | ## 引用 158 | 159 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/08-Drawing-Function) 160 | - [LineTypes](https://docs.opencv.org/3.3.1/d0/de1/group__core.html#gaf076ef45de481ac96e0ab3dc2c29a777) 161 | - [Drawing Functions in OpenCV](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_gui/py_drawing_functions/py_drawing_functions.html) 162 | -------------------------------------------------------------------------------- /docs/basic/extra-09-image-gradients.md: -------------------------------------------------------------------------------- 1 | # 番外篇:图像梯度 2 | 3 | ![](http://cos.codec.wang/cv2_horizen_vertical_edge_detection.jpg) 4 | 5 | 了解图像梯度和边缘检测的相关概念。图片等可到文末引用处下载。 6 | 7 | 还记得前面[平滑图像](./smoothing-images)中提到的滤波与模糊的区别吗?我们说低通滤波器是模糊,高通滤波器是锐化,这节我们就来看看高通滤波器。 8 | 9 | ## [图像梯度](https://baike.baidu.com/item/图像梯度/8528837?fr=aladdin) 10 | 11 | 如果你还记得高数中用一阶导数来求极值的话,就很容易理解了:把图片想象成连续函数,因为边缘部分的像素值是与旁边像素明显有区别的,所以对图片局部求极值,就可以得到整幅图片的边缘信息了。不过图片是二维的离散函数,导数就变成了差分,这个差分就称为图像的梯度。 12 | 13 | 当然,大部分人应该是早忘记高数了\( ̄ ▽  ̄\)",所以看不懂的话,就把上面的解释划掉,我们重新从卷积的角度来看看。 14 | 15 | ### 垂直边缘提取 16 | 17 | 滤波是应用卷积来实现的,卷积的关键就是卷积核,我们来考察下面这个卷积核: 18 | 19 | $$ 20 | k1 = \left[ 21 | \begin{matrix} 22 | -1 & 0 & 1 \newline 23 | -2 & 0 & 2 \newline 24 | -1 & 0 & 1 25 | \end{matrix} 26 | \right] 27 | $$ 28 | 29 | 这个核是用来提取图片中的垂直边缘的,怎么做到的呢?看下图: 30 | 31 | ![](http://cos.codec.wang/cv2_understand_sobel_edge_detection.jpg) 32 | 33 | 当前列左右两侧的元素进行差分,由于边缘的值明显小于(或大于)周边像素,所以边缘的差分结果会明显不同,这样就提取出了垂直边缘。同理,把上面那个矩阵转置一下,就是提取水平边缘。这种差分操作就称为图像的梯度计算: 34 | 35 | $$ 36 | k2 = \left[ 37 | \begin{matrix} 38 | -1 & -2 & -1 \newline 39 | 0 & 0 & 0 \newline 40 | 1 & 2 & 1 41 | \end{matrix} 42 | \right] 43 | $$ 44 | 45 | ![垂直和水平边缘提取](http://cos.codec.wang/cv2_horizen_vertical_edge_detection.jpg) 46 | 47 | > 还记得滤波函数`cv2.filter2D()`吗?([番外篇:卷积基础](./extra-08-padding-and-convolution))我们来手动实现上面的功能: 48 | 49 | ```python 50 | img = cv2.imread('sudoku.jpg', 0) 51 | 52 | # 自己进行垂直边缘提取 53 | kernel = np.array([[-1, 0, 1], 54 | [-2, 0, 2], 55 | [-1, 0, 1]], dtype=np.float32) 56 | dst_v = cv2.filter2D(img, -1, kernel) 57 | # 自己进行水平边缘提取 58 | dst_h = cv2.filter2D(img, -1, kernel.T) 59 | # 横向并排对比显示 60 | cv2.imshow('edge', np.hstack((img, dst_v, dst_h))) 61 | cv2.waitKey(0) 62 | ``` 63 | 64 | ### Sobel 算子 65 | 66 | 上面的这种差分方法就叫[Sobel 算子](https://baike.baidu.com/item/Sobel%E7%AE%97%E5%AD%90/11000092?fr=aladdin),它先在垂直方向计算梯度$G_x=k_1×src$,再在水平方向计算梯度$G_y=k_2×src$,最后求出总梯度:$G=\sqrt{Gx^2+Gy^2}$ 67 | 68 | 我们可以把前面的代码用 Sobel 算子更简单地实现: 69 | 70 | ```python 71 | sobelx = cv2.Sobel(img, -1, 1, 0, ksize=3) # 只计算 x 方向 72 | sobely = cv2.Sobel(img, -1, 0, 1, ksize=3) # 只计算 y 方向 73 | ``` 74 | 75 | :::tip 76 | 很多人疑问,Sobel 算子的卷积核这几个值是怎么来的呢?事实上,并没有规定,你可以用你自己的。 77 | ::: 78 | 79 | 比如,最初只利用领域间的原始差值来检测边缘的[Prewitt 算子](https://baike.baidu.com/item/Prewitt%E7%AE%97%E5%AD%90/8415245?fr=aladdin): 80 | 81 | $$ 82 | K = \left[ 83 | \begin{matrix} 84 | -1 & 0 & 1 \newline 85 | -1 & 0 & 1 \newline 86 | -1 & 0 & 1 87 | \end{matrix} 88 | \right] 89 | $$ 90 | 91 | 还有比 Sobel 更好用的**Scharr 算子**,大家可以了解下: 92 | 93 | $$ 94 | K = \left[ 95 | \begin{matrix} 96 | -3 & 0 & 3 \newline 97 | -10 & 0 & 10 \newline 98 | -3 & 0 & 3 99 | \end{matrix} 100 | \right] 101 | $$ 102 | 103 | 这些算法都是一阶边缘检测的代表,网上也有算子之间的对比资料,有兴趣的可参考文末引用。 104 | 105 | ### Laplacian 算子 106 | 107 | 高数中用一阶导数求极值,在这些极值的地方,二阶导数为 0,所以也可以通过求二阶导计算梯度: 108 | 109 | $$ 110 | dst=\frac{\partial^2 f}{\partial x^2}+\frac{\partial^2 f}{\partial y^2} 111 | $$ 112 | 113 | 一维的一阶和二阶差分公式分别为: 114 | 115 | $$ 116 | \frac{\partial f}{\partial x}=f(x+1)-f(x) 117 | $$ 118 | 119 | $$ 120 | \frac{\partial^2 f}{\partial x^2}=f(x+1)+f(x-1)-2f(x) 121 | $$ 122 | 123 | 提取前面的系数,那么一维的 Laplacian 滤波核是: 124 | 125 | $$ 126 | K=\left[ 127 | \begin{matrix} 128 | 1 & -2 & 1 129 | \end{matrix} 130 | \right] 131 | $$ 132 | 133 | 而对于二维函数 f\(x,y\),两个方向的二阶差分分别是: 134 | 135 | $$ 136 | \frac{\partial^2 f}{\partial x^2}=f(x+1,y)+f(x-1,y)-2f(x,y) 137 | $$ 138 | 139 | $$ 140 | \frac{\partial^2 f}{\partial y^2}=f(x,y+1)+f(x,y-1)-2f(x,y) 141 | $$ 142 | 143 | 合在一起就是: 144 | 145 | $$ 146 | \triangledown^2 f(x,y)=f(x+1,y)+f(x-1,y)+f(x,y+1)+f(x,y-1)-4f(x,y) 147 | $$ 148 | 149 | 同样提取前面的系数,那么二维的 Laplacian 滤波核就是: 150 | 151 | $$ 152 | K = \left[ 153 | \begin{matrix} 154 | 0 & 1 & 0 \newline 155 | 1 & -4 & 1 \newline 156 | 0 & 1 & 0 157 | \end{matrix} 158 | \right] 159 | $$ 160 | 161 | 这就是 Laplacian 算子的图像卷积模板,有些资料中在此基础上考虑斜对角情况,将卷积核拓展为: 162 | 163 | $$ 164 | K = \left[ 165 | \begin{matrix} 166 | 1 & 1 & 1 \newline 167 | 1 & -8 & 1 \newline 168 | 1 & 1 & 1 169 | \end{matrix} 170 | \right] 171 | $$ 172 | 173 | OpenCV 中直接使用`cv2.Laplacian()`函数: 174 | 175 | ```python 176 | laplacian = cv2.Laplacian(img, -1) # 使用 Laplacian 算子 177 | ``` 178 | 179 | ![](http://cos.codec.wang/cv2_laplacian.jpg) 180 | 181 | Laplacian 算子是二阶边缘检测的典型代表,一/二阶边缘检测各有优缺点,大家可自行了解。 182 | 183 | ## 练习 184 | 185 | 1. (选做)同志们有空补补高数~~姿势~~(知识)呗!\(✿◕‿◕✿\) 186 | 187 | ## 引用 188 | 189 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/Extra-09-Image-Gradients) 190 | - [Image Gradients](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_gradients/py_gradients.html) 191 | - [Sobel 导数](http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/imgproc/imgtrans/sobel_derivatives/sobel_derivatives.html#sobel-derivatives) 192 | - [维基百科:边缘检测](https://zh.wikipedia.org/wiki/%E8%BE%B9%E7%BC%98%E6%A3%80%E6%B5%8B) 193 | - [数字图像 - 边缘检测原理 - Sobel, Laplace, Canny 算子](https://www.jianshu.com/p/2334bee37de5) 194 | -------------------------------------------------------------------------------- /docs/start/challenge-01-draw-dynamic-clock.md: -------------------------------------------------------------------------------- 1 | # 挑战任务:画动态时钟 2 | 3 | ![](http://cos.codec.wang/cv2_draw_clock_dynamic_sample.gif) 4 | 5 | 挑战任务:使用 OpenCV 绘制一个随系统时间动态变化的时钟。 6 | 7 | ## 挑战内容 8 | 9 | > **完成如下图所展示的动态时钟,时钟需随系统时间变化,中间显示当前日期。** 10 | 11 | ![](http://cos.codec.wang/cv2_draw_clock_dynamic_sample.gif) 12 | 13 | 其实本次任务涉及的 OpenCV 知识并不多,但有助于提升大家的编程实践能力。 14 | 15 | **挑战题不会做也木有关系,但请务必在自行尝试后,再看下面的解答噢,**不然...我也没办法\( ̄ ▽  ̄\)" 16 | 17 | ## 挑战解答 18 | 19 | ### 方案 20 | 21 | 本次挑战任务旨在提升大家的动手实践能力,解决实际问题,所以我们得先有个解题思路和方案。观察下常见的时钟表盘: 22 | 23 | ![](http://cos.codec.wang/cv2_draw_clock_actual_clock_sample.jpg) 24 | 25 | 整个表盘其实只有 3 根表针在动,所以可以先画出静态表盘,然后获取系统当前时间,根据时间实时动态绘制 3 根表针就解决了。 26 | 27 | ### 绘制表盘 28 | 29 | 表盘上只有 60 条分/秒刻线和 12 条小时刻线,当然还有表盘的外部轮廓圆,也就是重点在如何画 72 根线。先把简单的圆画出来: 30 | 31 | ```python 32 | import cv2 33 | import math 34 | import datetime 35 | import numpy as np 36 | 37 | margin = 5 # 上下左右边距 38 | radius = 220 # 圆的半径 39 | center = (center_x, center_y) = (225, 225) # 圆心 40 | 41 | # 1. 新建一个画板并填充成白色 42 | img = np.zeros((450, 450, 3), np.uint8) 43 | img[:] = (255, 255, 255) 44 | 45 | # 2. 画出圆盘 46 | cv2.circle(img, center, radius, (0, 0, 0), thickness=5) 47 | ``` 48 | 49 | ![](http://cos.codec.wang/cv2_draw_clock_blank_circle.jpg) 50 | 51 | 前面我们使用 OpenCV 画直线的时候,需知道直线的起点和终点坐标,那么画 72 根线就变成了获取 72 组坐标。 52 | 53 | 在平面坐标系下,已知半径和角度的话,A 点的坐标可以表示为: 54 | 55 | $$ 56 | \begin{matrix} 57 | x=r\times \cos\alpha \newline 58 | y=r\times \sin\alpha 59 | \end{matrix} 60 | $$ 61 | 62 | ![](http://cos.codec.wang/cv2_draw_clock_center_shift.jpg) 63 | 64 | 先只考虑将坐标系原点移动到左上角,角度依然是平面坐标系中的逆时针计算,那么新坐标是: 65 | 66 | $$ 67 | \begin{matrix} 68 | x=r+r\times \cos\alpha \newline 69 | y=r+r\times \sin\alpha 70 | \end{matrix} 71 | $$ 72 | 73 | 对于 60 条分/秒刻线,刻线间的夹角是 360°/60=6°,对于小时刻线,角度是 360°/12=30°,这样就得到了 72 组起点坐标,那怎么得到终点坐标呢?其实同样的原理,用一个同心的小圆来计算得到 B 点: 74 | 75 | ![](http://cos.codec.wang/cv2_draw_clock_a_b_position.jpg) 76 | 77 | 通过 A/B 两点就可以画出直线: 78 | 79 | ```python 80 | pt1 = [] 81 | 82 | # 3. 画出 60 条秒和分钟的刻线 83 | for i in range(60): 84 | # 最外部圆,计算 A 点 85 | x1 = center_x+(radius-margin)*math.cos(i*6*np.pi/180.0) 86 | y1 = center_y+(radius-margin)*math.sin(i*6*np.pi/180.0) 87 | pt1.append((int(x1), int(y1))) 88 | 89 | # 同心小圆,计算 B 点 90 | x2 = center_x+(radius-15)*math.cos(i*6*np.pi/180.0) 91 | y2 = center_y+(radius-15)*math.sin(i*6*np.pi/180.0) 92 | 93 | cv2.line(img, pt1[i], (int(x2), int(y2)), (0, 0, 0), thickness=2) 94 | 95 | # 4. 画出 12 条小时的刻线 96 | for i in range(12): 97 | # 12 条小时刻线应该更长一点 98 | x = center_x+(radius-25)*math.cos(i*30*np.pi/180.0) 99 | y = center_y+(radius-25)*math.sin(i*30*np.pi/180.0) 100 | # 这里用到了前面的 pt1 101 | cv2.line(img, pt1[i*5], (int(x), int(y)), (0, 0, 0), thickness=5) 102 | 103 | # 到这里基本的表盘图就已经画出来了 104 | ``` 105 | 106 | ![](http://cos.codec.wang/cv2_draw_clock_blank_clock.jpg) 107 | 108 | ### 角度换算 109 | 110 | 接下来算是一个小难点,首先**时钟的起始坐标在正常二维坐标系的 90° 方向,其次时钟跟图像一样,都是顺时针计算角度的**,所以三者需要统一下: 111 | 112 | ![](http://cos.codec.wang/cv2_draw_clock_different_clock_contrast.jpg) 113 | 114 | 因为角度是完全对称的,顺逆时针没有影响,所以平面坐标系完全不用理会,放在这里只是便于大家理解。对于时钟坐标和图像坐标,时钟 0 的 0° 对应图像的 270°,时钟 15 的 90° 对应图像的 360°,时钟 30 的 180° 对应图像的 450°(360°+90°)... 115 | 116 | 所以两者之间的关系便是: 117 | 118 | ```text 119 | 计算角度 = 时钟角度 +270° 120 | 计算角度 = 计算角度 if 计算角度<=360° else 计算角度-360° 121 | ``` 122 | 123 | ### 同步时间 124 | 125 | Python 中如何获取当前时间和添加日期文字都比较简单,看代码就行,我就不解释了。代码中角度计算我换了一种方式,其实是一样的,看你能不能看懂\(●ˇ∀ˇ●\): 126 | 127 | ```python 128 | while(1): 129 | # 不断拷贝表盘图,才能更新绘制,不然会重叠在一起 130 | temp = np.copy(img) 131 | 132 | # 5. 获取系统时间,画出动态的时 - 分 - 秒三条刻线 133 | now_time = datetime.datetime.now() 134 | hour, minute, second = now_time.hour, now_time.minute, now_time.second 135 | 136 | # 画秒刻线 137 | # OpenCV 中的角度是顺时针计算的,所以需要转换下 138 | sec_angle = second*6+270 if second <= 15 else (second-15)*6 139 | sec_x = center_x+(radius-margin)*math.cos(sec_angle*np.pi/180.0) 140 | sec_y = center_y+(radius-margin)*math.sin(sec_angle*np.pi/180.0) 141 | cv2.line(temp, center, (int(sec_x), int(sec_y)), (203, 222, 166), 2) 142 | 143 | # 画分刻线 144 | min_angle = minute*6+270 if minute <= 15 else (minute-15)*6 145 | min_x = center_x+(radius-35)*math.cos(min_angle*np.pi/180.0) 146 | min_y = center_y+(radius-35)*math.sin(min_angle*np.pi/180.0) 147 | cv2.line(temp, center, (int(min_x), int(min_y)), (186, 199, 137), 8) 148 | 149 | # 画时刻线 150 | hour_angle = hour*30+270 if hour <= 3 else (hour-3)*30 151 | hour_x = center_x+(radius-65)*math.cos(hour_angle*np.pi/180.0) 152 | hour_y = center_y+(radius-65)*math.sin(hour_angle*np.pi/180.0) 153 | cv2.line(temp, center, (int(hour_x), int(hour_y)), (169, 198, 26), 15) 154 | 155 | # 6. 添加当前日期文字 156 | font = cv2.FONT_HERSHEY_SIMPLEX 157 | time_str = now_time.strftime("%d/%m/%Y") 158 | cv2.putText(img, time_str, (135, 275), font, 1, (0, 0, 0), 2) 159 | 160 | cv2.imshow('clocking', temp) 161 | if cv2.waitKey(1) == 27: # 按下 ESC 键退出 162 | break 163 | ``` 164 | 165 | ![](http://cos.codec.wang/cv2_draw_clock_sample.jpg) 166 | 167 | 本此挑战旨在锻炼一步步解决实际问题的思路(虽然有点数学知识\( ̄ ▽  ̄\)"),大家再接再厉噢! 168 | 169 | ## 引用 170 | 171 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/Challenge-01-Draw-Dynamic-Clock) 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 面向初学者的 OpenCV-Python 教程 2 | 3 | ![](http://cos.codec.wang/opencv-python-tutorial-amend-new-cover.png) 4 | 5 | ## 访问 6 | 7 | 1. 可直接访问博客专栏:http://codec.wang/#/opencv/ 8 | 9 | 2. 使用 Docker 访问: 10 | 11 | ```bash 12 | docker run -d -p 8080:80 codecwang/opencv-python-tutorial 13 | ``` 14 | 15 | 3. 源码构建: 16 | 17 | 本教程网站由[Docusaurus 2](https://docusaurus.io)构建,如感兴趣,可直接克隆代码自行构建: 18 | 19 | ```bash 20 | # 克隆仓库 21 | git clone git@github.com:CodecWang/opencv-python-tutorial.git 22 | 23 | # 安装依赖 24 | npm install 25 | # 本地调试 26 | npm start 27 | # 构建 28 | npm build 29 | ``` 30 | 31 | ## 目录 32 | 33 | ### 入门篇 34 | 35 | | 标题 | 简介 | 36 | | :--------------------------------------------------------------------------------------------------------------- | :--------------------------------------- | 37 | | [简介与安装](http://codec.wang/#/opencv/start/01-introduction-and-installation) | 了解和安装 OpenCV-Python | 38 | | [番外篇:代码性能优化](http://codec.wang/#/opencv/start/extra-01-code-optimization) | 度量运行时间/提升效率的几种方式 | 39 | | [基本元素:图片](http://codec.wang/#/opencv/start/02-basic-element-image) | 图片的载入/显示和保存 | 40 | | [番外篇:无损保存和 Matplotlib 使用](http://codec.wang/#/opencv/start/extra-02-high-quality-save-and-matplotlib) | 高保真保存图片、Matplotlib 库的简单使用 | 41 | | [打开摄像头](http://codec.wang/#/opencv/start/03-open-camera) | 打开摄像头捕获图片/录制视频/播放本地视频 | 42 | | [番外篇:滑动条](http://codec.wang/#/opencv/start/extra-03-trackbar) | 滑动条的使用 | 43 | | [图像基本操作](http://codec.wang/#/opencv/start/04-basic-operations) | 访问像素点/ROI/通道分离合并/图片属性 | 44 | | [颜色空间转换](http://codec.wang/#/opencv/start/05-changing-colorspaces) | 颜色空间转换/追踪特定颜色物体 | 45 | | [阈值分割](http://codec.wang/#/opencv/start/06-image-thresholding) | 阈值分割/二值化 | 46 | | [番外篇:Otsu 阈值法](http://codec.wang/#/opencv/start/extra-04-otsu-thresholding) | 双峰图片/Otsu 自动阈值法 | 47 | | [图像几何变换](http://codec.wang/#/opencv/start/07-image-geometric-transformation) | 旋转/平移/缩放/翻转 | 48 | | [番外篇:仿射变换与透视变换](http://codec.wang/#/opencv/start/extra-05-warpaffine-warpperspective) | 基于 2×3 的仿射变换/基于 3×3 的透视变换 | 49 | | [绘图功能](http://codec.wang/#/opencv/start/08-drawing-function) | 画线/画圆/画矩形/添加文字 | 50 | | [番外篇:鼠标绘图](http://codec.wang/#/opencv/start/extra-06-drawing-with-mouse) | 用鼠标实时绘图 | 51 | | [挑战篇:画动态时钟](http://codec.wang/#/opencv/start/challenge-01-draw-dynamic-clock) | / | 52 | | [挑战篇:PyQt5 编写 GUI 界面](http://codec.wang/#/opencv/start/challenge-02-create-gui-with-pyqt5) | / | 53 | 54 | ### 基础篇 55 | 56 | | 标题 | 简介 | 57 | | :----------------------------------------------------------------------------------------------- | :------------------------------------- | 58 | | [图像混合](http://codec.wang/#/opencv/basic/09-image-blending) | 算数运算/混合/按位运算 | 59 | | [番外篇:亮度与对比度](http://codec.wang/#/opencv/basic/extra-07-contrast-and-brightness) | 调整图片的亮度和对比度 | 60 | | [平滑图像](http://codec.wang/#/opencv/basic/10-smoothing-images) | 卷积/滤波/模糊/降噪 | 61 | | [番外篇:卷积基础 - 图片边框](http://codec.wang/#/opencv/basic/extra-08-padding-and-convolution) | 了解卷积/滤波的基础知识/给图片添加边框 | 62 | | [边缘检测](http://codec.wang/#/opencv/basic/11-edge-detection) | Canny/Sobel 算子 | 63 | | [番外篇:图像梯度](http://codec.wang/#/opencv/basic/extra-09-image-gradients) | 了解图像梯度和边缘检测的相关概念 | 64 | | [腐蚀与膨胀](http://codec.wang/#/opencv/basic/12-erode-and-dilate) | 形态学操作/腐蚀/膨胀/开运算/闭运算 | 65 | | [轮廓](http://codec.wang/#/opencv/basic/13-contours) | 寻找/绘制轮廓 | 66 | | [番外篇:轮廓层级](http://codec.wang/#/opencv/basic/extra-10-contours-hierarchy) | 了解轮廓间的层级关系 | 67 | | [轮廓特征](http://codec.wang/#/opencv/basic/14-contour-features) | 面积/周长/最小外接矩\(圆\)/形状匹配 | 68 | | [番外篇:凸包及更多轮廓特征](http://codec.wang/#/opencv/basic/extra-11-convex-hull) | 计算凸包/了解更多轮廓特征 | 69 | | [直方图](http://codec.wang/#/opencv/basic/15-histograms) | 计算绘制直方图/均衡化 | 70 | | [模板匹配](http://codec.wang/#/opencv/basic/16-template-matching) | 图中找小图 | 71 | | [霍夫变换](http://codec.wang/#/opencv/basic/17-hough-transform) | 提取直线/圆 | 72 | | [挑战任务:车道检测](http://codec.wang/#/opencv/basic/challenge-03-lane-road-detection) | / | 73 | 74 | > 如果您觉得写的不错的话,欢迎打赏,我会努力写出更好的内容!✊🤟 75 | 76 | ![](http://cos.codec.wang/wechat_alipay_pay_pic.png) 77 | -------------------------------------------------------------------------------- /docs/basic/14-contour-features.md: -------------------------------------------------------------------------------- 1 | # 14: 轮廓特征 2 | 3 | ![](http://cos.codec.wang/cv2_min_rect_rect_bounding.jpg) 4 | 5 | 学习计算轮廓特征,如面积、周长、最小外接矩形等。图片等可到文末引用处下载。 6 | 7 | ## 目标 8 | 9 | - 计算物体的周长、面积、质心、最小外接矩形等 10 | - OpenCV 函数:`cv2.contourArea()`, `cv2.arcLength()`, `cv2.approxPolyDP()` 等 11 | 12 | ## 教程 13 | 14 | 在计算轮廓特征之前,我们先用上一节的代码把轮廓找到: 15 | 16 | ![](http://cos.codec.wang/cv2_31_handwriting_sample.jpg) 17 | 18 | ```python 19 | import cv2 20 | import numpy as np 21 | 22 | img = cv2.imread('handwriting.jpg', 0) 23 | _, thresh = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) 24 | image, contours, hierarchy = cv2.findContours(thresh, 3, 2) 25 | 26 | # 以数字 3 的轮廓为例 27 | cnt = contours[0] 28 | ``` 29 | 30 | 为了便于绘制,我们创建出两幅彩色图,并把轮廓画在第一幅图上: 31 | 32 | ```python 33 | img_color1 = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) 34 | img_color2 = np.copy(img_color1) 35 | cv2.drawContours(img_color1, [cnt], 0, (0, 0, 255), 2) 36 | ``` 37 | 38 | ### 轮廓面积 39 | 40 | ```python 41 | area = cv2.contourArea(cnt) # 4386.5 42 | ``` 43 | 44 | 注意轮廓特征计算的结果并不等同于像素点的个数,而是根据几何方法算出来的,所以有小数。 45 | 46 | > 如果统计二值图中像素点个数,应尽量避免循环,**可以使用`cv2.countNonZero()`**,更加高效。 47 | 48 | ### 轮廓周长 49 | 50 | ```python 51 | perimeter = cv2.arcLength(cnt, True) # 585.7 52 | ``` 53 | 54 | 参数 2 表示轮廓是否封闭,显然我们的轮廓是封闭的,所以是 True。 55 | 56 | ### 图像矩 57 | 58 | 矩可以理解为图像的各类几何特征,详情请参考:\[[Image Moments](http://en.wikipedia.org/wiki/Image_moment)\] 59 | 60 | ```python 61 | M = cv2.moments(cnt) 62 | ``` 63 | 64 | M 中包含了很多轮廓的特征信息,比如 M\['m00'\] 表示轮廓面积,与前面`cv2.contourArea()`计算结果是一样的。质心也可以用它来算: 65 | 66 | ```python 67 | cx, cy = M['m10'] / M['m00'], M['m01'] / M['m00'] # (205, 281) 68 | ``` 69 | 70 | ### 外接矩形 71 | 72 | 形状的外接矩形有两种,如下图,绿色的叫外接矩形,表示不考虑旋转并且能包含整个轮廓的矩形。蓝色的叫最小外接矩,考虑了旋转: 73 | 74 | ![](http://cos.codec.wang/cv2_min_rect_rect_bounding.jpg) 75 | 76 | ```python 77 | x, y, w, h = cv2.boundingRect(cnt) # 外接矩形 78 | cv2.rectangle(img_color1, (x, y), (x + w, y + h), (0, 255, 0), 2) 79 | ``` 80 | 81 | ```python 82 | rect = cv2.minAreaRect(cnt) # 最小外接矩形 83 | box = np.int0(cv2.boxPoints(rect)) # 矩形的四个角点取整 84 | cv2.drawContours(img_color1, [box], 0, (255, 0, 0), 2) 85 | ``` 86 | 87 | 其中 np.int0\(x\) 是把 x 取整的操作,比如 377.93 就会变成 377,也可以用 x.astype\(np.int\)。 88 | 89 | ### 最小外接圆 90 | 91 | 外接圆跟外接矩形一样,找到一个能包围物体的最小圆: 92 | 93 | ```python 94 | (x, y), radius = cv2.minEnclosingCircle(cnt) 95 | (x, y, radius) = np.int0((x, y, radius)) # 圆心和半径取整 96 | cv2.circle(img_color2, (x, y), radius, (0, 0, 255), 2) 97 | ``` 98 | 99 | ![](http://cos.codec.wang/cv2_min_enclosing_circle.jpg) 100 | 101 | ### 拟合椭圆 102 | 103 | 我们可以用得到的轮廓拟合出一个椭圆: 104 | 105 | ```python 106 | ellipse = cv2.fitEllipse(cnt) 107 | cv2.ellipse(img_color2, ellipse, (255, 255, 0), 2) 108 | ``` 109 | 110 | ![](http://cos.codec.wang/cv2_fitting_ellipse.jpg) 111 | 112 | ### 形状匹配 113 | 114 | `cv2.matchShapes()`可以检测两个形状之间的相似度,返回**值越小,越相似**。先读入下面这张图片: 115 | 116 | ![](http://cos.codec.wang/cv2_match_shape_shapes.jpg) 117 | 118 | ```python 119 | img = cv2.imread('shapes.jpg', 0) 120 | _, thresh = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) 121 | image, contours, hierarchy = cv2.findContours(thresh, 3, 2) 122 | img_color = cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR) # 用于绘制的彩色图 123 | ``` 124 | 125 | 图中有 3 条轮廓,我们用 A/B/C 表示: 126 | 127 | ```python 128 | cnt_a, cnt_b, cnt_c = contours[0], contours[1], contours[2] 129 | print(cv2.matchShapes(cnt_b, cnt_b, 1, 0.0)) # 0.0 130 | print(cv2.matchShapes(cnt_b, cnt_c, 1, 0.0)) # 2.17e-05 131 | print(cv2.matchShapes(cnt_b, cnt_a, 1, 0.0)) # 0.418 132 | ``` 133 | 134 | 可以看到 BC 相似程度比 AB 高很多,并且图形的旋转或缩放并没有影响。其中,参数 3 是匹配方法,详情可参考:[ShapeMatchModes](https://docs.opencv.org/4.0.0/d3/dc0/group__imgproc__shape.html#gaf2b97a230b51856d09a2d934b78c015f),参数 4 是 OpenCV 的预留参数,暂时没有实现,可以不用理会。 135 | 136 | 形状匹配是通过图像的 Hu 矩来实现的\(`cv2.HuMoments()`\),大家如果感兴趣,可以参考:[Hu-Moments](http://en.wikipedia.org/wiki/Image_moment#Rotation_invariant_moments) 137 | 138 | ## 练习 139 | 140 | 1. 前面我们是对图片中的数字 3 进行轮廓特征计算的,大家换成数字 1 看看。 141 | 2. (选做)用形状匹配比较两个字母或数字(这相当于很简单的一个[OCR](https://baike.baidu.com/item/%E5%85%89%E5%AD%A6%E5%AD%97%E7%AC%A6%E8%AF%86%E5%88%AB/4162921?fr=aladdin&fromid=25995&fromtitle=OCR)噢)。 142 | 143 | ## 小结 144 | 145 | 常用的轮廓特征: 146 | 147 | - `cv2.contourArea()`算面积,`cv2.arcLength()`算周长,`cv2.boundingRect()`算外接矩。 148 | - `cv2.minAreaRect()`算最小外接矩,`cv2.minEnclosingCircle()`算最小外接圆。 149 | - `cv2.matchShapes()`进行形状匹配。 150 | 151 | ## 接口文档 152 | 153 | - [cv2.contourArea()](https://docs.opencv.org/4.0.0/d3/dc0/group__imgproc__shape.html#ga2c759ed9f497d4a618048a2f56dc97f1) 154 | - [cv2.arcLength()](https://docs.opencv.org/4.0.0/d3/dc0/group__imgproc__shape.html#ga8d26483c636be6b35c3ec6335798a47c) 155 | - [cv2.moments()](https://docs.opencv.org/4.0.0/d3/dc0/group__imgproc__shape.html#ga556a180f43cab22649c23ada36a8a139) 156 | - [cv2.boundingRect()](https://docs.opencv.org/4.0.0/d3/dc0/group__imgproc__shape.html#ga103fcbda2f540f3ef1c042d6a9b35ac7) 157 | - [cv2.minAreaRect()](https://docs.opencv.org/4.0.0/d3/dc0/group__imgproc__shape.html#ga3d476a3417130ae5154aea421ca7ead9) 158 | - [cv2.minEnclosingCircle()](https://docs.opencv.org/4.0.0/d3/dc0/group__imgproc__shape.html#ga8ce13c24081bbc7151e9326f412190f1) 159 | - [cv2.fitEllipse()](https://docs.opencv.org/4.0.0/d3/dc0/group__imgproc__shape.html#gaf259efaad93098103d6c27b9e4900ffa) 160 | - [cv2.matchShapes()](https://docs.opencv.org/4.0.0/d3/dc0/group__imgproc__shape.html#gaadc90cb16e2362c9bd6e7363e6e4c317) 161 | - [cv2.ShapeMatchModes](https://docs.opencv.org/4.0.0/d3/dc0/group__imgproc__shape.html#gaf2b97a230b51856d09a2d934b78c015f) 162 | 163 | ## 引用 164 | 165 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/14-Contour-Features) 166 | - [Contour Features](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contour_features/py_contour_features.html) 167 | - [Contours : More Functions](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contours_more_functions/py_contours_more_functions.html) 168 | -------------------------------------------------------------------------------- /static/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/basic/10-smoothing-images.md: -------------------------------------------------------------------------------- 1 | # 10: 平滑图像 2 | 3 | ![](http://cos.codec.wang/cv2_bilateral_vs_gaussian.jpg) 4 | 5 | 学习模糊/平滑图像,消除噪点。图片等可到文末引用处下载。 6 | 7 | ## 目标 8 | 9 | - 模糊/平滑图片来消除图片噪声 10 | - OpenCV 函数:`cv2.blur()`, `cv2.GaussianBlur()`, `cv2.medianBlur()`, `cv2.bilateralFilter()` 11 | 12 | ## 教程 13 | 14 | ### 滤波与模糊 15 | 16 | > 推荐大家先阅读:[番外篇:卷积基础 (图片边框)](./extra-08-padding-and-convolution/),有助于理解卷积和滤波的概念。 17 | 18 | 关于滤波和模糊,很多人分不清,我来给大家理理(虽说如此,我后面也会混着用,,ԾㅂԾ,,): 19 | 20 | - 它们都属于卷积,不同滤波方法之间只是卷积核不同(对线性滤波而言) 21 | - 低通滤波器是模糊,高通滤波器是锐化 22 | 23 | 低通滤波器就是允许低频信号通过,在图像中边缘和噪点都相当于高频部分,所以低通滤波器用于去除噪点、平滑和模糊图像。高通滤波器则反之,用来增强图像边缘,进行锐化处理。 24 | 25 | > 常见噪声有 [椒盐噪声](https://baike.baidu.com/item/%E6%A4%92%E7%9B%90%E5%99%AA%E5%A3%B0/3455958?fr=aladdin) 和 [高斯噪声](https://baike.baidu.com/item/%E9%AB%98%E6%96%AF%E5%99%AA%E5%A3%B0),椒盐噪声可以理解为斑点,随机出现在图像中的黑点或白点;高斯噪声可以理解为拍摄图片时由于光照等原因造成的噪声。 26 | 27 | ### 均值滤波 28 | 29 | 均值滤波是一种最简单的滤波处理,它取的是卷积核区域内元素的均值,用`cv2.blur()`实现,如 3×3 的卷积核: 30 | 31 | $$ 32 | kernel = \frac{1}{9}\left[ 33 | \begin{matrix} 34 | 1 & 1 & 1 \newline 35 | 1 & 1 & 1 \newline 36 | 1 & 1 & 1 37 | \end{matrix} 38 | \right] 39 | $$ 40 | 41 | ```python 42 | img = cv2.imread('lena.jpg') 43 | blur = cv2.blur(img, (3, 3)) # 均值模糊 44 | ``` 45 | 46 | > 所有的滤波函数都有一个可选参数 borderType,这个参数就是 [番外篇:卷积基础 (图片边框)](./extra-08-padding-and-convolution/) 中所说的边框填充方式。 47 | 48 | ### 方框滤波 49 | 50 | 方框滤波跟均值滤波很像,如 3×3 的滤波核如下: 51 | 52 | $$ 53 | k = a\left[ 54 | \begin{matrix} 55 | 1 & 1 & 1 \newline 56 | 1 & 1 & 1 \newline 57 | 1 & 1 & 1 58 | \end{matrix} 59 | \right] 60 | $$ 61 | 62 | 用`cv2.boxFilter()`函数实现,当可选参数 normalize 为 True 的时候,方框滤波就是均值滤波,上式中的 a 就等于 1/9;normalize 为 False 的时候,a=1,相当于求区域内的像素和。 63 | 64 | ```python 65 | # 前面的均值滤波也可以用方框滤波实现:normalize=True 66 | blur = cv2.boxFilter(img, -1, (3, 3), normalize=True) 67 | ``` 68 | 69 | ### 高斯滤波 70 | 71 | 前面两种滤波方式,卷积核内的每个值都一样,也就是说图像区域中每个像素的权重也就一样。高斯滤波的卷积核权重并不相同:中间像素点权重最高,越远离中心的像素权重越小,来,数学时间\( ╯□╰ \),还记得标准正态分布的曲线吗? 72 | 73 | ![](http://cos.codec.wang/cv2_gaussian_kernel_function_theory.jpg) 74 | 75 | 显然这种处理元素间权值的方式更加合理一些。图像是 2 维的,所以我们需要使用[2 维的高斯函数](https://en.wikipedia.org/wiki/Gaussian_filter),比如 OpenCV 中默认的 3×3 的高斯卷积核(具体原理和卷积核生成方式请参考文末的[番外小篇](#番外小篇高斯滤波卷积核)): 76 | 77 | $$ 78 | k = \left[ 79 | \begin{matrix} 80 | 0.0625 & 0.125 & 0.0625 \newline 81 | 0.125 & 0.25 & 0.125 \newline 82 | 0.0625 & 0.125 & 0.0625 83 | \end{matrix} 84 | \right] 85 | $$ 86 | 87 | OpenCV 中对应函数为`cv2.GaussianBlur(src,ksize,sigmaX)`: 88 | 89 | ```python 90 | img = cv2.imread('gaussian_noise.bmp') 91 | # 均值滤波 vs 高斯滤波 92 | blur = cv2.blur(img, (5, 5)) # 均值滤波 93 | gaussian = cv2.GaussianBlur(img, (5, 5), 1) # 高斯滤波 94 | ``` 95 | 96 | 参数 3 σx 值越大,模糊效果越明显。高斯滤波相比均值滤波效率要慢,但可以有效消除高斯噪声,能保留更多的图像细节,所以经常被称为最有用的滤波器。均值滤波与高斯滤波的对比结果如下(均值滤波丢失的细节更多): 97 | 98 | ![](http://cos.codec.wang/cv2_gaussian_vs_average.jpg) 99 | 100 | ### 中值滤波 101 | 102 | [中值](https://baike.baidu.com/item/%E4%B8%AD%E5%80%BC)又叫中位数,是所有数排序后取中间的值。中值滤波就是用区域内的中值来代替本像素值,所以那种孤立的斑点,如 0 或 255 很容易消除掉,适用于去除椒盐噪声和斑点噪声。中值是一种非线性操作,效率相比前面几种线性滤波要慢。 103 | 104 | 比如下面这张斑点噪声图,用中值滤波显然更好: 105 | 106 | ```python 107 | img = cv2.imread('salt_noise.bmp', 0) 108 | # 均值滤波 vs 中值滤波 109 | blur = cv2.blur(img, (5, 5)) # 均值滤波 110 | median = cv2.medianBlur(img, 5) # 中值滤波 111 | ``` 112 | 113 | ![](http://cos.codec.wang/cv2_median_vs_average.jpg) 114 | 115 | ### 双边滤波 116 | 117 | 模糊操作基本都会损失掉图像细节信息,尤其前面介绍的线性滤波器,图像的边缘信息很难保留下来。然而,边缘(edge)信息是图像中很重要的一个特征,所以这才有了[双边滤波](https://baike.baidu.com/item/%E5%8F%8C%E8%BE%B9%E6%BB%A4%E6%B3%A2)。用`cv2.bilateralFilter()`函数实现: 118 | 119 | ```python 120 | img = cv2.imread('lena.jpg') 121 | # 双边滤波 vs 高斯滤波 122 | gau = cv2.GaussianBlur(img, (5, 5), 0) # 高斯滤波 123 | blur = cv2.bilateralFilter(img, 9, 75, 75) # 双边滤波 124 | ``` 125 | 126 | ![](http://cos.codec.wang/cv2_bilateral_vs_gaussian.jpg) 127 | 128 | 可以看到,双边滤波明显保留了更多边缘信息。 129 | 130 | ## 番外小篇:高斯滤波卷积核 131 | 132 | 要解释高斯滤波卷积核是如何生成的,需要先复习下概率论的知识(What??又是数学\( ╯□╰ \)) 133 | 134 | 一维的高斯函数/正态分布$X\sim N(\mu, \sigma^2)$: 135 | 136 | $$ 137 | G(x)=\frac{1}{\sqrt{2\pi}\sigma}exp(-\frac{(x-\mu)^2}{2\sigma^2}) 138 | $$ 139 | 140 | 当$\mu=0, \sigma^2=1$时,称为标准正态分布$X\sim N(0, 1)$: 141 | 142 | $$ 143 | G(x)=\frac{1}{\sqrt{2\pi}}exp(-\frac{x^2}{2}) 144 | $$ 145 | 146 | 二维 X/Y 相互独立的高斯函数: 147 | 148 | $$ 149 | G(x,y)=\frac{1}{2\pi\sigma_x\sigma_y}exp(-\frac{(x-\mu_x)^2+(y-\mu_y)^2}{2\sigma_x\sigma_y})=G(x)G(y) 150 | $$ 151 | 152 | 由上可知,**二维高斯函数具有可分离性**,所以 OpenCV 分两步计算二维高斯卷积,先水平再垂直,每个方向上都是一维的卷积。OpenCV 中这个一维卷积的计算公式类似于上面的一维高斯函数: 153 | 154 | $$ 155 | G(i)=\alpha *exp(-\frac{(i-\frac{ksize-1}{2})^2}{2\sigma^2}) 156 | $$ 157 | 158 | 其中 i=0…ksize-1,α 是一个常数,也称为缩放因子,它使得$(\sum{G(i)}=1)$ 159 | 160 | 比如我们可以用[`cv2.getGaussianKernel(ksize,sigma)`](https://docs.opencv.org/3.3.1/d4/d86/group__imgproc__filter.html#gac05a120c1ae92a6060dd0db190a61afa)来生成一维卷积核: 161 | 162 | - sigma<=0 时,`sigma=0.3*((ksize-1)*0.5 - 1) + 0.8` 163 | - sigma>0 时,sigma=sigma 164 | 165 | ```python 166 | print(cv2.getGaussianKernel(3, 0)) 167 | # 结果:[[0.25][0.5][0.25]] 168 | ``` 169 | 170 | 生成之后,先进行三次的水平卷积: 171 | 172 | $$ 173 | I×\left[ 174 | \begin{matrix} 175 | 0.25 & 0.5 & 0.25 \newline 176 | 0.25 & 0.5 & 0.25 \newline 177 | 0.25 & 0.5 & 0.25 178 | \end{matrix} 179 | \right] 180 | $$ 181 | 182 | 然后再进行垂直的三次卷积: 183 | 184 | $$ 185 | I×\left[ 186 | \begin{matrix} 187 | 0.25 & 0.5 & 0.25 \newline 188 | 0.25 & 0.5 & 0.25 \newline 189 | 0.25 & 0.5 & 0.25 190 | \end{matrix} 191 | \right]×\left[ 192 | \begin{matrix} 193 | 0.25 & 0.25 & 0.25 \newline 194 | 0.5 & 0.5 & 0.5 \newline 195 | 0.25 & 0.25 & 0.25 196 | \end{matrix} 197 | \right] =I×\left[ 198 | \begin{matrix} 199 | 0.0625 & 0.125 & 0.0625 \newline 200 | 0.125 & 0.25 & 0.125 \newline 201 | 0.0625 & 0.125 & 0.0625 202 | \end{matrix} 203 | \right] 204 | $$ 205 | 206 | 这就是 OpenCV 中高斯卷积核的生成方式。其实,OpenCV 源码中对小于 7×7 的核是直接计算好放在数组里面的,这样计算速度会快一点,感兴趣的可以看下源码:[getGaussianKernel()](https://github.com/ex2tron/OpenCV-Python-Tutorial/blob/master/10.%20%E5%B9%B3%E6%BB%91%E5%9B%BE%E5%83%8F/cv2_source_code_getGaussianKernel.cpp) 207 | 208 | 上面矩阵也可以写成: 209 | 210 | $$ 211 | \frac{1}{16}\left[ 212 | \begin{matrix} 213 | 1& 2 & 1 \newline 214 | 2 & 4 & 2 \newline 215 | 1 & 2 & 1 216 | \end{matrix} 217 | \right] 218 | $$ 219 | 220 | ## 小结 221 | 222 | - 在不知道用什么滤波器好的时候,优先高斯滤波`cv2.GaussianBlur()`,然后均值滤波`cv2.blur()`。 223 | - 斑点和椒盐噪声优先使用中值滤波`cv2.medianBlur()`。 224 | - 要去除噪点的同时尽可能保留更多的边缘信息,使用双边滤波`cv2.bilateralFilter()`。 225 | - 线性滤波方式:均值滤波、方框滤波、高斯滤波(速度相对快)。 226 | - 非线性滤波方式:中值滤波、双边滤波(速度相对慢)。 227 | 228 | ## 接口文档 229 | 230 | - [cv2.blur()](https://docs.opencv.org/4.0.0/d4/d86/group__imgproc__filter.html#ga8c45db9afe636703801b0b2e440fce37) 231 | - [cv2.boxFilter()](https://docs.opencv.org/4.0.0/d4/d86/group__imgproc__filter.html#gad533230ebf2d42509547d514f7d3fbc3) 232 | - [cv2.GaussianBlur()](https://docs.opencv.org/4.0.0/d4/d86/group__imgproc__filter.html#gaabe8c836e97159a9193fb0b11ac52cf1) 233 | - [cv2.getGaussianKernel()](https://docs.opencv.org/4.0.0/d4/d86/group__imgproc__filter.html#gac05a120c1ae92a6060dd0db190a61afa) 234 | - [cv2.medianBlur()](https://docs.opencv.org/4.0.0/d4/d86/group__imgproc__filter.html#ga564869aa33e58769b4469101aac458f9) 235 | - [cv2.bilateralFilter()](https://docs.opencv.org/4.0.0/d4/d86/group__imgproc__filter.html#ga9d7064d478c95d60003cf839430737ed) 236 | 237 | ## 引用 238 | 239 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/10-Smoothing-Images) 240 | - [Smoothing Images](http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_filtering/py_filtering.html) 241 | - [图像平滑处理](http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/imgproc/gausian_median_blur_bilateral_filter/gausian_median_blur_bilateral_filter.html) 242 | -------------------------------------------------------------------------------- /docs/basic/challenge-03-lane-road-detection.md: -------------------------------------------------------------------------------- 1 | # 挑战任务:车道检测 2 | 3 | ![](http://cos.codec.wang/cv2_lane_detection_result_sample.jpg) 4 | 5 | 挑战任务:实际公路的车道线检测。图片等可到文末引用处下载。 6 | 7 | ## 挑战内容 8 | 9 | > **1. 在所提供的公路图片上检测出车道线并标记:** 10 | 11 | ![](http://cos.codec.wang/cv2_lane_detection_result_sample.jpg) 12 | 13 | > **2. 在所提供的公路视频上检测出车道线并标记:** 14 | 15 | 本次挑战内容来自 Udacity 自动驾驶纳米学位课程,素材中车道保持不变,车道线清晰明确,易于检测,是车道检测的基础版本,网上也有很多针对复杂场景的高级实现,感兴趣的童鞋可以自行了解。 16 | 17 | **挑战题不会做也木有关系,但请务必在自行尝试后,再看下面的解答噢,**不然...我也没办法\( ̄ ▽  ̄\)" 18 | 19 | ## 挑战解答 20 | 21 | ### 方案 22 | 23 | 要检测出当前车道,就是要检测出左右两条车道直线。由于无人车一直保持在当前车道,那么无人车上的相机拍摄的视频中,车道线的位置应该基本固定在某一个范围内: 24 | 25 | ![](http://cos.codec.wang/cv2_lane_detection_roi_sample.jpg) 26 | 27 | 如果我们手动把这部分 ROI 区域抠出来,就会排除掉大部分干扰。接下来检测直线肯定是用霍夫变换,但 ROI 区域内的边缘直线信息还是很多,考虑到只有左右两条车道线,一条斜率为正,一条为负,可将所有的线分为两组,每组再通过均值或最小二乘法拟合的方式确定唯一一条线就可以完成检测。总体步骤如下: 28 | 29 | 1. 灰度化 30 | 2. 高斯模糊 31 | 3. Canny 边缘检测 32 | 4. 不规则 ROI 区域截取 33 | 5. 霍夫直线检测 34 | 6. 车道计算 35 | 36 | 对于视频来说,只要一幅图能检查出来,合成下就可以了,问题不大。 37 | 38 | ### 图像预处理 39 | 40 | 灰度化和滤波操作是大部分图像处理的必要步骤。灰度化不必多说,因为不是基于色彩信息识别的任务,所以没有必要用彩色图,可以大大减少计算量。而滤波会削弱图像噪点,排除干扰信息。另外,根据前面学习的知识,边缘提取是基于图像梯度的,梯度对噪声很敏感,所以平滑滤波操作必不可少。 41 | 42 | ![原图 vs 灰度滤波图](http://cos.codec.wang/cv2_lane_detection_gray_blur_result.jpg) 43 | 44 | 这次的代码我们分模块来写,规范一点。其中`process_an_image()`是主要的图像处理流程: 45 | 46 | ```python 47 | import cv2 48 | import numpy as np 49 | 50 | # 高斯滤波核大小 51 | blur_ksize = 5 52 | # Canny 边缘检测高低阈值 53 | canny_lth = 50 54 | canny_hth = 150 55 | 56 | def process_an_image(img): 57 | # 1. 灰度化、滤波和 Canny 58 | gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) 59 | blur_gray = cv2.GaussianBlur(gray, (blur_ksize, blur_ksize), 1) 60 | edges = cv2.Canny(blur_gray, canny_lth, canny_hth) 61 | 62 | if __name__ == "__main__": 63 | img = cv2.imread('test_pictures/lane.jpg') 64 | result = process_an_image(img) 65 | cv2.imshow("lane", np.hstack((img, result))) 66 | cv2.waitKey(0) 67 | ``` 68 | 69 | ![边缘检测结果图](http://cos.codec.wang/cv2_lane_detection_canny_result.jpg) 70 | 71 | ### ROI 截取 72 | 73 | 按照前面描述的方案,只需保留边缘图中的红线部分区域用于后续的霍夫直线检测,其余都是无用的信息: 74 | 75 | ![](http://cos.codec.wang/cv2_lane_detection_canny_roi_reserve.jpg) 76 | 77 | 如何实现呢?还记得图像混合中的这张图吗? 78 | 79 | ![](http://cos.codec.wang/cv2_understand_mask.jpg) 80 | 81 | 我们可以创建一个梯形的 mask 掩膜,然后与边缘检测结果图混合运算,掩膜中白色的部分保留,黑色的部分舍弃。梯形的四个坐标需要手动标记: 82 | 83 | ![掩膜 mask](http://cos.codec.wang/cv2_lane_detection_mask_sample.jpg) 84 | 85 | ```python 86 | def process_an_image(img): 87 | # 1. 灰度化、滤波和 Canny 88 | 89 | # 2. 标记四个坐标点用于 ROI 截取 90 | rows, cols = edges.shape 91 | points = np.array([[(0, rows), (460, 325), (520, 325), (cols, rows)]]) 92 | # [[[0 540], [460 325], [520 325], [960 540]]] 93 | roi_edges = roi_mask(edges, points) 94 | 95 | def roi_mask(img, corner_points): 96 | # 创建掩膜 97 | mask = np.zeros_like(img) 98 | cv2.fillPoly(mask, corner_points, 255) 99 | 100 | masked_img = cv2.bitwise_and(img, mask) 101 | return masked_img 102 | ``` 103 | 104 | 这样,结果图"roi_edges"应该是: 105 | 106 | ![只保留关键区域的边缘检测图](http://cos.codec.wang/cv2_lane_detection_masked_roi_edges.jpg) 107 | 108 | ### 霍夫直线提取 109 | 110 | 为了方便后续计算直线的斜率,我们使用统计概率霍夫直线变换(因为它能直接得到直线的起点和终点坐标)。霍夫变换的参数比较多,可以放在代码开头,便于修改: 111 | 112 | ```python 113 | # 霍夫变换参数 114 | rho = 1 115 | theta = np.pi / 180 116 | threshold = 15 117 | min_line_len = 40 118 | max_line_gap = 20 119 | 120 | def process_an_image(img): 121 | # 1. 灰度化、滤波和 Canny 122 | 123 | # 2. 标记四个坐标点用于 ROI 截取 124 | 125 | # 3. 霍夫直线提取 126 | drawing, lines = hough_lines(roi_edges, rho, theta, threshold, min_line_len, max_line_gap) 127 | 128 | def hough_lines(img, rho, theta, threshold, min_line_len, max_line_gap): 129 | # 统计概率霍夫直线变换 130 | lines = cv2.HoughLinesP(img, rho, theta, threshold, minLineLength=min_line_len, maxLineGap=max_line_gap) 131 | 132 | # 新建一副空白画布 133 | drawing = np.zeros((img.shape[0], img.shape[1], 3), dtype=np.uint8) 134 | # draw_lines(drawing, lines) # 画出直线检测结果 135 | 136 | return drawing, lines 137 | 138 | def draw_lines(img, lines, color=[0, 0, 255], thickness=1): 139 | for line in lines: 140 | for x1, y1, x2, y2 in line: 141 | cv2.line(img, (x1, y1), (x2, y2), color, thickness) 142 | ``` 143 | 144 | `draw_lines()`是用来画直线检测的结果,后面我们会接着处理直线,所以这里注释掉了,可以取消注释看下效果: 145 | 146 | ![霍夫变换结果图](http://cos.codec.wang/cv2_lane_detection_hough_lines_direct_result.jpg) 147 | 148 | 对本例的这张测试图来说,如果打印出直线的条数`print(len(lines))`,应该是有 16 条。 149 | 150 | ### 车道计算 151 | 152 | 这部分应该算是本次挑战任务的核心内容了:前面通过霍夫变换得到了多条直线的起点和终点,我们的目的是通过某种算法只得到左右两条车道线。 153 | 154 | **第一步、根据斜率正负划分某条线是左车道还是右车道。** 155 | 156 | $$ 157 | 斜率=\frac{y_2-y_1}{x_2-x_1}(\leq0:左,>0:右) 158 | $$ 159 | 160 | :::tip 161 | 再次强调,斜率计算是在图像坐标系下,所以斜率正负/左右跟平面坐标有区别。 162 | ::: 163 | 164 | **第二步、迭代计算各直线斜率与斜率均值的差,排除掉差值过大的异常数据。** 165 | 166 | 注意这里迭代的含义,意思是第一次计算完斜率均值并排除掉异常值后,再在剩余的斜率中取均值,继续排除……这样迭代下去。 167 | 168 | **第三步、最小二乘法拟合左右车道线。** 169 | 170 | 经过第二步的筛选,就只剩下可能的左右车道线了,这样只需从多条直线中拟合出一条就行。拟合方法有很多种,最常用的便是最小二乘法,它通过最小化误差的平方和来寻找数据的最佳匹配函数。 171 | 172 | 具体来说,假设目前可能的左车道线有 6 条,也就是 12 个坐标点,包括 12 个 x 和 12 个 y,我们的目的是拟合出这样一条直线: 173 | 174 | $$ 175 | f(x_i) = ax_i+b 176 | $$ 177 | 178 | 使得误差平方和最小: 179 | 180 | $$ 181 | E=\sum(f(x_i)-y_i)^2 182 | $$ 183 | 184 | Python 中可以直接使用`np.polyfit()`进行最小二乘法拟合。 185 | 186 | ```python 187 | def process_an_image(img): 188 | # 1. 灰度化、滤波和 Canny 189 | 190 | # 2. 标记四个坐标点用于 ROI 截取 191 | 192 | # 3. 霍夫直线提取 193 | 194 | # 4. 车道拟合计算 195 | draw_lanes(drawing, lines) 196 | 197 | # 5. 最终将结果合在原图上 198 | result = cv2.addWeighted(img, 0.9, drawing, 0.2, 0) 199 | 200 | return result 201 | 202 | def draw_lanes(img, lines, color=[255, 0, 0], thickness=8): 203 | # a. 划分左右车道 204 | left_lines, right_lines = [], [] 205 | for line in lines: 206 | for x1, y1, x2, y2 in line: 207 | k = (y2 - y1) / (x2 - x1) 208 | if k < 0: 209 | left_lines.append(line) 210 | else: 211 | right_lines.append(line) 212 | 213 | if (len(left_lines) <= 0 or len(right_lines) <= 0): 214 | return 215 | 216 | # b. 清理异常数据 217 | clean_lines(left_lines, 0.1) 218 | clean_lines(right_lines, 0.1) 219 | 220 | # c. 得到左右车道线点的集合,拟合直线 221 | left_points = [(x1, y1) for line in left_lines for x1, y1, x2, y2 in line] 222 | left_points = left_points + [(x2, y2) for line in left_lines for x1, y1, x2, y2 in line] 223 | right_points = [(x1, y1) for line in right_lines for x1, y1, x2, y2 in line] 224 | right_points = right_points + [(x2, y2) for line in right_lines for x1, y1, x2, y2 in line] 225 | 226 | left_results = least_squares_fit(left_points, 325, img.shape[0]) 227 | right_results = least_squares_fit(right_points, 325, img.shape[0]) 228 | 229 | # 注意这里点的顺序 230 | vtxs = np.array([[left_results[1], left_results[0], right_results[0], right_results[1]]]) 231 | # d. 填充车道区域 232 | cv2.fillPoly(img, vtxs, (0, 255, 0)) 233 | 234 | # 或者只画车道线 235 | # cv2.line(img, left_results[0], left_results[1], (0, 255, 0), thickness) 236 | # cv2.line(img, right_results[0], right_results[1], (0, 255, 0), thickness) 237 | 238 | def clean_lines(lines, threshold): 239 | # 迭代计算斜率均值,排除掉与差值差异较大的数据 240 | slope = [(y2 - y1) / (x2 - x1) for line in lines for x1, y1, x2, y2 in line] 241 | while len(lines) > 0: 242 | mean = np.mean(slope) 243 | diff = [abs(s - mean) for s in slope] 244 | idx = np.argmax(diff) 245 | if diff[idx] > threshold: 246 | slope.pop(idx) 247 | lines.pop(idx) 248 | else: 249 | break 250 | 251 | def least_squares_fit(point_list, ymin, ymax): 252 | # 最小二乘法拟合 253 | x = [p[0] for p in point_list] 254 | y = [p[1] for p in point_list] 255 | 256 | # polyfit 第三个参数为拟合多项式的阶数,所以 1 代表线性 257 | fit = np.polyfit(y, x, 1) 258 | fit_fn = np.poly1d(fit) # 获取拟合的结果 259 | 260 | xmin = int(fit_fn(ymin)) 261 | xmax = int(fit_fn(ymax)) 262 | 263 | return [(xmin, ymin), (xmax, ymax)] 264 | ``` 265 | 266 | 这段代码比较多,请每个步骤单独来看。最后得到的是左右两条车道线的起点和终点坐标,可以选择画出车道线,这里我直接填充了整个区域: 267 | 268 | ![](http://cos.codec.wang/cv2_lane_detection_result_sample.jpg) 269 | 270 | ### 视频处理 271 | 272 | 搞定了一张图,视频也就没什么问题了,关键就是视频帧的提取和合成,为此,我们要用到 Python 的视频编辑包[moviepy](https://pypi.org/project/moviepy/#files): 273 | 274 | ```python 275 | pip install moviepy 276 | ``` 277 | 278 | 另外还需要 ffmpeg,首次运行 moviepy 时会自动下载,也可[手动](https://github.com/imageio/imageio-binaries/tree/master/ffmpeg)下载。 279 | 280 | 只需在开头导入 moviepy,然后将主函数改掉就可以了,其余代码不需要更改: 281 | 282 | ```python 283 | # 开头导入 moviepy 284 | from moviepy.editor import VideoFileClip 285 | 286 | # 主函数更改为: 287 | if __name__ == "__main__": 288 | output = 'test_videos/output.mp4' 289 | clip = VideoFileClip("test_videos/cv2_white_lane.mp4") 290 | out_clip = clip.fl_image(process_an_image) 291 | out_clip.write_videofile(output, audio=False) 292 | ``` 293 | 294 | 本文实现了车道检测的基础版本,如果你感兴趣的话,可以自行搜索或参考引用部分了解更多。 295 | 296 | ## 引用 297 | 298 | - [图片和视频素材](http://cos.codec.wang/cv2_lane_detection_material.zip) 299 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/Challenge-03-Lane-Road-Detection) 300 | - [从零开始学习无人驾驶技术 --- 车道检测](https://zhuanlan.zhihu.com/p/25354571) 301 | - [无人驾驶之高级车道线检测](https://blog.csdn.net/u010665216/article/details/80152458) 302 | -------------------------------------------------------------------------------- /docs/start/extra-05-warpaffine-warpperspective.md: -------------------------------------------------------------------------------- 1 | # 番外篇:仿射变换与透视变换 2 | 3 | ![](http://cos.codec.wang/cv2_image_transformation_sample.jpg) 4 | 5 | 常见的 2D 图像变换从原理上讲主要包括基于 2×3 矩阵的[仿射变换](https://baike.baidu.com/item/%E4%BB%BF%E5%B0%84%E5%8F%98%E6%8D%A2)和基于 3×3 矩阵[透视变换](https://baike.baidu.com/item/%E9%80%8F%E8%A7%86%E5%8F%98%E6%8D%A2)。 6 | 7 | ## 仿射变换 8 | 9 | 基本的图像变换就是二维坐标的变换:从一种二维坐标\(x,y\) 到另一种二维坐标\(u,v\) 的线性变换: 10 | 11 | $$ 12 | \begin{matrix} 13 | u=a_1x+b_1y+c_1 \newline 14 | v=a_2x+b_2y+c_2 15 | \end{matrix} 16 | $$ 17 | 18 | 如果写成矩阵的形式,就是: 19 | 20 | $$ 21 | \left[ 22 | \begin{matrix} 23 | u \newline 24 | v 25 | \end{matrix} 26 | \right] = \left[ 27 | \begin{matrix} 28 | a_1 & b_1 \newline 29 | a_2 & b_2 30 | \end{matrix} 31 | \right] \left[ 32 | \begin{matrix} 33 | x \newline 34 | y 35 | \end{matrix} 36 | \right]+\left[ 37 | \begin{matrix} 38 | c_1 \newline 39 | c_2 40 | \end{matrix} 41 | \right] 42 | $$ 43 | 44 | 作如下定义: 45 | 46 | $$ 47 | R=\left[ 48 | \begin{matrix} 49 | a_1 & b_1 \newline 50 | a_2 & b_2 51 | \end{matrix} 52 | \right], t=\left[ 53 | \begin{matrix} 54 | c_1 \newline 55 | c_2 56 | \end{matrix} 57 | \right],T=\left[ 58 | \begin{matrix} 59 | R & t 60 | \end{matrix} 61 | \right] 62 | $$ 63 | 64 | 矩阵 T\(2×3\) 就称为仿射变换的变换矩阵,R 为线性变换矩阵,t 为平移矩阵,简单来说,仿射变换就是线性变换 + 平移。变换后直线依然是直线,平行线依然是平行线,直线间的相对位置关系不变,因此**非共线的三个对应点便可确定唯一的一个仿射变换**,线性变换 4 个自由度 + 平移 2 个自由度 →**仿射变换自由度为 6**。 65 | 66 | ![](http://cos.codec.wang/cv2_warp_affine_image_sample_introduction2.jpg) 67 | 68 | 来看下 OpenCV 中如何实现仿射变换: 69 | 70 | ```python 71 | import cv2 72 | import numpy as np 73 | import matplotlib.pyplot as plt 74 | 75 | img = cv2.imread('drawing.jpg') 76 | rows, cols = img.shape[:2] 77 | 78 | # 变换前的三个点 79 | pts1 = np.float32([[50, 65], [150, 65], [210, 210]]) 80 | # 变换后的三个点 81 | pts2 = np.float32([[50, 100], [150, 65], [100, 250]]) 82 | 83 | # 生成变换矩阵 84 | M = cv2.getAffineTransform(pts1, pts2) 85 | dst = cv2.warpAffine(img, M, (cols, rows)) 86 | 87 | plt.subplot(121), plt.imshow(img), plt.title('input') 88 | plt.subplot(122), plt.imshow(dst), plt.title('output') 89 | plt.show() 90 | ``` 91 | 92 | 三个点我已经在图中标记了出来。用`cv2.getAffineTransform()`生成变换矩阵,接下来再用`cv2.warpAffine()`实现变换。 93 | 94 | > 思考:三个点我标记的是红色,为什么 Matplotlib 显示出来是下面这种颜色? 95 | 96 | ![仿射变换前后对比图](http://cos.codec.wang/cv2_affine_transformation_drawing.jpg) 97 | 98 | 其实平移、旋转、缩放和翻转等变换就是对应了不同的仿射变换矩阵,下面分别来看下。 99 | 100 | ![](http://cos.codec.wang/cv2_image_transformation_sample.jpg) 101 | 102 | ### 平移 103 | 104 | ![](http://cos.codec.wang/cv2_warp_affine_shift_sample.jpg) 105 | 106 | 平移就是 x 和 y 方向上的直接移动,可以上下/左右移动,自由度为 2,变换矩阵可以表示为: 107 | 108 | $$ 109 | \left[ 110 | \begin{matrix} 111 | u \newline 112 | v 113 | \end{matrix} 114 | \right] = \left[ 115 | \begin{matrix} 116 | 1 & 0 \newline 117 | 0 & 1 118 | \end{matrix} 119 | \right] \left[ 120 | \begin{matrix} 121 | x \newline 122 | y 123 | \end{matrix} 124 | \right]+\left[ 125 | \begin{matrix} 126 | t_x \newline 127 | t_y 128 | \end{matrix} 129 | \right] 130 | $$ 131 | 132 | ### 旋转 133 | 134 | ![](http://cos.codec.wang/cv2_warp_affine_rotation_sample.jpg) 135 | 136 | 旋转是坐标轴方向饶原点旋转一定的角度 θ,自由度为 1,不包含平移,如顺时针旋转可以表示为: 137 | 138 | $$ 139 | \left[ 140 | \begin{matrix} 141 | u \newline 142 | v 143 | \end{matrix} 144 | \right] = \left[ 145 | \begin{matrix} 146 | \cos \theta & -\sin \theta \newline 147 | \sin \theta & \cos \theta 148 | \end{matrix} 149 | \right] \left[ 150 | \begin{matrix} 151 | x \newline 152 | y 153 | \end{matrix} 154 | \right]+\left[ 155 | \begin{matrix} 156 | 0 \newline 157 | 0 158 | \end{matrix} 159 | \right] 160 | $$ 161 | 162 | > 思考:如果不是绕原点,而是可变点,自由度是多少呢?(请看下文刚体变换) 163 | 164 | ### 翻转 165 | 166 | 翻转是 x 或 y 某个方向或全部方向上取反,自由度为 2,比如这里以垂直翻转为例: 167 | 168 | $$ 169 | \left[ 170 | \begin{matrix} 171 | u \newline 172 | v 173 | \end{matrix} 174 | \right] = \left[ 175 | \begin{matrix} 176 | 1 & 0 \newline 177 | 0 & -1 178 | \end{matrix} 179 | \right] \left[ 180 | \begin{matrix} 181 | x \newline 182 | y 183 | \end{matrix} 184 | \right]+\left[ 185 | \begin{matrix} 186 | 0 \newline 187 | 0 188 | \end{matrix} 189 | \right] 190 | $$ 191 | 192 | ### 刚体变换 193 | 194 | 旋转 + 平移也称刚体变换(Rigid Transform),就是说如果**图像变换前后两点间的距离仍然保持不变**,那么这种变化就称为刚体变换。刚体变换包括了平移、旋转和翻转,自由度为 3。变换矩阵可以表示为: 195 | 196 | $$ 197 | \left[ 198 | \begin{matrix} 199 | u \newline 200 | v 201 | \end{matrix} 202 | \right] = \left[ 203 | \begin{matrix} 204 | \cos \theta & -\sin \theta \newline 205 | \sin \theta & \cos \theta 206 | \end{matrix} 207 | \right] \left[ 208 | \begin{matrix} 209 | x \newline 210 | y 211 | \end{matrix} 212 | \right]+\left[ 213 | \begin{matrix} 214 | t_x \newline 215 | t_y 216 | \end{matrix} 217 | \right] 218 | $$ 219 | 220 | 由于只是旋转和平移,刚体变换保持了直线间的长度不变,所以也称欧式变换(变化前后保持欧氏距离)。 221 | 222 | ### 缩放 223 | 224 | ![](http://cos.codec.wang/cv2_warp_affine_scale_sampel.jpg) 225 | 226 | 缩放是 x 和 y 方向的尺度(倍数)变换,在有些资料上非等比例的缩放也称为拉伸/挤压,等比例缩放自由度为 1,非等比例缩放自由度为 2,矩阵可以表示为: 227 | 228 | $$ 229 | \left[ 230 | \begin{matrix} 231 | u \newline 232 | v 233 | \end{matrix} 234 | \right] = \left[ 235 | \begin{matrix} 236 | s_x & 0 \newline 237 | 0 & s_y 238 | \end{matrix} 239 | \right] \left[ 240 | \begin{matrix} 241 | x \newline 242 | y 243 | \end{matrix} 244 | \right]+\left[ 245 | \begin{matrix} 246 | 0 \newline 247 | 0 248 | \end{matrix} 249 | \right] 250 | $$ 251 | 252 | ### 相似变换 253 | 254 | 相似变换又称缩放旋转,相似变换包含了旋转、等比例缩放和平移等变换,自由度为 4。在 OpenCV 中,旋转就是用相似变换实现的: 255 | 256 | 若缩放比例为 scale,旋转角度为 θ,旋转中心是$(center_x,center_y)$,则仿射变换可以表示为: 257 | 258 | $$ 259 | \left[ 260 | \begin{matrix} 261 | u \newline 262 | v 263 | \end{matrix} 264 | \right] = \left[ 265 | \begin{matrix} 266 | \alpha & \beta \newline 267 | -\beta & \alpha 268 | \end{matrix} 269 | \right] \left[ 270 | \begin{matrix} 271 | x \newline 272 | y 273 | \end{matrix} 274 | \right]+\left[ 275 | \begin{matrix} 276 | (1-\alpha)center_x-\beta center_y \newline 277 | \beta center_x+(1-\alpha)center_y 278 | \end{matrix} 279 | \right] 280 | $$ 281 | 282 | 其中, 283 | 284 | $$ 285 | \alpha=scale \cdot \cos \theta,\beta=scale \cdot \sin \theta 286 | $$ 287 | 288 | **相似变换相比刚体变换加了缩放,所以并不会保持欧氏距离不变,但直线间的夹角依然不变。** 289 | 290 | :::tip 291 | OpenCV 中默认按照逆时针旋转噢~ 292 | ::: 293 | 294 | 总结一下([原图\[\#计算机视觉:算法与应用 p39\]](http://cos.codec.wang/cv2_transformation_matrix_dof_summary.jpg)): 295 | 296 | | 变换 | 矩阵 | 自由度 | 保持性质 | 297 | | :--: | :--------------: | :----: | :--------------------------: | 298 | | 平移 | \[I, t\](2×3) | 2 | 方向/长度/夹角/平行性/直线性 | 299 | | 刚体 | \[R, t\](2×3) | 3 | 长度/夹角/平行性/直线性 | 300 | | 相似 | \[sR, t\](2×3) | 4 | 夹角/平行性/直线性 | 301 | | 仿射 | \[T\](2×3) | 6 | 平行性/直线性 | 302 | | 透视 | \[T\](3×3) | 8 | 直线性 | 303 | 304 | ## 透视变换 305 | 306 | 前面仿射变换后依然是平行四边形,并不能做到任意的变换。 307 | 308 | ![](http://cos.codec.wang/cv2_warp_perspective_image_sample4.jpg) 309 | 310 | [透视变换](https://baike.baidu.com/item/%E9%80%8F%E8%A7%86%E5%8F%98%E6%8D%A2)(Perspective Transformation)是将二维的图片投影到一个三维视平面上,然后再转换到二维坐标下,所以也称为投影映射(Projective Mapping)。简单来说就是二维 → 三维 → 二维的一个过程。 311 | 312 | $$ 313 | \begin{matrix} 314 | X=a_1 x + b_1 y + c_1 \newline 315 | Y=a_2 x + b_2 y + c_2 \newline 316 | Z=a_3 x + b_3 y + c_3 317 | \end{matrix} 318 | $$ 319 | 320 | 这次我写成齐次矩阵的形式: 321 | 322 | $$ 323 | \left[ 324 | \begin{matrix} 325 | X \newline 326 | Y \newline 327 | Z 328 | \end{matrix} 329 | \right] = \left[ 330 | \begin{matrix} 331 | a_1 & b_1 & c_1 \newline 332 | a_2 & b_2 & c_2 \newline 333 | a_3 & b_3 & c_3 334 | \end{matrix} 335 | \right] \left[ 336 | \begin{matrix} 337 | x \newline 338 | y \newline 339 | 1 340 | \end{matrix} 341 | \right] 342 | $$ 343 | 344 | 其中,$\left[ \begin{matrix} a_1 & b_1 \newline a_2 & b_2 \newline \end{matrix} \right]$表示线性变换,$\left[ \begin{matrix} a_3 & b_3 \end{matrix} \right]$产生透视变换,其余表示平移变换,因此仿射变换是透视变换的子集。接下来再通过除以 Z 轴转换成二维坐标: 345 | 346 | $$ 347 | x^{’}=\frac{X}{Z}=\frac{a_1x+b_1y+c_1}{a_3x+b_3y+c_3 } 348 | $$ 349 | 350 | $$ 351 | y^{’}=\frac{Y}{Z}=\frac{a_2x+b_2y+c_2}{a_3x+b_3y+c_3 } 352 | $$ 353 | 354 | 透视变换相比仿射变换更加灵活,变换后会产生一个新的四边形,但不一定是平行四边形,所以需要**非共线的四个点才能唯一确定**,原图中的直线变换后依然是直线。因为四边形包括了所有的平行四边形,所以透视变换包括了所有的仿射变换。 355 | 356 | OpenCV 中首先根据变换前后的四个点用`cv2.getPerspectiveTransform()`生成 3×3 的变换矩阵,然后再用`cv2.warpPerspective()`进行透视变换。实战演练一下: 357 | 358 | ![矫正一鸣的卡片](http://cos.codec.wang/cv2_perspective_transformations_inm.jpg) 359 | 360 | ```python 361 | img = cv2.imread('card.jpg') 362 | 363 | # 原图中卡片的四个角点 364 | pts1 = np.float32([[148, 80], [437, 114], [94, 247], [423, 288]]) 365 | # 变换后分别在左上、右上、左下、右下四个点 366 | pts2 = np.float32([[0, 0], [320, 0], [0, 178], [320, 178]]) 367 | 368 | # 生成透视变换矩阵 369 | M = cv2.getPerspectiveTransform(pts1, pts2) 370 | # 进行透视变换,参数 3 是目标图像大小 371 | dst = cv2.warpPerspective(img, M, (320, 178)) 372 | 373 | plt.subplot(121), plt.imshow(img[:, :, ::-1]), plt.title('input') 374 | plt.subplot(122), plt.imshow(dst[:, :, ::-1]), plt.title('output') 375 | plt.show() 376 | ``` 377 | 378 | > 代码中有个`img[:, :, ::-1]`还记得吗?忘记的话,请看 [练习](#练习)。 379 | 380 | 当然,我们后面学习了特征提取之后,就可以自动识别角点了。透视变换是一项很酷的功能。比如我们经常会用手机去拍身份证和文件,无论你怎么拍,貌似都拍不正或者有边框。如果你使用过手机上面一些扫描类软件,比如"[扫描全能王](https://baike.baidu.com/item/%E6%89%AB%E6%8F%8F%E5%85%A8%E8%83%BD%E7%8E%8B)","[Office Lens](https://baike.baidu.com/item/Office%20Lens)",它们能很好地矫正图片,这些软件就是应用透视变换实现的。 381 | 382 | ## 练习 383 | 384 | 1. 请复习:[无损保存和 Matplotlib 使用](./extra-02-high-quality-save-and-matplotlib.md)。 385 | 386 | ## 引用 387 | 388 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/Extra-05-Warpaffine-Warpperspective) 389 | - [计算机视觉:算法与应用](http://cos.codec.wang/Computer%20Vision%EF%BC%9AAlgorithms%20and%20Applications.pdf) 390 | - [维基百科:仿射变换](https://zh.wikipedia.org/wiki/%E4%BB%BF%E5%B0%84%E5%8F%98%E6%8D%A2) 391 | - [如何通俗地讲解「仿射变换」这个概念?](https://www.zhihu.com/question/20666664) 392 | -------------------------------------------------------------------------------- /docs/start/challenge-02-create-gui-with-pyqt5.md: -------------------------------------------------------------------------------- 1 | # 挑战任务:PyQt5 编写 GUI 界面 2 | 3 | ![](http://cos.codec.wang/cv2_pyqt_gui_sample.jpg) 4 | 5 | 拓展挑战:编写 GUI 图像处理应用程序。 6 | 7 | ## 挑战内容 8 | 9 | 前面我们学习的 OpenCV 内容都是运行在命令行中的,没有界面,所以本次的拓展挑战内容便是: 10 | 11 | > **了解 Python 编写**[**GUI**](https://baike.baidu.com/item/GUI)**界面的方法,使用 PyQt5 编写如下的图像处理应用程序,实现打开摄像头、捕获图片、读取本地图片、灰度化和 Otsu 自动阈值分割的功能。** 12 | 13 | ![](http://cos.codec.wang/cv2_pyqt_gui_sample.jpg) 14 | 15 | **挑战题不会做也木有关系,但请务必在自行尝试后,再看下面的解答噢,**不然...我也没办法\( ̄ ▽  ̄\)" 16 | 17 | ## 挑战解答 18 | 19 | ### 简介 20 | 21 | > 目前我们学的内容都是跑在命令行中的,并没有界面,那么"脚本语言"Python 如何搭建 GUI 界面呢? 22 | 23 | 其实 Python 支持多种图形界面库,如[Tk\(Tkinter\)](https://wiki.python.org/moin/TkInter)、[wxPython](https://www.wxpython.org/)、[PyQt](https://wiki.python.org/moin/PyQt)等,虽然 Python 自带 Tkinter,无需额外安装包,但我更推荐使用 PyQt,一是因为它完全基于 Qt,跨平台,功能强大,有助于了解 Qt 的语法,二是 Qt 提供了 Designer 设计工具,界面设计上可以拖控件搞定,非常方便,大大节省时间。 24 | 25 | - 最新版本:PyQt 5.x 26 | - 官网(可能需要科学上网):[https://www.riverbankcomputing.com/software/pyqt/](https://www.riverbankcomputing.com/software/pyqt/) 27 | 28 | 大家感兴趣的话,除去官网,下面是一些可参考的资源: 29 | 30 | - [Python Wiki: PyQt](https://wiki.python.org/moin/PyQt) 31 | - [PyQt/Tutorials](https://wiki.python.org/moin/PyQt/Tutorials) 32 | - PyQt5 tutorial:[英文原版](http://zetcode.com/gui/pyqt5/) 33 | - PyQt4 tutorial:[中文版](http://www.qaulau.com/books/PyQt4_Tutorial/index.html)、[英文原版](http://zetcode.com/gui/pyqt4/) 34 | - [Qt5 Documentation](https://doc.qt.io/qt-5/) 35 | - 中文参考书:[PyQt5 快速开发与实战](https://www.amazon.cn/dp/B075VWFYFH/ref=sr_1_1?ie=UTF8&qid=1543407852&sr=8-1&keywords=PyQt5%E5%BF%AB%E9%80%9F%E5%BC%80%E5%8F%91%E4%B8%8E%E5%AE%9E%E6%88%98) 36 | - [基于 Qt 的 Python IDE Eric](http://eric-ide.python-projects.org/) 37 | 38 | ### 安装 39 | 40 | ```python 41 | pip install pyqt5 42 | ``` 43 | 44 | 下载速度慢的话,可以到[PyPI](https://pypi.org/project/PyQt5/#files)上下载离线版安装。另外我推荐使用 Qt Designer 来设计界面,如果你装的是 Anaconda 的话,就已经自带了 designer.exe,例如我的是在`D:\ProgramData\Anaconda3\Library\bin\`,如果是普通的 Python 环境,则需要自行安装: 45 | 46 | ```python 47 | pip install pyqt5-tools 48 | ``` 49 | 50 | 安装完成后,designer.exe 应该在 Python 安装目录下`xxx\Lib\site-packages\pyqt5_tools\`。 51 | 52 | 可以使用下面的代码生成一个简单的界面: 53 | 54 | ```python 55 | import sys 56 | from PyQt5.QtWidgets import QApplication, QWidget 57 | 58 | if __name__ == '__main__': 59 | app = QApplication(sys.argv) 60 | 61 | window = QWidget() 62 | window.setWindowTitle('Hello World!') 63 | window.show() 64 | 65 | sys.exit(app.exec_()) 66 | ``` 67 | 68 | ![](http://cos.codec.wang/cv2_pyqt5_hello_world_sample.jpg) 69 | 70 | ### 界面设计 71 | 72 | 根据我们的挑战内容,解决思路是使用 Qt Designer 来设计界面,使用 Python 完成代码逻辑。打开 designer.exe,会弹出创建新窗体的窗口,我们直接点击`create`: 73 | 74 | ![](http://cos.codec.wang/cv2_pyqt5_designer_main_ui.jpg) 75 | 76 | 界面的左侧是 Qt 的常用控件`Widget Box`,右侧有一个控件属性窗口`Property Editor`,其余暂时用不到。本例中我们只用到了`Push Button`控件和`Label`控件:最上面的三个 Label 控件用于显示图片,可以在属性窗口调整它的大小,我们统一调整到 150×150: 77 | 78 | ![](http://cos.codec.wang/cv2_pyqt5_main_ui_rough.jpg) 79 | 80 | ![](http://cos.codec.wang/cv2_pyqt5_designer_property_windows.jpg) 81 | 82 | 另外,控件上显示的文字`text`属性和控件的名字`objectName`属性需要修改,便于显示和代码调用。可以按照下面我推荐的命名: 83 | 84 | | 控件 | 显示内容 text | 控件名 objectName | 85 | | :--------: | :--------------: | :---------------: | 86 | | PushButton | 打开摄像头 | btnOpenCamera | 87 | | PushButton | 捕获图片 | btnCapture | 88 | | PushButton | 打开图片 | btnReadImage | 89 | | PushButton | 灰度化 | btnGray | 90 | | PushButton | 阈值分割\(Otsu\) | btnThreshold | 91 | | Label | 摄像头 | labelCamera | 92 | | Label | 捕获图 | labelCapture | 93 | | Label | 结果图 | labelResult | 94 | 95 | 这样大致界面就出来了,很简单: 96 | 97 | ![](http://cos.codec.wang/cv2_pyqt5_main_ui_word.jpg) 98 | 99 | ### 按钮事件 100 | 101 | 如果你之前有过一些 GUI 开发经验,比如 MFC,WinForm 等,就知道 GUI 是通过事件驱动的,什么意思呢?比如前面我们已经设计好了界面,接下来就需要实现"打开摄像头"到"阈值分割"这 5 个按钮的功能,也就是给每个按钮指定一个"函数",逻辑代码写在这个函数里面。这种函数就称为事件,Qt 中称为槽连接。 102 | 103 | 点击 Designer 工具栏的`Edit Signals/Slots`按钮,进入槽函数编辑界面,点击旁边的`Edit Widgets`可以恢复正常视图: 104 | 105 | ![](http://cos.codec.wang/cv2_pyqt5_designer_edit_singals_slots.jpg) 106 | 107 | 然后点击按钮并拖动,当产生类似于电路中的接地符号时释放鼠标,参看下面动图: 108 | 109 | ![](http://cos.codec.wang/cv2_pyqt5_how_to_create_slots.gif) 110 | 111 | 在弹出的配置窗口中,可以看到左侧是按钮的常用事件,我们选择点击事件"clicked()",然后添加一个名为"btnOpenCamera_Clicked()"的槽函数: 112 | 113 | ![](http://cos.codec.wang/cv2_pyqt5_how_to_create_slots2.gif) 114 | 115 | 重复上面的步骤,给五个按钮添加五个槽函数,最终结果如下: 116 | 117 | ![](http://cos.codec.wang/cv2_pyqt5_main_click_event.jpg) 118 | 119 | 到此,我们就完成了界面设计的所有工作,按下 Ctrl+S 保存当前窗口为.ui 文件。.ui 文件其实是按照 XML 格式标记的内容,可以用文本编辑器将.ui 文件打开看看。 120 | 121 | ### ui 文件转 py 代码 122 | 123 | 因为我们是用 Designer 工具设计出的界面,并不是用 Python 代码敲出来的,所以要想真正运行,需要使用 pyuic5 将 ui 文件转成 py 文件。pyuic5.exe 默认在`%\Scripts\`下,比如我的是在`D:\ProgramData\Anaconda3\Scripts\`。 124 | 125 | 打开 cmd 命令行,切换到 ui 文件的保存目录。Windows 下有个小技巧,可以在目录的地址栏输入 cmd,一步切换到当前目录: 126 | 127 | ![](http://cos.codec.wang/cv2_pyqt5_pyuic_quick_cmd.gif) 128 | 129 | 然后执行这条指令: 130 | 131 | ```python 132 | pyuic5 -o mainForm.py using_pyqt_create_ui.ui 133 | ``` 134 | 135 | 如果出现 pyuic5 不是内部命令的错误,说明 pyuic5 的路径没有在环境变量里,添加下就好了。执行正常的话,就会生成 mainForm.py 文件,里面应该包含一个名为"Ui_MainWindow"的类。 136 | 137 | ### 编写逻辑代码 138 | 139 | :::tip 140 | mainForm.py 文件是根据 ui 文件生成的,也就是说**重新生成会覆盖掉**。所以为了使界面与逻辑分离,我们需要新建一个逻辑文件。 141 | ::: 142 | 143 | 在同一工作目录下新建一个"mainEntry.py"的文件,存放逻辑代码。代码中的每部分我都写得比较独立,没有封装成函数,便于理解。代码看上去很长,但很简单,可以每个模块单独看,有几个需要注意的地方我做了注释: 144 | 145 | ```python 146 | import sys 147 | import cv2 148 | 149 | from PyQt5 import QtCore, QtGui, QtWidgets 150 | from PyQt5.QtCore import * 151 | from PyQt5.QtGui import * 152 | from PyQt5.QtWidgets import QFileDialog, QMainWindow 153 | 154 | from mainForm import Ui_MainWindow 155 | 156 | 157 | class PyQtMainEntry(QMainWindow, Ui_MainWindow): 158 | def __init__(self): 159 | super().__init__() 160 | self.setupUi(self) 161 | 162 | self.camera = cv2.VideoCapture(0) 163 | self.is_camera_opened = False # 摄像头有没有打开标记 164 | 165 | # 定时器:30ms 捕获一帧 166 | self._timer = QtCore.QTimer(self) 167 | self._timer.timeout.connect(self._queryFrame) 168 | self._timer.setInterval(30) 169 | 170 | def btnOpenCamera_Clicked(self): 171 | ''' 172 | 打开和关闭摄像头 173 | ''' 174 | self.is_camera_opened = ~self.is_camera_opened 175 | if self.is_camera_opened: 176 | self.btnOpenCamera.setText("关闭摄像头") 177 | self._timer.start() 178 | else: 179 | self.btnOpenCamera.setText("打开摄像头") 180 | self._timer.stop() 181 | 182 | def btnCapture_Clicked(self): 183 | ''' 184 | 捕获图片 185 | ''' 186 | # 摄像头未打开,不执行任何操作 187 | if not self.is_camera_opened: 188 | return 189 | 190 | self.captured = self.frame 191 | 192 | # 后面这几行代码几乎都一样,可以尝试封装成一个函数 193 | rows, cols, channels = self.captured.shape 194 | bytesPerLine = channels * cols 195 | # Qt 显示图片时,需要先转换成 QImgage 类型 196 | QImg = QImage(self.captured.data, cols, rows, bytesPerLine, QImage.Format_RGB888) 197 | self.labelCapture.setPixmap(QPixmap.fromImage(QImg).scaled( 198 | self.labelCapture.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) 199 | 200 | def btnReadImage_Clicked(self): 201 | ''' 202 | 从本地读取图片 203 | ''' 204 | # 打开文件选取对话框 205 | filename, _ = QFileDialog.getOpenFileName(self, '打开图片') 206 | if filename: 207 | self.captured = cv2.imread(str(filename)) 208 | # OpenCV 图像以 BGR 通道存储,显示时需要从 BGR 转到 RGB 209 | self.captured = cv2.cvtColor(self.captured, cv2.COLOR_BGR2RGB) 210 | 211 | rows, cols, channels = self.captured.shape 212 | bytesPerLine = channels * cols 213 | QImg = QImage(self.captured.data, cols, rows, bytesPerLine, QImage.Format_RGB888) 214 | self.labelCapture.setPixmap(QPixmap.fromImage(QImg).scaled( 215 | self.labelCapture.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) 216 | 217 | def btnGray_Clicked(self): 218 | ''' 219 | 灰度化 220 | ''' 221 | # 如果没有捕获图片,则不执行操作 222 | if not hasattr(self, "captured"): 223 | return 224 | 225 | self.cpatured = cv2.cvtColor(self.captured, cv2.COLOR_RGB2GRAY) 226 | 227 | rows, columns = self.cpatured.shape 228 | bytesPerLine = columns 229 | # 灰度图是单通道,所以需要用 Format_Indexed8 230 | QImg = QImage(self.cpatured.data, columns, rows, bytesPerLine, QImage.Format_Indexed8) 231 | self.labelResult.setPixmap(QPixmap.fromImage(QImg).scaled( 232 | self.labelResult.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) 233 | 234 | def btnThreshold_Clicked(self): 235 | ''' 236 | Otsu 自动阈值分割 237 | ''' 238 | if not hasattr(self, "captured"): 239 | return 240 | 241 | _, self.cpatured = cv2.threshold( 242 | self.cpatured, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) 243 | 244 | rows, columns = self.cpatured.shape 245 | bytesPerLine = columns 246 | # 阈值分割图也是单通道,也需要用 Format_Indexed8 247 | QImg = QImage(self.cpatured.data, columns, rows, bytesPerLine, QImage.Format_Indexed8) 248 | self.labelResult.setPixmap(QPixmap.fromImage(QImg).scaled( 249 | self.labelResult.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) 250 | 251 | @QtCore.pyqtSlot() 252 | def _queryFrame(self): 253 | ''' 254 | 循环捕获图片 255 | ''' 256 | ret, self.frame = self.camera.read() 257 | 258 | img_rows, img_cols, channels = self.frame.shape 259 | bytesPerLine = channels * img_cols 260 | 261 | cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB, self.frame) 262 | QImg = QImage(self.frame.data, img_cols, img_rows, bytesPerLine, QImage.Format_RGB888) 263 | self.labelCamera.setPixmap(QPixmap.fromImage(QImg).scaled( 264 | self.labelCamera.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) 265 | 266 | 267 | if __name__ == "__main__": 268 | app = QtWidgets.QApplication(sys.argv) 269 | window = PyQtMainEntry() 270 | window.show() 271 | sys.exit(app.exec_()) 272 | ``` 273 | 274 | ![](http://cos.codec.wang/cv2_pyqt_gui_sample2.jpg) 275 | 276 | 本文只是抛砖引玉,介绍了 PyQt5 的简单使用,想要深入学习,可以参考本文开头的参考资料噢\(●ˇ∀ˇ●\) 277 | 278 | ## 引用 279 | 280 | - [本节源码](https://github.com/codecwang/OpenCV-Python-Tutorial/tree/master/Challenge-02-Create-GUI-with-PyQt5) 281 | -------------------------------------------------------------------------------- /static/img/undraw_docusaurus_tree.svg: -------------------------------------------------------------------------------- 1 | 2 | Focus on What Matters 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /static/img/undraw_docusaurus_mountain.svg: -------------------------------------------------------------------------------- 1 | 2 | Easy to Use 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | --------------------------------------------------------------------------------