搜索技巧

前言

  Shaun 一直以为自己的搜索能力还可以,基本上自己想要的东西都能搜到,但是自从接触到这个世界,才知道自己大概只是个入门的水平(或者说连入门都说不上 /つ∇T)),和网上的一些大神相比还有比较大的差距,此文只是网上一些资料的整理,方便 Shaun 熟练使用,提高驾驶技巧,毕竟信息检索能力还是很重要的。

前言

  Shaun 一直以为自己的搜索能力还可以,基本上自己想要的东西都能搜到,但是自从接触到这个世界,才知道自己大概只是个入门的水平(或者说连入门都说不上 /つ∇T)),和网上的一些大神相比还有比较大的差距,此文只是网上一些资料的整理,方便 Shaun 熟练使用,提高驾驶技巧,毕竟信息检索能力还是很重要的。

搜索篇

Google 可以通过添加一些字符优化搜索结果,如:

搜索需求对应字符
搜索社交媒体在用于搜索社交媒体的字词前加上 @。例如:@twitter
从搜索结果中排除特定字词在您要排除的字词前加上 -。例如:jaguar speed -car
搜索完全匹配的结果为字词或短语加上引号。例如:"tallest building"
搜索通配符或未知字词在字词或短语中您要放置占位符的地方加上 *。例如:"largest * in the world"
在某个数字范围内执行搜索在两个数字之间加上 ..。例如:camera $50..$100
组合搜索在各个搜索查询之间加上“OR”。例如:marathon OR race
搜索特定网站在相应网站或域名前加上“site:”。例如:site:youtube.comsite:.gov
搜索相关网站在已知网址前加上“related:”。例如:related:time.com
查找链接到某个特定网页的网页在已知网址前加上“link:”。例如:link:chongbuluo.com,就能查到哪些网页中包含链接chongbuluo.com
查找在URL地址里有搜索关键词的页面在已知网址前加上“inurl:”。例如:inurl:chongbuluo,就能查到哪些网页 url 中包含链接chongbuluo
查找在网页标题里有搜索关键词的页面在已知网址前加上“intitle:”。例如:intitle:chongbuluo,就能查到哪些网页标题中包含链接chongbuluo
查找在网页正文里有搜索关键词的页面在已知网址前加上“intext:”。例如:intext:chongbuluo,就能查到哪些网页正文中包含链接chongbuluo
查找pdf,xml,xls,txt,doc,csv等特定格式的结果在特定文件格式前加上filetype:。例如 filetype:pdf
查找关键词的定义在关键词前加上define:。例如 define:搜索

  经 Shaun 实测,以上大部分字符对百度搜索引擎同样适用。当然,最好的搜索方式是使用高级搜索,高级搜索可以进行一系列设置,比如时间范围,从而使搜索结果更精确,更容易得到想要的结果,不过在不十分确定的时候,不要做太多限制,不然可能会过滤掉关键信息。

  Google 和百度对英文字符大小写都不敏感,搜索 QQqq 所得到的结果是一样的。搜索是否成功最关键的地方还是在于关键词的选取,关键词的选取这没什么好说的,只能提高搜索熟练度及对问题的把握程度了。

  哦,还有一点忘记说了,就是在 Google 中如果要搜索那个的话,必须要在搜索设置里面关闭安全搜索功能,如果简体中文不能关闭安全搜索功能的话,就在搜索设置里将语言更换到繁体中文或英语应该就能关闭安全搜索功能。

技巧篇

  有时候第一个页面没有想要的结果,于是需要看第二个、第三个页面的结果,是不是觉得翻页很麻烦?(如果不觉得麻烦可以直接跳过🙄),但是设置增加搜索结果条目又会提高显示延迟,这时可以使用 Super_preloaderPlus_one 脚本(Chrome 中可以使用 AutoPagerize 插件),只需鼠标继续往下滚轮就会自动加载下页搜索结果,无需点击翻页。

  当点击网页链接却出现404错误或无法显示页面的错误时,这个时候可以使用搜索引擎的「网页快照」功能,Google的这个功能点击搜索结果页面标题下的 绿色小三角 即可看到,百度的这个叫 百度快照,在搜索结果 url 地址的末端,即结果最后面。快照功能最强大的是 互联网档案馆Internet Archive),又叫『网站时光倒流机器』(Wayback Machine),在知道链接的情况下,将链接输入到 Internet Archive 点击搜索就能查看历史快照了。

  如果要查找某个网页出现的关键词,可以利用 Chrome 和 Firefox 的网页搜索功能,一般可以通过 Ctrl + F 去检索。也可以通过 F12 或「鼠标右键」(Firefox 点击「查看元素」,Chrome 点击「检查」,都是右键弹框最后一个选项)进入浏览器控制台开发工具,Chrome 中还要通过 Ctrl + F 才能检索元素,而 Firefox 可以直接搜索 HTML,输入要查找的关键词,回车即可。

专项篇

  图像搜索:这个一般用 Google 和百度的以图搜图就可以了,不过也有些特殊的图像搜索引擎,如 TinEye 等,当然,图像搜索最好还是安装相应的插件,在 Chrome 中可以安装一个叫 二箱 的插件,在 Firefox 中可以安装一个叫 Search by Image 的插件。

  音乐搜索:这个如果能听出歌词的话就直接去搜索引擎上搜听到的歌词,如果无法听出歌词的话,可以尝试各大音乐软件的「听歌识曲」功能。

  当然更精确的专项搜索一般只存在于特定的网站,这就要看对整个互联网的了解程度了,这里强推「虫部落-快搜」,不管是日常搜索需求还是特殊搜索需求基本都能满足。

后记

  尽量优先使用 Google,百度或许能找到自己想要的,但太浪费时间了,浪费时间就是浪费生命,不过能看到本文的看官,应该也是能熟练使用 Google 的了。

参考资料

[1] 如何在 Google 中进行搜索(https://support.google.com/websearch/#topic=3081620)

[2] 优化网页搜索

[3] 提高搜索能力的关键技巧(如何查找可靠出处)

[4] 谷歌搜索技巧:搜索语法+隐藏彩蛋+高级设置

矩阵的应用之图像仿射变换

前言

  好像很久没写新的东西了,主要是最近期末有一大堆事情要做,忙着写各种结课论文和复习数学,又加上最近忙着把《奥日与黑暗森林:终极版》剧情通关,这游戏不管是画面还是音乐都特别棒,但是对键盘用户太不友好了,不能改键位,需要一只手控制键盘,一只手控制鼠标,如果只是普通的键位就算了,它还要万恶的 shift 键配合,手残党完全吃不消 (´・ω・`)。最终死亡 658 次好歹剧情通关了,最后悔的是没把三段跳点出来 /つ∇T)。这些事情一搞完,Shaun 这不就又开始写了嘛( 说什么也摆脱不了你拖延症晚期的事实 ( ̄ε(# ̄)☆╰╮( ̄▽ ̄///) )。 选修的《数字图像处理》是也早结课了,虽然教的东西大都事前已经了解了,但是好像还没写过图像处理相关的 blog,所以谨以此篇最基础的 blog 来表示一下。

前言

  好像很久没写新的东西了,主要是最近期末有一大堆事情要做,忙着写各种结课论文和复习数学,又加上最近忙着把《奥日与黑暗森林:终极版》剧情通关,这游戏不管是画面还是音乐都特别棒,但是对键盘用户太不友好了,不能改键位,需要一只手控制键盘,一只手控制鼠标,如果只是普通的键位就算了,它还要万恶的 shift 键配合,手残党完全吃不消 (´・ω・`)。最终死亡 658 次好歹剧情通关了,最后悔的是没把三段跳点出来 /つ∇T)。这些事情一搞完,Shaun 这不就又开始写了嘛( 说什么也摆脱不了你拖延症晚期的事实 ( ̄ε(# ̄)☆╰╮( ̄▽ ̄///) )。 选修的《数字图像处理》是也早结课了,虽然教的东西大都事前已经了解了,但是好像还没写过图像处理相关的 blog,所以谨以此篇最基础的 blog 来表示一下。

预备篇

  首先需要了解的是图像中坐标系和数学课本中常用的坐标系略有不同,图像中坐标系是以左上角为原点,水平向右为 X 轴,垂直向下为 Y 轴;而数学课本中常见的坐标系是以图像中心为原点,水平向右为 X 轴,垂直向上为 Y 轴。所以由图像中坐标 \((x,y)\) 转数学课本中常见的坐标 \((x',y')\) 的公式为 $x' = x - C/2; y' = -y + R/2; $ 其中 \(C\) 表示原图像总列数,即原图像宽度,\(R\) 表示原图像总行数,即原图像高度。

X 轴Y 轴X 轴Y 轴数学坐标系图像坐标系

  ※注:值得注意的是因为 MATLAB 和 OpenCV 的像素索引坐标形式为 (行坐标,列坐标) ,所以若以本文这样定的图像坐标,则图像中坐标 \((x,y)\) 对应的像素值为 \(f(y,x)\)

变换篇

据 OpenCV 文档所说:

  1. 一个任意的仿射变换都能表示为 乘以一个矩阵 (线性变换) 接着再 加上一个向量 (平移).

  2. 综上所述, 我们能够用仿射变换来表示:

    1. 旋转 (线性变换)
    2. 平移 (向量加)
    3. 缩放操作 (线性变换)

    你现在可以知道, 事实上, 仿射变换代表的是两幅图之间的 关系 .

  3. 我们通常使用 2 矩阵来表示仿射变换.

    A = \begin{bmatrix}     a_{00} & a_{01} \\     a_{10} & a_{11}     \end{bmatrix}_{2 \times 2} B = \begin{bmatrix}     b_{00} \\     b_{10}     \end{bmatrix}_{2 \times 1} M = \begin{bmatrix}     A & B     \end{bmatrix} =\begin{bmatrix}     a_{00} & a_{01} & b_{00} \\     a_{10} & a_{11} & b_{10}\end{bmatrix}_{2 \times 3}  

    考虑到我们要使用矩阵 AB 对二维向量 X = \begin-{bmatrix}x \ y\end-{bmatrix} 做变换, 所以也能表示为下列形式:

    T = A \begin-{bmatrix}x \ y\end-{bmatrix} + B or T = M ^{T}

    T=\begin-{bmatrix}a_{00}x + a_{01}y + b_{00} \ a_{10}x + a_{11}y + b_{10}\end-{bmatrix}  

而在冈萨雷斯的《数字图像处理_第三版》里有:

最常用的空间坐标变换之一是仿射变换,其一般形式如下: \[ \begin{bmatrix} x & y & 1 \end{bmatrix} = \begin{bmatrix} v & w & 1 \end{bmatrix} \textbf{T} = \begin{bmatrix} v & w & 1 \end{bmatrix} \begin{bmatrix} t_{11} & t_{12} & 0 \\ t_{21} & t_{22} & 0 \\ t_{31} & t_{32} & 1 \end{bmatrix} \] 其中 \((v,w)\) 为原坐标,\((x,y)\) 为变换后的坐标,可根据变换矩阵 \(\textbf{T}\) 中的元素选择的值,对一组坐标点做尺度、旋转、平移或偏移变换。一些常见的变换矩阵及作用如下表:

变换名称仿射变换矩阵\(\textbf{T}\)坐标公式
恒等变换\(\begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix}\)\(\begin{cases} x=v , \\ y=w \end{cases}\)
尺度变换\(\begin{bmatrix} c_x & 0 & 0 \\ 0 & c_y & 0 \\ 0 & 0 & 1 \end{bmatrix}\)\(\begin{cases} x=vc_x , \\ y=wc_y \end{cases}\)
旋转变换(以逆时针为正)\(\begin{bmatrix} cos(\theta) & sin(\theta) & 0 \\ -sin(\theta) & cos(\theta) & 0 \\ 0 & 0 & 1 \end{bmatrix}\)\(\begin{cases} x=vcos(\theta)-wsin(\theta) , \\ y=vsin(\theta)+wcos(\theta) \end{cases}\)
旋转变换(以顺时针为正)\(\begin{bmatrix} cos(\theta) & -sin(\theta) & 0 \\ sin(\theta) & cos(\theta) & 0 \\ 0 & 0 & 1 \end{bmatrix}\)\(\begin{cases} x=vcos(\theta)+wsin(\theta) , \\ y=-vsin(\theta)+wcos(\theta) \end{cases}\)
平移变换\(\begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ t_x & t_y & 1 \end{bmatrix}\)\(\begin{cases} x=v+t_x , \\ y=w+t_y \end{cases}\)
偏移变换(水平)\(\begin{bmatrix} 1 & 0 & 0 \\ s_h & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix}\)\(\begin{cases} x=v+ws_h , \\ y=w \end{cases}\)
偏移变换(垂直)\(\begin{bmatrix} 1 & s_v & 0 \\\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix}\)\(\begin{cases} x=v , \\ y=vs_v+w \end{cases}\)

