利用OpenCV对图像倾斜矩形目标区域进行固定大小裁剪

文章目录

  • 目的
  • 效果展示
  • 为什么要固定大小裁剪?
  • 代码及解释
    • 原始文件
    • 代码
    • 代码解释
      • ① 主程序
      • ② ReadTxt() 函数
      • ③ rotate() 函数


目的

这篇博客主要介绍如何使用 OpenCV 根据已有的像素点定位坐标文件集在 png 图像集上裁剪大小固定的、倾斜的腰椎间盘。

效果展示

原腰椎间盘 png 图像集文件夹如下所示:
利用OpenCV对图像倾斜矩形目标区域进行固定大小裁剪_第1张图片
文件夹中包括多张 png 图像以及与之同名的 txt 文件,我们要做的就是利用 txt 文件裁剪与之同名的 png 图像,并且按照一定的裁剪顺序重新命名裁剪出的图像,经程序处理后的 png 图像集如下所示:
利用OpenCV对图像倾斜矩形目标区域进行固定大小裁剪_第2张图片

为什么要固定大小裁剪?

我们在将椎间盘区域进行裁剪之后,后续过程通常是利用裁剪的图像进行椎间盘病变程度分类,而椎间盘高度属于椎间盘类别的一个重要的判别特征,如果我们将椎间盘裁剪成不同的大小,在后续将图像送入神经网络时需要缩放成等大小,不同大小的椎间盘图像会导致缩放程度不同,从而导致椎间盘高度这一特征失效。因此,我们需要在裁剪时就将图像裁剪成等高等宽的固定大小。

代码及解释

原始文件

原始文件为多张 png 图像和与之对应的同名像素点坐标 txt 文件。单张 png 图像示例如下所示:
利用OpenCV对图像倾斜矩形目标区域进行固定大小裁剪_第3张图片单个像素点坐标 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对图像倾斜矩形目标区域进行固定大小裁剪_第4张图片
txt 文件中从上到下所表示的11个点,分别与原图像中用白点标注出来的从上到下的11个位置相对应。可以发现11个位置中,从第1个位置开始,每隔一个点便标注了一个椎间盘中心,椎间盘与椎间盘之间的坐标代表椎体中心的位置。其实在真正处理图像的时候,我们只需要知道 txt 文件中的11个点所代表的是什么即可,无需在原图中标注出这些点,因为标注出这些点可能会影响后续的流程处理。

代码

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

import cv2
import numpy as np
from math import *
import math
import tensorflow as tf
 
'''旋转图像并剪裁'''
def rotate(img, i, eleven_xy, newImagePath):
    if i == 0:
        return
    
    point_up = eleven_xy[i-1]
    
    if i == 10:
        point_down = [0, 0]
        point_down[0] = int(2*eleven_xy[i][0] - eleven_xy[i-1][0])
        point_down[1] = int(2*eleven_xy[i][1] - eleven_xy[i-1][1])
    else:
        point_down = eleven_xy[i+1]
    
    # 矩形框的高度和宽度
    heightRect = "高度值"
    widthRect = "宽度值"
    
    # 图片旋转角度
    angle = -math.atan((point_up[0] - point_down[0]) / (point_up[1] - point_down[1] + 1e-6)) * (180 / math.pi)
 
    # 原始图像高度和宽度
    height = img.shape[0]
    width = img.shape[1]
    
    # 按angle角度以椎间盘为中心来旋转图像
    rotateMat = cv2.getRotationMatrix2D(tuple(eleven_xy[i]), angle, 1)  
    
    heightNew = int(width * fabs(sin(radians(angle))) + height * fabs(cos(radians(angle))))
    widthNew = int(height * fabs(sin(radians(angle))) + width * fabs(cos(radians(angle))))
 
    rotateMat[0, 2] += (widthNew - width) / 2
    rotateMat[1, 2] += (heightNew - height) / 2
    imgRotation = cv2.warpAffine(img, rotateMat, (widthNew, heightNew), borderValue=(255, 255, 255))

    # 旋转后图像的四点坐标:左下、右下、右上、左上
    pt1 = [0, 0]
    pt2 = [0, 0]
    pt3 = [0, 0]
    pt4 = [0, 0]
    [[pt4[0]], [pt4[1]]] = np.dot(rotateMat, np.array([[point_up[0]], [point_up[1]], [1]]))
    [[pt1[0]], [pt1[1]]] = np.dot(rotateMat, np.array([[point_down[0]], [point_down[1]], [1]]))
    new_center = [0, 0]
    new_center[0] = int((pt1[0] + pt4[0])/2)
    new_center[1] = int((pt1[1] + pt4[1])/2)
    pt1 = [0, 0]
    pt4 = [0, 0]
    pt1[0], pt1[1] = new_center[0] - widthRect//2, new_center[1] + heightRect//2
    pt4[0], pt4[1] = new_center[0] - widthRect//2, new_center[1] - heightRect//2
    pt2[0], pt2[1] = pt1[0] + widthRect, pt1[1]
    pt3[0], pt3[1] = pt4[0] + widthRect, pt4[1]
 
    imgOut = imgRotation[int(pt4[1]):int(pt1[1]), int(pt1[0]):int(pt2[0])]
    cv2.imwrite(newImagePath, imgOut)  # 裁减得到的旋转矩形框
 
