运用Qpython实现对小孩学习/员工工作的实时监控和情况查询

之前写过多篇文章,主要还是利用电脑作为Flask服务器端,接收手机端上传的照片,应用pytorch模型,分析照片反映小孩学习情况,再反馈给手机,利用手机SL4A中tts,读给小孩听,提醒小孩认真学习。

这个过程,需要同时开手机和电脑和无线路由,一是比较耗电,用笔记本电脑做服务器我测了一下功率大概是20W左右,虽然不多,但是每天都要开10小时,长期以往用电量也不小。二是比较麻烦,笔记本开机后又要开手机的app。

之前也考虑直接只用手机来实现这个功能,但是发现Qpython不支持pytorch,后来发现Qpython3.5.1版本可以支持pytorch,我马上改代码,实现这一功能。当然,走了这么多弯路还是有好处的,之前的想法只想能够提醒小孩就可以,后来发现可以利用Flask作为服务器端,开发网页显示小孩学习的实时状态,和生成学习时间轴图表,这个做法可以移植到Qpython,因为Qpython支持Flask。当然之前使用的是plotly模块生成时间轴图表,但Qpython不支持plotly模块,还是需要用回matplotlib模块。

训练大量图片生成模型还是用电脑,我没有在手机Qpython尝试过,估计也比较慢,本文的模型还是大家根据自己拍摄到的照片,放在电脑训练。参考:用Python给老板开发一监控员工摸鱼的工具(1)-优快云博客中二点。

一、Qpython的拍照程序。

# -*- coding: utf-8 -*-
"""
Created on Thu Jan 23 10:50:57 2025
本地qpython自动保存拍照图片并检测小孩学习/工作情况 要求Qpython3.5.1支持pytorch版本
@author: YBK
"""
import datetime
import androidhelper
import time
from PIL import Image
import torch
import torch.nn as nn
from torchvision import transforms, models
import qpy
import shutil
import os
import sqlite3
from pathlib import Path
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches

# 加载模型
# 加载模型并设置为评估模式
model = models.resnet18(pretrained=False)  # 假设使用的是resnet18
num_ftrs = model.fc.in_features  # 获取输入特征数量
model.fc = nn.Linear(num_ftrs, 3)  # 将输出特征数量改为3
model.load_state_dict(torch.load('best33_resnet18_model.pth', map_location='cpu'))  # 加载模型参数
model.to('cpu')  # 移动到设备上
model.eval()  # 设置为评估模式

