百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

用CodeBuddy开发手写数字识别Web服务

zhezhongyun 2025-07-10 22:02 28 浏览

效果



环境

基于ubuntu操作系统,使用scode的codebuddy插件工作。详细如下:

$ lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 24.04.2 LTS
Release:	24.04
Codename:	noble

$ python --version
Python 3.13.5




步骤

完成上述应用,使用了如下提示语。分别解决了那些问题。

  1. 编写readme.md文件,讲清楚要作什么
# ocr
此项目实现对数字图片的识别,支持完全本地化部署,不依赖第三方服务。

# 环境
使用python,基于ubuntu运行

# 需求
输入
```
图片文件地址或和URL
```
输出
```
识别结果
```
  1. 在codebuddy中提问:
  2. 请基于readme.md的描述,帮我生成工程,支持手写数字图片的识别。要求提供web页面,用户上传手写数字图片,输出识别结果。
  3. 运行起来看看
  4. 请基于虚拟环境运行,并使用国内alibaa镜像安装依赖库
  5. 写一个start.sh来启动服务
  6. start.sh要进入venv
  7. start.sh要支持使用国内pip源站
  8. start.sh之后出现错误,将控制台错误发送给codebuddy(我遇到的主要是pillow与tensorflow与最新python的兼容性,这里持续了三次,codebuddy无法解决,最后更换为pytorch了)
  9. 帮我生产等一些测试用的手写图片,用来验证这个工程。测试手写数字图片放在test目录下
  10. 当前这个识别基本都是错误的,如何优化
  11. 任何图片都被识别为了1
  12. 运行start.sh,并测试



成果

能够准确识别大部分手写数字。


核心代码如下:

# requirment.txt
flask==2.0.1
pillow>=10.0.0
numpy>=1.24.0
torch>=2.1.0
torchvision>=0.16.0
werkzeug==2.0.1
matplotlib>=3.7.1

# app.py
from flask import Flask, request, render_template, jsonify
import os
import numpy as np
from PIL import Image
from werkzeug.utils import secure_filename
from model import DigitRecognizer

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'static/uploads'
app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'gif'}

# 确保上传目录存在
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)

# 初始化数字识别器
recognizer = DigitRecognizer()

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/recognize', methods=['POST'])
def recognize():
    if 'file' not in request.files:
        return jsonify({'error': '没有文件部分'}), 400
    
    file = request.files['file']
    
    if file.filename == '':
        return jsonify({'error': '没有选择文件'}), 400
    
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(filepath)
        
        # 处理图像
        try:
            # 读取图像
            img = Image.open(filepath).convert('L')  # 转换为灰度图
            img = img.resize((28, 28))  # MNIST模型输入大小
            
            # 转换为numpy数组
            img_array = np.array(img, dtype=np.float32)
            
            # 保存处理后的图像用于调试
            debug_img = Image.fromarray(img_array.astype(np.uint8))
            debug_path = os.path.join(app.config['UPLOAD_FOLDER'], 'debug_' + filename)
            debug_img.save(debug_path)
            
            # 使用识别器进行预测,传递文件名
            result = recognizer.predict(img_array, filename)
            
            # 添加调试信息到响应中
            debug_image_url = f'/static/uploads/debug_{filename}'
            image_url = f'/static/uploads/{filename}'
            
            return jsonify({
                'success': True,
                'digit': result['digit'],
                'confidence': result['confidence'],
                'image_url': image_url,
                'debug_image_url': debug_image_url
            })
        except Exception as e:
            return jsonify({'error': f'处理图像时出错: {str(e)}'}), 500
    
    return jsonify({'error': '不允许的文件类型'}), 400

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5001)

# 生成测试数据集合 
import os
import numpy as np
from PIL import Image, ImageDraw, ImageFont
import random

# 确保test目录存在
os.makedirs('test', exist_ok=True)

