OpenCV 答题卡识别判卷实战:从图像预处理到自动评分

1. 项目背景与目标

在教育信息化场景中,答题卡自动识别是一项非常实用的技术。传统的人工阅卷不仅耗时,而且容易出错。借助 OpenCV 的图像处理能力,我们可以实现答题卡填涂区域的自动检测、答案比对和评分,大幅提升阅卷效率。

本文将以一个完整的 Python 案例为主线,详细讲解如何利用 OpenCV 实现答题卡识别判卷。案例的核心流程包括:图像预处理、轮廓检测、透视变换、填涂区域定位、答案比对与评分。通过本案例,你将掌握 OpenCV 在文档扫描与自动判卷中的典型应用方法。

2. 核心思路与整体流程

答题卡识别判卷的整体流程可以概括为以下几个关键步骤:

  1. 图像预处理:将彩色图像转换为灰度图,进行高斯模糊去噪,再通过 Canny 边缘检测提取轮廓信息。
  2. 轮廓检测与筛选:查找图像中的外部轮廓,按面积排序,找到近似为四边形的轮廓作为答题卡区域。
  3. 透视变换:对检测到的答题卡区域进行透视校正,得到正视角的标准化图像。
  4. 填涂区域定位:对校正后的图像进行二值化处理,查找圆形填涂轮廓,并按行列排序。
  5. 答案比对与评分:通过掩膜统计每个填涂区域的非零像素数量,判断填涂状态,与正确答案比对后计算得分。

下面将结合代码逐段讲解每个步骤的实现细节。

3. 辅助函数详解

在正式处理图像之前,代码定义了几个辅助函数,用于坐标排序、透视变换、轮廓排序和图像显示。理解这些函数是掌握整个案例的关键。

3.1 坐标点排序函数 order_points

透视变换要求输入四个点,且顺序必须为左上、右上、右下、左下。由于轮廓检测返回的点顺序是不确定的,因此需要先对四个点进行排序。

python 复制代码
def order_points(pts):
    # 一共4个坐标点
    rect = np.zeros((4, 2), dtype="float32")  # 用来存储排序之后的坐标位置
    # 按顺序找到对应坐标0123分别是 左上,右上,右下,左下
    s = pts.sum(axis=1)  # 对pts矩阵的每一行进行求和操作。(x+y)
    rect[0] = pts[np.argmin(s)]
    rect[2] = pts[np.argmax(s)]
    diff = np.diff(pts, axis=1)  # 对pts矩阵的每一行进行求差操作。(y-x)
    rect[1] = pts[np.argmin(diff)]
    rect[3] = pts[np.argmax(diff)]
    return rect

排序原理如下:

  • 左上角的点 x+y 最小,右下角的点 x+y 最大,因此通过求和可以确定左上和右下。
  • 右上角的点 y-x 最小,左下角的点 y-x 最大,因此通过求差可以确定右上和左下。

这种基于几何特性的排序方法简洁高效,是透视变换前的重要准备工作。

3.2 透视变换函数 four_point_transform

透视变换的目的是将倾斜拍摄的答题卡校正为正视角图像,便于后续处理。

python 复制代码
def four_point_transform(image, pts):
    # 获取输入坐标点
    rect = order_points(pts)
    (tl, tr, br, bl) = rect
    # 计算输入的w和h值
    widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
    widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
    maxWidth = max(int(widthA), int(widthB))
    heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
    heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
    maxHeight = max(int(heightA), int(heightB))
    # 变换后对应坐标位置
    dst = np.array([[0, 0], [maxWidth - 1, 0],
                    [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]], dtype="float32")
    M = cv2.getPerspectiveTransform(rect, dst)
    warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
    # 返回变换后结果
    return warped

函数首先计算原始四边形四条边的欧氏距离,取最大值作为变换后图像的宽和高,确保校正后的图像不会丢失内容。然后构造目标矩形坐标,通过 cv2.getPerspectiveTransform 计算变换矩阵,最后用 cv2.warpPerspective 完成图像校正。

3.3 轮廓排序函数 sort_contours

检测到的填涂轮廓顺序是杂乱的,需要按行或按列排序,才能与题目序号对应。

