Dust8 的博客

读书百遍其义自见

0%

前不久看到 tensorflow 微信公众号上的一篇文章说用 posenet 来玩体感游戏的, 就中了草,
离我买 kinnet 已经快 10 年的了, 时间过得真快. 最近追剧 完美关系 , 在里面看到张馨予玩的
体感游戏就去搜了下, 叫 体感碰碰球 , 感觉很有意思. 还有最近很火的 健身环大冒险,
体感越来越接近生活. 刚好 tensorflow 2020 开发大会 上看到 tenorflow hub 上了 2 个新模型,
有个叫 handpose 追踪手势轨迹的, 就想用来做个利用摄像头追逐手势的体感碰碰球.
先上了个简单的, 利用一个点来控制飞机飞行.

源码: HandPlaneGame
查看 demo: HandPlaneGame

截图

参考链接

实现定长验证码的识别. 使用 tensorflow 2.1 版的 keras 高级接口来训练模型.
模型使用了 cnn, rnn, ctc 来预测 4 位的验证码.
项目地址: ocr_shixin

1
2
3
4
5
6
import matplotlib.pyplot as plt
import tensorflow as tf
import numpy as np

%matplotlib inline
%config InlineBackend.figure_format = 'retina'

探索数据

预处理数据可以看 preprocess.py

1
2
3
4
5
6
7
8
from config import labels_to_text
from data_sequence import ShiXinSequence

sx_sequence = ShiXinSequence('./dataset/binary')

(x_train,y_train,_,_),_ = sx_sequence[0]
plt.title(labels_to_text(y_train[1]))
plt.imshow(np.squeeze(x_train[1],-1),cmap='gray')
<matplotlib.image.AxesImage at 0x1f4aa567bc8>

png

ShiXin 模型

1
2
3
4
5
6
7
8
9
from config import OUTPUT_PATH,TARGET_PATH
from network.model import ShiXinModel


# 创建和编译模型
sx_train_model = ShiXinModel()
sx_train_model.compile()

callbacks = sx_train_model.get_callbacks(logdir=OUTPUT_PATH,checkpoint=TARGET_PATH, monitor="loss",verbose=1)

训练模型

1
h = sx_train_model.fit_generator(generator=sx_sequence,epochs=15,callbacks=callbacks,verbose=1)
Epoch 1/15
345/345 [==============================] - 46s 133ms/step - loss: 15.6317
Epoch 2/15
345/345 [==============================] - 44s 128ms/step - loss: 13.4181
Epoch 3/15
345/345 [==============================] - 44s 128ms/step - loss: 4.6008
Epoch 4/15
345/345 [==============================] - 45s 131ms/step - loss: 0.6347
Epoch 5/15
345/345 [==============================] - 45s 130ms/step - loss: 0.2896
Epoch 6/15
345/345 [==============================] - 46s 134ms/step - loss: 0.1451
Epoch 7/15
345/345 [==============================] - 46s 134ms/step - loss: 0.0937
Epoch 8/15
345/345 [==============================] - 47s 137ms/step - loss: 0.0306
Epoch 9/15
345/345 [==============================] - 47s 137ms/step - loss: 0.0362
Epoch 10/15
345/345 [==============================] - 48s 140ms/step - loss: 0.0111
Epoch 11/15
345/345 [==============================] - 49s 142ms/step - loss: 0.0521
Epoch 12/15
345/345 [==============================] - 49s 143ms/step - loss: 0.0319
Epoch 13/15
345/345 [==============================] - 50s 145ms/step - loss: 0.3152
Epoch 14/15
345/345 [==============================] - 51s 146ms/step - loss: 0.1556
Epoch 15/15
345/345 [==============================] - 51s 148ms/step - loss: 0.0471

预测

因为 ctc 的训练和预测输入和输出不一样,所以需要新建一个模型来载入已经训练好的权重