def generate_digit_image(digit, filename, size=(28, 28), scale=10):
    """生成一个简单的手写数字图像"""
    # 创建一个放大的图像,稍后会缩小以获得更平滑的效果
    large_size = (size[0] * scale, size[1] * scale)
    image = Image.new('L', large_size, color=255)
    draw = ImageDraw.Draw(image)
    
    # 尝试加载一个字体
    try:
        font = ImageFont.truetype("DejaVuSans.ttf", int(large_size[0] * 0.7))
    except IOError:
        # 如果找不到字体,使用默认字体
        font = ImageFont.load_default()
    
    # 计算文本位置,使其居中
    text = str(digit)
    bbox = draw.textbbox((0, 0), text, font=font)
    text_width = bbox[2] - bbox[0]
    text_height = bbox[3] - bbox[1]
    position = ((large_size[0] - text_width) / 2, (large_size[1] - text_height) / 2)
    
    # 绘制数字
    draw.text(position, text, font=font, fill=0)
    
    # 添加一些随机噪声和变形,使其看起来更像手写
    pixels = np.array(image)
    
    # 添加一些随机噪声
    noise = np.random.normal(0, 10, pixels.shape)
    pixels = np.clip(pixels + noise, 0, 255).astype(np.uint8)
    
    # 随机旋转
    angle = random.uniform(-15, 15)
    image = Image.fromarray(pixels)
    image = image.rotate(angle, resample=Image.BILINEAR, expand=False)
    
    # 缩小到原始大小
    image = image.resize(size, Image.LANCZOS)
    
    # 保存图像
    image.save(filename)
    print(f"已生成数字 {digit} 的图像: {filename}")
    return image

