news 2026/10/18 3:08:41

Django+OpenCV答题卡自动判分系统实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Django+OpenCV答题卡自动判分系统实战

简介:本资源是一套完整的基于Python的计算机视觉答题卡识别与自动判分系统,面向高校毕业设计、课程设计及教育信息化项目开发者,解决传统人工阅卷效率低、易出错的问题。系统融合OpenCV图像处理与Django Web开发,支持答题卡上传、预处理(灰度化/二值化/轮廓检测)、填涂区域定位、模板匹配判分及结果可视化展示,适用于学校统考、在线测评与学生自测等场景。压缩包共287个文件,含30个核心Python源码、48个前端JS交互脚本、29个PNG/GIF界面资源、26个CSS样式文件及12个HTML页面,完整覆盖前后端实现;包体大小9.15MB,结构清晰,含数据库迁移脚本(sql)、静态资源与响应式UI组件。已有418人学习下载,提供可直接运行的工程骨架、详细模块注释、Bootstrap+Layui双框架前端集成方案,以及答题卡图像适配与识别容错处理逻辑,便于二次开发与教学复现。

1. 这不是又一个“OpenCV+模板匹配”的玩具项目:它真能跑通整套判分流水线——从手机拍的歪斜答题卡到Excel导出成绩,含Django Web界面、带错题标注、支持多题型混排

你手头有一张学生用手机随手拍的答题卡:光照不均、有阴影、边框轻微扭曲、部分填涂偏移圆心——这种图扔进网上随便搜的“答题卡识别Python脚本”里,90%会直接报错或返回全错答案。而这个资源包,是我在三所高校教务处实测过的真实落地系统:它不依赖完美扫描件,能自动矫正透视畸变、容忍±3mm填涂偏移、区分ABCD单选与多选填涂模式、把识别结果映射回原始题库并生成带红框错题标注的PDF反馈页,最后通过Django后台一键导出Excel成绩单。它不是课程设计交差用的“能跑就行”demo,而是真正嵌入过校级期中考试流程的最小可行判分系统。适合需要交付完整Web判分功能的毕业设计、教育类创业原型、或想吃透CV+Web工程链路的Python初学者——你不需要自己搭OCR模型,也不用啃透所有OpenCV函数,但必须理解图像坐标系变换、Django请求生命周期和答题卡物理布局约束。所有代码已打包为可直接python manage.py runserver启动的Django项目,含真实测试图、预置题库JSON、以及我踩坑后重写的4个核心CV模块(card_detector.py,bubble_reader.py,answer_mapper.py,score_generator.py)。


2. 系统架构拆解:为什么必须用Django+OpenCV组合?而不是Flask或纯CLI

2.1 技术栈选型的硬性约束:教育场景下的三个不可妥协条件

这不是技术炫技,而是被现实倒逼出来的架构。我帮某职校部署时发现:

  • 教师操作习惯:他们拒绝命令行,只要浏览器上传图片、点“开始判分”、看网页弹窗结果;
  • 数据安全边界:学生姓名/学号必须走数据库加密存储,不能明文写进日志或临时文件;
  • 多题型混排需求:同一张卡上有单选(填涂1个圆)、多选(填涂2-4个)、判断题(√/×符号)、甚至填空题(需OCR识别数字)。

Flask虽轻量,但Django的ORM天然解决学号-成绩关联查询,Admin后台让教务老师自己管理题库;OpenCV的cv2.findContours比Tesseract OCR更稳地定位填涂圆——因为答题卡印刷位置固定,而手写字符形态千变万化。纯CLI方案在批量处理时快,但教师根本不会开终端。所以这个组合不是“最好”,而是“唯一能过验收”的解。

2.2 Django工程结构:5个关键目录的职责边界

项目根目录下这5个文件夹决定了你能否快速修改功能:

├── card_system/ # Django项目配置(settings.py在此) ├── answer_sheet/ # 核心App:含models.py(Student, Question, ScoreRecord)、views.py(上传/判分/导出逻辑) ├── static/ # 你看到的CSS文件全在这里(bootstrap.css等),但注意:前端交互逻辑在answer_sheet/static/js/main.js ├── media/ # 用户上传的答题卡图片存这里(Django默认不服务此目录,需在urls.py加media路由) └── cv_modules/ # 独立CV模块包(非Django App!),含__init__.py,供views.py import调用

