基于U-Net的皮肤检测毛孔分割实战:从数据集制作到模型训练验证

基于YOLOv11的痤疮智能检测系统设计与优化 目标检测技术在医疗影像分析中扮演着重要角色,其核心原理是通过深度学习模型自动识别图像中的特定目标。YOLOv11作为当前最先进的目标检测算法之一,通过动态标签分配和跨阶段特征融合等创新,显著提升了小目标检测性能。这些技术特性使其特别适合医疗场景下的密集小目标识别,如痤疮病灶检测。在实际工程应用中,针对痤疮检测的特殊需求,需要对模型架构进行针对性优化,包括调整anchor比例、改进NMS算法等。通过结合PyTorch框架和TensorRT部署工具,可以实现从模型训练到边缘设备落地的完整解决方案。该系统在三甲医 阅读详情

听说YOLO 和U-Net,也是好几年了,对于这两个熟悉的陌生人我是想用的时候才用一下,可能是和老派视觉库合作太久了吧。下面就来记录一下老人新手使用U-Net.
(1) 了解U-Net
在这里插入图片描述
然后我又在想YOLO和U-Net 有什么不同,不都是要做检测的嘛。
具体如下:
在这里插入图片描述
看看同一张图两者输出的结果:
在这里插入图片描述
YOLO大哥只需要框出来就行,U-Net 还得分割出具体轮廓。这是检测和分割的具象差异了吧。

看起来如果我只需要统计一张图上有多少个毛孔,那YOLO其实就够了,如果我还需要分析毛孔的大小(直径/体素/面积),那我还是得用U-Net分割。
比较遗憾,我做标注用的还是标准的圆,并不止是圈出毛孔,还包括了些许外围。此时我就在想,U-net 的标注图应该怎么构建呢?因为他分割的是物体本身,对于形态不固定的物体怎么才能使得标注只包含需要的前景?这个问题可能要困扰我一段时间。新手尴尬期。。。到现在我更加认可yolo的矩形标注,其实就是告诉模型目标在框里。
看看这个标注,我只能把肉眼非常确定的毛孔圈出来。而且我发现labelme 有个确定,如果把标注形状画的超出了图像边界,必然要卡死。还有如果导入图片有中文名也是要卡死的。
在这里插入图片描述
(2) 训练模型
这个其实就是完整的过程。可以概括为如下步骤:
①收集数据: 收集需要做分割的图像数据
②数据预处理
这一步还蛮关键的,如果肉眼上看图中目标和背景之间有比较好的对比差异,那模型学习的效果也更好。
所以我们在这一步应该要做一些去噪、提升目标和背景的差异,还有最重要的一步就是统一好所有图像的分辨率。这样确保送到标注软件中的图规格一致。
我的预处理目标就是去噪和做适度的锐化还有确保输入的图像分辨率一致。因为我要分割出毛孔目标,我比较适合的是先做保边平滑,然后再做适当锐化。总之就是希望我后面做标注的时候圈里面有较好的均匀性,而又保留甚至增强毛孔的边界。
在这里插入图片描述
附上我的图像批量预处理代码,先做图像分辨率统一,然后做一个保边平滑,然后再锐化。‘
preprocess.py

import os
import cv2
import numpy as np
from concurrent.futures import ThreadPoolExecutor, as_completed
from tqdm import tqdm

# ================= 中文路径读写支持 =================
def read_image_cn(path):
    """
    支持中文路径的图像读取函数
    """
    try:
        # 以二进制形式读取文件,绕开路径编码问题
        raw_data = np.fromfile(path, dtype=np.uint8)  
        # 从内存缓冲区解码为 OpenCV 图像格式
        image = cv2.imdecode(raw_data, cv2.IMREAD_COLOR)  
        return image
    except Exception as e:
        print(f"\n[读取失败] {path} - {e}")
        return None

def save_image_cn(image, path):
    """
    支持中文路径的图像保存函数
    """
    try:
        # 将图像编码为内存缓冲区 (这里默认保存为 jpg,可根据需要改后缀)
        success, encoded = cv2.imencode(".jpg", image)
        if success:
            # 以二进制方式写入中文路径
            encoded.tofile(path)  
    except Exception as e:
        print(f"\n[保存失败] {path} - {e}")
# ========================================================

def adaptive_preprocess(img):
    """
    根据输入图像的分辨率进行自适应预处理(统一输出为宽720 x 高960)
    """
    h, w = img.shape[:2]
    
    if w == 720 and h == 960:
        return img
    elif w == 960 and h == 720:
        return cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
    elif w == 1280 and h == 720:
        target_w, target_h = 960, 720
        start_x = (w - target_w) // 2 
        cropped_img = img[0:target_h, start_x:start_x+target_w]
        return cv2.rotate(cropped_img, cv2.ROTATE_90_CLOCKWISE)
    else:
        print(f"\n[警告] 发现未定义的图像尺寸: {w}x{h},已跳过该图")
        return None