# 定义预处理步骤
preprocess = transforms.Compose([
    transforms.Resize(224),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
#数据库用sqlite3
db_filepath = Path(__file__).joinpath("../jk.db").resolve()
#在手机相册建立以当前日期命名的文件夹
now = datetime.datetime.now()
date_str = now.strftime("%Y%m%d")
UPLOAD_FOLDER = f'/storage/emulated/0/DCIM/Camera/{date_str}'
if not os.path.exists(UPLOAD_FOLDER):
    os.makedirs(UPLOAD_FOLDER)
#在手机相册建立qk文件夹,用于储存matplotlib生成的工作情况图
if not os.path.exists('/storage/emulated/0/DCIM/Camera/qk/'):
    os.makedirs('/storage/emulated/0/DCIM/Camera/qk/')
#使用SL4A
droid = androidhelper.Android()

def insertdb(sj,furl,xl,zt,qk): #每10秒运行后插入一行数据zt1为有人0为无人,以前的数据表结构,懒得改
    conn = sqlite3.connect(db_filepath, timeout=10, check_same_thread=False)
    c = conn.cursor()
    insert_query = "INSERT INTO jkme(sj,furl,xl,zt,qk) VALUES(?,?,?,?,?);"
    insert_data = (sj,furl,xl,zt,qk)
    c.execute(insert_query,insert_data)
    conn.commit()
    c.close()
    conn.close    

def sc_xxqkpng():  #生成matplotlib生成的工作情况图,之前使用的是plotly,在Qpython用不了,matplotlib也需要在扩展里面安装
    conn = sqlite3.connect(db_filepath, timeout=10, check_same_thread=False)
    c = conn.cursor()
    cursor = c.execute("SELECT id,sj FROM jkme WHERE qk = 4 and sj > datetime('now', 'localtime', '-120 minute') order by id desc")
    row = cursor.fetchone()
    res0 = '休息时间'
    if row:
        lastxxid = row[0]
        cursor = c.execute(f"SELECT count(id) FROM jkme WHERE id > {lastxxid}")
        row = cursor.fetchone()
        if row[0] > 50:
            plt.rcParams['axes.unicode_minus'] = False
            # 创建一个图和一个坐标轴
            fig, ax = plt.subplots(figsize=(7, 2))
            ax.axis('off')
            colors = ['cyan','blue','yellow', 'green']
            cursor = c.execute(f"SELECT qk FROM jkme WHERE id > {lastxxid}")
            rows = cursor.fetchall()
            list0 = [row[0] for row in rows]
            # 初始化一个空列表来存储结果
            result = []    
            # 初始化变量来跟踪当前数字及其起始位置
            current_number = None
            start_index = None    
            alltime=0
            rztime=0
            lktime=0
            # 遍历数字序列
            for i, number in enumerate(list0):
                # 如果当前数字与之前的数字不同(或者是第一个数字)
                if number == 0:
                    lktime = lktime + 1
                if number in (1,2):
                    alltime = alltime + 1
                    if number == 1:
                        rztime = rztime + 1
                if current_number is None or number != current_number:
                    # 如果之前已经有过数字,则记录其范围
                    if current_number is not None:
                        result.append([current_number, start_index, i - 1])
                    # 更新当前数字和起始位置
                    current_number = number
                    start_index = i

            # 处理序列中的最后一个数字范围
            if current_number is not None:
                result.append([current_number, start_index, len(list0) - 1])
            all_data_tj = len(list0)
            for xlist in result:    
                # print(xlist)
                # 添加矩形   
                # 添加一个矩形形状,没有边框
                rect = mpatches.Rectangle(
                    (xlist[1]*0.02, 0.2), ((xlist[2]+1)*0.02 - xlist[1]*0.02), 0.2, fill=True, color=colors[xlist[0]]
                )
                ax.add_patch(rect)
            
            xxtime = int(alltime / 6)
            rztime = round(rztime / alltime * 100,2)
            lktime = round(lktime / all_data_tj * 100,2)
            # 设置图的标题和坐标轴的限制
            cursor = c.execute(f"SELECT sj FROM jkme WHERE id > {lastxxid} order by id asc")
            row = cursor.fetchone()
            bsj = row[0]
            cursor = c.execute("SELECT sj FROM jkme order by id desc")
            row = cursor.fetchone()
            esj = row[0]
            sjstr = bsj.split()[0] + ' ' + bsj.split()[1] + '-' + esj.split()[1]
 
            # 设置布局以隐藏背景和坐标轴
            # 更新图形的布局,隐藏坐标轴和背景
            ax.set_title(f'{sjstr} 学习情况图', fontsize=16)
            ax.set_xlim(0, 2.5)
            ax.set_ylim(0, 0.5)
            ax.set_aspect('equal')  # 保持矩形的比例
            pngfilename = esj.replace('-','').replace(' ','').replace(':','') 

            fig.text(0.5, 0.3, f'工作{xxtime}分钟,认真工作时间占{rztime}%,离开占总时间{lktime}%',
                     ha='center', va='center', fontsize=14)
            plt.savefig(f'/storage/emulated/0/DCIM/Camera/qk/{pngfilename}.png')
            insert_query = "INSERT INTO qkpng(url) VALUES(?);"
            insert_data = (f'qk/{pngfilename}.png',)
            c.execute(insert_query,insert_data)
            conn.commit()
 
            print(f'{pngfilename}.png')
            c.close()
            conn.close()
            res0 = f'工作{xxtime}分钟,认真工作时间占{rztime}%'
        else:
            c.close()
            conn.close()
            res0 = '学习情况不能统计!'
    else:
        c.close()
        conn.close()
        res0 = '学习情况不能统计!'
    return res0


def getprexl(): #获取最后一个xl
    conn = sqlite3.connect(db_filepath, timeout=10, check_same_thread=False)
    c = conn.cursor()
    cursor = c.execute("select xl from jkme order by id desc limit 0,1;")
    row = cursor.fetchone()
    if row:
        xl = row[0]
    else:
        xl = 0
    c.close()
    conn.close
    return xl


# 主程序,循环10秒运行一次
while True:
    res = ''
    start_time = time.time()
    va=droid.getMediaVolume()
    droid.setMediaVolume(0)
    ret = droid.cameraCapturePicture(targetPath=qpy.tmp+"/test.png",cameraId=1).result
    #ret = droid. cameraInteractiveCapturePicture(qpy.tmp+"/test.png").result
    droid.setMediaVolume(va[1])
    # print("Result:"+str(ret))
    # print("Please open "+qpy.tmp+"/test.png"+" to check the picutre")
    
    # 要发送的图片文件路径
    file_path = qpy.tmp+"/test.png"
    # 打开PNG图片,转化成小尺寸的jpg文件
    input_image = Image.open(file_path)
    output_image = input_image.resize((640, 480))
    output_image.save(qpy.tmp+"/test.jpg", "JPEG", quality=85)
    file_path = qpy.tmp+"/test.jpg"
    filename = os.path.basename(file_path)
    now = datetime.datetime.now()
    # 按日期为文件夹保存到手机DCIM
    nowstr = now.strftime("%Y-%m-%d %H:%M:%S")
    savefilepath = os.path.join(UPLOAD_FOLDER, nowstr.replace(' ','').replace('-','').replace(':','') + '.' + filename.rsplit('.', 1)[-1])
    filepath = savefilepath.replace('/storage/emulated/0/DCIM/Camera/', '')
    shutil.move(file_path, savefilepath)    
    savefilename = os.path.basename(savefilepath)
    img = Image.open(savefilepath)  # 加载图片
    img_tensor = preprocess(img)  # 预处理图片
    img_tensor = img_tensor.unsqueeze(0)  # 增加batch维度
    img_tensor = img_tensor.to('cpu')  # 移动到设备上
    # 进行推理
    with torch.no_grad():
        outputs = model(img_tensor)
        _, predicted = torch.max(outputs, 1)
    # 解释结果
    result = int(predicted.item())
    if result == 0:
        print(f'{savefilename}——{result}无人')
    elif result == 1:
        print(f'{savefilename}——{result}工作')
    elif result == 2:
        print(f'{savefilename}——{result}玩')
    #正常情况
    xl = 0 
    zt = 0
    #如果26分钟内1和2,已经超过138个,那么设置为2,2    
    conn = sqlite3.connect(db_filepath, timeout=10, check_same_thread=False)
    c = conn.cursor()
    cursor = c.execute("SELECT count(*) FROM jkme WHERE zt = 0 and (qk > 0 and qk < 4) and sj > datetime('now', 'localtime', '-26 minute')")
    row = cursor.fetchone()
    tj = row[0]
    print(f'工作统计={tj}个10秒')
    if tj > 137 or getprexl() == 2:
        xl = 2 
        zt = 2
        result = 4
        if tj in (136,137):
            res = '学习时间快到了,准备休息。'
        if tj == 138:
            print(sc_xxqkpng())
            res = '可以休息了。'
    #如果6分钟内2,2已经超过30个,那么设置为0,0
    cursor = c.execute("SELECT count(*) FROM jkme WHERE xl = 2 and zt = 2 and sj > datetime('now', 'localtime', '-6 minute')")
    row = cursor.fetchone()
    tjx = row[0]
    print(f'休息统计={tjx}个10秒')
    if tjx > 30:
        xl = 0 
        zt = 0
        res = '休息时间完了。'
        droid.ttsSpeak(res)
        if tjx == 31:
            res = '休息时间结束。'
            
    #如果上一个为2,这一个也是2,那么设置为1,0
    cursor = c.execute("SELECT qk FROM jkme order by id desc limit 0,1;")
    row = cursor.fetchone()
    qkz = row[0]
    print(f'上一个={qkz}')
    if qkz == 2:
        xl = 1 #1为提醒响铃
        zt = 0  
    if result == 0 and zt != 2:
        xl = 3 #3为警报响铃
    cursor = c.execute("SELECT id,msg FROM jkmsg WHERE zt = 0 limit 0,1;")
    row = cursor.fetchone()
    if row:
        msg = row[1]
        mid = row[0]
        c.execute(f"UPDATE jkmsg SET zt = 1 WHERE id = {mid}")
        conn.commit()
    c.close()
    conn.close
    insertdb(now.strftime("%Y-%m-%d %H:%M:%S"),filepath,xl,zt,result)
    # 返回包含接收到的描述和文件路径的响应
    if result == 4:
        res = 'ok'
        for iii in range(0,5):
            print("########################")
    if res != 'ok':
        # droid.ttsSpeak(res)
        print(res)

    end_time = time.time()
    execution_time = end_time - start_time
    print(f"本次运行时间:{execution_time:.2f}秒。")
    time.sleep(10-execution_time)

里面的best33_resnet18_model.pth为自己训练的3分类模型,0为无人,1为认真工作,2为在玩。droid.ttsSpeak(res)前面的#删掉就可以说话。

二、Qpython的服务器端。

# -*- coding: utf-8 -*-
"""
Created on Wed Jan 15 22:39:02 2025
@author: Ybk
"""
from flask import Flask, jsonify, send_from_directory, render_template, request
from pathlib import Path
import sqlite3
import datetime
import re
import torch
import torch.nn as nn
from torchvision import transforms, models
from PIL import Image

# 加载模型
# 加载模型并设置为评估模式
model = models.resnet18(pretrained=False)  # 假设使用的是resnet18
num_ftrs = model.fc.in_features  # 获取输入特征数量
model.fc = nn.Linear(num_ftrs, 3)  # 将输出特征数量改为3
model.load_state_dict(torch.load('best33_resnet18_model.pth', map_location='cpu'))  # 加载模型参数
model.to('cpu')  # 移动到设备上
model.eval()  # 设置为评估模式

# 定义预处理步骤
preprocess = transforms.Compose([
    transforms.Resize(224),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])

db_filepath = Path(__file__).joinpath("../jk.db").resolve()
# 根据手机的IP地址设置服务器地址
fwqip = '192.168.99.162'

 
app = Flask(__name__)

def insertmsg(msg): #插入一行发送信息的数据
    conn = sqlite3.connect(db_filepath, timeout=10, check_same_thread=False)
    c = conn.cursor()
    # msg = '阿弥陀佛#20#'
    match = re.search(r'#(\d+)#', msg)    
    if match:
        msg = msg.split('#')[0]
        number_str = match.group(1)
        number_int = int(number_str)
        for i in range(0,number_int):
            print(msg)
            insert_query = "INSERT INTO jkmsg(sj,msg,zt) VALUES(?,?,0);"
            now = datetime.datetime.now()
            sj = now.strftime("%Y-%m-%d %H:%M:%S")
            insert_data = (sj,msg)
            c.execute(insert_query,insert_data)
            conn.commit()
    else:
        insert_query = "INSERT INTO jkmsg(sj,msg,zt) VALUES(?,?,0);"
        now = datetime.datetime.now()
        sj = now.strftime("%Y-%m-%d %H:%M:%S")
        insert_data = (sj,msg)
        c.execute(insert_query,insert_data)
        conn.commit()
    c.close()
    conn.close 
    
@app.route('/')
def index():
    # 获取最新的图片,了解情况
    conn = sqlite3.connect(db_filepath, timeout=10, check_same_thread=False)
    c = conn.cursor()
    cursor = c.execute("select furl,qk from jkme order by id desc limit 0,1;")
    row = cursor.fetchone()
    upic = row[0]
    result = row[1]
    res = 0
    if result == 0:
        res = '没在工作'
    elif result == 1:
        img = Image.open(f'/storage/emulated/0/DCIM/Camera/{upic}')  # 加载图片
        img_tensor = preprocess(img)  # 预处理图片
        img_tensor = img_tensor.unsqueeze(0)  # 增加batch维度
        img_tensor = img_tensor.to('cpu')  # 移动到设备上
        # 进行推理
        with torch.no_grad():
            outputs = model(img_tensor)
            _, predicted = torch.max(outputs, 1)
        # 解释结果
        result = int(predicted.item())
        if result == 1:
            res = '认真工作'
        elif result == 6:
            res = '没有坐好'
        else:
            res = '认真工作'
    elif result == 2:
        res = '在玩'
    elif result == 3:
        res = '是其他人'
    elif result == 4:
        res = '休息时间'
    c.close
    conn.close
    furl = upic
    image_url = f'http://{fwqip}:5000/static/upimages/{furl}'
    return render_template('index.html', image_url=image_url, qk = res)

@app.route('/qk')
def index0():
    urls = []
    conn = sqlite3.connect(db_filepath, timeout=10, check_same_thread=False)
    c = conn.cursor()
    cursor = c.execute("SELECT url FROM qkpng order by id desc limit 0,5;")
    rows = cursor.fetchall()
    for row in rows:
        urls.append(row[0])
    c.close()
    conn.close
    return render_template('index0.html', image_urls=urls)

@app.route('/static/images/<path:filename>')
def custom_static(filename):
    return send_from_directory('/storage/emulated/0/DCIM/Camera/', filename)

@app.route('/static/upimages/<path:filename>')
def custom_static_gz(filename):
    return send_from_directory('/storage/emulated/0/DCIM/Camera/', filename)

@app.route('/submit', methods=['POST'])
def handle_form_submit():
    # 从POST请求中获取名为'textInput'的参数
    text_input = request.form.get('textInput')
    insertmsg(text_input)
    # 处理接收到的数据(这里只是简单地返回它)
    response = {
        'status': 'success',
        'receivedText': text_input
    }    
    # 返回JSON格式的响应
    return jsonify(response)


if __name__ == '__main__':
    app.run(host=f'{fwqip}')

IP地址的更改参考我之前的文章:Flask+Beeware制作局域网小学生认字游戏-优快云博客,Qpython运行上面2个程序,要将软锁模式打开,同时一个程序在后台运行。然后在浏览器打开,前面说的IP地址:5000网页。哦,忘记发网页的代码了。

三、网页代码

在上面2个py文件的目录下建立templates这个文件,我使用的是在电脑端将好,用Qpython的ftp功能拉过去。

index.html文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>实时工作情况图</title>
    <script type="text/javascript">
        // 设置定时器,每20秒(20000毫秒)刷新一次页面
        setInterval(function(){ 
            location.reload(); 
        }, 20000);
    </script>    
    <script>
        function submitForm() {
            // 获取文本输入框的值
            var text = document.getElementById('textInput').value;
 
            // 创建一个XMLHttpRequest对象
            var xhr = new XMLHttpRequest();
 
            // 配置请求:POST方法,请求的URL,以及是否异步
            xhr.open("POST", "/submit", true);
 
            // 设置请求头,指定内容类型为表单数据
            xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
 
            // 定义当请求完成并成功时的回调函数
            xhr.onreadystatechange = function () {
                if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
                    // 解析并处理服务器的响应
                    var response = xhr.responseText;
                    document.getElementById('response').innerText = response;
                }
            };
 
            // 编码表单数据并发送
            var formData = "textInput=" + encodeURIComponent(text);
            xhr.send(formData);
 
            // 阻止表单的默认提交行为
            return false;
        }
    </script>
</head>
<body>
    <h1>工作情况,每20秒刷新一次页面</h1>
    <div>
        <img src="{{ image_url }}" alt="Latest Image">
    </div>
    <h1>状态:{{ qk }} </h1>
    <form onsubmit="return submitForm();">
        <label for="textInput">输入文字发给手机说:</label><br><br>
        <textarea id="textInput" name="textInput" rows="1" required></textarea><br><br>
        <input type="submit" value="提交">
    </form>
    <p id="response"></p>
<p>
<a href="/qk" target="_self">查看工作进度情况</a>
</p>
</body>
</html>

index0.html文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>最新工作情况图</title>    
</head>
<body>
    <h1>最新工作情况图:</h1>
    <div>
        <!-- 遍历 image_urls 列表并显示每个图片 -->
        {% for url in image_urls %}
            <img src="{{ url_for('static', filename='images/' ~ url) }}" alt="Image">
        {% endfor %}
    </div>
<p>
<a href="../" target="_self">查看实时工作情况</a>
</p>
</body>
</html>

四、效果

上面的图文字还没修改好。哈哈。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值