vieyahn2017 / iBlog

44 stars 0 forks source link

12.17 opencv3+python3.5成语填字游戏 #379

Closed vieyahn2017 closed 3 months ago

vieyahn2017 commented 3 years ago

opencv3+python3.5成语填字游戏

https://blog.csdn.net/weixin_41553161/article/details/78974832

vieyahn2017 commented 3 years ago

这是一个成语填字游戏,大概就是一张成语填字游戏图片,通过opencv图像识别后转为矩阵,再通过解算法,解出答案,在显示到图片上。

源代码:https://github.com/mayue801/crossword-puzzle--idiom

vieyahn2017 commented 3 years ago

本文采用投影分割法对印刷体汉字进行分割。

投影分割是先水平方向投影,在竖直方向投影,或者先竖直方向再水平方向投影。本文选用先竖直,再水平。

1.竖直投影。

代码:

#针对的是印刷版的汉字,所以采用了投影法分割
#此函数是行分割,结果是一行文字
def YShadow(path):
    img  = cv2.imread(path)   #原图像
    gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) #灰度图像
    height,width = img.shape[:2]

    #blur = cv2.GaussianBlur(gray,(5,5),0) #高斯模糊

    blur = cv2.blur(gray,(8,8)) #均值模糊
    thresh = cv2.adaptiveThreshold(blur,255,1,1,11,2)  #自适应阈值分割
    temp = thresh

    if(width > 500 and height > 400): #图像字体较小时,需要进行膨胀操作
        kernel = np.ones((5,5),np.uint8) #卷积核
        dilation = cv2.dilate(thresh,kernel,iterations = 1) #膨胀操作使得单个文字图像被黑像素填充
        temp = dilation

    '''
    cv2.imshow('image',temp)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    '''

    perPixelValue = 1 #每个像素的值
    projectValArry = np.zeros(width, np.int8) #创建一个用于储存每列黑色像素个数的数组

    for i in range(0,height):
        for j in range(0,width):
            perPixelValue = temp[i,j]
            if (perPixelValue == 255): #如果是黑字,对应位置的值+1
                projectValArry[i] += 1
       # print(projectValArry[i])

    canvas = np.zeros((height,width), dtype="uint8")

    for i in range(0,height):
        for j in range(0,width):
            perPixelValue = 255 #白色背景
            canvas[i, j] = perPixelValue

    for i in range(0,height):
        for j in range(0,projectValArry[i]):
            perPixelValue = 0 #黑色直方图投影
            canvas[i, width-j-1] = perPixelValue
    '''
    cv2.imshow('canvas',canvas)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    '''

    list = []
    startIndex = 0 #记录进入字符区的索引  
    endIndex = 0 #记录进入空白区域的索引  
    inBlock = 0 #是否遍历到了字符区内  

    for i in range(height):
        if (inBlock == 0 and projectValArry[i] != 0): #进入字符区
            inBlock = 1  
            startIndex = i
        elif (inBlock == 1 and projectValArry[i] == 0):#进入空白区
            endIndex = i
            inBlock = 0
            subImg = gray[startIndex:endIndex+1,0:width] #将对应字的图片截取下来
            #print(startIndex,endIndex+1)
            list.append(subImg)#添加这个字图像到list
    #print(len(list))
    return list

2.水平投影

