文章目录

  • 目的
  • 效果展示
  • 代码及解释
  • 原始文件
  • 代码
  • 代码解释
  • ① 主程序
  • ② ReadTxt() 函数
  • ③ rotate() 函数
  • ④ drawRect() 函数
  • 框大小不固定的倾斜矩形框



目的

这篇博客主要介绍如何使用 OpenCV 根据已有的像素点坐标文件在 jpg 图像上为腰椎间盘框大小固定的、倾斜的矩形框,并在矩形框的旁边标注相应的文本信息。文章还会对如何框大小不固定的倾斜矩形框进行说明。

效果展示

原腰椎间盘 jpg 图像如下所示:

opencv倾斜图像校正 opencv斜矩形_计算机视觉


经程序处理后的 jpg 图像如下所示:

opencv倾斜图像校正 opencv斜矩形_像素点_02

代码及解释

原始文件

原始文件为一张 jpg 图像和一个像素点坐标 txt 文件。jpg 图像在上面已经给出,像素点坐标 txt 文件的内容如下所示:

251,123
247,146
247,172
240,195
236,221
236,244
236,270
235,294
232,317
232,341
236,363

在 txt 文件中,除最后一行为空行外,其余的11行每一行代表一个图像中的像素位置。每一行为用逗号分隔开的两个数值,第一个数值为 x 轴坐标,第二个数值为 y 轴坐标。在图像中,坐标原点为图像的左上角,x 轴正方向水平向右,y 轴正方向竖直向下。如下图所示,这里为了更清楚地表达,将坐标原点稍微偏离了原位置(原位置在图像左上角)。

opencv倾斜图像校正 opencv斜矩形_opencv倾斜图像校正_03


txt 文件中从上到下所表示的11个点,分别与原图像中用白点标注出来的从上到下的11个位置相对应。可以发现11个位置中,从第1个位置开始,每隔一个点便标注了一个椎间盘中心,椎间盘与椎间盘之间的坐标代表椎体中心的位置。其实在真正处理图像的时候,我们只需要知道 txt 文件中的11个点所代表的是什么即可,无需在原图中标注出这些点,因为标注出这些点可能会影响后续的流程处理。

代码

在了解了原文件的内容之后,先给出图像处理的代码再详细解释:

import cv2
import math
from math import *
import numpy as np