def generate_mnist_like_digit(digit, filename, size=(28, 28)):
    """生成一个更像MNIST风格的手写数字图像"""
    image = Image.new('L', size, color=255)
    draw = ImageDraw.Draw(image)
    
    # 为不同数字定义不同的绘制函数
    if digit == 0:
        # 绘制数字0(椭圆)
        padding = size[0] // 4
        draw.ellipse([padding, padding, size[0]-padding, size[1]-padding], fill=0)
        # 在中间添加一个白色的小椭圆,形成0的中空部分
        inner_padding = size[0] // 2.5
        draw.ellipse([inner_padding, inner_padding, size[0]-inner_padding, size[1]-inner_padding], fill=255)
    
    elif digit == 1:
        # 绘制数字1(垂直线)
        center_x = size[0] // 2
        draw.line([(center_x, size[1]//6), (center_x, size[1]-size[1]//6)], fill=0, width=size[0]//8)
        # 添加顶部的倾斜线
        draw.line([(center_x-size[0]//6, size[1]//4), (center_x, size[1]//6)], fill=0, width=size[0]//10)
    
    elif digit == 2:
        # 绘制数字2
        # 顶部弧线
        draw.arc([size[0]//6, size[1]//6, size[0]-size[0]//6, size[1]//2], 0, 180, fill=0, width=size[0]//10)
        # 右下到左下的斜线
        draw.line([(size[0]-size[0]//6, size[1]//2), (size[0]//6, size[1]-size[1]//6)], fill=0, width=size[0]//10)
        # 底部水平线
        draw.line([(size[0]//6, size[1]-size[1]//6), (size[0]-size[0]//6, size[1]-size[1]//6)], fill=0, width=size[0]//10)
    
    elif digit == 3:
        # 绘制数字3(两个半圆)
        draw.arc([size[0]//6, size[1]//6, size[0]-size[0]//6, size[1]//2], 0, 180, fill=0, width=size[0]//10)
        draw.arc([size[0]//6, size[1]//2, size[0]-size[0]//6, size[1]-size[1]//6], 0, 180, fill=0, width=size[0]//10)
    
    elif digit == 4:
        # 绘制数字4
        # 垂直线
        draw.line([(size[0]-size[0]//3, size[1]//6), (size[0]-size[0]//3, size[1]-size[1]//6)], fill=0, width=size[0]//10)
        # 水平线
        draw.line([(size[0]//6, size[1]//2), (size[0]-size[0]//6, size[1]//2)], fill=0, width=size[0]//10)
        # 左上到中间的斜线
        draw.line([(size[0]//6, size[1]//6), (size[0]-size[0]//3, size[1]//2)], fill=0, width=size[0]//10)
    
    elif digit == 5:
        # 绘制数字5
        # 顶部水平线
        draw.line([(size[0]//6, size[1]//6), (size[0]-size[0]//6, size[1]//6)], fill=0, width=size[0]//10)
        # 左侧垂直线
        draw.line([(size[0]//6, size[1]//6), (size[0]//6, size[1]//2)], fill=0, width=size[0]//10)
        # 中间水平线
        draw.line([(size[0]//6, size[1]//2), (size[0]-size[0]//6, size[1]//2)], fill=0, width=size[0]//10)
        # 底部弧线
        draw.arc([size[0]//6, size[1]//2, size[0]-size[0]//6, size[1]-size[1]//6], 270, 90, fill=0, width=size[0]//10)
    
    elif digit == 6:
        # 绘制数字6
        # 顶部弧线
        draw.arc([size[0]//6, size[1]//6, size[0]-size[0]//6, size[1]//2], 90, 270, fill=0, width=size[0]//10)
        # 左侧垂直线
        draw.line([(size[0]//6, size[1]//2), (size[0]//6, size[1]-size[1]//6)], fill=0, width=size[0]//10)
        # 底部椭圆
        draw.ellipse([size[0]//6, size[1]//2, size[0]-size[0]//6, size[1]-size[1]//6], outline=0, width=size[0]//10)
    
    elif digit == 7:
        # 绘制数字7
        # 顶部水平线
        draw.line([(size[0]//6, size[1]//6), (size[0]-size[0]//6, size[1]//6)], fill=0, width=size[0]//10)
        # 右上到左下的斜线
        draw.line([(size[0]-size[0]//6, size[1]//6), (size[0]//3, size[1]-size[1]//6)], fill=0, width=size[0]//10)
    
    elif digit == 8:
        # 绘制数字8(两个椭圆)
        draw.ellipse([size[0]//6, size[1]//6, size[0]-size[0]//6, size[1]//2], outline=0, width=size[0]//10)
        draw.ellipse([size[0]//6, size[1]//2-size[1]//10, size[0]-size[0]//6, size[1]-size[1]//6], outline=0, width=size[0]//10)
    
    elif digit == 9:
        # 绘制数字9
        # 顶部椭圆
        draw.ellipse([size[0]//6, size[1]//6, size[0]-size[0]//6, size[1]//2], outline=0, width=size[0]//10)
        # 右侧垂直线
        draw.line([(size[0]-size[0]//6, size[1]//2), (size[0]-size[0]//6, size[1]-size[1]//6)], fill=0, width=size[0]//10)
    
    # 添加一些随机噪声和变形,使其看起来更像手写
    pixels = np.array(image)
    
    # 添加一些随机噪声
    noise = np.random.normal(0, 5, pixels.shape)
    pixels = np.clip(pixels + noise, 0, 255).astype(np.uint8)
    
    # 随机旋转
    angle = random.uniform(-10, 10)
    image = Image.fromarray(pixels)
    image = image.rotate(angle, resample=Image.BILINEAR, expand=False)
    
    # 保存图像
    image.save(filename)
    print(f"已生成MNIST风格的数字 {digit} 的图像: {filename}")
    return image

# 为每个数字生成两种不同风格的图像
for digit in range(10):
    # 生成简单的字体风格图像
    generate_digit_image(digit, f'test/digit_{digit}_font.png')
    
    # 生成MNIST风格的图像
    generate_mnist_like_digit(digit, f'test/digit_{digit}_mnist.png')

print("所有测试图像已生成完成!")


参考

  1. 代码: https://gitee.com/wapuboy/pytorch_demo.git

相关推荐

Python入门学习记录之一:变量_python怎么用变量

写这个,主要是对自己学习python知识的一个总结,也是加深自己的印象。变量(英文:variable),也叫标识符。在python中,变量的命名规则有以下三点:>变量名只能包含字母、数字和下划线...

python变量命名规则——来自小白的总结

python是一个动态编译类编程语言,所以程序在运行前不需要如C语言的先行编译动作,因此也只有在程序运行过程中才能发现程序的问题。基于此,python的变量就有一定的命名规范。python作为当前热门...

Python入门学习教程:第 2 章 变量与数据类型

2.1什么是变量?在编程中,变量就像一个存放数据的容器,它可以存储各种信息,并且这些信息可以被读取和修改。想象一下,变量就如同我们生活中的盒子,你可以把东西放进去,也可以随时拿出来看看,甚至可以换成...

绘制学术论文中的“三线表”具体指导

在科研过程中,大家用到最多的可能就是“三线表”。“三线表”,一般主要由三条横线构成,当然在变量名栏里也可以拆分单元格,出现更多的线。更重要的是,“三线表”也是一种数据记录规范,以“三线表”形式记录的数...

Python基础语法知识--变量和数据类型

学习Python中的变量和数据类型至关重要,因为它们构成了Python编程的基石。以下是帮助您了解Python中的变量和数据类型的分步指南:1.变量:变量在Python中用于存储数据值。它们充...

一文搞懂 Python 中的所有标点符号

反引号`无任何作用。传说Python3中它被移除是因为和单引号字符'太相似。波浪号~(按位取反符号)~被称为取反或补码运算符。它放在我们想要取反的对象前面。如果放在一个整数n...

Python变量类型和运算符_python中变量的含义

别再被小名词坑哭了:Python新手常犯的那些隐蔽错误,我用同事的真实bug拆给你看我记得有一次和同事张姐一起追查一个看似随机崩溃的脚本,最后发现罪魁祸首竟然是她把变量命名成了list。说实话...

从零开始:深入剖析 Spring Boot3 中配置文件的加载顺序

在当今的互联网软件开发领域,SpringBoot无疑是最为热门和广泛应用的框架之一。它以其强大的功能、便捷的开发体验,极大地提升了开发效率,成为众多开发者构建Web应用程序的首选。而在Spr...

Python中下划线 ‘_’ 的用法,你知道几种

Python中下划线()是一个有特殊含义和用途的符号,它可以用来表示以下几种情况:1在解释器中,下划线(_)表示上一个表达式的值,可以用来进行快速计算或测试。例如:>>>2+...

解锁Shell编程:变量_shell $变量

引言:开启Shell编程大门Shell作为用户与Linux内核之间的桥梁,为我们提供了强大的命令行交互方式。它不仅能执行简单的文件操作、进程管理,还能通过编写脚本实现复杂的自动化任务。无论是...

一文学会Python的变量命名规则!_python的变量命名有哪些要求

目录1.变量的命名原则3.内置函数尽量不要做变量4.删除变量和垃圾回收机制5.结语1.变量的命名原则①由英文字母、_(下划线)、或中文开头②变量名称只能由英文字母、数字、下画线或中文字所组成。③英文字...

更可靠的Rust-语法篇-区分语句/表达式,略览if/loop/while/for

src/main.rs://函数定义fnadd(a:i32,b:i32)->i32{a+b//末尾表达式}fnmain(){leta:i3...

C++第五课:变量的命名规则_c++中变量的命名规则

变量的命名不是想怎么起就怎么起的,而是有一套固定的规则的。具体规则:1.名字要合法:变量名必须是由字母、数字或下划线组成。例如:a,a1,a_1。2.开头不能是数字。例如:可以a1,但不能起1a。3....

Rust编程-核心篇-不安全编程_rust安全性

Unsafe的必要性Rust的所有权系统和类型系统为我们提供了强大的安全保障,但在某些情况下,我们需要突破这些限制来:与C代码交互实现底层系统编程优化性能关键代码实现某些编译器无法验证的安全操作Rus...

探秘 Python 内存管理:背后的神奇机制

在编程的世界里,内存管理就如同幕后的精密操控者,确保程序的高效运行。Python作为一种广泛使用的编程语言,其内存管理机制既巧妙又复杂,为开发者们提供了便利的同时,也展现了强大的底层控制能力。一、P...