1
2
3
sx_predict_model = ShiXinModel()
sx_predict_model.compile()
sx_predict_model.load_checkpoint(TARGET_PATH)
1
2
out = sx_predict_model.predict(x=np.array([x_train[1]]))
out
WARNING:tensorflow:From c:\users\tom\appdata\local\programs\python\python37\lib\site-packages\tensorflow_core\python\keras\backend.py:5783: sparse_to_dense (from tensorflow.python.ops.sparse_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Create a `tf.sparse.SparseTensor` and use `tf.sparse.to_dense` instead.





array([[ 3,  4,  7, 17]], dtype=int64)
1
2
3
4
5
6
7
8
9
10
11
from pathlib import Path

from PIL import Image


imgs = list(Path('./dataset/val_binary').glob('*'))

x_v = np.array( [np.expand_dims(np.array(Image.open(filename)), -1) / 255.0 for filename in imgs])
out = sx_predict_model.predict(x=x_v)
for idx ,val in enumerate(out):
print(f'y_pred:{labels_to_text(val)} y_true:{imgs[idx].name[:4]}')
y_pred:aafe y_true:AAFE
y_pred:bxmp y_true:bxmp
y_pred:f4rh y_true:f4rh
y_pred:fmew y_true:fmew
y_pred:gemt y_true:gemt
y_pred:hl8n y_true:hl8n
y_pred:mrhu y_true:mrhu
y_pred:rvay y_true:rvay
y_pred:sdtn y_true:sdtn
y_pred:txy7 y_true:txy7
y_pred:y7m8 y_true:y7m8

这里可以看到 aafe 也正确识别出来了, 如果解码算法有问题的话, 可能只能识别出一个 a 字符

模型结构

1
2
3
4
5
from tensorflow.keras.utils import plot_model
from IPython.display import Image

plot_model(sx_predict_model.model, to_file='ctc.png', show_shapes=True)
Image('ctc.png')

ctc.png

1
sx_predict_model.model.summary()
Model: "model_2"
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
the_input (InputLayer)       [(None, 70, 160, 1)]      0
_________________________________________________________________
conv1 (Conv2D)               (None, 70, 160, 16)       160
_________________________________________________________________
batch_normalization_3 (Batch (None, 70, 160, 16)       64
_________________________________________________________________
activation_3 (Activation)    (None, 70, 160, 16)       0
_________________________________________________________________
max1 (MaxPooling2D)          (None, 35, 80, 16)        0
_________________________________________________________________
conv2 (Conv2D)               (None, 35, 80, 16)        2320
_________________________________________________________________
batch_normalization_4 (Batch (None, 35, 80, 16)        64
_________________________________________________________________
activation_4 (Activation)    (None, 35, 80, 16)        0
_________________________________________________________________
max2 (MaxPooling2D)          (None, 17, 40, 16)        0
_________________________________________________________________
conv3 (Conv2D)               (None, 17, 40, 16)        2320
_________________________________________________________________
batch_normalization_5 (Batch (None, 17, 40, 16)        64
_________________________________________________________________
activation_5 (Activation)    (None, 17, 40, 16)        0
_________________________________________________________________
max3 (MaxPooling2D)          (None, 8, 40, 16)         0
_________________________________________________________________
permute_1 (Permute)          (None, 40, 8, 16)         0
_________________________________________________________________
time_distributed_1 (TimeDist (None, 40, 128)           0
_________________________________________________________________
gru_1 (Bidirectional)        (None, 40, 1024)          1972224
_________________________________________________________________
gru_2 (Bidirectional)        (None, 40, 1024)          4724736
_________________________________________________________________
out_dense (Dense)            (None, 40, 37)            37925
=================================================================
Total params: 6,739,877
Trainable params: 6,739,781
Non-trainable params: 96
_________________________________________________________________

导出模型

1
!python export.py
Export path: Shixin\1
Already saved a model, cleaning up
Saved mode: Shixin\1


WARNING:tensorflow:From C:\Users\tom\AppData\Local\Programs\Python\Python37\lib\site-packages\tensorflow_core\python\ops\resource_variable_ops.py:1786: calling BaseResourceVariable.__init__ (from tensorflow.python.ops.resource_variable_ops) with constraint is deprecated and will be removed in a future version.
Instructions for updating:
If using Keras pass *_constraint arguments to layers.

使用 TensorFlow Serving 和 Docker 部署模型

1
!docker run -p 8501:8501 --mount type=bind,source=d:/workspace/ocr_shixin/Shixin/,target=/models/Shixin -e MODEL_NAME=Shixin -t -d tensorflow/serving
df5d7fe164023375968ae4a7892e7845a5ecde08a95614efc25246262c479fca

Python 客户端示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
from tensorflow.keras import backend as K


url = 'http://localhost:8501/v1/models/Shixin:predict'
data = {
"instances":x_v.tolist()
}

response = requests.post(url,json=data)
out = np.array(response.json()['predictions'])
out = K.get_value(
K.ctc_decode(
out,
np.ones(out.shape[0]) * out.shape[1]
)[0][0]
)

for i in out:
print(labels_to_text(i))
aafe
bxmp
f4rh
fmew
gemt
hl8n
mrhu
rvay
sdtn
txy7
y7m8

参考链接

最近在写个人收款项目,需要识别收款码里面的信息,例如支付类型,金额,链接,就想到了用 tesseract 来识别金额,用 pyzbar 来识别链接,在根据链接判断支付类型。事实证明是可行的,并做成了个包发布到 pypi 上.

dpayocr : 可以识别支付宝微信收款码信息。例如支付类型,支付金额,支付链接。

开发问题

tesseract

  • 主要是虚拟环境找不到命令,需要指定路径。
  • 支付宝和微信用同样的命令识别不了金额,对它们分别用不同的参数。前者用 --psm 6, 后者用 --psm 11

上传 pypi

现在可以用 Github Actions 来自动发布到 pypi 了。需要先去 pypi 生成 api-token 放到仓库的设置里面。当收到推送后,检测到发布标签就上传到 pypi。这里要注意在本地打了标签后,还需要 Sharing Tags ,例如 git push origin v1.5 这样才能检测到。具体的可以看下面的参考链接。

安装

1
$ pip install dpayocr

使用

命令行使用

1
dpayocr 66.JPG

输出

1
PayQrCode(pay_type=1, price=66.0, url='https://qr.alipay.com/xxx')

接口

1
2
3
4
>>> from dpayocr import parse_pay_qr_code
>>> fp = '66.JPG' # 可以传入文件名或者二进制IO
>>> parse_pay_qr_code(fp)
PayQrCode(pay_type=1, price=66.0, url='https://qr.alipay.com/xxx')

问题

在虚拟环境找不到 Tesseract 命令

1
2
3
# If you don't have tesseract executable in your PATH, include the following:
pytesseract.pytesseract.tesseract_cmd = r'<full_path_to_your_tesseract_executable>'
# Example tesseract_cmd = r'C:\Program Files (x86)\Tesseract-OCR\tesseract'

参考链接

最近开发浏览器扩展碰到的问题记录下, 会不断更新. 详细的开发资料可以看下面的参考链接, 里面已经总结的很好了, 这里只记录下在实践开发中遇到的问题和解决办法.

建了个 QQ 群: 742072171 , 提供一个互相交流学习的地方.

Content Scripts

jsonp 请求回调未定义

出现这个问题是浏览器扩展安全限制, 目标页面和content-script不能共享 js, 所以我们只需要将回调函数注入到目标页面就可以了. 原理是 content-script 能操作目标页面的 DOM, 我们就可以通过 js 动态的插入 script 链接资源.

web_accessible_resources 介绍.

1
2
3
# manifest.json

"web_accessible_resources": ["js/inject.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
// content.js

// 向页面注入JS文件
// <script src="js/inject.js"></script>
function injectCustomJs(jsPath) {
jsPath = jsPath || "js/inject.js";
var temp = document.createElement("script");
temp.setAttribute("type", "text/javascript");
// 获得的地址类似:chrome-extension://ihcokhadfjfchaeagdoclpnjdiokfakg/js/inject.js
temp.src = chrome.extension.getURL(jsPath);
temp.onload = function() {
// 放在页面不好看,执行完后移除掉
this.parentNode.removeChild(this);
};
document.head.appendChild(temp);
}

injectCustomJs();

// jsonp 请求
$.ajax({
url: "http://xxx.com/api",
type: "post",
dataType: "jsonp",
jsonpCallback: "cb",
data: {
signType: "v3"
},
success: function(data) {
console.log("success");
}
});
1
2
3
4
5
6
// inject.js

// 回调函数
function cb() {
console.log("jsonp 回调");
}

content script css 文件引入图片资源

1
2
/* content.css */
background-image: url("chrome-extension://__MSG_@@extension_id__/images/rotate.png");
1
2
# mainfest.json
"web_accessible_resources": ["images/rotate.png"],

多个 content 分别在文档开始和结束注入

1
2
3
4
5
6
7
8
9
10
11
12
"content_scripts": [
{
"matches": ["http://www.google.com/*"],
"js": ["start.js"],
"run_at": "document_start"
},
{
"matches": ["http://www.google.com/*"],
"js": ["end.js"],
"run_at": "document_end"
}
]

屏蔽原始网页的内容

屏蔽原始网页的内容很常见, 如果用 js 来屏蔽会先出现,然后在消失. 给人感觉就不好了. 可以在 css 里面屏蔽, 这样就不会出现了. 当然也要设置 "run_at": "document_start" .

1
2
3
#userinfo {
display: none !important;
}

存储相关

chrome.storage 获取不到数据

原因是这些接口是异步的, 它有时还没取到数据就执行后面的脚本, 导致有时获取不到数据.
方法一:回调函数

1
2
3
4
5
6
7
8
9
10
11
12
function read(key, callback) {
if (key != null) {
chrome.storage.local.get(key, function(obj) {
callback(obj);
});
}
}

// Usage
read("test", function(val) {
// val...
});

方法二:承诺

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function read(key) {
return new Promise((resolve, reject) => {
if (key != null) {
chrome.storage.local.get(key, function(obj) {
resolve(obj);
});
} else {
reject(null);
}
});
}

// 1. Classic usage
read("test")
.then(function(val) {
// val...
})
.catch(function() {
// looks like key is null
});

// 2. Use async/await
var val = await read(test);
console.log(val);

chrome.tabs

扩展里面打开新标签页

除了 windows.open 还可以用扩展的接口

1
2
3
4
5
6
7
8
9
10
11
12
chrome.tabs.create(
{
url: "http://xxx.com",
// 先不激活
active: false
},
function(tab) {
console.log("tab was created", tab);
// 激活标签
chrome.tabs.update(tab.id, { active: true });
}
);

查询和更新关闭标签页

因为这些接口都是异步, 如果需要先判断在执行后面的操作, 最好用承诺来处理, 下面是截取的一部分代码, 不要照搬, 领会原理就好了

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
function query(urlWd) {
return new Promise((resolve, reject) => {
chrome.tabs.query({ url: "https://www.baidu.com/s?wd=*" }, function(tabs) {
resolve(tabs, urlWd);

// else {
// reject(null);
// }
});
});
}

function update(tabs, urlWd) {
return new Promise((resolve, reject) => {
// chrome.tabs.update(tabs[0].id, { url: urlWd, active: true }, function(tab) {
// resolve(tab);
// });
[...tabs].forEach(tab => {
chrome.tabs.remove(tab.id);
});
});
}

query(urlWd)
.then(tabs => {
update(tabs);
})
.then(e => {
gotoBaidu(info, tab);
});

chrome.cookies

要获取 cookie 比如用 chrome.cookies.getAll({}) 获取所有的 cookies, 会返回空数组, 是因为权限没有设置好. 除了要设置 cookies 还要在权限里面设置域名, 要在 chrome://extensions/ 页面点击扩展的刷新按钮, 仔细检查看扩展有没有错误按钮,有的话要进去看是什么错误, 这也是一种很好检查错误的方式.

1
2
3
4
"permissions": [
"cookies",
"*://*.baidu.com/*",
]

设置 cookie 的时候我一直不太明白 urldomain 的区别, 文档也说的不是让人一看就清楚, 直到看来例子里面的代码才明白了 url 要怎么设置. 还有就是要清除 url 里面多余的 . 符号.

1
2
3
4
5
6
7
8
function getUrlByCookie(cookie) {
let domain = cookie.domain;
if (domain.charAt(0) == ".") {
domain = domain.substring(1);
}

return "http" + (cookie.secure ? "s" : "") + "://" + domain + cookie.path;
}

其他

页面是 https 的,自己的接口是 http 的会报不安全错误

可以通过通信传给 background 调用接口

使用 eval

Content Security Policy (CSP)

1
2
3
# mainfest.json

"content_security_policy": "script-src 'self' https://example.com; object-src 'self'"

参考链接

原理是把 Hexo 的源文件和 Actions 放到 blog-source 分支, 生成的静态文件放到主分支. 这样 Actions 里面监听到 blog-source 分支的推送就构建环境生成静态文件, 然后推送到主分支, 这样就可以了.
好处有几个:

  • 源代码不用带着跑, 担心丢失, 一直在分支里面
  • 不用手动生成静态文件, 部署, 专注于写作
  • 不需要用第三方自动构建工具了

Github Actions 配置

在源文件分支新建 .github/workflows/ci.yml

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
name: 使用 Actions 自动部署 Hexo 博客

# 当推送的分支是源文件分支时才操作
on:
push:
branches:
- blog-source

env:
GIT_NAME: xxx
GIT_EMAIL: xxx@users.noreply.github.com
# 放源文件的分支
BRANCH_SOURCE: blog-source

jobs:
build:
name: 构建与部署博客
runs-on: ubuntu-latest
steps:
- name: 切换到源文件分支
uses: actions/checkout@v2
with:
ref: ${{ env.BRANCH_SOURCE }}

- name: 配置 Git 环境
run: |
git config --global user.name "$GIT_NAME"
git config --global user.email "$GIT_EMAIL"

- name: 设置 NodeJS
uses: actions/setup-node@v1
with:
node-version: "10.x"

- name: 安装 Hexo 及其相关插件
run: |
export TZ='Asia/Shanghai'
npm install hexo-cli -g
npm install hexo-generator-feed --save

- name: 生成静态文件
run: hexo g

- name: 提交静态文件
run: |
cd ./public
git init
git add --all .
git commit -m "Actions CI Auto Builder"

- name: 推送静态文件
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
force: true
directory: public

GITHUB_TOKEN, 在 https://github.com/settings/tokens ,设置名字为 GITHUB_TOKEN , 然后勾选 repo, admin:repo_hook, workflow 等选项,最后点击 Generate token 即可。

参考链接