学校网站里有个强度极低的验证码真是太方便了,这一定是学校故意留给咱练手的
# 基础操作
# 图片读取与保存
#读取 | |
filepath = "图片路径" | |
im = cv2.imread(filepath) | |
#保存 | |
def saveimg(img, name): | |
dirpath = "目录路径" | |
filepath = os.path.join(dirpath, name) #拼接目录路径和文件名 | |
cv2.imwrite(filepath, img) |
# 查看图片的方式
#传入一个 opencv 的图片对象 | |
def showimg(img): | |
cv2.namedWindow("Image", 0) #建立窗口,第二个参数 0 表示自适应图片大小 | |
cv2.imshow("Image", img) #图片展示到窗口 | |
cv2.waitKey(0) #保持窗口显示直到有按键按下 |
用于测试的极弱验证码(再识别不出来就铁人工智障了):
<img src="https://cdn.jsdelivr.net/gh/ERUIHNIYHBKBNF/picapica@main/ml-for-annual-project/2021092801.508hy4wuhwo0.png" width="300px">
# 对验证码的处理
这个大概是前半段参考的原文链接 用 Python 识别验证码
# 处理色彩
#色彩处理部分 | |
#灰度化 | |
im_gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY) | |
#二值化(这里参数也许需要调整一下) | |
ret, im_inv = cv2.threshold(im_gray, 127, 255, cv2.THRESH_BINARY_INV) | |
#应用高斯模糊对图片进行降噪,高斯模糊的本质是用高斯核和图像做卷积(不懂,反正是抄的,能用就行 qwq) | |
kernel = 1 / 16 * np.array([[1, 2, 1], [2, 4, 2], [1, 2, 1]]) | |
im_blur = cv2.filter2D(im_inv, -1, kernel) | |
#降噪后再二值化处理 | |
ret, im_res = cv2.threshold(im_blur, 127, 255, cv2.THRESH_BINARY) |
# 获取矩形轮廓
提取文字轮廓:
#返回一个列表,每个元素对应一个轮廓的所有坐标点集 | |
contours, hierarchy = cv2.findContours(im_res, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) |
两个返回值分别是:
contours
:一个 list,每个元素是每个单独文字的轮廓(ndarray)
hierarchy
: contours[i]
对应 hierarchy[i][0:4]
四个元素,表示其后 / 前 / 父 / 子轮廓的索引。
* 将边框绘制到图像上:
def showImgWithCons(img, cons): | |
#绘制轮廓,参数:图片,轮廓,第几个(-1 全部),颜色,粗细 | |
cv2.drawContours(img, cons, -1, (0, 0, 255), 1) | |
showimg(img) |
获取矩形轮廓:
#列表中每个元素对应一个轮廓的 [x, y, w, h](左上坐标和宽度高度) | |
boundings = [cv2.boundingRect(cnt) for cnt in contours] |
* 将矩形轮廓绘制到图像上:
def showImgWithRect(img, boundings): | |
for rect in boundings: | |
[x, y, w, h] = rect | |
#绘制矩形轮廓,参数:图片,左上点,右下点,颜色,粗细 | |
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 1) | |
showimg(img) |
# 图片大小处理
正常操作下来可以顺利拿到矩形轮廓并绘制,然而整成了这样:
<img src="https://cdn.jsdelivr.net/gh/ERUIHNIYHBKBNF/picapica@main/ml-for-annual-project/2021092802.14noltd2fr7g.png" width="300px">
看一眼二值化灰度化处理的结果,是这样子:
<img src="https://cdn.jsdelivr.net/gh/ERUIHNIYHBKBNF/picapica@main/ml-for-annual-project/2021092803.2dz19imblqqs.png" width="300px">
问了下实验室的前辈,大概是因为图片尺寸太小,这样处理:
#放大图片方便处理,长和宽分别放大十倍 | |
im = cv2.resize(im, None, fx = 10, fy = 10, interpolation = cv2.INTER_LINEAR) | |
#顺带调整了一些二值化的参数 | |
ret, im_inv = cv2.threshold(im_gray, 180, 255, cv2.THRESH_BINARY_INV) |
看起了就可以了的样子:
<img src="https://cdn.jsdelivr.net/gh/ERUIHNIYHBKBNF/picapica@main/ml-for-annual-project/2021092804.6hbe0s4bl300.png" width="300px">
找出来的最小矩形轮廓也不令人意外:
<img src="https://cdn.jsdelivr.net/gh/ERUIHNIYHBKBNF/picapica@main/ml-for-annual-project/2021092805.5jq1yr1m0gg0.png" width="300px">
# 切割轮廓并保存
轮廓的切割主要是通过数组切片实现的。
搜了半天没想到咋整。。突然想起这图片本身就是用数组按顺序存的像素值,直接切数组就好了唔 qwq
for rect in boundings: | |
[x, y, w, h] = rect | |
roi = im_res[y : y + h + 1, x : x + w + 1] #直接切割原图片对应数组即可 | |
roi = cv2.resize(roi, (30, 30)) #统一调整为 30*30 大小编于后期处理 | |
saveimg(roi, "dig" + str(int(time.time() * 1e6)) + ".jpg") #通过时间戳命名保存(防止重名) |
看起来是这个样子的:
# 功能封装
对于图片切割处理的部分,全部封装成一个函数:
传入一张验证码图片,返回一个数组,数组中的每个元素对应已经处理好的(黑白 8*8)每个数字:
def divide(img): | |
im = cv2.resize(im, None, fx = 10, fy = 10, interpolation = cv2.INTER_LINEAR) | |
im_gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY) | |
ret, im_inv = cv2.threshold(im_gray, 180, 255, cv2.THRESH_BINARY_INV) | |
kernel = 1 / 16 * np.array([[1, 2, 1], [2, 4, 2], [1, 2, 1]]) | |
im_blur = cv2.filter2D(im_inv, -1, kernel) | |
ret, im_res = cv2.threshold(im_blur, 127, 255, cv2.THRESH_BINARY) | |
contours, hierarchy = cv2.findContours(im_res, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
boundings = [cv2.boundingRect(cnt) for cnt in contours] | |
digits = [] | |
for rect in boundings: | |
[x, y, w, h] = rect | |
roi = im_res[y : y + h + 1, x : x + w + 1] | |
roi = cv2.resize(roi, (8, 8)) | |
digits.append(roi) | |
return digits |
# 收集训练数据
# 利用爬虫收集验证码图片
学校内网里的一个会随机返回~~(强度极低的)~~ 验证码的接口。
import requests as rq | |
from constants import Constants | |
import time | |
totimgs = Constants().getTot() | |
url = "http://222.194.10.249/inc/validatecode.asp" #这个 ip 只支持校内访问 qwq | |
for i in range(1, totimgs + 1): | |
res = rq.get(url) | |
with open('./origin_images/{}.jpg'.format(i) ,'wb') as fb: | |
fb.write(res.content) | |
time.sleep(1) # 请求过快会导致多次请求到同一张图片 |
顺带一提这里有个常量:(为了增加代码量显得做了很多工作(逃
constants.py:
class Constants: | |
__totimgs = 20 # 总共几张验证码 | |
def getTot(self): | |
return self.__totimgs |
# 利用 OpenCV 切割数字并保存
这里仍然没什么意外的地方。
from picdiv import divide | |
import cv2 | |
from constants import Constants | |
from utils import saveimg | |
totimgs = Constants().getTot() | |
for i in range(1, totimgs + 1): | |
veryCode = cv2.imread("./origin_images/" + str(i) + '.jpg') | |
digits = divide(veryCode) | |
for j in range(0, len(digits)): | |
saveimg(digits[j], str((i - 1) * 4 + j + 1) + '.jpg') |
# 模型训练
# 手动标注训练数据
切出来的图片是这样的,像这样标记,改一下文件名就可以啦。
<img src="https://cdn.jsdelivr.net/gh/ERUIHNIYHBKBNF/picapica@main/ml-for-annual-project/2021092901.png" width="750px">
# 训练 k-NN 分类器
跟中期代码一样: 摸一摸 k-NN 算法
本以为 ctrlCV 直接完成工作自信满满结果被奇奇怪怪的异常卡了好几天 QAQ~~(其实是遇到异常之后鸽了好几天)~~
# 问题描述
先放一段一开始写的代码:
totimgs = Constants().getTot() | |
dir_path = "./train_data" | |
train_data = [] | |
train_label = [] | |
for i in range(0, 10): | |
digit = cv2.imread(dir_path + str(i) + ".jpg") | |
train_data.append(digit) | |
train_label.append(i) | |
train_data = np.array(train_data).astype(np.float32) | |
train_label = np.array(train_label).astype(np.float32) | |
knn = cv2.ml.KNearest_create() | |
knn.train(train_data, cv2.ml.ROW_SAMPLE, train_label) |
先是不认识的蜜汁报错:
<img src="https://cdn.jsdelivr.net/gh/ERUIHNIYHBKBNF/picapica@main/ml-for-annual-project/2021100401.png" width="750px">
搜一波全是 OpenCV 调用摄像头的错误 qwq(咱哪里调过摄像头啊呜呜呜 QAQ
# 解决过程
总之输出一下 train_data 看看:
with open("test.txt","w") as f: | |
f.write(str(train_data[0])) |
test.txt:
[array([[[ 0, 0, 0], | |
[255, 255, 255], | |
[254, 254, 254], | |
[255, 255, 255], | |
[255, 255, 255], | |
[253, 253, 253], | |
[255, 255, 255], | |
[ 0, 0, 0]], | |
[[254, 254, 254], | |
[174, 174, 174], | |
[ 2, 2, 2], | |
[ 1, 1, 1], | |
...... |
对比之前 ac 的代码可以发现,原来用范围 0~1 的 float32 表示颜色(或者该叫黑白程度?),而这里通过 imread 读到的却是 rgb 颜色值
对比:
array([[ 0., 0., 5., 13., 9., 1., 0., 0.], | |
[ 0., 0., 13., 15., 10., 15., 5., 0.], | |
[ 0., 3., 15., 2., 0., 11., 8., 0.], | |
[ 0., 4., 12., 0., 0., 8., 8., 0.], | |
[ 0., 5., 8., 0., 0., 9., 8., 0.], | |
[ 0., 4., 11., 0., 1., 12., 7., 0.], | |
[ 0., 2., 14., 5., 10., 12., 0., 0.], | |
[ 0., 0., 6., 13., 10., 0., 0., 0.]]) |
(然后搜了一波颜色的表示方法还有 rgb 如何转 float32 之类的还是没看懂 QAQ~~(其实是懒得看)~~
后来突然悟了。。knn 本身就是算个距离,这边 rgb 又都一样(因为灰度二值处理过),取 rgb 任意一个值转成 float32 用就行,也没必要弄成 0~1 之间的值啦。
utils.py 添加函数:
传入一个二维数组(imread)(其实是三维, 是一个长为 3 的列表,表示的是图片第 i 行 j 列像素点的 rgb)转换成 np 的一维向量形式,详见注释:
# OpenCV 的 knn 模型要求 train_data 应该是一个二维数组 | |
# 其中每个元素都是将图片展开成一维,值是 float32 表示的颜色值 | |
# 构造一个 digit 数组,长度是 img 的 hegiht * width | |
# 由于 img 灰度二值处理过,rgb 三个值相同取一个即可 | |
def rgbToFloat32(img): | |
data = [] | |
for i in range(0, len(img)): | |
for j in range(0, len(img[i])): | |
data.append(img[i][j][0]) | |
return np.array(data, dtype='float32') |
# 训练模型
import numpy as np | |
import cv2 | |
from constants import Constants | |
from utils import rgbToFloat32 | |
from sklearn import metrics as mts | |
totimgs = Constants().getTot() | |
dir_path = "./train_data/" | |
train_data = [] | |
train_label = [] | |
for i in range(0, 10): | |
# imread 读入的是 rgb 值,返回一个三维的 ndarray,[行 [列 [RGB]]] | |
img = cv2.imread(dir_path + str(i) + ".jpg") | |
# 转换成 ndarray 表示的 | |
digit = rgbToFloat32(img) | |
train_data.append(digit) | |
train_label.append(i) | |
train_data = np.array(train_data, dtype='float32') | |
train_label = np.array(train_label, dtype='float32') | |
knn = cv2.ml.KNearest_create() | |
knn.train(train_data, cv2.ml.ROW_SAMPLE, train_label) | |
# 拿自己测一下看看应该没什么问题的样子 | |
_, res, _, _ = knn.findNearest(train_data, k=1) | |
print(mts.accuracy_score(res, train_label)) | |
# 1.0 |
# 结果测试与性能评估
顺带在 knn.py 里塞一个函数:
def getVeryValue(digits): | |
test_data = [] | |
for i in range(0, len(digits)): | |
npDigit = [] | |
for j in range(0, len(digits[i])): | |
for k in range(0, len(digits[i][j])): | |
npDigit.append(digits[i][j][k]) | |
test_data.append(np.array(npDigit, dtype='float32')) | |
test_data = np.array(test_data, dtype='float32') | |
_, res, _, _ = knn.findNearest(test_data, k=1) | |
return res |
再用 img_spider.py 爬一些验证码来存个别的文件夹里:
with open('./test_images/{}.jpg'.format(i) ,'wb') as fb: |
test.py:
from picdiv import divide | |
from knn import getVeryValue | |
from constants import Constants | |
import cv2 | |
totimgs = Constants().getTot() | |
dir_path = "./test_images/" | |
for i in range(1, totimgs + 1): | |
veryCode = cv2.imread(dir_path + str(i) + ".jpg") | |
digits = divide(veryCode) #这里 digits 应该是长为 4 的列表,每个元素 [行 [列 [颜色值 0-255 不是数组]]] | |
res = getVeryValue(digits) | |
value = 0 | |
for i in res[::-1]: | |
value = value * 10 + int(i) | |
print(value) |
<img src="https://cdn.jsdelivr.net/gh/ERUIHNIYHBKBNF/picapica@main/ml-for-annual-project/2021100404.png" height="350px">
爽啊 (´▽`) ノ♪
# 应用到网络爬虫
爬取一张验证码并获取值:(从哪里爬起来的就从哪里继续爬)
import requests as rq | |
import cv2 | |
import time | |
import os | |
from picdiv import divide | |
from knn import getVeryValue | |
from utils import showimg | |
url = 'http://222.194.10.249/inc/validatecode.asp' | |
res = rq.get(url) | |
# 文件名加个时间戳 | |
fileName = str(int(time.time())) + '.jpg' | |
# 由于不会在内存中直接转换二进制到 rgb 就只能存了再读了 qwq | |
with open(fileName, 'wb') as f: | |
f.write(res.content) | |
img = cv2.imread(fileName) | |
digits = divide(img) | |
res = getVeryValue(digits) | |
value = 0 | |
for i in res[::-1]: | |
value = value * 10 + int(i) | |
# showimg(img) | |
print(value) | |
# 记得删除文件 qwq | |
os.remove(fileName) |
然后用 selenium 之类的(或者说一开始获取验证码就该用 selenium 的 qwq)就可以爬了。
呜呜呜咱先爬了 QAQ