基于语义分割实现人脸图像的皱纹检测定位与分割

news2024/9/28 21:18:46

前言

人脸皱纹主要区分有额纹、川字纹、眼下纹、法令纹、嘴角纹,眼角纹等,在美颜相机,智能医美等于应用领域里,需要对人脸皱纹进行检测、定位、分割,测量等。

传统图像算法的皱纹检测

1.传统算法的皱纹检测可参考《人脸图像的皱纹检测与定量评价研究》这篇论文,下面是论文复现的代码。

from PIL import Image
import cv2
import time
import numpy as np
from skimage.filters import frangi, gabor
from skimage import measure, morphology
from Partition import practice14 as pa
#
# class UnionFindSet(object):
#
#     def __init__(self, m):
#         # m, n = len(grid), len(grid[0])
#         self.roots = [i for i in range(m)]
#         self.rank = [0 for i in range(m)]
#         self.count = m
#
#         for i in range(m):
#             self.roots[i] = i
#
#     def find(self, member):
#         tmp = []
#         while member != self.roots[member]:
#             tmp.append(member)
#             member = self.roots[member]
#         for root in tmp:
#             self.roots[root] = member
#         return member
#
#     def union(self, p, q):
#         parentP = self.find(p)
#         parentQ = self.find(q)
#         if parentP != parentQ:
#             if self.rank[parentP] > self.rank[parentQ]:
#                 self.roots[parentQ] = parentP
#             elif self.rank[parentP] < self.rank[parentQ]:
#                 self.roots[parentP] = parentQ
#             else:
#                 self.roots[parentQ] = parentP
#                 self.rank[parentP] -= 1
#             self.count -= 1
#
#
# class Solution(object):
#     def countComponents(self, n, edges):
#         """
#         :type n: int
#         :type edges: List[List[int]]
#         :rtype: int
#         """
#         ufs = UnionFindSet(n)
#         # print ufs.roots
#         for edge in edges:
#             start, end = edge[0], edge[1]
#             ufs.union(start, end)
#
#         return ufs.count
#
#
# # def connected_component(thresh_A):
# #     thresh_A_copy = thresh_A.copy()  # 复制thresh_A到thresh_A_copy
# #     thresh_B = np.zeros(thresh_A.shape).astype(int)  # thresh_B大小与A相同,像素值为0
# #
# #     kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))  # 3×3结构元
# #
# #     count = []  # 为了记录连通分量中的像素个数
# #     xy_count = []
# #     # 循环,直到thresh_A_copy中的像素值全部为0
# #     while thresh_A_copy.any():
# #         Xa_copy, Ya_copy = np.where(thresh_A_copy > 0)  # thresh_A_copy中值为255的像素的坐标
# #         thresh_B[Xa_copy[0]][Ya_copy[0]] = 1  # 选取第一个点,并将thresh_B中对应像素值改为255
# #
# #         # 连通分量算法,先对thresh_B进行膨胀,再和thresh_A执行and操作(取交集)
# #         for i in range(thresh_A.shape[0]):
# #             dilation_B = cv2.dilate(thresh_B, kernel, iterations=1)
# #             print(type(thresh_A), type(dilation_B.astype(int)))
# #             thresh_B = cv2.bitwise_and(np.uint8(thresh_A), dilation_B.astype(int))
# #
# #         # 取thresh_B值为255的像素坐标,并将thresh_A_copy中对应坐标像素值变为0
# #         Xb, Yb = np.where(thresh_B > 0)
# #         thresh_A_copy[Xb, Yb] = 0
# #         xy_count.append(np.c_[Xb, Yb])
# #         # 显示连通分量及其包含像素数量
# #         count.append(len(Xb))
# #         if len(count) == 0:
# #             print("无连通分量")
# #         if len(count) == 1:
# #             print("第1个连通分量为{}".format(count[0]))
# #             print(xy_count[0], "2")
# #             ecc = measure.regionprops(xy_count[0])
# #             print(ecc[0].eccentricity)
# #         if len(count) >= 2:
# #             if (count[-1] - count[-2]) < 2:
# #                 continue
# #             print("第{}个连通分量为{}".format(len(count), count[-1] - count[-2]))
# #             print(xy_count[len(count) - 1][len(xy_count[-2]):len(xy_count[-1])], "2")
# #             ecc = measure.regionprops(xy_count[len(count) - 1][len(xy_count[-2]):len(xy_count[-1])])
# #             print(ecc[0].eccentricity)
# def connected_component(path):
#     img_A = cv2.imread(path)
#     gray_A = cv2.cvtColor(img_A, cv2.COLOR_BGR2GRAY)  # 转换成灰度图
#     ret, thresh_A = cv2.threshold(gray_A, 20, 255, cv2.THRESH_BINARY_INV)  # 灰度图转换成二值图像
#     print(thresh_A, "thresh_A")
#     thresh_A_copy = thresh_A.copy()  # 复制thresh_A到thresh_A_copy
#     thresh_B = np.zeros(thresh_A.shape, np.uint8)  # thresh_B大小与A相同,像素值为0
#
#     kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))  # 3×3结构元
#
#     count = []  # 为了记录连通分量中的像素个数
#     xy_count = []
#     # 循环,直到thresh_A_copy中的像素值全部为0
#     while thresh_A_copy.any():
#         Xa_copy, Ya_copy = np.where(thresh_A_copy > 0)  # thresh_A_copy中值为255的像素的坐标
#         thresh_B[Xa_copy[0]][Ya_copy[0]] = 255  # 选取第一个点,并将thresh_B中对应像素值改为255
#
#         # 连通分量算法,先对thresh_B进行膨胀,再和thresh_A执行and操作(取交集)
#         for i in range(200):
#             dilation_B = cv2.dilate(thresh_B, kernel, iterations=1)
#             thresh_B = cv2.bitwise_and(thresh_A, dilation_B)
#
#         # 取thresh_B值为255的像素坐标,并将thresh_A_copy中对应坐标像素值变为0
#         Xb, Yb = np.where(thresh_B > 0)
#         thresh_A_copy[Xb, Yb] = 0
#         xy_count.append(np.c_[Xb, Yb])
#         # 显示连通分量及其包含像素数量
#         count.append(len(Xb))
#         if len(count) == 0:
#             print("无连通分量")
#         if len(count) == 1:
#             print("第1个连通分量为{}".format(count[0]))
#             # print(np.c_[Xb, Yb], "1")
#             print(xy_count[0], "2")
#
#             ecc = measure.regionprops(xy_count[0])
#             print(ecc[0].eccentricity)
#         if len(count) >= 2:
#             if (count[-1] - count[-2]) < 2:
#                 continue
#             print("第{}个连通分量为{}".format(len(count), count[-1] - count[-2]))
#             # print(np.c_[Xb, Yb], "1")
#             print(xy_count[len(count) - 1][len(xy_count[-2]):len(xy_count[-1])], "2")
#             ecc = measure.regionprops(xy_count[len(count) - 1][len(xy_count[-2]):len(xy_count[-1])])
#             print(ecc[0].eccentricity)


