快速判断三角形与长方体相交

前言

  一种快速判断空间中三角形与 box 相交的方法,出自论文:Tomas Akenine-Moller. Fast 3D triangle-box overlap testing. A. K. Peters, Ltd. 2002.

前言

  一种快速判断空间中三角形与 box 相交的方法,出自论文:Tomas Akenine-Moller. Fast 3D triangle-box overlap testing. A. K. Peters, Ltd. 2002.

预备篇

  该论文的理论基础来自分离轴理论(separating axis theorem, AST),AST 常用于检测两凸多边形是否相交。一句话描述 AST 即为:若两多边形能用一条直线分隔开,则两多边形不相交。如何判断该直线存在即为 AST 的关键。常用的判断方法为找出两多边形所有边向量(多边形相邻两点构成的向量,顺时针或逆时针都行)的法向量,使用向量点积分别计算两多边形在各法向量上的投影(一般以多边形上的点和原点构成一个向量与法向量做点积),从而得到两个投影集合,判断两集合是否相交(找出两个集合的最大值和最小值,若最小值大于最大值,则不相交),若不相交,则 AST 中的直线存在,即两多边形不相交,若相交,则继续判断在其它法向量上的投影,若所有法向量上的投影都相交,则两凸多边形相交。AST 常用于二维下判断两凸多边形的相交情况,三维下的情况比较复杂。

正文篇

  该论文给定 13 个向量,若 box 的边和三角形的边在这 13 个向量中的投影均相交,则认为 box 与三角形相交。为简化运算,box 直接假定为轴向包围盒(axis-aligned bounding box, AABB),坐标轴原点为 box 中心,由于可以将普通 box 通过旋转平移等一系列变换,变成以原点为中心的 AABB, 所以该假定是有效的。设三角形的顶点为 \(v_0, v_1, v_2\) ,box 的一半长宽高为 \(h_x, h_y, h_z\) ,则这 13 个向量分别为 \(e_0(1, 0, 0), e_1(0, 1, 0), e_2(0, 0, 1)\) ,三角形的法向量 \(n\) (通过三角形两边向量叉乘得到),剩下九个向量分别为 \(a_{ij} = e_i \times f_j , i,j \in \{0, 1, 2\}\) ,其中 \(f\) 为三角形的边向量 \(f_0 = v_1 - v_0, f_1 = v_2 - v_1, f_2 = v_0 - v_2\)\(\times\) 代表向量叉乘。若直接这样一个个的计算投影是否相交,虽然能达到目的,但快速就无法体现了,所以作者根据向量计算方法和一些策略将其中一些需要计算投影的地方极大的简化了,所以加快的计算速度。具体简化过程为:

  1. 首先来看最后九个向量,\(a_{00} = e_0 \times f_0 = (0, -f_{0z}, f_{0y})\) ,三角形三个顶点在在该向量上的投影分别为:

    \(p_0 = a_{00} \cdot v_0 = (0, -f_{0z}, f_{0y}) \cdot v_0 = v_{0z}v_{1y} - v_{0y}v_{1z}\)

    \(p_1 = a_{00} \cdot v_1 = (0, -f_{0z}, f_{0y}) \cdot v_1 = v_{0z}v_{1y} - v_{0y}v_{1z} = p_0\)

    $p_2 = a_{00} v_2 = (0, -f_{0z}, f_{0y}) v_2 = (v_{1y} - v_{0y})v_{2z} - (v_{1z} - v_{0z})v_{2y} $

    由于 \(p_0 == p_1\), 所以在求最大最小值时只需要做一次比较,接着求 box 在该向量上的投影,box 中心在原点,所以投影半径 \(r\) 可以以一种简单的方式求出:

    \(r = h_x|a_{00x}| + h_y|a_{00y}| + h_z|a_{00z}| = h_y|a_{00y}| + h_z|a_{00z}|\)

    计算投影是否重合也很简单:

    \(if(min(p_0, p_2) > r \ || \ max(p_0, p_2) < -r) \quad return \ false\) 否则两者投影相交,继续计算其它向量。

  2. 三个轴向单位向量 e 中的投影是否重合就更好判断了,完全不需要计算投影,只需要计算三角形的最小 AABB,判断两个 AABB 是否相交即可(取两个 AABB 最小的顶点和最大的顶点,从三维上判断最小是否的大于最大的即可,若任意一个维度上最小的比最大的大,则两者不相交),

  3. 至于判断最后一个向量——三角形的法向量上的投影是否重合,相当于判断三角形所在平面是否与 box 相交。判断 box 与平面相交有一种简单快速的方式,即通过公式 \(|d| <= a_1 |n \cdot A^1| + a_2|n \cdot A^2| + a_3 |n \cdot A^3|\) ,其中 \(d\) 为 box 中心到平面的距离(中心点到平面上一点构成的向量与平面法向量做点积),\(n\) 为平面法向量,\(A^1\) 为 box 侧面法向量,对于 AABB 可为 \((1, 0, 0)\)\(a_1\) 为 box 中心到侧面的距离,对于 AABB 可为 \(h_x\),同理 \(A^2\) 为 box 顶面法向量,对于 AABB 可为 \((0, 1, 0)\)\(a_2\) 为 box 中心到顶面距离,对于 AABB 可为 \(h_y\)\(A^3\) 为 box 正面法向量,对于 AABB 可为 \((0, 0, 1)\)\(a_3\) 为 box 中心到正面距离,对于 AABB 可为 \(h_z\),即在 AABB 中,该公式可简化为 \(|d| <= h_x|n_x| + h_y|n_y| + h_z|n_z|\) ,满足该公式,即可判定平面与 AABB 相交。

  如此 13 个向量全部判断完毕,如全都相交,则可认定三角形与长方体相交,若其中一个不相交,则三角形与长方体不相交。三维的都能判断,二维的三角形与矩形相交判断就更简单了,分成两类法向量后,利用向量运算先简化运算量,再计算投影是否相交即可。

附录

  还有一种根据距离判断两个 AABB 是否相交的办法,即先取两个 AABB 的中心 \((x_1, y_1, z_1)\)\((x_2, y_2, z_2)\),然后计算两个中心点之间的三个维度的距离,将 x 维度的距离与两个AABB 的 \(h_x\) 之和比较,若中心点 x 维度的距离较大,则不相交。即:\(if (|x_1 - x_2| > h_{x1} + h_{x2}) \quad return \ false\) ,否则比较 y 维度, z 维度,若所有都小,则两 AABB 相交。这种方式是 Shaun 在一次面试中被问到没答出后在网上找到的答案,其实感觉和比较最小最大顶点也差不多,都是从不相交出发,因为直接判断相交基本不可能,而不相交很容易判断,把所有的不相交情况判断完,那就只剩相交了,可惜面试官只想要这种方案 ╮(╯▽╰)╭。

参考资料

[1] Code by Tomas Akenine-Möller

[2] Simple Intersection Tests For Games

网页菜单纯 css 实现

前言

  最近搞了些前端的工作,本来做菜单栏的时候想直接用 bootstrap 的,但是感觉 bootstrap 太大了,而且依赖有点多,在 webpack 中也不是很好打包(虽然可以绕过去),所以就索性自己在网上找了一些实现方式,改改感觉也还可以。这次主要实现了两种菜单栏,具体如下。

前言

  最近搞了些前端的工作,本来做菜单栏的时候想直接用 bootstrap 的,但是感觉 bootstrap 太大了,而且依赖有点多,在 webpack 中也不是很好打包(虽然可以绕过去),所以就索性自己在网上找了一些实现方式,改改感觉也还可以。这次主要实现了两种菜单栏,具体如下。

鼠标悬停下拉菜单

  鼠标悬停下拉菜单应该是最常见的一种菜单栏了,当鼠标悬停在菜单栏上时,子菜单缓缓下拉,看起来就很舒服,用 flex 布局结合列表也很好实现(不用像以前那种 float 了,舒服)。具体实现方式如下:

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
<style>
.flex-body {
display: flex;
flex-direction: column;
}

