{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 第5章 边缘检测"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5.1 简介"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "边缘是图像中的重要局部变化，并且是分析图像的重要特征，通常出现在图像中两个不同区域之间的边界上。边缘检测的目标是识别图像中的突然变化，换言之，可以将图像中的大多数语义和形状信息在边缘进行编码。边缘检测最理想的结果是类似于艺术家的线条画，如图 5-1 所示。\n",
    "\n",
    "<center>\n",
    "    <img style=\"border-radius: 0.3125em;\" \n",
    "    src=\"1.png\" width=200>\n",
    "    <br>\n",
    "    <div style=\"color:orange; \n",
    "    display: inline-block;\n",
    "    color: #999;\n",
    "    padding: 2px;\">图5-1 边缘检测示意</div>\n",
    "</center>\n",
    "\n",
    "在本章中，我们将学习边缘检测的相关知识，并在测试图片中进行相应的实验。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5.2 图像梯度"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在正式学习边缘检测之前，我们先来学习一些预备知识。\n",
    "\n",
    "学过微积分的同学应该都知道，一维函数的一阶微分的基本定义为：\n",
    "\n",
    "$$\\frac{df}{dx} = \\lim_{\\Delta x \\rightarrow 0} \\frac{f(x+\\Delta x)-f(x)}{\\Delta x}$$\n",
    "\n",
    "而我们知道，图像是一个二维的函数，我们将一维的微分拓展到二维，即微分方程：\n",
    "\n",
    "$$\\frac{\\partial f(x,y)}{\\partial x} = \\lim_{\\epsilon \\rightarrow 0} \\frac{f(x+\\epsilon, y)-f(x,y)}{\\epsilon}$$\n",
    "\n",
    "$$\\frac{\\partial f(x,y)}{\\partial y} = \\lim_{\\epsilon \\rightarrow 0} \\frac{f(x,y+\\epsilon)-f(x,y)}{\\epsilon}$$\n",
    "\n",
    "由上述公式我们不难发现，图像梯度即为当前像素点对$X$轴和$Y$轴的偏导数，所以梯度我们也可以理解为像素灰度值变化的速度。由于图像是离散的，上式还可以写成：\n",
    "\n",
    "$$ \\frac{\\partial f(x,y)}{\\partial x} = {f(x+1, y)-f(x,y)}$$\n",
    "\n",
    "$$ \\frac{\\partial f(x,y)}{\\partial y} = {f(x,y+1)-f(x,y)}$$\n",
    "\n",
    "在图像中，我们也可以近似的认为：\n",
    "\n",
    "$$ \\frac{\\partial f(x,y)}{\\partial x} \\approx \\frac{f(x+1, y)-f(x-1,y)}{2}$$\n",
    "\n",
    "$$ \\frac{\\partial f(x,y)}{\\partial y} \\approx \\frac{f(x,y+1)-f(x,y-1)}{2}$$\n",
    "\n",
    "不难发现，微积分在图像处理表现的形式为计算当前像素点沿偏微分方向的差值，所以实际的应用是不需要用到求导的，只需进行简单的加减运算。\n",
    "\n",
    "除此之外，梯度的模也是一个重要的概念，它表示在其最大变化率方向上单位距离所增加的量，数学表示为：\n",
    "\n",
    "$$G[f(x,y)] = \\sqrt{(\\frac{\\partial f}{\\partial x})^2 + (\\frac{\\partial f}{\\partial y})^2}$$\n",
    "\n",
    "\n",
    "那么图像梯度和边缘检测有什么关系呢？我们知道在图像中边缘部分往往与周围的像素差值较大，即边缘处的“梯度”比较明显。因此，我们可以考虑使用梯度的方法对图像进行边缘检测。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5.3 边缘检测算法"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "边缘检测算法通常有三个步骤：\n",
    "\n",
    "1. 滤波：边缘检测算法主要是基于图像强度的微分，但导数的计算对噪声很敏感，因此我们首先对图像进行滤波，以此来提高边缘检测器的性能。值得注意的是，大多数滤波器在降低噪声的同时也导致了边缘强度的损失，因此，增强边缘和降低噪声之间的比例需要进行权衡．\n",
    "\n",
    "2. 增强：将图像中强度值有显著变化的点突显出来，这一过程通常是基于图像梯度的模实现。\n",
    "\n",
    "3. 检测：确定计算图像中的边缘\n",
    "\n",
    "接下来，我们将介绍两种不同的边缘检测算法：Sobel算子和Canny算法\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.3.1 Sobel 算子"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sobel算子是整像素图像边缘检测中最重要的算子之一，该算子包含两组3x3的矩阵，分别为横向及纵向。两算子的数学形式如下：\n",
    "\n",
    "$$G_x = \\begin{bmatrix}\n",
    "   +1 & 0 & -1 \\\\\n",
    "   +2 & 0 & -2 \\\\\n",
    "   +1 & 0 & -1\n",
    "  \\end{bmatrix},\\quad\n",
    "  G_y = \\begin{bmatrix}\n",
    "   +1 & +2 & +1 \\\\\n",
    "   0 & 0 & 0 \\\\\n",
    "   -1 & -2 & -1\n",
    "  \\end{bmatrix}\n",
    "$$\n",
    "\n",
    "我们选取$X$方向的Sobel算子，将其拆分为：\n",
    "\n",
    "$$\n",
    "G_x = \\begin{bmatrix}\n",
    "   +1 & 0 & -1 \\\\\n",
    "   +2 & 0 & -2 \\\\\n",
    "   +1 & 0 & -1\n",
    "  \\end{bmatrix} = \\begin{bmatrix} \n",
    "  1 \\\\ 2 \\\\ 1\n",
    "  \\end{bmatrix}\\begin{bmatrix} \n",
    "  +1 & 0 & -1\n",
    "  \\end{bmatrix}\n",
    "$$\n",
    "\n",
    "前者是一个高斯滤波器，而后者是一个差分。我们将后者带入图片中，即为：$f(x+1, y)-f(x-1, y)$，可以近似看成是$X$方向上图片的梯度。\n",
    "\n",
    "Sobel算子是高斯平滑与微分操作的结合体，其抗噪声能力很强，用途较多。我们将Sobel算子与图片进行卷积，即可得到图片在某一方向上的边缘。接下来，我们将编写相关函数，测试Sobel算子的性能。\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 101216 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "import sys\n",
    "sys.path.append('../')\n",
    "\n",
    "from utils import *\n",
    "img = Image.open('lena.jpeg')\n",
    "img = img.convert('L')\n",
    "img = np.array(img)\n",
    "\n",
    "kx = np.array([\n",
    "    [1,0,-1],\n",
    "    [2,0,-2],\n",
    "    [1,0,-1]\n",
    "])\n",
    "\n",
    "ky = np.array([\n",
    "    [1, 2, 1],\n",
    "    [0, 0, 0],\n",
    "    [-1,-2,-1]\n",
    "])\n",
    "# 展示原图\n",
    "conv_x = conv_2d(img, kx)\n",
    "conv_y = conv_2d(img, ky)\n",
    "conv_x.plot()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 5296 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 2 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 100836 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# x轴\n",
    "conv_x.plot_kernel()\n",
    "conv_x.plot_conv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 5320 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 2 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 89280 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# y轴\n",
    "conv_y.plot_kernel()\n",
    "conv_y.plot_conv()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.3.2 Canny算法"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "虽然Soble算子简单好用，但检测图像边缘仍然受方向的限制。因此，接下来我们介绍另一种常用的算法：Canny边缘检测算法。\n",
    "\n",
    "Canny算法可能是计算机视觉中最广泛使用的边缘检测器，它通常通过以下几步加以实现：\n",
    "\n",
    "1. 图像平滑：即抑制噪声；\n",
    "2. 计算梯度幅度及方向；\n",
    "3. 应用非最大抑制；\n",
    "4. 利用滞后阈值法来检测边缘。\n",
    "\n",
    "我们将按照其步骤分别进行详细介绍。\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 5.3.2.1 图像平滑"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "由于边缘检测对图像噪声非常的敏感，因此在进行后续的操作之前，我们需要先对图像进行去噪。结合第3章中所学的知识，我们一般使用高斯滤波器进行去噪。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 125060 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 80904 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "from __future__ import print_function\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "import matplotlib.image as mpimg\n",
    "from time import time\n",
    "\n",
    "kernel_size = 5\n",
    "sigma = 1.4\n",
    "\n",
    "img = Image.open('chong.png')\n",
    "img = img.convert('L')\n",
    "img = np.array(img)\n",
    "# Define 5x5 Gaussian kernel with std = sigma\n",
    "kernel = gaussian_kernel(kernel_size, sigma)\n",
    "# Convolve image with kernel to achieve smoothed effect\n",
    "smoothed = cv2.filter2D(img, -1, kernel)\n",
    "plt.imshow(img, cmap='gray')\n",
    "plt.title('Original image')\n",
    "plt.axis('off')\n",
    "plt.show()\n",
    "\n",
    "plt.imshow(smoothed, cmap='gray')\n",
    "plt.title('Smoothed image')\n",
    "plt.axis('off')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 5.3.2.2 求取图像梯度"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "当我们获得了平滑后的图像之后，我们可以利用类似Sobel算子的方式来计算图片的水平方向梯度$G_x$和竖直方向梯度$G_y$；接着，我们便可以计算图像中每个梯度的大小$G=\\sqrt{G_x^2+G_y^2}$，其中梯度的方向是$\\theta = {\\rm arctan}(\\frac{G_y}{G_x})$，梯度的方向通常垂直于边缘方向。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 47236 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 45852 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "def partial_x(img):\n",
    "    Hi, Wi = img.shape  \n",
    "    out = np.zeros((Hi, Wi))\n",
    "    k = np.array([[0,0,0],[0.5,0,-0.5],[0,0,0]])\n",
    "    out = cv2.filter2D(img, -1, k)\n",
    "    return out\n",
    "\n",
    "\n",
    "def partial_y(img):\n",
    "    Hi, Wi = img.shape  \n",
    "    out = np.zeros((Hi, Wi))\n",
    "    k = np.array([[0,0.5,0],[0,0,0],[0,-0.5,0]])\n",
    "    out = cv2.filter2D(img, -1, k)\n",
    "    return out\n",
    "\n",
    "Gx = partial_x(smoothed)\n",
    "Gy = partial_y(smoothed)\n",
    "\n",
    "plt.imshow(Gx, cmap='gray')\n",
    "plt.title('Derivative in x direction')\n",
    "plt.axis('off')\n",
    "plt.show()\n",
    "\n",
    "plt.imshow(Gy, cmap='gray')\n",
    "plt.title('Derivative in y direction')\n",
    "plt.axis('off')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 52920 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# 计算梯度以及方向\n",
    "def gradient(img):\n",
    "    G = np.zeros(img.shape)\n",
    "    theta = np.zeros(img.shape)\n",
    "    dx = partial_x(img)\n",
    "    dy = partial_y(img)\n",
    "    G = np.sqrt(dx**2 + dy**2)\n",
    "    theta = np.rad2deg(np.arctan2(dy, dx))\n",
    "    theta %= 360\n",
    "\n",
    "    return G, theta\n",
    "\n",
    "G, theta = gradient(smoothed)\n",
    "plt.imshow(np.uint8(G), cmap='gray')\n",
    "plt.title('Gradient magnitude')\n",
    "plt.axis('off')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 5.3.2.3  非最大抑制"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们能够看到从平滑图像的梯度中提取的边缘非常厚且模糊，因此我们需要使用非最大抑制进行下一步的处理。此步骤的目的是将“模糊”边缘转换为“锐利”边缘，这是通过保留梯度图像中的所有局部最大值并丢弃其他所有内容来完成的。\n",
    "\n",
    "我们先将图像中每个像素的梯度方向四舍五入投影到最近的8邻域（即将每个梯度方向变为$0^{\\circ}$、$45^{\\circ}$、$90^{\\circ}$ ... $315^{\\circ}$）等。接着，将当前像素的边缘强度与该像素在正负梯度方向的边缘强度进行比较。 例如，如果梯度方向为南（$\\theta=90$），则与北和南的像素进行比较。如果当前像素的边缘强度最大，则保留边缘强度的值，反之，删除该值。\n",
    "\n",
    "<center>\n",
    "    <img style=\"border-radius: 0.3125em;\" \n",
    "    src=\"2.png\" width=200>\n",
    "    <br>\n",
    "    <div style=\"color:orange; \n",
    "    display: inline-block;\n",
    "    color: #999;\n",
    "    padding: 2px;\">图5-2 非最大抑制</div>\n",
    "</center>\n",
    "\n",
    "例如在图 5-2 中，$(x,y)$点在边缘上，因此我们可以利用$(x^{'}, y^{'})$和$(x^{''}, y^{''})$点来判断$(x,y)$的梯度是否为最大值，如果是局部最大值，则保留，反之，则判断该点不是边缘，将其值置为0。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 39748 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "def non_maximum_suppression(G, theta):\n",
    "    H, W = G.shape\n",
    "    out = np.zeros((H, W))\n",
    "\n",
    "    # Round the gradient direction to the nearest 45 degrees\n",
    "    theta = np.floor((theta + 22.5) / 45) * 45\n",
    "\n",
    "    theta %= 360\n",
    "    out=G.copy()\n",
    "    for i in range(1,H-1):\n",
    "        for j in range(1,W-1):\n",
    "            angle = theta[i,j]\n",
    "            if angle==0 or angle==180:\n",
    "                ma=max(G[i,j-1],G[i,j+1])\n",
    "            elif angle==45 or angle==45+180:\n",
    "                ma=max(G[i-1,j-1],G[i+1][j+1])\n",
    "            elif angle==90 or angle==90+180:\n",
    "                ma=max(G[i-1,j],G[i+1,j])\n",
    "            elif angle==135 or angle==135+180:\n",
    "                ma=max(G[i-1,j+1],G[i+1,j-1])\n",
    "            else:\n",
    "                print(angle)\n",
    "                raise\n",
    "            if ma>G[i,j]:\n",
    "                out[i,j]=0\n",
    "    return out\n",
    "\n",
    "\n",
    "nms = non_maximum_suppression(G, theta)\n",
    "plt.imshow(np.uint8(nms), cmap='gray')\n",
    "plt.title('Non-maximum suppressed')\n",
    "plt.axis('off')\n",
    "plt.show()\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 5.3.2.4 滞后阈值确定边缘"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在非最大抑制步骤之后剩余的边缘像素许多仍可能是图像中的真实边缘，但有些可能是由噪声或颜色变化引起的。区分这些的最简单方法是使用阈值，以便仅保留比某个值更强的边缘。Canny边缘检测算法使用双阈值，即强于高阈值的边缘像素被标记为强，而弱于低阈值的边缘像素被抑制（即变为0），两个阈值之间的边缘像素被标记为弱。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 13736 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 20808 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "def double_thresholding(img, high, low):\n",
    "    strong_edges = np.zeros(img.shape, dtype=np.bool)\n",
    "    weak_edges = np.zeros(img.shape, dtype=np.bool)\n",
    "    a,b = img.shape\n",
    "    for i in range(a):\n",
    "        for j in range(b):\n",
    "            if img[i,j]>high:\n",
    "                strong_edges[i,j] = 1\n",
    "            elif img[i,j]<=high and img[i,j]>low:\n",
    "                weak_edges[i,j] = 1\n",
    "\n",
    "    return strong_edges, weak_edges\n",
    "\n",
    "\n",
    "low_threshold = 3\n",
    "high_threshold = 6\n",
    "\n",
    "strong_edges, weak_edges = double_thresholding(nms, high_threshold, low_threshold)\n",
    "\n",
    "edges=strong_edges * 1.0 + weak_edges * 0.5\n",
    "\n",
    "plt.imshow(strong_edges, cmap='gray')\n",
    "plt.title('Strong Edges')\n",
    "plt.axis('off')\n",
    "plt.show()\n",
    "\n",
    "plt.imshow(edges, cmap='gray')\n",
    "plt.title('Strong+Weak Edges')\n",
    "plt.axis('off')\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "强边缘可以立即包含在最终边缘图像中。当且仅当弱边连接到强边时，它们才包括在内。接下来，我们将强边和与其有关的弱边相联系。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 15036 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "def get_neighbors(y, x, H, W):\n",
    "    neighbors = []\n",
    "    for i in (y-1, y, y+1):\n",
    "        for j in (x-1, x, x+1):\n",
    "            if i >= 0 and i < H and j >= 0 and j < W:\n",
    "                if (i == y and j == x):\n",
    "                    continue\n",
    "                neighbors.append((i, j))\n",
    "\n",
    "    return neighbors\n",
    "\n",
    "\n",
    "def link_edges(strong_edges, weak_edges):\n",
    "    H, W = strong_edges.shape\n",
    "    indices = np.stack(np.nonzero(strong_edges)).T\n",
    "    edges = np.zeros((H, W), dtype=np.bool)\n",
    "\n",
    "    weak_edges = np.copy(weak_edges)\n",
    "    edges = np.copy(strong_edges)\n",
    "    \n",
    "    q = [(i,j) for i in range(H) for j in range(W) if strong_edges[i,j]]\n",
    "    while q:\n",
    "        i,j=q.pop()\n",
    "        for a,b in get_neighbors(i,j,H,W):\n",
    "            if weak_edges[a][b]:\n",
    "                weak_edges[a][b] = 0\n",
    "                edges[a][b] = 1\n",
    "                q.append((a,b))\n",
    "                \n",
    "    return edges\n",
    "\n",
    "\n",
    "edges = link_edges(strong_edges, weak_edges)\n",
    "\n",
    "plt.imshow(edges, cmap='gray')\n",
    "plt.axis('off')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "综上，我们将Canny边缘检测器封装成如下函数："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 125060 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 17024 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "def canny(img, kernel_size=5, sigma=1.4, high=6, low=3):\n",
    "    \n",
    "    kernel = gaussian_kernel(kernel_size, sigma)\n",
    "    smoothed = cv2.filter2D(img, -1, kernel)    \n",
    "    G, theta = gradient(smoothed)\n",
    "    nms = non_maximum_suppression(G, theta)\n",
    "    strong_edges, weak_edges = double_thresholding(nms, high, low)\n",
    "    edge = link_edges(strong_edges, weak_edges)\n",
    "\n",
    "    return edge\n",
    "\n",
    "plt.imshow(img, cmap='gray')\n",
    "plt.title('Original image')\n",
    "plt.axis('off')\n",
    "plt.show()\n",
    "\n",
    "canny = canny(img)\n",
    "plt.imshow(canny, cmap='gray')\n",
    "plt.title('Edges of image')\n",
    "plt.axis('off')\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5.4 总结"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在这一章中，我们学习了如何对图片进行边缘检测。Sobel算子使用方便灵活，但检测边缘受方向限制，相比较之下，Canny边缘检测器则显得效果更突出。Canny检测器的效果也受高斯核的大小与标准差、强弱边的阈值的影响，感兴趣的同学可以自行尝试。在接下来的章节中，我们会把目光聚焦到比“边缘”更细微的地方：角点检测。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