仿射变换的实现由两种方式:一种是 前向映射(Forward Mapping):直接采用利用原图像坐标 \((v,w)\) 通过 \(\begin{bmatrix} x & y & 1\end{bmatrix}=\begin{bmatrix} v & w & 1\end{bmatrix} \textbf{T}\) 得到变换后的坐标 \((x,y)\),使用前向映射会导致一些问题:可能会有多个像素坐标映射到输出图像的同一位置,也可能输出图像的某些位置完全没有相应的输入图像像素与它匹配,也就是没有被映射到,造成有规律的空洞(黑色的花纹状);更好的一种方式是采用 反向映射(Inverse Mapping):扫描输出图像的位置 \((x,y)\),通过 \(\begin{bmatrix} v & w & 1\end{bmatrix}= \begin{bmatrix} x & y & 1\end{bmatrix}\textbf{T}^{-1}\)(其中 \(\textbf{T}^{-1}\)\(\textbf{T}\) 的逆矩阵)计算输入图像对应的位置 \((v,w)\),通过插值方法决定输出图像该位置的灰度值。

  本文这里采取冈萨雷斯的《数字图像处理_第三版》的变换矩阵方式,毕竟所学的矩阵论也是将变换矩阵放在后面作为第二个因子。虽然仿射变换都有现成的 API 可以调用,而且速度一般要比自己写的要快,但是知其然终究也要知其所以然。

  下面就以旋转变换为例了,因为尺度变换和平移变换只需要相应的缩放图像即可,而旋转变换不仅需要更改图像大小,还要确定旋转中心,而旋转中心一般以图像中心为标准。

旋转变换

  旋转变换首先需要确定旋转中心,若以图像左上角为旋转中心,则只需要像做尺度变换和平移变换那样通过 \(\begin{bmatrix} v & w & 1\end{bmatrix}= \begin{bmatrix} x & y & 1\end{bmatrix}\textbf{T}^{-1}\) 做普通的变换即可。而以图像中心为旋转中心,首先需要做坐标变换,将以左上角为原点,水平向右为 X 轴,垂直向下为 Y 轴的图像坐标系转换为以图像中心为原点,水平向右为 X 轴,垂直向下为 Y 轴的数学坐标系;再做正常的旋转变换;随后再将数学坐标系转换为图像坐标系,所以图像中心为旋转中心的旋转变换总共需要做三次变换。这里就以图像中心为旋转中心为例,由于有三次变换,所以应该有三个变换矩阵相乘,设 图像坐标系==》数学坐标系的变换矩阵为 T1,旋转变换矩阵为 T2,数学坐标系==》图像坐标系的变换矩阵为 T3,设顺时针旋转角度为 \(\theta\) ,原图像宽度为 \(C\),高度为 \(R\),旋转后图像宽度为 \(W\),高度为 \(H\),则: \[ T1=\begin{bmatrix} 1 & 0 & 0 \\ 0 & -1 & 0 \\ -0.5C & 0.5R & 1 \end{bmatrix}   T2=\begin{bmatrix} cos(\theta) & -sin(\theta) & 0 \\ sin(\theta) & cos(\theta) & 0 \\ 0 & 0 & 1 \end{bmatrix}   T3=\begin{bmatrix} 1 & 0 & 0 \\ 0 & -1 & 0 \\ 0.5W & 0.5H & 1 \end{bmatrix} \] 则旋转变换最终形式为:\(\begin{bmatrix} x & y & 1 \end{bmatrix}=\begin{bmatrix} v & w & 1\end{bmatrix} \textbf{T}=\begin{bmatrix} v & w & 1 \end{bmatrix}T1*T2*T3\)

※BTW:旋转变换中,旋转后图像宽度 \(W\),高度 \(H\) 与 原图像宽度 \(C\),高度 \(R\) 的关系为: \[ \begin{cases} H = |R*cos(\theta)| + |C*sin(\theta)| , \\ W = |C*cos(\theta)| + |R*sin(\theta)| \end{cases} \]

插值篇

  因为经过采用反向映射1方式的仿射变换之后,得到的原图像坐标 \((v,w)\) 往往不是整数值,所以无法知道其对应的像素值 \(f(w,v)\),这时需要采取插值的方式近似估计该坐标位置的像素值。

  常用的插值方法有最近邻插值(nearest neighbor interpolation)、双线性插值(bilinear interpolation)和双三次插值(bicubic interpolation),其中双三次插值在保持图像细节方面最好,但花费时间也最多,PS 中的消除锯齿和羽化效果好像就采用了双三次插值。

  最近邻插值只考虑相邻最近的像素,双线性插值考虑相邻的 4 个像素点,双三次插值则考虑相邻的 16 个像素点。

最近邻插值

  最近邻插值最简单,将变换后图像的坐标 \((x,y)\) 通过反向映射得到原图像坐标 \((v,w)\) ,直接对 \((v,w)\) 进行四舍五入得到相应的整数坐标 \((⌊v+0.5⌋,⌊w+0.5⌋)\)2,用该整数坐标的像素值近似估计 \((v,w)\) 的像素值,令 \(f(y,x)=f(⌊w+0.5⌋,⌊v+0.5⌋)\) ,从而得到变换后图像每个像素点的像素值。

双线性插值

  双线性插值是线性插值方法的一种扩展,它是 X 和 Y 两个方向上线性插值的组合。

X 轴Y 轴P1P2P3P4Z1Z2P(v,w)

如上图,设变换后图像的坐标 \((x,y)\) 通过反向映射得到原图像坐标 \((v,w)\) ,即点 \(P\) 正好处于四个像素点 \(P1(v_0, w_0)\)\(P2(v_0+1, w_0)\)\(P3(v_0+1, w_0+1)\)\(P4(v_0, w_0+1)\) 的中间,其中 \(v_0=⌊v⌋\)\(w_0=⌊w⌋\) ,点 \(P\) 对应的像素值为 \(f(P)\) 因为双线性插值即在 \(X\)\(Y\) 两个方向进行线性插值,首先计算 \(X\) 方向的插值: \[ \begin{cases} \frac{f(P2)-f(P1)}{P2.x-P1.x}=\frac{f(Z1)-f(P1)}{Z1.x-P1.x} , \\ \frac{f(P3)-f(P4)}{P3.x-P4.x}=\frac{f(Z2)-f(P4)}{Z2.x-P4.x} \end{cases} \] 即: \[ \begin{cases} f(Z1)=\frac{Z1.x-P1.x}{P2.x-P1.x}f(P2)+\frac{P2.x-Z1.x}{P2.x-P1.x}f(P1) =(v-v_0)f(P2)+(v_0+1-v)f(P1), \\ f(Z2)=\frac{Z2.x-P4.x}{P3.x-P4.x}f(P3)+\frac{P3.x-Z2.x}{P3.x-P4.x}f(P4) =(v-v_0)f(P3)+(v_0+1-v)f(P4) \end{cases}\tag{1} \] 然后计算 \(Y\) 方向的插值: \[ \begin{equation} \frac{f(Z2)-f(Z1)}{Z2.y-Z1.y}=\frac{f(P)-f(Z1)}{P.y-Z1.y} \end{equation} \] 即: \[ \begin{equation} f(P)=\frac{P.y-Z1.y}{Z2.y-Z1.y}f(Z2)+\frac{Z2.y-P.y}{Z2.y-Z1.y}f(Z1) =(w-w_0)f(Z2)+(w_0+1-w)f(Z1) \end{equation}\tag{2} \] 结合式(1)和式(2)可得: \[ \begin{equation} f(P)=(v_0+1-v)(w_0+1-w)f(P1)+(v-v_0)(w_0+1-w)f(P2)+(v-v_0)(w-w_0)f(P3)+(v_0+1-v)(w-w_0)f(P4) \end{equation} \] 用矩阵形式可表示为: \[ f(P)=\begin{bmatrix} v_0+1-v & v-v_0 \end{bmatrix} \begin{bmatrix} f(P1) & f(P4) \\ f(P2) & f(P3) \end{bmatrix} \begin{bmatrix} (w_0+1-w) \\ (w-w_0) \end{bmatrix} \] 即: \[ f(y,x)=f(w,v)=\begin{bmatrix} ⌊v⌋+1-v & v-⌊v⌋ \end{bmatrix} \begin{bmatrix} f(⌊w⌋,⌊v⌋) & f(⌊w⌋+1,⌊v⌋) \\ f(⌊w⌋,⌊v⌋+1) & f(⌊w⌋+1,⌊v⌋+1) \end{bmatrix} \begin{bmatrix} (⌊w⌋+1-w) \\ (w-⌊w⌋) \end{bmatrix} \]

双三次插值

X 轴Y 轴P11P(v,w)

  如上图,双三次插值需要考虑相邻16个像素(4×4),用双三次插值重采样的图像更平滑并且更能保留图像细节,在这三种插值算法中,双三次插值效果最好,但处理速度最慢。同样设变换后图像的坐标 \((x,y)​\) 通过反向映射得到原图像坐标 \((v,w)​\) ,与其左上角相邻最近的 点P11 坐标则为 \((⌊v⌋,⌊w⌋)​\) ,该插值方法需要选取一个合适的插值基函数,参照维基百科 Bicubic interpolation 的一般为: \[ W(x) = \begin{cases} (a+2)|x|^3-(a+3)|x|^2+1 & \text{for } |x| \leq 1, \\ a|x|^3-5a|x|^2+8a|x|-4a & \text{for } 1 < |x| < 2, \\ 0 & \text{otherwise}, \end{cases} \] 其中 \(a\) 一般取 -0.5 、-0.75 或 -1;则:\(f(y,x)=f(w,v)=A*B*C\) ,其中: \[ A=\begin{bmatrix} W( v-(⌊v⌋-1) ) & W(v-⌊v⌋) & W( (⌊v⌋+1)-v ) & W( (⌊v⌋+2)-v ) \end{bmatrix} \\ B=\begin{bmatrix} f(⌊w⌋-1,⌊v⌋-1) & f(⌊w⌋,⌊v⌋-1) & f(⌊w⌋+1,⌊v⌋-1) & f(⌊w⌋+2,⌊v⌋-1) \\ f(⌊w⌋-1,⌊v⌋) & f(⌊w⌋,⌊v⌋) & f(⌊w⌋+1,⌊v⌋) & f(⌊w⌋+2,⌊v⌋) \\ f(⌊w⌋-1,⌊v⌋+1) & f(⌊w⌋,⌊v⌋+1) & f(⌊w⌋+1,⌊v⌋+1) & f(⌊w⌋+2,⌊v⌋+1) \\ f(⌊w⌋-1,⌊v⌋+2) & f(⌊w⌋,⌊v⌋+2) & f(⌊w⌋+1,⌊v⌋+2) & f(⌊w⌋+2,⌊v⌋+2) \end{bmatrix} \\ C=\begin{bmatrix} W( w-(⌊w⌋-1) ) \\ W(w-⌊w⌋) \\ W( (⌊w⌋+1)-w ) \\ W( (⌊w⌋+2)-w ) \end{bmatrix} \]

即: \[ f(y,x)=f(w,v)= \sum\limits_{row=-1}^2\sum\limits_{col=-1}^2f(⌊w⌋+row,⌊v⌋+col)W(row-(w-⌊w⌋))W(col-(v-⌊v⌋)) \] 另附:网上也有人中间那个矩阵 \(B\) 是本文中间矩阵 \(B\) 的转置,经过下文实践,感觉效果差不多,但从理论上来说,应该本文这样写才是对的吧🤔。

Lanczos 插值

  Lanczos 插值和双三次插值本质上差不多,不同的是插值权重基函数不一样,Lanczos 插值算法的基函数为: \[ L(x) = \begin{cases} 1 & \text{for } x = 0, \\ a*sin(\pi x)sin(\pi x/a)/(\pi x)^2 & \text{for } 0 < |x| \leq a, \\ 0 & \text{otherwise}, \end{cases} \] 其中 a 代表插值窗口半径,若 a = 2,则代表取周围 4×4 邻域像素点进行插值,和双三次插值除权重不一样外其它都一样,此时适用于对图像下采样(缩小),若 a = 3,则代表取周围 9×9 邻域像素点进行插值,此时适用于对图像上采样(放大),OpenCV 的 INTER_LANCZOS4 插值参数就代表取周围 8×8 邻域的像素点进行插值。

实践篇

  本次实践采用 Matlab R2016b,具体 matlab 实现代码为:

1
2
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
clc;clear;close all;

img = imread('lena_gray.jpg'); % 读取图像
[R, C] = size(img); % 获取图像大小

theta = 45 * pi / 180.0; % 旋转角度
H = ceil(abs(R*cos(theta)) + abs(C*sin(theta))); % 变换后图像的高度
W = ceil(abs(C*cos(theta)) + abs(R*sin(theta))); % 变换后图像的宽度
res = zeros(H, W); % 构造结果矩阵。每个像素点默认初始化为0(黑色)

T1 = [1 0 0; 0 -1 0; -0.5*C 0.5*R 1]; % 将原图像坐标映射到数学笛卡尔坐标
T2 = [cos(theta) -sin(theta) 0; sin(theta) cos(theta) 0; 0 0 1]; % 数学笛卡尔坐标下顺时针旋转的变换矩阵
T3 = [1 0 0; 0 -1 0; 0.5*W 0.5*H 1]; % 将数学笛卡尔坐标映射到旋转后的图像坐标
T = T1*T2*T3;
inv_T = inv(T); % 求逆矩阵
% inv_T = [cos(theta) -sin(theta) 0; sin(theta) cos(theta) 0; -0.5*W*cos(theta)-0.5*H*sin(theta)+0.5*C 0.5*W*sin(theta)-0.5*H*cos(theta)+0.5*R 1];

