本人常用小工具安利

前言

  由于 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

利用回调函数计算函数运行时间

前言

  曾有一段时间在写一个小程序,由于其对运行时间有要求,所以每写一段代码就要测试一下运行时间,如果超出就需要优化一下代码或换一种方法和算法。但是每次都需要插在某两个位置插两段代码感觉有点烦,也有点浪费时间,毕竟浪费时间就是浪费生命,本着保尔柯察金关于生命的言论,Shaun 不愿虚度年华,所以只得寻找一个方便简洁的方法计算运行时间(说了这么多,说到底其实就是懒吧 */ω\*)。后面就想到了回调函数,将想要计算运行时间的代码段放入一个函数中,并将其作为回调函数,用事先写好的计算时间函数调用它,从而方便计算该代码段的运行时间。

前言

  曾有一段时间在写一个小程序,由于其对运行时间有要求,所以每写一段代码就要测试一下运行时间,如果超出就需要优化一下代码或换一种方法和算法。但是每次都需要插在某两个位置插两段代码感觉有点烦,也有点浪费时间,毕竟浪费时间就是浪费生命,本着保尔柯察金关于生命的言论,Shaun 不愿虚度年华,所以只得寻找一个方便简洁的方法计算运行时间(说了这么多,说到底其实就是懒吧 */ω\*)。后面就想到了回调函数,将想要计算运行时间的代码段放入一个函数中,并将其作为回调函数,用事先写好的计算时间函数调用它,从而方便计算该代码段的运行时间。

正文

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
#include <ctime>
#include <cstdio>

#define _CALLED_ printf("The function %s", __FUNCTION__);

// 使用回调函数计算一段代码执行时间
void computeTotalTime(void(*processingCallback)() = 0)
{
clock_t start_time = clock();
processingCallback();
clock_t end_time = clock();
printf(" takes: %fs.\n", (double)(end_time - start_time) / CLOCKS_PER_SEC);
}

void test()
{
for (int i = 0; i < 1000; i++)
{
printf("Hello World!\n");
}
_CALLED_;
}

int main(int argc, char *argv[])
{
computeTotalTime(test);
return 0;
}

以上代码在 Win10 VS2013 中编译运行成功。

后记

  本来是想在网上找一个的,谁知道并没有找到,就只有自己动手实现一个了 ╮(╯_╰)╭。后面使用了一下该函数,发现好像并没有提高生产力 o(╯□╰)o,所以就没人放在网上?-_-!,不过确实从实现过程中学到了一些东西 ↖(^ω^)↗。

参考资料

[1] C/C++之回调函数http://www.cnblogs.com/danshui/category/345046.html

[2] c/c++在windows下获取时间和计算时间差的几种方法总结http://blog.csdn.net/coder_xia/article/category/837943

[3] (转)用宏获取函数名http://www.cnblogs.com/steady/category/264974.html

论如何科学的上网

科学式上网推荐组合:Chrome,Proxy SwitchyOmega,Lantern 等代理工具。

前言

  所谓的科学式上网,懂的自然懂,Shaun 也就不做过多解释了。本来一直在用别人免费提供的 pac 代理,但最近可能别人关掉了,上不了 google 了,就只能另寻他路了。所谓的另寻他路也就是尝试云端框架网站站长 枂下 提供的另外几种科学式上网攻略。本文只是对 枂下 站长的攻略做一下试验记录,若想看原滋原味的攻略,还请移步 云端框架

科学式上网推荐组合:Chrome,Proxy SwitchyOmega,Lantern 等代理工具。

前言

  所谓的科学式上网,懂的自然懂,Shaun 也就不做过多解释了。本来一直在用别人免费提供的 pac 代理,但最近可能别人关掉了,上不了 google 了,就只能另寻他路了。所谓的另寻他路也就是尝试云端框架网站站长 枂下 提供的另外几种科学式上网攻略。本文只是对 枂下 站长的攻略做一下试验记录,若想看原滋原味的攻略,还请移步 云端框架

科学的上网方法

  尽量使用 Chrome 进行科学式上网,因为其有一个代理管理插件 Proxy SwitchyOmega,该插件称之为代理切换神器也不为过,网上大量的教程和配置文件也是基于该神器做的。使用 Proxy SwitchyOmega 需要进行配置,这对初学者有一定的难度,这里 Shaun 推荐直接使用站长 枂下 提供的配置文件,至于 SwitchyOmega 的配置文件可以去站长的 云端框架 网站上去下,也可以联系 Shaun 。至于代理工具请看下文,Shaun 目前也只尝试过使用以下几种工具。

lantern

  其实 Shaun 最先尝试的工具是 XX-NET,但是其配置起来稍显繁琐,而且在第一步的时候必须处在科学式上网环境,而 Lantern 就比较简单了,只要装上之后再稍微动动手脚就可以了,所以就把 lantern 写在第一位了。从站长 枂下 那下载【蓝】灯电脑破解版压缩包,不过 Shaun 觉得应该随便在哪里下载个原版 lantern-2.2.5 安装都可以,只要后续的破解方法一样即可。

  具体破解方法为:主要是令 lantern 一直保持在 2.2.5 版本不变。但是一般来说 lantern 在安装之后会自动更新到最新版(Shaun 在两台电脑上都安装过 lantern,其中一台安装完之后打开 lantern 安装文件夹发现其已更新,而另一台却没有更新,这就有点玄学了 -_-!),至于判断 lantern 有没有更新的办法是:首先进入 lantern 的安装文件夹:C:\Users\XXX\AppData\Roaming\Lantern(将XXX改成自己的用户名),1、看 lantern.exe 文件的修改日期,如果还是 2016 年的,就说明其还没更新;2、显示隐藏文件,看有没有 .lantern.exe.old 文件,如果没有,则也还没更新。如果已经更新了,参考站长 枂下 的说法:

删除lantern.exe文件,修改.lantern.exe.old为lantern.exe

这样就又可以回退至 lantern-2.2.5 版。如果没更新的话就不用进行删除回退这一步,直接进行下一步。

  下一步为修改 lantern-2.2.5.yaml 文件中的更新路径 updateserverurl,使 lantern 永远不再更新,一直维持在 2.2.5 版本不变。具体更改方式为:

将其中的

updateserverurl: https://update.getlantern.org

修改为

updateserverurl: https://pic.black1ce.com