def master_control(image):
    # image = cv2.resize(image, (int(image.shape[1]*0.3), int(image.shape[0]*0.3)), interpolation=cv2.INTER_CUBIC)  # 图片分辨率很大是要变小
    b, g, r = cv2.split(image)  # image

    sk_frangi_img = frangi(g, scale_range=(0, 1), scale_step=0.01, beta1=1.5, beta2=0.01)  # 线宽范围,步长,连接程度(越大连接越多),减少程度(越大减得越多)0.015
    sk_frangi_img = morphology.closing(sk_frangi_img, morphology.disk(1))
    sk_gabor_img_1, sk_gabor_1 = gabor(g, frequency=0.35, theta=0)
    sk_gabor_img_2, sk_gabor_2 = gabor(g, frequency=0.35, theta=45)  # 越小越明显
    sk_gabor_img_3, sk_gabor_3 = gabor(g, frequency=0.35, theta=90)
    sk_gabor_img_4, sk_gabor_4 = gabor(g, frequency=0.35, theta=360)  # 横向皱纹
    sk_gabor_img_1 = morphology.opening(sk_gabor_img_1, morphology.disk(2))
    sk_gabor_img_2 = morphology.opening(sk_gabor_img_2, morphology.disk(1))
    sk_gabor_img_3 = morphology.opening(sk_gabor_img_3, morphology.disk(2))
    sk_gabor_img_4 = morphology.opening(sk_gabor_img_4, morphology.disk(2))
    all_img = cv2.add(0.1 * sk_gabor_img_2, 0.9 * sk_frangi_img)  # + 0.02 * sk_gabor_img_1 + 0.02 * sk_gabor_img_2 + 0.02 * sk_gabor_img_3
    all_img = morphology.closing(all_img, morphology.disk(1))
    _, all_img = cv2.threshold(all_img, 0.3, 1, 0)
    img1 = all_img
    # print(all_img, all_img.shape, type(all_img))
    # contours, image_cont = cv2.findContours(all_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    # all_img = all_img + image_cont
    bool_img = all_img.astype(bool)
    label_image = measure.label(bool_img)
    count = 0

    for region in measure.regionprops(label_image):
        if region.area < 10: #   or region.area > 700
            x = region.coords
            for i in range(len(x)):
                all_img[x[i][0]][x[i][1]] = 0
            continue
        if region.eccentricity > 0.98:
            count += 1
        else:
            x = region.coords
            for i in range(len(x)):
                all_img[x[i][0]][x[i][1]] = 0

    skel, distance = morphology.medial_axis(all_img.astype(int), return_distance=True)
    skels = morphology.closing(skel, morphology.disk(1))
    trans1 = skels  # 细化
    return skels, count  # np.uint16(skels.astype(int))


