详解U-Net分割网络,提供详细代码技术细节及完整项目代码

一. 原始模型整体概述

U-Net网络是Ronneberger等人在2015年发表于计算机医学影像顶刊 MICCAI上的一篇论文,该论文首次提出了一种U型结构来进行图像的语义分割,论文的下载链接如下:U-Net: Convolutional Networks for Biomedical Image Segmentation

该网络赢得了2015年ISBI细胞跟踪挑战赛,一经提出便获得了巨大的关注,在计算机视觉领域的影响堪比凯明大神的ResNet网络,也正巧是同一年提出的,2015年真是一个AI的爆发之年,再后来便就是2017年的transformer网络提出了。

闲话少说,我们先来看一下U-Net网络的整体架构图,如下图所示:

该模型主要由两个部分组成,左边是编码器(文章里称作收缩路径,contracting path),右边是解码器( 文章里称作膨胀路径,expansive path)。

其中编码器的每一行遵守常规卷积神经网络的典型架构,由两个3x3的卷积(无padding操作)构成。每个卷积层的后面跟随一个整流线性单元ReLU和一个2x2的最大池化操作,步幅为2,用于下采样,在每个下采样的过程中将特征通道数翻倍

解码器的每一步都包含特征图的上采样,然后是一个将特征通道数量减半的2x2卷积(“上卷积”),与收缩路径中相应裁剪的特征map进行连接,以及两个3x3卷积,每个卷积后面都有一个ReLU。由于在每次卷积中边界像素的损失,所以需要裁剪(crop)。在最后一层,使用1x1卷积将每个64个分量的特征向量map到所需的类数。这个网络总共有23个卷积层。

二. U-Net模型的改进

通过上述模型架构设计,我们很容易发现这个架构图有一些不便之处,我们可以将模型改成以下结构来方便模型构建并改善模型性能。这也是当前最流行的U-Net构造方法。

1.将卷积层的padding设置为1。由于编码器和解码器中使用的卷积层都没有进行padding操作,卷积层核大小为3x3,stride为1,padding=0,这就是一个很普通的卷积层,卷积之后,特征图的高和宽会各减少2。具体计算步骤:N=(W-F+2P)/S+1,其中N为输出特征图的大小,W为输入特征图大小,F为卷积核大小,P为padding的像素个数,S为stride的大小。则N = (572-3+0)/1 +1= 570。

我们如果将padding设置为1,则卷积操作后特征图的高和宽都不会发生变化,这样的话就免去了拼接时由于尺寸不一致而产生额外的裁剪操作,改进之后可以直接concat,而不用crop操作。此外,这种做法可以保证输入的图像大小核输出的图像大小完全一致,避免了边缘缺失导致的误差。

2.在每个卷积层和ReLu之间加上Batch normalization,这个做可以加速网络训练并提升网络精度。

三.改进U-Net模型的完整代码 