# 读出文件中的坐标值
def ReadTxt(img_dir, txt_dir, img_result_dir):
    getTxt = open(txt_dir, encoding = "utf8")  # 读取txt文件
    lines = getTxt.readlines()
    getTxt.close()
    
    eleven_xy = []
    for xy in lines:
        xy = xy.strip()
        if not xy:
            continue
        point_xy = [eval(i) for i in xy.split(",")]
        eleven_xy.append(point_xy)
    
    imgSrc = cv2.imread(img_dir)
    for i in range(0, 11, 2):
        new_img = img_result_dir + img_dir.split("\\")[-1][:-4] + "-" + str(6-i//2) + ".jpg" # 截取的文件保存在哪
        rotate(imgSrc, i, eleven_xy, new_img)
 
 
if __name__=="__main__":
    
    all_img = tf.data.Dataset.list_files("png图像文件夹路径")

    for img_name in all_img:
        img_name = str(img_name.numpy(), encoding = "utf8") # 图像文件路径
        txt_name = img_name.replace("png", "txt") # 文本文件路径
        img_result_dir = "截取结果存放文件目录"
        ReadTxt(img_name, txt_name, img_result_dir)

代码解释

① 主程序

if __name__=="__main__":
    
    all_img = tf.data.Dataset.list_files("png图像文件夹路径")

    for img_name in all_img:
        img_name = str(img_name.numpy(), encoding = "utf8") # 图像文件路径
        txt_name = img_name.replace("png", "txt") # 文本文件路径
        img_result_dir = "截取结果存放文件目录"
        ReadTxt(img_name, txt_name, img_result_dir)

在主程序中,我们首先利用 tensorflow 库函数列出了存放 png 图像集文件夹下的所有 png 图像的路径+名称,将结果存放在列表 all_img 里,对其中的元素举例说明:tf.Tensor(b'路径名称\\图像名称.png', shape=(), dtype=string)。这样的形式可以避免后期需要再组合路径和图像名称,但是目前的元素类型是字节形式,而不是字符串形式,如果直接用该元素来读图像或者文本文件是无法正确识别路径的,我们需要将元素转换为字符串类型。

遍历列表 all_img 里的每一个元素,并对每一个元素执行以下操作:

  • 将字节形式的路径名称转换为字符串类型并存在 img_name 中;
  • 将 img_name 中的 png 后缀改成 txt 就可以得到对应的定位坐标文件路径名称 txt_name;
  • 设置存放结果图像集的文件夹目录 img_result_dir;
  • 将以上三个变量作为参数传给 ReadTxt() 函数。

② ReadTxt() 函数

def ReadTxt(img_dir, txt_dir, img_result_dir):
    getTxt = open(txt_dir, encoding = "utf8")  # 读取txt文件
    lines = getTxt.readlines()
    getTxt.close()
    
    eleven_xy = []
    for xy in lines:
        xy = xy.strip()
        if not xy:
            continue
        point_xy = [eval(i) for i in xy.split(",")]
        eleven_xy.append(point_xy)
    
    imgSrc = cv2.imread(img_dir)
    for i in range(0, 11, 2):
        new_img = img_result_dir + img_dir.split("\\")[-1][:-4] + "-" + str(6-i//2) + ".jpg" # 截取的文件保存在哪
        rotate(imgSrc, i, eleven_xy, new_img)

ReadTxt() 函数首先将 txt_dir 所表示的像素点坐标 txt 文件读取到列表 eleven_xy 中,列表 eleven_xy 的形式为[[x1, y1], [x2, y2], [x3, y3], ... , [x12, y12]],然后用 OpenCV 的 imread() 函数读取 img_dir 对应的 png 图像得到其数组形式 imgSrc 。接下来逐渐遍历列表 eleven_xy 中由上到下对应的6个椎间盘定位坐标,并以遍历顺序来命名来裁剪得到的椎间盘图像 new_img 。将数组变量 imgSrc 、当前遍历的椎间盘坐标索引 i,列表 eleven_xy 和 new_img 作为参数传给 rotate() 函数。

③ rotate() 函数

def rotate(img, i, eleven_xy, newImagePath):
    if i == 0:
        return
    
    point_up = eleven_xy[i-1]
    
    if i == 10:
        point_down = [0, 0]
        point_down[0] = int(2*eleven_xy[i][0] - eleven_xy[i-1][0])
        point_down[1] = int(2*eleven_xy[i][1] - eleven_xy[i-1][1])
    else:
        point_down = eleven_xy[i+1]
    
    # 矩形框的高度和宽度
    heightRect = "高度值"
    widthRect = "宽度值"
    
    # 图片旋转角度
    angle = -math.atan((point_up[0] - point_down[0]) / (point_up[1] - point_down[1] + 1e-6)) * (180 / math.pi)
 
    # 原始图像高度和宽度
    height = img.shape[0]
    width = img.shape[1]
    
    # 按angle角度以椎间盘为中心来旋转图像
    rotateMat = cv2.getRotationMatrix2D(tuple(eleven_xy[i]), angle, 1)  
    
    heightNew = int(width * fabs(sin(radians(angle))) + height * fabs(cos(radians(angle))))
    widthNew = int(height * fabs(sin(radians(angle))) + width * fabs(cos(radians(angle))))
 
    rotateMat[0, 2] += (widthNew - width) / 2
    rotateMat[1, 2] += (heightNew - height) / 2
    imgRotation = cv2.warpAffine(img, rotateMat, (widthNew, heightNew), borderValue=(255, 255, 255))

    # 旋转后图像的四点坐标:左下、右下、右上、左上
    pt1 = [0, 0]
    pt2 = [0, 0]
    pt3 = [0, 0]
    pt4 = [0, 0]
    [[pt4[0]], [pt4[1]]] = np.dot(rotateMat, np.array([[point_up[0]], [point_up[1]], [1]]))
    [[pt1[0]], [pt1[1]]] = np.dot(rotateMat, np.array([[point_down[0]], [point_down[1]], [1]]))
    new_center = [0, 0]
    new_center[0] = int((pt1[0] + pt4[0])/2)
    new_center[1] = int((pt1[1] + pt4[1])/2)
    pt1 = [0, 0]
    pt4 = [0, 0]
    pt1[0], pt1[1] = new_center[0] - widthRect//2, new_center[1] + heightRect//2
    pt4[0], pt4[1] = new_center[0] - widthRect//2, new_center[1] - heightRect//2
    pt2[0], pt2[1] = pt1[0] + widthRect, pt1[1]
    pt3[0], pt3[1] = pt4[0] + widthRect, pt4[1]
 
    imgOut = imgRotation[int(pt4[1]):int(pt1[1]), int(pt1[0]):int(pt2[0])]
    cv2.imwrite(newImagePath, imgOut)  # 裁减得到的旋转矩形框

通过网上查询,我发现大部分的 OpenCV 教程只能在图像中裁剪方方正正的矩形区域,而不能裁剪斜矩形区域。但是我们的椎间盘目标区域的边框通常并不与图像边框平行。这里我的思路是:我们可以将图像以当前要切割的椎间盘中心为旋转中心,以相邻椎体中心连线与 y 轴的角度的相反值作为旋转角度,进而来旋转图像,旋转后的图像可以将当前椎间盘摆正,就可以利用 OpenCV 的函数进行图像裁剪了。

旋转角度为什么是相邻椎体中心连线与 y 轴的角度的相反值呢?因为相邻椎体中心连线与 y 轴的角度计算的是矩形框要旋转的角度,而我们现在不旋转矩形框而是图像,因此角度应该取计算得到的相反值。

裁剪某一椎间盘的流程大致如下:

  • 根据椎间盘位置索引来得到椎间盘相邻椎体中心坐标。

    针对我的个人项目来说,后续处理并不需要最上方定位到的椎间盘,因此在 rotate() 函数中,如果定位到的是最上方的椎间盘,直接返回,不做任何处理;而如果定位到的是最下方的椎间盘,需要根据均值规则求出下椎体中心坐标。

  • 初始化要裁剪的目标区域的高度 heightRect 和宽度 widthRect ,并根据上下椎体中心坐标计算图片旋转角度 angle,计算原图片的高度 height 和宽度 width。

  • 调用 OpenCV 里的 getRotationMatrix2D() 函数来获得旋转矩阵,该函数传入三个参数,分别是旋转中心,旋转角度,第三个参数 1 表示进行等比列的缩放,最终函数返回一个旋转矩阵 rotateMat。

    图像的旋转矩阵一般为:
    在这里插入图片描述
    但是单纯的这个矩阵是在原点处进行变换的,为了能够在任意位置进行旋转变换,OpenCV 采用了另一种方式:
    利用OpenCV对图像倾斜矩形目标区域进行固定大小裁剪_第5张图片
    以上就是 rotateMat 的样式。

  • 根据旋转角度计算出旋转后的图片的新高度 heightNew 和宽度 widthNew,并利用原始的和新的图像高度和宽度来更新旋转矩阵 rotateMat 的值。

  • 调用 OpenCV 里的 warpAffine() 仿射变函数来旋转图像,该函数传入四个参数,分别是原图像(数组形式)、旋转矩阵 rotateMat、 输出图像的大小 (widthNew, heightNew) 和边界填充值 borderValue,最终函数返回一个裁剪后的图像数组形式。

  • 将原来的椎体上下中心坐标值与旋转矩阵相乘可以得到旋转后的上下椎体中心坐标,根据旋转后的上下椎体中心坐标可以得到旋转后的椎间盘中心坐标 new_center;

  • 根据 new_center 以及初始设定的要裁剪的目标区域的高度 heightRect 和宽度 widthRect 来计算椎间盘矩形框的四个顶点坐标,根据顶点坐标来裁剪原图像数组,并调用 OpenCV 里的 imwrite 函数将裁剪后的结果写入保存在新路径 newImagePath 里。

你可能感兴趣的:(技术博,opencv,计算机视觉,python)