def face_wrinkle(path):
    # result = pa.curve(path, backage)
    result = cv2.imread(path)
    img, count = master_control(result)
    print(img.astype(float))
    result[img > 0.1] = 255
    cv2.imshow("result", img.astype(float))
    cv2.waitKey(0)


if __name__ == '__main__':
    path = "2885.png"
    # backage = r'shape_predictor_81_face_landmarks.dat'

    face_wrinkle(path)

检测结果:
在这里插入图片描述
2.基于传统的图像算法的皱纹检测,对于一些复杂的使用场景,要做的预处理相对麻烦很多。

基于ENet算法的皱纹检测

1.ENet 网络

1.ENet 是的论文《ENet: A Deep Neural Network Architecture for Real-Time Semantic Segmentation》在2017年发布,ENet算法的目标是语义分割的快速实现,在考虑分割精确度的同时,还要考虑分割的实时性能。语义分割的基本网络结构为编码-解码结构,即通过下采样实现像素级的分类、上采样实现图像目标的定位。要想提高算法的实时性,必须在上采样阶段减少计算量,提高采样速度。
ENet算法选择在初始化模块中设计一个池化操作与一个步长为2的卷积操作并行,并合并结果特征图,在网络早期使用了更小尺寸和更少数量的特征图,大大减少了网络参数,提高了网络的运行速度,ENet算法在下采样过程使用了扩张卷积,可以很好的平衡图像分辨率和图像感受野,实现在不降低特征图分辨率的同时扩大图像目标的感受野。不同于SegNet算法是一个非常对称的结构,编码器和解码器结构大小相同,ENet算法包含了一个大的编码器和一个小的解码器,有助于降低ENet网络的参数量。
2.ENet网络源码可以在git上获取,但最初的使用的学习框架是caffe,为了之后模型的扩展,这里要框架改成pytorch.

2. 数据集标注

1.数据是从网上收集的,大概有100多张图像,有各种人脸皱纹。
在这里插入图片描述
2.因为没有找到标注单独线条的工具,这里使用labelme对数据进行标注。标注结果如下:
在这里插入图片描述
在这里插入图片描述

3.数据集处理

1.生成数据集,运行下面脚本,能生成样本标签和训练集与测试集。

import os
import sys
import glob
import json
import math
import uuid
import random

import numpy as np
import PIL.Image
import PIL.ImageDraw
from tqdm import tqdm
import  cv2 as cv