python 复制代码
def sort_contours(cnts, method='left-to-right'):
    reverse = False
    i = 0
    if method == "right-to-left" or method == 'bottom-to-top':
        reverse = True
    if method == 'top-to-bottom' or method == 'bottom-to-top':
        i = 1
    boundingBoxes = [cv2.boundingRect(c) for c in cnts]
    (cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),
                                        key=lambda b: b[1][i],
                                        reverse=reverse))
    return cnts, boundingBoxes

函数通过 cv2.boundingRect 获取每个轮廓的外接矩形,然后根据指定的排序方法(如 top-to-bottom)对轮廓进行排序。排序键为外接矩形的 y 坐标(按行)或 x 坐标(按列),通过 reverse 参数控制升序或降序。

3.4 图像显示函数 cv_show

python 复制代码
def cv_show(name, img):
    cv2.imshow(name, img)
    cv2.waitKey(0)

该函数用于在调试过程中显示中间结果图像。cv2.waitKey(0) 会等待用户按键后继续执行,方便逐步观察每个处理阶段的效果。

4. 图像预处理与答题卡外四边形轮廓定位

模块目标:从原始拍摄图像中提取答题卡的外边界四边形,为后续透视矫正提供四个角点。

核心思路:先通过灰度化、高斯模糊、Canny 边缘检测得到干净的边缘图,再查找最外层轮廓,按面积降序筛选出近似四边形的轮廓作为答题卡区域。

关键约束 :使用 cv2.RETR_EXTERNAL 只取最外层轮廓,避免内部填涂圆圈干扰;多边形逼近的顶点数必须恰好为 4。

python 复制代码
# 预处理
image = cv2.imread(r'./images/test_01.png')
contours_img = image.copy()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, ksize=(5, 5), sigmaX=0)
edged = cv2.Canny(blurred, 75, 200)
轮廓检测
cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)
docCnt = None
for c in cnts:
peri = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.02 * peri, True)
if len(approx) == 4:
docCnt = approx
break

灰度化减少计算量并保留边缘信息;高斯模糊抑制噪声,避免 Canny 产生过多虚假边缘;Canny 低阈值 75、高阈值 200 提取强边缘。按面积降序遍历轮廓,用 cv2.approxPolyDP 做多边形逼近,当顶点数为 4 时即认定为答题卡外边界。

5. 四点排序工具函数与透视变换图像矫正

模块目标:将倾斜拍摄的答题卡校正为正视角图像,并保证四个角点顺序为左上、右上、右下、左下。

核心思路 :利用几何特性对四个角点排序------左上角 x+y 最小、右下角 x+y 最大、右上角 y-x 最小、左下角 y-x 最大;再计算四边形边长最大值作为目标尺寸,构造透视变换矩阵完成矫正。

关键约束:轮廓检测返回的角点顺序不确定,必须先排序再变换;目标宽高取四条边欧氏距离的最大值,避免校正后内容丢失。

python 复制代码
def order_points(pts):
    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
def four_point_transform(image, pts):
rect = order_points(pts)
(tl, tr, br, bl) = rect
widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
maxWidth = max(int(widthA), int(widthB))
heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
maxHeight = max(int(heightA), int(heightB))
dst = np.array([[0, 0], [maxWidth - 1, 0],
[maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]], dtype="float32")
M = cv2.getPerspectiveTransform(rect, dst)
warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
return warped
执行透视变换
warped_t = four_point_transform(image, docCnt.reshape(4, 2))
warped_new = warped_t.copy()
warped = cv2.cvtColor(warped_t, cv2.COLOR_BGR2GRAY)

order_points 通过求和与求差确定四个角点的几何位置;four_point_transform 计算原始四边形四条边的欧氏距离,取最大值作为变换后图像的宽和高,再通过 cv2.getPerspectiveTransform 计算变换矩阵,最后用 cv2.warpPerspective 完成矫正。矫正后的彩色图 warped_t 用于后续绘制,灰度图 warped 用于二值化。

6. 矫正图二值化与筛选选项圆形候选轮廓

模块目标:从矫正后的灰度图中提取所有填涂圆圈的候选轮廓,并按尺寸和形状过滤。

核心思路 :使用 Otsu 自适应阈值配合 THRESH_BINARY_INV 反向二值化,使填涂区域变为白色前景;再次查找轮廓后,通过宽高比和最小尺寸过滤出近似圆形的填涂区域。

