使用滑动窗口的细长目标分割方法

本文介绍了一种针对细长目标的图像分割算法,通过滑动窗口的方式逐步调整阈值来实现精确分割。该方法适用于有反光背景的场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原始素材

在这里插入图片描述

需求描述

从有反光的黑色或者白色背景中分割出红蓝绿色的单色的前景。

问题分析

背景有反光,前景也有反光,而且三维的前景还会在背景下留下影子。目标是圆柱型的,因此在有光的情况下,总会有一个角度会出现反光的情况。对于这种情况,做图像增强是不可能的,前景和背景都横跨了颜色空间的大部分。

方法1:使用全局阈值的方法

目标最明显的特征是颜色,所以使用hsv或者lab颜色空间,根据人为指定的前景区域上的一个点,确定前景区域阈值,在光照条件较好的情况下可以分割。但是这种方法,人为指定前景区域的不同,分割结果不同。

方法2:使用深度神经网络的方法

目标的特征除了颜色,还有细长形状,走向不会突变等特征。深度模型学习到这些特征,可以直接进行图像的分割。该目标识别或者图像分割的情况和目前图像深度神经网络应用的情况有些差别,不能照搬已有模型。

方法3:使用滑动窗口的方法

滑动窗口的思想是在局部(一个窗口)进行前景的分割,然后利用目标细长的特性,沿着目标进行窗口的滑动,根据上次窗口和这次窗口交集中前景的颜色进行阈值的选取。可以应对使用全局阈值不能解决目标前景颜色沿着生长方向渐变的问题。本文主要介绍的是方法3:

实现算法

  1. 选择窗口大小(根据细长目标的直径确定,是正方形)
  2. 设置滑动起始点(由人为交互指定)
  3. 设置滑动次数
  4. 设置每次滑动步长(初步设为窗口边长的1/5)
  5. 根据初次窗口前景确定初始阈值
  6. 朝一个方向滑动窗口一步(滑动方向由目标最小包围矩形长边方向确定)
  7. 取这次窗口和上次窗口交集,由交集确定本次窗口分割阈值。
  8. 重复第6步和第7步,直到到达目标尽头或图像边界。
  9. 6,7,8步重新执行,第六步改为朝另一个方向滑动窗口一步。

关键代码

分割方法封装后调用

这里分割的方法进行了封装:

CableMeasure cm;
cm.setWindowSize(150, 150);  //设置窗口大小
cm.setInitData(src, points[pointnum]); //设置初始数据
cm.setMaxIter(100);   //设置向两边搜索的范围
cm.setdebug(true);  //是否调试
cm.run();//开始分割

超两个不同方向搜索

run函数实现超两个不同方向搜索的逻辑:

void CableMeasure::run()
{
	//朝正方向搜索
	reInit(true);
	while (once())
	{
		if (debug)
		{
			Rect rect = getCurrentWindow();
			Mat rectshow = rawpic.clone();
			rectangle(rectshow, rect, Scalar(0, 0, 255), 4);
			imshow("rectshow", rectshow);

			//Mat rectmin = rawpic(rect);

			waitKey();
		}
	}

	//朝相反方向搜索
	reInit(false);
	while (once())
	{
		if (debug)
		{
			Rect rect = getCurrentWindow();
			Mat rectshow = rawpic.clone();
			rectangle(rectshow, rect, Scalar(0, 0, 255), 4);
			imshow("rectshow", rectshow);
			waitKey();
		}
	}
} 

如何知道超哪个方向滑动并保持

换方向前调用reInit函数,把初始窗口设为交互的点,并设置滑动方向。滑动方向根据两向量夹角的余弦值确定。

