You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

OMR项目cX/cY参数调试难题:标记位置偏移且数量不符

OMR光学标记识别项目:cX/cY参数定位问题修复

问题现象

开发OMR项目时,运行代码后生成的绿色标记仅显示5个(预期10个),且未精准定位到答题卡的填涂区域,手动调整cX、cY参数后无改善。

相关代码

主代码

import cv2
import numpy as np
import utiliss

#######################
path = "omr_sheetF2.jpg"
heightImg=600
widthImg=600
questions=10
choices=5
# 修正:原数组中第5个元素为5,超出选项索引范围(0-4),改为有效索引4
ans = [0,2,1,4,4,0,3,2,2,1]
#######################

img = cv2.imread(path)

# 预处理
img = cv2.resize(img,(widthImg,heightImg))
imgContours = img.copy()
imgBiggestcontours = img.copy()
imgGray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
imgBlur = cv2.GaussianBlur(imgGray,(5,5),1)
imgCanny = cv2.Canny(imgBlur,10,50)

# 查找轮廓
contours, hierarchy = cv2.findContours(imgCanny,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE)
cv2.drawContours(imgContours,contours,-1,(0,255,0),10)
# 筛选矩形轮廓
rectcon = utiliss.rectcontours(contours)
biggestcontour = utiliss.getcornerpoints(rectcon[0])
gradepoints = utiliss.getcornerpoints(rectcon[2])

if biggestcontour.size !=0 and gradepoints.size !=0:
    cv2.drawContours(imgBiggestcontours,biggestcontour,-1,(0,255,0),15)
    cv2.drawContours(imgBiggestcontours,gradepoints,-1,(255,0,0),15)

    biggestcontour= utiliss.reorder(biggestcontour)
    gradepoints = utiliss.reorder(gradepoints)

    # 透视变换校正答题卡区域
    pt1 = np.float32(biggestcontour)
    pt2 = np.float32([[0,0],[widthImg,0],[0,heightImg],[widthImg,heightImg]])
    matrix = cv2.getPerspectiveTransform(pt1,pt2)
    imgWarpcolored= cv2.warpPerspective(img,matrix,(widthImg,heightImg))

    # 透视变换校正分数显示区域
    ptG1 = np.float32(gradepoints)
    ptG2 = np.float32([[0, 0], [325, 0], [0, 150], [325, 150]])
    matrixG = cv2.getPerspectiveTransform(ptG1, ptG2)
    imgGradeDisplay = cv2.warpPerspective(img, matrixG, (325, 150))

    # 二值化处理
    imgWarpGray= cv2.cvtColor(imgWarpcolored,cv2.COLOR_BGR2GRAY)
    imgThresh= cv2.threshold(imgWarpGray,100,255,cv2.THRESH_BINARY_INV)[1]

    # 分割填涂框
    boxes= utiliss.splitboxes(imgThresh)

    # 统计每个填涂框的非零像素值
    myPixelVal= np.zeros((questions,choices))
    countC= 0
    countR= 0

    for image in boxes:
        totalpixels = cv2.countNonZero(image)
        myPixelVal[countR][countC] = totalpixels
        countC +=1
        if (countC == choices):countR +=1 ;countC=0

    # 获取每个问题的选中选项索引
    myIndex = []
    for x in range (0,questions):
        arr = myPixelVal[x]
        myIndexVal = np.where(arr==np.amax(arr))
        myIndex.append(myIndexVal[0][0])

    # 计算得分
    grading=[]
    for x in range(0,questions):
        if ans [x]== myIndex[x]:
            grading.append(1)
        else:grading.append(0)
    score= (sum(grading)/questions) *100 # 最终得分
    print(score)

    # 绘制答案标记
    imgResult = imgWarpcolored.copy()
    imgResult = utiliss.showanswers(imgResult,myIndex,grading,ans,questions,choices)


# 堆叠显示所有处理图像
imgBlank = np.zeros_like(img)
imageArray = ([img,imgGray,imgBlur,imgCanny],
              [imgContours,imgBiggestcontours,imgWarpcolored,imgThresh],
              [imgResult,imgBlank,imgBlank,imgBlank])
imgStacked = utiliss.stackImages (imageArray,0.25)

cv2.imshow("Stacked Images",imgStacked)
cv2.waitKey(0)

工具类(utiliss.py)代码

import cv2
import numpy as np