from typing import Dict
import torch
import torch.nn as nn
import torch.nn.functional as F# 定义双卷积类
class DoubleConv(nn.Sequential):def __init__(self, in_channels, out_channels, mid_channels=None):if mid_channels is None:mid_channels = out_channelssuper(DoubleConv, self).__init__(# 定义padding为1,保持卷积后特征图的宽度和高度不变,具体计算N= (W-F+2P)/S+1nn.Conv2d(in_channels, mid_channels, kernel_size=3, padding=1, bias=False),# 加入BN层,提升训练速度,并提高模型效果nn.BatchNorm2d(mid_channels),nn.ReLU(inplace=True),# 第二个卷积层,同第一个nn.Conv2d(mid_channels, out_channels, kernel_size=3, padding=1, bias=False),# BN层nn.BatchNorm2d(out_channels),nn.ReLU(inplace=True))# 调用上面定义的双卷积类,定义下采样类
class Down(nn.Sequential):def __init__(self, in_channels, out_channels):super(Down, self).__init__(# 最大池化,步长为2,池化核大小为2,计算公式同卷积,则 N = (W-F+2P)/S+1,  N= (W-2+0)/4 + 1nn.MaxPool2d(2, stride=2),DoubleConv(in_channels, out_channels))class Up(nn.Module):def __init__(self, in_channels, out_channels):super(Up, self).__init__()# 调用转置卷积的方法进行上采样,使特征图的高和宽翻倍,out  =(W−1)×S−2×P+F,通道数减半self.up = nn.ConvTranspose2d(in_channels, in_channels // 2, kernel_size=2, stride=2)# 调用双层卷积类,通道数是否减半要看out_channels接收的值self.conv = DoubleConv(in_channels, out_channels)def forward(self, x1: torch.Tensor, x2: torch.Tensor) -> torch.Tensor:x1 = self.up(x1)# X的shape为[N, C, H, W],下面三行代码主要是为了保证x1和x2在维度为2和3的地方保持一致,方便cat操作不出错。diff_y = x2.size()[2] - x1.size()[2]diff_x = x2.size()[3] - x1.size()[3]# 增加padding操作,padding_left, padding_right, padding_top, padding_bottomx1 = F.pad(x1, [diff_x // 2, diff_x - diff_x // 2,diff_y // 2, diff_y - diff_y // 2])x = torch.cat([x2, x1], dim=1)x = self.conv(x)return x# 定义输出卷积类
class OutConv(nn.Sequential):def __init__(self, in_channels, num_classes):super(OutConv, self).__init__(nn.Conv2d(in_channels, num_classes, kernel_size=1))class UNet(nn.Module):def __init__(self,in_channels: int = 1,  # 默认输入图像的通道数为1,这里一般黑白图像为1,而彩色图像为3num_classes: int = 2,  # 默认输出的分类类别数为2# 默认基础通道为64,这里也可以改成大于2的任意2的次幂,不过越大模型的复杂度越高,参数越大,模型的拟合能力也就越强base_c: int = 64):super(UNet, self).__init__()self.in_channels = in_channelsself.num_classes = num_classes# 编码器的第1个双卷积层,不包含下采样过程,输入通道为1,输出通道数为base_c,这个值可以为64或者32        self.in_conv = DoubleConv(in_channels, base_c)# 编码器的第2个双卷积层,先进行下采样最大池化使得特征图高和宽减半,然后通道数翻倍        self.down1 = Down(base_c, base_c * 2)# 编码器的第3个双卷积层,先进行下采样最大池化使得特征图高和宽减半,然后通道数翻倍       self.down2 = Down(base_c * 2, base_c * 4)# 编码器的第4个双卷积层,先进行下采样最大池化使得特征图高和宽减半,然后通道数翻倍        self.down3 = Down(base_c * 4, base_c * 8)# 编码器的第5个双卷积层,先进行下采样最大池化使得特征图高和宽减半,然后通道数翻倍        self.down4 = Down(base_c * 8, base_c * 16)# 解码器的第1个上采样模块,首先进行一个转置卷积,使特征图的高和宽翻倍,通道数减半;# 对x1(x1可以到总的forward函数中可以知道它代指什么)进行padding,使其与x2的尺寸一致,然后在第1维通道维度进行concat,通道数翻倍。# 最后再进行一个双卷积层,通道数减半,高和宽不变。       self.up1 = Up(base_c * 16, base_c * 8)# 解码器的第2个上采样模块,操作同上        self.up2 = Up(base_c * 8, base_c * 4)# 解码器的第3个上采样模块,操作同上       self.up3 = Up(base_c * 4, base_c * 2)# 解码器的第4个上采样模块,操作同上        self.up4 = Up(base_c * 2, base_c)# 解码器的输出卷积模块,改变输出的通道数为分类的类别数        self.out_conv = OutConv(base_c, num_classes)def forward(self, x: torch.Tensor) -> Dict[str, torch.Tensor]:# 假设输入的特征图尺寸为[N, C, H, W],[4, 3, 480, 480],依次代表BatchSize, 通道数量,高,宽;   则输出为[4, 64, 480,480]x1 = self.in_conv(x)# 输入的特征图尺寸为[4, 64, 480, 480];  输出为[4, 128, 240,240]x2 = self.down1(x1)# 输入的特征图尺寸为[4, 128, 240,240];  输出为[4, 256, 120,120]x3 = self.down2(x2)# 输入的特征图尺寸为[4, 256, 120,120];  输出为[4, 512, 60,60]x4 = self.down3(x3)# 输入的特征图尺寸为[4, 512, 60,60];  输出为[4, 1024, 30,30]x5 = self.down4(x4)# 输入的特征图尺寸为[4, 1024, 30,30];  输出为[4, 512, 60, 60]x = self.up1(x5, x4)# 输入的特征图尺寸为[4, 512, 60,60];  输出为[4, 256, 120, 120]x = self.up2(x, x3)# 输入的特征图尺寸为[4, 256, 120,120];  输出为[4, 128, 240, 240]x = self.up3(x, x2)# 输入的特征图尺寸为[4, 128, 240,240];  输出为[4, 64, 480, 480]x = self.up4(x, x1)# 输入的特征图尺寸为[4, 64, 480,480];  输出为[4, 2, 480, 480]logits = self.out_conv(x)return {"out": logits}

模型代码解读:

1.模型更改:上面是一个完整的U-Net网络的结构,该代码相对于原文的实现主要更改了卷积层的padding和添加了BN层。改善了边缘信息丢失并且由于BN层的添加使模型效率更高;

2.代码主要由两部分组成,一个编码器,一个解码器。编码器由1个初始双卷积层和4个下采样层构成;解码器由4个上采样层和1个输出卷积层构成,其中上采样层由一个转置卷积,对x1进行padding操作,cat操作和1个双卷积层构成。这里解释一下为什么还要padding,如果你在训练数据和测试数据输入时严格按照480*480进行输入,则不需要padding,x1和x2的也会保持相同的形状。而大部分情况下,测试数据不会做裁剪再输入,所以很难保证输入时由于分辨率为奇数时,由于池化和转置卷积的宽和高的减半和翻倍原因导致尺寸有出入而不匹配。如果嫌麻烦,可以删掉padding部分的代码,然后将测试集的图像大小严格resize;

3.模型的执行过程按照UNet类的forward中的顺序执行。

四.模型的关键代码展示

1. transform.py  一些关于数据增强的功能函数类,其实这些类在torchvision中也有实现。

import numpy as np
import randomimport torch
from torchvision import transforms as T
from torchvision.transforms import functional as Fdef pad_if_smaller(img, size, fill=0):# 如果图像最小边长小于给定size,则用数值fill进行paddingmin_size = min(img.size)if min_size < size:ow, oh = img.sizepadh = size - oh if oh < size else 0padw = size - ow if ow < size else 0img = F.pad(img, (0, 0, padw, padh), fill=fill)return imgclass Compose(object):def __init__(self, transforms):self.transforms = transformsdef __call__(self, image, target):for t in self.transforms:image, target = t(image, target)return image, targetclass RandomResize(object):def __init__(self, min_size, max_size=None):self.min_size = min_sizeif max_size is None:max_size = min_sizeself.max_size = max_sizedef __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.NEARESTtarget = F.resize(target, size, interpolation=T.InterpolationMode.NEAREST)return image, targetclass RandomHorizontalFlip(object):def __init__(self, flip_prob):self.flip_prob = flip_probdef __call__(self, image, target):if random.random() < self.flip_prob:image = F.hflip(image)target = F.hflip(target)return image, targetclass RandomVerticalFlip(object):def __init__(self, flip_prob):self.flip_prob = flip_probdef __call__(self, image, target):if random.random() < self.flip_prob:image = F.vflip(image)target = F.vflip(target)return image, targetclass RandomCrop(object):def __init__(self, size):self.size = sizedef __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, targetclass CenterCrop(object):def __init__(self, size):self.size = sizedef __call__(self, image, target):image = F.center_crop(image, self.size)target = F.center_crop(target, self.size)return image, targetclass ToTensor(object):def __call__(self, image, target):image = F.to_tensor(image)target = torch.as_tensor(np.array(target), dtype=torch.int64)return image, targetclass Normalize(object):def __init__(self, mean, std):self.mean = meanself.std = stddef __call__(self, image, target):image = F.normalize(image, mean=self.mean, std=self.std)return image, target

2. my_dataset.py  ,用于数据集的提取和格式化

import os
from PIL import Image
import numpy as np
from torch.utils.data import Datasetclass DriveDataset(Dataset):  # 继承Dataset类def __init__(self, root: str, train: bool, transforms=None):super(DriveDataset, self).__init__()self.flag = "training" if train else "test"  # 根据train这个布尔类型确定需要处理的是训练集还是测试集data_root = os.path.join(root, "DRIVE", self.flag)  # 得到数据集根目录assert os.path.exists(data_root), f"path '{data_root}' does not exists."   # 判断路径是否存在self.transforms = transforms   # 初始化图像变换操作img_names = [i for i in os.listdir(os.path.join(data_root, "images")) if i.endswith(".tif")]   # 遍历图像文件夹获取每个图像的文件名self.img_list = [os.path.join(data_root, "images", i) for i in img_names]  # 获取图像路径self.manual = [os.path.join(data_root, "1st_manual", i.split("_")[0] + "_manual1.gif")  # 获取手动标签的路径for i in img_names]# 检查手动标签文件是否存在for i in self.manual:if os.path.exists(i) is False:raise FileNotFoundError(f"file {i} does not exists.")# 获取分割的ROI区域掩码self.roi_mask = [os.path.join(data_root, "mask", i.split("_")[0] + f"_{self.flag}_mask.gif")for i in img_names]# check filesfor i in self.roi_mask:if os.path.exists(i) is False:raise FileNotFoundError(f"file {i} does not exists.")def __getitem__(self, idx):img = Image.open(self.img_list[idx]).convert('RGB')  # 加载图像,并转换为RGB模式manual = Image.open(self.manual[idx]).convert('L')   # 加载手动标注图像,并转换为灰度模式manual = np.array(manual) / 255   # 进行归一化操作roi_mask = Image.open(self.roi_mask[idx]).convert('L')  # 加载ROI图像,并转换为灰度模式roi_mask = 255 - np.array(roi_mask)   # 对图像数组取反,使用这个方法将背景和前景颜色反转,白色是255,黑色是0,反转后ROI变成了内黑外白mask = np.clip(manual + roi_mask, a_min=0, a_max=255)   # 将手动标注图像和反转后的ROI图像相加,使用np.clip()将像素值控制在0-255范围,# 这里转回PIL的原因是,transforms中是对PIL数据进行处理mask = Image.fromarray(mask)if self.transforms is not None:img, mask = self.transforms(img, mask)return img, mask# 获取图像数据集长度def __len__(self):return len(self.img_list)# 用于将批量的图像和标签数据合并为一个批张量。@staticmethoddef collate_fn(batch):images, targets = list(zip(*batch))  # 将批量数据拆分为图像和标签两个列表batched_imgs = cat_list(images, fill_value=0)  # 使用 cat_list() 函数将图像和标签列表合并成张量。用于将列表中的 PIL 图像数据堆叠成张量,batched_targets = cat_list(targets, fill_value=255)return batched_imgs, batched_targetsdef cat_list(images, fill_value=0):max_size = tuple(max(s) for s in zip(*[img.shape for img in images]))  # 找到图像中最大的形状,以元组形式返回给max_sizebatch_shape = (len(images),) + max_size   # 计算出堆叠后的张量形状,包括批量大小和图像大小两个维度batched_imgs = images[0].new(*batch_shape).fill_(fill_value)  # 创建一个新的空白张量 batched_imgs,其形状与 batch_shape 相同,并将其填充为指定的填充值 fill_valuefor img, pad_img in zip(images, batched_imgs):  # 使用 zip() 函数将输入列表中的每个图像与其对应的空白张量进行拼接,以得到一个完整的张量。pad_img[..., :img.shape[-2], :img.shape[-1]].copy_(img)  # 将每个图像按照其实际大小插入到空白张量的左上角,以保持图像的相对位置不变。return batched_imgs

3. train.py, 模型训练的代码

import os
import time
import datetimeimport torchfrom src import UNet
from train_utils import train_one_epoch, evaluate, create_lr_scheduler
from my_dataset import DriveDataset
import transforms as T# 定义训练集图像的预处理方式
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)]   # 对图像的短边(长和宽中最短的)进行随机缩放以适应不同图像输入尺寸,缩放范围为【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(),  # 将数组矩阵转换为tensor类型,规范化到【0,1】范围T.Normalize(mean=mean, std=std),  # 加入图像归一化,并定义均值和标准差,RGB三通道的])# trans是一个列表类型,包含各种了变换,将这些变换组成一个compose变换,注意transforms.Compose()函数需要接收一个列表类型self.transforms = T.Compose(trans)# 使用__call__()函数来调用transforms变换def __call__(self, img, target):return self.transforms(img, target)  # target是指标签图像,img是指待分割图像# 定义验证集的图像预处理组合类,比较简单,只有张量化和规范化两个操作,这里规范化使用的是ImageNet推荐的参数,注意这种做法是针对彩色图像
class SegmentationPresetEval:def __init__(self, mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)):self.transforms = T.Compose([T.ToTensor(),T.Normalize(mean=mean, std=std),])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 = 565crop_size = 480# 检查train是否为Trueif train:return SegmentationPresetTrain(base_size, crop_size, mean=mean, std=std)else:return SegmentationPresetEval(mean=mean, std=std)# 定义模型创建函数,实例化UNet类创建模型,传入通道数,分割类别数,
def create_model(num_classes):model = UNet(in_channels=3, num_classes=num_classes, base_c=64)   # 输入通道数为3,分类类别数为2,# model = MobileV3Unet(num_classes=num_classes, pretrain_backbone=True)# model = VGG16UNet(num_classes=num_classes, pretrain_backbone=True)return modeldef main(args):device = torch.device(args.device if torch.cuda.is_available() else "cpu")batch_size = args.batch_size# using compute_mean_std.pymean = (0.709, 0.381, 0.224)std = (0.127, 0.079, 0.043)# 用来保存训练以及验证过程中信息results_file = "results{}.txt".format(datetime.datetime.now().strftime("%Y%m%d-%H%M%S"))train_dataset = DriveDataset(args.data_path,train=True,transforms=get_transform(train=True, mean=mean, std=std))val_dataset = DriveDataset(args.data_path,train=False,transforms=get_transform(train=False, mean=mean, std=std))num_workers = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8])    # 如果batch_size>1, 线程数num_workers取min(cpu核数,batch_size)train_loader = torch.utils.data.DataLoader(train_dataset,batch_size=batch_size,num_workers=num_workers,shuffle=True,pin_memory=True,collate_fn=train_dataset.collate_fn)val_loader = torch.utils.data.DataLoader(val_dataset,batch_size=1,num_workers=num_workers,pin_memory=True,collate_fn=val_dataset.collate_fn)model = create_model(num_classes=args.num_classes)model.to(device)params_to_optimize = [p for p in model.parameters() if p.requires_grad]optimizer = torch.optim.SGD(params_to_optimize,lr=args.lr, momentum=args.momentum, weight_decay=args.weight_decay)scaler = torch.cuda.amp.GradScaler() if args.amp else None# 创建学习率更新策略,这里是每个step更新一次(不是每个epoch)lr_scheduler = create_lr_scheduler(optimizer, len(train_loader), args.epochs, warmup=True)if args.resume:checkpoint = torch.load(args.resume, map_location='cpu')model.load_state_dict(checkpoint['model'])optimizer.load_state_dict(checkpoint['optimizer'])lr_scheduler.load_state_dict(checkpoint['lr_scheduler'])args.start_epoch = checkpoint['epoch'] + 1if args.amp:scaler.load_state_dict(checkpoint["scaler"])best_dice = 0.start_time = time.time()for epoch in range(args.start_epoch, args.epochs):mean_loss, lr = train_one_epoch(model, optimizer, train_loader, device, epoch, args.num_classes,lr_scheduler=lr_scheduler, print_freq=args.print_freq, scaler=scaler)confmat, dice = evaluate(model, val_loader, device=device, num_classes=args.num_classes)val_info = str(confmat)print(val_info)print(f"dice coefficient: {dice:.3f}")# write into txtwith open(results_file, "a") as f:# 记录每个epoch对应的train_loss、lr以及验证集各指标train_info = f"[epoch: {epoch}]\n" \f"train_loss: {mean_loss:.4f}\n" \f"lr: {lr:.6f}\n" \f"dice coefficient: {dice:.3f}\n"f.write(train_info + val_info + "\n\n")if args.save_best is True:if best_dice < dice:best_dice = diceelse:continuesave_file = {"model": model.state_dict(),"optimizer": optimizer.state_dict(),"lr_scheduler": lr_scheduler.state_dict(),"epoch": epoch,"args": args}if args.amp:save_file["scaler"] = scaler.state_dict()if args.save_best is True:torch.save(save_file, "save_weights/best_model_mobilenet_unet.pth")else:torch.save(save_file, "./save_weights/model_amp_{}.pth".format(epoch))total_time = time.time() - start_timetotal_time_str = str(datetime.timedelta(seconds=int(total_time)))print("training time {}".format(total_time_str))def parse_args():import argparseparser = argparse.ArgumentParser(description="pytorch unet training")parser.add_argument("--data-path", default="./", help="DRIVE root")# exclude backgroundparser.add_argument("--num-classes", default=2, type=int)parser.add_argument("--device", default="cuda", help="training device")parser.add_argument("-b", "--batch-size", default=4, type=int)parser.add_argument("--epochs", default=100, type=int, metavar="N",help="number of total epochs to train")parser.add_argument('--lr', default=0.01, type=float, help='initial learning rate')parser.add_argument('--momentum', default=0.9, type=float, metavar='M',help='momentum')parser.add_argument('--wd', '--weight-decay', default=1e-4, type=float,metavar='W', help='weight decay (default: 1e-4)',dest='weight_decay')parser.add_argument('--print-freq', default=1, type=int, help='print frequency')# 从上次训练停止的地方重新开始训练parser.add_argument('--resume', default='', help='resume from checkpoint')parser.add_argument('--start-epoch', default=0, type=int, metavar='N',help='start epoch')# 保存最佳模型parser.add_argument('--save-best', default=True, type=bool, help='only save best dice weights')# 混合精度训练参数,用于加快模型训练parser.add_argument("--amp", default=True, type=bool,  help="Use torch.cuda.amp for mixed precision training")# 解析命令行参数,并将解析结果保存在args对象中args = parser.parse_args()# 返回解析结果return argsif __name__ == '__main__':args = parse_args()if not os.path.exists("./save_weights"):os.mkdir("./save_weights")main(args)

