核酸检测识别系统——总章

目录

核酸检测结果识别系统

源代码(FOR FREE)

产品介绍

产品功能

技术栈

使用说明

技术算法讲解

组成部分

easyocr

opencv

excel表和数据库

Pyecharts可视化界面

运行控制

网站部分

前端

后端

 项目部署

域名

服务器

项目依赖

Python定时任务

run.py

zip.py

clear.py

pm2项目挂载

pm2常用命令

总结

异常处理

核酸检测结果识别系统

HELLO,大家好。现在是2022年8月9日20:14:44。

在老师和伙伴的帮助下,我完成了核酸检测结果识别系统1.0。相信这套简单方便的系统,能够为大家提供便利。

源代码(FOR FREE)

abcdefg-png/-system (github.com)

产品介绍

产品功能

由于高校频繁的核酸检测,导致每次结果统计非常麻烦。这套系统可以极大减轻负责人(例如辅导员)的工作压力。同学们把检测后的图片上传到网站入口,在下方输入框内填上学号即可。

后台会自动识别图片的检测结果、时间并写入数据库汇总,实时反馈提交情况并制作图标,反馈信息。

技术栈

开发工具:VSCode,PyCharm,sqlyog

前端技术:HTML+CSS+JS,jquery,echarts,bootstrap

后端技术:Python,OCR,opencv,SQL,numpy,pyecharts

使用说明

需要提供给网站管理员一份本班的学号+姓名表格作为数据库,生成的数据表格(result.xlsx)。

可视化数据界面实时更新提交和识别情况,属于私有界面,不会对外公开,只对班级的管理员开放,因此可以很大程度上保证使用者的信息安全。

识别出的情况分类:

时间识别:

识别结果 原因
识别成功(年月日和当日时间) 报告符合规范
时间匹配失败 图片损坏、图片不是核酸检测结果、图片字体过于艺术等等

结果识别:

识别结果 原因
阴/阳性 识别出了图片为绿/红色的主色调
结果匹配失败 图片损坏、图片的主色调不是红或绿

特别说明:如果有人传错了图(不是核酸检测结果报告),会识别出阴性阳性,但时间匹配必然失败。所以遇到时间匹配失败+阳性,请勿惊慌,要联系学生重新提交

技术算法讲解

组成部分

easyocr图像文字提取+opencv颜色识别+正则表达式结果匹配+Python写入Excel和数据库+pyecharts可视化界面 + schedule控制挂载运行;

HTML+CSS+JS+jquery+bootstrap(前端)+ PHP(后端)+ Nginx反向代理 + pm2(项目部署)

easyocr

easyocr是python 2021年推出的第三方包,可以非常精准的识别、提取图片文字(亲测有效)。这项功能主要被用来提取学生的核酸检测时间。下面是easyocr的具体使用:

f = open('result.txt', 'w')
for filename in os.listdir(directory_name):
    reader = easyocr.Reader(['ch_sim', 'en'], gpu=False)  # GPU or CPU
    result = reader.readtext(directory_name + r'/' + filename, detail=0)
    result = str.join(result)
    f.write(result)
    f.write("\n")

这里要注意两点,['ch_sim', 'en']是识别语言的列表(中、英文),如果需要其他语言可以去搜它对应的代码。gpu=false 表示我的电脑配置没有gpu,设置成用cpu跑深度学习的代码。

另外,reader.readtext() 是不支持中文路径的,读取的路径一定要注意!

读取完图片文字后,我们把他写入一个txt中,下面要从杂乱的文字中提取出学生的核酸检测时间:

    try:
        timeresult = re.search('采集时间[\d,\-,:,.]*', result) # 获取采集时间
        timeres.append(timeresult.group()[4:]) # 去掉前四个字“采集时间”
​
    except Exception as err:
        timeres.append("时间匹配失败")

这里我们一定要加入异常处理,防止因为匹配失败而直接报错中断运行。