def preprocess_single_image(input_path, output_path):
    try:
        # 1. 读取与尺寸自适应
        img = read_image_cn(input_path)
        if img is None: return
        processed_img = adaptive_preprocess(img)
        if processed_img is None: return  

        # 2. 【保边去噪】稍微放宽颜色容忍度,更彻底地抹平油光和杂色
        # sigmaColor=85 能让皮肤底色更干净,同时死死锁住毛孔边缘
        denoised_img = cv2.bilateralFilter(processed_img, d=11, sigmaColor=85, sigmaSpace=75)

        # 3. 【增强版 USM 锐化】大幅提升毛孔边缘对比度
        gaussian_blur = cv2.GaussianBlur(denoised_img, (0, 0), sigmaX=2.5)
        # 权重 1.8 和 -0.8 会让毛孔的凹陷轮廓更加深邃、立体
        final_img = cv2.addWeighted(denoised_img, 1.8, gaussian_blur, -0.8, 0)        
        # 4. 【安全边界】防止像素值溢出产生彩色伪影
        final_img = np.clip(final_img, 0, 255).astype(np.uint8)

        # 5. 保存结果
        save_image_cn(final_img, output_path)

    except Exception as e:
        print(f"\n[失败] 处理 {input_path} 时出错: {e}")

def batch_preprocess(input_folder, output_folder, max_workers=None):
    """
    批量预处理入口,使用多线程提升效率
    """
    if not os.path.exists(output_folder):
        os.makedirs(output_folder)

    image_extensions = ('.jpg', '.jpeg', '.png', '.bmp', '.tiff')
    
    tasks = []
    for filename in os.listdir(input_folder):
        if filename.lower().endswith(image_extensions):
            input_path = os.path.join(input_folder, filename)
            output_path = os.path.join(output_folder, filename)
            tasks.append((input_path, output_path))

    print(f"发现 {len(tasks)} 张图像,开始多线程批量预处理...")

    if max_workers is None:
        max_workers = os.cpu_count() * 2 

    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = [executor.submit(preprocess_single_image, task[0], task[1]) for task in tasks]
        for future in tqdm(as_completed(futures), total=len(futures), desc="预处理进度"):
            future.result() 

    print("所有图像预处理完成!")

if __name__ == "__main__":
    RAW_IMAGE_DIR = "./raw_images"         
    PROCESSED_DIR = "./images_for_labelme" 
    
    batch_preprocess(RAW_IMAGE_DIR, PROCESSED_DIR)

③图像标注
这一块很重要,尽量把最确定的标记出来,标记出优质的。我用的labelme,用文件夹的方式加载,每标注一张图就保存然后自然过度到下一张,直到标注完所有。
④基于标注输出的json 文件和其对应的原始图像生成mask 图
这一步跟yolo 的处理真的非常不同。
需要读标注输出的json 并把json中记录的位置在mask 图中标白,而背景为黑,也就是生成目标和背景mask,且与原图分辨率一致。
这里我不想用官方的 labelme2voc.py 文件了,因为太繁琐了,依赖项很多,要pip一堆。还不如自己直接用openCV 生成;
附上我的json 转mask 的py 代码:

import json
import numpy as np
import cv2
import os
# ==========================================
# 👇 【在此处配置您的文件夹路径】👇
# 请将下面的路径替换为您存放图片和JSON的文件夹绝对路径
# 注意:Windows路径中可以使用双反斜杠 \\ 或者正斜杠 /
# =============这是我的json和图像存储路径=============================
INPUT_DIR = r"D:\VS_programm\Skin_UNet\images_for_labelme"  
# ==========================================