def shape_to_mask(img_shape, points, shape_type=None,
                  line_width=10, point_size=5):
    mask = np.zeros(img_shape[:2], dtype=np.uint8)
    mask = PIL.Image.fromarray(mask)
    draw = PIL.ImageDraw.Draw(mask)
    xy = [tuple(point) for point in points]
    if shape_type == 'circle':
        assert len(xy) == 2, 'Shape of shape_type=circle must have 2 points'
        (cx, cy), (px, py) = xy
        d = math.sqrt((cx - px) ** 2 + (cy - py) ** 2)
        draw.ellipse([cx - d, cy - d, cx + d, cy + d], outline=1, fill=1)
    elif shape_type == 'rectangle':
        assert len(xy) == 2, 'Shape of shape_type=rectangle must have 2 points'
        draw.rectangle(xy, outline=1, fill=1)
    elif shape_type == 'line':
        assert len(xy) == 2, 'Shape of shape_type=line must have 2 points'
        draw.line(xy=xy, fill=1, width=line_width)
    elif shape_type == 'linestrip':
        draw.line(xy=xy, fill=1, width=line_width)
    elif shape_type == 'point':
        assert len(xy) == 1, 'Shape of shape_type=point must have 1 points'
        cx, cy = xy[0]
        r = point_size
        draw.ellipse([cx - r, cy - r, cx + r, cy + r], outline=1, fill=1)
    else:
        assert len(xy) > 2, 'Polygon must have points more than 2'
        draw.polygon(xy=xy, outline=1, fill=1)
    mask = np.array(mask, dtype=bool)
    return mask

def shapes_to_label(img_shape, shapes, label_name_to_value):
    cls = np.zeros(img_shape[:2], dtype=np.int32)
    ins = np.zeros_like(cls)
    instances = []
    for shape in shapes:
        points = shape['points']
        label = shape['label']
        group_id = shape.get('group_id')
        if group_id is None:
            group_id = uuid.uuid1()
        shape_type = shape.get('shape_type', None)

        cls_name = label
        instance = (cls_name, group_id)

        if instance not in instances:
            instances.append(instance)
        ins_id = instances.index(instance) + 1
        cls_id = 1
        # cls_id = label_name_to_value[cls_name]

        mask = shape_to_mask(img_shape[:2], points, shape_type)
        cls[mask] = cls_id
        ins[mask] = ins_id

    return cls, ins

def lblsave(filename, lbl):
    if os.path.splitext(filename)[1] != '.png':
        filename += '.png'
    # Assume label ranses [-1, 254] for int32,
    # and [0, 255] for uint8 as VOC.
    if lbl.min() >= 0 and lbl.max() <= 255:
        lbl_pil = PIL.Image.fromarray(lbl.astype(np.uint8), mode='L')
        lbl_pil.save(filename)
    else:
        raise ValueError(
            '[%s] Cannot save the pixel-wise class label as PNG. '
            'Please consider using the .npy format.' % filename
        )

if __name__ == '__main__':
    data_path = 'D:/DL/ENet/wrinkle/'
    out_path = 'D:/DL/ENet/datasets'
    if not os.path.exists(out_path):
        os.makedirs(out_path)
    label_name_to_value = {
        '_background_': 0,
        'ww': 1,
    }
    json_fns = glob.glob(os.path.join(data_path, '**/*.json'), recursive=True)
    out_lst = []
    for json_fn in tqdm(json_fns):
        with open(json_fn, 'r') as f:
            data = json.load(f)
        img_shape = (data['imageHeight'], data['imageWidth'])
        lbl, _ = shapes_to_label(img_shape, data['shapes'], label_name_to_value)
        image_fn = json_fn.replace('.json', '.jpg')
        label_fn = json_fn.replace('.json', '_label.png')
        lblsave(label_fn, lbl)
        if not os.path.exists(image_fn):
            print(image_fn + ' not exists')
            continue
        else:
            #img = PIL.Image.open(image_fn)
            #mask = PIL.Image.open(label_fn)
            img = cv.imread(image_fn)
            mask = cv.imread(label_fn)
            if img.size != mask.size:
                print(image_fn, img.size, mask.size)
                continue

        out_lst.append(image_fn + ',' + label_fn)
    random.shuffle(out_lst)
    trn_num = int(len(out_lst) * 0.9)
    with open(os.path.join(out_path, 'train.txt'), 'w') as f:
        f.write('\n'.join(out_lst[:trn_num]))
    with open(os.path.join(out_path, 'val.txt'), 'w') as f:
        f.write('\n'.join(out_lst[trn_num:]))

2.如果使用不做处理,直接用这个使用这个标签来训练模型,最终得到的结果会是整个皱纹包括边际,这种结果并不理想,如果要精细化,则要佬轮廓骨架提取。
在这里插入图片描述
在这里插入图片描述
3.对轮廓进行骨架提取
在这里插入图片描述
在这里插入图片描述
骨架提取代码:

void skeleton_extraction(cv::Mat& cv_src, cv::Mat &cv_dst)
{
	cv_dst = cv_src.clone();
	std::vector<cv::Point> deletelist1;
	int index[9];
	int nl = cv_dst.rows;
	int nc = cv_dst.cols;
	while (true)
	{
		for (int j = 1; j < (nl - 1); j++)
		{
			uchar* data_last = cv_dst.ptr<uchar>(j - 1);
			uchar* data = cv_dst.ptr<uchar>(j);
			uchar* data_next = cv_dst.ptr<uchar>(j + 1);
			for (int i = 1; i < (nc - 1); i++)
			{
				if (data[i] == 255)
				{
					index[0] = 1;
					if (data_last[i] == 255) index[1] = 1;
					else  index[1] = 0;
					if (data_last[i + 1] == 255) index[2] = 1;
					else  index[2] = 0;
					if (data[i + 1] == 255) index[3] = 1;
					else  index[3] = 0;
					if (data_next[i + 1] == 255) index[4] = 1;
					else  index[4] = 0;
					if (data_next[i] == 255) index[5] = 1;
					else  index[5] = 0;
					if (data_next[i - 1] == 255) index[6] = 1;
					else  index[6] = 0;
					if (data[i - 1] == 255) index[7] = 1;
					else  index[7] = 0;
					if (data_last[i - 1] == 255) index[8] = 1;
					else  index[8] = 0;
					int whitepointtotal = 0;
					for (int k = 1; k < 9; k++)
					{
						whitepointtotal = whitepointtotal + index[k];
					}
					if ((whitepointtotal >= 2) && (whitepointtotal <= 6))
					{
						int ap = 0;
						if ((index[1] == 0) && (index[2] == 1)) ap++;
						if ((index[2] == 0) && (index[3] == 1)) ap++;
						if ((index[3] == 0) && (index[4] == 1)) ap++;
						if ((index[4] == 0) && (index[5] == 1)) ap++;
						if ((index[5] == 0) && (index[6] == 1)) ap++;
						if ((index[6] == 0) && (index[7] == 1)) ap++;
						if ((index[7] == 0) && (index[8] == 1)) ap++;
						if ((index[8] == 0) && (index[1] == 1)) ap++;
						if (ap == 1)
						{
							if ((index[1] * index[7] * index[5] == 0) && (index[3] * index[5] * index[7] == 0))
							{
								deletelist1.push_back(cv::Point(i, j));
							}
						}
					}
				}
			}
		}
		if (deletelist1.size() == 0) break;
		for (size_t i = 0; i < deletelist1.size(); i++)
		{
			cv::Point tem;
			tem = deletelist1[i];
			uchar* data = cv_dst.ptr<uchar>(tem.y);
			data[tem.x] = 0;
		}
		deletelist1.clear();

		for (int j = 1; j < (nl - 1); j++)
		{
			uchar* data_last = cv_dst.ptr<uchar>(j - 1);
			uchar* data = cv_dst.ptr<uchar>(j);
			uchar* data_next = cv_dst.ptr<uchar>(j + 1);
			for (int i = 1; i < (nc - 1); i++)
			{
				if (data[i] == 255)
				{
					index[0] = 1;
					if (data_last[i] == 255) index[1] = 1;
					else  index[1] = 0;
					if (data_last[i + 1] == 255) index[2] = 1;
					else  index[2] = 0;
					if (data[i + 1] == 255) index[3] = 1;
					else  index[3] = 0;
					if (data_next[i + 1] == 255) index[4] = 1;
					else  index[4] = 0;
					if (data_next[i] == 255) index[5] = 1;
					else  index[5] = 0;
					if (data_next[i - 1] == 255) index[6] = 1;
					else  index[6] = 0;
					if (data[i - 1] == 255) index[7] = 1;
					else  index[7] = 0;
					if (data_last[i - 1] == 255) index[8] = 1;
					else  index[8] = 0;
					int whitepointtotal = 0;
					for (int k = 1; k < 9; k++)
					{
						whitepointtotal = whitepointtotal + index[k];
					}
					if ((whitepointtotal >= 2) && (whitepointtotal <= 6))
					{
						int ap = 0;
						if ((index[1] == 0) && (index[2] == 1)) ap++;
						if ((index[2] == 0) && (index[3] == 1)) ap++;
						if ((index[3] == 0) && (index[4] == 1)) ap++;
						if ((index[4] == 0) && (index[5] == 1)) ap++;
						if ((index[5] == 0) && (index[6] == 1)) ap++;
						if ((index[6] == 0) && (index[7] == 1)) ap++;
						if ((index[7] == 0) && (index[8] == 1)) ap++;
						if ((index[8] == 0) && (index[1] == 1)) ap++;
						if (ap == 1)
						{
							if ((index[1] * index[3] * index[5] == 0) && (index[3] * index[1] * index[7] == 0))
							{
								deletelist1.push_back(cv::Point(i, j));
							}
						}
					}
				}
			}
		}
		if (deletelist1.size() == 0) break;
		for (size_t i = 0; i < deletelist1.size(); i++)
		{
			cv::Point tem;
			tem = deletelist1[i];
			uchar* data = cv_dst.ptr<uchar>(tem.y);
			data[tem.x] = 0;
		}
		deletelist1.clear();
	}
}