#对行字进行单个字的分割
def XShadow(path):
    img  = cv2.imread(path)       
    gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
    height,width = img.shape[:2]
   # print(height,width)
    #blur = cv2.GaussianBlur(gray,(5,5),0)

    blur = cv2.blur(gray,(8,8))
    thresh = cv2.adaptiveThreshold(blur,255,1,1,11,2) 

    if(width > 500):
        kernel = np.ones((4, 4),np.uint8) #卷积核
    else:
        kernel = np.ones((2, 2),np.uint8) #卷积核
    dilation = cv2.dilate(thresh,kernel,iterations = 1) #膨胀操作使得单个文字图像被黑像素填充

    '''
    cv2.imshow('image',thresh)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    '''

    perPixelValue = 1 #每个像素的值
    projectValArry = np.zeros(width, np.int8) #创建一个用于储存每列黑色像素个数的数组

    for i in range(0,width):
        for j in range(0,height):
            perPixelValue = dilation[j,i]
            if (perPixelValue == 255): #如果是黑字
                projectValArry[i] += 1
       # print(projectValArry[i])

    canvas = np.zeros((height,width), dtype="uint8")

    for i in range(0,width):
        for j in range(0,height):
            perPixelValue = 255 #白色背景
            canvas[j, i] = perPixelValue

    for i in range(0,width):
        for j in range(0,projectValArry[i]):
            perPixelValue = 0 #黑色直方图投影
            canvas[height-j-1, i] = perPixelValue
    '''
    cv2.imshow('canvas',canvas)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    '''

    list = []
    startIndex = 0 #记录进入字符区的索引  
    endIndex = 0 #记录进入空白区域的索引  
    inBlock = 0 #是否遍历到了字符区内  

    for i in range(width):
        if (inBlock == 0 and projectValArry[i] != 0): #进入字符区
            inBlock = 1  
            startIndex = i
        elif (inBlock == 1 and projectValArry[i] == 0): #进入投影区
            endIndex = i
            inBlock = 0
            #subImg = gray[0:height, startIndex:endIndex+1] #endIndex+1
            #print(startIndex,endIndex+1)
            list.append([startIndex, 0, endIndex-startIndex-1, height])
    #print(len(list))
    return list

分割完后,将对应图片样本存储到对应文件夹,每个字共10种样本

将这些样本及标记保存后,分别加载到samples.npy, label.npy中。供后续的机器学习算法训练使用。

下篇讲解填字图片汉字的提取与机器学习算法训练样本,识别汉字的过程。

vieyahn2017 commented 3 years ago

opencv3+python3.5成语填字游戏(二)填字图片汉字提取和识别

https://blog.csdn.net/weixin_41553161/article/details/78984489 mler801 2018-01-05 19:24:00 1410 收藏 分类专栏: python3-x opencv3 版权 上一篇说的是汉字的分割。今天该实际填字图片的解析了。实际图片如下:

这是一个10*10的方格,所以我们应该先提取100个小方格,然后在提取其中的汉字。

用到的函数主要是python的opencv函数库里的findContours函数,用于找出图片中所有的轮廓层级。