def convert_json_to_mask(input_dir):
    """将指定文件夹下的JSON文件批量转换为Mask掩膜图"""
    
    # 检查传入的文件夹是否存在
    if not os.path.isdir(input_dir):
        print(f"错误: 指定的路径 '{input_dir}' 不存在!请检查代码中的 INPUT_DIR 配置。")
        return

    # 在输入文件夹下自动创建一个 'masks' 子文件夹用于存放结果
    mask_dir = os.path.join(input_dir, "masks")
    os.makedirs(mask_dir, exist_ok=True)
    
    # 获取文件夹下所有的JSON文件
    json_files = [f for f in os.listdir(input_dir) if f.endswith('.json')]
    
    if not json_files:
        print("警告: 指定的文件夹中没有找到任何 JSON 文件!")
        return

    success_count = 0
    for file_name in json_files:
        base_name = os.path.splitext(file_name)[0]
        
        # 1. 寻找同名的原始图片 (支持常见图片格式)
        image_path = None
        for ext in ['.jpg', '.png', '.jpeg', '.bmp']:
            test_path = os.path.join(input_dir, base_name + ext)
            if os.path.exists(test_path):
                image_path = test_path
                break
                
        if not image_path:
            print(f"跳过: 未找到与 {file_name} 对应的原始图像")
            continue

        # 2. 读取JSON数据并获取图像尺寸
        json_path = os.path.join(input_dir, file_name)
        with open(json_path, 'r', encoding='utf-8') as f:
            data = json.load(f)
            
        img_height = data['imageHeight']
        img_width = data['imageWidth']
        
        # 3. 创建全黑的单通道掩膜图 (背景为0)
        mask = np.zeros((img_height, img_width), dtype=np.uint8)
        
        # 4. 遍历标注形状,将标注区域(圆形/多边形)填充为白色(255)
        for shape in data['shapes']:
            # 获取标注类型和坐标点
            shape_type = shape.get('shape_type', 'polygon')
            points = np.array(shape['points'], dtype=np.int32)
            
            # 🌟 核心修复:专门处理 Labelme 的圆形标注
            if shape_type == 'circle':
                # Labelme的圆只记录了 [圆心点, 圆周上的点]
                center = tuple(points[0])
                # 计算两点之间的距离作为半径
                radius = int(np.linalg.norm(points[0] - points[1]))
                # 使用 cv2.circle 绘制实心圆 (-1表示填充)
                cv2.circle(mask, center, radius, color=255, thickness=-1)
                
            else: 
                # 其他情况(如多边形)默认按 fillPoly 处理
                cv2.fillPoly(mask, [points], color=255)
            
        # 5. 保存生成的Mask图到 masks 文件夹
        save_mask_path = os.path.join(mask_dir, base_name + ".png")
        cv2.imwrite(save_mask_path, mask)
        success_count += 1
        
    print(f"转换完毕!共成功处理 {success_count}/{len(json_files)} 个文件。")
    print(f"Mask掩膜图已保存至: {mask_dir}")


if __name__ == "__main__":
    convert_json_to_mask(INPUT_DIR)

总之就这个么一段代码,我的标注图就转成对应的mask了,因为我只标注了肉眼非常确定是毛孔的位置,而且我避免了靠近边界不完整目标。
在这里插入图片描述
⑤构造训练集和测试集
在这里插入图片描述
基于上一步生成的mask图及其对应的预处理后的图,需要按照比例归并到训练集train 和测试集 test 文件夹。注意并不是乱放的,一般训练集占比70%——80%,而测试集占比20%——30%。 还有train 和test文件夹中的图和mask是一一对应的匹配关系。千万不可张冠李戴。
⑥ 训练模型并调参
这里需要基于输出和测试结果进行调整和优化,使得随机测试和外加测试图上都可以得到一个比较好的识别结果为止。
在这里插入图片描述
附上我的训练代码train.py, 其调用了优化处理 dataset.py 和model.py

如下为 train.py的代码

import torch
import torch.optim as optim
from torch.utils.data import DataLoader
from tqdm import tqdm
from dataset import SkinPoreDataset, get_transforms
from model import UNET

# ================= 关键配置区域 =================
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
LEARNING_RATE = 1e-4
EPOCHS = 50  # 训练轮数
BATCH_SIZE = 8
NUM_WORKERS = 2
PIN_MEMORY = True
LOAD_MODEL = False  # 是否加载之前的模型继续训练
SAVE_MODEL = True
CHECKPOINT_DIR = "./checkpoints/"
TRAIN_IMG_DIR = r"D:\VS_programm\Skin_UNet\images_for_labelme\dataset\train\images"
TRAIN_MASK_DIR = r"D:\VS_programm\Skin_UNet\images_for_labelme\dataset\train\masks"
VAL_IMG_DIR = r"D:\VS_programm\Skin_UNet\images_for_labelme\dataset\test\images"
VAL_MASK_DIR = r"D:\VS_programm\Skin_UNet\images_for_labelme\dataset\test\masks"
# ===========================================

def train_fn(loader, model, optimizer, loss_fn, scaler):
    loop = tqdm(loader)
    for batch_idx, (data, targets) in enumerate(loop):
        data = data.to(device=DEVICE)
        targets = targets.to(device=DEVICE)

        with torch.cuda.amp.autocast():
            predictions = model(data)
            loss = loss_fn(predictions, targets)

        optimizer.zero_grad()
        scaler.scale(loss).backward()
        scaler.step(optimizer)
        scaler.update()

        loop.set_postfix(loss=loss.item())

def main():
    train_transform, val_transform = get_transforms()

    train_dataset = SkinPoreDataset(TRAIN_IMG_DIR, TRAIN_MASK_DIR, transform=train_transform)
    val_dataset = SkinPoreDataset(VAL_IMG_DIR, VAL_MASK_DIR, transform=val_transform)

    train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=NUM_WORKERS, pin_memory=PIN_MEMORY)
    val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS, pin_memory=PIN_MEMORY)

    model = UNET(in_channels=3, out_channels=1).to(DEVICE)
    optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)
    loss_fn = nn.BCEWithLogitsLoss() # 适合二分类分割
    scaler = torch.cuda.amp.GradScaler()

    if LOAD_MODEL:
        checkpoint = torch.load(os.path.join(CHECKPOINT_DIR, "my_checkpoint.pth.tar"))
        model.load_state_dict(checkpoint["state_dict"])
        optimizer.load_state_dict(checkpoint["optimizer"])

    for epoch in range(EPOCHS):
        print(f"\nEpoch [{epoch+1}/{EPOCHS}]")
        train_fn(train_loader, model, optimizer, loss_fn, scaler)

        # 保存模型
        if SAVE_MODEL:
            checkpoint = {
                "state_dict": model.state_dict(),
                "optimizer": optimizer.state_dict(),
            }
            save_checkpoint(checkpoint, filename="my_checkpoint.pth.tar")