def rotate(img, points, newImagePath):
    
    dis_tt = math.sqrt((points[1][0]-points[3][0])**2+(points[1][1]-points[3][1])**2)

    for point_i in range(6):
        if point_i == 0:
            ptup_center = [2*points[0][0]-points[1][0], 2*points[0][1]-points[1][1]]
        else:
            ptup_center = points[2*point_i-1]
            
        if point_i == 5:
            ptdown_center = [2*points[10][0]-points[9][0], 2*points[10][1]-points[9][1]]
        else:
            ptdown_center = points[2*point_i+1]
        
        newup_center = [0, 0]
        newdown_center = [0, 0]
        rect_center = (np.array(ptup_center) + np.array(ptdown_center))/2  #整体框的矩形中心
        
        if ptup_center[0] > rect_center[0]:
            newup_center[0] = 0.5*dis_tt/math.sqrt(1+((ptup_center[1]-rect_center[1])/(ptup_center[0]-rect_center[0]))**2) + rect_center[0]
        else:
            newup_center[0] = -0.5*dis_tt/math.sqrt(1+((ptup_center[1]-rect_center[1])/(ptup_center[0]-rect_center[0]))**2) + rect_center[0]
            
        if ptdown_center[0] > rect_center[0]:
            newdown_center[0] = 0.5*dis_tt/math.sqrt(1+((ptdown_center[1]-rect_center[1])/(ptdown_center[0]-rect_center[0]))**2) + rect_center[0]
        else:
            newdown_center[0] = -0.5*dis_tt/math.sqrt(1+((ptdown_center[1]-rect_center[1])/(ptdown_center[0]-rect_center[0]))**2) + rect_center[0]
        
        newup_center[1] = 0.5*dis_tt/math.sqrt(1+((ptup_center[0]-rect_center[0])/(ptup_center[1]-rect_center[1]))**2) + rect_center[1]
        newdown_center[1] = -0.5*dis_tt/math.sqrt(1+((ptdown_center[0]-rect_center[0])/(ptdown_center[1]-rect_center[1]))**2) + rect_center[1]

        # pt1:左上,pt2:左下,pt3:右下,pt4:右上
        pt1 = [0, 0]
        pt2 = [0, 0]
        pt3 = [0, 0]
        pt4 = [0, 0]
        angle = math.radians(90)
        # 顺时针旋转90,ptdown_center为要旋转的点,ptup_center为旋转中心
        pt1[0] = -0.6*((newdown_center[0]-newup_center[0])*cos(angle) - (newdown_center[1]-newup_center[1])*sin(angle))+newup_center[0]
        pt1[1] = -0.6*((newdown_center[0]-newup_center[0])*sin(angle) + (newdown_center[1]-newup_center[1])*cos(angle))+newup_center[1]
        # 逆时针旋转90,ptup_center为要旋转的点,ptdown_center为旋转中心
        pt2[0] = -0.6*((newup_center[0]-newdown_center[0])*cos(angle) + (newup_center[1]-newdown_center[1])*sin(angle))+newdown_center[0]
        pt2[1] = -0.6*((newup_center[1]-newdown_center[1])*cos(angle) - (newup_center[0]-newdown_center[0])*sin(angle))+newdown_center[1]
        # 顺时针旋转90,ptup_center为要旋转的点,ptdown_center为旋转中心
        pt3[0] = ((newup_center[0]-newdown_center[0])*cos(angle) + (newup_center[1]-newdown_center[1])*sin(angle))+newdown_center[0]
        pt3[1] = ((newup_center[1]-newdown_center[1])*cos(angle) - (newup_center[0]-newdown_center[0])*sin(angle))+newdown_center[1]
        # 逆时针旋转90,ptdown_center为要旋转的点,ptup_center为旋转中心
        pt4[0] = ((newdown_center[0]-newup_center[0])*cos(angle) - (newdown_center[1]-newup_center[1])*sin(angle))+newup_center[0]
        pt4[1] = ((newdown_center[0]-newup_center[0])*sin(angle) + (newdown_center[1]-newup_center[1])*cos(angle))+newup_center[1]
        
        new_pt1 = tuple(int(i) for i in pt1)
        new_pt2 = tuple(int(i) for i in pt2)
        new_pt3 = tuple(int(i) for i in pt3)
        new_pt4 = tuple(int(i) for i in pt4)
        
        text_point = ((new_pt1[0]+new_pt2[0])//2-20, (new_pt1[1]+new_pt2[1])//2+5)
    
        drawRect(img, new_pt1, new_pt2, new_pt3, new_pt4, (0, 255, 0), 1)
        cv2.putText(img, str(point_i+1), text_point, cv2.FONT_HERSHEY_COMPLEX, 0.8, (0, 0, 255), 1)
    cv2.imwrite(newImagePath, img)
    
# 根据四点画原矩形
def drawRect(img, pt1, pt2, pt3, pt4, color, lineWidth):
    cv2.line(img, pt1, pt2, color, lineWidth)
    cv2.line(img, pt2, pt3, color, lineWidth)
    cv2.line(img, pt3, pt4, color, lineWidth)
    cv2.line(img, pt1, pt4, color, lineWidth)

def ReadTxt(directory, imageName, newimage):
    getTxt = open(directory)
    lines = getTxt.readlines()
    
    xy = []
    for x in lines:
        x = x.strip()
        if len(x) == 0:
            continue
        m, n = x.split(",")
        xy.append([eval(m), eval(n)])
        
    imgSrc = cv2.imread(imageName)
    rotate(imgSrc, xy[::-1], newimage)
 
 
if __name__ == "__main__":
    directory = "像素点坐标txt文件路径"
    imageName = "原jpg图像文件路径"
    newimage = "处理后的图像存放路径及名称"
    ReadTxt(directory, imageName, newimage)

代码解释

① 主程序

if __name__ == "__main__":
    directory = "像素点坐标txt文件路径"
    imageName = "原jpg图像文件路径"
    newimage = "处理后的图像存放路径及名称"
    ReadTxt(directory, imageName, newimage)

我们在主程序中定义了三个变量,分别是 directory(像素点坐标txt文件路径)、imageName(jpg图像文件路径)和 newimage(处理后的图像存放路径及名称),然后将这三个变量作为参数传给 ReadTxt() 函数。

② ReadTxt() 函数

def ReadTxt(directory, imageName, newimage):
    getTxt = open(directory)
    lines = getTxt.readlines()
    getTxt.close()
    xy = []
    for x in lines:
        x = x.strip()
        if len(x) == 0:
            continue
        m, n = x.split(",")
        xy.append([eval(m), eval(n)])
        
    imgSrc = cv2.imread(imageName)
    rotate(imgSrc, xy[::-1], newimage)

ReadTxt() 函数首先将 directory 所表示的像素点坐标txt文件读取到列表 xy 中,列表 xy 的形式为[[x1, y1], [x2, y2], [x3, y3], ... , [x12, y12]]。然后用 OpenCV 的 imread() 函数读取原 jpg 图像,得到一个形状为 (480, 480, 3) 的数组,将该数组、列表 xy 的逆序(即[[x12, y12], [x11, y11], [x10, y10], ... , [x1, y1]])和 newimage 变量作为参数传给 rotate() 函数。

为什么要将列表 xy 逆序呢?从效果展示的结果图中,我们可以看出,在矩形框旁边标注的文本信息,从下往上依次是1、2、3、4、5、6,所以我们对列表 xy 进行了逆序,便于从下往上依次画框并标注文本信息。

③ rotate() 函数

def rotate(img, points, newImagePath):
    
    dis_tt = math.sqrt((points[1][0]-points[3][0])**2+(points[1][1]-points[3][1])**2)

    for point_i in range(6):
        if point_i == 0:
            ptup_center = [2*points[0][0]-points[1][0], 2*points[0][1]-points[1][1]]
        else:
            ptup_center = points[2*point_i-1]
            
        if point_i == 5:
            ptdown_center = [2*points[10][0]-points[9][0], 2*points[10][1]-points[9][1]]
        else:
            ptdown_center = points[2*point_i+1]
        
        newup_center = [0, 0]
        newdown_center = [0, 0]
        rect_center = (np.array(ptup_center) + np.array(ptdown_center))/2  #整体框的矩形中心
        
        if ptup_center[0] > rect_center[0]:
            newup_center[0] = 0.5*dis_tt/math.sqrt(1+((ptup_center[1]-rect_center[1])/(ptup_center[0]-rect_center[0]))**2) + rect_center[0]
        else:
            newup_center[0] = -0.5*dis_tt/math.sqrt(1+((ptup_center[1]-rect_center[1])/(ptup_center[0]-rect_center[0]))**2) + rect_center[0]
            
        if ptdown_center[0] > rect_center[0]:
            newdown_center[0] = 0.5*dis_tt/math.sqrt(1+((ptdown_center[1]-rect_center[1])/(ptdown_center[0]-rect_center[0]))**2) + rect_center[0]
        else:
            newdown_center[0] = -0.5*dis_tt/math.sqrt(1+((ptdown_center[1]-rect_center[1])/(ptdown_center[0]-rect_center[0]))**2) + rect_center[0]
        
        newup_center[1] = 0.5*dis_tt/math.sqrt(1+((ptup_center[0]-rect_center[0])/(ptup_center[1]-rect_center[1]))**2) + rect_center[1]
        newdown_center[1] = -0.5*dis_tt/math.sqrt(1+((ptdown_center[0]-rect_center[0])/(ptdown_center[1]-rect_center[1]))**2) + rect_center[1]

        # pt1:左上,pt2:左下,pt3:右下,pt4:右上
        pt1 = [0, 0]
        pt2 = [0, 0]
        pt3 = [0, 0]
        pt4 = [0, 0]
        angle = math.radians(90)
        # 顺时针旋转90,ptdown_center为要旋转的点,ptup_center为旋转中心
        pt1[0] = -0.6*((newdown_center[0]-newup_center[0])*cos(angle) - (newdown_center[1]-newup_center[1])*sin(angle))+newup_center[0]
        pt1[1] = -0.6*((newdown_center[0]-newup_center[0])*sin(angle) + (newdown_center[1]-newup_center[1])*cos(angle))+newup_center[1]
        # 逆时针旋转90,ptup_center为要旋转的点,ptdown_center为旋转中心
        pt2[0] = -0.6*((newup_center[0]-newdown_center[0])*cos(angle) + (newup_center[1]-newdown_center[1])*sin(angle))+newdown_center[0]
        pt2[1] = -0.6*((newup_center[1]-newdown_center[1])*cos(angle) - (newup_center[0]-newdown_center[0])*sin(angle))+newdown_center[1]
        # 顺时针旋转90,ptup_center为要旋转的点,ptdown_center为旋转中心
        pt3[0] = ((newup_center[0]-newdown_center[0])*cos(angle) + (newup_center[1]-newdown_center[1])*sin(angle))+newdown_center[0]
        pt3[1] = ((newup_center[1]-newdown_center[1])*cos(angle) - (newup_center[0]-newdown_center[0])*sin(angle))+newdown_center[1]
        # 逆时针旋转90,ptdown_center为要旋转的点,ptup_center为旋转中心
        pt4[0] = ((newdown_center[0]-newup_center[0])*cos(angle) - (newdown_center[1]-newup_center[1])*sin(angle))+newup_center[0]
        pt4[1] = ((newdown_center[0]-newup_center[0])*sin(angle) + (newdown_center[1]-newup_center[1])*cos(angle))+newup_center[1]
        
        new_pt1 = tuple(int(i) for i in pt1)
        new_pt2 = tuple(int(i) for i in pt2)
        new_pt3 = tuple(int(i) for i in pt3)
        new_pt4 = tuple(int(i) for i in pt4)
        
        text_point = ((new_pt1[0]+new_pt2[0])//2-20, (new_pt1[1]+new_pt2[1])//2+5)
    
        drawRect(img, new_pt1, new_pt2, new_pt3, new_pt4, (0, 255, 0), 1)
        cv2.putText(img, str(point_i+1), text_point, cv2.FONT_HERSHEY_COMPLEX, 0.8, (0, 0, 255), 1)
    cv2.imwrite(newImagePath, img)

我们打算让所有框的高度和宽度都分别对应相等,高度为两块相邻椎体中心之间的距离,而宽度可以根据高度来确定:

  • 对于矩形框的高度,由于人体相邻腰椎中心之间的距离都大致相等,因此我们这里直接取最下方的两块相邻腰椎中心之间的距离作为矩形框的高度,而没有计算所有相邻腰椎中心的距离再取平均;
  • 对于矩形框的宽度,我们以相邻腰椎中心连线的垂线作为方向,以腰椎中心作为分隔点,向右取长度为矩形框高度的距离,向左取长度为矩形框高度×0.6的距离,作为矩形框的总宽度。如下图所示:

opencv倾斜图像校正 opencv斜矩形_计算机视觉_04


接下来我们开始遍历存放坐标点的列表 points,然后对椎间盘逐个框框。由于所有的矩形框等高等宽,但是不同的相邻椎体中心连线方向不相同,因此我们需要在保证矩形框高度相同的情况下,更新后的矩形框上下分隔中心点连线方向与更新前相邻椎体中心连线方向相同。如下图所示,红色点是腰椎椎体上下中心,绿色点是计算后的矩形框上下分隔中心点,蓝色点是矩形框中心,也是椎间盘中心。

opencv倾斜图像校正 opencv斜矩形_计算机视觉_05


由于人体腰椎的高度基本相同,因此我们可以利用腰椎椎体上下中心点取平均来获得椎间盘中心点,而不必根据列表 points 中的坐标来获得椎间盘中心。这样做的好处是,我们得到的椎间盘中心与腰椎上下中心是在同一条直线上,不会存在因 points 中定位坐标不准确而导致的三点不一线的情况。而之所以让椎间盘中心与腰椎上下中心在同一条直线上,是为了方便根据椎间盘中心来确定矩形框上下分隔中心点的坐标。

接下来我们根据腰椎上下中心点来推导矩形框上下中心分隔点:

  • 假设腰椎上中心坐标值存放在列表 ptup_center 中,腰椎下中心坐标值存放在列表 ptdown_center 中,矩形框的高度根据最下方的两块相邻腰椎中心坐标来计算并将结果存放在 dis_tt 中。
  • 根据腰椎上下中心坐标值可计算当前椎间盘中心(也是矩形框中心)坐标值 rect_center。
  • 假设要求的矩形框上下中心坐标值分别存放在变量 newup_center 和 newdown_center 中,那我们可以得到如下等式:


    注意,对于椎体来说,我们的定位坐标中只包含腰椎椎体的相关坐标,而不包含胸椎和尾椎,但是我们框框却需要把胸椎与腰椎、腰椎与尾椎的椎间盘框出来,这时候我们可以反其道行之,根据椎间盘中心坐标与腰椎椎体中心坐标求出对应的胸椎(或尾椎)椎体中心坐标。

根据计算得出来的矩形上下中心分隔点来计算矩形框的四个顶点:左上 pt1,左下 pt2, 右下 pt3,右上 pt4。这里我的计算思想是:

  • 如果我们要求左上顶点,那我们以矩形框上分隔中心点为旋转中心,对矩形框上下中心分隔点连线线段缩成原来的0.6倍并顺时针旋转90度;
  • 如果我们要求左下顶点,那我们以矩形框下分隔中心点为旋转中心,对矩形框上下中心分隔点连线线段缩成原来的0.6倍并逆时针旋转90度;
  • 如果我们要求右下顶点,那我们以矩形框下分隔中心点为旋转中心,对矩形框上下中心分隔点连线线段顺时针旋转90度;
  • 如果我们要求右上顶点,那我们以矩形框上分隔中心点为旋转中心,对矩形框上下中心分隔点连线线段逆时针旋转90度。

因为图像是以像素作为基本单位的,而每一像素的位置坐标值都是整数,因此我们需要把 pt1、pt2、pt3 和 pt4 的值变成整数,得到新的变量 new_pt1、new_pt2、new_pt3 和 new_pt4。

接下来我们确定标注文本信息的位置坐标 text_point。如果我们要在每个框的左侧进行文本注释,那我们可以将文本标注在矩形框左中心左侧20像素、上方5像素处。

接下来,我们调用自定义的 drawRect 函数在图像上画线,再调用 OpenCV 中的 putText 函数在图片指定位置进行文本注释。

当在所有的椎间盘上都进行画框和文本标注后,调用 OpenCV 中的 imwrite 函数将新图片(相比原来增加了画的框和标注的文本)写入新图片存放路径 newImagePath 中。

④ drawRect() 函数

# 根据四点画原矩形
def drawRect(img, pt1, pt2, pt3, pt4, color, lineWidth):
    cv2.line(img, pt1, pt2, color, lineWidth)
    cv2.line(img, pt2, pt3, color, lineWidth)
    cv2.line(img, pt3, pt4, color, lineWidth)
    cv2.line(img, pt1, pt4, color, lineWidth)

该函数传入7个参数:img 代表要进行画线的图片(数组形式),pt1-pt4 代表矩形框的四个顶点,color 代表线的颜色,lineWidth 代表线的宽度。在函数的内部,其实就是调用了四次 OpenCV 里的 line 画线函数,该函数接收5个参数:img 代表要进行画线的图片(数组形式),后面两个 pt 参数代表画线的起点和终点,color 代表线的颜色,lineWidth 代表线的宽度。

框大小不固定的倾斜矩形框

相比于大小固定的框,框大小不固定的框更容易实现,我们既不需要在一开始就确定矩形框的宽度 dis_tt,也不需要计算矩形框的上下中心点,只需要将腰椎中心作为矩形框的上下中心即可。对于矩形框的四个顶点,我们依旧可以利用旋转来获得。