for y = 1 : H % 变换后图像的纵坐标,行,高
for x = 1 : W % 变换后图像的横坐标,列,宽
original_coordinate = [x y 1] * inv_T; % 矩阵乘法
v = original_coordinate(1); % 原图像的横坐标,列,宽
w = original_coordinate(2); % 原图像的纵坐标,行,高
% 变换后的位置判断是否越界
if v>=1 && w>=1 && v<=C && w<=R
res(y, x) = img(round(w), round(v)); % 用原图像对应坐标的像素值填充变换后的图像(最邻近插值)

% ------------- 双线性插值(bilinear interpolation)-----------------
left = floor(v); right = ceil(v); top = floor(w); bottom = ceil(w);
dC = v-left; % 列偏差
dR = w-top; % 行偏差
res(y, x) = (1-dC)*(1-dR)*img(top, left) + dC*(1-dR)*img(top,right) + (1-dC)*dR*img(bottom, left) + dC*dR*img(bottom, right);

% ------------- 双三次插值(bicubic interpolation) -------------------------
if left>=2 && top>=2 && left<=(C-2) && top<=(R-2)
img = double(img);
MA = [bicubic(1+dC) bicubic(dC) bicubic(1-dC) bicubic(2-dC)];
MB = [img(top-1,left-1) img(top,left-1) img(top+1,left-1) img(top+2,left-1);
img(top-1,left) img(top,left) img(top+1,left) img(top+2,left);
img(top-1,left+1) img(top,left+1) img(top+1,left+1) img(top+2,left+1);
img(top-1,left+2) img(top,left+2) img(top+1,left+2) img(top+2,left+2)];
% MB = MB'; % 求转置矩阵
MC = [bicubic(1+dR); bicubic(dR); bicubic(1-dR); bicubic(2-dR)];
res(y, x) = MA*MB*MC;
end
end
end
end;

figure, imshow(uint8(res)); % 显示图像

BiCubic 基函数 Matlab 代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function W = bicubic(x)
%bicubic 双三次插值基函数
a = -1; % 默认取a为-1
x1 = abs(x);
x2 = x1*x1;
x3 = x1*x2;

if x1 <= 1
W = 1 - (a+3)*x2 + (a+2)*x3;
elseif x1>1 && x1<=2
W = -4*a + 8*a*x1 - 5*a*x2 + a*x3;
else
W = 0;
end

  旋转变换中感觉插值的作用没体现出来,以肉眼来看感觉三种插值方法的效果差不多,可能是 Shaun 选取的示例不好,为了体现插值效果,应该采用尺度变换(缩放变换)的。以上代码改为尺度变换也简单,自定义图像缩放后的宽高,以两倍为例,H = R * 2; W = C * 2;,再将旋转变换矩阵改为尺度变换矩阵,尺度变换矩阵中 \(c_x=W/C;c_y=H/R\)

为了便于理解,Shaun 对代码就不进行优化了(其实是你懒吧 _(:з」∠)_)。

后记

  本文算是数字图像处理中最基础的知识了,但 Shaun 在写时还是查阅了大量相关的资料,有些地方理解的还不是很透彻,行文思路有点混乱 ╮(╯▽╰)╭。本来是不想使用图片的,但本文不用图片很难理解清楚,又为了不使用外部图,最后只得参考 SVG 教程如何创建SVG箭头和polymarker——marker元素 采用 SVG 绘制相应图片了。等有时间再把用 OpenCV 实现的 C++ 代码也贴上吧。最后再感叹一下 Matlab 确实是做科研的好工具(°Д°)Ъ,吐槽一下 MathJax 排版好痛苦啊,太多需要转义符\的地方了吧。,搞错了 Σ(゚д゚;),这主要和 markdown 渲染有关,hexo 默认的 markdown 渲染插件 hexo-renderer-marked 太普通了,有些东西根本没办法渲染或者渲染有问题 (╯‵□′)╯︵┴─┴,Shaun 最后决定使用 hexo-renderer-pandoc 插件渲染 markdown,这样就完美了 (๑•̀ㅂ•́)و✧ 。

  至于具体怎么使用 hexo-renderer-pandoc 替换默认的渲染器可参考:如何禁止 hexo 在 html 代码里插入<br>标签?。具体如下:

1
2
3
4
5
# 1、安装 Pandoc,可以不顺带安装 MiKTex
# 2、卸载默认渲染器
npm uninstall hexo-renderer-marked --save
# 3、安装 hexo-renderer-pandoc
npm install hexo-renderer-pandoc --save

附录

  挖的坑总是要填的,呐,这就是用 OpenCV 实现的旋转变换,实现语言为 C++ :

1
2
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
#include <opencv2/opencv.hpp>

#define M_PI 3.14159265358979323846

double bicubic(double x)
{
// bicubic 双三次插值基函数
int a = -1; // 默认取a为 - 1
double x1 = fabs(x);
double x2 = x1*x1;
double x3 = x1*x2;

if (x1 <= 1)
{
return 1 - (a + 3)*x2 + (a + 2)*x3;
}
else if (x1 > 1 && x1 <= 2)
{
return -4 * a + 8 * a*x1 - 5 * a*x2 + a*x3;
}
else
{
return 0;
}
}

int main(int argc, char *argv[])
{
cv::Mat img = cv::imread("../Data/lena_gray.jpg", 0); // 以灰度模式读取图片

int R = img.rows; // 获取原图像高度
int C = img.cols; // 获取原图像宽度

double theta = 45 * M_PI / 180.0; // 旋转角度

int H = ceil(fabs(R*cos(theta)) + fabs(C*sin(theta))); // 变换后图像的高度
int W = ceil(fabs(C*cos(theta)) + fabs(R*sin(theta))); // 变换后图像的宽度

cv::Mat res = cv::Mat::zeros(H, W, CV_8UC1); // 构造结果矩阵。每个像素点默认初始化为0(黑色)

cv::Mat T1 = (cv::Mat_<double>(3, 3) << 1, 0, 0, 0, -1, 0, -0.5*C, 0.5*R, 1); // 将原图像坐标映射到数学笛卡尔坐标
cv::Mat T2 = (cv::Mat_<double>(3, 3) << cos(theta), -sin(theta), 0, sin(theta), cos(theta), 0, 0, 0, 1); // 数学笛卡尔坐标下顺时针旋转的变换矩阵
double t3[3][3] = { { 1, 0, 0 }, { 0, -1, 0 }, { 0.5*W, 0.5*H, 1 } }; // 将数学笛卡尔坐标映射到旋转后的图像坐标
cv::Mat T3 = cv::Mat(3, 3, CV_64FC1, t3);
cv::Mat T = T1*T2*T3;
cv::Mat inv_T = T.inv(); // 求逆矩阵
//cv::Mat inv_T = (cv::Mat_<double>(3, 3) << cos(theta), -sin(theta), 0, sin(theta), cos(theta), 0, -0.5*W*cos(theta) - 0.5*H*sin(theta) + 0.5*C, 0.5*W*sin(theta) - 0.5*H*cos(theta) + 0.5*R, 1);

for (int y = 0; y < H; y++)
{
for (int x = 0; x < W; x++)
{
cv::Mat point = (cv::Mat_<double>(1, 3) << x, y, 1);
cv::Mat original_coordinate = point * inv_T; // 矩阵乘法
double v = original_coordinate.at<double>(0, 0); // 原图像的横坐标,列,宽
double w = original_coordinate.at<double>(0, 1); // 原图像的纵坐标,行,高

// 变换后的位置判断是否越界
if (v >= 0 && w >= 0 && v <= C - 1 && w <= R - 1)
{
res.at<uchar>(y, x) = img.at<uchar>(round(w), round(v)); // 用原图像对应坐标的像素值填充变换后的图像(最邻近插值)

// ------------ - 双线性插值(bilinear interpolation)---------------- -
int left = floor(v), right = ceil(v), top = floor(w), bottom = ceil(w);
double dC = v - left; // 列偏差
double dR = w - top; // 行偏差
res.at<uchar>(y, x) = (1 - dC)*(1 - dR)*img.at<uchar>(top, left) + dC*(1 - dR)*img.at<uchar>(top, right) + (1 - dC)*dR*img.at<uchar>(bottom, left) + dC*dR*img.at<uchar>(bottom, right);

// ------------ - 双三次插值(bicubic interpolation)------------------------ -
if (left >= 1 && top >= 1 && left <= (C - 3) && top <= (R - 3))
{
cv::Mat MA = (cv::Mat_<double>(1, 4) << bicubic(1 + dC), bicubic(dC), bicubic(1 - dC), bicubic(2 - dC));
cv::Mat MB = img(cv::Rect(left - 1, top - 1, 4, 4)); // 提取当前相邻区域16个像素点做插值
MB.convertTo(MB, CV_64FC1); // 变换为浮点型数据
MB = MB.t(); // 求转置矩阵
cv::Mat MC = (cv::Mat_<double>(4, 1) << bicubic(1 + dR), bicubic(dR), bicubic(1 - dR), bicubic(2 - dR));
cv::Mat result = MA*MB*MC;
res.at<uchar>(y, x) = static_cast<uchar>(result.at<double>(0, 0));
}
}
}
}

cv::imshow("result", res); // 显示变换后图像
cv::waitKey(0);

return 0;
}

  以上 C++ 代码在 VS2013 下能完美运行,不管是用 OpenCV-2.4.11 还是 OpenCV-3.2.0。其实完全理解的话,不管用什么工具都能实现,只是看哪个工具方便一点而已,就这个而言,感觉 Matlab 要方便很多,Shaun 就不继续挖 Python 的坑了,毕竟如果要用 Python 实现其实还是用 OpenCV,只是用 OpenCV Python 版的接口而已。

参考资料

[1] 第4章 图像几何变换

[2] 图像旋转原理及实现

[3] 仿射变换http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/tutorials.html

[4] 图像处理常用插值方法总结

[5] Wikipedia Bilinear interpolation

[6] 双线性插值算法的详细总结

[7] Wikipedia Bicubic interpolation

[8] 双三次插值(bicubic interpolation)原理及MATLAB源码实现

[9] 图像缩放】双立方(三次)卷积插值https://dailc.github.io/blog/tags.html#%E6%8F%92%E5%80%BC%E7%AE%97%E6%B3%95


  1. 为啥不说前向映射呢?这是因为若原图像坐标 \((v,w)\) 通过前向映射方式得到变换后图像的坐标 \((x,y)\) ,而且这个坐标为小数的话,一般采用四舍五入的方式得到变换后图像对应的整数坐标 \((⌊x+0.5⌋,⌊y+0.5⌋)\),令 \(f(⌊y+0.5⌋,⌊x+0.5⌋)=f(w,v)\)↩︎

  2. \(⌊x⌋\) 表示向下取整,称为 Floor,指的是小于或等于 \(x\) 的最大整数;\(⌈x⌉\) 表示向上取整,称为 Ceil,指的是大于或等于 \(x\) 的最小整数,\(eg:⌊5.6⌋ = 5,⌊-5.6⌋ = -6;⌈5.6⌉ = 6,⌈-5.6⌉ = -5。\)↩︎

本人常用小工具安利

前言

  由于 Shaun 目前使用最多的是 Windows 系统,所以以下推荐的软件基本都是 Windows 下的软件,全凭 Shaun 主观感觉推荐,也算是个人备份吧。

  首先推荐的自然是装机软件 ULTRAISO,下个绿色版就好,只有 1M 多一点 或者 Rufus 好像也不错。下载 Windows 镜像的地方推荐为:MSDN,I Tell You。接下来就是正式的软件推荐篇了:

前言

  由于 Shaun 目前使用最多的是 Windows 系统,所以以下推荐的软件基本都是 Windows 下的软件,全凭 Shaun 主观感觉推荐,也算是个人备份吧。

  首先推荐的自然是装机软件 ULTRAISO,下个绿色版就好,只有 1M 多一点 或者 Rufus 好像也不错。下载 Windows 镜像的地方推荐为:MSDN,I Tell You。接下来就是正式的软件推荐篇了:

大众篇

  首先推荐浏览器:首选的自然是 ChromeFirefox,还有一个比较偏门的是 Tor Browser(如果在一些特殊时段,各种番(fang)茄(qiang)工具都失效的情况下,这个可以临时用用)。推荐完浏览器,自然也要玩浏览器,

这里极为推荐的是脚本管理插件:Tampermonkey(支持多种浏览器),有 Tampermonkey 和没 Tampermonkey 是两种浏览体验,会用 Tampermonkey 和没用 Tampermonkey 的是两个世界的人(ᖗ乛◡乛ᖘ),Tampermonkey 是通过 Javascript 改变浏览器的,顺便推荐几个常用的脚本:1、护眼脚本;2、Super_preloaderPlus_one;3、解除百度云大文件下载限制(这个配合 IDM 食用效果最佳);4、贴吧全能助手;5、破解VIP会员视频集合;6、Cat Mouse Translation;7、解除B站区域限制;8、bye-flash-hello-html5 | 再见flash 你好html5;9、网页限制解除(改)。再顺便说一下这两个浏览器中一些比较好用的插件和好看的主题吧。