关键约束:宽高均不小于 20 像素,宽高比在 0.9 到 1.1 之间,确保筛选出的是近似圆形的填涂区域。

python 复制代码
# 阈值处理
thresh = cv2.threshold(warped, 0, 255,
                       cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
# 找到每一个圆圈轮廓
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
筛选圆形候选轮廓
questionCnts = []
for c in cnts:
(x, y, w, h) = cv2.boundingRect(c)
ar = w / float(h)
if w >= 20 and h >= 20 and 0.9 <= ar <= 1.1:
questionCnts.append(c)

为什么使用 THRESH_BINARY_INV 反向二值化? 原始答题卡中,填涂的圆圈是深色(铅笔或墨水),背景是浅色。默认的 THRESH_BINARY 会把浅色背景置为 255(白色)、深色填涂置为 0(黑色)。而后续的掩膜统计依赖 cv2.countNonZero 统计白色像素数量,因此需要反向二值化,把填涂区域变成白色前景、背景变成黑色,这样填涂区域的非零像素数才会显著高于未填涂区域。

过滤条件确保候选轮廓是近似圆形:宽高比接近 1 排除细长噪声,最小尺寸 20 像素排除小噪点。筛选结果还需按从上到下排序,使轮廓顺序与题目行号对应。

7. 轮廓排序工具函数实现

模块目标:将检测到的填涂轮廓按行或按列排序,使轮廓顺序与题目序号对应。

核心思路 :通过 cv2.boundingRect 获取每个轮廓的外接矩形,根据排序方法选择 xy 坐标作为排序键,用 reverse 参数控制升序或降序。

关键约束 :排序键必须与题目布局一致------按行排列的题目用 top-to-bottomy 坐标),按列排列的题目用 left-to-rightx 坐标)。

python 复制代码
def sort_contours(cnts, method='left-to-right'):
    reverse = False
    i = 0
    if method == "right-to-left" or method == 'bottom-to-top':
        reverse = True
    if method == 'top-to-bottom' or method == 'bottom-to-top':
        i = 1
    boundingBoxes = [cv2.boundingRect(c) for c in cnts]
    (cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),
                                        key=lambda b: b[1][i],
                                        reverse=reverse))
    return cnts, boundingBoxes
按行排序,使轮廓顺序与题目行号对应
questionCnts = sort_contours(questionCnts, method="top-to-bottom")[0]

该函数返回排序后的轮廓列表及其外接矩形。主流程中先按 top-to-bottom 排序得到每行题目的轮廓,再在后续评分逻辑中按 left-to-right 对每行内部的 5 个选项排序。

8. 掩码填涂检测、标准答案比对与计分判分

模块目标:逐行识别考生填涂的答案,与标准答案比对并计算得分。

核心思路 :为每个选项轮廓创建掩膜,通过 cv2.bitwise_and 提取掩膜区域内的二值化像素,用 cv2.countNonZero 统计非零像素数量,像素数最大的选项即为填涂答案;再与 ANSWER_KEY 比对,答对绿色标记、答错红色标记。

关键约束 :每行固定 5 个选项,按 np.arange(0, len(questionCnts), 5) 切分;掩膜必须与二值化图像尺寸一致。

python 复制代码
correct = 0
# 每排有5个选项
for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)):
    cnts = sort_contours(questionCnts[i:i + 5])[0]  # 行内按左到右排序
    bubbled = None
    for (j, c) in enumerate(cnts):
        # 为每个选项创建掩膜并填充
        mask = np.zeros(thresh.shape, dtype="uint8")
        cv2.drawContours(mask, [c], -1, 255, -1)
        # 只保留掩膜区域内的像素
        thresh_mask_and = cv2.bitwise_and(thresh, thresh, mask=mask)
        total = cv2.countNonZero(thresh_mask_and)
        if bubbled is None or total > bubbled[0]:
            bubbled = (total, j)
    # 与标准答案比对
    color = (0, 0, 255)
    k = ANSWER_KEY[q]
    if k == bubbled[1]:
        color = (0, 255, 0)
        correct += 1
    cv2.drawContours(warped_new, [cnts[k]], -1, color, 3)