def save_checkpoint(state, filename="my_checkpoint.pth.tar"):
    import os
    os.makedirs(CHECKPOINT_DIR, exist_ok=True)
    print("=> Saving checkpoint")
    torch.save(state, os.path.join(CHECKPOINT_DIR, filename))

if __name__ == "__main__":
    import torch.nn as nn
    main()

如下为 dataset.py的代码

import os
import torch
from torch.utils.data import Dataset
from PIL import Image
import numpy as np
import albumentations as A
from albumentations.pytorch import ToTensorV2

class SkinPoreDataset(Dataset):
    def __init__(self, image_dir, mask_dir, transform=None):
        self.image_dir = image_dir
        self.mask_dir = mask_dir
        self.transform = transform
        # 获取所有图片文件名
        self.images = [f for f in os.listdir(image_dir) if f.endswith(('.jpg', '.png', '.jpeg'))]

    def __len__(self):
        return len(self.images)

    def __getitem__(self, index):
        img_path = os.path.join(self.image_dir, self.images[index])
        # 假设Mask文件名与原图一致,只是后缀可能是png
        mask_name = os.path.splitext(self.images[index])[0] + ".png"
        mask_path = os.path.join(self.mask_dir, mask_name)

        # 读取图片
        image = np.array(Image.open(img_path).convert("RGB"), dtype=np.uint8)
        mask = np.array(Image.open(mask_path).convert("L"), dtype=np.uint8)

        # 确保Mask只有0和1 (归一化)
        mask[mask == 255] = 1.0

        # 数据增强 (如果传入了transform)
        if self.transform:
            augmented = self.transform(image=image, mask=mask)
            image = augmented["image"]
            mask = augmented["mask"]

        # 增加通道维度 (H,W) -> (1,H,W),因为U-Net通常需要单通道Mask输入
        mask = mask.unsqueeze(0)

        return image, mask.float()

# 定义训练集和测试集的变换策略
def get_transforms():
    train_transform = A.Compose([
        A.Resize(height=256, width=256), # 统一尺寸
        A.HorizontalFlip(p=0.5),         # 随机水平翻转
        A.VerticalFlip(p=0.5),           # 随机垂直翻转
        A.Rotate(limit=30, p=0.5),       # 随机旋转
        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), # ImageNet标准化
        ToTensorV2(),
    ])

    val_transform = A.Compose([
        A.Resize(height=256, width=256),
        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
        ToTensorV2(),
    ])

    return train_transform, val_transform

如下为:model.py的代码

import torch
import torch.nn as nn

class DoubleConv(nn.Module):
    def __init__(self, in_channels, out_channels):
        super(DoubleConv, self).__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(in_channels, out_channels, 3, 1, 1, bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_channels, out_channels, 3, 1, 1, bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
        )

    def forward(self, x):
        return self.conv(x)

