本文是对2016年网络程序设计课程项目的总结。
SA16225060 付何山 coding.net 课程项目地址
重新点开上学年网络程序设计课程的项目,感慨良多。现在在实习期间在公司也做一些深度学习框架的修改与支持工作,虽然频繁接触框架的源码,对所谓的黑盒深度学习底层实现了解比以前深了,但却仍然只是工程性的填补,对深度学习上层的架构的理解依然停留在入门。重新接触这个项目,依然感觉很是吃力,毕竟是许多人一学期的结晶,想要通过在工作间隙的了解依然十分困难,下面仅就这个项目写写自己的理解,如有错误请多海涵。
该课程旨在完成一个基于机器学习神经网络的一个医学辅助诊断系统。首先通过对病人血常规化验单进行图像识别来采集血常规数据,再利用通过大量真实数据训练出来的预测模型对病人的性别和年龄进行预测,通过对各项血常规数据的分析来实现对病人性别和年龄的预测。
个人项目地址
安装前置依赖
sudo apt-get install python2.7 python2.7-dev python3.2 python3.2-dev
sudo apt-get install build-essential libssl-dev libevent-dev libjpeg-dev libxml2-dev libxslt-dev
安装python模块
本例使用的是python 2.7
安装pip
sudo apt-get install python-pip
安装numpy
sudo apt-get install python-numpy
安装opencv
sudo apt-get install python-opencv
更新环境变量
vim /etc/bash.bashrc
在文件末尾添加两行代码
export PYTHONPATH=$PYTHONPATH:/usr/local/lib/python2.7/dist-packages
export PYTHONPATH="${PYTHONPATH+${PYTHONPATH}:}/usr/local/lib/python2.7/site-packages"
安装OCR和预处理的相关依赖
sudo apt-get install tesseract-ocr
sudo pip install pytesseract
sudo apt-get install python-tk
sudo pip install pillow
安装Flask框架、mongodb’
sudo pip install Flask
sudo apt-get install mongodb
sudo service mongodb started
sudo pip install pymongo
WARNING:若import cv2报no module name cv2,需要从源码编译opencv安装。在我的项目中因为opencv版本问题,对opencv接口做了少量修改,可能造成无法移植的问题
安装tensorflow
pip install --upgrade
https://storage.googleapis.com/tensorflow/linux/cpu/tensorflow-
0.12.0rc0-cp27-none-linux_x86_64.whl
运行demo
cd BloodTestReportOCR
python view.py
在浏览器打开http://localhost:8080,上传图像
部署到web服务器
参考文章
点击生成按钮,系统识别出图片中的数据,填充在按钮下方的表格中,用户可以修改部分有误差的数据
本项目分三大模块:web可视化模块、图像处理和OCR模块、学习预测模块。
采用Flask搭建web应用;前端采用Bootstrap,jQuery, Vue.js 来实现
文件名:view.py
Web 端上传图片到服务器,存入mongodb并获取oid; 前端采用了vue.js, mvvm模式
根据图像的特点做几何特征提取、识别和预处理等,比如滤波,傅里叶变换,边缘检测等
文件名:imageFilter.py
对获取到的上传的血常规化验单图片进行预处理,主要是为了减小噪声,以便识别,主要封装了两个方法:对图像透视裁剪和OCR,以便于模块间的交互,规定适当的接口
文件名:classifier.py
用于判定裁剪矫正后的报告和裁剪出检测项目的编号
文件名:imgproc.py
将识别的图像进行处理二值化等操作,提高识别率 包括对中文和数字的处理
文件名:digits
将该文件替换Tesseract-OCR\tessdata\configs中的digits
对样本数据进行机器学习的神经网络训练,并预测性别和年龄
文件名:tf_predict.py
对页面上传过来的数据使用训练好的模型进行预测性别和年龄
文件名:sex_predict.py、age_predict.py
进行样本的训练,训练出性别和年龄的预测模型
文件名:train.csv
训练数据
1、imageFilter.py 封装了的图像处理函数
对图像透视裁剪和OCR进行了简单的封装,以便于模块间的交互,规定适当的接口。
mageFilter = ImageFilter() # 传入一个Mat
num = 22
print imageFilter.ocr(num)
2、perspect 透视处理函数
@param 透视参数
做初步的图片矫正,用于透视image,会缓存一个透视后的opencv numpy矩阵,并返回该矩阵。如果透视失败,则会返回None,并打印不是报告。
预处理流程如下:
# 载入图像
img_sp = self.img.shape
ref_lenth = img_sp[0] * img_sp[1] * ref_lenth_multiplier
# 灰度化
img_gray = cv2.cvtColor(self.img, cv2.COLOR_BGR2GRAY)
# 高斯平滑
img_gb = cv2.GaussianBlur(img_gray, (gb_param, gb_param), 0)
# 开闭运算:腐蚀、膨胀
closed = cv2.morphologyEx(img_gb, cv2.MORPH_CLOSE, kernel)
opened = cv2.morphologyEx(closed, cv2.MORPH_OPEN, kernel)
线段检测
为了对图片各个数值所在的区域进行定位,需要检测出图片中比较明显的标识,3条黑线,然后利用这三条线对整张图片进行标定。主要用到了以下3个步骤:
# 描绘边缘,Canny边缘检测
edges = cv2.Canny(opened, canny_param_lower , canny_param_upper)
# 调用findContours提取轮廓
contours, hierarchy = cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# 求最小外接矩形
def getbox(i):
rect = cv2.minAreaRect(contours[i])
box = cv2.cv.BoxPoints(rect)
box = np.int0(box)
return box
关于param
参数的形式为[p1, p2, p3 ,p4 ,p5],p1,p2,p3,p4,p5都是整型。
其中p1必须是奇数,是高斯模糊的参数,p2和p3是canny边缘检测的高低阈值,p4和p5是和筛选有关的乘数。
如果化验报告单放在桌子上时,有的边缘会稍微翘起,产生比较明显的阴影,这种阴影有可能被识别出来,导致定位失败。 解决的方法是调整p2和p3,来将阴影线筛选掉。但是如果将p2和p3调的比较高,就会导致其他图里的黑线也被筛选掉了。参数的选择是一个问题,在getinfo.default中设置的是一个较低的阈值,p2=70,p3=30,这个阈值不会屏蔽阴影线。如果改为p2=70,p3=50则可以屏蔽,但是会导致其他图片识别困难。
就现在来看,得到较好结果的前提主要有三个:
①化验单尽量平整
②图片中应该包含全部的三条黑线
③图片尽量不要包含化验单的边缘,如果有的话,请尽量避开有阴影的边缘。
3、filter 过滤图片函数
@param filter参数
过滤掉不合格的或非报告图片
返回img经过透视过后的PIL格式的Image对象,如果缓存中有PerspectivImg则直接使用,没有先进行透视 过滤失败则返回None。
4、autocut 图像裁剪函数
@num 剪切项目数
@param 剪切参数
将图片中性别、年龄、日期和各项目名称数据分别剪切出来
用于剪切ImageFilter中的img成员,剪切之后临时图片保存在out_path,如果剪切失败,返回-1,成功返回0,剪切出来的图片在BloodTestReportOCR/temp_pics/ 文件夹下,函数输出为data0.jpg,data1.jpg……等一系列图片,分别是白细胞计数,中性粒细胞记数等的数值的图片。
#输出年龄
img_age = self.PerspectiveImg[15 : 70, 585 : 690]
cv2.imwrite(self.output_path + 'age.jpg', img_age)
#输出性别
img_gender = self.PerspectiveImg[15 : 58, 365 : 420]
cv2.imwrite(self.output_path + 'gender.jpg', img_gender)
#输出时间
img_time = self.PerspectiveImg[722 : 760, 430 : 630]
cv2.imwrite(self.output_path + 'time.jpg', img_time)
#转换后的图分辨率是已知的,直接从该点读数据即可
startpoint = [199, 132]
vertical_lenth = 37
lateral_lenth = 80
def getobjname(i, x, y):
region_roi = self.PerspectiveImg[y : y+vertical_lenth, x : x+170]
filename = self.output_path + 'p' + str(i) + '.jpg'
cv2.imwrite(filename, region_roi)
def getobjdata(i, x, y):
region_roi = self.PerspectiveImg[y : y+vertical_lenth, x : x+lateral_lenth]
filename = self.output_path + 'data' + str(i) + '.jpg'
cv2.imwrite(filename, region_roi)
#输出图片
if num <= 13 and num > 0:
for i in range(num):
getobjname(int(i), 25, startpoint[1])
getobjdata(int(i), startpoint[0], startpoint[1])
startpoint[1] = startpoint[1] + 40
elif num > 13:
for i in range(13):
getobjname(int(i), 25, startpoint[1])
getobjdata(int(i), startpoint[0], startpoint[1])
startpoint[1] = startpoint[1] + 40
startpoint = [700, 135]
for i in range(num-13):
getobjname(int(i+13), 535, startpoint[1])
getobjdata(int(i+13), startpoint[0], startpoint[1])
startpoint[1] = startpoint[1] + 40
5、ocr 图像识别函数
@num 规定剪切项目数
模块主函数返回识别数据,用于对img进行ocr识别,他会先进行剪切,之后进一步做ocr识别,返回一个json对象。如果剪切失败,则返回None。
# pytesseract 识别检测项目编号及数字
for i in range(num):
item = read('temp_pics/p' + str(i) + '.jpg')
item_num = classifier.getItemNum(item)
image = read('temp_pics/data' + str(i) + '.jpg')
image = imgproc.digitsimg(image)
digtitstr = image_to_string(image)
digtitstr = digtitstr.replace(" ", '')
digtitstr = digtitstr.replace("-", '')
digtitstr = digtitstr.strip(".")
data['bloodtest'][item_num]['value'] = digtitstr
json_data = json.dumps(data,ensure_ascii=False,indent=4)
6、使用TensorFlow平台训练数据
TensorFlow是谷歌基于DistBelief进行研发的第二代人工智能学习系统,其命名来源于本身的运行原理。Tensor(张量)意味着N维数组,Flow(流)意味着基于数据流图的计算,TensorFlow为张量从流图的一端流动到另一端计算过程。TensorFlow是将复杂的数据结构传输至人工智能神经网中进行分析和处理过程的系统。
TensorFlow可被用于语音识别或图像识别等多项机器深度学习领域,对2011年开发的深度学习基础架构DistBelief进行了各方面的改进,它可在小到一部智能手机、大到数千台数据中心服务器的各种设备上运行。TensorFlow将完全开源,任何人都可以用。TensorFlow对机器学习的大部分函数模型都进行了高度封装。
7、normalized 归一化处理函数
由于采集的数据可能会由于本身的数值不统一等因素而对机器学习结果造成影响,因此需要对样本集进行一定的预处理,如去均值与归一化。
去均值的具体做法是在每个样本上减去数据的统计平均值,去均值的意义主要在于扩大分类的效果。查看TensorFlow的MNIST源码时可以看到,程序中对每个像素点的像素值都减去了128,这就是去均值操作。
数据尺度归一化的原因是:数据中每个维度表示的意义不同,所以有可能导致该维度的变化范围不同,因此有必要将他们都归一化到一个固定的范围,一般情况下是归一化到[0 1]或者[-1 1]。同样在TensorFlow的MNIST源码中可以看到,去均值后,会将每点的像素值除以128,进行了归一化操作。
项目中的去均值和归一化操作如下:
def normalized(a,b):
for i in range(22):
# 去均值
tmp = np.mean(a[:, i])
a[:, i] = a[:, i] - tmp
b[:, i] = b[:, i] - tmp
# 归一化
if np.min(a[:, i]) != np.max(a[:, i]):
b[:, i] = 2 * (b[:, i] - np.min(a[:, i])) / (np.max(a[:, i]) - np.min(a[:, i])) - 1
else:
b[:, i] = 0
return b
8、定义模型
在TensorFlow中定义模型
#建立性别模型,年龄模型类似
x_sex = tf.placeholder("float", [None, n_input])
y_sex = tf.placeholder("float", [None, n_classes_sex])
def multilayer_perceptron_sex(x_sex, weights_sex, biases_sex):
# Hidden layer with RELU activation
layer_1 = tf.add(tf.matmul(x_sex, weights_sex['h1']), biases_sex['b1'])
layer_1 = tf.nn.relu(layer_1)
# Hidden layer with RELU activation
layer_2 = tf.add(tf.matmul(layer_1, weights_sex['h2']), biases_sex['b2'])
layer_2 = tf.nn.relu(layer_2)
# Output layer with linear activation
out_layer = tf.matmul(layer_2, weights_sex['out']) + biases_sex['out']
return out_layer
weights_sex = {
'h1': tf.Variable(tf.random_normal([n_input, n_hidden_1_sex])),
'h2': tf.Variable(tf.random_normal([n_hidden_1_sex, n_hidden_2_sex])),
'out': tf.Variable(tf.random_normal([n_hidden_2_sex, n_classes_sex]))
}
biases_sex = {
'b1': tf.Variable(tf.random_normal([n_hidden_1_sex])),
'b2': tf.Variable(tf.random_normal([n_hidden_2_sex])),
'out': tf.Variable(tf.random_normal([n_classes_sex]))
}
pred_sex = multilayer_perceptron_sex(x_sex, weights_sex, biases_sex)
# 定义损失函数和优化函数
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(pred, y))
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)
9、数据读取方式
#读取数据为ndarray类型
data = np.loadtxt(open("./data.csv","rb"),delimiter=",",skiprows=0)
tmp = normalized(data[:,2:])
# 转化标签为one-hot格式(类别为两类,男和女)
tmp_label_sex = one_hot(data[:,0:1],data.shape[0])
# 数据划分为训练和预测两部分
train_label_sex = tmp_label_sex[:1858, :]
test_label_sex = tmp_label_sex[1858:, :]
train_data = tmp[:1858,:]
test_data = tmp[1858:,:]
10、训练
# 激活计算图
sess.run(init_op)
# 启动队列
threads = tf.train.start_queue_runners(sess=sess)
# 迭代次数 = 10000
for i in range(10000):
# batch
image, label = sess.run([img_batch, label_batch])
# 输出局部正确率
if i % 100 == 0:
train_accuracy = accuracy.eval(feed_dict={x: image, y_:dense_to_one_hot(label)})
print("step %d, training accuracy %g" % (i, train_accuracy))
result = sess.run(merged,feed_dict={x:image,y_:dense_to_one_hot(label)})
writer.add_summary(result,i)
train_step.run(feed_dict={x: image, y_: dense_to_one_hot(label)})
# 加载测试集
test_img, test_label = sess.run([test_img_batch, test_label_batch])
# 输出整体正确率
print("test accuracy %g" % accuracy.eval(feed_dict={x: test_img, y_: dense_to_one_hot(test_label)}))
# 保存模型
save_path = saver.save(sess, cwd + "/ckpt_sex/sex.ckpt", write_meta_graph=None)
print("Model saved in file: %s" % save_path)
11、预测
saver.restore(sess, "./model.ckpt")
print ("load model success!")
p_sex = sess.run(pred_sex, feed_dict={x_sex: data_predict})
p_age = sess.run(pred_age, feed_dict={x_age: data_predict})
短暂重新接触这门课的过程中让我学到很多东西,总结一下:
其实或许当初不应该选择放弃,至少运行和稍微有所改进并不困难,希望今后不会再惧怕挑战,也谢谢孟老师的关心与照顾,祝老师身体健康!