opencv

本身是打算用easyocr直接提取“阴性”这两个字,但是发现这两个字实在不好提取。所以决定用opencv的颜色识别解决结果的判断问题。首先我们列出一个颜色清单,命名为colorList.py。为了识别更加准确,我们只留下红绿两种颜色。

def getColorList():
    dict = collections.defaultdict(list)
​
    # 红色
    lower_red = np.array([156, 43, 46])
    upper_red = np.array([180, 255, 255])
    color_list = []
    color_list.append(lower_red)
    color_list.append(upper_red)
    dict[r'colorList/red'] = color_list
​
​
    # 绿色
    lower_green = np.array([35, 43, 46])
    upper_green = np.array([77, 255, 255])
    color_list = []
    color_list.append(lower_green)
    color_list.append(upper_green)
    dict[r'coloList/green'] = color_list
​
​
    return dict
​

之后用opencv函数进行颜色判断;但cv2.imwrite函数只支持jpg图片,所以我们在网站收集图片时,可以将目标图片进行.jpg文件转换。之后import刚刚的colorList,使用opencv识别图片的颜色。

def get_color(frame):
    hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
    maxsum = -100
    color = None
    color_dict = colorList.getColorList()
    for d in color_dict:
        mask = cv2.inRange(hsv, color_dict[d][0], color_dict[d][1])
        cv2.imwrite(d + '.jpg', mask) # 这里一定要保证,读取的图片是jpg
        binary = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY)[1]
        binary = cv2.dilate(binary, None, iterations=2)
        cnts, hiera = cv2.findContours(binary.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        sum = 0
        for c in cnts:
            sum += cv2.contourArea(c)
        if sum > maxsum:
            maxsum = sum
            color = d
​
    return color

excel表和数据库

为了方便管理者导出,我们用python将结果写入了excel表格;为了做出可视化界面,我们也需要将结果写入database。数据库的建立比较简单,分为四个字段:

字段 数据类型
sno(学号) varchar
sname(姓名) varchar
time_result(时间检测结果) varchar
test_result(检测结果) varchar

python写入excel表格的操作比较简单,另外,如果直接在浏览器输入.xlsx的文件地址,可以直接下载获取,也是十分方便。

在写入之前,我们需要先清理干净excel表格,我选择直接删除Sheet1,然后新建一张。做数据库也是一样,每次清理一次数据库,防止旧数据干扰新一次的检测。刷新后,写入本次的结果。

db = pymysql.connect(host='localhost', user='root', passwd='root', port=3306, db='hesuan_result_collection')
​
cursor = db.cursor()
​
bg = op.load_workbook(r"result.xlsx")  # 应先将excel文件放入到工作目录下
bg.remove(bg["Sheet1"])
​
bg.create_sheet("Sheet1", index=0)
sheet = bg["Sheet1"]
sheet.cell(1, 1, "学号")
sheet.cell(1, 2, "姓名")
sheet.cell(1, 3, "检测时间")
sheet.cell(1, 4, "检测结果") # 刷新excel表格的数据
​
sql_delete = "Update xinan set time_result = '' , test_result = '' "
cursor.execute(sql_delete)
db.commit()                # 刷新数据库的数据
​
if len(timeres) == len(testres):
    for i in range(1, len(timeres) + 1):
        sql_fetch_name = "select sname from xinan where sno = '%s' " % student_sno[i - 1] # 获取学号对应的姓名
        cursor.execute(sql_fetch_name)
        
        sheet.cell(i + 1, 1, student_sno[i - 1])
        try:
            sheet.cell(i + 1, 1, student_sno[i - 1])
        except Exception as err:
            sheet.cell(i + 1, 1, "找不到姓名")
        sheet.cell(i + 1, 3, timeres[i - 1])
        sheet.cell(i + 1, 4, testres[i - 1])  # 分别写入学号、姓名、检测时间、检测结果
        bg.save(r"result.xlsx")  # 对文件进行保存
        
        sql_update = "UPDATE `xinan` " \
                     "SET `time_result` ='%s' , `test_result` = '%s' " \
                     "WHERE sno = '%s'; " % (timeres[i - 1], testres[i - 1], student_sno[i - 1])
        try:
            sql_update_result = cursor.execute(sql_update)
            db.commit()         # 提交数据并保存
        except Exception as err:
            print("数据库写入失败:", err) # 这里同样也要加入异常处理
​
else:
    print("长度不匹配")
print("finished")

Pyecharts可视化界面

做好了算法部分,我们也要能够展示实时提交的情况。这样可以更方便班委管理,及时催促指定同学提交核酸检测报告。首先我们连接数据库,写好相关的sql语句:

import pymysql
import main
​
db = pymysql.connect(host='localhost', user='root', passwd='root', port=3306, db='hesuan_result_collection')
cursor = db.cursor()  # 数据库连接
​
sql_find_failed = "SELECT DISTINCT sname,sno,time_result,test_result FROM xinan WHERE time_result = '时间匹配失败' or test_result = '结果存疑'"  #  查找检测失败的同学名单
sql_readyto_submit = "SELECT DISTINCT sname,sno,time_result,test_result FROM xinan " \
                     "WHERE time_result = '时间匹配失败' or test_result = '结果存疑'" \
                     "or time_result = '' or test_result = ''" #  查找需要重新提交的同学名单,包括结果为空和结果错误的总和
​
main.student_failed = cursor.execute(sql_find_failed)
main.student_left = main.student_num - main.student_checked - main.student_failed

之后使用sql查找的结果,制作饼图和表格

def pie_base():
    label = ['识别成功', '未提交', '识别错误']
    values = [main.student_checked, main.student_left, main.student_failed] # 计算的三个变量制作饼图,识别成功+未提交+识别错误 == 所有学生
    c = (
        Pie()
            .add("", [list(z) for z in zip(label, values)])
            .set_global_opts(title_opts=opts.TitleOpts(title="20信安提交结果"))
            .set_series_opts(label_opts=opts.LabelOpts(formatter="{b}:{c}人 {d}%"))  # 值得一提的是,{d}%为百分比
    )
    return c
 
​
def table_base():
    cursor.execute(sql_find_failed)
    temp = cursor.fetchall()  # 获取全部的结果
    failed_match = []
    for i in temp:
        x = list(i)
        failed_match.append(x) # 这里fetchall()函数返回的是元组,需要将元组转成列表进行表格制作
    table = Table()
    headers = ["学号", "姓名", "检测时间", "检测结果"]
    rows = failed_match
    table.add(headers, rows).set_global_opts(
        title_opts=opts.ComponentTitleOpts(title="识别失败名单")
    )
    return table
  
def page_simple_layout():
    page = Page(layout=Page.SimplePageLayout)  # 简单布局
    page.add(
        pie_base(), # 展示饼图
        table_base(), # 展示识别失败名单
        table_base2(), # 展示
    )
    page.render("./result.html")
​
​
if __name__ == "__main__":
    page_simple_layout()

运行控制

使用python的schedule进行运行控制,我们可以有很多控制方法,将这个文件设置成run.py,之后将他用pm2或者nohup挂载运行即可。

import schedule
import os
​
def run():
    os.system("python view.py")
​
​
schedule.every().day.at("10:30").do(run)  # 每天的10:30执行一次任务
# schedule.every().monday.do(run)  # 每周一的这个时候执行一次任务
# schedule.every().hour.do(run)  # 每隔一小时执行一次任务
​
while True:
    schedule.run_pending()  # run_pending:运行所有可以运行的任务

网站部分

前端

前端部分使用传统三件套+jquery+bootstrap,可以展示项目开发人员列表和提交要求,上传图片时实现图片预览,并且检测用户输入的是否为空,是否为学号。

首先我们引入jquery和bootstrap的核心css,js文件

<!-- jquery -->
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
​
<!-- bootstrap核心 js 与 css 文件 -->
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
        <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
​
<!-- bootstrap 图片上传相关文件 -->
        <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-fileinput/4.4.9/css/fileinput.min.css" media="all" rel="stylesheet" type="text/css" />
        <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-fileinput/4.4.9/js/locales/(lang).js"></script>

首先,设置一个简单的空白抬头,使页面可以在中间显示:

style>
     #main-form
     {
         margin-top: 30px;
     }
</style>

之后在body下面套一层div和form表单:

<form id="form" action="hesuan.php" class="form-horizontal" method="post" enctype="multipart/form-data" onSubmit="return finaljudgeSno();">
...  
...
</form>

我们用textarea展示文本内容,例如开发者、鸣谢者名单和提交要求之类的信息。class="form-control" 是bootstrap专有样式,使用rows = 7 可以对宽度进行调整。这里还要说明一下,textarea真的是textarea,回车空行都是会显示的,比较方面。

<div class="form-group">
                    <label class="col-md-2 control-label small"><span class="text-danger"></span>系统介绍:</label>
                    <div class="col-md-10 has-success">
                        <textarea name="introduction" readonly type="text" class="form-control" placeholder="" rows = "7">系统作者:杨浩然,彭兴锴,曾品典,方友清,王晓晖
​
指导老师:吕皖丽老师,郭星老师,郭佳明(导生),张雨(学长)
​
特别鸣谢:丛言
                        </textarea>
                    </div>
</div>

选择图片后的界面是这样的:

使用这段网上写好的js(现在真的找不到原作者了,大海捞针)来实现图片的预览,我本人也不是什么JavaScript高手,有些代码还是喜欢搬运一下哈哈哈

这里很多设置也都加入了注释,允许同时上传文件个数我们设置成1。

<script>
            // 图上传与预览相关控制
            $('#pic').fileinput
            (
                {
                    showUpload : false, //是否显示上传按钮,跟随文本框的那个
                    showRemove : false, //显示移除按钮,跟随文本框的那个
                    showCaption : true,//是否显示标题,就是那个文本框
                    showPreview : true, //是否显示预览,不写默认为true
                    dropZoneEnabled : false,//是否显示拖拽区域,默认不写为true,但是会占用很大区域
                    maxFileCount : 1, //表示允许同时上传的最大文件个数
                    enctype : 'multipart/form-data',
                    validateInitialCount : true,
                    previewFileIcon : "<i class='glyphicon glyphicon-king'></i>",
                    allowedFileTypes : [ 'image' ],//配置允许文件上传的类型
                    allowedPreviewTypes : [ 'image' ],//配置所有的被预览文件类型
                    allowedPreviewMimeTypes : [ 'jpg', 'png', 'gif' ],//控制被预览的所有mime类型
                    language : 'zh'
                }
            )
​
            // 控制生成的代码的样式
            $('input.file-caption-name').attr('placeholder', '点击右方按钮选择图片');
            $('span.hidden-xs').text("选择图片");
        </script>

之后再用这段代码,判断一下用户输入的是不是学号。

<script>
            var tag = 0;
            judgeSno = function() {
            var sno = document.getElementById("sno");
            var str = "";
            str =sno.value
            var Regex = /\w/;  //英文 + 字母
            var str1 = str[0];
            var str1Code = str1.charCodeAt();
            var hightag = 0;
            if(str1Code >= 65 && str1Code <= 90){
                hightag = 1; // 判断首字母是否为大写
            }
            if (str.length == 9 && (sno.value.match(Regex) && hightag == 1 )) {
                 document.getElementById("checkSno").innerText = "格式正确";
                 document.getElementById("checkSno").style.color = "green";
                tag = 1;
            } else {
                document.getElementById("checkSno").innerText = "格式不对";
                document.getElementById("checkSno").style.color = "red";
                tag = 0;
            }
            return tag;
        }
        </script>
        <script>
            finaljudgeSno = function(){
                var finaltag = tag;
                if(finaltag == 0){
                    alert("请输入正确的学号!")
                    return false;
                    location='https://www.xinanzhijia.xyz/hesuan.html';
                }
                else{
                    return true;
                }
            }
        </script>

后端

后端使用简单的php,实现将上传图片移动到服务器某目录的功能。多余的一项功能是将上传的图片命名成学号,下面是PHP的后端代码

<?php
header("Content-type: text/html; charset=utf-8");
$upload_file = $_FILES["file"];
$upload_name = $_POST["name"];
$store_dir = 'hesuan/Xinan/';  // 改!!!!!1
if($upload_file["error"]>0){
    // echo "错误:".$file["error"];
    if($upload_file["error"]==4){
        echo "<script>alert('请选择图片提交');
        location='hesuan.html'
                </script>";
    }         
    }// 链接改!!!!!!
if($upload_name==null)
    {
        echo "<script>alert('请输入学号');
        location='hesuan.html'
                </script>";
    }
else{
        $arr = ".jpg";
        $new_name ="{$upload_name}{$arr}";
        $upload_file["name"] = $new_name;
        $name = iconv('utf-8','gbk',"hesuan/Xinan/".$upload_file["name"]); // 改!!!!!
        if(move_uploaded_file($upload_file['tmp_name'],$name)){
             move_uploaded_file($upload_file['tmp_name'],$store_dir.$new_name);
             echo "<script>alert('提交成功');
                    location='hesuan.html';
                </script>";
        }                                       
        else{
             echo "<script>alert('提交失败');
                 location='hesuan.html';
             </script>";
        }
}
?>

 项目部署

域名

这里特别说一下,我们的域名和服务器均来自腾讯云。腾讯云的某些秒杀活动需要谨慎参加,因为后期的续费可能变得难以负担,同志们谨慎消费!

服务器

服务器购买了腾讯云的2核4G服务器,峰值带宽:30Mbps。

服务器简单配置了Linux宝塔面板:

wget -O install.sh http://download.bt.cn/install/install-ubuntu_6.0.sh && sudo bash install.sh

安装好后一定要记住当下的用户名和密码,以及端口。我们要在服务器防火墙的安全组中添加规则。

根据宝塔面板提供的帮助,我们可以装好:PHP,phpmyadmin,Nginx,mysql,FTPServer

因为这是一个以Python为基础的项目,因此Python安装很重要(Python3.6版本)

命令如下:

wget https://www.python.org/ftp/python/3.6.3/Python-3.6.3.tgz
tar -xf Python-3.6.3.tgz
​
​
 cd Python-3.6.3
 ./configure --prefix=/usr/local/python3 
 
 make
 make install
 
 ln -s /usr/local/python3/bin/python3 /usr/bin/python3
 ln -s /usr/local/python3/bin/pip3 /usr/bin/pip3
 vim ~/.bash_profile  
 
 写入如下内容:
 # Get the aliases and functions
 if [ -f ~/.bashrc ]; then
. ~/.bashrc
 fi
 # User specific environment and startup programs
 PATH=$PATH:$HOME/bin:/usr/local/python3/bin
 export PATH
 
 source ~/.bash_profile
 sudo echo alias python=python3 >> ~/.bashrc
 source ~/.bashrc
 python3 -V
 pip3 -V

这里已经设置了Python的默认版本并且安装了pip3,但是为了避免不必要的麻烦,可以直接用python3 xxx.py 进行操作,这样最稳妥,不会出现任何异常。

项目依赖

装好Python3.6后安装对应的第三方依赖包,,我们也用pip3进行第三方包安装:

pip3 install -r requirements.txt -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com

之后安装nodejs(14版本)和npm

sudo apt update
sudo apt install nodejs npm
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -   14版本是最新最稳定的!
sudo apt install nodejs
​
node --version
npm --version
sudo apt install build-essential

Python定时任务

run.py

# encoding: utf-8
​
import os
import schedule
​
dirname = r"/www/wwwroot/hesuan/testimg2"
def run():
    if(len(os.listdir(dirname))!=0):   # 检查项目源文件是否为空,非空则开始运行
        os.system("python3 view.py")
​
schedule.every(1).minutes.do(run)      # 为了减少运行压力,每分钟执行一次
​
while True:
    schedule.run_pending()

zip.py

# encoding: utf-8
​
import schedule
import os
import zipfile
import shutil
import pymysql
​
dirname = r"/www/wwwroot/hesuan/testimg1"  # 压缩文件目录
​
def zip():
    file = dirname
    zipfile_name = os.path.basename(file) + '.zip'  # 将目标文件压缩为zip文件
    with zipfile.ZipFile(zipfile_name, 'w') as zfile:
        for foldername, subfolders, files in os.walk(file):
            zfile.write(foldername)
            for i in files:
                zfile.write(os.path.join(foldername, i))
        zfile.close()
​
schedule.every().monday.at("00:00").do(zip)   # 每周一 00:00执行一次压缩
​
while True:
    schedule.run_pending()

clear.py

# encoding: utf-8
​
import schedule
import os
import zipfile
import shutil
import pymysql
​
filepath = r"/www/wwwroot/hesuan/testimg1"  # 定期清空的目标文件
​
host = "127.0.0.1"
username = "hesuan_result"
passwd = "???"
dbname = "hesuan_result"
port = 3306
charset = "utf8"
table_name = "xinan"
​
​
def clear():
    db = pymysql.connect(host=host, user=username, passwd=passwd, port=port, db=dbname)
    cursor = db.cursor()
    sql_delete = "Update %s set time_result = '' , test_result = '' " % table_name
    cursor.execute(sql_delete)    #  清空数据表
    db.commit()
    shutil.rmtree(filepath)       #  删除目标文件
    os.mkdir(filepath)            #  再新建一个一样的文件
    print("Okkkkkkkkkkkkkkk")
​
​
# clear()
schedule.every().tuesday.at("00:00").do(clear)
​
while True:
    schedule.run_pending()

pm2项目挂载

sudo npm install -g pm2

pm2常用命令

pm2 list
pm2 start (all)
pm2 delete (all)
pm2 stop (all)
pm2 restart
​
pm2  start run.py  -x --interpreter  python3  # 挂载Python项目

每次启动成功后,要 pm2 save , pm2 list 保存并查看是不是挂载成功。

本项目对CPU要求较高,我们重点关注负载,而不是CPU使用(因为代码工作量确实大)

总结

到这里,我们终于完成了逻辑闭环。

Step1:同学们将核酸检测的截图提交到网页(并输入合规的学号)

Step2:服务器将文件统一收到某个文件夹下,进行智能识别匹配,将结果写入对应班级的数据库。

Step3:从数据库调出目前的提交情况,反馈到可视化界面和Excel表格(管理员可查看\下载)

Step4:Python实现定时工作并定期删除全部的报告,数据实现阶段更新的大循环。

异常处理

  1. 首先,我们能够保证学生提交的图片:命名为学号,格式为.jpg。这样一来,python的路径中将不含有中文路径,opencv读取的图片也一定是jpg格式。

  2. 不论是图片损坏,图片时间识别失败,图片结果识别失败,都有对应的异常处理,可以反馈识别失败和结果存疑这类的异常。

  3. 项目部署没有问题,可以长期挂载运行python的执行脚本,根据需求安排进程,保证服务器的负载均衡和服务稳定。

猜你喜欢

转载自blog.csdn.net/qq_52504945/article/details/126291293