Firefox 最新版的插件 Shaun 目前还没发现几个好用的,但是 Adblock Plus 绝对是必须要装的,Firefox 的主题 Shaun 目前在用为 Blue space 2

而 Chrome 中好用的插件就有很多了,首先自然也是 Adblock Plus,有一个 AutoPagerize 插件可以替代 Super_preloaderPlus_one 脚本,还有一个 划词翻译 可以替换 Cat Mouse Translation 脚本,由于 Chrome 没有撤销关闭的标签按钮,只能通过快捷键 Crtl+Shift+T 操作,对于 Shaun 这种习惯用 Firefox 恢复按钮的人来说这很不人性化,所以只能使用 SimpleUndoClose 插件来代替了,当然番茄之所以推荐使用 Chrome 有很大一部分原因在于 Chrome 中有一代理神器 Proxy SwitchyOmega,好像也正在开发 Firefox 版的 Proxy SwitchyOmega,已经在 Firefox 测试版上使用了,Chrome 的主题 Shaun 目前在用的为 Blue Space Sunset Chrome Theme ,浏览器相关的东西就推荐到这里了,接下来推荐下载工具吧。

附浏览器使用小技巧,有时点击链接不会新建标签页,只是在原标签页刷新,这不符合国人的使用习惯,这时可按住 Crtl 键再点击链接,这时会强制使用新标签页打开链接。

  下载工具首推的自然是 IDM,全称是:Internet Download Manager;种子和磁力链接的下载工具推荐 μtorrent 或者 BitTorrent,好像还有一个 qBittorrent ;至于迅雷,勉强推荐个极速版吧,最后一版为 ThunderSpeed1.0.35.366。还有一款免费的下载工具 Free Download Manager 也还不错,可以一定程度上替代 IDM。

  解压缩工具首推的是 Bandizip(ta家的图片浏览器 Honeyview 也还不错),其次 7-Zip,最后是 WinRAR(主要是最近版本的广告太恼火,好像 5.2 版本没广告),BTW:IDM 下载百度云的大文件可能会造成部分文件损坏,这时可能需要 WinRAR 的修复工具去修复受损的压缩文件(具体操作为用 WinRAR 打开损坏的压缩文件,选中菜单栏的“工具”==》“修复压缩文件”),才能解压出正常文件。※附:

其实 WinRAR 官方也有无广告版的,只是下载链接被隐藏,这位大佬:武文隹山发现了,具体可参考:

  1. WinRAR官方不带弹窗广告的简体中文版,其隐藏的链接为:

    WinRAR5.4官方无广告简体中文版64位 下载链接

    http://www.win-rar.com/fileadmin/winrar-versions/sc20160819/wrr/winrar-x64-540sc.exe

    WinRAR5.4官方无广告简体中文版32位 下载链接

    http://www.win-rar.com/fileadmin/winrar-versions/sc20160819/wrr/winrar-x32-540sc.exe

  2. WinRAR5.5官方不带弹窗广告的简体中文版,具体链接为:

    WinRAR v5.50 简体中文官方版(试用版,注册后没有广告弹窗!)

    32位:http://www.win-rar.com/fileadmin/winrar-versions/sc20170830/wrr/wrar550sc.exe

    64位:http://www.win-rar.com/fileadmin/winrar-versions/sc20170830/wrr/winrar-x64-550sc.exe

    列位看官应该从下载链接中发现了其中的规律(๑乛◡乛๑)。

  拼音输入法,Shaun 使用的是 搜狗拼音智慧版,搜狗拼音的皮肤 Shaun 选择 雨后莲色,不过有人说 Win10 自带的微软拼音也还行。

  至于清理垃圾的可以用 Advanced SystemCare(ASC),也可以用 CCleaner ,至于 Win10 不需要装杀毒软件,国内的的什么 360 全家桶、百度全家桶、腾讯全家桶(诶,好像就差阿里全家桶了,什么时候阿里再来一个,就装个 BAT 全家桶 (๑乛◡乛๑))可以丢了 ((╯°□°)╯︵ ┻━┻)。

  QQ 还是用 TIM 版吧。

  播放器推荐的是 Potplayer。不过有一款解码工具叫 终极解码,可以充当播放器(很多人确实把它当播放器用,比如 Shaun (๑乛◡乛๑))。

  音乐软件 Shaun 用 网易云音乐(等升到 10 级就把它卸了,只听本地音乐)。

  PDF 阅读器还是推荐 Adobe Acrobat_DC,毕竟能和 Office 联合使用,有时 Word 转 PDF 需要加密就靠它了。

  截图工具推荐 FastStone CaptureFSCapture),国人的 Snipaste 也非常不错。

小众篇

  远程控制工具当然推荐 TeamViewer

  文件管理相关强推 搜索工具 Everything 和文件资源管理器(即 Windows 快捷键「Win+E」打开的「我的电脑」)增强工具 Listary (有个开源实现的同类产品 Wox ,还有国产的 火萤酱 也很不错,而且功能更全且附带一些实用小工具),这两个工具有重叠的地方,如果硬要只装一个的话推荐后者。还有一个文件管理器增强工具 QTTabBar ,可以为文件管理器增加标签页,类似于浏览器标签页那种,当然还有其它一些功能。当然 Windows 下最强的文件管理器当属 Total Commander,只是学习成本略高。

  快捷键程序快速启动工具 RunAny ,修改配置文件可以通过快捷键快速启动任意程序,以任意程序打开文件,该工具集成 Everything 和支持 AutoHotkey 热键格式。

  Office Tabs 给 Microsoft Office 添加标签页界面,类似于浏览器标签页那种,以实现文档快速切换。

  Moo0 System Monitor 系统监视器,可视化监控内存 / 磁盘 / 网络 / CPU 等运行状态。

再推荐一些程序员的工具吧。

  首先当然是编辑器(可别和编译器搞混了),Shaun 就不加入 Vim 和 Emacs 的党争了,就直接推荐 VS Code吧(巨硬出品,必属精品(^_^))。

  Windows 下的命令行没一个好用的,要真矮子里面挑高个的话,只能推荐 Git Bash 了,其实以前有个 Babun 也挺好用的,可惜早已停止更新。但是 Git Bash 有时输出中文会乱码,Windows 下真正的命令行神器是 Cmder,完整包直接集成 Git for Windows,也就是包含 Git Bash,mini 包只包含基本命令行工具,关于 Cmder 的一些设置和用法可参考 Win下必备神器之Cmder,解压之后为了方便通过右键菜单使用 Cmder,需要在配置好之后以管理员方式执行 Cmder.exe /REGISTER ALL 命令。

  Markdown 编辑器推荐的是 Typora,不过现在还是 Beta 版,也还能使用,期待正式版,希望到时即使收费的话也能继续推出一个免费版。(推出正式版看看效果怎么样吧,如果真的很不错就去支持一下,如果改进不大的话我还是老老实实继续用 beta 版吧)。

  在 VS 下写 C++ 自然少不了 Visual Assist X 这款插件,用 OpenCV 的自然少不了 ImageWatchVS2017版ImageWatch )这款神器。

  编程的字体 Shaun 目前在用是 Arial monospaced for SAP(优化版)http://www.vimer.cn/archives/396.html/comment-page-1),背景颜色使用护眼色:R: 204,G: 232,B: 207。

  代理工具:Shaun 使用的是 XX-NetLantern,非特殊情况还是很好用的,还有比较推荐的是 赛风

偏门篇

  鼠标手势软件 WGestures,可以利用鼠标手势做一些前进后退,复制粘贴搜索等简单操作。

  Tickeys,让打字发出音效。

  Windows 系统优(mei)化工具 Dism++ ,桌面美化工具可以用雨滴桌面 Rainmeter,或软媒魔方绿色版※注:软媒魔方一定要是绿色版),桌面美化工具需要占用一定的配置,配置不高的老电脑还是别用比较好。

后记

如果以后碰到更多有意思的小东西再和大家分享吧 ↖(^ω^)↗。

解决无法打开某个网页问题

前言

  Shaun 最近在某台 Win10 的电脑中打开网页 https://www.typora.io/ 时出现了问题,一直出现无法连接现象。

前言

  Shaun 最近在某台 Win10 的电脑中打开网页 https://www.typora.io/ 时出现了问题,一直出现无法连接现象。

问题篇

Chrome 中出现:

未连接到互联网

请试试以下办法:

DNS_PROBE_FINISHED_NO_INTERNET

Firefox 中出现:

我们无法连接至 www.typora.io 的服务器。 如果确定此网址正确,您可以尝试:

  • 过会儿再重试。
  • 检查您的网络连接是否正常。
  • 如果您部署有网络防火墙,请检查 Firefox 是否已被授权访问网络。

手机和其它设备在同一网络下能正常连接,打开 host 文件也没发现域名被劫持的情况,挂代理也能连接上。

解决方案篇

Shaun 尝试过的方法:

  1. 刷新 DNS 缓存:在命令行界面中输入 ipconfig /flushdns,无效;

  2. 改 DNS 服务器:把电脑的 dns 修改为首选 8.8.8.8,备用 114.114.114.114,和将首选改成 8.8.4.4 均无效;

  3. Disable Path MTU discovery,具体操作方法为:

    单击“开始”,单击“运行”,键入 regedit,然后单击“确定”。

    在注册表中找到下面的项: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters

    找到EnablePMTUDiscovery,将其值改为 0,如果没找到,则在“编辑”菜单上,指向“新建”,然后单击“DWORD 值”,键入 EnablePMTUDiscovery,然后按 Enter,在“编辑”菜单上,单击“修改”,在“数值数据”框中,键入 0,然后单击“确定”。

    退出注册表编辑器,然后重新启动计算机。

  4. 设置 MTU 值,将其调小,完美解决。具体操作方法为:以管理员身份运行命令提示符,在命令行界面输入 netsh interface ipv4 show subinterfaces,查看传入字节和传出字节的接口,修改对应接口的 MTU 的值,具体命令为:netsh interface ipv4 set subinterface "对应接口名" mtu=值 store=persistent,其中 对应接口名 需要替换成相应修改的东西。

后记

  将 MTU 值调小,可能会造成网速变慢,但 Shaun 又无法去改变其它的东西,既然不能改变其它,只能改变自己喽 ╮(╯﹏╰)╭。但 Shaun 这里觉得奇怪的是:别人的电脑设置默认 MTU 的值为 1500 也能访问啊,无奈 (╯‵□′)╯︵┴─┴。

参考资料

[1] 电脑上部分网页打不开,但是手机可以,如何解决?

[2] mtu值怎样设置才网速最快

PyQt5使用小结

本文所用的 Python 版本为 python-3.6.2,PyQt5 版本为 pyqt5-5.9.1,OpenCV 版本为 opencv-python-3.3.0.10 和 opencv-contrib-python-3.3.0.10,TensorFlow 版本为 tensorflow-1.4.0,编程语言为 python3,系统环境为 Windows 10。

前言

  本文是上一篇(TensorFlow Object Detection API使用小结)的后续,因为那个 project 还需要一个界面,所以 Shaun 使用 PyQt 做了这么个界面,其中借用 OpenCV 的图像数据显示。

本文所用的 Python 版本为 python-3.6.2,PyQt5 版本为 pyqt5-5.9.1,OpenCV 版本为 opencv-python-3.3.0.10 和 opencv-contrib-python-3.3.0.10,TensorFlow 版本为 tensorflow-1.4.0,编程语言为 python3,系统环境为 Windows 10。

前言

  本文是上一篇(TensorFlow Object Detection API使用小结)的后续,因为那个 project 还需要一个界面,所以 Shaun 使用 PyQt 做了这么个界面,其中借用 OpenCV 的图像数据显示。

准备篇

  首先使用 pip 安装所需库,由于上一篇已经安装了 tensorflow,所以本文其实只需要安装 pyqt5 和 opencv-python 就可以了,安装 pyqt5 指令为:pip install pyqt5,相关依赖关系解决办法在上一篇中已提到,这里不再赘述,然后再使用指令 pip install opencv-python 安装 opencv,这里 Shaun 发现在 python 中配置 OpenCV 简直不要太轻爽 O(∩_∩)O~~,就一个 pip 就解决了,哪有 C++ 那么麻烦,以后可能还会继续使用 python 版的 opencv,所以就顺便把它 python 版的扩展包也顺便一起装上,安装指令为:pip install opencv-contrib-python。至此所需环境库安装完毕。

  ※注:相对于上文中使用 pip 在线安装的方式,还有另一种使用 pip 进行离线安装的方式,在 Unofficial Windows Binaries for Python Extension Packages上下载离线包,即 XXXXXX.whl 文件,文件名一般包含库名称和对应版本、python 版本以及是 64 位还是 32 位的等信息,这里以离线包 numpy-1.13+mkl 为例,首先下载适合自己的库版本,适合 Shaun 的当然是 numpy-1.13.3+mkl-cp36-cp36m-win_amd64.whl(这适合 64 位的 python3.6 安装),将命令行目录切换至 numpy-1.13.3+mkl-cp36-cp36m-win_amd64.whl 文件所在目录,输入指令 pip install numpy-1.13.3+mkl-cp36-cp36m-win_amd64.whl 即可离线安装 numpy-1.13+mkl 库。相比在线安装,这种离线安装更加灵活,而且能够安装一些在线安装无法安装的库,像上文中的 numpy-1.13+mkl 库只能采取离线安装的方式,在线安装只能安装不带 mkl 的 numpy 库。采用离线安装方式也可以直接安装带扩展包的 opencv-python库:opencv_python‑3.3.1+contrib‑cp36‑cp36m‑win_amd64.whl ,不需要像在线安装那样装两个库。