4.如果只需要皱纹的线条,可以直接在样本标注生成之后,对数据标签进行骨架提取后再训练,效果要比训练后再做骨架提取要好。

4.训练模型

1.使用的是conda的搭建环境

conda create --name enet python=3.8
source activate enet
conda install pytorch torchvision torchaudio pytorch-cuda=11.7 -c pytorch -c nvidia
pip install cython matplotlib tqdm opencv-python scipy pillow

2.训练模型
运行以下命令:

python main.py -m train --save-dir save/ENet_Card --name ENet --dataset card --dataset-dir datasets --epochs 300 --height 512 --width 512 --print-step

参数说明:
– save-dir:训练模型保存路径
–dataset:训练数据类型
–dataset-dir:train.txt和val.txt所在路径
–epochs:训练总epoch数
–height:输入高度
–width:输入宽度
–print-step:是否打印每个step的loss

3.转onnx模型

import os
import argparse
from addict import Dict
import yaml
import torch
import torch.optim as optim
from models.enet import ENet
import utils

def get_args():
    parser = argparse.ArgumentParser(description='convert pytorch weights to onnx')
    parser.add_argument('--input', type=str, required=True, help="input pytorch model")
    parser.add_argument('--output', type=str, required=True, help="output onnx model")
    args = parser.parse_args()
    return args

if __name__ == '__main__':
    args = get_args()
    model = ENet(2, encoder_relu=True).to('cpu')
    save_dir = os.path.dirname(args.input)
    name = os.path.basename(args.input)
    optimizer = optim.Adam(model.parameters())
    model = utils.load_checkpoint(model, optimizer, save_dir, name)[0]
    inputs = torch.FloatTensor(1, 3, 512, 512)
    torch.onnx.export(model, inputs, args.output, opset_version=9)

5.模型推理

1.转成onnx模型后,可以使用opencv的dnn进行推理

int demo() 
{
	std::string path = "demo";
	std::vector<std::string> filenames;
	cv::glob(path, filenames, false);

	for (auto img_name : filenames)
	{
		cv::Mat cv_src = cv::imread(img_name,1);
		cv::Mat cv_or = cv_src.clone();
		cv::Size reso(512, 512);
		cv::Mat blob = cv::dnn::blobFromImage(cv_src, 1.0 / 255, reso,
			cv::Scalar(0, 0, 0), false, false);
		cv::dnn::Net net = cv::dnn::readNet("model/ENetv2.onnx");
		net.setInput(blob);

		cv::Mat out = net.forward();

		cv::Mat cv_segm = cv::Mat::zeros(out.size[2], out.size[3], CV_8UC1);
		for (int i = 0; i < out.size[2] * out.size[3]; ++i)
		{
			if (out.ptr<float>(0, 0)[i] < out.ptr<float>(0, 1)[i])
			{
				cv_segm.data[i] = 255;
			}
		}
		//cv::Mat cv_seg;
		//skeleton_extraction(cv_segm, cv_seg);

		//show_img("segm", cv_seg);
		cv::Mat cv_dst;
		cv::Mat cv_edge;
		cv::resize(cv_segm, cv_edge, cv_src.size());

		for (int i = 0; i < cv_src.rows; i++)
		{
			for (int j = 0; j < cv_src.cols; j++)
			{
				int pix = int(cv_edge.at<uchar>(i, j));
				if (pix >0)
				{
					cv_src.at<cv::Vec3b>(i, j)[0] = 0;
					cv_src.at<cv::Vec3b>(i, j)[1] = 255;
					cv_src.at<cv::Vec3b>(i, j)[2] = 255;
				}
			}
		}
		show_img("src", cv_or);
		show_img("out", cv_src);
		cv::waitKey();
	}
	return 0;
}