修改完之后保存退出。这里 Shaun 觉得可以随便将其修改成其它路径即可,毕竟只是让其不更新而已,这个路径应该除了更新就没有其它作用了,这纯属 Shaun 拙劣的猜测,有(ai)兴(gao)趣(shi)的童靴可以试试 :-P。

  原本以为到这一步就完成了,但是 枂下 站长后来又补发了一步,就是上面几步只是让 lantern 不再更新,而 500M 流量之后限速的问题仍然存在(Shaun 目前还没超过 500M,所以不知道这个问题,但抱着有备无患的心态先把 枂下 站长的攻略记一下 O(∩_∩)O~),所以接下来才是上正菜,破解“限制500M流量”问题的具体方法为:当使用 lantern 流量超过 500M 时,打开 lantern 的安装目录,打开 lantern-2.2.5.yaml 文件,

修改其中第九行的设备号,随意更换一个数字或者字母即可。

枂下 站长的说法是 8 位随机字母数字大小写均可,只是为方便起见推荐只改动某位即可。

eg:Shaun 目前第 9 行为:deviceid: Gu25Sfoz,一旦 500M 流量用完了,Shaun 就只需要将其修改为 deviceid: Gu25Sfoa 即可。

到这一步 lantern 的破解算是基本完成了吧,如果 枂下 站长有新的更新且被 Shaun 看到的话再进行实验更新吧。


XX-NET

  该工具应该是 Shaun 尝试配置的首款代理工具,不得不说其配置和 lantern 相比实在是太复杂了,而且其中有一步还必须处在科学式网络环境中,Shaun 还是借助别人的 VPN 上的(当时还没用 lantern,所以没用其 500M 免费不限速的流量 ~~o(>_<)o~~)。Shaun 经过实测 XX-NET 无法在 Firefox 中用 google 搜索,一用 google 搜就会报错:

您的连接并不安全

www.google.com 的网站管理员未正确配置网站。为避免您的信息被窃,Firefox 没有与该网站建立连接。

此网站采用了 HTTP 严格传输安全(HSTS)机制,要求 Firefox 只能与其建立安全连接。正因如此,您也不能将此证书加入例外列表。

www.google.com 使用了无效的安全证书。 该证书因为其颁发者证书未知而不被信任。 该服务器可能未发送相应的中间证书。 可能需要导入一个额外的根证书。 错误代码: SEC_ERROR_UNKNOWN_ISSUER

https://www.google.com/search?q=test&ie=utf-8&oe=utf-8

对等端的证书颁发者不受认可。

HTTP 严格传输安全(HSTS):false HTTP 公钥钉扎:true

证书链:

-----BEGIN CERTIFICATE----- MIIDkzCCAnugAwIBAgIQSu4RvcIwnqiEQE6Z68FlaDANBgkqhkiG9w0BAQsFADBz MQswCQYDVQQGEwJDTjERMA8GA1UECAwISW50ZXJuZXQxDzANBgNVBAcMBkNlcm5l dDEQMA4GA1UECgwHR29BZ2VudDEVMBMGA1UECwwMR29BZ2VudCBSb290MRcwFQYD VQQDDA5Hb0FnZW50IFhYLU5ldDAeFw0xNzA5MTYxNDIzMjRaFw0yNzA5MTQxNDMz MjRaMHgxCzAJBgNVBAYTAkNOMREwDwYDVQQIDAhJbnRlcm5ldDEPMA0GA1UEBwwG Q2VybmV0MRcwFQYDVQQLDA5Hb0FnZW50IEJyYW5jaDEVMBMGA1UEAwwMKi5nb29n bGUuY29tMRUwEwYDVQQKDAwqLmdvb2dsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA A4IBDwAwggEKAoIBAQDL3K1OgwalKOJPtO4urpAiu+lioGNax/EIaYR1D2kH66AJ lpal0pYFhXF6MOYCUNfpZIqP5qAQs7JGuRmFdo7rWaLHZ+3S+TlIHdZkoLvyYBcX ENVBcLQvZ7IL7DDUZObK/R7OOKz82dEoITQnT+q/lecR9wQ7QNdNVNqn0xS0NPt7 bS76irMxkJcO2q7Lu4R56ImCox/G7dUEepjL0Po516l6fLKG3qi5org2z6ap0yl2 Etu8cRfqiqaqhO0HI1Twz+Rbp/8KUdUBgnNkjcod83HE+jJKxIUDmn18+l7J8sBi a0JvWSIYy2ccFXoR8L4lfvIa8PhTuMmpxyDkwDdPAgMBAAGjHjAcMBoGA1UdEQEB /wQQMA6CDCouZ29vZ2xlLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEADiM6yWCaGNLn ggirjN0b34j5JmjgYYx3bRaKDe4We2emjlLsdskBo2ztkd/tPBfUa7DWExgFPvVq B2FeEf85Zj72kMmc2JikJBtPF1qK9fa4O1gST4VE0xIF99zGrgkDhGaYd1ocElWS qfBNQfzwsO+nl2OQf99ATMqMSCGacN7z+LJBLn65de+ODzYUkIHzhU5/xJMian3y fQzNFCAgK8OMf16excqRUcX8zfGPfvtAafDrdOYEXcGayLIvt4tGr8T+tii+MtCR O5hXK8/ABMLGI74zgLYloVFjJv21VsLrNCvvD43T5E3c+8d1MENozdEnsyzWkTkp knP4aEiLOQ== -----END CERTIFICATE-----

Shaun 不知道为什么 (+﹏+)~。所以为了能正常使用科学式网络,还是老老实实的用 Chrome 吧,何况其还有 Switchyomega 神器,并且建议把 Chrome 设置为默认浏览器。具体配置流程如下:

  从 枂下 站长那下载 XX-NET(Shaun 其实最先是直接在 GitHub 上下载最新的 XX-NET,但 Shaun 由于在 Firefox 上尝试失败了,当时也没在 Chrome 上尝试,以为最新版 XX-NET 有问题,后来用上 枂下 站长那下的 XX-NET 在 Chrome 上试了下可以,而 Firefox 不行,就知道可能是 Firefox 的问题,而既然已经能用了,Shaun 也没用最新的 XX-NET 在 Chrome 上尝试了),直接解压到某个文件夹,然后将 XX-Net-3.3.6 文件夹重命名为 XX-Net,即去掉末尾的版本号,据 枂下 站长说法是为了减少后续 XX-NET 出错(Shaun 这里老老实实的照 枂下 站长说的做了,所以也不知道如果没去掉版本号会有什么后果)。接着以管理员身份运行 XX-Net 目录下的 start.vbs 文件(这里右键是没有“以管理员身份运行”选项的,要想以管理员身份运行就只有使用 Windows 命令行了,具体做法就是以管理员身份运行“命令提示符”,再在其中运行 start.vbs 文件即可),运行成功后将弹出

已经导入GoAgent证书,请重启浏览器.