实践篇

  以前有用过 Qt 的基础,所以这次使用 PyQt5 感觉上手很快,毕竟这里面的语法有很多是相通的,再加上网上的资料也有很多,所以很快就做了个简陋的界面。不过直接用代码控制界面的布局确实很麻烦,每改下布局都要重新运行一下看看,而且启动时间还有点长 ╮(╯﹏╰)╭。网上有种说法是:

可以先通过QtDesigner设计UI,然后通过Qt提供的命令行工具pyuic5将.ui文件转换成python代码,具体用法是:若ui文件名称为firstPyQt5.ui,则在命令行界面中输入指令:pyuic5 -o firstPyQt5.py firstPyQt5.ui,即可将firstPyQt5.ui文件转换成python代码文件firstPyQt5.py

不过 Shaun 这里由于界面比较简陋,没有几个控件,所以就直接将其用 python 代码控制了,没去尝试这个命令行工具 pyuic5 了,下次有机会再尝试吧 ↖(^ω^)↗。

  Shaun 做的这个小界面实现的功能是:1、可以选择已经训练好的模型来检测选定图片中的目标;2、可以播放选定的视频;3、还有打开摄像头,显示摄像头拍摄的视频。

其中由于 Shaun 电脑无法实时检测目标,所以在视频和摄像头拍摄中就没有添加检测的代码,只有选择图片时才会执行检测功能,有需要的童靴可以自行添加(•̀ᴗ•́)。

附完整代码如下:

1
2
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
import os
import sys

from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *

import numpy as np
import cv2

import tensorflow as tf
from object_detection.utils import label_map_util
from object_detection.utils import visualization_utils as vis_util

class Detector(object):
def __init__(self):
self.PATH_TO_CKPT = './model/hand_model_faster_rcnn_resnet101.pb' # 选择模型文件
self.PATH_TO_LABELS = r'./model/hands_label_map.pbtxt' # 选择类别标签文件
self.NUM_CLASSES = 1
self.detection_graph = self._load_model() # 加载模型
self.category_index = self._load_label_map()

def _load_model(self):
detection_graph = tf.Graph()
with detection_graph.as_default():
od_graph_def = tf.GraphDef()
with tf.gfile.GFile(self.PATH_TO_CKPT, 'rb') as fid:
serialized_graph = fid.read()
od_graph_def.ParseFromString(serialized_graph)
tf.import_graph_def(od_graph_def, name='')
return detection_graph

def _load_label_map(self):
label_map = label_map_util.load_labelmap(self.PATH_TO_LABELS)
categories = label_map_util.convert_label_map_to_categories(label_map,
max_num_classes=self.NUM_CLASSES,
use_display_name=True)
category_index = label_map_util.create_category_index(categories)
return category_index

def detect(self, image):
with self.detection_graph.as_default():
with tf.Session(graph=self.detection_graph) as sess:
# Expand dimensions since the model expects images to have shape: [1, None, None, 3]
image_np_expanded = np.expand_dims(image, axis=0)
image_tensor = self.detection_graph.get_tensor_by_name('image_tensor:0')
boxes = self.detection_graph.get_tensor_by_name('detection_boxes:0')
scores = self.detection_graph.get_tensor_by_name('detection_scores:0')
classes = self.detection_graph.get_tensor_by_name('detection_classes:0')
num_detections = self.detection_graph.get_tensor_by_name('num_detections:0')
# Actual detection.
(boxes, scores, classes, num_detections) = sess.run(
[boxes, scores, classes, num_detections],
feed_dict={image_tensor: image_np_expanded})
# Visualization of the results of a detection.
vis_util.visualize_boxes_and_labels_on_image_array(
image,
np.squeeze(boxes),
np.squeeze(classes).astype(np.int32),
np.squeeze(scores),
self.category_index,
use_normalized_coordinates=True,
line_thickness=8)

return image



class DetectUI(QWidget):

def __init__(self):
super().__init__()

self.initUI()
self.detector = Detector()
self.cap = cv2.VideoCapture()

def initUI(self):
self.timer = QTimer(self) # 初始化一个定时器
self.timer.timeout.connect(self.showFrame) # 计时结束调用showFrame()方法

self.show_pic_label = QLabel(self)
self.show_pic_label.resize(640, 480)
self.show_pic_label.move(10, 10)
self.show_pic_label.setStyleSheet("border-width: 1px; border-style: solid; border-color: rgb(255, 170, 0);")

self.show_filename_lineEdit = QLineEdit(self)
self.show_filename_lineEdit.resize(200, 22)
self.show_filename_lineEdit.move(10, 500)

self.select_img_btn = QPushButton('Select File', self)
self.select_img_btn.clicked.connect(self.selectImg)
self.select_img_btn.resize(self.select_img_btn.sizeHint())
self.select_img_btn.move(218, 500)

self.open_camera_btn = QPushButton('Open Camera', self)
self.open_camera_btn.clicked.connect(self.openCamera)
self.open_camera_btn.resize(self.open_camera_btn.sizeHint())
self.open_camera_btn.move(292, 500)

self.select_model_btn = QPushButton('Select Model', self)
self.select_model_btn.clicked.connect(self.selectModel)
self.select_model_btn.resize(self.select_model_btn.sizeHint())
self.select_model_btn.move(366, 500)

self.show_modelname_lineEdit = QLineEdit(self)
self.show_modelname_lineEdit.setText('hand_model_faster_rcnn_resnet101.pb')
self.show_modelname_lineEdit.resize(200, 22)
self.show_modelname_lineEdit.move(450, 500)

self.setGeometry(200, 100, 660, 530)
self.setWindowTitle('Hand Detector')
self.show()


def showImg(self, src_img, qlabel):
src_img = cv2.cvtColor(src_img, cv2.COLOR_BGR2RGB)

# src_img = self.detector.detect(src_img) # 检测目标

height, width, bytesPerComponent = src_img.shape
bytesPerLine = bytesPerComponent * width
# 转为QImage对象
q_image = QImage(src_img.data, width, height, bytesPerLine, QImage.Format_RGB888)
qlabel.setPixmap(QPixmap.fromImage(q_image).scaled(qlabel.width(), qlabel.height()))


def showFrame(self):
if(self.cap.isOpened()):
ret, frame = self.cap.read()
if ret:
self.showImg(frame, self.show_pic_label)
else:
self.cap.release()
self.timer.stop() # 停止计时器


def selectImg(self):
if self.cap.isOpened():
self.cap.release()

file_name, file_type = QFileDialog.getOpenFileName(self,
"选取文件",
"./",
"Image Files (*.jpg *.png *.bmp *.tif);;Video Files (*.avi *.mp4)") #设置文件扩展名过滤,注意用双分号间隔过滤,用空格分隔多个文件
# print(file_name,file_type)

if file_type.find("Image") >= 0:
if file_name:
self.show_filename_lineEdit.setText(os.path.split(file_name)[1])

img = cv2.imread(file_name, cv2.IMREAD_COLOR)
cv2.cvtColor(img, cv2.COLOR_BGR2RGB, img)

img = self.detector.detect(img) # 检测目标

height, width, bytesPerComponent = img.shape
bytesPerLine = bytesPerComponent * width
# 转为QImage对象
q_image = QImage(img.data, width, height, bytesPerLine, QImage.Format_RGB888)
self.show_pic_label.setPixmap(QPixmap.fromImage(q_image).scaled(self.show_pic_label.width(), self.show_pic_label.height()))

if file_type.find("Video") >= 0:
if file_name:
self.show_filename_lineEdit.setText(os.path.split(file_name)[1])

self.cap.open(file_name)
self.timer.start(30) # 设置时间隔30ms并启动


def openCamera(self):
self.cap.open(0) # 默认打开0号摄像头
self.timer.start(30) # 设置时间隔30ms并启动


def selectModel(self):
model_name, file_type = QFileDialog.getOpenFileName(self,
"选取文件",
"./",
"model Files (*.pb);;All Files (*)") #设置文件扩展名过滤,注意用双分号间隔过滤,用空格分隔多个文件

if model_name:
self.show_modelname_lineEdit.setText(os.path.split(model_name)[1])
self.detector.PATH_TO_CKPT = model_name
self.detector.detection_graph = self.detector._load_model() # 重新加载模型



if __name__ == '__main__':
app = QApplication(sys.argv)
dtcui = DetectUI()
sys.exit(app.exec_())

后记

  初次使用 Python 做一个小东西,其语法确实简洁,不过对于 Shaun 这种习惯用 C++ 的人来说确实还有点不太习惯 (˘•ω•˘)。

参考资料

[1] 用PyQt5+Caffe+Opencv搭建一个人脸识别登录界面

[2] PyQt5学习笔记09----标准文件打开保存框QFileDialog

[3] PyQt5教程——第一个程序(2)http://www.cnblogs.com/archisama/tag/PyQt5/

[4] PyQt5应用与实践

[5] PyQt5系列教程(二)利用QtDesigner设计UI界面http://www.cnblogs.com/tkinter/tag/pyqt5/

[6] OpenCV 3.2.0/OpenCV-Python Tutorials/Gui Features in OpenCV/Getting Started with Images

[7] OpenCV 3.2.0/OpenCV-Python Tutorials/Gui Features in OpenCV/Getting Started with Videos

[8] python3.3 分割路径与文件名 小例

TensorFlow Object Detection API使用小结

本文所用的 Python 版本为 python-3.6.2,TensorFlow 版本为tensorflow-1.4.0,编程语言为 python3,系统环境为 Windows 10。

前言

  很久没写过东西了,主要原因是最近研究生课程开始陆续结课,Shaun 也要忙于应付各种结课时的考试、论文、project 等一堆麻烦事。这不深度学习结课时需要做个 project,Shaun 也顺便将做这个 project 的过程记录下来。

本文所用的 Python 版本为 python-3.6.2,TensorFlow 版本为tensorflow-1.4.0,编程语言为 python3,系统环境为 Windows 10。

前言

  很久没写过东西了,主要原因是最近研究生课程开始陆续结课,Shaun 也要忙于应付各种结课时的考试、论文、project 等一堆麻烦事。这不深度学习结课时需要做个 project,Shaun 也顺便将做这个 project 的过程记录下来。

准备篇

  该 project 主要利用 TensorFlow 中的 Object Detection API 进行训练和检测。在开始使用该 API 之前需要安装配置 Python 环境。

  既然是 Python 首先需要 下载安装Python,安装完之后,为了顺利使用 pip 需要配置环境变量,在 Windows 系统环境变量中 Path 末尾添加:

变量名变量值
Path;C:\Users\admin\AppData\Local\Programs\Python\Python36\; C:\Users\admin\AppData\Local\Programs\Python\Python36\Scripts\

其中 C:\Users\admin\AppData\Local\Programs\Python\Python36 为 python-3.6.2 默认安装目录。

  然后为了方便使用命令行工具,下载安装git,安装方式一路默认即可。

  接下来利用 pip 安装 TensorFlow,鼠标右键桌面空白处,点击“Git Bash Here”,打开 bash 命令行,输入 pip install tensorflow,其中一些依赖关系可能需要手动解决,手动解决的办法就是用 pip install 相关依赖库,这是 CPU 版的 TensorFlow,若要使用 GPU,则需要安装 GPU 版的 TensorFlow,安装命令为:pip install tensorflow-gpu,以同样方式解决依赖关系。由于 Shaun 电脑没 N 卡,所以没安装 GPU 版的 TensorFlow,所以如果想使用 GPU 版的 TensorFlow 请另行尝试。

  然后安装 TensorFlow Object Detection API 依赖库,在命令行中输入:

1
2
3
4
pip install pillow
pip install lxml
pip install jupyter
pip install matplotlib

  因为 tensorflow 并没有默认自带 Object Detection API,所以该 API 需要自行下载,下载地址为:https://github.com/tensorflow/models ,下载之后解压,Shaun 解压目录为:D:\ProgramFiles\PythonLibs\tensorflow,解压完之后需要配置环境目录,在系统环境目录中添加:

变量名变量值
PYTHONPATHD:\ProgramFiles\PythonLibs\tensorflow\models; D:\ProgramFiles\PythonLibs\tensorflow\models\research; D:\ProgramFiles\PythonLibs\tensorflow\models\research\slim;

  下载配置 Object Detection API 完之后需要安装 Protoc,进入 Protoc下载页,下载 protoc-3.4.0-win32.zip,解压之后将 bin 文件夹内的 protoc.exe 拷贝到 C:\windows\system32 目录下(用于将 protoc.exe 所在的目录配置到环境变量当中),当然也可以在系统环境变量 Path 中添加该 bin 文件夹路径。

  最后在命令行中切换目录至:D:\ProgramFiles\PythonLibs\tensorflow\models\research 文件夹,即 object_detection 文件夹所在目录,在命令行中输入:

1
protoc object_detection/protos/*.proto --python_out=.

编译 object_detection/protos 文件夹下的 proto 文件,生成对应的 python 文件。

  至此,Windows 下 TensorFlow中 的 Object Detection API 的使用配置全部完成,至于 Ubuntu 下的配置可参考其官方文档

  至于如何验证,可以在命令行中切换目录至 object_detection,输入:jupyter notebook,稍等一会,浏览器将自动打开 http://localhost:8888/tree jupyter 界面,点击 object_detection_tutorial.ipynb 文件,进入打开的新标签,点击“Cell”中的“Run All”,耐心等待几 ~ 十几分钟(因为它需要下载相应的模型),将会在浏览器下方显示检测结果。

  截止本文完成前,该API公开的有以下几个模型:

Model nameSpeed (ms)COCO mAP1Outputs
ssd_mobilenet_v1_coco3021Boxes
ssd_inception_v2_coco4224Boxes
faster_rcnn_inception_v2_coco5828Boxes
faster_rcnn_resnet50_coco8930Boxes
faster_rcnn_resnet50_lowproposals_coco64Boxes
rfcn_resnet101_coco9230Boxes
faster_rcnn_resnet101_coco10632Boxes
faster_rcnn_resnet101_lowproposals_coco82Boxes
faster_rcnn_inception_resnet_v2_atrous_coco62037Boxes
faster_rcnn_inception_resnet_v2_atrous_lowproposals_coco241Boxes
faster_rcnn_nas183343Boxes
faster_rcnn_nas_lowproposals_coco540Boxes

  根据上述模型可推知,利用该 API 可能只能训练 Faster-RCNN、R-FCN 和 SSD 三种算法的模型。

接下来介绍如何使用该 API 来训练自己的模型进行物体检测。

实践篇

数据准备篇

  既然要训练自己的模型,当然要准备相应的数据,而 TensorFlow 有其独特的输入数据格式 TFRecord,所以通常还要将自己的数据转换成 TFRecord 格式以输入 TensorFlow 中进行训练。以 datitran/raccoon_dataset 数据集为例,该作者在 Google image 上收集了 200 张 Raccoon 图片,用 LabelImg 对这些图片进行标记,并将标记以 PASCAL VOC 格式保存为 xml 文件。作者在文中也提到了另一个图片标记工具 FIAT (Fast Image Data Annotation Tool) 。保存为 PASCAL VOC 格式的 xml 文件之后,可以使用 object_detection 文件夹中的 create_pascal_tf_record.py 文件将数据转化为 TFRecord 格式,用法为:

1
2
3
./create_pascal_tf_record --data_dir=/home/user/VOCdevkit \
--year=VOC2012 \
--output_path=/home/user/pascal.record

当然也可以使用 datitran 作者提供的 xml_to_csv.py 文件将 xml 文件先转化为 csv 文件,再利用 generate_tfrecord.py 文件将 csv 文件转化成 TFRecord 格式文件。

  注意,使用 xml_to_csv.py 和 generate_tfrecord.py 其文件结构应该是这样的:

.
├── annotations
├── generate_tfrecord.py
├── images
└── xml_to_csv.py

2 directories, 2 files

其中 images 文件夹存的是 jpg 图片,annotations 文件夹存的是 xml 标签文件。generate_tfrecord.py 文件中的:

1
2
3
4
5
def class_text_to_int(row_label):
if row_label == 'raccoon':
return 1
else:
None

其中的 raccoon 注意要改成自己的类别标签。如此,数据的问题就解决了。

训练篇

  然后就是正式开始训练了,以 Faster-RCNN 算法为例。首先准备相应的数据,Shaun 准备的数据文件结构如下:

TensorFlowObjectDetectionAPITest
├── data
│  ├── model.ckpt.data-00000-of-00001
│  ├── model.ckpt.index
│  ├── model.ckpt.meta
│  ├── object_label_map.pbtxt
│  ├── test.record
│  └── train.record
├── eval
├── eval.py
├── export_inference_graph.py
├── faster_rcnn_resnet101_coco.config
├── model
├── train
└── train.py

4 directories, 10 files

其中,TensorFlowObjectDetectionAPITest 为项目文件夹,该 project 在此文件夹下运行;

data 文件夹中三个 model.ckpt 文件:model.ckpt.data-00000-of-00001model.ckpt.indexmodel.ckpt.meta 来自 faster_rcnn_resnet101_coco 模型,用来初始化网络参数;

object_label_map.pbtxt 文件内容如下:

item { ​ id: 1 ​ name: 'raccoon' }

将其中的 raccoon 改成自己的类别标签,如果有多个类别标签则可以参考 object_detection\data 文件夹中的 pascal_label_map.pbtxt 文件格式;

test.recordtrain.record 是生成的 TFRecord 数据,分别为待输入的测试数据和训练数据;

eval 文件夹为空文件夹用来输出测试结果;train 文件夹为空文件夹用来输出训练结果(包括checkpoint文件和最终的模型文件);

faster_rcnn_resnet101_coco.config 为配置文件,包括各种参数和输入输出数据的配置,其来自 object_detection\samples\configs 文件夹中 faster_rcnn_resnet101_coco.config 文件,在使用时需对其做如下修改:

  1. 首先是 num_classes,这是待检测的类别数目,如果只要检测一种,则将其值改为 1;

  2. fine_tune_checkpoint: "PATH_TO_BE_CONFIGURED/model.ckpt",将 PATH_TO_BE_CONFIGURED 改为 ./data

  3. ``` train_input_reader: { tf_record_input_reader { input_path: "PATH_TO_BE_CONFIGURED/mscoco_train.record" } label_map_path: "PATH_TO_BE_CONFIGURED/mscoco_label_map.pbtxt" }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    将其中的的 `PATH_TO_BE_CONFIGURED/mscoco_train.record` 改为 `./data/train.record`,将其中的 `PATH_TO_BE_CONFIGURED/mscoco_label_map.pbtxt` 改为 `./data/object_label_map.pbtxt`;

    4. ```
    eval_input_reader: {
    tf_record_input_reader {
    input_path: "PATH_TO_BE_CONFIGURED/mscoco_val.record"
    }
    label_map_path: "PATH_TO_BE_CONFIGURED/mscoco_label_map.pbtxt"
    shuffle: false
    num_readers: 1
    num_epochs: 1
    }

    将其中的的 PATH_TO_BE_CONFIGURED/mscoco_val.record 改为 ./data/test.record,将其中的 PATH_TO_BE_CONFIGURED/mscoco_label_map.pbtxt 改为 ./data/object_label_map.pbtxt

至于其它的参数可以选择默认,不对其进行修改;

train.py 为训练代码,其来自 object_detection/ 文件夹中的 train.py,直接复制出来使用即可,具体用法为:

1
python train.py --logtostderr --train_dir=./train --pipeline_config_path=faster_rcnn_resnet101_coco.config

其在运行过程中会在 train 文件夹生成一系列训练过程文件,比如 checkpoint、model.ckpt-{num}({num} 代表训练过程保存的第几个网络模型,一般从 0 开始,包括 .index、.meta和 .data 三个文件)等文件。

eval.py 为评估代码,其来自 object_detection/ 文件夹中的 eval.py,直接复制出来使用即可,具体用法为:

1
python eval.py --logtostderr --checkpoint_dir=./train --eval_dir=./eval --pipeline_config_path=./faster_rcnn_resnet101_coco.config

其在运行过程中会在 eval 文件夹生成一系列评估文件,每个文件对应一个测试 image。

export_inference_graph.py 为导出 pb 模型代码,其来自 object_detection/ 文件夹中的 export_inference_graph.py,直接复制出来使用即可,具体用法为:

1
python export_inference_graph.py --input_type image_tensor --pipeline_config_path ./faster_rcnn_resnet101_coco.config --trained_checkpoint_prefix ./train/model.ckpt-18298 --output_directory ./model

其中 model.ckpt-18298 表示使用第 18298 次保存的网络模型导出 pb 模型文件,导出的模型文件保存在 model 文件夹,主要有一下几个文件:

- graph.pbtxt

- model.ckpt.data-00000-of-00001

- model.ckpt.info

- model.ckpt.meta

- frozen_inference_graph.pb

其中 frozen_inference_graph.pb 就是训练成功用来检测目标的模型。

  TensorFlow 训练时可以随时查看训练过程,如损失函数的值下降曲线等,所用命令为:在命令行中切换目录至 project 运行目录,即 train.py 所在文件夹,Shaun 这里即 TensorFlowObjectDetectionAPITest 文件夹,输入:tensorboard --logdir=./,等待片刻,在浏览器地址栏输入:http://localhost:6006/,即可看到训练过程曲线。

检测篇

  检测结果使用 opencv 窗口显示(至于 python 中 opencv 的使用详见下一篇(PyQt5使用小结)),具体调用自己训练的模型进行检测的 Python 代码(该代码为 eli 大佬参考 object_detection 文件夹中的 object_detection_tutorial.ipynb(该文件可在 jupyter 中查看)改的)为:

1
2
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
import cv2
import numpy as np
import tensorflow as tf
from object_detection.utils import label_map_util
from object_detection.utils import visualization_utils as vis_util


class Detector(object):
def __init__(self):
self.PATH_TO_CKPT = r'./model/frozen_inference_graph.pb' # 选择模型
self.PATH_TO_LABELS = r'./data/object_label_map.pbtxt' # 选择类别标签文件
self.NUM_CLASSES = 1
self.detection_graph = self._load_model()
self.category_index = self._load_label_map()

def _load_model(self):
detection_graph = tf.Graph()
with detection_graph.as_default():
od_graph_def = tf.GraphDef()
with tf.gfile.GFile(self.PATH_TO_CKPT, 'rb') as fid:
serialized_graph = fid.read()
od_graph_def.ParseFromString(serialized_graph)
tf.import_graph_def(od_graph_def, name='')
return detection_graph

def _load_label_map(self):
label_map = label_map_util.load_labelmap(self.PATH_TO_LABELS)
categories = label_map_util.convert_label_map_to_categories(label_map,
max_num_classes=self.NUM_CLASSES,
use_display_name=True)
category_index = label_map_util.create_category_index(categories)
return category_index

def detect(self, image):
with self.detection_graph.as_default():
with tf.Session(graph=self.detection_graph) as sess:
# Expand dimensions since the model expects images to have shape: [1, None, None, 3]
image_np_expanded = np.expand_dims(image, axis=0)
image_tensor = self.detection_graph.get_tensor_by_name('image_tensor:0')
boxes = self.detection_graph.get_tensor_by_name('detection_boxes:0')
scores = self.detection_graph.get_tensor_by_name('detection_scores:0')
classes = self.detection_graph.get_tensor_by_name('detection_classes:0')
num_detections = self.detection_graph.get_tensor_by_name('num_detections:0')
# Actual detection.
(boxes, scores, classes, num_detections) = sess.run(
[boxes, scores, classes, num_detections],
feed_dict={image_tensor: image_np_expanded})
# Visualization of the results of a detection.
vis_util.visualize_boxes_and_labels_on_image_array(
image,
np.squeeze(boxes),
np.squeeze(classes).astype(np.int32),
np.squeeze(scores),
self.category_index,
use_normalized_coordinates=True,
line_thickness=8)

cv2.namedWindow("detection", cv2.WINDOW_NORMAL)
cv2.imshow("detection", image)
cv2.waitKey(0)

if __name__ == '__main__':
image = cv2.imread('./test_img.jpg') # 选择待检测的图片
detector = Detector()
detector.detect(image)

后记

  经过这次 TensorFlow 训练,感觉深度学习 真tm 吃硬件,费时间,也难怪神经网络理论出来几十年之后才火,当年的硬件根本无法支持这么大的计算量。

附录

最后附上 datitran 作者提供的 xml_to_csv.py 文件源码和 generate_tfrecord.py 文件源码:

xml_to_csv.py 源码如下:

1
2
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
import os
import glob
import pandas as pd
import xml.etree.ElementTree as ET


def xml_to_csv(path):
xml_list = []
for xml_file in glob.glob(path + '/*.xml'):
tree = ET.parse(xml_file)
root = tree.getroot()
for member in root.findall('object'):
value = (root.find('filename').text,
int(root.find('size')[0].text),
int(root.find('size')[1].text),
member[0].text,
int(member[4][0].text),
int(member[4][1].text),
int(member[4][2].text),
int(member[4][3].text)
)
xml_list.append(value)
column_name = ['filename', 'width', 'height', 'class', 'xmin', 'ymin', 'xmax', 'ymax']
xml_df = pd.DataFrame(xml_list, columns=column_name)
return xml_df


def main():
image_path = os.path.join(os.getcwd(), 'annotations')
xml_df = xml_to_csv(image_path)
xml_df.to_csv('raccoon_labels.csv', index=None)
print('Successfully converted xml to csv.')


main()

generate_tfrecord.py 文件源码 如下:

1
2
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
"""
Usage:
# From tensorflow/models/
# Create train data:
python generate_tfrecord.py --csv_input=data/train_labels.csv --output_path=train.record
# Create test data:
python generate_tfrecord.py --csv_input=data/test_labels.csv --output_path=test.record
"""
from __future__ import division
from __future__ import print_function
from __future__ import absolute_import

import os
import io
import pandas as pd
import tensorflow as tf

from PIL import Image
from object_detection.utils import dataset_util
from collections import namedtuple, OrderedDict

flags = tf.app.flags
flags.DEFINE_string('csv_input', '', 'Path to the CSV input')
flags.DEFINE_string('output_path', '', 'Path to output TFRecord')
FLAGS = flags.FLAGS


# TO-DO replace this with label map
def class_text_to_int(row_label):
if row_label == 'raccoon':
return 1
else:
None


def split(df, group):
data = namedtuple('data', ['filename', 'object'])
gb = df.groupby(group)
return [data(filename, gb.get_group(x)) for filename, x in zip(gb.groups.keys(), gb.groups)]


def create_tf_example(group, path):
with tf.gfile.GFile(os.path.join(path, '{}'.format(group.filename)), 'rb') as fid:
encoded_jpg = fid.read()
encoded_jpg_io = io.BytesIO(encoded_jpg)
image = Image.open(encoded_jpg_io)
width, height = image.size

filename = group.filename.encode('utf8')
image_format = b'jpg'
xmins = []
xmaxs = []
ymins = []
ymaxs = []
classes_text = []
classes = []

for index, row in group.object.iterrows():
xmins.append(row['xmin'] / width)
xmaxs.append(row['xmax'] / width)
ymins.append(row['ymin'] / height)
ymaxs.append(row['ymax'] / height)
classes_text.append(row['class'].encode('utf8'))
classes.append(class_text_to_int(row['class']))

tf_example = tf.train.Example(features=tf.train.Features(feature={
'image/height': dataset_util.int64_feature(height),
'image/width': dataset_util.int64_feature(width),
'image/filename': dataset_util.bytes_feature(filename),
'image/source_id': dataset_util.bytes_feature(filename),
'image/encoded': dataset_util.bytes_feature(encoded_jpg),
'image/format': dataset_util.bytes_feature(image_format),
'image/object/bbox/xmin': dataset_util.float_list_feature(xmins),
'image/object/bbox/xmax': dataset_util.float_list_feature(xmaxs),
'image/object/bbox/ymin': dataset_util.float_list_feature(ymins),
'image/object/bbox/ymax': dataset_util.float_list_feature(ymaxs),
'image/object/class/text': dataset_util.bytes_list_feature(classes_text),
'image/object/class/label': dataset_util.int64_list_feature(classes),
}))
return tf_example


def main(_):
writer = tf.python_io.TFRecordWriter(FLAGS.output_path)
path = os.path.join(os.getcwd(), 'images')
examples = pd.read_csv(FLAGS.csv_input)
grouped = split(examples, 'filename')
for group in grouped:
tf_example = create_tf_example(group, path)
writer.write(tf_example.SerializeToString())

writer.close()
output_path = os.path.join(os.getcwd(), FLAGS.output_path)
print('Successfully created the TFRecords: {}'.format(output_path))


if __name__ == '__main__':
tf.app.run()

参考资料

[1] 对于谷歌开源的TensorFlow Object Detection API视频物体识别系统实现教程

[2] TensorFlow学习——Tensorflow Object Detection API(win10,CPU)

[3] How to train your own Object Detector with TensorFlow’s Object Detector API

[4] TensorFlow 之 物体检测http://rensanning.iteye.com/category/374992


  1. See MSCOCO evaluation protocol.↩︎

C语言中整型提升问题

前言

  今天有人问了 Shaun 一个移位的问题,就是下面这段 C 语言代码:

1
2
unsigned short a = 0xffff;
printf("%#hx\n", a << 4 >> 8 << 4);

你认为会输出什么结果? ੧ಡ ⌣ ಡ੭

前言

  今天有人问了 Shaun 一个移位的问题,就是下面这段 C 语言代码:

1
2
unsigned short a = 0xffff;
printf("%#hx\n", a << 4 >> 8 << 4);

你认为会输出什么结果? ੧ಡ ⌣ ಡ੭

解答篇

  正确答案是:0xfff0。恐怕有一部分会像 Shaun 一样觉得答案就是 0xff0 才对,还像模像样的给出对应的说法:看 a 首先向左移四位,即去掉最左边的 f,右边补 4 个 0 变成这样 0xfff0;然后再向右移 8 位,a 将会变成这样 0x00ff;最后向左移四位,得到 0x0ff0,所以应该输出 0xff0但是,正确答案终究是正确答案。之所以会输出正确答案,是因为这里面还有一个整型提升。所谓的整型提升就是:

在一个表达式中,如果int能够表示原始类型中的所有数值,那么这个数值就被转成int型,否则,它被转成unsigned int型。这种规则被称为整型提升。所有其它类型都不会被整型提升改变。

  所以在 a << 4 >> 8 << 4 中,会先将 a 提升为 int 型,即 a 会变成 0x0000ffff,接着向左移四位,a 变成 0x000ffff0,再向右移 8 位,变成 0x00000fff,最后向左移 4 位,变成 0x0000fff0,最后为了输出,再做一个隐式的类型转换(由 int 转 unsigned short),得到 0xfff0,所以最后输出 0xfff0

后记

  这个问题是一个刚入大学的童靴问 Shaun 的,刚问 Shaun 时 Shaun 还没反应过来,后来才想起有整型提升这么回事 o(╯□╰)o。btw,这位童靴主要是想去掉高 4 位和低 4 位只取中间 8 位的值,其实最简单的办法就是直接 a & 0x0ff0,这样管它有没有整型提升,肯定能得到中间 8 位的值 (╯▽╰)。

参考资料

[1] C语言进阶:整型提升http://blog.csdn.net/mishifangxiangdefeng/article/category/1058873

[2] 对 unsigned char 先左移 后右移 可以出现两种结果

TXT数据转OpenCV中的Mat数据

前言

  本文是以前做的一个小东西的处理前奏,当时也记录过,现在把它翻出来重新看看。那个东西需要利用深度图,Shaun 当时还没拿到 Kinect,就在网上下了一些数据http://eeeweba.ntu.edu.sg/computervision/people/home/renzhou/HandGesture.htm),该数据集包含了彩色图及对应的深度图,但是该数据集没有以图像形式存储深度值,而是用 txt 文本以行列形式存储真正的深度值(单位为 mm),所以并不能直观的看到深度图像,Shaun 需要把这些深度值从 txt 文本提取出来并把它以图像的形式呈现出来,由于需求比较特殊,网上没看到现成的解决的方案,所以 Shaun 只有用现成的轮子自己做一个了。

前言

  本文是以前做的一个小东西的处理前奏,当时也记录过,现在把它翻出来重新看看。那个东西需要利用深度图,Shaun 当时还没拿到 Kinect,就在网上下了一些数据http://eeeweba.ntu.edu.sg/computervision/people/home/renzhou/HandGesture.htm),该数据集包含了彩色图及对应的深度图,但是该数据集没有以图像形式存储深度值,而是用 txt 文本以行列形式存储真正的深度值(单位为 mm),所以并不能直观的看到深度图像,Shaun 需要把这些深度值从 txt 文本提取出来并把它以图像的形式呈现出来,由于需求比较特殊,网上没看到现成的解决的方案,所以 Shaun 只有用现成的轮子自己做一个了。

思路篇

  程序的基本思路是:先找到目录及子目录下的所有 txt 文件路径;再根据路径分别读取 txt 文件,按行读取之后再进行字符串分割提取其中的深度值;为了便于以图像形式显示,将深度值归一化至 0~255 存入 8 位单通道的 Mat 类型数据中,最后以 png 图像形式保存至各个目录。

实现篇

  因为当时还在用 opencv-2.4.11,所以本文所实现的代码是基于 opencv-2.4.11,不过应该只要在 opencv-2.0 版本及以上只要有 Mat 数据结构的都能用,毕竟 Shaun 只用到了 OpenCV 中的 Mat 数据结构。Talk is cheap, show you the code(代码很乱,估计也只用这么一次,所以就没怎么注意了 :-P)。具体 C++ 实现代码为:

1
2
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
#include <opencv2/core/core.hpp>  
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>

#include <io.h>
#include <direct.h>

#include <fstream>
#include <iostream>
using namespace cv;
using namespace std;

// ******************************************************************
// @refer to [C++文件读写操作(二)逐字符读取文本和逐行读取文本](http://blog.csdn.net/wangshihui512/article/details/8921924)
// [字符串分割(C++)](http://www.cnblogs.com/MikeZhang/archive/2012/03/24/MySplitFunCPP.html)
// [C++读取文件夹中所有的文件或者是特定后缀的文件](http://blog.csdn.net/adong76/article/details/39432467)
// [C/C++ 判断文件夹是否存在以及创建、删除文件夹 windows以及linux通用](http://blog.csdn.net/u012005313/article/details/50688257)
// [Split a string in C++?](http://stackoverflow.com/questions/236129/split-a-string-in-c)
// [Kinect开发学习笔记之(六)带游戏者ID的深度数据的提取](http://blog.csdn.net/zouxy09/article/details/8151044)
// [Depth Map Tutorial](http://www.pages.drexel.edu/~nk752/depthMapTut.html)
// ******************************************************************
// ----- 逐个字符读取文件 --------
void testByChar()
{
fstream testByCharFile;
char c;
testByCharFile.open("./test.txt",ios::in);

while(!testByCharFile.eof())
{
testByCharFile>>c;
cout<<c;
}
testByCharFile.close();
}

// -------- 逐行读取文件 -------------------
void testByLine()
{
char buffer[256];
fstream outFile;
outFile.open("./test.txt",ios::in);

while(!outFile.eof())
{
outFile.getline(buffer, 256, '\n');//getline(char *,int,char) 表示该行字符达到256个或遇到换行就结束
cout<<buffer<<endl;
}
outFile.close();
}

// ------- 分割字符串 --------------
void splitString()
{
char buffer[1280];
fstream outFile;
outFile.open("./test.txt",ios::in);

while(!outFile.eof())
{
outFile.getline(buffer, 1280, '\n');//getline(char *,int,char) 表示该行字符达到1280个或遇到换行就结束
cout<<buffer<<endl;

const char *d = " ,*";
char *p;
p = strtok(buffer, d);
while(p)
{
printf("%s\n", p);
p=strtok(NULL, d);
}
}

outFile.close();
}

// 获取文件夹下指定格式所有文件名
void getAllFormatFiles( string path, string format, vector<string>& files )
{
//文件句柄
long hFile = 0;
//文件信息
struct _finddata_t fileinfo;
string pathName;
if((hFile = _findfirst(pathName.assign(path).append("/*." + format).c_str(),&fileinfo)) != -1)
{
do
{
//如果是目录,迭代之
//如果不是,加入列表
if((fileinfo.attrib & _A_SUBDIR))
{
if(strcmp(fileinfo.name,".") != 0 && strcmp(fileinfo.name,"..") != 0)
{
//files.push_back(p.assign(path).append("/").append(fileinfo.name) );
getAllFormatFiles( pathName.assign(path).append("/").append(fileinfo.name), format, files);
}
}
else
{
files.push_back(pathName.assign(path).append("/").append(fileinfo.name) );
}
}while(_findnext(hFile, &fileinfo) == 0);

_findclose(hFile);
}
}

// http://stackoverflow.com/questions/236129/split-a-string-in-c
// ---- stackoverflow上大神的C++版本分割字符串 --------------------
std::vector<std::string> split(const std::string& text, const std::string& delims)
{
std::vector<std::string> tokens;
std::size_t start = text.find_first_not_of(delims), end = 0;

while((end = text.find_first_of(delims, start)) != std::string::npos)
{
tokens.push_back(text.substr(start, end - start));
start = text.find_first_not_of(delims, end);
}
if(start != std::string::npos)
tokens.push_back(text.substr(start));

return tokens;
}

// 创建文件夹及子文件夹
void makeDir(const string &path)
{
std::vector<std::string> tokens;
std::size_t start = 0, end = 0;
while ((end = path.find('/', start)) != std::string::npos)
{
if (end != start)
{
tokens.push_back(path.substr(0, end));
}
start = end + 1;
}
if (end != start)
{
tokens.push_back(path);
}

vector<string>::const_iterator itp = tokens.begin();
while (itp != tokens.end())
{
if (access(itp->c_str(), 0) == -1) // 判断文件夹是否存在
{
cout<<*itp<<" is not existing"<<endl;
cout<<"now make it"<<endl;
if (mkdir(itp->c_str()) == 0) // 不存在则创建,只能一级一级的创建
{
cout<<"make successfully"<<endl;
}
}
cout << *itp++ <<endl;
}
}

// Txt文件转opencv Mat(txt文件中存的是以行列形式的深度值)
cv::Mat Txt2DepthMat(const string &txtname)
{
cv::Mat result(480, 640, CV_8UC1, cv::Scalar(0));

char buffer[12800]; // 按行读取文件
fstream outFile;
const char *d = ","; // 以,为分割点
char *p; // 分割出的子串
outFile.open(txtname, ios::in);

for (int i = 0; outFile.getline(buffer, 12800, '\n') != NULL && i < result.rows; i++)
{
p = strtok(buffer, d);
for (int j = 0; p && j < result.cols; j++)
{
int realDepth = (atoi(p) & 0xfff8) >> 3; //提取距离信息,高13位
int depth = (int)(256 * realDepth / 0x0fff); //因为提取的信息是距离信息,为了便于显示,这里归一化为0-255
result.at<uchar>(i, j) = cv::saturate_cast<uchar>(depth);
p = strtok(NULL, d);
}
}

outFile.close();

return result;
}

// 以颜色表示深度信息,越暖(红色)越近,越冷(蓝色)越远
cv::Mat Depth2Color(const cv::Mat &depth)
{
cv::Mat result(depth.size(), CV_8UC3, cv::Scalar::all(0));
int tempDepth, depthRed, depthGreen, depthBlue;
for (int i = 0; i < result.rows; i++)
{
for (int j = 0; j < result.cols; j++)
{
tempDepth = 255 - depth.at<uchar>(i, j);
if(tempDepth < 43)
{
depthRed = tempDepth * 6;
depthGreen = 0;
depthBlue = tempDepth * 6;
}
if(tempDepth > 42 && tempDepth < 85)
{
depthRed = 255 - (tempDepth - 43) * 6;
depthGreen = 0;
depthBlue = 255;
}
if(tempDepth > 84 && tempDepth < 128)
{
depthRed = 0;
depthGreen = (tempDepth - 85) * 6;
depthBlue = 255;
}
if(tempDepth > 127 && tempDepth < 169)
{
depthRed = 0;
depthGreen = 255;
depthBlue = 255 - (tempDepth - 128) * 6;
}
if(tempDepth > 168 && tempDepth < 212)
{
depthRed = (tempDepth - 169) * 6;
depthGreen = 255;
depthBlue = 0;
}
if(tempDepth > 211 && tempDepth < 254)
{
depthRed = 255;
depthGreen = 255 - (tempDepth - 212) * 6;
depthBlue = 0;
}
if(tempDepth > 253)
{
depthRed = 255;
depthGreen = 0;
depthBlue = 0;
}
if (tempDepth == 255)
{
depthRed = 0;
depthGreen = 0;
depthBlue = 0;
}

result.at<Vec3b>(i, j)[0] = depthBlue;
result.at<Vec3b>(i, j)[1] = depthGreen;
result.at<Vec3b>(i, j)[2] = depthRed;
}
}
return result;
}

int main(int argc, char *argv[])
{
string filePath = "C:/Users/XXXXXX/Downloads/NTU-Microsoft-Kinect-HandGesture Dataset/Depth";
vector<string> files;
//读取所有文件
string format = "*"; // 不知道为什么在我电脑读不了特定文件?
getAllFormatFiles(filePath, format, files);

for (int i = 0; i < files.size(); i++)
{
cv::Mat tempMat = Txt2DepthMat(files[i]);
files[i].replace(0, 66, "../data");
files[i].replace(files[i].find(".txt"), files[i].length() - 1, ".png");
cout<< files[i] << endl;
string tempString = files[i].substr(0, files[i].find_last_of("/"));
makeDir(tempString);
cv::imwrite(files[i], tempMat);
}
cout << "File Size: " << files.size() << endl;

//cv::imshow("test", Depth2Color(Txt2DepthMat("./1.txt")));
cv::waitKey(0);
return 0;
}

2018-01-01 BTW:以上代码在 VS2010+Win7 下编译运行通过,在 VS2013+Win10 下 for (int i = 0; outFile.getline(buffer, 12800, '\n') != NULL && i < result.rows; i++) 会报错,可能需要改成 for (int i = 0; outFile.getline(buffer, 12800, '\n') && i < result.rows; i++) ,即去掉后面的 != NULL

后记

  正如前言所说,本文是以前记录过的,一些细节也快忘记,这次重写算是回顾一下吧,这段程序可能也确实只用这么一次,但其中用到了不少 C++ 处理字符串和读写文件等相关知识,而这些知识,在以后有极大的可能会再次用到,因此记录 ↖(^ω^)↗。

参考资料

[1] C++文件读写操作(二)逐字符读取文本和逐行读取文本http://blog.csdn.net/shihui512/article/category/1397194

[2] 字符串分割(C++)http://www.cnblogs.com/MikeZhang/category/345894.html

[3] C++读取文件夹中所有的文件或者是特定后缀的文件http://blog.csdn.net/adong76/article/category/1632029

[4] C/C++ 判断文件夹是否存在以及创建、删除文件夹 windows以及linux通用http://blog.csdn.net/u012005313/article/category/5586103

[5] Split a string in C++?http://stackoverflow.com/questions/236129/split-a-string-in-c

[6] Kinect开发学习笔记之(六)带游戏者ID的深度数据的提取http://blog.csdn.net/zouxy09/article/category/1273380

[7] Depth Map Tutorialhttp://www.pages.drexel.edu/~nk752/depthMapTut.html

OpenCV中滑动条和鼠标事件响应操作的使用小结

前言

  既然在上一篇中提到了回调函数,Shaun 就干脆把 OpenCV 中较常使用的两个使用回调函数的函数使用方法也一并记录下来吧。

前言

  既然在上一篇中提到了回调函数,Shaun 就干脆把 OpenCV 中较常使用的两个使用回调函数的函数使用方法也一并记录下来吧。

说明篇

OpenCV 中使用回调函数的两个函数为:

  1. 鼠标事件响应操作函数:void cv::setMouseCallback(const string& winname, MouseCallback onMouse, void* userdata = 0);

    参数浅解:

    const string& winname:窗口名称,对名为winname的窗口执行鼠标事件响应操作;

    MouseCallback onMouse:鼠标响应事件回调函数,监听鼠标的点击,移动,松开,判断鼠标的操作类型并做出相应处理;

    void* userdata:对应回调函数的可选参数,若使用全局变量可以忽略该参数。

    对应的回调函数声明为:typedef void (*MouseCallback)(int event, int x, int y, int flags, void* userdata);

    参数浅解:

    int event:鼠标滑动(CV_EVENT_MOUSEMOVE)、左键单击(CV_EVENT_LBUTTONDOWN)、右键单击(CV_EVENT_RBUTTONDOWN )等10种鼠标点击事件的int型代号;

    int x, int y:鼠标位于窗口的(x,y)坐标位置,窗口左上角默认为原点,向右为x正轴,向下为y正轴;

    int flags:鼠标左键拖拽(CV_EVENT_FLAG_LBUTTON)、右键拖拽(CV_EVENT_FLAG_RBUTTON)等6种鼠标拖拽事件的int型代号;

    void* userdata:回调函数的参数,若使用全局变量可以忽略该参数。

  2. 创建滑动条函数:int cv::createTrackbar(const string& trackbarname, const string& winname, int* value, int count, TrackbarCallback onChange=0, void* userdata=0);

    参数浅解:

    const string& trackbarname:创建的滑动条名称;

    const string& winname:所在窗口名称,对名为winname的窗口添加滑动条;

    int* value:滑块的位置,其初始值对应滑块的初始位置;

    int count:滑块可达到的最大位置的值,滑块最小位置的值总为0;

    TrackbarCallback onChange:滑动条事件回调函数,当滑动条上位置改变的时,则执行该回调函数;

    void* userdata:对应回调函数的可选参数,若使用全局变量可以忽略该参数。

    对应的回调函数声明为:typedef void (CV_CDECL *TrackbarCallback)(int pos, void* userdata);

    参数浅解:

    int pos:滑动条的位置对应的值;

    void* userdata:回调函数的参数,若使用全局变量可以忽略该参数。

※注:本文的函数说明采用的是 opencv-2.4.11 的函数声明,与 opencv-3.2.0 的函数声明区别在于 string 类型,opencv-3.2.0 采用的是其自己实现的一个 String 类。

实例篇

Show u the code,具体 C++ 实现代码为:

1
2
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
#include <opencv2/opencv.hpp> 

#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>

// ---------------- 鼠标事件回调函数 ---------------------------------
static cv::Mat src_img; // 原始图像全局变量
static void mouseCallback(int event, int x, int y, int flags, void *)
{
bool selected = false;
static cv::Point left_top_vertex, right_down_vertex; // 左上角顶点和右下角顶点
// When the left mouse button is pressed, record its position and save it in corner1
if (event == CV_EVENT_LBUTTONDOWN) // 左键按下
{
left_top_vertex.x = x;
left_top_vertex.y = y;
std::cout << "Corner 1 recorded at " << left_top_vertex << std::endl;
}
// When the left mouse button is released, record its position and save it in corner2
if (event == cv::EVENT_LBUTTONUP) // 左键弹起
{
// Also check if user selection is bigger than 20 pixels (jut for fun!)
if (abs(x - left_top_vertex.x) > 10 && abs(y - left_top_vertex.y) > 10)
{
right_down_vertex.x = x;
right_down_vertex.y = y;
std::cout << "Corner 2 recorded at " << right_down_vertex << std::endl << std::endl;
selected = true;
}
else
{
std::cout << "Please select a bigger region" << std::endl;
}
}
// Update the box showing the selected region as the user drags the mouse
if (flags == CV_EVENT_FLAG_LBUTTON) // 左键拖拽
{
cv::Point pt;
pt.x = x;
pt.y = y;
cv::Mat local_img = src_img.clone();
rectangle(local_img, left_top_vertex, pt, cv::Scalar(0, 0, 255));
imshow("Cropping app", local_img);
}
// Define ROI and crop it out when both corners have been selected
if (selected)
{
cv::Rect box;
box.width = abs(left_top_vertex.x - right_down_vertex.x);
box.height = abs(left_top_vertex.y - right_down_vertex.y);
box.x = cv::min(left_top_vertex.x, right_down_vertex.x);
box.y = cv::min(left_top_vertex.y, right_down_vertex.y);
// Make an image out of just the selected ROI and display it in a new window
cv::Mat crop(src_img, box);
cv::namedWindow("Crop");
imshow("Crop", crop);
}
}

// ---------- 响应鼠标事件 ------------------------------------
void setMouseCallbackTest()
{
src_img = cv::imread("../data/lena.jpg", CV_LOAD_IMAGE_ANYDEPTH | CV_LOAD_IMAGE_ANYCOLOR);
cv::namedWindow("Cropping app");
imshow("Cropping app", src_img);
// Set the mouse event callback function
cv::setMouseCallback("Cropping app", mouseCallback);

while (char(cv::waitKey(30)) != 'q') {}
}



// -------------- 滑动条回调函数 ------------------------
static void thresholdCallback(int slider_value, void* gray)
{
//static_cast<>用于安全转换指针
cv::Mat *tmp_gray = static_cast<cv::Mat *>(gray);
cv::Mat tmp = *tmp_gray;
cv::Mat dst;
threshold(tmp, dst, slider_value, 255, CV_THRESH_BINARY);

//显示效果图
cv::imshow("Trackbar Demo", dst);
}

// ------------ 创建滑动条 ----------------------------------
void createTrackbarTest()
{
cv::Mat src_gray = cv::imread("../data/lena.jpg", 0);

const int max_value = 255; //滑动条的最大值
int slider_value = 0; // 滑动条的初始值

char *window_name = "Trackbar Demo";
char *trackbar_name = "Value:";

// 创建一个窗口显示图片
cv::namedWindow(window_name, CV_WINDOW_AUTOSIZE);
imshow(window_name, src_gray);

// 创建滑动条来控制阈值
createTrackbar(trackbar_name, window_name, &slider_value, max_value, thresholdCallback, &src_gray);

while (char(cv::waitKey(30)) != 'q') {}
}

// ------- 将两个函数在同一个窗口执行 ------------
void callbackTest()
{
src_img = cv::imread("../data/lena.jpg", 0);
const int max_value = 255; //滑动条的最大值
int slider_value = 0; // 滑动条的初始值

char *window_name = "Callback Demo";
char *trackbar_name = "Value:";

// 创建一个窗口显示图片
cv::namedWindow(window_name, CV_WINDOW_AUTOSIZE);
imshow(window_name, src_img);

// 创建滑动条来控制阈值
createTrackbar(trackbar_name, window_name, &slider_value, max_value, thresholdCallback, &src_img);

// 鼠标事件响应
cv::setMouseCallback(window_name, mouseCallback);

while (char(cv::waitKey(30)) != 'q') {}
}


int main(int argc, char *argv[])
{
//setMouseCallbackTest();
//createTrackbarTest();
callbackTest();

while (char(cv::waitKey(30)) != 'q') {}

return 0;
}

  经 Shaun 测试,上面示例程序在 Win10 的 VS2013 中 opencv-2.4.11 和 opencv-3.2.0 下都能完美运行。

后记

  本来这两个函数都已经写(chao)好了,但为了更好的体现示例程序,又稍作了修改:添加鼠标左键拖拽事件及不使用全局变量等。

参考资料

[1] opencv2 使用鼠标绘制矩形并截取和保存矩形区域图像http://www.cnblogs.com/lidabo/category/516776.html

[2] Opencv中添加进度条及回调函数http://blog.csdn.net/weixin_35738542/article/category/6337413

[3] OpenCV2中滑动条(Trackbar)回调函数的小发现http://blog.csdn.net/u014291399/article/category/3097955

[4] OpenCV GUI基本操作,回调函数,进度条,裁剪图像等http://blog.csdn.net/wangyaninglm/article/category/1653815