## 轮廓提取
image, contours, hierarchy = cv2.findContours(dilated,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
  1. 提取100个方格的代码;

    for i in range(len(hierarchy[0])):
    if hierarchy[0][i][3] == 0:
        boxes.append(hierarchy[0][i])
        indexs.append(i)
  2. 提取方格中的数字,还有将白色空白方格填'1',黄色方格填“0”,主要是为了形成初始填字矩阵,便于后续的解密算法的进行。代码:

    #提取方格中的汉字
    for j in range(len(boxes)):
    if boxes[j][2] == -1: #方格中空白
        x,y,w,h = cv2.boundingRect(contours[indexs[j]])
        number_boxes.append([x,y,w,h])
        #cv2.rectangle(img,(x-1,y-1),(x+w-10,y+h-10),(0,0,255),1)
        centerColor = img[round((2*y+h)/2),round((2*x+w)/2)]
        #print(centerColor)
        if(centerColor[0] > 200): #区分出黄色格与白色格,黄色(0,255,255)白色(255,255,255)
            #print(y/box_h,round(y/box_h),x/box_w,round(x/box_w))
            miyu[round(y/box_h)][round(x/box_w)] = "1" #白色空格填‘1’
    elif boxes[j][2] != -1: #方格中有字
        x,y,w,h = cv2.boundingRect(contours[boxes[j][2]])
        #print(x,y,w,h)
    
        number_boxes.append([x,y,w,h])
        #cv2.rectangle(img,(x-1,y-1),(x+w+1,y+h+1),(0,255,0),1)
        #img = cv2.drawContours(img, contours, boxes[j][2], (0,255,0), 1)
        ## 对提取的数字进行处理
        number_roi = gray[y:y+h, x:x+w]
        ## 统一大小
        resized_roi=cv2.resize(number_roi,(30,30))
        thresh1 = cv2.adaptiveThreshold(resized_roi,255,1,1,11,2) 
        ## 归一化像素值
        normalized_roi = thresh1/255.  
        '''
        cv2.imshow("thresh1", thresh1)
        cv2.waitKey(0)
        cv2.destroyAllWindows()
        '''
        ## 展开成一行让knn识别
        sample1 = normalized_roi.reshape((1,len(normalized_roi)*len(normalized_roi[0])))
        sample1 = np.array(sample1,np.float32)
    
        ## knn识别
        retval, results, neigh_resp, dists = model.findNearest(sample1, 1)        
        number = int(results.ravel()[0])
        #print(number)
        #numbers.append(number)
    
        # 第一个参数为打印的坐标,第二个为打印的文本,第三个为字体颜色,第四个为字体
        draw.text((x+(w/2)+10,y-10), str(hanzis[number-1]), (0, 0, 255), font=font) 
    
        ## 求在矩阵中的位置
        miyu[round(y/box_h)][round(x/box_w)] = str(hanzis[number-1])

3.图片中汉字的识别,使用的是knn算法,代码:

#创建knn对象并训练样本
model = cv2.ml.KNearest_create()
model.train(samples,cv2.ml.ROW_SAMPLE,labels)

识别函数代码:

## knn识别
retval, results, neigh_resp, dists = model.findNearest(sample1, 1)   #预测测试样本     
number = int(results.ravel()[0]) #得出预测样本的样本标记

knn主要是监督学习算法,先对已有数据样本训练,然后根据已有样本预测测试样本,准确度依赖于原始样本的准确度,并且不能保证100%的准确率。

识别结果:

此准确度是在多次调试之后才完全正确的,包括对测试图片本身大小像素调整,还有训练样本图片的调整,期间试了很多次,一直识别不对,所以识别准确度只针对本项目中的图片和样本。

源代码:https://github.com/mayue801/crossword-puzzle--idiom

vieyahn2017 commented 3 years ago

opencv3+python3.5成语填字游戏(三)成语填字游戏解密算法

mler801 2018-01-25 20:49:03 1788 收藏 2 分类专栏: python3-x opencv3 版权 本篇介绍填字游戏解密算法,本算法尚且存在一些问题,并不适合所有成语填字游戏。

源代码:https://github.com/mayue801/crossword-puzzle--idiom 1.找到横向四字成语,并返回对应的在方格数组中的位置,还有对应下标

#寻找横向的四字成语方格
def findXFour(mi, i, j):
    if (j+1 == 10 or mi[i][j+1] == '0') and j-1>=0 and mi[i][j-1] != '0':
        return [[mi[i][j-3],mi[i][j-2],mi[i][j-1],mi[i][j]],
                [i,j-3,i,j-2,i,j-1,i,j]]
    elif (j+2 ==10 or (0 if j+2>10 else mi[i][j+2] == '0')) and j-1>=0 and mi[i][j-1] != '0':
        return [[mi[i][j-2],mi[i][j-1],mi[i][j],mi[i][j+1]],
                [i,j-2,i,j-1,i,j,i,j+1]]
    elif (j+3 ==10 or (0 if j+3>10 else mi[i][j+3] == '0')) and j-1>=0 and mi[i][j-1] != '0':
        return [[mi[i][j-1],mi[i][j],mi[i][j+1],mi[i][j+2]],
                [i,j-1,i,j,i,j+1,i,j+2]]
    elif (j+4 ==10 or (0 if j+4>10 else mi[i][j+4] == '0')) and (mi[i][j+1] != '0'):
        return [[mi[i][j],mi[i][j+1],mi[i][j+2],mi[i][j+3]],
                [i,j,i,j+1,i,j+2,i,j+3]]
    else:
        return []
  1. 找到纵向四字成语,并返回对应的在方格数组中的位置,还有对应下标
    #寻找纵向的四字成语方格
    def findYFour(mi, i, j):
    if (i+1==10 or mi[i+1][j] == '0') and i-1>=0 and mi[i-1][j] != '0':
        return [[mi[i-3][j],mi[i-2][j],mi[i-1][j],mi[i][j]],
                [i-3,j,i-2,j,i-1,j,i,j]]
    elif (i+2==10 or (0 if i+2>10 else mi[i+2][j] == '0')) and i-1>=0 and mi[i-1][j] != '0':
        return [[mi[i-2][j],mi[i-1][j],mi[i][j],mi[i+1][j]],
                [i-2,j,i-1,j,i,j,i+1,j]]
    elif (i+3==10 or (0 if i+3>10 else mi[i+3][j] == '0')) and i-1>=0 and mi[i-1][j] != '0':
        return [[mi[i-1][j],mi[i][j],mi[i+1][j],mi[i+2][j]],
                [i-1,j,i,j,i+1,j,i+2,j]]
    elif (i+4==10 or (0 if i+4>10 else mi[i+4][j] == '0')) and (mi[i+1][j] == '0'):
        return [[mi[i][j],mi[i+1][j],mi[i+2][j],mi[i+3][j]],
                [i,j,i+1,j,i+2,j,i+3,j]]
    else:
        return []

3.将找到的成语答案填入到相应成语方格数组中.

#改变对应方格的原生成成语填字矩阵
def changeMi(res, micopy):
    counts= []
    for j in range(len(chengyus)):
        count = 0
        if res[0][0] == chengyus[j][0]:
            count += 1
        if res[0][1] == chengyus[j][1]:
            count += 1
        if res[0][2] == chengyus[j][2]:
            count += 1
        if res[0][3] == chengyus[j][3]:
            count += 1
        counts.append(count)
    m = counts.index(max(counts))    
    #print(max(counts))
    micopy[res[1][0]][res[1][1]] = chengyus[m][0]
    micopy[res[1][2]][res[1][3]] = chengyus[m][1]
    micopy[res[1][4]][res[1][5]] = chengyus[m][2]
    micopy[res[1][6]][res[1][7]] = chengyus[m][3]
    chengyus.remove([chengyus[m][0],chengyus[m][1],chengyus[m][2],chengyus[m][3]])

4.判断临时的成语数组中是否存在找到的成语

#判断res此四字成语是否在nodes里面存在    
def isExist(nodes,res):
    for i in range(len(nodes)):
        if(nodes[i][0][0] == res[0][0] and nodes[i][0][1] == res[0][1] 
           and nodes[i][0][2] == res[0][2] and nodes[i][0][3] == res[0][3]):
            return 1
    return 0

5.得到每一个对应i,j位置的成语,先横向查找,否则纵向查找,如果存在且未替换过则填入答案

#实际的查找成语方格并修改原成语矩阵的函数
def getResult(mi, i,j, nodes, micopy):
    temp = findXFour(mi, i, j)
    res = (len(temp) != 0 and temp or findYFour(mi, i, j))

    if(len(res) > 0 and not isExist(nodes, res)):
        nodes.append(res)
        changeMi(res, micopy)

6.得到所有答案

import copy
#总体的揭解谜函数    
def solve(mi):
    nodes = []
    micopy = copy.deepcopy(mi)  #对象拷贝,深拷贝
    for i in range(len(mi)):
        for j in range(len(mi[i])):
            if(mi[i][j] != '0'):
                getResult(mi, i,j, nodes, micopy)
   # print(nodes)
    return micopy

后台打印得到的成语填字数组:

生成的填字游戏:

[['清', '1', '1', '1', '0', '0', '劳', '1', '1', '高'], ['0', '0', '辨', '0', '0', '0', '0', '0', '0', '1'], ['0', '0', '1', '0', '0', '0', '0', '0', '0', '1'], ['1', '亲', '1', '1', '0', '0', '1', '如', '1', '水'], ['0', '0', '0', '步', '1', '1', '花', '0', '0', '0'], ['0', '0', '0', '1', '0', '0', '1', '0', '0', '0'], ['比', '1', '可', '1', '0', '0', '1', '1', '归', '1'], ['1', '0', '0', '0', '0', '0', '0', '0', '0', '1'], ['1', '0', '0', '0', '0', '0', '0', '0', '0', '水'], ['飞', '1', '1', '火', '0', '0', '1', '起', '1', '秀']]

求解后的填字游戏:

[['清', '风', '明', '月', '0', '0', '劳', '苦', '功', '高'], ['0', '0', '辨', '0', '0', '0', '0', '0', '0', '山'], ['0', '0', '是', '0', '0', '0', '0', '0', '0', '流'], ['非', '亲', '非', '故', '0', '0', '心', '如', '止', '水'], ['0', '0', '0', '步', '步', '莲', '花', '0', '0', '0'], ['0', '0', '0', '自', '0', '0', '怒', '0', '0', '0'], ['比', '屋', '可', '封', '0', '0', '放', '虎', '归', '山'], ['翼', '0', '0', '0', '0', '0', '0', '0', '0', '清'], ['双', '0', '0', '0', '0', '0', '0', '0', '0', '水'], ['飞', '蛾', '扑', '火', '0', '0', '后', '起', '之', '秀']]

最后,将得到的结果打印到图片上(解谜前后):