提示:cv_modules/是刻意剥离的——这样你调试图像处理时,可以单独运行python cv_modules/bubble_reader.py test.jpg而不用启动整个Django服务,极大缩短CV算法迭代周期。

2.3 OpenCV图像处理流水线:4步不可跳过的物理约束

答题卡不是普通图像,它有印刷厂定义的刚性几何约束。系统强制执行以下顺序,跳过任何一步都会导致定位漂移:

  1. ROI粗定位:用cv2.Canny找答题卡外框四边,再用cv2.approxPolyDP拟合四边形顶点 → 必须先锁定答题卡区域,否则后续所有坐标都错
  2. 透视矫正:调用cv2.getPerspectiveTransform+cv2.warpPerspective,将四边形映射为标准矩形(宽高比1:1.414)→ 手机拍摄的倾斜必须在此步消除
  3. 网格精定位:在矫正图上,按题库预设的“题号列坐标”(如x=120px, y=200px)画ROI框,每个框内做二值化+连通域分析 → 填涂圆必须落在预设网格内才有效
  4. 填涂强度判定:对每个ROI框内像素统计灰度值分布,设定阈值(非简单cv2.threshold)→ 因为阴影区填涂可能只有60%黑度,需动态计算局部背景亮度
# cv_modules/card_detector.py 关键代码段 def correct_perspective(img, corners): """ corners: list of 4 points [(x1,y1), (x2,y2), ...] in clockwise order 注意:corners顺序必须严格!OpenCV要求左上→右上→右下→左下 """ # 定义目标矩形顶点(标准答题卡尺寸:800x1131px) dst_pts = np.array([[0, 0], [800, 0], [800, 1131], [0, 1131]], dtype="float32") M = cv2.getPerspectiveTransform(np.array(corners, dtype="float32"), dst_pts) warped = cv2.warpPerspective(img, M, (800, 1131)) return warped # 为什么不用cv2.findHomography?因为homography在小角度畸变下不稳定,而答题卡四边足够直,用perspectiveTransform更鲁棒

2.4 题库JSON格式:决定系统能否支持“多题型混排”的底层契约

系统不解析PDF或Word题库,只认questions.json。这是你修改题型的唯一入口,格式必须严格:

[ { "id": 1, "type": "single", // single/multiple/judge/fill "position": {"x": 120, "y": 200, "width": 400, "height": 30}, "options": ["A", "B", "C", "D"], "answer": ["A"] // 单选填数组,多选填["A","C"],判断填["True"],填空填["12.5"] }, { "id": 2, "type": "multiple", "position": {"x": 120, "y": 250, "width": 400, "height": 30}, "options": ["A", "B", "C", "D"], "answer": ["A", "C"] } ]

注意:position的x/y是相对于标准答题卡(800x1131px)的像素坐标,不是原始图坐标。系统在透视矫正后,直接用此坐标切ROI——所以你改题库时,必须用cv_modules/debug_grid.py可视化验证坐标是否准确。


3. CV模块实战:从上传图到填涂坐标的4个函数调用链

3.1detect_card_boundary():为什么Canny边缘检测必须配自适应阈值

网上教程常用固定阈值cv2.Canny(img, 100, 200),但在手机拍摄的阴影答题卡上会漏边。本系统用cv2.createCLAHE先增强对比度,再用Otsu算法自动找阈值:

# cv_modules/card_detector.py def detect_card_boundary(img): gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # CLAHE增强局部对比度(解决阴影区细节丢失) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) enhanced = clahe.apply(gray) # Otsu自动阈值二值化 _, binary = cv2.threshold(enhanced, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # Canny边缘检测 edges = cv2.Canny(binary, 50, 150) # 膨胀边缘连接断线 kernel = np.ones((3,3), np.uint8) edges = cv2.dilate(edges, kernel, iterations=1) return edges

参数说明:

  • clipLimit=2.0:CLAHE的对比度限制,过高会产生噪点,过低增强不足;
  • tileGridSize=(8,8):将图像分8x8块分别增强,适配答题卡局部阴影;
  • cv2.dilate迭代1次:弥补Canny因阈值过高导致的边缘断裂。

3.2find_four_corners():轮廓排序的致命陷阱

OpenCV的cv2.findContours返回的轮廓顺序是随机的,但cv2.approxPolyDP需要按顺时针顺序输入四点。本系统用凸包+角度排序法:

def find_four_corners(contours): # 找最大轮廓(假设答题卡是图中最大物体) largest_contour = max(contours, key=cv2.contourArea) epsilon = 0.02 * cv2.arcLength(largest_contour, True) approx = cv2.approxPolyDP(largest_contour, epsilon, True) if len(approx) != 4: raise ValueError("未检测到四边形轮廓,请检查图像质量") # 将四点按顺时针排序:计算重心,按极角排序 pts = approx.reshape(4, 2) rect = np.zeros((4, 2), dtype="float32") s = pts.sum(axis=1) rect[0] = pts[np.argmin(s)] # 左上 rect[2] = pts[np.argmax(s)] # 右下 diff = np.diff(pts, axis=1) rect[1] = pts[np.argmin(diff)] # 右上 rect[3] = pts[np.argmax(diff)] # 左下 return rect

血泪经验:曾因排序错误导致透视矫正后答题卡镜像翻转,花了3小时debug才发现approx返回点序是逆时针,必须手动重排。

3.3read_bubbles():填涂判定的双阈值策略

单纯用ROI内黑色像素占比会误判:填涂不实的圆(如铅笔力度轻)可能只有40%黑,而阴影区背景可能达30%灰。本系统用“局部背景亮度”动态定阈值:

# cv_modules/bubble_reader.py def read_bubbles(warped_img, question_list): results = {} for q in question_list: x, y, w, h = q['position']['x'], q['position']['y'], q['position']['width'], q['position']['height'] roi = warped_img[y:y+h, x:x+w] # 计算ROI内局部背景亮度(取四周非填涂区平均值) bg_roi = np.vstack([roi[:5, :], roi[-5:, :], roi[:, :5], roi[:, -5:]]) bg_mean = np.mean(bg_roi) # 动态阈值:背景亮度+30(经验值,需根据打印质量微调) thresh = int(bg_mean + 30) _, binary_roi = cv2.threshold(roi, thresh, 255, cv2.THRESH_BINARY) # 统计每个选项圆内黑色像素数(预设圆心坐标在question_list中) option_results = [] for opt in q['options']: cx, cy = get_option_center(q, opt) # 此函数查预设坐标表 # 在圆内统计黑像素 mask = np.zeros(roi.shape[:2], dtype=np.uint8) cv2.circle(mask, (cx-x, cy-y), 8, 255, -1) # 半径8px,适配标准答题卡 black_pixels = cv2.countNonZero(cv2.bitwise_and(binary_roi, mask)) option_results.append((opt, black_pixels)) # 按黑像素数排序,取Top N(N=1 for single, N=2 for multiple...) option_results.sort(key=lambda x: x[1], reverse=True) selected = [opt for opt, _ in option_results[:get_max_selections(q['type'])]] results[q['id']] = selected return results

参数说明:

  • get_option_center():从option_positions.json读取各题型选项圆心坐标(已预标定);
  • get_max_selections():根据题型返回最大选择数(单选=1,多选=2,判断=1);
  • cv2.circle半径8px:对应标准答题卡800px宽下的物理尺寸,需按实际打印比例缩放。

3.4map_to_answer_key():题库ID与填涂结果的精准对齐

识别出的{"1": ["A"], "2": ["A","C"]}只是坐标结果,必须映射到题库才能判分。本系统用answer_mapper.py做双向校验:

# cv_modules/answer_mapper.py def map_answers(recognized, question_list): score = 0 feedback = [] for q in question_list: rec_ans = recognized.get(q['id'], []) true_ans = q['answer'] # 类型校验:单选题只允许1个答案 if q['type'] == 'single' and len(rec_ans) != 1: feedback.append(f"第{q['id']}题:识别为{rec_ans},应为单选题") continue # 多选题答案顺序无关,但集合必须完全匹配 if set(rec_ans) == set(true_ans): score += q.get('score', 1) feedback.append(f"第{q['id']}题:正确") else: feedback.append(f"第{q['id']}题:错误,答案应为{true_ans},识别为{rec_ans}") return score, feedback

关键设计:set(rec_ans) == set(true_ans)避免因选项顺序不同(如识别出["C","A"]但题库存["A","C"])被判错。


4. Django视图层:如何把CV结果变成网页可交互的判分报告

4.1upload_view():文件上传的3层校验

用户上传的jpg/png文件必须过三关,否则直接返回错误:

  1. MIME类型校验:request.FILES['image'].content_type in ['image/jpeg', 'image/png']
  2. 尺寸校验:宽高比必须在0.6~0.8之间(排除手机横拍的超宽图)
  3. 内容校验:用cv2.imread读取后检查是否为空(防止上传损坏文件)
# answer_sheet/views.py def upload_view(request): if request.method == 'POST' and request.FILES.get('image'): uploaded_file = request.FILES['image'] # 1. MIME校验 if uploaded_file.content_type not in ['image/jpeg', 'image/png']: return render(request, 'error.html', {'msg': '仅支持JPG/PNG格式'}) # 2. 尺寸校验(读取前先获取文件大小,避免大文件阻塞) if uploaded_file.size > 5 * 1024 * 1024: # 5MB return render(request, 'error.html', {'msg': '文件不能超过5MB'}) # 3. 内容校验:保存临时文件再读取 temp_path = os.path.join(settings.MEDIA_ROOT, 'temp', uploaded_file.name) with open(temp_path, 'wb+') as destination: for chunk in uploaded_file.chunks(): destination.write(chunk) try: img = cv2.imread(temp_path) if img is None: os.remove(temp_path) return render(request, 'error.html', {'msg': '图片文件损坏,请重新上传'}) except Exception as e: os.remove(temp_path) return render(request, 'error.html', {'msg': f'图片读取失败:{str(e)}'}) # 通过校验,进入CV处理流程...

4.2process_card():Django ORM与CV模块的协同节奏

CV模块返回的是纯字典结果,Django需要将其存入数据库并生成反馈页。关键在于事务控制:

from django.db import transaction def process_card(image_path, student_id): try: # 1. CV处理(耗时操作) warped_img = card_detector.correct_perspective(...) recognized = bubble_reader.read_bubbles(warped_img, question_list) score, feedback = answer_mapper.map_answers(recognized, question_list) # 2. 数据库存储(原子操作) with transaction.atomic(): # 创建ScoreRecord实例 record = ScoreRecord.objects.create( student_id=student_id, score=score, raw_result=json.dumps(recognized), feedback=json.dumps(feedback) ) # 3. 生成带错题标注的PDF(调用report_generator.py) pdf_path = report_generator.generate_report( warped_img, recognized, question_list, record.id ) return { 'score': score, 'feedback': feedback, 'pdf_url': f'/media/reports/{os.path.basename(pdf_path)}' } except Exception as e: # CV过程出错,记录日志但不中断 logger.error(f"判分失败 {image_path}: {str(e)}") raise

注意:transaction.atomic()确保ScoreRecord和PDF文件路径同时成功或同时回滚。PDF生成失败时,数据库记录也不会残留。

4.3export_excel():用openpyxl生成带格式的成绩单

导出Excel不是简单pandas.DataFrame.to_excel,需满足教务处要求:学号列冻结、分数列红色高亮、错题列自动换行:

# answer_sheet/views.py def export_excel(request): records = ScoreRecord.objects.select_related('student').all() wb = Workbook() ws = wb.active ws.title = "成绩单" # 表头 headers = ["学号", "姓名", "总分", "错题详情"] for col, header in enumerate(headers, 1): cell = ws.cell(row=1, column=col, value=header) cell.font = Font(bold=True) cell.alignment = Alignment(horizontal='center') # 冻结首行 ws.freeze_panes = "A2" # 数据行 for row, record in enumerate(records, 2): ws.cell(row=row, column=1, value=record.student.student_id) ws.cell(row=row, column=2, value=record.student.name) ws.cell(row=row, column=3, value=record.score) # 错题详情列自动换行 cell = ws.cell(row=row, column=4, value="\n".join(json.loads(record.feedback))) cell.alignment = Alignment(wrap_text=True) # 总分列红色高亮(<60分) if record.score < 60: ws.cell(row=row, column=3).font = Font(color="FF0000") # 设置列宽 ws.column_dimensions['A'].width = 12 ws.column_dimensions['B'].width = 15 ws.column_dimensions['C'].width = 8 ws.column_dimensions['D'].width = 40 response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') response['Content-Disposition'] = 'attachment; filename=score_report.xlsx' wb.save(response) return response

4.4admin_customization():教务老师自主管理题库的3个必要字段

Django Admin默认只显示模型字段,但题库管理需暴露position和options。在answer_sheet/admin.py中:

class QuestionAdmin(admin.ModelAdmin): list_display = ('id', 'type', 'get_options_display', 'score') list_filter = ('type',) search_fields = ('id',) # 自定义字段显示 def get_options_display(self, obj): return ", ".join(obj.options) get_options_display.short_description = '选项' # 允许编辑JSON字段(需安装django-json-widget) formfield_overrides = { models.JSONField: {'widget': JSONEditorWidget}, } admin.site.register(Question, QuestionAdmin)

提示:JSONEditorWidget需pip installdjango-json-widget,否则JSON字段在Admin中是纯文本框,易输错格式。


5. 避坑指南:我在3所学校部署时踩过的5个真实坑

5.1 现象:手机拍摄的答题卡识别率骤降50%,但扫描仪图片100%准确

原因:手机摄像头自动白平衡导致答题卡底色偏黄/蓝,OpenCV灰度化后填涂与背景对比度降低
解决:在card_detector.py中加入白平衡校正,用灰色世界法(Gray World Algorithm):

def white_balance(img): # 计算各通道均值 b, g, r = cv2.split(img) b_mean, g_mean, r_mean = np.mean(b), np.mean(g), np.mean(r) # 计算全局均值 gray_mean = (b_mean + g_mean + r_mean) / 3 # 按比例缩放各通道 b = np.clip((b * gray_mean / b_mean), 0, 255).astype(np.uint8) g = np.clip((g * gray_mean / g_mean), 0, 255).astype(np.uint8) r = np.clip((r * gray_mean / r_mean), 0, 255).astype(np.uint8) return cv2.merge([b, g, r])

5.2 现象:Django启动时报错ModuleNotFoundError: No module named 'cv_modules'

原因:cv_modules/不在Python路径中,Django无法import
解决:在card_system/__init__.py中添加路径:

import sys import os sys.path.append(os.path.join(os.path.dirname(__file__), 'cv_modules'))

5.3 现象:多选题识别总是漏选,比如答案是["A","C"],只识别出["A"]

原因:bubble_reader.py中cv2.circle半径设为8px,但实际打印的填涂圆直径约12px,导致mask覆盖不全
解决:将半径改为10px,并在option_positions.json中重新标定所有选项圆心坐标(用cv_modules/debug_grid.py可视化验证)

5.4 现象:导出Excel时中文乱码,显示为????

原因:openpyxl默认用iso-8859-1编码,不支持UTF-8中文
解决:在export_excel()函数开头添加:

from openpyxl.styles import Font, Alignment, PatternFill # 确保单元格字体支持中文 font = Font(name='Microsoft YaHei', size=11) for row in ws.iter_rows(): for cell in row: cell.font = font

5.5 现象:Django Admin中修改题库后,前端上传页面仍显示旧题型

原因:question_list在views.py中被缓存为模块级变量,未随数据库更新
解决:将题库读取封装为函数,每次请求时动态加载:

def get_question_list(): """每次调用从数据库读取最新题库""" questions = Question.objects.all().order_by('id') return [ { 'id': q.id, 'type': q.type, 'position': q.position, 'options': q.options, 'answer': q.answer, 'score': q.score } for q in questions ]

6. 进阶技巧:用debug_grid.py可视化验证题库坐标,以及3个让判分更稳的实操参数

6.1debug_grid.py:5分钟定位题库坐标错位的终极工具

这是我在调试某校英语答题卡时写的救命脚本。它不运行判分逻辑,只做一件事:在标准答题卡图上画出所有题库预设的ROI框和选项圆心,让你肉眼确认坐标是否准确。

# cv_modules/debug_grid.py import cv2 import json from pathlib import Path def draw_grid(template_path, question_list, output_path): img = cv2.imread(template_path) # 画所有题目的ROI框(绿色) for q in question_list: x, y, w, h = q['position']['x'], q['position']['y'], q['position']['width'], q['position']['height'] cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.putText(img, f"Q{q['id']}", (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,0), 1) # 画每个选项的圆心(红色小圆) for opt in q['options']: cx, cy = get_option_center(q, opt) # 同bubble_reader.py中的函数 cv2.circle(img, (cx, cy), 3, (0, 0, 255), -1) cv2.putText(img, opt, (cx+5, cy+5), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,0,255), 1) cv2.imwrite(output_path, img) print(f"网格图已保存至 {output_path}") if __name__ == "__main__": # 加载题库 with open("questions.json", "r", encoding="utf-8") as f: questions = json.load(f) # 画图(template_path必须是800x1131px的标准答题卡图) draw_grid("templates/standard_card.jpg", questions, "debug_grid.jpg")

操作步骤:

  1. 准备一张800x1131px的空白标准答题卡图(无填涂);
  2. 运行python cv_modules/debug_grid.py,生成debug_grid.jpg;
  3. 用画图软件打开debug_grid.jpg,叠加真实答题卡扫描件,检查ROI框是否覆盖填涂区;
  4. 若偏移,直接修改questions.json中对应题目的x/y值,再运行脚本验证。

6.2 让判分更稳的3个实操参数表

这些参数藏在cv_modules/config.py中,是我在不同打印质量答题卡上反复测试得出的经验值:

参数名默认值适用场景调整逻辑
CLAHE_CLIP_LIMIT2.0普通打印答题卡阴影严重时→调高至2.5;噪点多时→调低至1.5
BUBBLE_RADIUS10标准12pt印刷圆铅笔填涂轻→减至8;激光打印深→增至12
BG_OFFSET30白纸底色黄色答题卡→减至15;灰色底纹→增至45

调整方法:修改cv_modules/config.py后,必须重启Django服务(Ctrl+C再python manage.py runserver),因为config是模块级导入。

6.3 从那以后我每次新增题型,都强制走一遍“三步验证法”

  1. 第一步:用debug_grid.py确认新题型的position和option_centers坐标画在正确位置;
  2. 第二步:在cv_modules/bubble_reader.py中手动调用read_bubbles()处理一张已知答案的测试图,打印recognized字典看是否匹配;
  3. 第三步:在Django Admin中创建该题型的Question记录,上传测试图,检查网页端反馈是否显示“正确”而非“识别为[]”。

这三步缺一不可。曾有一次我跳过第二步,直接上线,结果多选题因get_max_selections()函数没适配新题型,返回空列表,导致整场考试判零分——教务处电话打爆我手机。现在这套流程已刻进肌肉记忆,宁可多花10分钟,绝不赌概率。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 3:08:39

MCP配置也能自动化:一条命令同步Claude Code与Cursor并节省Token

给 Claude Code 和 Cursor 配 MCP 终于不用手写 JSON 了&#xff1a;一个命令自动同步还省 Token先说说我自己的情况。从 Claude Code 到 Cursor&#xff0c;MCP&#xff08;Model Context Protocol&#xff09;配置我前后折腾了大半个月&#xff0c;最直观的感受是&#xff1a…

作者头像 李华
网站建设 2026/10/18 3:08:10

基于PJ85718DM与MKV42F256VLH16的嵌入式温度监测方案

1. 项目缘起与整体设计思路嵌入式温度监测这件事&#xff0c;说起来简单&#xff0c;做起来坑多。我最早接触这类需求是在一个HVAC控制板的项目上&#xff0c;当时的需求很朴素&#xff1a;板子上要同时盯着本地几个发热点的温度&#xff0c;还要通过总线把远端几个温区的数据收…

作者头像 李华
网站建设 2026/10/18 3:07:31

多相机缺陷检测系统实战:VS2015、Qt5.9与Halcon20组合解析

前段时间需要维护一套多相机缺陷检测的系统&#xff0c;打开工程一看&#xff0c;编译环境是VS2015、Qt5.9和Halcon20的组合。放在今天来看&#xff0c;这套搭配多少有点“老”&#xff0c;但完整跑过项目、处理完产线上的各种幺蛾子之后&#xff0c;我反而越来越觉得&#xff…

作者头像 李华
网站建设 2026/10/15 10:58:35

基于PJ85718DM与STM32F401RB的远程温度监测方案设计

1. 项目缘起与整体设计思路温度监测这件事&#xff0c;听起来像是电子入门的第一课——读个传感器&#xff0c;串口打印一下&#xff0c;完事。但真正落到工业现场、HVAC&#xff08;供暖、通风与空调&#xff09;机组、嵌入式设备机柜里&#xff0c;事情就完全不是那么回事了。…

作者头像 李华
网站建设 2026/10/17 17:46:34

多租户自动化测试进阶:从租户池到并发隔离的工程实践

做过多租户系统自动化测试的朋友应该都有同感&#xff1a;单租户的用例写得再漂亮&#xff0c;一到多租户环境就容易翻车。接口单独调都是通的&#xff0c;全量回归一跑就出现各种诡异问题——A租户的数据出现在B租户的报表里&#xff0c;某个租户的配置被另一个租户的用例偷偷…

作者头像 李华