点击确定即可。再次启动默认(Chrome)浏览器,将打开 127.0.0.1:8085 页面,即为 XX-NET 的配置界面。将看到 GAEProxy 状态信息(可能是由于 google 取消了公共 APPID,所以 Shaun 看到的不是“您正在使用公共APPID,....”这条消息,而是另外一条消息(具体什么消息 Shaun 忘记了 o(╯□╰)o)),打开显示详细信息(其实也没用,Shaun 并看不懂这么多 -_-|||),先放这里吧,部分信息以后再说,先进入正式配置步骤。

  1. 首先点击左边的“高级”选项,据枂下站长说,将自动调整扫描线程数关掉,最大扫描线程数设为 200,点击提交(可能这样连接速度更快一些)。Shaun 这里这里没有照做,而是保持默认设置,Shaun 只是要求能上就可以了,对速度要求可以稍微放松一点(或许以后会调成站长推荐的配置)。
  2. 接下来点击“部署服务端”选项,填写 AppID,点击“开始部署”。但是这一步,Shaun 并没有 AppID,所以只能上 google 申请,这样最麻烦的一步就来了。照着 枂下 站长的指引,Shaun 一步步的申请了 AppID。具体申请步骤如下(这一步需要登录 google 账号,必须处于科学式网络环境):
    1. 点击 打开Appid申请页面,登录 google 账号,创建项目,并在新建项目中修改项目 ID(这就是第一个 AppID ),为方便,建议直接以“项目名-00”的方式按顺序命名 AppID,创建成功后继续点击“Google Cloud Platform”旁的一个小三角,点击弹框的“+”,按上述方式进行创建新的 AppID,如此重复,Shaun 总共创建 10 个 AppID,10 个之后会提示配额已用完,硬是要创建只会覆盖掉第一个 AppID。
    2. 接下来就是需要选择语言和地区,只有为每个 AppID 选择语言和地区之后,该 AppID 才会生效。语言和地区的选择界面可以从“App Engine”界面中进入,也可以直接在添加 AppID 界面的那个弹框中点击相应的 AppID 进入,当然如果是第一个 AppID 选择语言可以直接点击界面上的“选择一种语言” 。语言选择 Python,地区选择亚洲(asia-northeast1),选错了后果自负(当然可以覆盖掉该 AppID 重新设置)。其实可以照 枂下 站长那样设置完第一个 AppID 的语言和地区之后直接修改浏览器的地址栏的 url 以快速设置 AppID 的语言和地区。具体修改方法为:将 url 地址末尾的 project 参数的值改为你想要设置的下一个 AppID,eg: Shaun 当前 AppID 为 test-00,对应的浏览器 url 地址末尾的参数为 lang=python&project=test-00,Shaun 想设置的下一个 AppID 为 test-01,则只需将其修改为 lang=python&project=test-01 回车即可快速设置 test-01 的语言和地区。如此重复,就可以设置完全部的 AppID 语言和地区。设置完之后,这些 AppID 就能进行部署了(据说每个 AppID 每天有 1G 的流量可以使用,并于每天下午三点更新,也就是 Shaun 每天有 10G 流量,一般是够用了 :-D)。
  3. 将上文申请并设置完成的 AppID 放入“GAE AppID”文本框中,多个 AppID 可以按这样的格式放入:test-01|test-02|test-03,两个 AppID 以|分隔即可。点击“开始部署”,会弹出一个登录 google 账号的标签页,登录并允许即可,等待 2~5 分钟,可发现日志页面出现 Done!Deploy 10 appid successed. 等字样即表示服务端部署成功。
  4. 点击“部署”选项,将上面部署服务端的 AppID 以相同格式输入“GAE AppID”文本框中,点击“保存”即可。保存完之后,即可在状态信息界面显示详细信息中 Appid 发现当前工作 AppID 就是部署的 AppID ,至于那个配置下的监听代理就是设置代理的地址和端口。

这样 XX-NET 就算是配置完成了,至于 枂下 站长说的扫描 ip,Shaun 就没做实验了,因为这样配置完就可以科学式上网了。


  以上代理工具配置完成后,即可在 Chrome 中畅游 Internet 了,但是正确的科学式上网姿势应该是:国内的网站走本地连接,而国外被屏蔽的网站才走代理。这就需要 Proxy SwitchyOmega 这款插件了,它能按照一定的规则自动选择走本地还是走代理,这样既不会浪费流量,也能使国内的网站联网速度不受影响。导入前文推荐的配置文件后,就可选择对应的代理方式。这里当然是选择自动切换,至于虚拟切换是选择 Lantern for 8787 还是 XXNET for GAE 就随便个人的喜好了(在走代理的时候别忘了把相应的代理工具开启);如果直接选择其中一种代理方式就相当于全局代理,这也就失去这款插件的作用,只有自动切换加上虚拟切换才能充分发挥这款神器的真正作用。

匿名网络

  要想使用匿名网络,当然少不了专用的浏览器:Tor Browser,下载并安装(下载时需要身处科学式网络环境,安装时最好改变一下目录,而且路径中最好不要有中文)。接下来就是配置了 Tor 网络了。具体配置流程如下:

  首先,它问直接连接 Tor 网络还是配置网桥或代理,这里当然是选择配置;其次它问互联网服务提供商( ISP )是否对 Tor 网络连接进行了封锁或审查,这里选,据枂下站长所说因为国内网桥大部分已失效,连接网桥没有意义还会拖慢速度;然后它问是否本地代理访问互联网,这里当然选择;最后填写本地代理配置,这里需要注意,枂下站长提供的部分代理配置是:

SSR/SS Socks5//127.0.0.1 : 1080

Seed HTTP//127.0.0.1 : 1080

Lantern Socks5//127.0.0.1 : 8287(2系列),三系列的在Lantern设置页面查看

Psiphon 可以在配置页面自定义

  其中经 Shaun 实测,上面 Lantern 的代理配置是连接不上的,Shaun 后来参考 SwitchyOmega 配置文件中 Lantern 的代理为 HTTP//127.0.0.1 : 8787,经尝试如此配置可以连接 Tor 网络,所以 Lantern 的正确配置应为:

Lantern HTTP//127.0.0.1 : 8787(2系列),三系列的在Lantern设置页面查看

设置完成后等待片刻就能连上 Tor 网络了,最好就保持原来的 DuckDuckGo 搜索引擎,不要更改,接下来就可随心所欲的畅游 Internet 了。

  至于想访问暗网,可以参考Hacking/整理的暗网网址Tor.txtWorking Links to the Deep Web或者直接用站长枂下给的网址:torlinkbgs6aabns.onionxmh57jrzrnw6insl.onion

枂下站长回答:

XX-Net可以作tor的前置代理吗?不行的,xx-net是假http协议

所以 XX-NET 不能用作 Tor 的代理配置。

  最后再简要记录一下 Chrome 调用 Tor Browser 的代理吧。Shaun 没有像 枂下 站长那样用命令行去实验,只是享受了一下 ta 的试验成果(O(∩_∩)O谢谢)。总而言之,还是利用 SwitchyOmega,代理方式选择 Tor for 9150,就可以在 Chrome 中调用 Tor Browser 的代理,畅游 Internet 了。


更新于:2017-11-27

  时久达期间及之后,lantern 的那种破解方式从时灵时不灵,到完全失效,而 XX-NET 则一开始就失效了,这见证了 Google 和 GFW 的斗智斗勇(๑乛◡乛๑),但很明显,google 失败了,事实证明没有 GFW 封不了的,只是看它想不想封 (๑•ั็ω•็ั๑)。至于 lantern 和 XX-Net 的复活方式请移步 枂下 站长的网站,Shaun 这里就不再赘述了。这里需要更新的一点是:Tor 的网桥配置采用“meet-amazon”(亚马逊的云计算平台)或者“meet-azure”(微软的云计算平台)传输也能实现科学式上网,但速度很慢,仅能浏览网页而已,可以当做备选临时用用。


更新于 2017-12-24:今天使用了下 赛风(Psiphon),其操作完全傻瓜式,简直不要太好用,而且为单个绿色文件,携带也方便,用的时候只要设置一下端口就行。不出意外的话,妈妈再也不用担心 Shaun 搞科研了(؏؏☝ᖗ乛◡乛ᖘ☝؏؏)。


更新于 2018-03-14:现在最新版的 Firefox 59.0 也有 Proxy SwitchyOmega 插件了,虽然还是测试版,但还是能够正常使用,既然 Firefox 有了这款神器,Shaun 也就有动力将 Firefox 使用 XX-Net 连接 Google 搜索出现的问题解决了,首先进入 Firefox 的 「选项」或设置 ==》「隐私安全」,下滑到最下面的「证书」,点击「查看证书」,在弹框中选中「证书机构」,点击「导入」,添加 XX-Net\data\gae_proxy目录下的 CA.crt 证书,在导入中出现的弹框全部选中信任,将会在上方的证书栏中出现 GoAgent XX-Net 证书,这样就能解决上文出现的问题,参考自:【已解决】Firefox报错:github.com 使用了无效的安全证书。 证书因为未提供证书发行链信而不被信任。 (错误码: sec_error_unknown_issuer)


后记

  最后的匿名网络是 Shaun 弄着好玩的,像暗网这种东西 Shaun 这种遵纪守法的好公民才不会访问呢 (ಡωಡ)。等以后时机到了再去买个国外 VPS 自己搭建一个科学式上网环境吧。最后感谢 枂下 站长的无私分享。

附录

  原本还以为 Shaun 搭建的 Hexo+GitHub 个人博客站点还是个深网,没想到搞完科学式上网后用 google 搜索竟然能搜到,虽然 Shaun 没做什么,但 google 仍然能搜到,google 的蜘蛛还挺厉害的,不过如果百度的蜘蛛没被 GitHub 屏蔽的话百度可能也能搜到(从某些原因上来说,GitHub 把百度屏蔽掉也好 O(∩_∩)O~)。既然已经被 google 收录了,Shaun 也就不去搞那个站点地图了,等以后想搞 SEO 了再去做吧。

参考资料

[1] Switchyomega超详细教程之Chrome与Firefox版本

[2] 【蓝】灯电脑破解版之2系列禁止自动升级最终办法

[3] XX-NET史上最详细完整教程

[4] XX-NET史上最详细完整教程之第一部分:Appid创建部分

[5] Tor Browser在国内Windows平台下的超详细教程

[6] Chrome等其他程序如何完美调用Tor Browser的代理来上网

[7] “如何翻墙”系列:TOR 已复活——meek 流量混淆插件的安装、优化、原理https://program-think.blogspot.com/search/label/IT

[8] 如何翻墙?——写在 BlogSpot 被封之后 {2015-08-28}

Hexo的SPFK主题修改小记

前言

  Shaun 一直在对 Hexo 的 SPFK 主题进行持续修改以符合 Shaun 自己的需求,在修改当中也会遇到一些小问题,以防遇到重复问题,特此记录所遇小问题,至于大问题可能会另外开篇。

前言

  Shaun 一直在对 Hexo 的 SPFK 主题进行持续修改以符合 Shaun 自己的需求,在修改当中也会遇到一些小问题,以防遇到重复问题,特此记录所遇小问题,至于大问题可能会另外开篇。

修改篇

1、修改 aboutme 排版问题

修改日期:2017-09-16

需求描述:Shaun 为了使 aboutme 排版好看一点,使“关于我”的内容更有段落感,Shaun 尝试在主题配置文件中 aboutme 对象的内容添加各种换行转义符号均于事无补,如 \n\r\n&#13;&#10;<br /> 等,站点不仅不会换行,还会直接将转义符号都显示出来 (╯﹏╰)。

解决办法:既然 Shaun 基本把所有的换行方法都试过了,还没有任何作用,那就只能是问题出在其它地方了。Shaun 首先找到显示 aboutme 内容的地方,其位于主题文件夹下 \layout\_partial\left-col.ejs,显示 aboutme 内容的代码为 <div id="js-aboutme"><%=theme.aboutme%></div>,查阅相关资料,具体为 与大家分享ejs源码阅读心得,其中有这样一段话:

关于ejs模板的五种模式对应几种指令