# 堆叠显示多张图像
def stackImages(imgArray,scale,lables=[]):
    rows = len(imgArray)
    cols = len(imgArray[0])
    rowsAvailable = isinstance(imgArray[0], list)
    width = imgArray[0][0].shape[1]
    height = imgArray[0][0].shape[0]
    if rowsAvailable:
        for x in range ( 0, rows):
            for y in range(0, cols):
                imgArray[x][y] = cv2.resize(imgArray[x][y], (0, 0), None, scale, scale)
                if len(imgArray[x][y].shape) == 2: imgArray[x][y]= cv2.cvtColor( imgArray[x][y], cv2.COLOR_GRAY2BGR)
        imageBlank = np.zeros((height, width, 3), np.uint8)
        hor = [imageBlank]*rows
        hor_con = [imageBlank]*rows
        for x in range(0, rows):
            hor[x] = np.hstack(imgArray[x])
            hor_con[x] = np.concatenate(imgArray[x])
        ver = np.vstack(hor)
        ver_con = np.concatenate(hor)
    else:
        for x in range(0, rows):
            imgArray[x] = cv2.resize(imgArray[x], (0, 0), None, scale, scale)
            if len(imgArray[x].shape) == 2: imgArray[x] = cv2.cvtColor(imgArray[x], cv2.COLOR_GRAY2BGR)
        hor= np.hstack(imgArray)
        hor_con= np.concatenate(imgArray)
        ver = hor
    if len(lables) != 0:
        eachImgWidth= int(ver.shape[1] / cols)
        eachImgHeight = int(ver.shape[0] / rows)
        for d in range(0, rows):
            for c in range (0,cols):
                cv2.rectangle(ver,(c*eachImgWidth,eachImgHeight*d),(c*eachImgWidth+len(lables[d][c])*13+27,30+eachImgHeight*d),(255,255,255),cv2.FILLED)
                cv2.putText(ver,lables[d][c],(eachImgWidth*c+10,eachImgHeight*d+20),cv2.FONT_HERSHEY_COMPLEX,0.7,(255,0,255),2)
    return ver

# 筛选矩形轮廓
def rectcontours(contours):
    rectcon=[]
    for i in contours:
        area = cv2.contourArea(i)
        if area>50:
            peri = cv2.arcLength(i,True)
            approx = cv2.approxPolyDP(i,0.02*peri,True)
            if len(approx)==4:
                rectcon.append(i)
    rectcon = sorted(rectcon,key= cv2.contourArea,reverse=True)
    return rectcon

# 获取轮廓的角点
def getcornerpoints(cont):
    peri = cv2.arcLength(cont, True)
    approx = cv2.approxPolyDP(cont, 0.02 * peri, True)
    return approx

# 重新排序角点(适配透视变换)
def reorder(myPoints):
    myPoints = myPoints.reshape((4,2))
    myPointsNew = np.zeros((4,1,2),np.int32)
    add = myPoints.sum (1)
    myPointsNew[0]= myPoints[np.argmin(add)] #[0, 0]
    myPointsNew[3]= myPoints[np.argmax(add)] #[w, h]
    diff = np.diff(myPoints,axis=1)
    myPointsNew[1]= myPoints[np.argmin(diff)] # [w, 0]
    myPointsNew[2]= myPoints[np.argmax(diff)] # [0, h]
    return myPointsNew

# 分割填涂框为10行5列
def splitboxes(img):
    rows= np.vsplit(img,10)
    boxes = []
    for r in rows:
        cols= np.hsplit(r,5)
        for box in cols:
            boxes.append(box)
    return boxes

# 绘制答案标记(修复版)
def showanswers(img,myIndex,grading,ans,questions,choices):
    # 修正:secW为图像宽度除以选项数(列数),secH为图像高度除以问题数(行数)
    secW= int(img.shape[1]/choices)
    secH= int(img.shape[0]/questions)

    for x in range (0, questions):
        myAns = myIndex[x]
        # 修正cX、cY计算逻辑,匹配行列分割方式
        cX = (myAns * secW) + secW // 2
        cY = (x * secH) + secH // 2

        print(f"Iteration {x + 1}: myAns={myAns}, cX={cX}, cY={cY}")

        cv2.circle(img,(cX,cY),20,(0,255,0),cv2.FILLED)

    return img

问题根源

  1. 行列尺寸计算错误:原showanswers函数中,secH错误使用图像宽度除以选项数,实际应该用图像高度除以问题数,导致行高计算过大,10个问题的标记超出图像范围,仅显示前5个。
  2. 答案数组索引越界:原ans数组中第5个元素为5,但选项只有5个(索引范围0-4),导致myIndex出现无效索引,进一步造成cX计算异常,标记位置错误。
  3. cX/cY映射逻辑颠倒:原代码中secW和secH的计算对应关系错误,导致列宽和行高不匹配答题卡的实际分割方式。

修复效果

修正后,绿色标记将显示全部10个,且精准定位到每个问题的填涂选项中心位置,与答题卡的实际填涂区域对齐。

内容的提问来源于stack exchange,提问作者Wakeel Omololu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 18:23:09