header {
z-index: 1;
}

ul,
ul li {
list-style: none;
margin: 0;
padding: 0;
}

.menu {
display: flex;
justify-content: start;
}

ul li {
width: 100px;
height: 50px;
line-height: 50px;
}

.menu li .submenu {
/* display: none; */
background-color: aqua;
}

.submenu li {
height: 0;
line-height: 0;
opacity: 0;
visibility: hidden;
}

.menu li:hover .submenu li {
/* display: block; */
height: 50px;
line-height: 50px;
opacity: 1;
visibility: visible;
transition: all 1s;
}
</style>
<div class="flex-body">
<header>
<ul class="menu">
<li>
<a>menu1</a>
<ul class="submenu">
<li><a>submenu1</a></li>
<li><a>submenu1</a></li>
<li><a>submenu1</a></li>
</ul>
</li>
<li>
<a>menu2</a>
<ul class="submenu">
<li><a>submenu1</a></li>
<li><a>submenu1</a></li>
<li><a>submenu1</a></li>
</ul>
</li>
<li>
<a>menu3</a>
<ul class="submenu">
<li><a>submenu1</a></li>
<li><a>submenu1</a></li>
<li><a>submenu1</a></li>
</ul>
</li>
</ul>
</header>
<main>mainmainmainmainmainmainmainmainmainmain</main>
</div>

  ※注: 实现该菜单栏有两个需要注意的点:1、不能用 display: noneblock 来使子菜单消失或出现,因为这样会造成缓缓下拉的动画失效,transition 并不支持 display,所以只能用 visibilityheight 来共同实现,以达到下拉动画效果;2、因为使用了 flex 布局,所以 z-index 只对同级 flex-item 有效,所以为防止菜单栏下面的内容出现在子菜单之上,即将子菜单栏位于最上层,需要将整个页面的布局都设置为 flex,并使 headerz-index 最大,如此才能保证子菜单的菜单覆盖 main 中的内容,不然就会有重叠干扰现象。

鼠标点击手风琴菜单

  手风琴特效也算是非常常见的了,一般的手风琴是鼠标悬停展开,这种比较好实现,难的是如何保持这种展开状态,focus 可以短暂保持展开状态,但是不能点击其他地方,局限性太大。所以需要引入其它的东西来记录这种展开状态,可以用 checkboxradiochecked 来记录这种状态,从而只用 css 即可实现该菜单,具体实现如下:

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
<style>
input[data-prop="menu-recorder"] {
display: none;
}

.menu-title {
display: block;
width: 500px;
height: 50px;
line-height: 50px;
border: 1px solid black;
}

.menu-content {
width: 500px;
max-height: 100px;
overflow-y: auto;
}

.menu-content>li {
height: 0;
line-height: 20px;
overflow: auto;
opacity: 0;
visibility: hidden;
transition: all 1s;
}

input[data-prop="menu-recorder"]:checked+.menu-content>li {
height: 20px;
line-height: 20px;
opacity: 1;
visibility: visible;
transition: all 1s;
}
</style>
<div class="accordion-menu">
<section class="menu-item">
<label class="menu-title" for="menu1">menu1</label>
<input id="menu1" type="checkbox" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
<section class="menu-item">
<label class="menu-title" for="menu2">menu2</label>
<input id="menu2" type="checkbox" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
<section class="menu-item">
<label class="menu-title" for="menu3">menu3</label>
<input id="menu3" type="checkbox" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
<section class="menu-item">
<label class="menu-title" for="menu4">menu4</label>
<input id="menu4" type="checkbox" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
</div>

  这里需要注意一点的就是 height 从 0 到 100% 并不会触发 transition 渐变动画,而是需要确切的高度值变化才能触发,所以上文这里添加了个 <li> 标签,直接在该标签上添加动画,还有一种就是在 .menu-content 上设定确定的 max-height,也能触发动画,但是有个缺点就是前后 max-height 的差距太大时,动画效果就很不理想了,这时可能只能依靠 js 了。上文中 checkbox 也可用 radio 替换,效果略有差异,一个是能全部展开或收起,而另一个则是能且仅能展开一个。

Tab 标签页切换菜单

  这个菜单和上面那个菜单的实现非常相似,先上代码:

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
<style>
.tab-menu {
display: flex;
position: relative;
width: 500px;
height: 300px;
}

input[data-prop="menu-recorder"] {
display: none;
}

.menu-title {
display: block;
width: 100px;
line-height: 50px;
text-align: center;
border: 1px solid black;
border-right: 0;
box-sizing: border-box;
transition: all 1s;
}

.tab-menu .menu-item:last-child .menu-title {
border-right: 1px solid black;
}

.menu-content {
position: absolute;
left: 0;
top: 51px;
height: calc(100% - 50px);
overflow-y: auto;
width: 100%;
border: 1px solid #000;
box-sizing: border-box;
font-size: 24px;
text-align: center;
opacity: 0;
visibility: hidden;
transition: all 1s;

}

input[data-prop="menu-recorder"]:checked+.menu-content {
opacity: 1;
visibility: visible;
transition: all 1s;
}
</style>
<div class="tab-menu">
<section class="menu-item">
<label class="menu-title" for="menu1">menu1</label>
<input id="menu1" type="radio" name="tab-control" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
<section class="menu-item">
<label class="menu-title" for="menu2">menu2</label>
<input id="menu2" type="radio" name="tab-control" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
<section class="menu-item">
<label class="menu-title" for="menu3">menu3</label>
<input id="menu3" type="radio" name="tab-control" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
<section class="menu-item">
<label class="menu-title" for="menu4">menu4</label>
<input id="menu4" type="radio" name="tab-control" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
</div>

  从代码上看关键点就是 .menu-content 的定位方式了,采用了绝对定位,并将整个菜单栏设置为相对定位,以保证所有 tab 标签页内容位置和大小保持一致。当然 tab 标签页肯定是唯一的,所以只能用 radio 记录显示标签页了。其中为了保证 .menu-title.menu-content 的边框不重叠,所以在 .menu-title 中只设置 line-height,而 .menu-contenttop 比其多一个像素。

后记

  这三种菜单应该是最常见也是用的最多的了,纯 css 实现的方式也比较类似,无非就是 flex 布局以及借助 css3 强大的选择器功能(父类选择器不知要到猴年马月了,比较遗憾 😥),就能相对简单的实现了,当然借助一些 js 库或框架可能会更简单一些 ,但能用 css 为何不用呢 😄。

参考资料

[1] 利用flex实现的二级导航栏

[2] CSS3动画下拉菜单(当transition遇到display的坑)

[3] CSS3手风琴下拉菜单

[4] 教你两招用纯CSS写Tab切换

hexo-theme-chi主题更新小记

前言

  Chi 主题的大体结构功能算是写完了,但是还有一些个性化的东西需要添加,所以以后有关于 Chi 主题更新的部分就都写在这里吧。但是由于是 Shaun 个性化定制的一些东西,所以如果不是大 Bug 或大优化的更新,一般就不进 Chi 主题仓库 中了。

前言

  Chi 主题的大体结构功能算是写完了,但是还有一些个性化的东西需要添加,所以以后有关于 Chi 主题更新的部分就都写在这里吧。但是由于是 Shaun 个性化定制的一些东西,所以如果不是大 Bug 或大优化的更新,一般就不进 Chi 主题仓库 中了。

功能篇

1. 脚注提示功能

功能描述: 鼠标悬停在脚注上即可显示对应脚注内容。 Shaun 的脚注由于是采用 pandoc 渲染的,所以也是属于个性化定制,就不将这个功能放进 Chi 主题仓库中了。

解决方案: 还是利用 Bootstrap 的 tooltip 提示插件,具体实现代码如下:

1
2
3
4
5
6
7
$('a.footnote-ref').each(function (index, elem) {
let post_id = $(this).parents('article').attr('id');
let fn_href = $(this).attr('href');
elem.setAttribute('data-toggle', 'tooltip');
elem.setAttribute('data-html', 'true');
elem.setAttribute('title', $("#" + post_id + " " + fn_href).html());
});

遍历脚注,先获取文章 id,再获取对应文章下的对应脚注内容,使用 tooltip 提示。

Bug 篇

1. 图片没居中

问题描述: 上次那篇翻译的文章有几张图片,在放置的时候发现图片没有居中,查看代码后发现居中样式没写,但由于其图片标签 <img> 是放在一个 <figure> 标签中,由于不确定是不是 pandoc 渲染的问题,所以就没将这个修正放进 Chi 主题仓库中了。

解决办法:style.styl 文件中添加样式:

1
2
3
figure {
text-align: center;
}

即可让图片居中。

动画篇

1. 鼠标跟随动画

  其实一直都想把『奥日与黑暗森林』中的鼠标轨迹特效移植过来,但是苦于水平有限,一直没法做到,恰好 19 年 StackOverflow 的愚人节彩蛋中有个鼠标跟随动画很有意思,有好事者还专门将该彩蛋做了个脚本:Will there be an option to permanently keep this year's April Fools design active? 。查看代码,知道实现原理后,发现用 jQuery 和 CSS3 实现一个类似的效果也不算很难,于是 Shaun 就尝试做了一下,并简单的美化了一下,感觉效果还行,就加到自己的个性化主题上了。至于在 Chrome 中的小尾巴和 Firefox 中的卡顿现象,是 Shaun 故意的,因为 Shaun 觉得这个小尾巴很有意思,从这个动画看,Chrome 确实比 Firefox 要流畅一点。最终实现代码如下:

css 代码:

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
.cursor-trail--item {
display: inline-block;
line-height: 1px;
position: fixed;
pointer-events: none;
touch-action: none;
z-index: 9999999;
will-change: transform;
font-size: 10px;
color: rgba(186, 227, 240, 0.1);
text-shadow: 0 0 2px #6CC2F8;
-webkit-animation: cursorTrail 0.9s ease;
animation: cursorTrail 0.9s ease;
}

@keyframes cursorTrail {
0% {
opacity: 1;
}

20% {
opacity: 0.5;
transform: scale(5);
}

100% {
opacity: 0;
transform: translate3D(0, -20px, 0) scale(1) rotate(90deg);
}
}

js 代码如下:

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
$(document).on('mousemove', function (e) {
e.preventDefault();
if (this.time && (Date.now() - this.time) < 16) return;
this.time = Date.now();
let trail_character = '•';
let mouse_x = e.originalEvent.x || e.originalEvent.layerX || 0;
let mouse_y = e.originalEvent.y || e.originalEvent.layerY || 0;
mouse_x = mouse_x + 20;
mouse_y = mouse_y + 26;


$('#cursor-trail').append(
'<span class="cursor-trail--item" style="left:'
+ mouse_x
+ 'px;top:'
+ mouse_y
+ 'px;">'
+ trail_character
+ '</span>'
);

$('.cursor-trail--item').each(function () {
let item = $(this);
setTimeout(function () {
$(item).remove();
}, 900);
});
});

若不要小尾巴,则只需要将移除元素的时间改快一点就行了,除开上面的代码,还需要在 html 页面中添加一个元素 <span id="cursor-trail"></span>

2. 点击波纹

  一次偶然的机会,看到一篇这样的文章:还原一个 Windows 10 Metro 布局 。感觉其中的点击波纹动画很有意思,Shaun 决定把这个动画也放进自己的个性化主题中。至于代码就不贴了,毕竟就是上面文章提供的代码,只是用这个动画的时候发现了 Chrome 的一个问题,就是在进行模糊动画的时候会出现正方形的右边框和下边框,单个模糊不进行动画,不会有边框,进行动画但没模糊也不会有边框,两个同时一起就会出现问题,Firefox 没有这个问题,但 Firefox 存在另一个问题,就是鼠标快速连点的时候,鼠标跟随动画可能会出现问题。浏览器的问题 Shaun 暂时没法解决了,就先这样吧 ╮(╯▽╰)╭。虽然对 Chrome 为啥会出现这样的问题有些猜想,但还是不说出来丢人了,万一不是 Shaun 想的这样就尴尬了 ,,ԾㅂԾ,, 。

后记

有更新再继续更新吧。

[译]为什么深度学习没有取代传统的计算机视觉

前言

  这是一篇译文,译自:Why Deep Learning Has Not Superseded Traditional Computer Vision,原作者为:Zbigatron 。Shaun 水平有限,仅供参考学习,更多内容还请自行查看原文。

至于为什么要翻译这篇文章,算是回答别人的一个问题吧。

前言

  这是一篇译文,译自:Why Deep Learning Has Not Superseded Traditional Computer Vision,原作者为:Zbigatron 。Shaun 水平有限,仅供参考学习,更多内容还请自行查看原文。

至于为什么要翻译这篇文章,算是回答别人的一个问题吧。

为什么深度学习还没有取代传统的计算机视觉?

scale-cv-dl

  编写这篇文章的原因是在论坛中经常有人问:深度学习是否取代了传统的计算机视觉?或者类似的问题:深度学习效果这么好,还有继续研究传统计算机视觉的必要?

  这是个好问题,深度学习(DL)确实彻底改变了计算机视觉(CV)和人工智能,许多曾经看起来不可能解决的问题都解决了,甚至达到了 机器的结果比人类更好 的程度,比如图像分类。正如 我之前讨论的深度学习确实为计算机视觉做了很大的贡献

  但是深度学习只是计算机视觉的一个工具,并不是解决所有问题的万能药,所以,在这篇文章中,我想详细说明一下为什么传统的计算机视觉仍然非常有用,并且应该继续学习。

这篇文章主要有一下几个观点:

  • 深度学习需要大量数据;
  • 深度学习有时大材小用了;
  • 传统的计算机视觉可以辅助深度学习。

  但是在我开始讨论这些观点之前,需要先解释一下什么是传统的计算机视觉,什么是深度学习,以及深度学习为何这么 dio。

背景知识

  在深度学习之前,如果要实现图像分类这样的功能,需要首先进行 特征提取。特征即为图像中的信息块,能代表图像的部分信息,可以通过 边缘检测角点检测物体检测 等技术来提取特征。

  在进行特征提取和图像分类之类的工作时,一个想法是从一类对象(例如椅子,马等)的图像中提取尽可能多的特征,并将这些特征视为对象的一种定义(比如 词袋模型),然后在其他图像中查找这些定义,若另一个图像中的特征和定义的特征很相似,则该图像可能包含该特定的对象(如椅子,马等)。

  在图像分类中,这种特征提取的难点在于每个图像都必须选择哪些特征进行查找,当需要分类的类别开始增加时,比如 10 或 20 种类别,这种方式将变得很麻烦以至于不可能实现。当然也可以考虑角点、边缘和纹理信息,使用不同的特征可以更好的描述不同类别的对象,但是如果选择使用很多特征,则必须处理大量参数,所有这些参数都必须微调。

  深度学习中有一个端到端学习的概念,其含义为告诉机器要针对每个特定类别对象学习需要查找的特征,它为每个对象设计了最具描述性和显著性的特征,换言之,告诉神经网络要发现图像中每个类别的基本模式

  因此,通过端到端学习,不需要再决定使用哪种传统的计算机视觉技术来描述特征,机器自动选择好了,Wired magazine 说过:

如果想教一个神经网络识别一只猫,不需要让它寻找胡须、耳朵、皮毛和眼睛,只需要给它大量猫的图像,它就会自动识别猫,若它将狐狸错认成猫,不需要重写代码,只需要继续训练即可。

下图表示了特征提取(使用传统计算机视觉)和端到端学习之间的差异:

traditional-cv-and-dl

  以上就是需要的背景知识,下面开始深入探讨为什么传统的计算机视觉仍然有存在的必要。

深度学习需要大量数据

  首先,深度学习需要大量的数据,那些著名的图像分类模型就是在海量数据集上训练的,训练数据集中最大的三个是:

  一般的图像分类任务不需要这么多图片,但仍然需要很多,如果没办法获得这么多图片怎么办?我们还是得训练我们所有的数据(有些方法可以增多我们的训练数据,但这些都是人工方法)。但是,没有足够的数据支持,训练出来的模型可能会在训练集之外表现不好,因为机器没有洞察能力,无法对没有的数据进行分类。而且无法直观查看训练好的模型并手动调整里面的数据,因为深度学习模型里面有数百万个参数,并且这些参数在训练时会自动微调,某种程度上,深度学习模型就是一个黑盒子。

  传统的计算机视觉完全透明,可以很清楚的判断自己的解决方案在训练数据之外是否可行,而且可以深入了解算法中存在的问题。如果有没法解决的问题,也可以更容易的找出原因并调整。

深度学习有时大材小用了

  这可能是我支持传统计算机视觉技术研究的最佳理由。

  训练深度神经网络需要很长时间,而且需要专门的硬件(高性能 GPU),如果想在普通的笔记本上训练最先进的图像分类模型,可以去外面玩一个星期,回来之后应该还没训练完 :) 。而且,如果训练好的模型表现不好怎么办?必须调整训练参数并重新开始训练,这个过程有时会重复数百次。

  但有时候使用深度学习是完全没有必要的,因为有时传统的计算机视觉技术可以比深度学习更有效的解决问题并且代码更少。比如,我曾经做过一个项目来检测传送带上每个罐头是否都有红色的勺子,解决这个问题可以训练深度神经网络来检测勺子,也可以针对红色编写一个简单的阈值分割算法(红色的某个范围内的像素点为白色,其它像素点为黑色),然后计算有多少个白色像素点,后者明显简单的多,一个小时就完成了。

  了解传统的计算机视觉有时会节省大量时间和避免不必要的麻烦

传统计算机视觉提高改进深度学习技能

  了解传统的计算机视觉可以帮助我们更好地进行深度学习。

  例如,计算机视觉中使用的最常见的神经网络是卷积神经网络。但什么是卷积?它实际上是一种广泛使用的图像处理技术(例如 Sobel边缘检测)。了解这一点可以帮助我们了解神经网络正在做什么,并因此可以更好的设计和调整神经网络来解决问题。

  深度学习中还可以对图像进行预处理,所谓的预处理是指对训练的数据进行一定的处理(Shaun 注:比如图像增强,图像去噪等),这些预处理操作一般由传统的计算机视觉技术完成,比如:当没有足够的训练数据时,可以使用一种叫数据增强的技术,使用数据增强让图像进行旋转,平移,裁剪等操作,从而增加“新”图像,通过执行这些操作,可以成倍的增加训练数据集。

总结

  在这篇文章中,我解释了为什么深度学习还没有取代传统的计算机视觉技术,因此还需要研究后者。首先我发现了深度学习要想表现的足够好需要大量数据的问题,有时候没法获得大量数据,这时只能用传统计算机视觉技术代替;其次,对于特定的任务,使用深度学习可能大材小用了,传统计算机视觉有时比深度学习更有效且代码量也更少;最后了解传统的计算机视觉可以更好的学习深度学习,因为这可以使我们更好的了解深度学习的内部机制,并且可以使用某些预处理操作来改善深度学习结果。

  简而言之,深度学习只是计算机视觉的一个工具,不是万能药,不要只是因为它现在很流行所以使用它,传统的计算机视觉技术仍然十分有用,了解它可以节省时间和避免许多麻烦。

后记

  翻译这篇文章的原因在于,因为某些原因,Shaun 不得不回答一个 “为什么不用深度学习?” 的问题,虽然这里面有极大的因素是客观原因(设备不够 ╮(╯▽╰)╭),但 Shaun 不能明说,正好在网上看到这篇文章,加上也看到过一个问题(类似于 “既然已经能用深度学习做高层次的工作,为何还要用深度学习做底层的工作?”,比如深度学习能做全景分割(可以说是基本完成了图像理解),为何还要做目标检测,图像分割),预计以后有很大的概率也会被问到,所以就需要借用文中的一些观点来进行回答。至于为啥还要用深度学习做底层的工作,可以这样认为,如果用深度学习做底层工作效果不错的话,应该可以对上层工作进行一些辅助,并且可以为上层工作的做法提供一些新的思路。

hexo-theme-chi主题开发小记

前言

  2019 年 2 月 20 日,Shaun 的站点主题终于完成了,终于能用上自己的主题了,开心 ~( ̄▽ ̄~)。

前言

  2019 年 2 月 20 日,Shaun 的站点主题终于完成了,终于能用上自己的主题了,开心 ~( ̄▽ ̄~)。

  在网上看到的建议都是多写内容少整主题,但强迫症表示受不了,Shaun 就是想要一个自己的主题,自己怎么看着舒服就怎么来,即使没有一个访客,至少也要让自己看着舒服,自己想怎么设计就怎么设计,想用什么技术就用什么技术,想怎么排版就怎么排版,尽量少受写约束,总而言之,Shaun 喜欢自由。

回顾篇

  自己写主题的想法由来已久,应该是从 17 年 12 月份开始的吧,当时就感觉自己魔改的 spfk_x 主题只能勉强符合自己的需求,但是因为已经用的是别人已经完成的框架,大体框架也不好做太多修改(如果要改的话还不如自己重新写一个),而且里面由于经手太多人的修改,里面的代码结构在 Shaun 看起来有点混乱,而且有很多冗余的代码,有些代码甚至完全没有用上,导致修改起来有点麻烦。

  至于为什么不在 17 年寒假期间就开始写,而要拖到 18 年 12 月份才开始动手,一部分原因是 Shaun 的拖延症,一部分原因是还没具体想好该怎么布局,该用什么技术,还有一部分就是外在客观原因了。

  18 年 12 月份开始动手写,是因为这段时间稍显空闲,虽然白天还是一样的忙,但至少没那么多心里负担了,而且此时 Shaun 对于自己想要的大体布局已经十分清楚了(虽然一些具体细节还没不是那么清楚),所以就趁着晚上有时间写写,有时候晚上高兴或没事干就写写,有时候没想法或还没想好具体怎么实现就不写,就这样断断续续的写,总算在 19 年 2 月完成了。

  本来想在 19 年之前完成的,可是由于本人的拖延症和一些客观原因,还是超期了很久,侯世达定律总是存在的( sigh~😔),不过总的开发时间也差不多是一个月吧。感觉花费这一个月的晚上时间还是挺值得的,虽然从中学到的东西不多,但总算也有点自己的东西了。