void CableMeasure::reInit(bool is)
{
	searchdirection = is;
	iter = 0;
	currentwindow.x = rawpoint.x - windowSize.width / 2;
	currentwindow.y = rawpoint.y - windowSize.height / 2;
	currentwindow.width = windowSize.width;
	currentwindow.height = windowSize.height;
	if (!limitwindowrange(currentwindow))   //限制滑动窗口范围
		cout << "初始点太靠近边界" << endl;
	prewindow = currentwindow;

	offset.xoffset = 0;
	offset.yoffset = 0;
	if (is)
	{
		preoffset.xoffset = 1;
		preoffset.yoffset = 1;
	}
	else
	{
		preoffset.xoffset = -1;
		preoffset.yoffset = -1;
	}
}

单次滑动处理过程

内部调用的once函数代表窗口滑动一次,算法的主要部分在这儿实现

bool CableMeasure::once() //窗口滑动一次
{
	iter++;
	currentwindow.x += offset.xoffset;
	currentwindow.y += offset.yoffset;
	
	if (!limitwindowrange(currentwindow))
	{
		cout << "迭代过程达到边界" << endl;
		return false;
	}
	if (prewindow == currentwindow && iter > 1)   //结束条件1,矩形框不再滑动
	{
		cout << "窗口到达边界,不再继续滑动" << endl;
		return false;
	}
	if (iter > maxiter)
	{
		cout << "滑动次数到达上限" << endl;
		return false;
	}

	//记录最终窗口位置
	if (!searchdirection)
		startwindow = currentwindow;
	else
		stopwindow = currentwindow;

	prewindow = currentwindow;
	//=======================================================//
	//对currentwindow进行分析,得到下次偏移位置
	Mat roi = rawpic(currentwindow);

	//=====================方法1:像素聚类===========================
	Mat dst;
	//Clustering(roi, dst);   //当输入比较复杂时,聚类没有结果
	//imshow("dst", dst);

	//=====================方法2===========================
	Mat lab;
	cvtColor(roi, lab, COLOR_BGR2Lab);

	//Vec3b a = lab.at<Vec3b>(currentwindow.width/2, currentwindow.height / 2);

	//找到在线缆区域中的点
	int L = 0, A = 0, B = 0;
	int istart, istop;
	int jstart, jstop;
	int sum;

	if (iter == 1)   //第一次滑动,阈值使用手动点击的点计算
	{
		istart = -2; istop = 3; jstart = istart; jstop = istop;
		sum = (istop - istart) * (jstop - jstart);
		for (int i = istart; i < istop; i++)
		{
			for (int j = jstart; j < jstop; j++)
			{
				L += lab.at<Vec3b>(currentwindow.width / 2 + i, currentwindow.height / 2 + j)[0];
				A += lab.at<Vec3b>(currentwindow.width / 2 + i, currentwindow.height / 2 + j)[1];
				B += lab.at<Vec3b>(currentwindow.width / 2 + i, currentwindow.height / 2 + j)[2];
			}
		}
		//自适应窗口大小

	}
	else   //使用和上次相交区域中线缆区域确定
	{
		//prewindow
		if (offset.xoffset > 0)
		{
			istart = offset.xoffset;
			istop = windowSize.width;
		}
		else
		{
			istart = 0;
			istop = windowSize.width + offset.xoffset;
		}
		if (offset.yoffset > 0)
		{
			jstart = offset.yoffset;
			jstop = windowSize.height;
		}
		else
		{
			jstart = 0;
			jstop = windowSize.height + offset.yoffset;
		}
		Mat temprect = rawpic.clone();
		
		sum = 0;
		for (int i = istart; i < istop; i++)
		{
			for (int j = jstart; j < jstop; j++)
			{
				//Vec3b v3b = prewindow.at<Vec3b>(i, j)
				if (prewindowmask.at<Vec3b>(i,j)[0] == 255)
				{
					L += prewindowlab.at<Vec3b>(i, j)[0];
					A += prewindowlab.at<Vec3b>(i, j)[1];
					B += prewindowlab.at<Vec3b>(i, j)[2];
					sum++;
				}
			}
		}
	}

	Vec3b a = Vec3b(L / sum, A / sum, B / sum);

	Vec3f low, high;
	float s[3] = { 1,1,1 };
	float range[3] = { 50,15,15 };

	//阈值策略
	for (int i = 0; i < 3; i++)
	{
		low[i] = (a[i] - range[i]) * s[i];// < 0 ? 0 : (a[i] - range[i]) * s[i];
		high[i] = (a[i] + range[i]) * s[i];// > 255 ? 255 : (a[i] + range[i]) * s[i];
	}

	//cout << "low:" << low[0] << "," << low[1] << "," << low[2] << endl;
	//cout << "high:" << high[0] << "," << high[1] << "," << high[2] << endl;

	Mat mask;
	inRange(lab, Scalar(low[0], low[1], low[2]), Scalar(high[0], high[1], high[2]), mask);

	//imshow("mask", mask);
	dst = Mat::zeros(lab.size(), CV_8UC3);
	Vec3b ff = Vec3b(255, 255, 255);
	Vec3b f0 = Vec3b(0, 0, 0);
	int forecount = 0;
	for (int r = 0; r < lab.rows; r++)
	{
		for (int c = 0; c < lab.cols; c++)
		{
			if (mask.at<uchar>(r, c) == 255)    //前景
			{
				dst.at<Vec3b>(r, c) = ff;
				forecount++;
			}
			else   //背景
			{
				dst.at<Vec3b>(r, c) = f0;
			}
		}
	}

	if (forecount > windowSize.width * windowSize.height / 2)
	{
		cout << "超出线缆区域" << endl;
		return false;
	}

	//========================结果处理===========================
	Mat dst2;
	cvtColor(dst, dst2, COLOR_BGR2GRAY);
	threshold(dst2, dst2, 100, 255, THRESH_BINARY); //90以上
	int dilation_size = 1;
	Mat element = getStructuringElement(MORPH_RECT,
		Size(2 * dilation_size + 1, 2 * dilation_size + 1),
		Point(dilation_size, dilation_size));
	//膨胀操作dilate  //腐蚀操作erode
	dilate(dst2, dst2, element);
	erode(dst2, dst2, element);
	
	//imshow("dst2", dst2);
	//保存这次结果,下次计算阈值使用
	prewindowmask = dst.clone();
	prewindowlab = lab.clone();

	//========================找前景轮廓===========================
	vector<vector<cv::Point> > contours_all;
	vector<cv::Vec4i> hierarchy_all;
	cv::findContours(dst2, contours_all, hierarchy_all, RETR_TREE, CHAIN_APPROX_NONE, Point(0, 0));
	
	Mat show(dst.size(), CV_8UC3);
	drawContours(show, contours_all, -1, Scalar(0,0,255),4);
	//imshow("show", show);

	//========================前景轮廓增加到===========================
	int maxindex = -1;
	int max = 0;
	for (int i = 0; i < contours_all.size(); i++)
	{
		if (contours_all[i].size() > max)
		{
			max = contours_all[i].size();
			maxindex = i;
		}
	}

	if (maxindex != -1)
	{
		vector<Point> ps;
		ps = contours_all[maxindex];
		foreground.push_back(ps);
		foregroundpos.push_back(currentwindow);

		string add;
		if (searchdirection)
			add = "0-";
		else
			add = "1-";
		string filename = add + to_string(iter) + ".jpg";
		//imwrite(filename, rawpic(currentwindow));
	}
	else
	{
		cout << "找不到前景,滑动结束" << endl;
		return false;
	}

	//========================计算Correction===========================
	Moments mo = moments(contours_all[maxindex]);
	int correctionx = mo.m10 / mo.m00;
	int correctiony = mo.m01 / mo.m00;

	//Point center = Point(correctionx, correctiony);
	//center.x += currentwindow.x;
	//center.y += currentwindow.y;
	////怎么找到轮廓的中心
	//if (searchdirection)
	//	centerpoints.push_back(center);
	//else
	//	centerpoints.push_front(center);


	correctionx = correctionx - currentwindow.width / 2;
	correctiony = correctiony - currentwindow.height / 2;


	//cout << "correctionx:" << correctionx << ",correctiony" << correctiony << endl;
	//========================计算offset===========================
	//offset赋值,使用最小包围矩形的角度
	RotatedRect rotaterect = minAreaRect(contours_all[maxindex]);
	float angle = 0;
	if (rotaterect.size.width > rotaterect.size.height)
		angle = rotaterect.angle;
	else
		angle = rotaterect.angle + 90;
	//cout << "angle:" << angle << endl;

	//======================怎么找到轮廓的中心======================
	//1.得到线缆垂直方向角度
	float vangle =  angle + 90;
	if (vangle > 90)
		vangle -= 180;
	if (vangle < -90)
		vangle += 180;

	kfLine line(vangle, rotaterect.center);
	float dis = 0, pre_dis = 0;
	vector<Point> crosspoint;
	//距离小于一定值的点
	for (int i = 0; i < contours_all[maxindex].size(); i++)
	{
		pre_dis = dis;
		dis = line.DisFromPoint(contours_all[maxindex][i]);
		if (dis < 1.4)
		{
			crosspoint.push_back(contours_all[maxindex][i]);
			i += 10;
		}	
	}
	
	//确保有2个点
	vector<Point> crosspointtemp;
	crosspointtemp.push_back(crosspoint[0]);
	int maxcrosspointindex = 0;
	float maxdis = 0;
	for (int i = 1; i < crosspoint.size(); i++)
	{
		float dis = getDistance(crosspoint[0], crosspoint[i]);
		if (dis > maxdis)
		{
			maxdis = dis;
			maxcrosspointindex = i;
		}
	}
	crosspointtemp.push_back(crosspoint[maxcrosspointindex]);
	crosspoint = crosspointtemp;
	

	//添加到处理结果
	Point center;
	if (crosspoint.size() == 2)    //如果数量不对,则这次结果不计入最后处理结果
	{
		center.x = (crosspoint[0].x + crosspoint[1].x) / 2 + currentwindow.x;
		center.y = (crosspoint[0].y + crosspoint[1].y) / 2 + currentwindow.y;

		Pointpair pp;
		pp.p1.x = crosspoint[0].x + currentwindow.x;
		pp.p1.y = crosspoint[0].y + currentwindow.y;
		pp.p2.x = crosspoint[1].x + currentwindow.x;
		pp.p2.y = crosspoint[1].y + currentwindow.y;
		pp.dis = sqrt((pp.p2.x - pp.p1.x) * (pp.p2.x - pp.p1.x) + (pp.p2.y - pp.p1.y) * (pp.p2.y - pp.p1.y));

		if (searchdirection)
		{
			centerpoints.push_back(center);
			pointpairs.push_back(pp);
		}
		else
		{
			centerpoints.push_front(center);
			pointpairs.push_front(pp);
		}
	}
	

	//判断offset和preoffset的夹角,判断是否需要换符号,以此解决朝一个方向搜索问题
	offset.yoffset = offsetdis * sin(angle * 3.1416 / 180);// +correctiony;
	offset.xoffset = offsetdis * cos(angle * 3.1416 / 180);// +correctionx;

	if (preoffset.xoffset * offset.xoffset + preoffset.yoffset * offset.yoffset > 0)  //两向量夹角公式的分子
	{
		offset.yoffset += correctiony;
		offset.xoffset += correctionx;
	}
	else
	{
		offset.yoffset = -offset.yoffset + correctiony;
		offset.xoffset = -offset.xoffset + correctionx;
	}

	//保证移动固定的距离
	float d = sqrt(offset.yoffset * offset.yoffset + offset.xoffset * offset.xoffset);
	float k = offsetdis / d;
	offset.yoffset *= k;
	offset.xoffset *= k;

	//cout << "距离:" << sqrt(offset.yoffset* offset.yoffset + offset.xoffset * offset.xoffset) << endl;

	preoffset = offset;
	//=======================================================//
	return true;
}