ejs主要提供了如下几种指令:

  • <%, 该指令主要通过js中的eval来执行js代码, 如上模板代码<% [1,2].forEach(function(v){ %>将通过eval编译成; [1,2].forEach(function(v){即直接可执行的js代码, 并且不会存放到__output函数中输出.
  • <%=, 该指令主要用于输出变量内容, 如上模板代码<%= v %>将通过escape函数编译成__append(escape(v)), 可以看到该指令用于输出变量内容, 最后将通过__output输出内容.
  • <%-, 该指令与<%=区别是, <%=指令使用escape函数来对特殊字符进行编码, 如将>转为%3E, 查看关于escape函数.
  • <%#, 该指令主要用于模板内注释, 既不会执行也不会输出.
  • <%%, 主要用于输出字面值%.

关于以上各个指令对应的解析, 可参考ejs源码根目录lib/ejs.js文件中的scanLine函数.

从中可得知 <%= 指令会将变量内容中一些特殊字符先转义,再原封不动的输出,所以 Shaun 无论怎么修改主题配置文件中 aboutme 对象的值,其输出内容都会是原封不动的 aboutme 对象的值。为了让其输出内容可以有相应的特殊格式,就不能让其转义,只能用 <%- 指令,将其修改为 <div id="js-aboutme"><%-theme.aboutme%></div>,这样就能使输出内容可以自定义特殊格式,Shaun 最后在 aboutme 对象的内容中需要换行的地方添加了 <br />,实测如此修改后可以换行。

2、给左栏添加滚动条

修改日期:2017-09-18

需求描述:SPFK 主题是双栏的主题。因为左栏主要是用来显示一些菜单和头像等内容,这些内容也不多,所以原作者就没有添加滚动条。但是由于 Shaun 添加了个本地搜索功能,在刚开始文章少的时候还不受影响,但是随着文章的增多,搜索功能就会影响左栏的布局,这是就必须添加一个滚动条了。本以为添加滚动条很简单,就是添加一个 overflow: auto;,谁知道还没这么简单 ╮(╯﹏╰)╭。

解决办法:Shaun 对问题的定位没问题,就是修改主题文件夹下的 \source\css\_partial\main.styl 文件中 .left-col 样式,问题在于怎么修改,本想直接在其中加入 overflow: auto;,按道理说问题就能解决的,但是 Shaun 去搜索试试,发现搜索框上方的头像,文字等全部消失了,滚动条没起到作用,而下方的菜单可以通过滚动条看到。于是 Shaun 觉得可能是 div 上界没撑开,而超出的地方却隐藏了,但下界为什么能撑开,Shaun 这里还是很不明白 ?_?。既然是这里隐藏了,Shaun 就去看相关标签有没有 overflow: hidden; 属性,谁知道要么是没有,要么是即是关闭了也没有作用,那问题应该不是出在这里。就只能是这些元素所在的子 div 里了,Shaun 找到其子 div 属性 .intrude-less,其中虽有 overflow: auto; 但没设置 height 属性,所以就不能发挥其作用,Shaun 于是给它加上 height 属性,搜索后发现有两个滚动条,这显然不简约,于是 Shaun 把 .intrude-less 的 overflow: auto; 属性注释掉,没想到居然能完美解决问题,可能是因为加上高度属性之后就能撑大父元素 div 了吧(来自某业余前端的猜测 (⊙_⊙))。后面为了更美观,Shaun 把下方菜单区域的 div 样式 .switch-area 高度 min-height 改小了一点,顺便也把主题文件夹下的 \layout\_partial\left-col.ejs 文件中首行注释掉 <!-- <div class="overlay"></div> --> 。Shaun 也曾想把 height 改为 min-height,谁知道又出现相同的问题,不得不又改回去。虽然这次已经解决了问题,但有些细节问题还是不太明白,只有等以后前端水平上去了再去想了,如有大佬知道还望不吝赐教 (^人^)。

3、更换鼠标指针

修改日期:2017-9-26 ~ 2017-9-27

需求描述:Shaun 在玩《Ori and the Blind Forest》这款游戏的时候觉得其鼠标指针很酷炫,于是想把其鼠标指针放在 Shaun 的博客站点中 (๑´ڡ`๑) 。

解决办法:要想更改指针,首先需要找到对应的指针文件,最终在万能的贴吧得到指引,在 RealWorld Graphics 上找到两个 ori指针 文件,一个是 动态的 ani指针文件,还有一个是 静态的 cur指针文件(好像该游戏的作者也在 steam 上的评论中提供了游戏中的指针文件,详见:I wanna use this games cursor. )。既然已经找到了指针文件,就可以开始更换炫酷的鼠标指针 (•̀ᴗ•́)。具体更改方法如下:将下好的指针文件放在主题文件下的 \source\img 文件夹中,在主题配置文件中添加 cursor 属性:

1
2
3
4
5
# set cursor | 设置鼠标指针图标
cursor:
on: true
cursor_0: img/cursor.ani # 首选指针
cursor_1: img/cursor.cur # 备选指针

其中 cursor_0 和 cursor_1 代表使用哪个指针,因为 firefox 和 chrome 不支持 ani文件 的指针(好像是 ani 文件有很大的漏洞),所以 ani 动态指针是用不了的,只能用 cur 格式的静态指针,而 IE 是可以加载 ani 格式的动态指针,所以 Shaun 这里就将两个指针文件全放上去了,首选加载动态指针;最后增加相应的代码调用 cursor 属性,加载指针文件,在主题文件夹下 /layout/_partial/background.ejs 文件末尾添加:

1
2
3
4
5
6
7
8
<% if (theme.cursor.on){ %>
<style>
body{
background: #3f3f3f;
cursor: url(<%- config.root %><%- theme.cursor.cursor_0 %>), url("<%- config.root %><%- theme.cursor.cursor_1 %>"), auto;
}
</style>
<% } %>

如此更新站点之后即可使用新鼠标指针样式,可能需要先进行 hexo clean 再发布。

BTW:这次修改是一个月之前的了,当时不知怎么的忘记记录了,还好 Shaun 的 git 提交记录比较详细,对应的提交记录为:add a function -- change cursorupdate set cursor function

4、修改打赏问题

修改日期:2017-10-13

问题描述:Shaun 突然想玩一下那个打赏小东西,但照配置文件中指示的那样在文章开头 ymal 格式中加入 reward: true 属性,没有任何作用,于是去主题文件夹搜索 reward 属性相应的代码,结果是“找不到结果”(坑爹了这是,摔!(╯‵□′)╯︵┴─┴ )。

解决办法:既然 reward 属性找不到就只有搜索 reward_type 属性,最终在主题文件夹下 \spfk_c\layout\_partial\article.ejs 文件中找到这样一条语句 <% if ((theme.reward_type === 2 || (theme.reward_type === 1 && post.toc)) && !index){ %>,其下面就是打赏相关的代码,查看 SPFK 主题原作者介绍信息(Hexo 主题:SPFK)发现 toc 属性是用来显示目录的(一个用来打赏的代码怎么与文章目录相关了 -_-#),所以上面的 toc 应该改成 reward,修改后的代码为 <% if ((theme.reward_type === 2 || (theme.reward_type === 1 && post.reward)) && !index){ %>,这时照配置文件中指示的那样在文章开头 ymal 格式中加入 reward: true 属性就能在相应的文章后面看到一个大大的“”字。

本来写到这里应该打赏这玩意应该完结了,但 Shaun 无意中在该文件的下面发现这样一段代码:

1
2
3
<% if (!index && post.toc != false && !is_page()){ %>
<%- partial('_partial/toc') %>
<% } %>

这是和 toc(即文章目录)真正相关的代码,功能大概就是判断是否加载文章目录相关的代码,如果在文章开头设置 toc: false,则该文章不会显示目录,但是如果在文章中不加 toc 属性,也会显示文章目录,但上面的打赏却不会显示,看起来 post.toc != falsepost.toc 应该逻辑差不多,这里是 Shaun 感到十分奇怪的一个地方?后面查阅相关资料(JavaScript undefined 属性)得知:

注释:null 表示无值,而 undefined 表示一个未声明的变量,或已声明但没有赋值的变量,或一个并不存在的对象属性。

而本文这里因为没有在文章开头设置 toc 属性,所以其为 undefined,其既不为 false 也不为 true,只为 undefined,当在 if 语句中做判断,会执行 else 分支,作 ! 运算,结果则为:true。所以 if(post.toc) 不能执行其下代码,因为 post.toc 为undefined,不为 true 也不为 false,而 if(post.toc != false) 能执行其下代码,因为 post.toc != false 为真。至于 javascript 中 if(a == ture)if(a) 的区别具体为:前一种是 a 必须为 1 或者 true 才执行;而后一种只要 a 不为 false undefined null 0 -0 NaN "" 这 7 个字符中的其中任何一个都能执行。

5、交换内容栏和左侧栏位置

修改日期:2017-11-22 ~ 2017-11-23

需求描述:Shaun 最近逛网站时发现,好像一些博客网站基本都是把内容放在左侧,百度和 google 的搜索结果也是在左侧,可能是内容在左侧要好一点吧,于是 Shaun 略微修改之后,将内容放在左侧,而原来的左侧栏放到最右侧,好像是顺眼了一点(不排除是心理作用 (ಡωಡ)),如果以后还觉得不错的话,再把相关的变量名换掉吧(此次修改仅仅是将 CSS 相关的值改变,div 类名没变)。

解决办法:首先当然是定位左侧栏 .left-col,它在主题文件夹下 source/css/_partial/main.styl 文件中,为其添加 right: 0px; 属性,使左侧栏靠右侧停放;在定位内容栏 .mid-col ,将 right:0; 改成 left:0;,将 left: 300px; 改成 right: left-col-width;,使内容栏靠左侧停放,同时使其距离右侧有左侧栏的宽度。最后就是再修改其他一些小东西(比如目录按钮和目录内容 div 等)的 css 值,关于这个 Shaun 就不细述了,反正也就是更改 left、right、bottom 以及 top 属性及其值,具体修改了哪些内容可以见 Shaun github 提交记录。

BTW:诶呀呀!昨天忘记测试手机端,今天用手机打开一看,手机端页面也距右边 left-col-width 宽度,这使得内容全挤在一起了,完全没法看 ರ_ರ ...。所以不得不添加手机端样式,定位手机端 .mid-col,它在主题文件夹下 /source/css/_partial/mobile.styl 文件中,为其添加 right: 0; 属性值;后面看见回到顶部、回到底部的导航栏也有点问题,就在该文件中 /*导航*/ 下面 .scroll 上面添加:

1
2
3
#scroll{
right: 0;
}

使得该导航栏靠右停靠。

6、更新站点部分 CSS 文件和代码结构

修改日期:2017-12-04 ~ 2017-12-06

更新日志:

  1. Shaun 将 left-col 相关的东西(比如 css 样式和 ejs 文件)全部重名为 right-col,毕竟经过几天的适应,感觉放在右侧还不错,就干脆也将其重命名算了,所以原左侧栏 left-col 从现在开始就完全变成右侧栏 right-col 了;

  2. 原来的本地搜索框有两个 .search 样式,本次修改将两个 .search 样式合并了,删除重复的样式,只留下一个合并后的 .search 样式;Shaun 同时还优化了一下本地搜索功能的结构,将原来主题配置文件中的 search_box 属性删掉,给 search 属性添加一个 on 的属性来代替 search_box 属性,这样让结构不那么混乱,只由一个 search 属性决定本地搜索功能的开启和关闭及功能的实现,而不是像以前那样由 search_box 属性决定右侧栏搜索框的显示,而 search 属性决定本地搜索功能的实现;

  3. 更改右侧栏 right-col 的 overflow 样式,原来是右侧栏 right-col 垂直超出滚动,水平超出隐藏,这样在屏幕比较窄的情况在右侧会出现两条滚动条,很不美观。现在 Shaun 将 overflow 样式改成 &:hover {overflow-y: auto; overflow-x: hidden;} ,这样只有在鼠标指针悬浮在右侧栏 right-col 上时才会再右侧栏出现滚动条,这样虽然不能从根本上解决问题,但稍微缓解了一下,等以后再看能不能彻底解决滚动条的问题 ರ_ರ ...;

  4. Shaun 以前添加 RevolverMaps 这个小部件的时候只是简单粗暴的添加 div 及对应的样式,完全没考虑到主题的扩展性和易修改性。于是 Shaun 将其改成配置文件的形式,在主题配置文件中添加 visual_visitor 属性,只要将其值设置为 RevolverMaps官网 获取的那串 script code,eg:

    <script type="text/javascript" src="//ra.revolvermaps.com/0/0/8.js?i=0lpycb5p234&amp;m=7&amp;c=ff0000&amp;cr1=ffffff&amp;f=arial&amp;l=49" async="async"></script>,即可在右侧栏菜单下的访问情况中看到一个 3D 地球实时显示访客的位置信息,Shaun 为了优化异步访问信息,将其中的 async="async" 改成 defer="defer",这样好像能优化加载次序。这两者的区别可参考 defer和async的区别,好像是都能异步加载,只是 async 是该 script 加载完立即执行,而 defer 是该 script 加载完之后在整个页面结束加载之前执行,也就是最后执行的;

  5. 最后还修改了 MathJax 的 CDN 地址及配置属性。MathJax 的配置属性可参考 加载和配置MathJax,具体如下:

    第一种配置Mahtjax的方法就是使用配置文件。MathJax附带了很多种预制配置文件。它们存储在MathJax/config 目录。主要有其中以下几个:

    • default.js:这个文件包含了所有MathJax可用的配置选项,并附有注释和说明,你可以编辑它们来满足你的需要。
    • TeX-AMS-MML_HTMLorMML.js:允许使用 TeX, LaTeX, 或者MathML 符号书写公式。如果浏览器支持就处理为MathML,否则就使用Html和Css渲染。
    • TeX-AMS_HTML.js:允许使用 TeX 或者 LaTeX 符号书写公式。使用Html和Css渲染。
    • MML_HTMLorMML.js:允许使用 MathML 符号书写公式。如果浏览器支持就处理为MathML,否则就使用Html和Css渲染。
    • AM_HTMLorMML.js:允许使用 AsciiMath 符号书写。如果浏览器支持就处理为MathML,否则就使用Html和Css渲染。
    • TeX-AMS-MML_SVG.js:允许使用 TeX, LaTeX, 或者MathML 符号书写公式。使用SVG产生输出。
    • TeX-MML-AM_HTMLorMML.js:允许使用 TeX, LaTeX,MathML,或者 AsciiMath 符号书写公式。如果浏览器支持就处理为MathML,否则就使用Html和Css渲染。

    第一个文件是提供给你修改的。它基本上包含了MathJax的所有配置选项,同时有注释解释。其他的文件就是我们联合配置文件。它们不仅仅配置Mathjax,还预加载了一些配置所需的文件。这些文件内容在 联合配置 中有详细的解释。

    原来的 CDN 地址 cdn.mathjax.org 已经在 2017-04-30 日关闭,所以必须更新 CDN 地址,其推荐的 CDN 地址为 cdnjs.cloudflare.com/ajax/libs/mathjax,而新的 MathJax 也提供一种一种新的配置文件 TeX-MML-AM_CHTML(允许使用 TeX, LaTeX,MathML或者 AsciiMath 符号书写公式,使用 CommonHTML 产生输出),新的 MathJax 推荐使用的就是这种配置文件,因为它计划在 V3.0 将 HTML-CSS 输出格式丢弃,只留下 CommonHTML 和 SVG 这两种输出格式。而且新的 CDN 地址不支持 /latest/MathJax.js 这种格式,必须指定一个确定的版本,截止 Shaun 此次修改日期之前,最新的版本为 2.7.2,所以比较推荐的一种加载格式为:

    <script type="text/javascript" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js?config=TeX-MML-AM_CHTML"></script>

7、添加 404 页面和一些插件等

修改日期:2017-12-09 ~ 2017-12-10

更新日志:

  1. 修改文章内 a 标签的高度,即 .article-entry p a 中 padding 的上下边距。文章内的 a 标签在外面加个虚线框本来就很突出了,还设置上下 padding 为 8px,这就显得有点浮夸了 :) 。最后将其上下边距设为 0px;

  2. 添加 404 页面,具体参考自:在 Hexo 中创建匹配主题的404页面

    1. 启动 Git Bash,进入 Hexo 所在文件夹,输入 hexo new page 404
    2. 打开刚新建的页面文件,默认在 Hexo 文件夹根目录下 /source/404/index.md
    3. 在顶部插入一行,写上permalink: /404,这表示指定该页固定链接为 http://"主页"/404.html
    1
    2
    3
    4
    5
    title: 404 Not Found:该页面无法显示
    toc: false
    comments: false
    permalink: /404
    ---
  3. 添加 hexo-abbrlink 插件,使文章生成唯一永久链接。这个插件最好是在建站之初就加上,不然写了很多文章之后又都得重新生成链接,搜索引擎需要再次抓取新链接,不利于 SEO,Shaun 这里也就只有等 Google 慢慢抓取更新,还好写的不多,不算太迟。安装完之后 Shaun 在站点配置文件中添加:

    1
    2
    3
    4
    permalink: posts/:abbrlink.html # 需安装hexo-abbrlink插件
    abbrlink:
    alg: crc32 # 算法:crc16(default) and crc32
    rep: hex # 进制:dec(default) and hex

    ※BTW:需要将原来的 permalink 注释掉或直接删除。

    更多设置可参考 hexo-abbrlink

  4. 添加 hexo-all-minifier 插件,快速压缩代码,分别对 html、css、js、images 进行优化。Shaun 这里就直接使用推荐的配置了,直接在站点配置文件中添加:

    1
    all_minifier: true  # 需安装hexo-all-minifier插件

    更多设置可参考 hexo-all-minifier

  5. 原来文章标题不可点击,反而日期可点击,这有点奇怪 ◔ ‸◔?。本次修改之后,点击文章标题即为刷新页面。

更新日期:2017-12-19 ~ 2017-12-19

具体修改内容:主要更新了 404 页面上面的动图,个人偏好喜欢一些星系漩涡之内的动图,偶然发现这个东西(HTML5+Three.js实现的3D可拖拽银河星系旋转动画特效源码),于是将它的源码略作修改放进 Shaun 的 404 页面,将相关的 js 文件放入主题文件夹中 \source\js 文件夹里,最初时是将相关 js 文件引入路径当成相对路径引入,没想到这样造成有的 404 页面会显示旋转动图,而有的 404 页面则不会显示,后面参考网上资料(解惑页面中的相对路径和绝对路径)了解到:

  1. html 中引入的资源(包括jscssimg
    • 相对路径:相对的是 网页本身的 URL
    • 绝对路径:相对的是 网页 URL 的根路径
  2. css 中引入的资源
    • 相对路径:相对的是 css 文件本身的 URL
    • 绝对路径:相对的是 网页 URL 的根路径

结论: html 中引入资源的相对路径与 网页的 URL 有关,而css中则与 css 资源本身 URL 有关。但使用绝对路径时,不管是在 html中,还是css中,都只与 网页 URL的根路径有关。

将相对路径改为绝对路径,即可在 Shaun 博客域名下所有 404 页面正常显示旋转动图。

8、添加 Gitalk 评论系统

修改日期:2017-12-16

  Gitalk 是一款类似 gitment 的评论系统,Shaun 先是照着它提供的配置添加之后,发现居然与 spfk 主题中的 require-2.1.6,jquery-1.9.1.min.js 冲突,显示不了 Gitalk,Shaun 以为是 bug,所以就去提了个 issue,作者 booxood 还是挺认真负责的(°Д°)Ъ,耐心的解决 Shaun 的问题,原来是 Shaun 的引用方式有问题,需要用 require 方式引用,大佬就是大佬 ○| ̄|_,小白还是小白,萌新完全没见过还有这种操作,也算是开眼界了 ✪ω✪。由于大佬解决完问题之后就直接把 issue 关闭了,所以 Shaun 就只有在这里表示感谢了 /つ∇T)。Shaun 最后添加 gitalk 的 ejs 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="comments" class="gitalk">
<div id="gitalk-container" class="article article-inner article-entry"></div>
<script type="text/javascript">
require(['https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.min.js'], function (Gitalk) {
var gitalk = new Gitalk({
clientID: '<%= theme.gitalk.client_id%>',
clientSecret: '<%= theme.gitalk.client_secret%>',
id: window.location.pathname,
repo: '<%= theme.gitalk.repo%>',
owner: '<%= theme.gitalk.owner%>',
admin: '<%= theme.gitalk.admin%>',
// facebook-like distraction free mode
distractionFreeMode: true
})
gitalk.render('gitalk-container')
})
</script>
</div>

还有其它的一些修改也是仿照 gitment 的代码添加的,我这上面没有添加 css 文件是因为 gitalk 原有的 css 文件与 Shaun 的主题不相符,所以就稍微修改了一下。

※BTW:上次修改文章内a标签的高度后突然发现打赏的“”字背景圆形变成椭圆了 o(╯□╰)o,后面发现原来它也继承文章内 a 标签的属性,没有自己的 padding,后面只有给 .dashang 添加个独立的 padding: 8px;

9、Fix Bugs

修改日期:2017-12-20

修复bug:鼠标悬浮 a 标签之上会出现显示 a 标签 title 内容的气泡,当 title 内容过多时,会造成气泡位置下调,从而遮住相应 a 标签内容的 bug。

解决方案为:定位气泡文件为主题文件夹下 \layout\_partial\post\TipTitle.ejs 文件,将其中气泡出现的位置改变,原来气泡的位置确定由 top 和 left 决定,现改为 bottom 和 left,毕竟气泡是出现在 a 标签上方,如果将 top 确定,则 title 内容过多时,其只能向下扩张,造成气泡位置下移现象,从而遮住原来的 a 标签内容,改为 bottom 确定之后,气泡只会向上扩张,气泡位置相对稳定,不会遮住原来的 a 标签内容。具体修改内容为,将 top: offset.top - a.outerHeight() - 15 替换为 bottom: window.innerHeight - offset.top + 10 ,其它内容保持不变。

修改日期:2017-12-26

BUG描述:由于 spfk 主题启用的是百度分享,而原作者没有为其添加邮件分享,Shaun 为了好玩就添加个邮件分享,但是在添加过程中,Shaun 发现了个 BUG,就是为其添加的 title 属性没有作用,它会自动更改 title 内容,而 spfk 主题有显示 title 的气泡 TipTitle,所以不需要用默认的东西显示 title 内容,但是,这个百度分享还是强制默认显示,TipTitle 并不能消除其 title 内容。

解决方案:定位百度分享强制添加 title 的代码,其位于主题文件夹下\source\static\api\js\view\share_view.js 中,具体代码如下:

1
2
3
4
function(e){
var i=e.partners,s=i[n]?"\u5206\u4eab\u5230"+i[n].name:"";
!r(t).attr("title")&&s&&r(t).attr("title",s)
}

因为 TipTitle 会首先将 title 去掉,所以这里会强制添加百度分享自己的 title,所以需要将其注释掉,具体注释代码为:/*&&r(t).attr("title",s)*/。本来这样就可以了,但是 Shaun 发现添加邮件分享之后,布局又不对,所以定位主题文件夹下 \source\css\_partial\baidushare.styl 文件,发现它的居中布局居然是有宽度决定的,于是 Shaun 为其添加 text-align: center; 谁知还是没变,不能自动居中,后来在主题文件夹下找到 \source\static\api\css\share_style2_24.css 文件,发现 .bdshare-button-style2-24 a 设置了浮动样式,难怪(⊙﹏⊙),最后将其注释掉就能自动居中了。最后还需要更改移动端 \source\css\_partial\mobile.styl 中 .bdshare-button-style2-24 的样式设置为自动居中就可以了。

10、使文章目录可折叠

修改日期:2017-12-28

首先声明,本次修改完全参考:为 Hexo 添加可折叠的文章目录,所用代码也来自其文(Shaun 只是做了点微不足道的修改),在此表示感谢 Yelee 主题的作者 MOxFIVE 👍 。

具体修改如下:首先在主题文件夹下 \layout\_partial\toc.ejs 中添加 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
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
<!-- 折叠目录 -->
<script type="text/javascript">
// -------------添加小图标--------------
var $itemHasChild = $("#toc .toc-item:has(> .toc-child)");
var $titleHasChild = $itemHasChild.children(".toc-link");
$itemHasChild.prepend("<i class='fa fa-caret-down'></i><i class='fa fa-caret-right'></i>");

var $iconToFold = $(".toc-item > .fa-caret-down");
var $iconToExpand = $(".toc-item > .fa-caret-right");
$iconToExpand.addClass("hide");

// --------------点击小图标--------------
var clickIcon = function () {
$("#toc .toc-item > i").click(function () {
$(this).siblings(".toc-child").slideToggle(100);
$(this).toggleClass("hide");
$(this).siblings("i").toggleClass("hide");
})
}()

// 默认展开目录,所以隐藏掉表示“目录已展开”的图标(向下的小三角)
var $iconToFold = $(".toc-item > .fa-caret-down");
$iconToExpand.addClass("hide");

// ------------点击大标题-----------------
var clickTitle = function () {
$titleHasChild.dblclick(function () {
$(this).siblings(".toc-child").hide(100);
$(this).siblings("i").toggleClass("hide");
})
// After dblclick enent
$titleHasChild.click(function () {
var $curentTocChild = $(this).siblings(".toc-child");
if ($curentTocChild.is(":hidden")) {
$curentTocChild.show(100);
$(this).siblings("i").toggleClass("hide");
}
})
}()

// ---------点击总标题-----------------
var clickTocTitle = function () {
var $iconToExpand = $(".toc-item > .fa-caret-right");
var $iconToFold = $(".toc-item > .fa-caret-down");
var $subToc = $titleHasChild.next(".toc-child");

var $tocTitle = $("#toc .toc-title");

// 当包含多级目录时再执行
if ($titleHasChild.length) {
$tocTitle.addClass("clickable");
$tocTitle.click(function () {
if ($subToc.is(":hidden")) {
$subToc.show(150);
$iconToExpand.removeClass("hide");
$iconToFold.addClass("hide");
} else {
$subToc.hide(100);
$iconToExpand.addClass("hide");
$iconToFold.removeClass("hide");
}
})
}
}()
</script>

然后添加相应的 css 样式,在主题文件夹下 \source\css\_partial\article.styl#toc 样式里添加 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
ol.toc li.toc-item i {
display: inline-block;
margin-left: -0.9em;
width: 0.9em;
color: #b3b3b3;
font-weight: bold;
cursor: pointer;

&:hover {
color: #000;
}

&.hide {
display: none;
}
}
.toc-title.clickable {
cursor: pointer;

&:hover {
color: #88acdb;
}
&:active {
color: #d3d3d3;
}
}

以上两步做完之后,点击目录前的小三角符号或双击目录名就能折叠相应目录,点击“文章目录”就能折叠所有目录。


待续。。。

后记

  先就写到这里,如后续修改中发现问题再继续记录吧 ↖(^ω^)↗。