全自动的超声图像动脉血管弹性分析首先应该将动脉血管检测出来,检测出来之后,我们才能对其进行定位,并分析其弹性。在这里我们提供了一些手臂动脉血管的超声图像以及视频,经过图像增强后并打上了标签,检测模型采用的时YOLOv7-tiny,经过训练之后可以获得一个比较精准的动脉血管实时检测模型。
图像数据位于项目datasets文件夹中,至于YOLOv7-tiny如何进行训练,网上大量的教程,再次不做赘述。
检测效果如图苏所示。
首先我们来说清楚什么是血管弹性分析。血管弹性就是血管收缩后能否再次扩大的能力,一般老年人的血管弹性较差,在病人接受检查时,超声探测头放在病人手臂上,看到的是病人的手臂横切面,所以动脉是呈现出一个类似于圆形的状态,也就是上面图片中检测到的东西。动脉在不停的收缩,所以这个圆的半径是不停的放大缩小放大缩小的。如何判断其弹性呢?一般在接受检查时,首先计算其动脉的半径变化,接着会在病人的胳膊上绑上一个橡胶带,让血管收缩,这时这个圆会逐渐缩小,经过一分钟,然后将橡胶带松开,这时血管会猛然扩大,如果该患者动脉血管弹性好,扩大后的半径要比绑橡胶带之前要大一些,如果患者血管弹性较差,则和绑橡胶带之前的半径相似,也就是说,我们通过对比帮橡胶带前后血管的大小变化来判断其血管弹性,从而分析疾病。
我们检测到动脉血管后,要实时分析其半径,这是一个持续的过程,我们这时采用了KCF跟踪算法来跟踪此动脉血管,以便在此区域中进行分析。其中我们做了一些优化,来提高跟踪的鲁棒性,代码如下。
if len(det):#是否有目标
# Rescale boxes from img_size to im0 size
det[:, :4] = scale_coords(img.shape[2:], det[:, :4], im0.shape).round()
# 置信度排序
det = det[det[:, 4].argsort(descending=True)]
# 最大置信度目标
max_conf_det = det[0]
#for *xyxy, conf, cls in reversed(max_conf_det):#循环这张图片里的所有目标
conf = max_conf_det[4]
xyxy = max_conf_det[:4]
cls = max_conf_det[5]
if conf > 0.5:
if not tracking_started:
# 停止检测,开始跟踪
tracking_started = True
# 初始化跟踪器
tracker = cv2.TrackerKCF_create()
bbox = (int(xyxy[0]), int(xyxy[1]), int(xyxy[2] - xyxy[0]), int(xyxy[3] - xyxy[1]))
#在bbox的上下左右均扩大1.1倍
bbox = (
int(bbox[0] - bbox[2] * 0.05),
int(bbox[1] - bbox[3] * 0.05),
int(bbox[2] * 1.1),
int(bbox[3] * 1.1)
)
tracker.init(im0, bbox)
当跟踪到动脉血管后,我们对该目标区域进行分析,目标区域如下图所示。
弹性分析,在超声图像中其实就是看这块黑色圆圈区域像素点的多少,具体的通过阈值分割方法对目标区域进行一个分割,再通过连通区域分析以及像素点的统计,来进行判断,代码如下:
def segmentation(box_img):
# Convert to grayscale
gray_img = cv2.cvtColor(box_img, cv2.COLOR_BGR2GRAY)
# 大津阈值
# _, thresh_img = cv2.threshold(gray_img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
# 自定义阈值(根据您的需求进行调整)
custom_threshold = 80
# 使用自定义阈值进行二值化
_, thresh_img = cv2.threshold(gray_img, custom_threshold, 255, cv2.THRESH_BINARY)
# 反转二值图像的颜色
thresh_img = cv2.bitwise_not(thresh_img)
# 连通组件标记
_, labels, stats, centroids = cv2.connectedComponentsWithStats(thresh_img)
# 找到最大连通区域的标签
largest_label = np.argmax(stats[1:, cv2.CC_STAT_AREA]) + 1
# 创建一个与原始图像大小相同的掩码图像
mask = np.zeros_like(thresh_img)
# 将最大连通区域对应的像素设置为白色(255)
mask[labels == largest_label] = 255
# 删除其他连通区域
thresh_img = cv2.bitwise_and(thresh_img, mask)
# 闭运算来填充小的黑色区域
kernel = np.ones((10, 10), np.uint8)
thresh_img = cv2.morphologyEx(thresh_img, cv2.MORPH_CLOSE, kernel)
# 统计白色像素
pixel = np.count_nonzero(thresh_img == 255) # Count non-zero pixels as vessel area
# Print vessel area
#print(f"面积: {pixel} pixels")
return thresh_img,pixel
可视化效果如下图所示,第一列为绑橡胶带前,第二列为绑橡胶带中,第三列为松橡胶带后。可以看出松绑之后明显要比帮之前要大,说明这个人血管弹性还是比较良好的。
但是这种算法有一个弊端,可以看出由于传统图像算法在进行分割时,往往会将红框中的非动脉区域错误分割,这也是不可避免的,所以我们提出了一种新的弹性分析算法。
该算法的核心思想为通过确定目标圆心,接着通过圆心计算半径。 圆心决定策略:由最大连通区域通过图像矩得到白色区域重心,并通过与检测框求平均从而决定圆心。 半径计算策略:以圆心为中心向四周以平均角度放出若干条射线,当到达背景区域则记录此射线长度。通过Z-score法过滤异常值,将过滤过的半径集合求平均得到半径。 该算法稍微复杂一些,具体代码如下,代码中的注释也解释了该算法具体如何实施的:
def detect_blood_vessel_radius(thresh_img_normal):
# 在二值化图像中找到轮廓
contours, _ = cv2.findContours(thresh_img_normal, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) == 0:
print("未找到血管轮廓。")
return None
# 获取最大轮廓(假设它是血管)
largest_contour = max(contours, key=cv2.contourArea)
largest_contour = largest_contour.reshape(len(largest_contour), 2)
# 绘制血管横截面的轮廓
# cv2.drawContours(image, [largest_contour], -1, (0, 255, 0), 2) # 这里用绿色绘制轮廓
# cv2.imshow("Blood Vessel Contour", image)
# cv2.waitKey(0)
# cv2.destroyAllWindows()
# 找到最大轮廓(血管)的中心点,这段代码是计算最大轮廓(血管)的中心点坐标的方法,基于图像矩(Moments)。图像矩是对图像像素值的统计特征,其中 m00 表示图像的零阶矩(图像的总和),m10 表示图像的一阶矩(关于 x 轴的总和),m01 表示图像的一阶矩(关于 y 轴的总和)。
M = cv2.moments(largest_contour)
center_x = int(M["m10"] / M["m00"])
center_y = int(M["m01"] / M["m00"])
img_center_x = thresh_img_normal.shape[1] // 2
img_center_y = thresh_img_normal.shape[0] // 2
center_x_last = (center_x + img_center_x) // 2
center_y_last = (center_y + img_center_y) // 2
# 从中心发射线段与血管轮廓相交
num_rays = 8 # 可根据需要调整线段数量
angles = np.linspace(0, 2 * np.pi, num_rays, endpoint=False) # 平分角度
boundary_points = []
rs = []
# 外点线段长度
n = int(math.sqrt((thresh_img_normal.shape[0] // 2) ** 2 + (thresh_img_normal.shape[1] // 2) ** 2) + 50)
for angle in angles:
# 1000是为了射线足够长
a = 1000 * np.cos(angle)
ray_x = int(center_x_last + n * np.cos(angle)) # 计算以中心点为起点、长度为 1000 的线段在 x 轴方向的偏移量
ray_y = int(center_y_last + n * np.sin(angle)) # 计算以中心点为起点、长度为 1000 的线段在 y 轴方向的偏移量
# 获取直线上的所有点
line_points = np.linspace((center_x_last, center_y_last), (ray_x, ray_y), num=100).astype(int)
# 遍历直线上的所有点,查询像素值,找到边界点
for point in line_points:
x, y = point[0], point[1]
if x>=thresh_img_normal.shape[1]:
x= thresh_img_normal.shape[1]-1
if y>=thresh_img_normal.shape[0]:
y= thresh_img_normal.shape[0]-1
pixel_value = thresh_img_normal[y, x] # OpenCV的像素访问是 (y, x)
if pixel_value == 0:
boundary_points.append((x, y))
r = int(np.sqrt((center_x_last - x) ** 2 + (center_y_last - y) ** 2))
rs.append(r)
break
if all(i==0 for i in rs):
print("无目标!")
return 0,(center_x_last,center_y_last)
else:
# 过滤掉异常值的半径(可根据需要调整阈值)
# 计算平均值和标准差
mean_radius = np.mean(rs)
std_radius = np.std(rs)
# 设置Z-score的阈值(例如Z-score绝对值超过2则认为是离群值)
z_score_threshold = 2
# 根据Z-score过滤离群值
acceptable_radii = [r for r in rs if abs((r - mean_radius) / std_radius) <= z_score_threshold]
# 计算平均半径
average_radius = int(np.mean(acceptable_radii))
print(average_radius)
# # 在图像中标记边界点
# for point in boundary_points:
# x, y = point
# cv2.circle(thresh_img_normal, (x, y), 3, (0, 255, 0), -1) # 绘制半径为3的绿色实心圆
# cv2.imwrite(r"E:\maibang\yolo\yolov7-main\image\huizhi.jpg",thresh_img_normal)
# cv2.circle(image, center, radius, color, thickness)
# color = (0, 255, 0) # 绘制圆的颜色,格式为 (B, G, R);这里使用绿色
# thickness = 2 # 绘制圆的线条宽度,为负值表示实心圆
# cv2.circle(image, (center_x_last, center_y_last), average_radius, color, thickness)
# cv2.imwrite(r"E:\maibang\yolo\yolov7-main\image\result.jpg", image)
return average_radius,(center_x_last,center_y_last)
该算法可以实时计算血管的半径以及收缩状况,我们并将圆心和半径组成的圆实时可视化显示,效果图如所示:
具体的演示效果可以参考演示视频。
我们在附件中放入了整个项目文件,包括所有的数据(图像数据和视频数据)以及代码。代码结构为YOLOv7的代码结构,环境参考YOLOv7环境,我们在文件中detect.py中进行了修改,运行detect.py文件进行展示。
希望对你有帮助!加油!
若您认为本文内容有益,请不吝赐予赞同并订阅,以便持续接收有价值的信息。衷心感谢您的关注和支持!