当成功分割时返回为true,分割识别或到达边界返回为false。

是否越界判断

在滑动过程中需要时刻判断是否达到窗口边界并进行修正

//返回true,没有到达边界
bool CableMeasure::limitwindowrange(Rect& rect)
{
	Rect rawrect = rect;
	if (rect.x < 0)
		rect.x = 0;
	if (rect.y < 0)
		rect.y = 0;
	if (rect.x > rawpic.cols - windowSize.width)
		rect.x = rawpic.cols - windowSize.width;
	if (rect.y > rawpic.rows - windowSize.height)
		rect.y = rawpic.rows - windowSize.height;

	if (rawrect == rect)
		return true;
	else
		return false;
}

实现效果

在这里插入图片描述

<think>嗯,用户的问题是关于如何提升CT图导出的PNG图片中游离气体检测的效果,目前用目标检测做了2000张数据集,但效果不好,有误检。首先,我需要明确游离气体的特点,它在脏器外部,而脏器内部的气体是普通气体,所以分类时可能会有混淆。用户可能已经尝试了基本的目标检测模型,比如YOLO或Faster R-CNN,但效果不佳。 首先,考虑数据的问题。2000张图片可能不够,尤其是对于医学图像这样的复杂场景。数据增强可能已经做了,但医学图像的特殊性可能需要特定的增强方法,比如调整对比度、模拟不同CT窗宽窗位,而不是普通的旋转翻转。另外,数据标注的准确性也很重要,可能存在标注错误或不一致的情况,需要重新检查。 然后是模型方面。可能使用的模型不够先进,或者没有针对医学图像进行优化。可以考虑使用更先进的检测框架,比如Detectron2中的Mask R-CNN,或者尝试最新的Transformer-based模型如DETR,但要注意数据量是否足够训练这些模型。另外,两阶段检测可能比单阶段更准,但速度慢些。类别不平衡的问题也可能存在,如果正负样本不均衡,需要采用Focal Loss或者过采样、欠采样策略。 误检的情况,可能需要调整后处理参数,比如NMS的阈值,或者增加分类器的阈值来减少假阳性。同时,可以结合形态学处理,比如游离气体可能具有特定的形状或位置,用OpenCV进行后处理过滤。 特征工程方面,CT图像的窗宽窗位调整可能有助于突出游离气体,预处理时可能需要固定窗位,比如肺窗或腹窗。多尺度检测也很重要,因为气体区域可能大小不一,需要模型能够检测不同尺度的目标。 领域知识方面,游离气体的解剖位置可能比较固定,比如在膈下、腹膜腔等,可以加入位置先验,用ROI提取特定区域。多任务学习可能有效,比如同时检测器官,再排除器官内部的气体,这样能减少误检。 如果现有数据不足,可以考虑迁移学习,用预训练的模型在医学图像上微调。或者使用生成对抗网络(GAN)生成更多数据,但医学图像生成比较困难,需要谨慎。主动学习可能帮助选择信息量大的样本进行标注,提升效率。 模型解释性工具如Grad-CAM可以帮助分析模型关注区域是否正确,进而调整。另外,集成多个模型或使用不同的backbone进行融合,可能提升效果。 最后,用户可能没有提到的点:医生的反馈很重要,需要与领域专家合作,确认标注和模型错误的原因;硬件条件是否允许使用更大的模型;是否考虑过3D上下文信息,因为CT本来就是三维的,可能相邻切片的信息有帮助,但用户导出的是PNG,可能只用了2D切片,可以考虑3D卷积或者处理相邻切片。 总结下来,可能的解决步骤是:1. 检查和增强数据,包括质量、数量和增强方法;2. 尝试更先进的模型或调整现有模型参数;3. 加入后处理和领域知识;4. 利用迁移学习和多任务学习;5. 分析模型错误,迭代优化。针对</think>CT图像中游离气体检测效果不佳的问题,可以从数据质量、模型优化、后处理策略和领域知识融合四个方向改进。以下是具体方案: --- ### **一、数据层面的优化** 1. **数据质量提升** - **标注复查**:医学标注易受主观影响,建议与放射科医生共同复核标注,尤其关注脏器边缘、膈下等游离气体高发区域。 - **医学特异性增强**: - 使用**窗宽窗位调整**(如腹窗:W=400, L=40)预处理,突出气体与组织的对比度; - 模拟不同CT设备参数(如噪声、伪影); - 添加仿射变换(避免破坏解剖结构)。 2. **数据量扩展** - **主动学习**:用当前模型预测未标注数据,筛选高不确定性样本(如分类置信度0.3-0.7)优先标注。 - **合成数据**:采用基于GAN的生成(如StyleGAN-Med),或通过CT值替换模拟游离气体(需医生指导)。 --- ### **二、模型改进策略** 1. **模型选型与改进** - **两阶段检测器优化**: - 使用Cascade R-CNN(降低误检率)或Mask R-CNN(增加像素级监督)。 - 在ROI Head中添加位置先验约束(如限制检测框在腹膜区域内)。 - **Transformer模型**:尝试Swin Transformer + FPN结构,利用长程依赖捕捉气体分布特征。 2. **多任务学习** - 联合训练器官分割(如肝脏、肠道)任务,通过硬标签排除脏器内部气体: ```python # 伪代码示例:模型输出器官掩膜,后处理中过滤脏器内部检测框 if gas_bbox.inside(organ_mask): suppress_bbox() ``` 3. **损失函数设计** - 对游离气体检测框使用**DIoU Loss**(提升定位精度),分类头采用**Focal Loss**(缓解正负样本不平衡)。 --- ### **三、后处理与领域规则融合** 1. **解剖学约束** - 构建游离气体空间分布概率图(如膈下、腹壁内侧概率高),通过贝叶斯规则重排序检测结果: ``` final_score = detection_score * spatial_probability ``` 2. **形态学过滤** - 对检测区域计算形态特征(如圆形度、面积),过滤不符合游离气体形态的候选框: ```python import cv2 contour = find_contours(roi) area = cv2.contourArea(contour) _, (w, h), _ = cv2.minAreaRect(contour) circularity = 4*pi*area/(w*h) # 排除细长条状区域 ``` 3. **多切片一致性验证** - 利用相邻CT切片(需DICOM原始数据)验证3D连续性: - 若当前切片检测到气体但相邻5mm内无支持,则降低置信度。 --- ### **四、实验设计建议** 1. **消融实验设计** - 对比不同窗宽窗位(肺窗 vs 腹窗)对mAP的影响。 - 测试Cascade R-CNN与RetinaNet在FP(假阳性)指标上的差异。 2. **错误分析工具** - 使用Grad-CAM可视化误检样本,观察模型关注区域: - 若聚焦于脏器纹理而非气体边缘,需增加边缘强化预处理。 --- ### **五、扩展方案(需资源允许)** 1. **3D检测架构** - 将2D切片堆叠为3D体积,使用3D U-Net或Voxel-RCNN捕捉立体特征。 - 需处理GPU显存问题(可采用滑动窗口法)。 2. **知识蒸馏** - 用大型模型(如nnDetection)作为教师模型,蒸馏到轻量级部署模型。 --- ### **实施步骤推荐** 1. **优先执行**:数据清洗 + 腹窗预处理 + Cascade R-CNN基线 2. **中级优化**:引入器官分割多任务 + 形态学后处理 3. **高阶方案**:3D上下文建模 + 医生反馈迭代 建议每步改进后通过混淆矩阵分析FP/FN类型,针对性调整策略。医学影像项目需紧密联合放射科医生进行结果验证。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

元点机智

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值