pytorch实战9:基于pytorch简单实现u-net

news2024/11/15 22:51:02

基于pytorch简单实现u-net

前言

​ 最近在看经典的卷积网络架构,打算自己尝试复现一下,在此系列文章中,会参考很多文章,有些已经忘记了出处,所以就不贴链接了,希望大家理解。

​ 完整的代码在最后。

本系列必须的基础

​ python基础知识、CNN原理知识、pytorch基础知识

本系列的目的

​ 一是帮助自己巩固知识点;

​ 二是自己实现一次,可以发现很多之前的不足;

​ 三是希望可以给大家一个参考。

参考资料

​ 来自b站大佬的项目库:

b站链接: https://space.bilibili.com/18161609
GitHub链接:https://github.com/WZMIAOMIAO/deep-learning-for-image-processing

目录结构

文章目录

    • 基于pytorch简单实现u-net
      • 1. 前言:
      • 2. 数据集介绍与下载:
      • 3. 项目流程说明:
      • 4. 数据加载器:
      • 5. 网络架构:
      • 6. 损失函数:
      • 7. 预处理方法:
      • 8. 训练:
      • 9. 预测:
      • 10. 总结:

1. 前言:

​ 本篇文章打算简单实现一下图像分割中的u-net网络,并讲解一下实现的流程和思路。其中,代码有很多的参考别人的地方,有些方法甚至是直接拷贝过来用的(有现成的你不用—狗头保命),特此申明一下。当然,虽然有些代码是参考的,但是自己把主要的数据加载器、网络、训练等自己实现了一次。

另外,这个实现不像之前实现分类网络,这个实现更复杂,因此代码讲解上会偏少,主要把思路理清

2. 数据集介绍与下载:

下载

​ u_net主要应用于医学数据上的分割,这里使用的数据集名为DRIVE,可以从下面的链接中下载:

链接:https://pan.baidu.com/s/1YjW9wOm-sLC5oDs8T6a9bw 
提取码:0b0w 

介绍

​ 图像目录结构:

├─test
│  ├─1st_manual
│  ├─2nd_manual
│  ├─images
│  └─mask
└─training
    ├─1st_manual
    ├─images
    └─mask

​ 其中:

  • manual是手工划分的真实图像分隔,黑色(0)为背景,白色(255)为对象
  • images是真实图像,大小都为565*584*3,彩色的
  • mask为一个蒙版,即黑色区域为不感兴趣的区域,白色区域为感兴趣的区域

在这里插入图片描述

3. 项目流程说明:

​ 整体流程如下:

在这里插入图片描述

  • 首先,定义数据加载器,加载数据,并返回image和mask(这里的mask与数据集中的mask不同,不同点在下面的4小节中介绍
  • 接着,定义网络结构、损失函数,并开始训练
  • 保存训练的权重参数,用于后期的预测

​ **提前说明:**当你把我写的代码下载后,需要修改路径参数,因为我的数据集是放在了和项目不同的文件夹下的。

​ 另外,我的整个项目文件夹结构为:

├─network_files
│  └─u_net.py
├─save_weights
├─utils
│  └─Loss.py
|  └─transforms.py
└─My_Dataset.py
└─train,py
└─predict.py

4. 数据加载器:

​ 这次的数据加载器实现起来很简单,主要思路如下:

在这里插入图片描述

​ 其中,我们需要特别对mask进行一定的处理:

在这里插入图片描述

​ 根据上述思路,可以完成代码(看注释):

import os
import torch
from torch import nn
from torch.utils.data import Dataset
from PIL import Image
import numpy as np

class My_Dataset(Dataset):
    def __init__(self,root,train=True,transforms=None):
        '''
        :param root: 路径,比如.\data
        :param train: 加载的数据集为训练还是测试集,默认为训练集
        :param transforms:  预处理方法
        '''
        super(My_Dataset, self).__init__()
        # 获取基础路径
        self.flag = 'training' if train else 'test'
        self.root = os.path.join(root,'DRIVE',self.flag) # .\data\DRIVE\training
        # 获取所需的路径
        self.img_name = [i for i in os.listdir(os.path.join(self.root,'images'))] # '21_training.tif'
        self.img_list = [os.path.join(self.root,'images',i) for i in self.img_name] # .\data\DRIVE\training\images\21_training.tif
        self.manual = [os.path.join(self.root,'1st_manual',i.split('_')[0])+'_manual1.gif' for i in self.img_name ] # .\data\DRIVE\training\1st_manual\21_manual1.gif
        self.roi_mask = [os.path.join(self.root,'mask',i.split('_')[0]+'_'+self.flag+'_mask.gif') for i in self.img_name] # .\data\DRIVE\training\mask\21_training_mask.gif
        # 初始化其它变量
        self.transforms = transforms

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

    def __getitem__(self, idx):
        # 打开图像(RGB模式)、manual(L-灰度图)、roi_mask(L-灰度图)
        image = Image.open(self.img_list[idx])
        manual = Image.open(self.manual[idx]).convert('L')
        roi_mask = Image.open(self.roi_mask[idx]).convert('L')
        # 对图像进行处理
        # 将对象设置为1,背景设置为0,不感兴趣的区域设置为255
        manual = np.array(manual) / 255  # 白色(对象)为255,变为了1
        roi_mask = 255 - np.array(roi_mask) # 不感兴趣区域为黑色(0),变为了255 ; 感兴趣为白色(255),变为了0
        mask = np.clip(manual+roi_mask,a_min=0,a_max=255)
        mask = Image.fromarray(mask) # 转为PIL,方便做预处理操作
        if self.transforms is not None:
            image,mask = self.transforms(image,mask)
        return image,mask

5. 网络架构:

​ 这里用b站up主的图来描述网络架构:

在这里插入图片描述

​ 这里要注意几点:

  • 原论文中每层双卷积后,图像尺寸都会减小,导致最后输出图像大小与输入大小不一致,这样一来,想要得到最终的分割图需要做一定的后处理。而我们这里双卷积不改变图像尺寸大小,只由下采样和上采样改变,如此一来,最终输出大小和输入大小一致,方便最后出结果
  • 下采样最后一层的双卷积,不改变图像通道数,仍然为512。是因为我们上采样用双线性插值,不改变通道数,如此一来,上采样后仍然为512,可以方便的和之前的进行拼接
  • 最终输出的通道数为2,是因为这里进行二分类,一个为背景,一个为分割对象

​ 基于上述结构图,可以理清实现网络架构的思路图:

在这里插入图片描述

​ 可以轻松写出下面的代码:

import torch
from torch import nn
from torch.nn import functional as F

# 上采样+拼接
class Up(nn.Module):
    def __init__(self,in_channels,out_channels,bilinear=True):
        '''
        :param in_channels: 输入通道数
        :param out_channels:  输出通道数
        :param bilinear: 是否采用双线性插值,默认采用
        '''
        super(Up, self).__init__()
        if bilinear:
            # 双线性差值
            self.up = nn.Upsample(scale_factor=2,mode='bilinear',align_corners=True)
            self.conv = doubleConv(in_channels,out_channels,in_channels//2) # 拼接后为1024,经历第一个卷积后512
        else:
            # 转置卷积实现上采样
            # 输出通道数减半,宽高增加一倍
            self.up = nn.ConvTranspose2d(in_channels,out_channels//2,kernel_size=2,stride=2)
            self.conv = doubleConv(in_channels,out_channels)

    def forward(self,x1,x2):
        # 上采样
        x1 = self.up(x1)
        # 拼接
        x = torch.cat([x1,x2],dim=1)
        # 经历双卷积
        x = self.conv(x)
        return x

# 双卷积层
def doubleConv(in_channels,out_channels,mid_channels=None):
    '''
    :param in_channels: 输入通道数 
    :param out_channels: 双卷积后输出的通道数
    :param mid_channels: 中间的通道数,这个主要针对的是最后一个下采样和上采样层
    :return: 
    '''
    if mid_channels is None:
        mid_channels = out_channels
    layer = []
    layer.append(nn.Conv2d(in_channels,mid_channels,kernel_size=3,padding=1,bias=False))
    layer.append(nn.BatchNorm2d(mid_channels))
    layer.append(nn.ReLU(inplace=True))
    layer.append(nn.Conv2d(mid_channels,out_channels,kernel_size=3,padding=1,bias=False))
    layer.append(nn.BatchNorm2d(out_channels))
    layer.append(nn.ReLU(inplace=True))
    return nn.Sequential(*layer)

# 下采样
def down(in_channels,out_channels):
    # 池化 + 双卷积
    layer = []
    layer.append(nn.MaxPool2d(2,stride=2))
    layer.append(doubleConv(in_channels,out_channels))
    return nn.Sequential(*layer)

# 整个网络架构
class U_net(nn.Module):
    def __init__(self,in_channels,out_channels,bilinear=True,base_channel=64):
        '''
        :param in_channels: 输入通道数,一般为3,即彩色图像
        :param out_channels: 输出通道数,即网络最后输出的通道数,一般为2,即进行2分类
        :param bilinear: 是否采用双线性插值来上采样,这里默认采取
        :param base_channel: 第一个卷积后的通道数,即64
        '''
        super(U_net, self).__init__()
        self.in_channels = in_channels
        self.out_channels = out_channels
        self.bilinear = bilinear

        # 输入
        self.in_conv = doubleConv(self.in_channels,base_channel)
        # 下采样
        self.down1 = down(base_channel,base_channel*2) # 64,128
        self.down2 = down(base_channel*2,base_channel*4) # 128,256
        self.down3 = down(base_channel*4,base_channel*8) # 256,512
        # 最后一个下采样,通道数不翻倍(因为双线性差值,不会改变通道数的,为了可以简单拼接,就不改变通道数)
        # 当然,是否采取双线新差值,还是由我们自己决定
        factor = 2  if self.bilinear else 1
        self.down4 = down(base_channel*8,base_channel*16 // factor) # 512,512
        # 上采样 + 拼接
        self.up1 = Up(base_channel*16 ,base_channel*8 // factor,self.bilinear) # 1024(双卷积的输入),256(双卷积的输出)
        self.up2 = Up(base_channel*8 ,base_channel*4 // factor,self.bilinear)
        self.up3 = Up(base_channel*4 ,base_channel*2 // factor,self.bilinear)
        self.up4 = Up(base_channel*2 ,base_channel,self.bilinear)
        # 输出
        self.out = nn.Conv2d(in_channels=base_channel,out_channels=self.out_channels,kernel_size=1)

    def forward(self,x):
        x1 = self.in_conv(x)
        x2 = self.down1(x1)
        x3 = self.down2(x2)
        x4 = self.down3(x3)
        x5 = self.down4(x4)
        # 不要忘记拼接
        x = self.up1(x5, x4)
        x = self.up2(x, x3)
        x = self.up3(x, x2)
        x = self.up4(x, x1)
        out = self.out(x)

        return {'out':out}

6. 损失函数:

​ 其实,完成了数据加载、网络结构后,其实基本上已经完成了大部分内容了,因为图像分割就是像素级的分类,因此损失函数完全可以简简单单的用个交叉熵损失函数就行了。

​ 但是,源码上不仅使用交叉熵作为损失函数,还使用了dice作为损失函数(度量两个集合的相似性)。

​ 其中,dice_loss的计算公式为:

在这里插入图片描述

​ 具体的实现,我就不多说了,因为不是重点,感兴趣的可以看看注释,不感兴趣的可以直接拿过来用:

import torch
from torch import nn

# dice : 度量两个集合的相似性

# loss
def criterion(inputs, target, loss_weight=None, num_classes: int = 2, dice: bool = True, ignore_index: int = -100):
    losses = {}
    for name, x in inputs.items():
        # x = torch.Size([4, 2, 480, 480]) ,4为batch,2表示输出通道数,480*480表示和原图大小一样
        # target = torch.Size([4, 480, 480]) 真实mask值
        # ignore_index = 255,表示忽略那些不感兴趣的部分
        loss = nn.functional.cross_entropy(x, target, ignore_index=ignore_index, weight=loss_weight)
        if dice is True:
            # 计算每个类别的dice(背景、前景),然后求均值
            # 因此需要为每个类别构建一个target
            dice_target = build_target(target, num_classes, ignore_index)
            loss += dice_loss(x, dice_target, multiclass=True, ignore_index=ignore_index)
        losses[name] = loss

    if len(losses) == 1:
        return losses['out']

    return losses['out'] + 0.5 * losses['aux']


def build_target(target: torch.Tensor, num_classes: int = 2, ignore_index: int = -100):
    """build target for dice coefficient"""
    dice_target = target.clone()
    if ignore_index >= 0:
        # 将255的区域设置为0,因为不感兴趣的不需要计算dice,因此先设置为0
        ignore_mask = torch.eq(target, ignore_index)
        dice_target[ignore_mask] = 0
        # target转为one-hot编码形式 [1 0]表示一个类别 and [0 1]表示一个类别
        dice_target = nn.functional.one_hot(dice_target, num_classes).float()
        # 将255的值又填充回去
        dice_target[ignore_mask] = ignore_index
    else:
        dice_target = nn.functional.one_hot(dice_target, num_classes).float()

    # [N, H, W] -> [N, H, W, C]
    return dice_target.permute(0, 3, 1, 2)


def dice_coeff(x: torch.Tensor, target: torch.Tensor, ignore_index: int = -100, epsilon=1e-6):
    # 计算一个batch中所有图片某个类别的dice_coefficient
    d = 0.
    batch_size = x.shape[0]
    # 遍历每张图片
    for i in range(batch_size):
        x_i = x[i].reshape(-1)
        t_i = target[i].reshape(-1)
        if ignore_index >= 0:
            # 找出mask中不为ignore_index的区域
            roi_mask = torch.ne(t_i, ignore_index)
            x_i = x_i[roi_mask]
            t_i = t_i[roi_mask]
        inter = torch.dot(x_i, t_i)
        sets_sum = torch.sum(x_i) + torch.sum(t_i)
        if sets_sum == 0:
            sets_sum = 2 * inter

        # 计算dice
        d += (2 * inter + epsilon) / (sets_sum + epsilon)

    return d / batch_size


def multiclass_dice_coeff(x: torch.Tensor, target: torch.Tensor, ignore_index: int = -100, epsilon=1e-6):
    """Average of Dice coefficient for all classes"""
    dice = 0.
    # 遍历每个channel,计算每个类别的dice
    for channel in range(x.shape[1]):
        dice += dice_coeff(x[:, channel, ...], target[:, channel, ...], ignore_index, epsilon)

    # 求均值
    return dice / x.shape[1]


def dice_loss(x: torch.Tensor, target: torch.Tensor, multiclass: bool = False, ignore_index: int = -100):
    # 在channel方向做softmax
    x = nn.functional.softmax(x, dim=1)
    # 选择采用的方法
    fn = multiclass_dice_coeff if multiclass else dice_coeff
    return 1 - fn(x, target, ignore_index=ignore_index)

7. 预处理方法:

​ 这个文件,我是直接拷贝过来的,因为里面实现的方法功能和torchvision.transforms一致,这里之所以重新定义一次,是因为我们需要同时处理图像和mask两个值。

​ 感兴趣的可以自己研究一下:

import numpy as np
import random
import torch
from torchvision import transforms as T
from torchvision.transforms import functional as F

# 填充
def pad_if_smaller(img, size, fill=0):
    # 获取图像最短边
    min_size = min(img.size)
    # 如果最短边小于给定的尺寸
    if min_size < size:
        # 用fill值进行填充
        ow, oh = img.size
        padh = size - oh if oh < size else 0
        padw = size - ow if ow < size else 0
        img = F.pad(img, (0, 0, padw, padh), fill=fill)
    return img


# 将所有预处理方法集合在一起,然后运行
class Compose(object):
    def __init__(self, transforms):
        self.transforms = transforms

    def __call__(self, image, target):
        for t in self.transforms:
            image, target = t(image, target)
        return image, target

# 随机缩放
class RandomResize(object):
    def __init__(self, min_size, max_size=None):
        self.min_size = min_size
        if max_size is None:
            max_size = min_size
        self.max_size = max_size

    def __call__(self, image, target):
        size = random.randint(self.min_size, self.max_size)
        # 这里size传入的是int类型,所以是将图像的最小边长缩放到size大小
        image = F.resize(image, size)
        # 这里的interpolation注意下,在torchvision(0.9.0)以后才有InterpolationMode.NEAREST
        # 如果是之前的版本需要使用PIL.Image.NEAREST
        target = F.resize(target, size, interpolation=T.InterpolationMode.NEAREST)
        return image, target

# 水平翻转
class RandomHorizontalFlip(object):
    def __init__(self, flip_prob):
        self.flip_prob = flip_prob

    def __call__(self, image, target):
        if random.random() < self.flip_prob:
            image = F.hflip(image)
            target = F.hflip(target)
        return image, target

# 垂直翻转
class RandomVerticalFlip(object):
    def __init__(self, flip_prob):
        self.flip_prob = flip_prob

    def __call__(self, image, target):
        if random.random() < self.flip_prob:
            image = F.vflip(image)
            target = F.vflip(target)
        return image, target

# 随机裁剪
class RandomCrop(object):
    def __init__(self, size):
        self.size = size

    def __call__(self, image, target):
        image = pad_if_smaller(image, self.size)
        target = pad_if_smaller(target, self.size, fill=255)
        crop_params = T.RandomCrop.get_params(image, (self.size, self.size))
        image = F.crop(image, *crop_params)
        target = F.crop(target, *crop_params)
        return image, target


# 中心裁剪
class CenterCrop(object):
    def __init__(self, size):
        self.size = size

    def __call__(self, image, target):
        image = F.center_crop(image, self.size)
        target = F.center_crop(target, self.size)
        return image, target


# 转为tensor
class ToTensor(object):
    def __call__(self, image, target):
        image = F.to_tensor(image)
        target = torch.as_tensor(np.array(target), dtype=torch.int64)
        return image, target


# 归一化
class Normalize(object):
    def __init__(self, mean, std):
        self.mean = mean
        self.std = std

    def __call__(self, image, target):
        image = F.normalize(image, mean=self.mean, std=self.std)
        return image, target

8. 训练:

​ 这里,简单的实现了一下训练的方法,主要的实现思路如下:

在这里插入图片描述

​ 按照上述思路,可以实现代码:

from network_files.u_net import U_net
from utils import transforms as T
from My_Dataset import My_Dataset
from utils import Loss

import torch
from torch import nn
from torch.utils.data import DataLoader
from torch import optim

# 训练的预处理方法获取
class SegmentationPresetTrain:
    def __init__(self, base_size, crop_size, hflip_prob=0.5, vflip_prob=0.5,
                 mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)):
        # 初始化
        min_size = int(0.5 * base_size)
        max_size = int(1.2 * base_size)

        # 随机裁剪
        trans = [T.RandomResize(min_size, max_size)]
        if hflip_prob > 0:
            trans.append(T.RandomHorizontalFlip(hflip_prob))
        if vflip_prob > 0:
            trans.append(T.RandomVerticalFlip(vflip_prob))
        trans.extend([
            T.RandomCrop(crop_size),
            T.ToTensor(),
            T.Normalize(mean=mean, std=std),
        ])
        self.transforms = T.Compose(trans)

    def __call__(self, img, target):
        return self.transforms(img, target)

# 获取预处理方法
def get_transform(train, mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)):
    base_size = 565
    crop_size = 480

    if train:
        # 训练模式获取的预处理方法
        return SegmentationPresetTrain(base_size, crop_size, mean=mean, std=std)


def main():
    # 设置基本参数信息
    # device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
    # 如果GPU不够用,可以用cpu
    device = torch.device('cpu')
    batch_size= 4
    epoch = 20
    num_classes = 2 # 1(object)+1(background)
    if num_classes == 2:
        # 设置cross_entropy中背景和前景的loss权重(根据自己的数据集进行设置)
        loss_weight = torch.as_tensor([1.0, 2.0], device=device)
    else:
        loss_weight = None
    # 加载数据
    mean = (0.709, 0.381, 0.224)
    std = (0.127, 0.079, 0.043)
    train_dataset = My_Dataset('../data',train=True,transforms=get_transform(train=True, mean=mean, std=std))
    val_dataset = My_Dataset('../data',train=False,transforms=get_transform(train=False, mean=mean, std=std))
    train_loader = DataLoader(train_dataset,batch_size=batch_size,shuffle=True)
    val_loader = DataLoader(val_dataset,batch_size=batch_size,shuffle=True)
    # 创建模型
    model = U_net(3,num_classes) # 输入通道数,输出通道数
    model.to(device)
    # 定义优化器
    params = [p for p in model.parameters() if p.requires_grad] # 定义需要优化的参数
    sgd = optim.SGD(params,lr=0.01,momentum=0.9,weight_decay=1e-4)
    # 开始训练
    model.train()
    for e in range(epoch):
        loss_temp = 0
        for i,(image,mask) in enumerate(train_loader):
            image,mask = image.to(device),mask.to(device)
            output = model(image)
            loss = Loss.criterion(output, mask, loss_weight, num_classes=num_classes, ignore_index=255)
            loss_temp += loss.item()
            sgd.zero_grad()
            loss.backward()
            sgd.step()
        print(f'第{e+1}个epoch,平均损失loss={loss_temp/(i+1)}')
    # 保存权重
    name = 'save_weights/u_net.pth'
    torch.save(model.state_dict(),name)



if __name__ == '__main__':
    main()

​ 运行结果展示,这里就展示一下正常运行的结果,大家可以自己去运行:

在这里插入图片描述

9. 预测:

​ 上面训练完后,就保存了相关的参数值,我们可以利用它来进行预测。

​ 预测实现的思路如下:
在这里插入图片描述

​ 代码如下:

import os
import time

import torch
from utils import transforms as  T
import numpy as np
from PIL import Image

from network_files.u_net import U_net

def main():
    # 设置基本参数,需要自己改变路径参数
    classes = 1
    weights_path = "./save_weights/u_net.pth"
    img_path = "../data/DRIVE/test/images/03_test.tif" # ,另外可以改变预测的图像对象
    roi_mask_path = "../data/DRIVE/test/mask/03_test_mask.gif"
    # 这个是图像归一化的参数
    mean = (0.709, 0.381, 0.224)
    std = (0.127, 0.079, 0.043)
    # 获取设备
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    # 创建模型,输入通道数为3,输出为2
    model = U_net(3,classes+1)
    # 加载权重
    model.load_state_dict(torch.load(weights_path))
    model.to(device)
    # 打开相关路径
    roi_img = Image.open(roi_mask_path).convert('L')
    original_img = Image.open(img_path).convert('RGB')
    # 预处理
    data_transform = T.Compose([T.RandomCrop(480),
                                         T.ToTensor(),
                                         T.Normalize(mean=mean, std=std)])
    img,roi_img = data_transform(original_img,roi_img)
    # 将三维的数据转为四维,因为需要添加batch这个维度
    img = torch.unsqueeze(img, dim=0)
    # 将roi转为array,方便后期处理
    roi_img = np.array(roi_img)
    model.eval()  # 进入验证模式
    with torch.no_grad():
        # 初始化一个全黑的图像,后期将白色的添加进去,就可以得到预测的mask值
        img_height, img_width = img.shape[-2:]
        init_img = torch.zeros((1, 3, img_height, img_width), device=device)
        model(init_img)
        output = model(img.to(device))
        prediction = output['out'].argmax(1).squeeze(0)
        prediction = prediction.to("cpu").numpy().astype(np.uint8)
        # 将前景对应的像素值改成255(白色)
        prediction[prediction == 1] = 255
        # 将不敢兴趣的区域像素设置成0(黑色)
        prediction[roi_img == 0] = 0
        mask = Image.fromarray(prediction)
        mask.save("test_result.png")

if __name__ == '__main__':
    main()

​ 结果展示:

在这里插入图片描述

​ 说明:由于预测的时候,我们将图像裁剪为了480*480,因此和真实结果看起来大小有一定的差距

​ 从图中,也可以看出,那些比较大的血管,基本都分割出来了,而较细微的血管,部分没有分割出来。综合来看,效果还是不错。

10. 总结:

​ 这里只是简单的实现了u-net网络,还有很多地方可以改进,比如:

  • 学习率没有调整
  • 没有添加验证集部分
  • 预测仅仅只是针对一张图片预测,没有实现集成的预测函数

​ 当然,代码写到这里,基本上也实现了我预期的内容了,再次感谢大佬们的开源工作,让我可以轻松copy一些代码过来。

全部代码和训练好的参数:

链接:https://pan.baidu.com/s/1Ro9MS9OnxJTUkiJLzQrcmA 
提取码:e3ol 

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

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

相关文章

组件等比例放大——scale和zoom

scale和zoom的区别 zoom的缩放是相对于左上角的&#xff1b;而scale默认是居中缩放&#xff0c;可以通过transform-origin修改基准点zoom的缩放改变了元素占据的空间大小&#xff1b;而scale的缩放占据的原始尺寸不变&#xff0c;页面布局不会发生变化。对文字的缩放规则不一致…

K8s基础2——部署单Master节点K8s集群、切换containerd容器运行时、基本命令

文章目录 一、部署K8S集群方式二、kubeadm工具搭建K8s集群2.1 资源配置2.2 服务器规划2.3 搭建流程2.3.1 操作系统初始化2.3.2 安装docker容器引擎2.3.3 安装cri-dockerd2.3.4 安装kubeadm&#xff0c;kubelet和kubectl2.3.5 master节点初始化2.3.6 加入node节点2.3.7 部署网络…

《花雕学AI》29:5秒钟就能为你的想法想出新点子?ChatGPT新点子指令模型告诉你怎么做

引言 你有没有遇到过这样的情况&#xff0c;你想出了一个想法&#xff0c;但是不知道怎么扩展或改进它&#xff1f;你有没有想过有一个工具&#xff0c;可以帮你在短时间内为你的想法生成各种新的点子&#xff1f;如果你有这样的需求&#xff0c;那么你一定要了解ChatGPT。 C…

Verilog 锁相环参数动态自动生成,Xilinx MMCM 和 PLL 动态配置频率

版权声明&#xff1a;本文为博主原创文章&#xff0c;遵循 CC 4.0 BY-SA 版权协议&#xff0c;转载请附上原文出处链接和本声明。 本文链接&#xff1a;https://blog.csdn.net/qq_46621272/article/details/130484100 Verilog 锁相环参数动态自动生成&#xff0c;Xilinx MMCM 和…

服务器中了勒索病毒,升级后的Malox勒索病毒特征,勒索病毒解密数据恢复

Mallox勒索病毒是网络上较为流行的勒索病毒&#xff0c;但是随着黑客加密技术的不断升级&#xff0c;Mallox勒索病毒的新升级版本Malox勒索病毒已经开始出现。Malox勒索病毒是一种最近在网络上广泛传播的恶意软件&#xff0c;其感染方式多种多样&#xff0c;主要以加密受害人的…

【五一创作】版本控制-从零开始学Git-02 Git中的基本概念与工作流程

前言 前面学习了版本控制系统和分布式版本控制系统-Git的相关入门知识【五一创作】版本控制-从零开始学Git-01什么是Git 一、Git中的最基本概念 注意:.git目录是一个隐藏文件夹&#xff0c;默认不可见,需要设置后才能显示出来。 二、Git状态与结构关系 2.1 三种状态 modif…

Zigbee 无线串口通信模块( DL-22 )

文章目录 一、DL-22简介二、模块配置三、串口通信(透明传输) 一、DL-22简介 DL-22无线串口模块为串口转2.4G无线模块&#xff0c;可以通过无线将两个或者多个串口连接起来。串口发入模块的数据会被模块使用无线发出&#xff0c;收到无线数据的模块会将这个数据使用串口发出&am…

大数据分析案例-基于XGBoost算法构建居民收入分类预测模型

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

互联网明厨亮灶智慧监管算法 yolov7

互联网明厨亮灶智慧监管系统基于pythoyolov7网络模型AI视觉图像分析技术&#xff0c;互联网明厨亮灶智慧监管算法模型可以识别人员行为及穿戴是否合规&#xff0c;不穿厨师服、不按要求穿戴厨师帽或者佩戴口罩和手套、行为如违规在后厨抽烟、出现老鼠等情景。近几年来&#xff…

数字化转型导师坚鹏:数字化时代企业管理变革与创新营销

数字化时代企业管理变革与创新营销 课程背景&#xff1a; 很多饮水机企业存在以下问题&#xff1a; 不清楚数字化时代企业面临的机遇与挑战&#xff1f; 不知道如何利用大数据发现问题与机会&#xff1f; 不清楚如何进行精准的数字化营销&#xff1f; 不知道如何进行…

【ChatGPT】ChatGPT+MindShow三分钟生成PPT

Yan-英杰的主页 悟已往之不谏 知来者之可追 C程序员&#xff0c;2024届电子信息研究生 目录 第一步&#xff1a;获取PPT内容大概&#xff1a; 1.打开ChatGPT镜像 2.向他提问&#xff0c;提问格式如下&#xff0c;主题替换成你想获得的信息。比如某本书的拆解&#xff0c;…

数据可视化第二版-拓展-网约车分析案例

文章目录 数据可视化第二版-拓展-网约车分析案例竞赛介绍 1等奖作品-虾虾蟹蟹哈哈的作品引言&#xff1a;背景度量指标 & 结论过程数据与思考一、数据处理二、大盘存量数据一览1. 分城市关键指标变化情况2. 各项指标相关性分析 三、数量维度分析1. 城市间比较1.1 工作日 vs…

HTML和CSS

1、概述 超文本标记语言&#xff08;HyperText Markup Language&#xff09;简称&#xff1a;HTML&#xff1b;是一种用于创建网页的标准标记语言。HTML 不是一种编程语言&#xff0c;而是一种标记语言&#xff1b;HTML文档也叫做 web 页面。 运行后的浏览器页面 我的第一个标…

鲁大师4月安卓新机性能/流畅榜:ROG游戏手机7摘得性能桂冠 vivo登顶流畅榜

性能榜&#xff1a;ROG 游戏手机7众望所归&#xff0c;摘得性能桂冠 2023年4月的手机市场也是异常火爆&#xff0c;各大厂商纷纷推出自己的旗舰手机。骁龙8 Gen2仍然是厂商们的首选芯片&#xff0c;不论是“小米照相机”、vivo折叠机还是ROG 游戏机&#xff0c;他们都纷纷搭载了…

你不能不知道的string类的基础知识

文章目录 前言string类1. 为什么要学习string类1.1 C语言中的字符串不够好用 2. 标准库中的string类2.1 string类(了解)2.2 string类的常用接口说明2.3 string类对象的容量操作2.4string类对象的访问及遍历操作2.5 string类对象的修改操作2.6 string类非成员函数2.7 vs和g下str…

《花雕学AI》大揭秘:ChatGPT 如何让你的聊天机器人更智能、更有趣、更有用

你是否想过有一个可以和你聊天、陪你玩耍、帮你学习、给你创意的机器人&#xff1f;如果你的答案是肯定的&#xff0c;那么你一定会喜欢 ChatGPT。 ChatGPT 是一个基于 GPT-3 或者 GPT-4 技术的聊天机器人&#xff0c;可以与人类进行自然和流畅的对话。GPT-4 是目前最先进的自…

[Gitops--8]微服务前置中间件部署

微服务前置中间件部署 1. MySQL主从 1.1 创建持久化存储 使用project-admin账号 进入sangomall项目 [存储],[存储卷],sangomall-mysql-master-pvc storageclass创建详见K8s集群中部署KubeSphere 2.1章节 在k8s环境下可以看到这个sc rootks-master:~/yaml# kubectl get sc NA…

Cadence Allegro(1):手动PCB封装制作(以TYPE-C 16Pin为例)

Cadence Allegro 16.6&#xff08;1&#xff09;&#xff1a;手动PCB封装制作&#xff08;以TYPE-C 16Pin为例&#xff09; 前提摘要&#xff1a; PCB设计软件版本&#xff1a; 焊盘设计 &#xff1a;Pad Designer 16.6PCB设计 &#xff1a;PCB Editor 16.6 个人说明&#xf…

【HarmonyOS】自定义组件之ArkUI实现通用标题栏组件

【关键字】 标题栏、常用内置组件整合、ArkUI、自定义组件 1、写在前面 在上一篇文章中我们通过Java语言实现了一个通用的标题栏组件&#xff0c;有需要的可以看下&#xff0c;文章地址&#xff1a; 华为开发者论坛 现在很多朋友都已经转战ArkTS语言了&#xff0c;那么今天…

项目集的定义及管理

一、什么是项目集 项目集是相互关联且被协调管理的项目、子项目集和项目集活动&#xff0c;以便获得分别管理所无法获 得的效益。 以项目集的形式管理项目、子项目集及项目集活动能确保项目集组件的战略和工作计划根据各组 件的成果做出相应调整&#xff0c;或者按照发起组织的…