【IP101】图像分割技术全解析:从传统算法到深度学习的进阶之路
图像分割详解 ✂️
欢迎来到图像处理的"手术室"!在这里,我们将学习如何像外科医生一样精准地"切割"图像。让我们一起探索这个神奇的图像"手术"世界吧!🏥
目录 📑
- 1. 图像分割简介
- 2. 阈值分割:最基础的"手术刀"
- 3. K均值分割:智能"分类手术"
- 4. 区域生长:组织扩张手术
- 5. 分水岭分割:地形分割手术
- 6. 图割分割:网络切割手术
- 7. 实验效果与应用
- 8. 性能优化与注意事项
1. 图像分割简介 🎯
1.1 什么是图像分割?
图像分割就像是给图像做"手术分区",主要目的是:
- ✂️ 分离不同区域(就像分离不同器官)
- 🎯 识别目标对象(就像定位手术部位)
- 🔍 提取感兴趣区域(就像取出病变组织)
- 📊 分析图像结构(就像进行组织检查)
1.2 为什么需要图像分割?
- 👀 医学图像分析(器官定位、肿瘤检测)
- 🛠️ 工业检测(缺陷检测、零件分割)
- 🌍 遥感图像分析(地物分类、建筑物提取)
- �� 计算机视觉(目标检测、场景理解)
常见的分割方法包括:
- 阈值分割(最基础的"手术刀")
- K均值分割(智能"分类手术")
- 区域生长("组织扩张"手术)
- 分水岭分割("地形分割"手术)
- 图割分割("网络切割"手术)
2. 阈值分割:最基础的"手术刀" 🔪
2.1 基本原理
阈值分割就像是用一把"魔法手术刀",根据像素的"亮度"来决定切还是不切。
数学表达式:
g ( x , y ) = { 1 , f ( x , y ) > T 0 , f ( x , y ) ≤ T g(x,y) = \begin{cases} 1, & f(x,y) > T \\ 0, & f(x,y) \leq T \end{cases} g(x,y)={1,0,f(x,y)>Tf(x,y)≤T
其中:
- f ( x , y ) f(x,y) f(x,y) 是输入图像
- g ( x , y ) g(x,y) g(x,y) 是分割结果
- T T T 是阈值("手术刀"的切割深度)
2.2 常见方法
-
全局阈值:
- 固定阈值(统一的"切割深度")
- Otsu方法(自动找最佳"切割深度")
-
局部阈值:
- 自适应阈值(根据局部区域调整"切割深度")
- 动态阈值(实时调整"手术刀")
2.3 实现步骤
-
预处理:
- 转换为灰度图
- 噪声去除
- 直方图均衡化
-
阈值计算:
- 手动设置
- 自动计算(Otsu等)
-
分割处理:
- 二值化
- 后处理优化
2.4 手动实现
C++实现
class ThresholdSegmentation {
public:static Mat segment(const Mat& src, double threshold, double maxVal = 255) {CV_Assert(!src.empty());// 转换为灰度图Mat gray;if (src.channels() == 3) {cvtColor(src, gray, COLOR_BGR2GRAY);} else {gray = src.clone();}Mat dst(gray.size(), CV_8UC1);// 使用OpenMP加速处理#pragma omp parallel for collapse(2)for (int y = 0; y < gray.rows; y++) {for (int x = 0; x < gray.cols; x++) {dst.at<uchar>(y, x) = gray.at<uchar>(y, x) > threshold ? maxVal : 0;}}return dst;}static Mat otsu(const Mat& src) {// 计算直方图vector<int> histogram(256, 0);Mat gray;if (src.channels() == 3) {cvtColor(src, gray, COLOR_BGR2GRAY);} else {gray = src.clone();}for (int y = 0; y < gray.rows; y++) {for (int x = 0; x < gray.cols; x++) {histogram[gray.at<uchar>(y, x)]++;}}// 计算Otsu阈值double totalPixels = gray.rows * gray.cols;double sumAll = 0;for (int i = 0; i < 256; i++) {sumAll += i * histogram[i];}double sumB = 0;int wB = 0;double maxVariance = 0;int threshold = 0;for (int t = 0; t < 256; t++) {wB += histogram[t];if (wB == 0) continue;int wF = totalPixels - wB;if (wF == 0) break;sumB += t * histogram[t];double mB = sumB / wB;double mF = (sumAll - sumB) / wF;double variance = wB * wF * (mB - mF) * (mB - mF);if (variance > maxVariance) {maxVariance = variance;threshold = t;}}return segment(gray, threshold);}
};
Python实现
class ThresholdSegmentation:@staticmethoddef segment(image, threshold, max_val=255):"""手动实现阈值分割Args:image: 输入图像threshold: 阈值max_val: 最大值Returns:分割后的二值图像"""if len(image.shape) == 3:gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)else:gray = image.copy()result = np.zeros_like(gray)result[gray > threshold] = max_valreturn result@staticmethoddef otsu(image):"""手动实现Otsu阈值分割Args:image: 输入图像Returns:分割后的二值图像"""if len(image.shape) == 3:gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)else:gray = image.copy()# 计算直方图histogram = np.bincount(gray.ravel(), minlength=256)total_pixels = gray.sizesum_all = np.sum(np.arange(256) * histogram)max_variance = 0threshold = 0sum_b = 0w_b = 0for t in range(256):w_b += histogram[t]if w_b == 0:continuew_f = total_pixels - w_bif w_f == 0:breaksum_b += t * histogram[t]m_b = sum_b / w_bm_f = (sum_all - sum_b) / w_fvariance = w_b * w_f * (m_b - m_f) ** 2if variance > max_variance:max_variance = variancethreshold = treturn ThresholdSegmentation.segment(gray, threshold)
3. K均值分割:智能"分类手术" 🎯
3.1 基本原理
K均值分割就像是给图像做"分类手术",将相似的像素"缝合"在一起。
数学表达式:
J = ∑ j = 1 k ∑ i = 1 n j ∥ x i ( j ) − c j ∥ 2 J = \sum_{j=1}^k \sum_{i=1}^{n_j} \|x_i^{(j)} - c_j\|^2 J=j=1∑ki=1∑nj∥xi(j)−cj∥2
其中:
- k k k 是分类数量("手术区域"数量)
- x i ( j ) x_i^{(j)} xi(j) 是第j类中的第i个像素
- c j c_j cj 是第j类的中心("手术区域"中心)
3.2 实现步骤
-
初始化中心:
- 随机选择k个中心(选择"手术点")
- 可以使用优化的初始化方法
-
迭代优化:
- 分配像素到最近中心(划分"手术区域")
- 更新中心位置(调整"手术点")
- 重复直到收敛
3.3 优化方法
-
加速收敛:
- K-means++
- Mini-batch K-means
-
并行计算:
- OpenMP
- GPU加速
3.4 手动实现
C++实现
class KMeansSegmentation {
public:static Mat segment(const Mat& src, int k, int maxIter = 100) {CV_Assert(!src.empty() && src.channels() == 3);// 将图像转换为特征向量Mat data;src.convertTo(data, CV_32F);data = data.reshape(1, src.rows * src.cols);// 随机初始化聚类中心vector<Vec3f> centers(k);RNG rng(getTickCount());for (int i = 0; i < k; i++) {int idx = rng.uniform(0, data.rows);centers[i] = Vec3f(data.at<float>(idx, 0),data.at<float>(idx, 1),data.at<float>(idx, 2));}// K均值迭代vector<int> labels(data.rows);for (int iter = 0; iter < maxIter; iter++) {// 分配标签#pragma omp parallel forfor (int i = 0; i < data.rows; i++) {float minDist = FLT_MAX;int minCenter = 0;Vec3f pixel(data.at<float>(i, 0),data.at<float>(i, 1),data.at<float>(i, 2));for (int j = 0; j < k; j++) {float dist = norm(pixel - centers[j]);if (dist < minDist) {minDist = dist;minCenter = j;}}labels[i] = minCenter;}// 更新聚类中心vector<Vec3f> newCenters(k, Vec3f(0, 0, 0));vector<int> counts(k, 0);#pragma omp parallel forfor (int i = 0; i < data.rows; i++) {int label = labels[i];Vec3f pixel(data.at<float>(i, 0),data.at<float>(i, 1),data.at<float>(i, 2));#pragma omp atomicnewCenters[label][0] += pixel[0];#pragma omp atomicnewCenters[label][1] += pixel[1];#pragma omp atomicnewCenters[label][2] += pixel[2];#pragma omp atomiccounts[label]++;}// 检查收敛bool converged = true;for (int i = 0; i < k; i++) {if (counts[i] > 0) {Vec3f newCenter = newCenters[i] / counts[i];if (norm(newCenter - centers[i]) > 1e-3) {converged = false;centers[i] = newCenter;}}}if (converged) break;}// 生成结果图像Mat result(src.size(), CV_8UC3);#pragma omp parallel forfor (int i = 0; i < data.rows; i++) {int y = i / src.cols;int x = i % src.cols;Vec3f center = centers[labels[i]];result.at<Vec3b>(y, x) = Vec3b(saturate_cast<uchar>(center[0]),saturate_cast<uchar>(center[1]),saturate_cast<uchar>(center[2]));}return result;}
};
Python实现
class KMeansSegmentation:@staticmethoddef segment(image, k=3, max_iters=100):"""手动实现K均值分割Args:image: 输入RGB图像k: 聚类数量max_iters: 最大迭代次数Returns:分割后的图像"""if len(image.shape) != 3:raise ValueError("输入必须是RGB图像")# 将图像转换为特征向量pixels = image.reshape((-1, 3)).astype(np.float32)# 随机初始化聚类中心centers = pixels[np.random.choice(pixels.shape[0], k, replace=False)]for _ in range(max_iters):old_centers = centers.copy()# 计算每个像素到中心的距离distances = np.sqrt(((pixels[:, np.newaxis] - centers) ** 2).sum(axis=2))# 分配标签labels = np.argmin(distances, axis=1)# 更新中心for i in range(k):mask = labels == iif np.any(mask):centers[i] = pixels[mask].mean(axis=0)# 检查收敛if np.allclose(old_centers, centers, rtol=1e-3):break# 重建图像result = centers[labels].reshape(image.shape)return result.astype(np.uint8)
4. 区域生长:组织扩张手术 🔪
4.1 基本原理
区域生长就像是进行"组织扩张"手术,从一个种子点开始,逐步"生长"到相似的区域。
生长准则:
∣ I ( x , y ) − I ( x s , y s ) ∣ ≤ T |I(x,y) - I(x_s,y_s)| \leq T ∣I(x,y)−I(xs,ys)∣≤T
其中:
- I ( x , y ) I(x,y) I(x,y) 是当前像素
- I ( x s , y s ) I(x_s,y_s) I(xs,ys) 是种子点
- T T T 是生长阈值(“相似度阈值”)
4.2 实现技巧
-
种子点选择:
- 手动选择(指定"手术起点")
- 自动选择(智能定位"手术点")
-
生长策略:
- 4邻域生长(上下左右扩张)
- 8邻域生长(全方位扩张)
4.3 优化方法
-
并行处理:
- 多线程区域生长
- GPU加速
-
内存优化:
- 使用位图存储
- 队列优化
4.4 手动实现
C++实现
class RegionGrowingSegmentation {
public:static Mat segment(const Mat& src, const Point& seedPoint, double threshold) {CV_Assert(!src.empty() && src.channels() == 3);// 创建标记图像Mat mask = Mat::zeros(src.size(), CV_8UC1);// 获取种子点颜色Vec3b seedColor = src.at<Vec3b>(seedPoint);// 创建队列存储待处理点queue<Point> points;points.push(seedPoint);mask.at<uchar>(seedPoint) = 255;// 定义8邻域const int dx[] = {-1, -1, -1, 0, 0, 1, 1, 1};const int dy[] = {-1, 0, 1, -1, 1, -1, 0, 1};// 区域生长while (!points.empty()) {Point current = points.front();points.pop();// 检查8邻域for (int i = 0; i < 8; i++) {Point neighbor(current.x + dx[i], current.y + dy[i]);// 检查边界if (neighbor.x >= 0 && neighbor.x < src.cols &&neighbor.y >= 0 && neighbor.y < src.rows &&mask.at<uchar>(neighbor) == 0) {// 计算颜色差异Vec3b neighborColor = src.at<Vec3b>(neighbor);double colorDiff = norm(Vec3d(neighborColor) - Vec3d(seedColor));// 如果颜色相似,加入区域if (colorDiff <= threshold) {points.push(neighbor);mask.at<uchar>(neighbor) = 255;}}}}// 生成结果图像Mat result = src.clone();result.setTo(Scalar(0, 0, 0), mask == 0);return result;}static Mat segmentMultiSeed(const Mat& src, const vector<Point>& seedPoints, double threshold) {CV_Assert(!src.empty() && src.channels() == 3 && !seedPoints.empty());// 创建标记图像Mat mask = Mat::zeros(src.size(), CV_8UC1);// 为每个种子点分配不同的标签Mat labels = Mat::zeros(src.size(), CV_32SC1);int currentLabel = 1;for (const auto& seedPoint : seedPoints) {if (mask.at<uchar>(seedPoint) > 0) continue;// 获取种子点颜色Vec3b seedColor = src.at<Vec3b>(seedPoint);// 创建队列存储待处理点queue<Point> points;points.push(seedPoint);mask.at<uchar>(seedPoint) = 255;labels.at<int>(seedPoint) = currentLabel;// 定义8邻域const int dx[] = {-1, -1, -1, 0, 0, 1, 1, 1};const int dy[] = {-1, 0, 1, -1, 1, -1, 0, 1};// 区域生长while (!points.empty()) {Point current = points.front();points.pop();// 检查8邻域for (int i = 0; i < 8; i++) {Point neighbor(current.x + dx[i], current.y + dy[i]);// 检查边界if (neighbor.x >= 0 && neighbor.x < src.cols &&neighbor.y >= 0 && neighbor.y < src.rows &&mask.at<uchar>(neighbor) == 0) {// 计算颜色差异Vec3b neighborColor = src.at<Vec3b>(neighbor);double colorDiff = norm(Vec3d(neighborColor) - Vec3d(seedColor));// 如果颜色相似,加入区域if (colorDiff <= threshold) {points.push(neighbor);mask.at<uchar>(neighbor) = 255;labels.at<int>(neighbor) = currentLabel;}}}}currentLabel++;}// 生成结果图像Mat result = Mat::zeros(src.size(), CV_8UC3);RNG rng(getTickCount());for (int y = 0; y < src.rows; y++) {for (int x = 0; x < src.cols; x++) {if (mask.at<uchar>(y, x) > 0) {int label = labels.at<int>(y, x);Vec3b color(rng.uniform(0, 255),rng.uniform(0, 255),rng.uniform(0, 255));result.at<Vec3b>(y, x) = color;}}}return result;}
};
Python实现
class RegionGrowingSegmentation:@staticmethoddef segment(image, seed_point=None, threshold=30):"""手动实现区域生长分割Args:image: 输入RGB图像seed_point: 种子点坐标(x,y),如果为None则使用图像中心threshold: 生长阈值Returns:分割后的图像"""if seed_point is None:h, w = image.shape[:2]seed_point = (w//2, h//2)# 创建标记图像mask = np.zeros(image.shape[:2], np.uint8)# 获取种子点的颜色seed_color = image[seed_point[1], seed_point[0]]# 定义8邻域neighbors = [(0,1), (1,0), (0,-1), (-1,0),(1,1), (-1,-1), (-1,1), (1,-1)]# 创建待处理点队列stack = [seed_point]mask[seed_point[1], seed_point[0]] = 255while stack:x, y = stack.pop()for dx, dy in neighbors:nx, ny = x + dx, y + dyif (0 <= nx < image.shape[1] and 0 <= ny < image.shape[0] andmask[ny, nx] == 0):# 计算颜色差异color_diff = np.abs(image[ny, nx] - seed_color)if np.all(color_diff < threshold):mask[ny, nx] = 255stack.append((nx, ny))# 应用掩码result = image.copy()result[mask == 0] = 0return result@staticmethoddef segment_multi_seed(image, seed_points, threshold=30):"""手动实现多种子点区域生长分割Args:image: 输入RGB图像seed_points: 种子点坐标列表[(x1,y1), (x2,y2), ...]threshold: 生长阈值Returns:分割后的图像,不同区域用不同颜色标记"""# 创建标记图像和标签图像mask = np.zeros(image.shape[:2], np.uint8)labels = np.zeros(image.shape[:2], np.int32)# 定义8邻域neighbors = [(0,1), (1,0), (0,-1), (-1,0),(1,1), (-1,-1), (-1,1), (1,-1)]current_label = 1for seed_point in seed_points:if mask[seed_point[1], seed_point[0]] > 0:continue# 获取种子点的颜色seed_color = image[seed_point[1], seed_point[0]]# 创建待处理点队列stack = [seed_point]mask[seed_point[1], seed_point[0]] = 255labels[seed_point[1], seed_point[0]] = current_labelwhile stack:x, y = stack.pop()for dx, dy in neighbors:nx, ny = x + dx, y + dyif (0 <= nx < image.shape[1] and 0 <= ny < image.shape[0] andmask[ny, nx] == 0):# 计算颜色差异color_diff = np.abs(image[ny, nx] - seed_color)if np.all(color_diff < threshold):mask[ny, nx] = 255labels[ny, nx] = current_labelstack.append((nx, ny))current_label += 1# 生成随机颜色colors = np.random.randint(0, 255, (current_label, 3), dtype=np.uint8)colors[0] = [0, 0, 0] # 背景为黑色# 生成结果图像result = colors[labels]return result
5. 分水岭分割:地形分割手术 🔪
5.1 基本原理
分水岭分割就像是在图像的"地形图"上注水,水位上升时形成的"分水岭"就是分割边界。
主要步骤:
-
计算梯度:
∥ ∇ f ∥ = ( ∂ f ∂ x ) 2 + ( ∂ f ∂ y ) 2 \|\nabla f\| = \sqrt{(\frac{\partial f}{\partial x})^2 + (\frac{\partial f}{\partial y})^2} ∥∇f∥=(∂x∂f)2+(∂y∂f)2 -
标记区域:
- 确定前景标记(“山谷”)
- 确定背景标记(“山脊”)
5.2 实现方法
-
传统分水岭:
- 基于形态学重建
- 容易过分割
-
标记控制:
- 使用标记点控制分割
- 避免过分割问题
5.3 优化技巧
-
预处理优化:
- 梯度计算优化
- 标记提取优化
-
后处理优化:
- 区域合并
- 边界平滑
5.4 手动实现
C++实现
class WatershedSegmentation {
public:static Mat segment(const Mat& src) {CV_Assert(!src.empty() && src.channels() == 3);// 转换为灰度图Mat gray;cvtColor(src, cv::COLOR_BGR2GRAY);// 使用Otsu算法进行二值化Mat binary;threshold(gray, binary, 0, 255, THRESH_BINARY_INV + THRESH_OTSU);// 形态学操作去除噪声Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));Mat opening;morphologyEx(binary, opening, MORPH_OPEN, kernel, Point(-1,-1), 2);// 确定背景区域Mat sureBg;dilate(opening, sureBg, kernel, Point(-1,-1), 3);// 确定前景区域Mat distTransform;distanceTransform(opening, distTransform, DIST_L2, 5);Mat sureFg;double maxVal;minMaxLoc(distTransform, nullptr, &maxVal);threshold(distTransform, sureFg, 0.7*maxVal, 255, 0);sureFg.convertTo(sureFg, CV_8U);// 找到未知区域Mat unknown;subtract(sureBg, sureFg, unknown);// 标记Mat markers;connectedComponents(sureFg, markers);markers = markers + 1;markers.setTo(0, unknown == 255);// 应用分水岭算法markers.convertTo(markers, CV_32S);watershed(src, markers);// 生成结果图像Mat result = src.clone();for (int y = 0; y < markers.rows; y++) {for (int x = 0; x < markers.cols; x++) {int marker = markers.at<int>(y, x);if (marker == -1) { // 边界result.at<Vec3b>(y, x) = Vec3b(0, 0, 255); // 红色边界}}}return result;}static Mat segmentWithMarkers(const Mat& src, const Mat& markers) {CV_Assert(!src.empty() && src.channels() == 3 && !markers.empty());// 转换标记为32位整型Mat markers32;markers.convertTo(markers32, CV_32S);// 应用分水岭算法watershed(src, markers32);// 生成随机颜色RNG rng(getTickCount());vector<Vec3b> colors;for (int i = 0; i < 255; i++) {colors.push_back(Vec3b(rng.uniform(0, 255),rng.uniform(0, 255),rng.uniform(0, 255)));}// 生成结果图像Mat result = src.clone();for (int y = 0; y < markers32.rows; y++) {for (int x = 0; x < markers32.cols; x++) {int marker = markers32.at<int>(y, x);if (marker == -1) { // 边界result.at<Vec3b>(y, x) = Vec3b(0, 0, 255);} else if (marker > 0) { // 标记区域result.at<Vec3b>(y, x) = colors[marker % colors.size()];}}}return result;}
};
Python实现
class WatershedSegmentation:@staticmethoddef segment(image):"""手动实现分水岭分割Args:image: 输入RGB图像Returns:分割后的图像,边界用红色标记"""# 转换为灰度图gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)# 使用Otsu算法进行二值化_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)# 形态学操作去除噪声kernel = np.ones((3,3), np.uint8)opening = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations=2)# 确定背景区域sure_bg = cv2.dilate(opening, kernel, iterations=3)# 确定前景区域dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)_, sure_fg = cv2.threshold(dist_transform, 0.7*dist_transform.max(), 255, 0)sure_fg = np.uint8(sure_fg)# 找到未知区域unknown = cv2.subtract(sure_bg, sure_fg)# 标记_, markers = cv2.connectedComponents(sure_fg)markers = markers + 1markers[unknown == 255] = 0# 应用分水岭算法markers = cv2.watershed(image, markers)# 生成结果图像result = image.copy()result[markers == -1] = [0, 0, 255] # 红色标记边界return result@staticmethoddef segment_with_markers(image, markers):"""使用自定义标记的分水岭分割Args:image: 输入RGB图像markers: 标记图像,不同区域用不同整数标记Returns:分割后的图像,不同区域用不同颜色标记,边界用红色标记"""# 确保标记是32位整型markers = markers.astype(np.int32)# 应用分水岭算法markers = cv2.watershed(image, markers)# 生成随机颜色colors = np.random.randint(0, 255, (255, 3), dtype=np.uint8)colors[0] = [0, 0, 0] # 背景为黑色# 生成结果图像result = image.copy()# 标记边界和区域result[markers == -1] = [0, 0, 255] # 红色边界for i in range(1, markers.max() + 1):result[markers == i] = colors[i % len(colors)]return result
6. 图割分割:网络切割手术 🔪
6.1 基本原理
图割分割就像是在图像的"关系网络"中寻找最佳的"切割路径"。
能量函数:
E ( L ) = ∑ p ∈ P D p ( L p ) + ∑ ( p , q ) ∈ N V p , q ( L p , L q ) E(L) = \sum_{p \in P} D_p(L_p) + \sum_{(p,q) \in N} V_{p,q}(L_p,L_q) E(L)=p∈P∑Dp(Lp)+(p,q)∈N∑Vp,q(Lp,Lq)
其中:
- D p ( L p ) D_p(L_p) Dp(Lp) 是数据项(像素与标签的匹配度)
- V p , q ( L p , L q ) V_{p,q}(L_p,L_q) Vp,q(Lp,Lq) 是平滑项(相邻像素的关系)
6.2 优化方法
-
最小割算法:
- 构建图模型
- 寻找最小割
-
GrabCut算法:
- 迭代优化
- 交互式分割
6.3 实现技巧
-
图构建:
- 节点表示
- 边权重计算
-
优化策略:
- 最大流/最小割
- 迭代优化
6.4 手动实现
C++实现
class GraphCutSegmentation {
public:static Mat segment(const Mat& src, const Rect& rect) {CV_Assert(!src.empty() && src.channels() == 3);// 创建掩码Mat mask = Mat::zeros(src.size(), CV_8UC1);mask(rect) = GC_PR_FGD; // 矩形区域内为可能前景// 创建临时数组Mat bgdModel, fgdModel;// 应用GrabCut算法grabCut(src, mask, rect, bgdModel, fgdModel, 5, GC_INIT_WITH_RECT);// 生成结果图像Mat result = src.clone();for (int y = 0; y < src.rows; y++) {for (int x = 0; x < src.cols; x++) {if (mask.at<uchar>(y, x) == GC_BGD ||mask.at<uchar>(y, x) == GC_PR_BGD) {result.at<Vec3b>(y, x) = Vec3b(0, 0, 0);}}}return result;}static Mat segmentWithMask(const Mat& src, Mat& mask, const Rect& rect) {CV_Assert(!src.empty() && src.channels() == 3 && !mask.empty());// 创建临时数组Mat bgdModel, fgdModel;// 应用GrabCut算法grabCut(src, mask, rect, bgdModel, fgdModel, 5, GC_INIT_WITH_MASK);// 生成结果图像Mat result = src.clone();Mat foregroundMask = (mask == GC_FGD) | (mask == GC_PR_FGD);result.setTo(Scalar(0, 0, 0), ~foregroundMask);return result;}static Mat segmentWithGraph(const Mat& src) {CV_Assert(!src.empty() && src.channels() == 3);// 转换为灰度图Mat gray;cvtColor(src, gray, COLOR_BGR2GRAY);// 创建图结构const int vertices = src.rows * src.cols;const int edges = 4 * vertices; // 4-连通性// 分配内存vector<float> capacities(edges);vector<int> fromVertices(edges);vector<int> toVertices(edges);// 构建图int edgeCount = 0;for (int y = 0; y < src.rows; y++) {for (int x = 0; x < src.cols; x++) {int vertex = y * src.cols + x;// 添加边if (x < src.cols - 1) { // 右边fromVertices[edgeCount] = vertex;toVertices[edgeCount] = vertex + 1;capacities[edgeCount] = calculateEdgeWeight(gray, Point(x, y), Point(x+1, y));edgeCount++;}if (y < src.rows - 1) { // 下边fromVertices[edgeCount] = vertex;toVertices[edgeCount] = vertex + src.cols;capacities[edgeCount] = calculateEdgeWeight(gray, Point(x, y), Point(x, y+1));edgeCount++;}}}// 最小割算法(这里使用简化版本)vector<bool> isSource(vertices, false);minCut(vertices, fromVertices, toVertices, capacities, isSource);// 生成结果图像Mat result = src.clone();for (int y = 0; y < src.rows; y++) {for (int x = 0; x < src.cols; x++) {int vertex = y * src.cols + x;if (!isSource[vertex]) {result.at<Vec3b>(y, x) = Vec3b(0, 0, 0);}}}return result;}private:static float calculateEdgeWeight(const Mat& gray, Point p1, Point p2) {float diff = abs(gray.at<uchar>(p1) - gray.at<uchar>(p2));return exp(-diff * diff / (2 * 30 * 30)); // sigma = 30}static void minCut(int vertices, const vector<int>& fromVertices,const vector<int>& toVertices, const vector<float>& capacities,vector<bool>& isSource) {// 这里实现一个简化版本的最小割算法// 实际应用中应该使用更高效的算法,如Push-Relabel或者Boykov-Kolmogorov算法// 初始化源点集合isSource[0] = true; // 假设第一个顶点为源点bool changed;do {changed = false;for (size_t i = 0; i < fromVertices.size(); i++) {int from = fromVertices[i];int to = toVertices[i];float cap = capacities[i];if (isSource[from] && !isSource[to] && cap > 0.5) {isSource[to] = true;changed = true;}}} while (changed);}
};
Python实现
class GraphCutSegmentation:@staticmethoddef segment(image, rect=None):"""手动实现图割分割(使用GrabCut)Args:image: 输入RGB图像rect: 矩形区域(x, y, width, height),如果为None则使用中心区域Returns:分割后的图像"""if rect is None:h, w = image.shape[:2]margin = min(w, h) // 4rect = (margin, margin, w - 2*margin, h - 2*margin)# 创建掩码mask = np.zeros(image.shape[:2], np.uint8)mask[rect[1]:rect[1]+rect[3], rect[0]:rect[0]+rect[2]] = cv2.GC_PR_FGD# 创建临时数组bgd_model = np.zeros((1,65), np.float64)fgd_model = np.zeros((1,65), np.float64)# 应用GrabCut算法cv2.grabCut(image, mask, rect, bgd_model, fgd_model, 5, cv2.GC_INIT_WITH_RECT)# 生成结果图像mask2 = np.where((mask==2)|(mask==0), 0, 1).astype('uint8')result = image * mask2[:,:,np.newaxis]return result@staticmethoddef segment_with_mask(image, mask, rect):"""使用自定义掩码的图割分割Args:image: 输入RGB图像mask: 掩码图像rect: 感兴趣区域Returns:分割后的图像"""# 创建临时数组bgd_model = np.zeros((1,65), np.float64)fgd_model = np.zeros((1,65), np.float64)# 应用GrabCut算法mask = mask.copy()cv2.grabCut(image, mask, rect, bgd_model, fgd_model, 5, cv2.GC_INIT_WITH_MASK)# 生成结果图像mask2 = np.where((mask==2)|(mask==0), 0, 1).astype('uint8')result = image * mask2[:,:,np.newaxis]return result@staticmethoddef segment_with_graph(image):"""使用图论方法的图割分割Args:image: 输入RGB图像Returns:分割后的图像"""# 转换为灰度图gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)h, w = gray.shape# 构建图结构vertices = h * wedges = []capacities = []# 添加边for y in range(h):for x in range(w):vertex = y * w + x# 右边if x < w - 1:weight = GraphCutSegmentation._calculate_edge_weight(gray[y,x], gray[y,x+1])edges.append((vertex, vertex + 1))capacities.append(weight)# 下边if y < h - 1:weight = GraphCutSegmentation._calculate_edge_weight(gray[y,x], gray[y+1,x])edges.append((vertex, vertex + w))capacities.append(weight)# 最小割算法is_source = GraphCutSegmentation._min_cut(vertices, edges, capacities)# 生成结果图像result = image.copy()for y in range(h):for x in range(w):vertex = y * w + xif not is_source[vertex]:result[y,x] = [0, 0, 0]return result@staticmethoddef _calculate_edge_weight(p1, p2):"""计算边的权重Args:p1, p2: 两个像素值Returns:边的权重"""diff = float(abs(int(p1) - int(p2)))return np.exp(-diff * diff / (2 * 30 * 30)) # sigma = 30@staticmethoddef _min_cut(vertices, edges, capacities):"""简化版本的最小割算法Args:vertices: 顶点数量edges: 边列表capacities: 容量列表Returns:布尔数组,表示每个顶点是否属于源点集合"""# 初始化源点集合is_source = np.zeros(vertices, dtype=bool)is_source[0] = True # 假设第一个顶点为源点# 迭代直到收敛while True:changed = Falsefor (from_vertex, to_vertex), capacity in zip(edges, capacities):if is_source[from_vertex] and not is_source[to_vertex] and capacity > 0.5:is_source[to_vertex] = Truechanged = Trueif not changed:breakreturn is_source
7. 实验效果与应用 🎯
7.1 应用场景
-
医学图像:
- 器官分割
- 肿瘤检测
- 血管提取
-
遥感图像:
- 地物分类
- 建筑物提取
- 道路检测
-
工业检测:
- 缺陷检测
- 零件分割
- 尺寸测量
7.2 注意事项
-
分割过程注意点:
- 预处理很重要(术前准备)
- 参数要适当(手术力度)
- 后处理必要(术后护理)
-
算法选择建议:
- 根据图像特点选择
- 考虑实时性要求
- 权衡精度和效率
8. 性能优化与注意事项 🔪
8.1 性能优化技巧
- SIMD加速:
// 使用AVX2加速阈值分割
inline void threshold_simd(const uchar* src, uchar* dst, int width, uchar thresh) {__m256i thresh_vec = _mm256_set1_epi8(thresh);for (int x = 0; x < width; x += 32) {__m256i pixels = _mm256_loadu_si256((__m256i*)(src + x));__m256i mask = _mm256_cmpgt_epi8(pixels, thresh_vec);_mm256_storeu_si256((__m256i*)(dst + x), mask);}
}
- OpenMP并行化:
#pragma omp parallel for collapse(2)
for (int y = 0; y < height; y++) {for (int x = 0; x < width; x++) {// 分割处理}
}
- 内存优化:
// 使用内存对齐
alignas(32) uchar buffer[256];
8.2 注意事项
-
分割过程注意点:
- 预处理很重要(术前准备)
- 参数要适当(手术力度)
- 后处理必要(术后护理)
-
算法选择建议:
- 根据图像特点选择
- 考虑实时性要求
- 权衡精度和效率
总结 🎯
图像分割就像是给图像做"手术"!通过阈值分割、K均值分割、区域生长、分水岭分割和图割分割等"手术方法",我们可以精确地分离图像中的不同区域。在实际应用中,需要根据具体情况选择合适的"手术方案",就像医生为每个病人制定专属的手术计划一样。
记住:好的图像分割就像是一个经验丰富的"外科医生",既要精确分割,又要保持区域的完整性!🏥
参考资料 📚
- Otsu N. A threshold selection method from gray-level histograms[J]. IEEE Trans. SMC, 1979
- Meyer F. Color image segmentation[C]. ICIP, 1992
- Boykov Y, et al. Fast approximate energy minimization via graph cuts[J]. PAMI, 2001
- Rother C, et al. GrabCut: Interactive foreground extraction using iterated graph cuts[J]. TOG, 2004
- OpenCV官方文档: https://docs.opencv.org/
- 更多资源: IP101项目主页
相关文章:
【IP101】图像分割技术全解析:从传统算法到深度学习的进阶之路
图像分割详解 ✂️ 欢迎来到图像处理的"手术室"!在这里,我们将学习如何像外科医生一样精准地"切割"图像。让我们一起探索这个神奇的图像"手术"世界吧!🏥 目录 📑 1. 图像分割简介2. 阈…...
华为设备链路聚合实验:网络工程实战指南
链路聚合就像为网络搭建 “并行高速路”,既能扩容带宽,又能保障链路冗余,超实用! 一、实验拓扑速览 图中两台交换机 LSW1 和 LSW2,PC1、PC2 归属 VLAN 10,PC3 归属 VLAN 30。LSW1 与 LSW2 通过 GE0/0/1、…...
C24-数组
数组的引入:方便对同一类型的数据进行管理(一个班级里的45个同学、一个篮子里的12个苹果)数组的定义: 数据类型 数组名[常量表达式(也就是元素的个数)];int a[10]; //这里定义了一个能存放10个元素的整形数组数组初始化 完全初始化 int arr[3]{5,6,8};部分初始化 int arr[10]{…...
Vue 项目中长按保存图片功能实现指南
在移动互联网应用中,用户常常有保存页面特定内容为图片的需求,比如保存二维码、海报等。在 Vue 项目开发中,如何实现长按保存图片的功能?本文将结合具体代码,详细讲解在 Vue 项目中通过长按操作保存图片的技术实现与应…...
行业先锋:六款产品的实战表现
行业先锋:六款产品的实战表现 北京先智先行科技有限公司,销售着“先知大模型”、“先行AI商学院”“先知AIGC超级工场”这三个旗舰产品,在行业内崭露头角。旗下的先知A1、先知大模型等更是备受关注。 先知大模型,作为核心产…...
RS485与Profibus网关自由口数据互换技巧
RS485与Profibus网关自由口数据互换技巧 兴达易控RS485转Profibus网关在自由口模式下的数据互换,是工业自动化领域内一项关键的技术应用,它实现了不同通信协议设备之间的有效连接与数据交换。在现代工业生产中,众多设备和系统往往采用不同的…...
java复杂度,包装类,泛型解析
如何衡量代码的好坏? 评价代码的好坏我们使用算法效率来判断,而算法效率分两种: 算法效率: 第一种是时间效率,第二种是空间效率,时间效率被称为时间复杂度,⽽空间效率被称作空间复杂度。 时间…...
K8S安装部署(v1.27.6)
一、机器配置 系统版本主机名IP基于服务centos 7.9k8s-master192.168.163.104dockerk8s-node1192.168.163.105k8s-node2192.168.163.106注:以上3台机器都是2C4G的虚拟机,仅供测试使用 docker部署可以参考:docker 部署 二、其他环境配置 #1、关闭防火墙 systemctl stop fir…...
Debezium BinaryLogClient详解
Debezium BinaryLogClient详解 1. 类的作用与功能 1.1 核心作用 BinaryLogClient是Debezium中负责与MySQL服务器建立和维护Binlog连接的核心类,主要功能包括: 连接管理:建立和维护与MySQL服务器的Binlog连接事件监听:接收和处理Binlog事件心跳保活:维护连接活跃状态断线…...
Leetcode 刷题记录 09 —— 链表第三弹
本系列为笔者的 Leetcode 刷题记录,顺序为 Hot 100 题官方顺序,根据标签命名,记录笔者总结的做题思路,附部分代码解释和疑问解答,01~07为C语言,08及以后为Java语言。 01 合并 K 个升序链表 /*** Definitio…...
加速项目落地(Trae编辑器)
目录 vscode安装python支持 vscode常用插件 Trae编辑器 两个界面合成 补充(QT开发的繁琐) AI编程哪家强?Cursor、Trae深度对比,超详细! - 知乎 Trae兼容vscode的插件,我们可以先在vscode里面装好再一…...
vue3的页面跳转方法汇总(路由跳转,组件跳转)
1.组件跳转 使用router-link组件进行组件导航(无需引入该组件,可直接使用),to后面跟组件路由。如果想要在当前页显示跳转的组件,可以通过<router-view>来显示当前路由匹配到的组件,也可以不使用<r…...
C++回顾 Day5
自实现string完整版 my_string.h using namespace std; class mystr{public://mystr();mystr(const char * new_str nullptr);mystr(const mystr & another);char * c_str();~mystr();mystr& operator(const mystr & another);mystr operator(const mystr &…...
OpenMVS 的编译与运行
Title: OpenMVS 的编译与运行 文章目录 I. 编译与准备1. 获得源码2. wiki3. 退出 Conda 环境4. 编译5. 数据准备 II. 命令了解1. 稠密重建 DensifyPointCloud2. 曲面重建 ReconstructMesh3. 网格优化 RefineMesh4. 纹理贴图 TextureMesh III. 命令运行1. 运行稠密重建2. 运行网…...
@Transactional注解的使用
目录 一.介绍 1.使用Transactional注解的位置 2.Transactional注解的作用 二.举例 1.需求场景 2.做法 3.效果展示 三.简单总结 一.介绍 1.使用Transactional注解的位置 我们在Java开发中,一般在service层的方法上,使用Transactional注解&#x…...
路由器NAT回流踩坑
路由器 H3C GR-3000AX-U 不支持NAT回流 核心问题定位 外网访问 ✅ 非Docker服务(直接运行在宿主机上的服务)可以访问❌ Docker服务 无法访问 内网访问 ✅ 内网IP访问(无论Docker还是非Docker)正常❌ 内网通过公网IP访问 全部失败…...
如何创建RDD
创建RDD(Resilient Distributed Dataset)主要有以下三种方法: 1. 从集合创建RDD 通过将本地集合(如列表、数组)传递给SparkContext的parallelize方法,可以将本地数据转换为RDD。这种方式通常用于测试或开…...
PTS-G5K13M RF Generator 5kW / 13MHz 射频电源User s Manual
PTS-G5K13M RF Generator 5kW / 13MHz 射频电源User s Manual...
vue3父组件调用子组件方法
需求:在vue3中需要在父组件调用子组件的方法 思路:通过ref和defineExpose直接暴露给父组件 1.子组件暴露表单验证方法 <template><a-form ref"formRef" :model"formState" :rules"rules"><a-form-item …...
Python小酷库系列:5个常用的dict属性化访问扩展库
5个常用的dict属性化访问扩展库 嵌套结构高级功能性能综合建议 在前面我们详细讲解了 Box和 Munch这两个dict属性化访问的扩展库,总体而言它们主要用于提升配置文件数据、JSON对象数据的可读性,减少了代码中双引号。在这一领域中还有dotmap、addict 和…...
day009-用户管理专题
文章目录 1. 创建包含时间的文件2. 与用户相关的文件3. 用户分类4. 与用户相关的命令4.1 添加用户4.2 删除用户4.3 查看用户4.4 修改用户密码 5. sudo6. 思维导图7. 老男孩思想-学习方法 1. 创建包含时间的文件 或$()是替换符号,可以将命令的结果作为字符串或变量的…...
微信小程序pinia的应用
情景:院校列表的关注状态的实时更新 新建一个ts文件存储关注状态,用于集中管理用户“已关注院校”的相关状态和操作 import {definStore} from pinia; import type { College_records } from /types/university;export const useFocusCollegeStore de…...
LWIP的超时事件笔记
那个马蜂佬,刚发就给我两个赞 lwIP超时事件处理简介 为每个与外界网络连接的任务都设定了timeout属性,即等待超时时间,例如TCP建立连接超时、ARP缓存表项的时间管理等,都需要超时操作来处理 lwIP超时事件机制 一共有四种 2.1&a…...
如何避免项目结束后知识流失
避免项目结束后知识流失的方法包括:建立项目知识库、实施定期知识回顾与总结、强化团队内部知识共享机制、利用合适的知识管理工具。项目知识库的建设尤其关键,它可帮助团队保留核心经验和方法,确保知识沉淀在组织内部。通过知识库࿰…...
【MCP】客户端配置(ollama安装、qwen2.5:0.5b模型安装、cherry-studio安装配置)
【MCP】客户端配置(ollama安装、qwen2.5:0.5b模型安装、cherry-studio安装配置) 客户端配置(1)下载安装ollama(2)安装qwen2.5:0.5b模型(3)安装配置cherry-studio 客户端配置 &#…...
Media3 中 Window 的时间相关属性详解
AndroidX Media3 的 Timeline.Window 类中,与时间相关的属性描述了媒体播放窗口(window)在时间维度上的关键信息。这些属性帮助开发者理解媒体的播放范围、起始点、持续时间以及与设备时间或直播流的同步关系。 Timeline.Window 的时间相关属…...
C 语言编码规范
在 C 语言开发过程中,遵循编码规范不仅能提高代码的可读性、可维护性,还能减少潜在的错误,提升团队协作效率。以下从多个维度详细阐述 C 语言编码过程中需要注意的规范要点。 一、命名规范 变量命名 变量命名应做到见名知意,采用…...
嵌入式开发学习日志Day15
一、指针指向字符型数组 (1)【const】:在指针变量中使用时,无法通过该指针修改被指向的变量; (2)【const】:关键字,在C和C中,能加就加,加了一定…...
从人脸扫描到实时驱动,超写实数字分身技术解析
在元宇宙浪潮中,数字人、虚拟数字人等新兴概念逐渐走进大众视野,其中数字分身作为虚拟数字人的细分领域,正引发广泛关注。数字分身依托人工智能与虚拟现实技术,能基于真人信息进行1:1复刻,具备与真人高度相似的外貌、声…...
Vue3 自定义指令的原理,以及应用
文章目录 前言一、原理说明二、注册与使用1. 全局注册2. 局部注册3. 使用方式 三、典型应用场景四、案例:权限控制指令五、注意事项 v-draggable✅ 目标效果:🧩 1. 自定义指令定义🧱 2. 在项目中注册🧪 3. 使用示例&am…...
306.检查是否所有A都在B之前
2124. 检查是否所有 A 都在 B 之前 - 力扣(LeetCode) class Solution {public boolean checkString(String s) {return !s.contains("ba");} } class Solution(object):def checkString(self, s):return s.find("ba")-1...
适合java程序员的Kafka消息中间件实战
创作的初心: 我们在学习kafka时,都是基于大数据的开发而进行的讲解,这篇文章为java程序员为核心,助力大家掌握kafka实现。 什么是kafka: 历史: 诞生与开源(2010 - 2011 年) 2010 年…...
当体育数据API遇上WebSocket:一场技术互补的「攻防战」
在世界杯决赛的最后一分钟,你正通过手机观看直播。突然,解说员大喊“球进了!”,但你的屏幕却卡在对方半场的回放画面——这种「延迟乌龙」的尴尬,正是实时体育应用面临的终极挑战。 在体育数字化浪潮下,用…...
1:点云处理—三种显示方法(自建点云)
1.彩色显示 *读取三维点云 dev_get_window(WindowHandle)dev_open_window(0, 0, 512, 512, black, WindowHandle1) read_object_model_3d(./19-12-26/t.ply, m, [], [], ObjectModel3D, Status)Instructions[0] : Rotate: Left button Instructions[1] : Zoom: Shift left…...
SCADA|KingSCADA运行报错:加载实时库服务失败
哈喽,你好啊,我是雷工! 最近在绵阳出差,在现场调试时遇到报错问题,翻了下以往记录没有该错误的相关笔记。 于是将问题过程及处理办法记录下来。 01 问题描述 昨天还好好的,可以正常运行的程序今天再次运行时报错: “加载 实时库服务 失败” 查看日志中错误信息如下: …...
k8s | Kubernetes 服务暴露:NodePort、Ingress 与 YAML 配置详解
CodingTechWork 引言 在 Kubernetes 集群中,服务暴露是将集群内部的服务对外部网络提供访问的关键环节。NodePort 和 Ingress 是两种常用的服务暴露方式,它们各有特点和适用场景。本文将详细介绍这两种方式的原理、配置方法以及如何通过 YAML 文件实现服…...
upload-labs靶场通关详解:第一关
一、一句话木马准备 新建一个文本文档,写入php代码,修改文件后缀名为php,保存。 phpinfo() 是 PHP 里的一个内置函数,其功能是输出关于当前 PHP 环境的详细信息。这些信息涵盖 PHP 版本、服务器配置、编译选项、PHP 扩展、环境变…...
SSA-CNN+NSGAII+熵权TOPSIS,附相关气泡图!
目录 效果一览基本介绍程序设计参考资料 效果一览 基本介绍 经典麻雀搜索算法深度学习多目标优化多属性决策!SSA-CNNNSGAII熵权TOPSIS,附相关气泡图!本文旨在通过优化卷积神经网络(CNN)以及采用NSGAII多目标优化与熵权…...
数据结构之栈与队列
一,栈和队列的区别 1、核心定义与特性 特性栈(Stack)队列(Queue)定义仅允许在栈顶(表尾)进行插入和删除的线性表,遵循 后进先出(LIFO)。允许在队尾插入、队…...
SSHv2 密钥交换(Key Exchange)详解
1. 算法协商 在密钥交换开始前,客户端和服务端会协商确定本次会话使用的算法组合。具体过程如下: 交换算法列表 客户端和服务端各自发送支持的算法列表,包括: 密钥交换算法(如 diffie-hellman-group14-sha256…...
从零开始学习three.js(15):一文详解three.js中的纹理映射UV
1. UV 映射基础概念 1.1 什么是 UV 坐标? 在三维计算机图形学中,UV 坐标是将二维纹理映射到三维模型表面的坐标系统。UV 中的 U 和 V 分别代表2D纹理空间的水平(X)和垂直(Y)坐标轴,与三维空间…...
解锁 Postgres 扩展日!与瀚高共探 C/Java 跨语言扩展技术的边界与未来
2025 年 5 月 13 日至 16 日(蒙特利尔时间),一年一度的 PostgreSQL 开发者大会 PGConf.dev(原 PGCON 会议)将在加拿大蒙特利尔盛大举行。同去年一样,在本次大会开幕的前一天同样会举办另外一个专场活动——…...
【Hive入门】Hive增量数据导入:基于Sqoop的关系型数据库同步方案深度解析
目录 引言 1 增量数据导入概述 1.1 增量同步与全量同步对比 1.2 增量同步技术选型矩阵 2 Sqoop增量导入原理剖析 2.1 Sqoop架构设计 2.2 增量同步核心机制 3 Sqoop增量模式详解 3.1 append模式(基于自增ID) 3.2 lastmodified模式(基…...
✍️【TS类型体操进阶】挑战类型极限,成为类型魔法师!♂️✨
哈喽类型战士们!今天我们要玩转TS类型体操,让你的类型系统像体操运动员一样灵活优雅~ 学会这些绝招,保准你的代码类型稳如老狗!(文末附类型体操段位表)🚀 一、什么是类型体操? &…...
部署Prometheus+Grafana简介、监控及设置告警(一)
部署PrometheusGrafana简介、监控及设置告警 一. 环境准备 服务器类型IP地址组件 Prometheus服务器、agent服务器、Grafana服务器192.168.213.7Prometheus 、node_expprter,Grafanaagent服务器192.168.213.8node_exporter 如果有防火请记得开启9090&am…...
k8s部署OpenELB
k8s部署OpenELB k8s部署OpenELB配置示例: layer2模式 k8s部署OpenELB 部署OpenELB至K8s集群 # k8s部署OpenELB kubectl apply -f https://raw.githubusercontent.com/openelb/openelb/refs/heads/master/deploy/openelb.yaml# 查看openelb的pod状态 kubectl get pods -n open…...
python打卡day18
聚类后的分析:推断簇的类型 知识点回顾: 推断簇含义的2个思路:先选特征和后选特征通过可视化图形借助ai定义簇的含义科研逻辑闭环:通过精度判断特征工程价值 作业:参考示例代码对心脏病数据集采取类似操作,并且评估特征…...
新品发布 | 96MHz主频 M0+内核低功耗单片机CW32L011产品介绍
CW32L011是基于 eflash 的单芯片低功耗微控制器,集成了主频高达 96MHz的 ARMCortex-M0内核、高速嵌入式存储器(多至 64K字节 FLASH 和多至 6K 字节 SRAM)以及一系列全面的增强型外设和 I/O 口。 所有型号都提供全套的通信接口(3路 UART、1路 SPI和1路12C)、12位高速…...
【面试 · 二】JS个别重点整理
目录 数组方法 字符串方法 遍历 es6 构造函数及原型 原型链 this指向 修改 vue事件循环Event Loop FormData 数组方法 改变原数组:push、pop、shift、unshift、sort、splice、reverse不改变原属组:concat、join、map、forEach、filter、slice …...
【详细教程】ROC曲线的计算方式与绘制方法详细介绍
《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发】2.【车牌识别与自动收费管理系统开发】3.【手势识别系统开发】4.【人脸面部活体检测系统开发】5.【图片风格快速迁移软件开发】6.【人脸表表情识别系统】7.【…...