五.模型的完整代码和数据集下载,请参考GitHub链接:待更新!!!

备注:此代码基于B站大佬:霹雳吧啦Wz,个人对这些代码进行了理解和注释说明得到。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

高中数学:抽象函数难点突破(拔高)

例题1 只证明前3个小题&#xff0c;4,5比较简单&#xff0c;不给与证明 这里&#xff0c;第3小题&#xff0c;难度最高 例题2 证明单调性的方法&#xff1a; 1、观察图像法&#xff1a;前提有具体解析式&#xff0c;且能画出图像 2、导数法&#xff1a;高三才学&#xff0c;且…

python和Vue开发的RBAC用户角色权限管理系统

后端框架&#xff1a;python的FastAPI作为后端服务和python-jose作为JWT认证 前端框架&#xff1a;Vue3构建页面和Vue Router作为路由管理&#xff0c;Pinia作为数据存储&#xff0c;Vite作为打包工具 可以实现菜单控制和路由控制&#xff0c;页面里面有按钮权限控制&#xf…

信息化平台管理系统智能引擎,互联网企业转型升级的新篇章-亿发

企业管理系统一直在伴随着中国互联网企业的发展而不断进步。过去&#xff0c;企业管理主要依赖于传统的表格和图表记录&#xff0c;但随着互联网企业的崛起&#xff0c;他们开始尝试自己开发简易的管理系统以满足业务需求。随着企业规模和业务复杂度的增加&#xff0c;互联网企…