2.推理结果
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/186516.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

仿京东PC网页商品详情的放大镜效果(原理+代码)

实现效果 黑色只不过是转gif出问题而已 准备工作 1. 访问该网址&#xff0c;理解我们要弄的放大镜效果&#xff0c;鼠标经过商品图片&#xff0c;显示一个黄色的放大选区&#xff0c;右边显示该选区的大图。 2. 获取商品图片和商品大图 【摩托罗拉moto X30 Pro】摩托罗拉mot…

protocol-buffers 基础(一)

protocol-buffers 官网 Github 一、概述 协议缓冲区&#xff08;protocol-buffers&#xff09;是一种与语言无关、与平台无关的可扩展机制&#xff0c;用于序列化结构化数据。 协议缓冲区提供了一种与语言无关、与平台无关、可扩展的机制&#xff0c;用于以向前兼容和向后兼…

【深度学习笔记】全卷积网络FCN及 ROI Pooling 和 ROI Align 的区别

问题 FCN 是语义分割的开山之作,没理由不了解。最近发现学习最快的方式还是看别人的教学视频,很多时候博客看一大推,没几个能讲地明白的,确实啊,视频都得用四五十分钟的东西,一个博客得写得多详细才能覆盖到方方面面呀,以后还是先借助视频来学习,然后再根据源码进一步…

leetcode 1669. 合并两个链表【python3一次遍历方法详细过程描述】

题目 给你两个链表list1和list2&#xff0c;它们包含的元素分别为n个和m个。请你将list1中下标从a到b的全部节点都删除&#xff0c;并将list2接在被删除节点的位置。 下图中蓝色边和节点展示了操作后的结果&#xff1a; 请你返回结果链表的头指针。 示例1&#xff1a; 输入&…

网络原理(TCP/IP五层协议)(二)

目录TCP 中的一些主要机制1.确认应答机制(安全机制)2.超时重传机制(安全机制)3.连接管理机制(安全机制)如何建立连接&#xff1a;三次握手如何断开连接&#xff1a;四次挥手TCP 中的一些主要机制 TCP对数据传输提供的管控机制&#xff0c;主要体现在两个方面&#xff1a;安全和…

C++右值引用相关

能够取地址值、有名字的就是左值&#xff0c;不能取地址值、没名字的就是右值。 移动语义 将内存的所有权从一个对象转换到另一个对象&#xff0c;高效的移动来替换效率低下的赋值。 完美转发 定义一个函数模板&#xff0c;该函数模板可以接受任意类型参数&#xff0c;然后将参…

即时通讯开发之TCP/IP中的IP协议、ARP协议

IP 协议是 TCP/IP 协议的核心,所有的 TCP,UDP,IMCP,IGCP 的数据都以 IP 数据格式传输。要注意的是,IP 不是可靠的协议,这是 说,IP 协议没有提供一种数据未传达以后的处理机制--这被认为是上层协议--TCP 或 UDP 要做的事情。所以这也就出现了 TCP 是一个可靠的协议,而 UDP 就没有…

前端开发:JS中关于八皇后算法的使用

前言 在前端开发过程中&#xff0c;关于算法的使用也是非常常见的操作&#xff0c;尤其是处理一些复杂的业务场景&#xff0c;还有就是前端获取到后端返回的复杂结构的数据&#xff0c;所以说前端开发中处处都有算法使用的场景。开发者从接触编程开发开始&#xff0c;就与算法脱…

Java switch case语句详解

if…else 语句可以用来描述一个“二岔路口”&#xff0c;我们只能选择其中一条路来继续走&#xff0c;然而生活中经常会碰到“多岔路口”的情况。switch 语句提供了 if 语句的一个变通形式&#xff0c;可以从多个语句块中选择其中的一个执行。switch 语句格式switch 语句是 Jav…

44物体检测算法:R-CNN,SSD,YOLO【动手学深度学习v2】