实现篇

  既然要开始写主题了,首先当然要想一个主题名字,本来最初是直接取原来主题的首尾字母作为名字,但后面想了想感觉有点不对劲,有点骂人的意味在里面 (´ ; ω ; `),后面又想了想,就用尾字母 X 算了,反正站点图标也是那个,后面感觉还是不好,单字母太单一了,后面突然想到希腊字母,就以希腊字母的英语命名了。

  名字想好之后,就要决定用什么技术了,由于 Shaun 只是一个前端小白,无论用什么技术对 Shaun 来说难度都差不多,Shaun 理想的主题是能够支持响应式的,所以为了尽可能简单的支持响应式,CSS 方面的库就采用 Bootstrap4,CSS 预处理器当然是用 Stylus 了, JS 方面最令 Shaun 纠结,到底是采用现代的 Vue 还是传统的 jQuery,最终从相对熟悉程度和相关资料广度方面考虑,Shaun 最终选择了 jQuery3(以后有机会再尝试一下 Vue 吧),接下来要选择的是 Node.js 模板引擎,候选的主要有三个 EJS、Swig 和 Jade(现已更名Pug),Shaun 原来的主题是用 EJS 的,著名的 NexT 主题用的 Swig,而 Pug 的语法目前有点接受不能,所以直接放弃了,而 Swig 早已停止更新,所以也放弃了,所以最终选择的还是相对熟悉的 EJS(而 EJS 最近的一次提交时间为 2018 年 11 月 28 日,看来应该也是要停止更新了,以后有机会试试 Pug 吧)。

  技术决定之后,就要开始写了,最开始写的时候,由于不清楚 hexo 的工作机制,导致完全无法显示页面,后面查阅了几篇关于如何写 hexo 主题的文章,加上自己的慢慢摸索,终于弄懂了,hexo 的最初页面需要有一个 index 文件,在 index 中写入的东西,将会显示在 http://localhost:4000/ 页面中,好了,终于能显示内容了,接下来要解决的是布局问题,hexo 的所有页面渲染都会通过一个叫 layout 的文件,layout 文件中必须存在一个 <%- body %> 语句,在渲染时,hexo 会用其它页面的内容替换 layout 中的 <%- body %> 语句,包括 index 中,所以在 layout 中写好 HTML 框架之后,其它页面只需要负责写内容就行。实现一个最简单的主题(只有纯文字版网页),只需要下面几步:

  1. 在 index.ejs 文件中写入 <%- include('_partial/archive', {item: page, is_index: true}) %>
  2. 然后在 _partial 文件夹中 archive.ejs 文件中写入:
1
2
3
4
5
6
7
8
9
10
<% page.posts.each(function(item) { %>
<%- include('article', {item: item, is_index: true}) %>
<% }) %>

<% if (page.total > 1){ %>
<%- paginator({
prev_text: '&laquo; Prev',
next_text: 'Next &raquo;'
}) %>
<% } %>
  1. 在 _partial 文件夹中 article.ejs 文件中写入 <div><%- item.content %></div>
  2. 最后在 layout.ejs 文件中写入:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
</head>

<body>
<div>
<%- body %>
</div>
</body>

</html>

即可将文章内容显示出来,前提是已有文章。当然也可把上面的1~3步合起来算作一步,直接在 index.ejs 文件中写入:

1
2
3
4
5
6
7
8
9
10
<% page.posts.each(function(post) { %>
<div><%- post.content %></div>
<% }) %>

<% if (page.total > 1){ %>
<%- paginator({
prev_text: '&laquo; Prev',
next_text: 'Next &raquo;'
}) %>
<% } %>

这样就算是实现了一个最简单的主题,只是这个主题十分丑陋,文章也没有对应的标题。

※注: 在写这几步的时候,Shaun 在这里发现 hexo-browsersync 插件和 hexo s 命令有冲突,会造成一个bug:hexo s 启动后,页面加载错误,大BUG,具体表现为:当直接使用 <%- post.content %> 在首页显示文章时,index.html 将会无法加载完全,后面的一部分会加载错误。解决办法为:卸载 hexo-browsersync 插件。

  完成上面几步,就算是写主题已经入门了,万事开头难,入门之后就相对简单了,只要按部就班的一步步走下去就行了,接下来就是慢慢进行排版布局问题,就这样,Shaun 一边参考其它一些主题的排版和写法,一边查阅网上资料实现自己的想法,一步一步的解决问题,直到完成该主题。

PS: 主题配置文件 _config.yml 中的配置变量的命名和其它编程语言一样,不能以数字作为第一个字符

文档篇

  Chi 主题涉及到的技术主要为:Bootstrap4、jQuery3、Stylus、EJS、HTML5、CSS3、ES6 等;运行环境为:node-v8.11.1、Hexo-3(hexo-cli-1.1.0)等;测试浏览器为Firefox 60.5.0 esr 和 Chrome 72.0.3626.109,其它浏览器没测过,也不考虑测,有兴趣的童鞋可以自己测自己改。

  Chi 主题的配色主要参考 Shaun 原来的主题 spfk_x,排版部分参考了 hexo-theme-freemind,部分代码也是来自这两个主题。Chi 主题的一些动画实现优先采用 CSS,除了那些无法用 CSS 实现或 Shaun 不知道如何用 CSS 实现的,所以 JS 代码部分不算很多,网页加载速度也勉强能够接受吧。

  Chi 主题的代码高亮部分本来想参考 为hexo博客加入prettify高亮插件 使用prettify替换默认的代码高亮,但使用了之后发现效果不是很好,就还是使用默认的样式了。

  Chi 主题的使用文档暂时就不写了,毕竟这个主题只是 Shaun 个人使用(应该也不会有其他人使用了,万一真有人要用却不太会用,欢迎提 issue,以后酌情考虑添加使用文档 🙃),由于 Shaun 只是前端小白,这个主题也只是当学习练手开发,里面肯定会有各种各样的问题,欢迎提 issue 或 fork 或 clone 后自行修改,提 issue 的话,Shaun 不保证一定能解决,毕竟水平有限,当然如果有代码优化想法还望不吝赐教 (。・ω・。)ノ♡ (至于 px 换 em 或 rem 就不用提了,没什么原因,主要是 Shaun 懒,更深层的原因在于 Shaun 手头上还没有更高分辨率的显示器,不知道显示差距有多大,不好进行测试 😓)。

PS: Shaun 使用了 Hexo 博客美化代码块| 梦魇小栈 中的脚本对 Markdown 代码部分进行了美化渲染,但是该脚本有时候渲染会出现一些问题,无法进行美化,可能是代码语法格式有错误,这时需要自己手动调整一下 Markdown 代码格式

使用 Chi 主题推荐安装插件:

  • hexo-renderer-pandoc # 使用pandoc渲染markdown,安装前需卸载默认渲染器hexo-renderer-marked
  • hexo-generator-feed # RSS订阅
  • hexo-generator-searchdb # 本地搜索
  • hexo-abbrlink # 文章唯一永久链接
  • hexo-all-minifier # 快速压缩优化代码

好了,暂时就写到这里了。

后记

  当 Shaun 有一天使用其它 blog 框架时,这个主题可能就不再维护更新了,但其中的排版样式应该还会或保留或更新。下面要做的事就不立 Flag 了,因为立了多半会完不成,这就很尴尬了,但不立又没有动力,真是让人脑壳疼啊 (๑•ั็ω•็ั๑),反正是一件长期要做的事了,长到可能接下来的业余时间可能都在做这件事,或开源或不开源,看以后的心情和完成程度了 (↖(^ ω ^)↗)。

Matlab和OpenCV混合编程小结

本文所用的 Matlab 版本为 Matlab R2017b,OpenCV 版本为 opencv-3.4.3,C++ IDE 为 Visual Studio 2017,系统环境为 Windows 10_x64。

前言

  秋招告一段落了,又要回到最初的起点,继续搞(qu)科(hua)研(shui)了,由于前人的代码主要是用 C++ 和 Matlab 混编实现的,而 Shaun 比较熟悉的是 C++ 和 OpenCV,而用 OpenCV 完全重写前人的代码工作量又太大了而且有些 API 不是很好互换,为了方便站在巨人的肩膀上继续前进,所以只能学习一下 OpenCV 和 Matlab 的混合编程了,这样在前人的基础上实现 Shaun 自己的想法相对来说更容易一些。

本文所用的 Matlab 版本为 Matlab R2017b,OpenCV 版本为 opencv-3.4.3,C++ IDE 为 Visual Studio 2017,系统环境为 Windows 10_x64。

前言

  秋招告一段落了,又要回到最初的起点,继续搞(qu)科(hua)研(shui)了,由于前人的代码主要是用 C++ 和 Matlab 混编实现的,而 Shaun 比较熟悉的是 C++ 和 OpenCV,而用 OpenCV 完全重写前人的代码工作量又太大了而且有些 API 不是很好互换,为了方便站在巨人的肩膀上继续前进,所以只能学习一下 OpenCV 和 Matlab 的混合编程了,这样在前人的基础上实现 Shaun 自己的想法相对来说更容易一些。

准备篇

  由于目前主要用 C++ 实现的是一些小功能,也不需要调试,所以就直接使用 VSCode 进行编程了(或许以后还是会用 VS 进行一些简单的调试),而在没有配置相关环境的前提下,VSCode 无法实现自动补全,所以需要在 VSCode 中配置相应环境。具体添加方法如下:在 VSCode 中点击菜单栏 “查看” ==》“命令面板...” ==》选择 “C/Cpp: Edit Configurations...”==》在出现的 c_cpp_properties.json 文件中 "includePath" 对应的值中添加 OpenCV 的 include 目录和 Matlab 的 include 目录,添加之后的 "includePath" 如下:

1
2
3
4
5
6
7
{
"includePath": [
"${workspaceFolder}/**"
, "D:/ProgramFiles/OpenCV/3.4.3/build/include/**"
, "C:/Program Files/MATLAB/R2017b/extern/include/**"
]
}

如此就能在 VSCode 中写 OpenCV 和 Matlab 相关函数时实现自动补全了。

Matlab 和 C++ 混编篇

  由于 Shaun 使用的是 OpenCV 的 C++ 接口,所以需要先知道 Matlab 和 C++ 混合编程如何进行。以实现两个数的加法为例,首先创建一个 mexAdd.cpp 文件,其中 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
#include <mex.h>    // 必须包含头文件 mex.h
#include <iostream>

// 检查输入是否合法
void checkInputs(int nrhs, const mxArray *prhs[])
{
if (nrhs != 2)
{
mexErrMsgTxt("Incorrect number of inputs. Function expects 2 inputs.");
}

if (!mxIsDouble(prhs[0]))
{
mexErrMsgTxt("Input number must be double.");
}
}

double add(double x, double y)
{
return x + y;
}

/**
* nlhs:matlab 函数左边变量个数,即返回值参数个数
* plhs: matlab 函数左边变量,即返回值参数
* nrhs: matlab 右边变量个数,即函数输入参数个数
* prhs: matlab 函数右边变量,即函数输入参数
*/
void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])
{
checkInputs(nrhs, prhs);

// 输入参数可以不使用指针,但输出参数必须使用指针
double *a = nullptr; // 输出参数
double b = 0.0, c = 0.0; // 两个输入参数
plhs[0] = mxCreateDoubleMatrix(1, 1, mxREAL); // 创建1x1的实数矩阵用作输出第一个参数
a = mxGetPr(plhs[0]); // 用指针a指向第一个输出
b = *(mxGetPr(prhs[0])); // b作为第一个输入
c = *(mxGetPr(prhs[1])); // c作为第二个输入
*a = add(b, c); // 计算b、c之和得到a
}

  若要使用 Matlab 混合编译 C++,必须要添加头文件 mex.h,使用 void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]) 函数接收输入输出参数,如此编译完成之后,就和使用普通的 Matlab 函数一样了。具体编译调用方法如下,新建 addTest.m 文件,其中 Matlab 代码如下:

1
2
3
4
5
6
7
8
9
clc, clear, close all;  % 清空变量和关闭所有打开窗口

current_folder = pwd; % 获取当前文件路径
addpath(genpath(current_folder)); % 添加matlab临时搜索路径,并包含子文件夹(matlab退出后该路径不存在)

mex mexAdd.cpp; % 混合编译C++,得到matlab可识别的函数

a = 3.1; b = 2.6;
c = mexAdd(a, b);

其中 mex mexAdd.cpp 可以直接在 Matlab 命令行窗口下预先执行编译动作,编译成功后会输出一个 mexAdd.mexw64 文件,若是 32 位系统则后缀为 mexw32,之后直接执行 ans = mexAdd(3.1, 2.6); 即可在 Matlab 中调用该函数。在 Matlab 首次执行 mex 命令时,Matlab 会自动选择 VS 编译器作为默认 C++ 编译器,也可以执行 mex -setup 初始化或更换默认编译器。

BTW: 最好在安装 Matlab 之前安装 Visual Studio,否则使用 mex 编译时,可能会出现找不到编译器的情况。

Matlab 和 OpenCV 混编篇

  Matlab 和 OpenCV 混编大体上和 C++ 混编差不多,最大的区别在于如何利用 OpenCV 的 cv::Mat 对象和相关的库函数,Matlab 良心的提供了 OpenCV 接口以实现 mexArray 和 cv::Mat 格式之间的互相转化,使用这些接口需要包含头文件 opencvmex.hpp 。如果不使用 Matlab 提供的这些接口而是自己写转换过程的话有点麻烦,因为 Matlab 的数据是以列优先方式存储的,而 OpenCV 的数据是以行优先方式存储的。至于如何进行混编,主要有以下三种方式:

  1. 第一种是自己写 make.m 文件,相当于 gcc 编译时需要的 Makefile 文件,需要手动拼接各种编译命令和添加相应的附加依赖库;
  2. 第二种是通过 Matlab 官方提供的 Computer Vision System Toolbox OpenCV Interface 功能,Matlab 没有默认安装该功能,这个功能需要另外安装,具体安装方法为:在 Matlab 命令行窗口输入 visionSupportPackages,即可弹出“附加功能资源管理器”窗口选择对应附加功能安装即可,安装完之后可通过 mexOpenCV 命令对包含 OpenCV 库函数的 .cpp 文件进行编译,查看 mexOpenCV.m 的源码可知,mexOpenCV 其实是对 mex 命令进行了封装,其调用的 OpenCV 库也是其工具箱自带的 OpenCV,而且有些库还没有包含,有一定的局限性,不过该附加功能自带了些示例程序,可以参考学习一下;
  3. 第三种是使用第三方的 mexopencv,不过需要安装与该工具对应的 OpenCV 版本,并需要进行一定的配置工作,略显麻烦。

  Shaun 这里直接使用的是第一种方式,自己写 make.m 文件,比较灵活,想怎么配置就怎么配置。下面具体以 RGB 转 GRAY 为例,首先新建 mexRGB2GRAY.cpp,其中 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
#include <opencvmex.hpp>

#define _DO_NOT_EXPORT
#if defined(_DO_NOT_EXPORT)
#define DllExport
#else
#define DllExport __declspec(dllexport)
#endif

/**
* Usage: [img_matrix] = mexRGB2GRAY('img.jpg');
* Input: a image file;
* Output: a matrix of image which can be read by Matlab
**/

// 检查输入是否合法
void checkInputs(int nrhs, const mxArray *prhs[])
{
if (nrhs != 1)
{
mexErrMsgTxt("Incorrect number of inputs. Function expects 1 inputs.");
}

if (mxGetNumberOfDimensions(prhs[0]) != 3) // 获取Matlab图像总的维度个数(灰度图为2维,RGB彩色图为3维)
{
mexErrMsgTxt("Incorrect number of dimensions. First input must be a RGB image.");
}

// 检查图像数据类型
if (!mxIsUint8(prhs[0]))
{
mexErrMsgTxt("Template and image must be UINT8.");
}
}

void exit_with_help()
{
mexPrintf("Uasge: [image_matrix] = mexRGB2GRAY('image_file.jpg');\n");
}

static void fakeAnswer(mxArray *plhs[])
{
plhs[0] = mxCreateNumericMatrix(0, 0, mxDOUBLE_CLASS, mxREAL); // 创建一个0x0的空双精度matlab矩阵
}

cv::Mat RGB2GRAY(const mxArray *prhs[])
{
cv::Ptr<cv::Mat> img_cv = ocvMxArrayToMat_uint8(prhs[0], true); // 将unit8数据类型的matlab矩阵转换为OpenCV的mat对象智能指针
if (img_cv.empty())
{
return cv::Mat_<double>(0, 0);
}

// 将RGB转化为GRAY图
cv::Mat gray((*img_cv).size(), CV_8UC1);
if ((*img_cv).channels() == 3)
{
cv::cvtColor(*img_cv, gray, CV_RGB2GRAY);
}
else if((*img_cv).channels() == 4)
{
cv::cvtColor(*img_cv, gray, CV_RGBA2GRAY);
}
else
{
(*img_cv).copyTo(gray);
}

return gray;
}

void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])
{
checkInputs(nrhs, prhs);

if (nrhs == 1)
{
cv::Mat gray = RGB2GRAY(prhs);
plhs[0] = ocvMxArrayFromMat_uint8(gray); // 将unit8数据类型的OpenCV的mat对象转换为matlab矩阵
}
else
{
exit_with_help();
fakeAnswer(plhs);
return ;
}
}

然后新建 makefile.m 文件,自己配置相关编译环境, 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
function makefile()

% 选择相应计算机系统版本
is_64bit = strcmp(computer, 'MACI64') || strcmp(computer, 'GLNXA64') || strcmp(computer, 'PCWIN64');

% 配置OpenCV编译环境,如果系统是64位的,则OpenCV也需要是64位的
out_dir = '.'; % 输出目录,这里为当前目录
CPPFLAGS = ' -O -DNDEBUG -I./ -ID:/ProgramFiles/OpenCV/3.4.3/build/include'; % OpenCV “include” 目录
LDFLAGS = ' -LD:/ProgramFiles/OpenCV/3.4.3/build/x64/vc15/lib -LC:/PROGRA~1/MATLAB/R2017b/extern/lib/win64/microsoft'; % OpenCV “lib” 目录 和 MatLab 附加库目录
LIBS = ' -lopencv_world343 -lmwocvmex'; % 添加OpenCV相关库和Matlab libmwocvmex.lib库

if is_64bit
CPPFLAGS = [CPPFLAGS ' -largeArrayDims'];
end

% 需要编译的 cpp 文件
compile_files = {
'mexRGB2GRAY.cpp'
'mexAdd.cpp'
};

% 开始编译
for k = 1 : length(compile_files)
str = compile_files{k};
fprintf('compilation of: %s\n', str);
str = [str ' -outdir ' out_dir CPPFLAGS LDFLAGS LIBS];
args = regexp(str, '\s+', 'split');
mex(args{:});
end

end

其中 Matlab 配置路径中的 PROGRA~1 是指 Windows 下的 C 盘中的 Program Files 文件夹,为了使用 Matlab 提供的转换接口,libmwocvmex.lib 是必须要添加的一个库。最后具体使用示例 Matlab 代码如下:

1
2
3
4
5
6
7
8
9
10
11
clc, clear, close all;  % 清空变量和关闭所有打开窗口

current_folder = pwd; % 获取当前文件路径
addpath(genpath(current_folder)); % 添加matlab临时搜索路径,并包含子文件夹(matlab退出后该路径不存在)

makefile();

image = imread('lena.jpg');

I = mexRGB2GRAY(image);
figure, imshow(I);

也可以将 makefile(); 函数预先执行。※注: 这里如果出现编译报错 “缺少依赖共享库” 的情况可能还需要把 OpenCV 的 bin 目录加到系统环境变量 Path 中,Shaun 这里是路径 D:\ProgramFiles\OpenCV\3.4.3\build\x64\vc15\bin,然后重启 Matlab

调试篇

  若要对写的 mexAdd.cpp 文件进行调试,则需要

  1. 先使用 mex -g mexAdd.cpp 编译该文件,由于添加了 -g 参数,此时除了会生成 mexAdd.mexw64 文件之外,还会生成一个 mexAdd.mexw64.pdb 文件,该文件即包含调试信息;

  2. 然后在相应 .m 文件中调用 mexAdd 函数的位置设置断点,运行该相关文件,matlab 程序会在调用 mexAdd 函数之前停下;

  3. 此时使用 VS2017 打开 mexAdd.cpp 文件,在需要调试的地方设置好断点,选中菜单栏中的“调试” ==》“附加到进程 ”,或者直接点击菜单栏上的绿色三角 附加...,选中 MATLAB.exe ,点击附加,即可看到 VS2017 调试程序已启动;

  4. 最后回到 matlab 中继续运行相关文件,即可看到程序跳转到 VS2017,并执行到设置断点的地方,此时即可在 VS2017 中按调试 C++ 程序一样对其进行调试。

  完成调试并执行完 mexAdd.cpp 之后,程序将回到 matlab 界面继续执行,直到整个 matlab 程序执行完成。

后记

  这次主要是记录 Matlab 如何调用 C++ 编写的函数,其实还可以用 C++ 调用 Matlab 编写的函数,不过那是另一种混编方式了,以后有机会碰到的话再继续记录吧。

参考资料

[1] Matlab与C++混合编程(依赖OpenCV)

[2] 更改默认编译器

[3] OpenCV Interface Support

[4] Matlab OpenCV混合编程

[5] vc与matlab连接的实用函数简介

Android实践小结

实践环境为:android-studio-bundle-162.4069837-windows(Android Studio 2.3.3 带 Android SDK 版)、该 Android Studio 自带的 JRE,系统环境为 Win10-1607。

前言

  由于上一届没有更新任何项目文档和学习文档,Shaun 只能自己去网上查找相关的资料,从零开始学习,顺便留下一些文档,正所谓:「代码未动,文档先行」,也算是实践出真知吧。

PS: 本次实践的项目主要来自 12.1Android 实战 :DrySister看妹子应用(第一版) — 项目搭建与简单实现http://www.runoob.com/w3cnote_genre/android) 以及目前手头上正在维护的项目。

实践环境为:android-studio-bundle-162.4069837-windows(Android Studio 2.3.3 带 Android SDK 版)、该 Android Studio 自带的 JRE,系统环境为 Win10-1607。

前言

  由于上一届没有更新任何项目文档和学习文档,Shaun 只能自己去网上查找相关的资料,从零开始学习,顺便留下一些文档,正所谓:「代码未动,文档先行」,也算是实践出真知吧。

PS: 本次实践的项目主要来自 12.1Android 实战 :DrySister看妹子应用(第一版) — 项目搭建与简单实现http://www.runoob.com/w3cnote_genre/android) 以及目前手头上正在维护的项目。

布局篇

  首先打开 Android Studio ,新建项目,一路默认下去即可,等待片刻,“MainActivity.java” 文件的错误提示就会自然消失,将左侧栏上方的 “Android” 切换为 “Project”,打开 app -> src -> main -> res -> layout -> activity_main.xml,由于其默认是以 “Text” 的模式打开(Android Studio 右侧有个 “Preview” 标签可以进行当前布局预览),这对于小白来说不大好控制布局,所以需要在该文件底部将 “Text” 切换为 “Design”,如此可以进行拖拽式布局,以下正式开始进入 Android UI 的布局。至于具体如何进行布局设置,可以参考 Android ConstraintLayout 使用指南Android实现拖拽式布局开发----约束性布局 以下两篇资料。

  而若要使控件大小根据屏幕大小自适应,一般可使用相对布局,但如今的 Android Studio 默认新建的页面就是一种类似于相对布局的页面,所以直接在控件中设置 android:layout_width="match_parent" 以及 android:layout_height="match_parent" 属性即可,再设置 layout_margin 属性进行调整即可,而若要让控件大小随控件内的内容自适应,则只需要将 match_parent 更改为 wrap_content 即可。

编码篇

  首先新建自己的业务逻辑 java 代码,具体新建方法可参考 Android studio怎么创建一个Java类文件 ,在 MainActivity.java 文件所在目录上鼠标右键 New -> Java Class,新建完成之后即可编写自己的业务逻辑。若要新建文件夹,则在目录上鼠标右键 New -> Package 。若要使级联目录展开,例如 com.example.admin.myapplicationtest,则需要在该目录的父级目录新建一个 com.example.admin.test Package 即可将com.example.admin 目录展开。

页面跳转

  首先新建一个页面 jump_test_activity,在 \MyApplicationTest\app\src\main\res\layout,即 layout 文件夹上鼠标右键,“New” ==》“Activity” ==》“Empty Activity” (有多种 Activity 样式可供选择,Shaun 这里就以 Empty Activity为例了),随后弹出窗口,在 Activity Name 栏填写页面逻辑控制代码文件名 JumpTestActivity,在 Layout Name 栏填写页面 UI 设计代码文件名 jump_test_activity,在 Package name 以及页面逻辑控制代码文件所在在包名 com.example.admin.myapplicationtest.ui.activity,其它设置为默认即可,这样新建的页面的有个问题就是会有一个丑陋的标题栏,所以还需要去掉该标题栏,具体方法为:

  1. MyApplicationTest\app\src\main\res\values\styles.xml 文件中添加:

    1
    2
    3
    4
    <style name="AppTheme.NoActionBar">
    <item name="windowActionBar">false</item>
    <item name="windowNoTitle">true</item>
    </style>
  2. MyApplicationTest\app\src\main\AndroidManifest.xml 中的 <activity android:name=".ui.activity.JumpTestActivity"></activity> 更改为 <activity android:name=".ui.activity.JumpTestActivity" android:theme="@style/AppTheme.NoActionBar"></activity> ,再次编译运行即可看到标题栏已消失。

好了,准备阶段已经搞完,接下来就是正式的页面跳转了,一般页面跳转是用户点击事件发生的,所以需要添加一个具有点击事件的控件,一般而言就是 Button 了,这里设该 button 的 id 为 page_jump_btn;该 button 所在页面为 MyApplicationTest\app\src\main\res\layout\activity_main.xml,则在对应的逻辑控制文件MyApplicationTest\app\src\main\java\com\example\admin\myapplicationtest\ui\activity\MainActivity.java 中的页面跳转代码为:

1
2
3
4
5
6
7
8
page_jump_btn = (Button) findViewById(R.id.page_jump_btn);
page_jump_btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, JumpTestActivity.class);
MainActivity.this.startActivity(intent);
}
});

如此,在主界面中点击跳转按钮,即可跳转到新建页面。

URL 中的坑

  在学 12.2 的时候,由于请求的URL地址中有中文“福利”,所以其返回的字符串为:

1
2
3
4
{
"error": false,
"results": []
}

可以看到 “results” 的值为空,这显然是错的,后面调试发现需要对 URL 地址转义(这都 8102 年了,为什么 URL 地址中还要有中文,或者说为什么 URL 地址还不支持解析中文 ╮(╯▽╰)╭),具体转义代码如下:

1
2
3
fetch_url = Uri.encode(fetch_url);  // 将URL地址转义
fetch_url = fetch_url.replace("%3A", ":"); // 将%3A替换为:
fetch_url = fetch_url.replace("%2F", "/"); // 将%2F替换为/

主要参考资料为:Android url中文乱码问题及解决办法Android URL encode 空格处理

SQL 语句中的坑

  在使用字符串拼写 SQL 语句时,一定要注意 SQL 语句中的空格,要不然拼起来的 SQL 语句可能语法不通而导致 APP 崩溃。如在使用创建表的 SQL 语句时,可能的错误写法(不注意空格)如下:

1
2
3
4
5
6
7
8
9
10
11
12
String create_table_sql = "CREATE TABLE IF NOT EXISTS" + TableDefine.TABLE_FULI + "("
+ TableDefine.COLUMN_ID + "INTEGER PRIMARY KEY AUTOINCREMENT,"
+ TableDefine.COLUMN_FULI_ID + "TEXT,"
+ TableDefine.COLUMN_FULI_CREATEAT + "TEXT,"
+ TableDefine.COLUMN_FULI_DESC + "TEXT,"
+ TableDefine.COLUMN_FULI_PUBLISHEDAT + "TEXT,"
+ TableDefine.COLUMN_FULI_SOURCE + "TEXT,"
+ TableDefine.COLUMN_FULI_TYPE + "TEXT,"
+ TableDefine.COLUMN_FULI_URL + "TEXT,"
+ TableDefine.COLUMN_FULI_USED + "BOOLEAN,"
+ TableDefine.COLUMN_FULI_WHO + "TEXT"
+ ")";

以上写法无法正确建表,甚至会因为错误 SQL 语句而导致 APP 闪退,正确的写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
String create_table_sql = "CREATE TABLE IF NOT EXISTS " + TableDefine.TABLE_FULI + " ("
+ TableDefine.COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ TableDefine.COLUMN_FULI_ID + " TEXT, "
+ TableDefine.COLUMN_FULI_CREATEAT + " TEXT, "
+ TableDefine.COLUMN_FULI_DESC + " TEXT, "
+ TableDefine.COLUMN_FULI_PUBLISHEDAT + " TEXT, "
+ TableDefine.COLUMN_FULI_SOURCE + " TEXT, "
+ TableDefine.COLUMN_FULI_TYPE + " TEXT, "
+ TableDefine.COLUMN_FULI_URL + " TEXT, "
+ TableDefine.COLUMN_FULI_USED + " BOOLEAN, "
+ TableDefine.COLUMN_FULI_WHO + " TEXT"
+ ")";

调试篇

  编码完成之后,一般而言需要进行调试,Android Studio 的调试可参考 【Android 开发入门】android studio 控制台打印输出日志 ,进入调试模式具体方法为:点击上方工具栏中的 Debug 'app' 图标,而不是直接 Run 'app',进入调试模式之后,点击下方的 “Android Monitor”,切换到 “logcat” 标签,即可查看调试信息。

  华为手机打印调试信息可参考 华为手机logcat不出日志解决方案。这里由于 Shaun 还没有对 AndroidManifest.xml 进行修改,所以该 APP 在点击 Button 的时候会直接闪退,调试窗口出现 java.lang.SecurityException: Permission denied (missing INTERNET permission?) 错误信息,参考 android菜瓜笔记之missing INTERNET permissionSecurityException: Permission denied (missing INTERNET permission?) 可知,该 APP 没有网络权限,所以需要在 AndroidManifest.xml 中添加网络权限,具体在 manifest 标签中添加语句 <uses-permission android:name="android.permission.INTERNET" />,如此该 APP 就能正常执行了。

PS: 如果是对应用发生闪退或崩溃的原因进行调试,建议直接在 logcat 中搜索 fatal 关键字

附录

更换马甲重新发布为另一个 app

  即一样的代码却编译出另一个相同的 app,在某些特殊的需求(使两个功能大致一样的 app 共存在一台手机上)上可能需要这个技巧,具体步骤如下(以 MyApplicationTest 更名为 MyApplication 为例):

  1. 首先将 MyApplicationTest 文件夹(即 APP 根目录)更名为 MyApplication;(这一步不是刚需)
  2. MyApplicationTest\app\src\main\res\values\strings.xml 文件中 <string name="app_name">MyApplicationTest</string> 更改为 <string name="app_name">MyApplication</string>
  3. MyApplicationTest\app\build.gradle 文件中 applicationId "com.example.admin.myapplicationtest" 更改为 applicationId "com.example.admin.myapplication" 。(这一步是刚需)

如此,即可另外安装一个全新 APP,同时保留原有 APP,至于 applicationId 的更多作用和用法可参考 设置应用 ID 。更换 APP 图标只需在 app 文件上鼠标右键, “New” ==》“Image Asset” ,即可弹出设置 app 图标窗口,或者直接更改 AndroidMainfest.xml 文件中的 android:icon 也可。

后记

  排版可能有点乱,毕竟是随便写的,碰到问题就简单的记录一下,Shaun 这次的实践过程可在 AndroidLearning 中查看,页面跳转的方法来自手头上正在开发维护的一个项目,附录中第一个问题的来源是对方提出的一个特殊需求。