score = (correct / 5.0) * 100
print("[INFO] score: {:.2f}%".format(score))
cv2.putText(warped_new, "{:.2f}%".format(score), (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
cv2.imshow("Original", image)
cv2.imshow("Exam", warped_new)
cv2.waitKey(0)

为什么选用 countNonZero 统计填涂区域? 反向二值化后,填涂区域为白色(255)、未填涂区域为黑色(0)。cv2.countNonZero 统计掩膜区域内灰度值不为 0 的像素数量,填涂选项的白色像素数远多于未填涂选项,因此像素数最大的选项即为考生填涂的答案。这种方法无需依赖颜色阈值,对铅笔、钢笔等不同填涂介质均适用。

当前 Demo 的缺陷:多选与空题场景

当前逻辑存在两个明显缺陷:

多选场景 :逻辑只取像素数最大的选项作为唯一答案(bubbled = (total, j) 只保留最大值),无法识别多选题。若考生填涂多个选项,程序只会把像素数最大的那个当作答案,其余填涂选项被忽略,导致判分错误。

空题场景 :若某题未填涂任何选项,所有选项的 total 都接近 0,但 bubbled 仍会取像素数相对最大的选项(即使该值极小),程序会误判为填涂了该选项,导致空题被错误判分。

改进方向:可设置非零像素阈值(如 total > 某阈值 才视为填涂),并统计所有超过阈值的选项索引以支持多选。

9. 结果展示与评分输出

最后,将评分结果绘制在图像上并显示。

python 复制代码
score = (correct / 5.0) * 100
print("[INFO] score: {:.2f}%".format(score))
cv2.putText(warped_new, "{:.2f}%".format(score), (10, 30),
            cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
cv2.imshow("Original", image)
cv2.imshow("Exam", warped_new)
cv2.waitKey(0)

得分计算公式为 正确题数 / 总题数 × 100。最终结果通过 cv2.putText 绘制在图像左上角,并同时显示原始图像和识别结果图像,方便直观对比。

结果展示:

10. 完整代码汇总

为了方便读者运行和调试,这里给出完整的代码:

python 复制代码
import numpy as np
import cv2
ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}  # 正确答案
def order_points(pts):
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
def four_point_transform(image, pts):
rect = order_points(pts)
(tl, tr, br, bl) = rect
widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
maxWidth = max(int(widthA), int(widthB))
heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
maxHeight = max(int(heightA), int(heightB))
dst = np.array([[0, 0], [maxWidth - 1, 0],
[maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]], dtype="float32")
M = cv2.getPerspectiveTransform(rect, dst)
warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
return warped
def sort_contours(cnts, method='left-to-right'):
reverse = False
i = 0
if method == "right-to-left" or method == 'bottom-to-top':
reverse = True
if method == 'top-to-bottom' or method == 'bottom-to-top':
i = 1
boundingBoxes = [cv2.boundingRect(c) for c in cnts]
(cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),
key=lambda b: b[1][i],
reverse=reverse))
return cnts, boundingBoxes
def cv_show(name, img):
cv2.imshow(name, img)
cv2.waitKey(0)
预处理
image = cv2.imread(r'./images/test_01.png')
contours_img = image.copy()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, ksize=(5, 5), sigmaX=0)
cv_show('blurred', blurred)
edged = cv2.Canny(blurred, 75, 200)
cv_show('edged', edged)
轮廓检测
cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
cv2.drawContours(contours_img, cnts, -1, (0, 0, 255), 3)
cv_show('contours_img', contours_img)
docCnt = None
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)
for c in cnts:
peri = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.02 * peri, True)
if len(approx) == 4:
docCnt = approx
break
执行透视变换
warped_t = four_point_transform(image, docCnt.reshape(4, 2))
warped_new = warped_t.copy()
cv_show('warped', warped_t)
warped = cv2.cvtColor(warped_t, cv2.COLOR_BGR2GRAY)
阈值处理
thresh = cv2.threshold(warped, 0, 255,
cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
cv_show('thresh', thresh)
thresh_Contours = thresh.copy()
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
warped_Contours = cv2.drawContours(warped_t, cnts, -1, (0, 255, 0), 1)
cv_show('warped_Contours', warped_Contours)
questionCnts = []
for c in cnts:
(x, y, w, h) = cv2.boundingRect(c)
ar = w / float(h)
if w >= 20 and h >= 20 and 0.9 <= ar <= 1.1:
questionCnts.append(c)
print(len(questionCnts))
questionCnts = sort_contours(questionCnts, method="top-to-bottom")[0]
correct = 0
for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)):
cnts = sort_contours(questionCnts[i:i + 5])[0]
bubbled = None
for (j, c) in enumerate(cnts):
mask = np.zeros(thresh.shape, dtype="uint8")
cv2.drawContours(mask, [c], -1, 255, -1)
cv_show('mask', mask)
thresh_mask_and = cv2.bitwise_and(thresh, thresh, mask=mask)
cv_show('thresh_mask_and', thresh_mask_and)
total = cv2.countNonZero(thresh_mask_and)
if bubbled is None or total > bubbled[0]:
bubbled = (total, j)
color = (0, 0, 255)
k = ANSWER_KEY[q]
if k == bubbled[1]:
color = (0, 255, 0)
correct += 1
cv2.drawContours(warped_new, [cnts[k]], -1, color, 3)
cv_show('warpeding', warped_new)
score = (correct / 5.0) * 100
print("[INFO] score: {:.2f}%".format(score))
cv2.putText(warped_new, "{:.2f}%".format(score), (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
cv2.imshow("Original", image)
cv2.imshow("Exam", warped_new)
cv2.waitKey(0)

11. 运行效果与结果分析

运行上述代码后,程序会依次弹出多个窗口,展示每个处理阶段的中间结果。最终输出识别得分,并在图像上标注每道题的判定结果。

从实际运行效果来看,该方案能够准确识别填涂的答案,并正确计算得分。绿色轮廓表示答对的题目,红色轮廓表示答错的题目,左上角显示最终得分百分比。

需要注意的是,识别效果受图像质量影响较大。如果拍摄角度过于倾斜、光线不均匀或填涂不完整,可能导致识别错误。此时可以适当调整 Canny 阈值、轮廓过滤尺寸或透视变换参数来优化效果。

12. 总结与扩展思考

本文通过一个完整的 OpenCV 案例,详细讲解了答题卡识别判卷的实现流程。核心知识点包括:

图像预处理:灰度化、高斯模糊、Canny 边缘检测的组合应用。

轮廓检测与筛选:通过面积排序和多边形逼近定位目标区域。

透视变换:校正倾斜图像,得到标准化视角。

掩膜统计:利用掩膜和像素统计判断填涂状态。

在此基础上,还可以进一步扩展:

支持多页答题卡批量识别,提高处理效率。

引入 OCR 技术识别考生信息和准考证号。

优化填涂检测算法,支持铅笔和钢笔等多种填涂方式。

结合深度学习模型,提升复杂场景下的鲁棒性。

希望本文能帮助你理解 OpenCV 在文档识别领域的应用思路,并激发更多实践灵感。

相关推荐
cd_9492172125 分钟前
AI自动生成纹理能保持角色不同部位风格一致吗?
人工智能
人工智能培训32 分钟前
人工智能数据安全下的个人信息保护实践方案
大数据·人工智能·算法·生活
大象AI共学33 分钟前
# 【AI成本治理】额度缩水 44%-76% 之后的自查清单:10 题体检表 + 三档模型组合 + 免费/本地化实测方案
人工智能
OceanBase数据库官方博客33 分钟前
亮相“十五五”公积金高质量发展大会“AI 数据库”助力智慧公积金升级
数据库·人工智能
学习星球39 分钟前
2026 AI游戏开发实战指南:从NVIDIA ACE到Summer Engine,AI如何重塑游戏开发
人工智能·microsoft
稚语成长43 分钟前
适趣大历史是什么?儿童历史启蒙 AI App 解析(H1–H7 分级、适合几岁、怎么用)
人工智能
dongd7031 小时前
2026年大语言模型AI网关行业调查报告已出刊:产业链、占有率、发展前景,一次讲透
大数据·人工智能·语言模型
一RTOS一1 小时前
2026世界机器人大会:鸿道为具身智能机器人打造“神经系统”
人工智能·机器人·鸿道操作系统·鸿道实时操作系统·国产嵌入式操作系统选型·智算控一体
auto_go1 小时前
大模型实战指南(10)——多模态实战:让模型“看懂”图片和视频
大数据·人工智能·音视频