class UNET(nn.Module):
    def __init__(self, in_channels=3, out_channels=1, features=[64, 128, 256, 512]):
        super(UNET, self).__init__()
        self.ups = nn.ModuleList()
        self.downs = nn.ModuleList()
        self.pool = nn.MaxPool2d(kernel_size=2, stride=2)

        # 下采样部分 (Encoder)
        for feature in features:
            self.downs.append(DoubleConv(in_channels, feature))
            in_channels = feature

        # 瓶颈层 (Bottleneck)
        self.bottleneck = DoubleConv(features[-1], features[-1]*2)

        # 上采样部分 (Decoder)
        for feature in reversed(features):
            self.ups.append(
                nn.ConvTranspose2d(feature*2, feature, kernel_size=2, stride=2)
            )
            self.ups.append(DoubleConv(feature*2, feature))

        # 最终输出层
        self.final_conv = nn.Conv2d(features[0], out_channels, kernel_size=1)

    def forward(self, x):
        skip_connections = []

        for down in self.downs:
            x = down(x)
            skip_connections.append(x)
            x = self.pool(x)

        x = self.bottleneck(x)
        skip_connections = skip_connections[::-1] # 反转列表以匹配上采样

        for idx in range(0, len(self.ups), 2):
            x = self.ups[idx](x)
            skip_connection = skip_connections[idx//2]

            # 处理尺寸不匹配的情况
            if x.shape != skip_connection.shape:
                x = nn.functional.interpolate(x, size=skip_connection.shape[2:])

            concat_skip = torch.cat((skip_connection, x), dim=1)
            x = self.ups[idx+1](concat_skip)

        return self.final_conv(x)

⑦测试模型并反馈调参模型
跑完上一步的训练以后就是跑预测了,如果预测结果不好,那说明需要调整参数,如果怎么调都不够好,那有可能是输入标注的不好。总之一切都有原因。慢慢调整和优化吧。
下面是我的预测代码 predict.py

import torch
import torchvision.transforms as transforms
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
from model import UNET
import os

DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
MODEL_PATH = "./checkpoints/my_checkpoint.pth.tar"
TEST_IMAGE_PATH = r"D:\VS_programm\Skin_UNet\images_for_labelme\dataset\test\images" # 指向测试集原图文件夹
OUTPUT_DIR = "./predictions"

def predict_and_save(image_path, model):
    model.eval()
    image = Image.open(image_path).convert("RGB")
    original_size = image.size  # 获取原图尺寸 (宽, 高)

    # 1. 预处理 (缩放到模型需要的 256x256)
    transform = transforms.Compose([
        transforms.Resize((256, 256)),
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
    ])
    img_tensor = transform(image).unsqueeze(0).to(DEVICE)

    # 2. 模型预测
    with torch.no_grad():
        predictions = torch.sigmoid(model(img_tensor))
        predictions = (predictions > 0.5).float()

    # 3. 后处理:将 Mask 还原为原始分辨率
    pred_np = predictions.squeeze().cpu().numpy()
    
    # 核心修复:使用双线性插值将 256x256 的 Mask 放大回原图尺寸
    pred_img_resized = Image.fromarray((pred_np * 255).astype(np.uint8))
    pred_img_resized = pred_img_resized.resize(original_size, Image.BILINEAR)
    
    # 再次二值化:因为插值放大后边缘可能会产生灰度过渡,这里重新把大于127的变白,否则变黑
    final_mask_np = np.array(pred_img_resized)
    final_mask_np[final_mask_np >= 127] = 255
    final_mask_np[final_mask_np < 127] = 0
    
    final_pred_img = Image.fromarray(final_mask_np.astype(np.uint8))

    # 4. 保存结果
    os.makedirs(OUTPUT_DIR, exist_ok=True)
    save_name = os.path.basename(image_path)
    final_pred_img.save(os.path.join(OUTPUT_DIR, f"pred_{save_name}"))
    print(f"预测完成并已还原至原始分辨率: {os.path.join(OUTPUT_DIR, f'pred_{save_name}')}")

    # 5. 可视化对比 (现在两张图尺寸完全一致了)
    plt.figure(figsize=(10, 5))
    plt.subplot(1, 2, 1)
    plt.imshow(image)
    plt.title("Original Image")
    plt.axis('off')

    plt.subplot(1, 2, 2)
    plt.imshow(final_pred_img, cmap='gray')
    plt.title("Predicted Mask (Original Size)")
    plt.axis('off')
    plt.show()

if __name__ == "__main__":
    model = UNET(in_channels=3, out_channels=1).to(DEVICE)
    if os.path.exists(MODEL_PATH):
        checkpoint = torch.load(MODEL_PATH, map_location=DEVICE)
        
        # 优化:因为保存时存的是字典,这里直接读取 "state_dict" 键,更加安全
        model.load_state_dict(checkpoint["state_dict"])
        print("模型加载成功!")

        # 遍历测试文件夹进行预测
        for img_name in os.listdir(TEST_IMAGE_PATH):
            if img_name.endswith(('.jpg', '.png')):
                full_path = os.path.join(TEST_IMAGE_PATH, img_name)
                predict_and_save(full_path, model)
    else:
        print(f"错误: 找不到模型文件 {MODEL_PATH},请先运行 train.py")

来看看我的预测结果,虽然看起来还不够理想,但是比我用传统CV 算法做的要好很多啊。而且我的输入其实也真不咋地。。。
后面再继续进一步优化,感觉希望就在眼前啊~
在这里插入图片描述

⑧把模型转成可以部署的onnx 格式
要部署,甚至脱离python部署,我就得把训练好得模型转成onnx 格式。
如下为export_onnx.py

import torch
from model import UNET

# 1. 加载训练好的模型权重
DEVICE = "cpu"  # 导出时建议使用 CPU
model = UNET(in_channels=3, out_channels=1).to(DEVICE)
checkpoint = torch.load("./checkpoints/my_checkpoint.pth.tar", map_location=DEVICE)
model.load_state_dict(checkpoint["state_dict"])
model.eval()  # 必须设置为评估模式

# 2. 构造一个虚拟输入(形状需与训练时一致)
dummy_input = torch.randn(1, 3, 256, 256, device=DEVICE)

# 3. 导出为 ONNX 格式
torch.onnx.export(
    model,
    dummy_input,
    "skin_unet.onnx",          # 导出的文件名
    opset_version=11,          # ONNX 算子集版本(推荐 11 或更高)
    input_names=["input"],     # 输入节点名称
    output_names=["output"],   # 输出节点名称
    do_constant_folding=True   # 开启常量折叠优化计算图
)
print("成功导出 skin_unet.onnx")

今天先到此吧,正好有了更多得数据,后面再来一大波,总之感觉这个U-Net 还挺好玩的。。。

基于YOLOv11的痤疮检测系统:医疗AI实践 计算机视觉在医疗领域的应用正逐步改变传统诊断方式,其中目标检测技术如YOLO系列算法因其高效性备受关注。YOLOv11通过改进的网络结构和优化策略,在保持实时性的同时提升了检测精度。在医疗场景中,这种技术能有效解决传统诊断效率低、主观性强等痛点,特别适用于皮肤病变检测等需要快速准确判断的领域。本文介绍的痤疮检测系统正是基于YOLOv11深度优化,通过添加CBAM注意力机制和病灶感知NMS等改进,实现了89.7%的mAP@0.5准确率。系统采用客户端-服务端架构,结合HTTP/2和gRPC协议,既保障了数据隐 阅读详情

相关推荐

LearnIR:基于后验采样的图像恢复技术,解决人脸去阴影与去雾难题

图像恢复是计算机视觉中的经典逆问题,旨在从退化的观测图像中重建出清晰的原始图像。其核心原理在于建模观测图像与潜在清晰图像之间的退化过程,并求解其逆过程。传统方法常面临保真度与感知质量之间的权衡困境,导致结果过于平滑或产生伪影。LearnIR技术通过引入后验采样思想,将恢复过程转化为从后验概率分布中采样的过程,从而在保持结构一致性的同时,生成多样且合理的细节。该技术在工程实践中,通过不确定性引导的注意力机制和条件化采样,有效应用于人脸图像去阴影和去雾等场景,显著提升了纹理细节的自然度与视觉质量。

weixin_30512043的博客 437

Open-AutoGLM皮肤状态监测:3步实现专业级肤质分析,普通人也能轻松上手

Open-AutoGLM皮肤状态监测:3步实现专业级肤质分析,普通人也能轻松上手。通过AI图像识别与多维度指标评估,快速检测痘痘、黑头、水油平衡等问题,适用于日常护肤与医美参考。操作简单、结果精准,居家护肤好帮手,值得收藏。

PixelIsle的博客 1172

AI生成可动画3D模型:从草图到FBX的工业级管线

可动画3D模型是游戏、影视与AR/VR内容生产的核心资产,其本质是具备拓扑完整性、骨骼层级结构和精确蒙皮权重的显式网格。传统流程依赖建模、拓扑、绑定多环节人工协作,效率低且易返工。AI驱动的隐式场生成技术正突破这一瓶颈,通过语义骨架引导、拓扑感知SDF+权重场联合建模及可微分绑定模拟,实现从2D草图到引擎就绪FBX的一键输出。该方案强调结构确定性而非纹理细节,确保生成模型天然支持FK/IK混合控制、UE5/Unity实时驱动及动作捕捉对接,真正解决‘概念→可动资产’的转化断点。本文详解其在真实工业场景中落地

weixin_34221073的博客 371

记录第一次使用YOLOv8-seg做皮肤色素块分割

记录第一使用yolov8-se做色素块分割

learner_jj的博客 274

混元图像3.0上线LiblibAI:中文可控生成新范式

图像生成模型正从英文主导的通用能力,转向深度适配中文语义与本土创作场景的可控生成阶段。其核心原理在于重构文本编码器、融合空间感知控制机制,并强化负向提示鲁棒性,从而提升对‘杭州西湖’‘汉服’‘青花瓷’等中文高维概念的理解精度与结构稳定性。这一技术升级显著提升了电商设计、国风插画、政务宣传等真实工作流中的交付确定性,使非技术用户无需调参即可获得构图合理、细节可信、风格一致的高质量图像。混元图像3.0在LiblibAI平台的落地,正是该范式从实验室走向千万级创作者桌面的关键实践。

weixin_30632883的博客 484

深度学习之Encoder-Only原理与实战

【代码】深度学习之Encoder-Only原理与实战。

yumiwangzi30的博客 270

扩散模型替代自回归:生成式AI的范式跃迁与工程落地

扩散模型作为一种基于噪声建模与可逆去噪过程的生成范式,正逐步突破传统自回归模型在长程依赖、局部编辑、跨模态对齐等方面的结构性瓶颈。其核心原理在于将生成任务重构为连续隐空间中的梯度驱动退火过程,而非离散token序列的条件概率链式预测。这种范式转变带来了显著的技术价值:支持任意区域掩码编辑、天然并行采样、端到端连续建模及细粒度可控干预。在图像生成、多模态合成、可控编辑等典型场景中,扩散模型已展现出更优的PSNR、FID与用户满意度指标。本文聚焦‘Diffusion Over Autoregression’这一

angou6476的博客 405

深度学习之Seq2Seq原理与实战

【代码】深度学习之Seq2Seq原理与实战。

yumiwangzi30的博客 136

基于物理光学模型的视频眼镜移除:从畸变校正到图像修复的工程实践

在计算机视觉领域,图像几何畸变校正是提升算法鲁棒性的基础技术,其核心原理在于通过数学模型(如多项式畸变模型)描述并逆向补偿由光学元件引入的非线性形变。这项技术的价值在于能够从源头恢复被扭曲的图像结构,为后续的人脸识别、特征点检测等高层视觉任务提供更精确的输入。其典型应用场景包括视频会议美化、证件照处理以及增强现实中的虚实融合。本文聚焦于视频眼镜移除这一具体应用,针对镜片带来的反光、折射等复杂畸变,深入探讨了如何构建一个从畸变参数估计到图像修复的完整处理流水线,并提供了关键模块的实现细节与调试指南,旨在帮助开

weixin_34245082的博客 434

深度学习之TF-IDF原理与实战

【代码】深度学习之TF_IDF原理与实战。

yumiwangzi30的博客 189

深度学习之LoRA(低秩适配)原理与实战

【代码】深度学习之LoRA(低秩适配)原理与实战。

yumiwangzi30的博客 260

老照片修复实战:用CodeFormer恢复泛黄照片的清晰神采

老照片修复是数字图像处理中的典型退化复原任务,核心在于应对银盐氧化、霉斑侵蚀、划痕叠加等多重物理损伤。其技术原理基于深度学习的退化建模与特征解耦机制,通过结构引导、人脸感知损失和可控保真度调节,在保留历史真实性的前提下实现高保真重建。相比传统Photoshop依赖人工经验与局部填充,深度学习方案具备跨样本稳定性、多退化协同处理能力及可复现性优势。当前主流应用聚焦于家庭影像抢救、摄影工作室批量修复与胶片数字化归档等场景,其中CodeFormer凭借轻量结构引导与CPU友好特性,成为泛黄老照片修复的高性价比首选

weixin_30619101的博客 308

将雷达连接到树莓派的串口

dev/ttyAMA0 是树莓派的主要 UART(用于 GPIO 引脚 8 (TXD) 和 10 (RXD))。通常,树莓派使用 GPIO 引脚 8 (TXD) 和 10 (RXD) 进行串口通信。树莓派上通常有两个串口:/dev/ttyS0 和 /dev/ttyAMA0。你需要确认雷达的 TX 和 RX 引脚,并与树莓派的对应引脚连接。VCC(电源)连接到树莓派的 3.3V 或 5V(根据雷达的要求)MR(Receive)连接到树莓派的 GPIO 8 (TXD)GND(地)连接到树莓派的 GND。

qq_45943628的博客 1465

高分辨率图像合成:融合Transformer表达力与CNN效率的混合架构

高分辨率图像合成是计算机视觉落地的关键瓶颈,其本质是在全局语义理解与局部细节建模之间取得平衡。Transformer凭借自注意力机制具备强大的长程依赖建模能力,但计算复杂度随分辨率平方增长,导致显存爆炸与推理延迟剧增;而CNN在像素级纹理生成和硬件部署上具有天然效率优势,却易丢失高层语义结构。将二者协同而非替代,已成为Adobe Firefly、NVIDIA GauGAN3等工业级系统的核心范式。本文聚焦于‘Transformers表达力’与‘CNN效率’的工程化融合路径,解析如何通过三阶段渐进式生成、语义

anheku1562的博客 485

VxWorks环境搭建与学习:vxworks6.8和workbench3.2

VxWorks操作系统是美国WindRiver公司于1983年设计开发的一种嵌入式实时操作系统(RTOS),是嵌入式开发环境的关键组成部分。良好的持续能力、高性能的内核以及友好的用户开发环境,在嵌入式实时操作系统领域占据一席之地。它以其良好的可靠性和卓越的实时性被广泛地应用在通信、军事、航空、航天等高精尖技术及实时性要求极高的领域中,如卫星通讯、军事演习、弹道制导、飞机导航等。

qq_41854911的博客 8469

LinuxCNC 源码解析:从界面开发到硬件交互的实战指南

本文深入解析LinuxCNC源码架构,从用户界面开发到硬件交互的实战指南。涵盖GTK与Tkinter界面开发、HAL硬件抽象层、G代码解析器改造及运动控制核心等关键模块,提供定制化开发技巧与性能优化方案,助力开发者高效掌握数控系统开发。

weixin_27066963的博客 190

2024计算机视觉前沿实践指南:5篇可落地的CV论文实操解析

计算机视觉(CV)已从理论突破迈向工程落地深水区。当前核心挑战在于模型泛化性差、推理延迟高、标注依赖强及跨域适应难。扩散模型、视觉语言模型(VLM)、图像/视频编辑、手-物交互(HOI)等方向正通过引入物理约束、归纳偏置和轻量架构重构技术路径。例如,分阶段去噪显著提升生成效率,指令微调降低多模态对齐门槛,BRDF一致性增强编辑真实感——这些方法共同指向‘用先验知识换计算效率’的新范式。本文聚焦2024年最新开源、可复现、带硬指标的5篇论文,覆盖A100/RTX 4090/Jetson Orin多平台实测,提

an4455的博客 470

Imatest-Dot Pattern

本文介绍了DotPattern模块测试方法,用于评估相机拍摄点阵图的表现。测试主要关注两点:色彩准确性(是否存在彩色噪点和杂边)和摩尔纹现象。测试步骤包括导入图片、选择分析区域、微调ROI、选择点状图类型和分析保存结果。测试结果包含横向色差、光学畸变和SMIATV畸变等数据,其中0.115%的色差值和0.004度的网格旋转角度表明相机表现良好,但-10.4%的SMIATV畸变显示存在明显桶形畸变。文章还提供了各项指标的具体解读标准。

weixin_64706696的博客 1351

图映ImgIng为何不设收费入口?免费策略背后的AI修图新范式

AI图像处理工具正从‘功能分级付费’转向‘信任驱动价值交换’。其核心原理在于通过多尺度语义分割、动态光照重建与跨模态风格迁移等底层技术,将专业修图能力封装为零参数自然语言交互,大幅降低用户的时间成本、学习成本与信任成本。这种技术深度支撑的极致免费体验,已超越传统Freemium模型,成为中小团队视觉工作流嵌入与个人创作者长期依赖的关键基础设施。图映ImgIng正是这一范式的典型代表,它用稳定开放的AI修图能力重构了工具类产品的价值衡量标准。

weixin_34364071的博客 412

YOLO11改进大全:卷积层、轻量化、注意力机制、损失函数、Backbone、SPPF、Neck、检测头全方位优化汇总

YOLOv11改进大全:卷积层、轻量化、注意力机制、损失函数、Backbone、SPPF、Neck、检测头全方位优化汇总

魔改工程师的博客 4527

从零开始之驱动发开、linux驱动(七、字符驱动之led最终版)

我们上一接用的gpiolib非常杂乱,使用了两种 1.各硬件厂商实现的,如s3c_gpio_cfgpin(S5PV210_GPJ0(5), S3C_GPIO_SFN(1)) 2.系统公共的,如gpio_set_value(S5PV210_GPJ0(4),val) 其实公共的也是对各硬件厂商的封装,用那个都可以,个人建议用系统公共的,应为这样换芯片厂商,不影响任何使用。   2.错误处理基...

To_run_away的博客 2387

深度学习0:数学知识:正态分布1

步骤内容结果1引入e−x2e^{-x^2}e−x2钟形曲线2算它的面积I∫−∞∞e−x2dxI∫−∞∞​e−x2dx3平方变成二重积分I2I^2I24换极坐标简化计算5算出I2πI^2 = \piI2πIπIπ​6代入σ\sigmaσ和μ\muμ做变量替换得到σ2πσ2π​7除以它归一化得到正态分布公式2π2π​不是随便写的,它来自高斯积分∫−∞∞e−x2。

tellmewhoisi的博客 199

高级用法:LangChain 安全防护(Guardrails)

在调用开始时校验请求,可做关键词过滤、身份校验、限流等。= "human":return {}],# 使用],= "human":return {}],