44物体检测算法&#xff1a;R-CNN&#xff0c;SSD&#xff0c;YOLO【动手学深度学习v2】 深度学习学习笔记 学习视频&#xff1a;https://www.bilibili.com/video/BV1if4y147hS/?spm_id_from333.337.search-card.all.click&vd_source75dce036dc8244310435eaf03de4e330 目标…

使用Springboot 2.7+Websocket+js实现服务端消息实时推送

文章目录诉求相关技术相关步骤项目创建编写相关配置消息逻辑处理使用JavaScript实现Websocket的前端功能功能测试诉求 模拟服务端主动推送消息给客户端&#xff0c;同时展示客户端发送给客户端的消息以及服务端推送给客户的消息。 相关技术 Springboot(2.7.0)Websocketjavascr…

Elasticsearch7.8.0版本入门——JavaAPI操作(文档操作)

目录一、pom文件依赖二、创建实体对象类三、文档操作代码示例3.1、创建文档代码示例3.2、修改文档代码示例3.3、查询文档代码示例3.4、删除文档代码示例一、pom文件依赖 引入相关依赖 <!-- elasticsearch 依赖 --> <dependency><groupId>org.elasticsearch&l…

【Pytorch】自定义autograd函数,使用graphviz画出计算图

使用pytorch.autograd.Function构建一个自动求导层1. 手工设计一个线性运算层2. 使用pytorch.autograd.Function编码实现3. graphviz进行可视化1. 手工设计一个线性运算层 设输入为x\bold{x}x&#xff0c;参数为w\bold{w}w和b\bold{b}b&#xff0c;运算如下&#xff1a; yw⊙x…

Tips for Confluence Administrators: Part 3

上一篇Part 2中&#xff0c;我们谈到了Confluence的一些已知Bug&#xff0c;例如&#xff1a;从垃圾桶中恢复同名页面不应覆盖现有页面复制/粘贴表格单元格无法按预期工作无法复制表头嵌套表格无法调整大小请不要吝惜你的投票&#xff0c;这有助于提高Atlassian修复它们的优先级…

四、Maven详细教程

本教程相关资料&#xff1a;https://www.aliyundrive.com/s/wMiqbd4Zws6 Maven是专门用于管理和构建Java项目的工具&#xff0c;它的主要功能有&#xff1a; 提供了一套标准化的项目结构 提供了一套标准化的构建流程&#xff08;编译&#xff0c;测试&#xff0c;打包&#xf…

MacOs安装Redis并设置为开机、后台启动

前言 最近闲来无事&#xff0c;将自己的MBP系统重装里&#xff0c;导致里面原来安装的软件都需要重新安装&#xff0c;今天记录一下MacOs安装Redis并设置为开机启动、后台启动的步骤&#xff0c;安装过程略有波折&#xff0c;参考里几篇文章才搞定。 一、安装Redis 两种方式…

【JavaEE】线程池

目录 线程池概念 线程池优点 使用线程池 Executor接口&#xff1a; ThreadPoolExecutor类&#xff1a; 构造方法 Executors工厂类&#xff1a; 工厂方法 线程池中的常用方法 线程池的工作流程 线程池的状态 RUNNING SHUTDOWN STOP TIDYING TERMINATED 简单实现…

Android10 开机向导流程

最近在弄开机向导&#xff0c;网上查了查&#xff0c;基本都是参照系统的​​​​​​Provision应用来做的&#xff0c;而且还要将apk打包到系统目录下的pri-app目录下&#xff0c;打包到其他目录下不行&#xff0c;参照着做是没问题&#xff0c;但是好奇为什么要这么做&#x…

esp32连接阿里云物联网平台进行MQTT通信

前提&#xff1a;IDE是采用arduino IDE&#xff0c;arduino使用的库是pubsubclient 开发板可以使用esp32&#xff08;esp8266也是一样的&#xff09; 已经学会pubsubclient库的基本使用 使用pubsubclient 库连接阿里云物联网平台 const char* ssid "........"; c…

java ssm校园勤工俭学助学志愿者兼职系统 idea maven

本论文主要论述了如何使用JSP技术开发一个校园勤工俭学兼职系统&#xff0c;本系统将严格按照软件开发流程进行各个阶段的工作&#xff0c;采用B/S架构&#xff0c;面向对象编程思想进行项目开发。在引言中&#xff0c;作者将论述校园勤工俭学兼职系统的当前背景以及系统开发的…