【分享贴】多项目并行,如何做好项目管理?

对于项目经理来说&#xff0c;多项目并行管理是工作中的常态&#xff0c;也是一大难点。当多个项目共同推进时&#xff0c;项目经理经常会出现手忙脚乱、四处救火的情形&#xff0c;例如&#xff1a; A.资源管理难&#xff1a;资源冲突、资源分配不合理会导致项目延期。 B.进度…

【STM32嵌入式系统设计与开发】——12IWDG(独立看门狗应用)

这里写目录标题 一、任务描述二、任务实施1、ActiveBeep工程文件夹创建2、函数编辑&#xff08;1&#xff09;主函数编辑&#xff08;2&#xff09;USART1初始化函数(usart1_init())&#xff08;3&#xff09;USART数据发送函数&#xff08; USART1_Send_Data&#xff08;&…

苹果App Store上架工具介绍

文章目录 摘要引言正文1. Xcode2. [appuploder](https://www.applicationloader.net/)3. [克魔助手](https://keymob.com/) 4.[ipa guard](https://www.ipaguard.com/)总结参考资料 摘要 苹果App Store作为iOS应用程序的主要分发渠道&#xff0c;上架应用程序需要遵守规定和通…

Hive3.0.0出库库表中timestamp字段读取为null

在利用sqoop1.99.7做数据迁移的时候&#xff0c;从mysql导出表格到hive建立对应的表格&#xff0c;字段中使用了timestamp类型&#xff0c;在读取数据的时候&#xff0c;发现数据为null。查找问题方法如下&#xff1a; 1、查询库表字段类型 命令&#xff1a;desc tablen…

00000基础搭建vue+flask前后端分离项目

我完全是参考的这个vue3flask前后端分离环境速建_flask vue3-CSDN博客 安装了node_js&#xff08;添加了环境变量&#xff09; 环境变量 把原来的镜像源换成了淘宝镜像源 npm config set registry https://registry.npmmirror.com/ 查看版本证明安装成功 npm - v 安装npm i…

正则表达式不会用?一篇教你快速搞懂 !

目录 前言一、基础字符二、一系列常用的字符&#xff1b;1、一些元字符&#xff08;Meta-characters&#xff09; 三、一些高级概念1、贪婪与懒惰匹配2、两个实例加深理解1.颜色值的匹配&#xff1a;RGBS值2.ipv4 地址匹配 四、正则表达式常用语法**1.Flags&#xff08;标志符或…

c语言中动态内存管理

说到内存&#xff0c;大家一定都知道。但是有一种函数可以实现动态内存管理&#xff0c;下面大家一起学习。 文章目录 一、为什么要有动态内存管理&#xff1f;二、malloc 和 free1.malloc2.free 三、calloc 和 realloc1.calloc2.realloc3.常见的动态内存的错误3.1对NULL指针的…

面试题 之 webpack

1.说说你对webpack理解&#xff1f;解决什么问题&#xff1f; Webpack 是实现前端项目的模块化&#xff0c;用于现代 JavaScript 应用程序的静态模块打包工具&#xff0c;被webpack 直接引用的资源打包进 bunde.js的资源&#xff0c;当webpack 处理应用程序时,它会在内部构建一…

理解CPU与执行指令原理

本文侧重介绍cpu的工作任务&#xff0c;与cpu执行指令的过程是怎么样的&#xff1f; 目录 1.理解CPU 1.1.CPU的功能 1.2.CPU的逻辑构成 2.认识指令 2.1.什么是指令 2.2.CPU执行指令的准备工作(重点) 3.指令的执行过程 前景知识&#xff1a; 什么是计算机 就是遵循冯诺依…

阿里云部署宝塔,设置了安全组还是打不开。

1.在安全组是开放正确的端口好。8888要开&#xff0c;但是不只是开放8888&#xff0c;举个例子&#xff0c;https://47.99.53.222:17677/49706cf7这个&#xff0c;要开放17677这个端口号。 2.安全组要挂载到实例上&#xff0c;从三个点的进入点击管理实例&#xff0c;加到对应的…

深入聊聊企业数字化转型这个事儿

01 什么是数字化&#xff1f; 聊数字化&#xff0c;就不得不聊聊信息化、智能化。佛性的说&#xff1a;信息化是数字化的前世&#xff0c;智能化是数字化的来生&#xff01;我习惯用一个结构化的图形来表示事物之间的关系&#xff0c;信息化、数字化、智能化的关系如下&#…

[flask] flask的基本介绍、flask快速搭建项目并运行

笔记 Flask Flask 本身相当于一个内核&#xff0c;其他几乎所有的功能都要用到扩展&#xff08;邮件扩展Flask-Mail&#xff0c;用户认证Flask-Login&#xff0c;数据库Flask-SQLAlchemy&#xff09;&#xff0c;都需要用第三方的扩展来实现。比如可以用 Flask 扩展加入ORM、…

JVM本地方法

本地方法接口 NAtive Method就是一个java调用非java代码的接口 本地方法栈&#xff08;Native Method Statck&#xff09; Java虚拟机栈用于管理Java方法的调用&#xff0c;而本地方法栈用于管理本地方法的调用。 本地方法栈&#xff0c;也是线程私有的。 允许被实现成固定或…

想做抖音小店又不会该怎么办?先学会做店出单逻辑,再入门

大家好&#xff0c;我是电商花花。 现在在这个巨大的电商市场中&#xff0c;很多人都被电商的巨大红利给勾起来了&#xff0c;在这个抖音小店的黑马项目中&#xff0c;很多人都在其中赚钱获利&#xff0c;吸引了一批又一批商家。 相信现在对电商感兴趣的仍不在少数&#xff0…

Navicat BI 工具 | 连接数据

早前&#xff0c;海外 LearnBI online 博主 Adam Finer 对 Navicat Charts Creator 这款 BI&#xff08;商业智能&#xff09;工具进行了真实的测评。上一期&#xff0c;我们介绍了这位博主对 Navicat BI 工具的初始之感。今天&#xff0c;我们来看看从连接数据的角度&#xff…

Tomcat 下载以及安装

Tomcat安装及配置教程主要分为四步&#xff1a; 步骤一&#xff1a;首先确认自己是否已经安装JDK 1. cmd&#xff1a;查看java的版本 步骤二&#xff1a;下载安装Tomcat 1. 下载tomcat :Apache Tomcat - Welcome! 2. 选择对应的tomcat版本&#xff1a; 3. 进行安装&#…

C# 登录界面代码

背景 MVVM 是一种软件架构模式&#xff0c;用于创建用户界面。它将用户界面&#xff08;View&#xff09;、业务逻辑&#xff08;ViewModel&#xff09;和数据模型&#xff08;Model&#xff09;分离开来&#xff0c;以提高代码的可维护性和可测试性。 MainWindow 类是 View&a…