Long_像风一样自由 316

【原创深度学习项目】基于深度学习的建筑工地防护装备检测系统|安全帽与反光衣检测系统(RT-DETR-L 模型)

本文设计并实现了一套基于RT-DETR-L的建筑工地防护装备检测系统,采用Django+Vue前后端架构,实现对安全帽、反光背心等7类装备及“无PPE”“未戴安全帽”等缺失状态的精准识别。系统支持图片、视频、摄像头实时检测,具备参数可调、数据可视化与统计分析功能,有效解决小目标漏检、误检及管理难问题,助力智慧工地安全管理落地。文末可获取源码。

qq_1445749146的博客 218

英飞凌Tricore实战系列导读

笔者结合项目应用经验对Tricore MCU的开发及应用经验进一步总结,成人达己,渡己渡人。

initiallizer的博客 3791

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

图解循环神经网络(RNN)

RNN(循环神经网络)就是“带记忆的网络”:让网络拥有一块“记忆”,在处理当前数据时,能同时参考之前看过的内容;它不一次性看完全部输入,而是像人读书一样,一个字一个字地读,每读一个字就更新一次脑子里的记忆,然后用这份记忆去理解当前这个字。但便签一直在被改写,新信息不断挤占旧信息,传到第 40 步时,“巴黎”的痕迹已经淡得几乎没了。读到“贵”的时候,网络并没有重新去查“好看”这个词——但“好看”的影响已经融在了便签里(那个 1.5)。这三个困难的共同点:答案不在当前这一个字里,而在“前面那串东西”里。

技术管理两手抓,两手都要硬. 370

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

上一篇: 一个DTI 宝藏软件 DSI Studio
下一篇: C++ OpenCV 使用 yolov8 onnx 的详细代码记录
发光的小豆芽
博客等级 码龄12年 2803粉丝 73原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值