AIGC专栏13——ComfyUI 插件编写细节解析-以EasyAnimateV3为例

news2024/9/25 11:13:46

AIGC专栏13——ComfyUI 插件编写细节解析-以EasyAnimateV3为例

  • 学习前言
  • 什么是ComfyUI
  • 相关地址汇总
    • ComfyUI
    • EasyAnimateV3
  • 节点例子
  • 复杂例子-以EasyAnimateV3为例
    • 节点文件
      • 必要库的导入
      • 载入模型节点定义
      • Image to Video节点定义
      • 节点名称映射
    • __init__.py文件
    • 插件导入comfyUI

学习前言

最近在给EasyAnimateV3写ComfyUI的工作流,以方便别人测试。学习了ComfyUI的基本操作,也看了一下别人是怎么写的,自己也折腾了一下。
在这里插入图片描述

什么是ComfyUI

在人工智能艺术创作的领域里,Stable Diffusion 凭借其开放源代码的特性,吸引了众多开发者与艺术家的目光,并且因为强大的社区支持而展现出强大的影响力。

Stable Diffusion 的两大主流创作平台分别是 Stable Diffusion WebUI 与 ComfyUI。前者以其即装即用的便捷性、丰富的基础功能及广泛的社区插件支持,成为了新手的理想选择。而ComfyUI则更注重算法细节,主要特点是 结合了 工作流和节点,达成更高级别的自动化,使得创作流程更易于复现与传播。不过,这也意味着较高的学习曲线,要求用户对 Stable Diffusion 及其扩展功能有深入理解,动手实践能力亦需达到一定水准。

着眼于未来的工业化生产与效率提升,ComfyUI 显现出更为广阔的潜力与前景。
在这里插入图片描述

相关地址汇总

ComfyUI

https://github.com/comfyanonymous/ComfyUI

EasyAnimateV3

https://github.com/aigc-apps/EasyAnimate
感谢大家的关注。

节点例子

插件其实本质上是一个个节点,在ComfyUI中,一个节点对应一个类,本文先以ExampleNode为例进行解析,如下所示:

ExampleNode有几个方法,必要是的__init__INPUT_TYPES和一个FUNCTION 对应的函数。

  • __init__是python类的通用方法,非必须有内容,所以我们可以先pass。
  • INPUT_TYPES是ComfyUI必须要有的一个方法,需要放在classmethod装饰器下面,在INPUT_TYPES我们可以指定requiredoptional代表参数是必要的或者可选的。 required和optional均为一个字典,字典的key是这个参数的名称,key对应的value是这个参数的类别,这个类别既可以自定义,也可以用一些通用的,常见通用类别有IMAGEINTFLOAT等,具体的设置方法如下的代码所示。
  • FUNCTION对应的函数代表该节点执行的函数,这个函数一般是自定义的,如下所示的example_func,做的工作就是将所有的像素点除以2后+1。

除此之外还需要设置几个变量名:

  • FUNCTION = "example_func"对应了FUNCTION的函数名
  • RETURN_TYPES = ("IMAGE",)代表这个节点返回的内容类别,常见通用类别有IMAGEINTFLOAT
  • RETURN_NAMES = ("image_output_demo",)代表在UI上显示的节点返回的名称
  • CATEGORY = "Example"代表这个节点的种类。
class ExampleNode:def __init__(self):
        pass
    
    @classmethod
    def INPUT_TYPES(s):
        return {
            "required": {
                "image_demo": ("IMAGE",),
                "int_demo": ("INT", {
                    "default": 0, 	# 默认值
                    "min": 0, 		# 最小值
                    "max": 4096, 	# 最大值
                    "step": 64, 	# 在INT类型中,每次改变的最小步长
                    "display": "number" # 该参数在ComfyUI上的外观: "number","slider"
                }),
                "float_demo": ("FLOAT", {
                    "default": 1.0,
                    "min": 0.0,
                    "max": 10.0,
                    "step": 0.01,
                    "round": 0.001, 	# 表示精度,不设置的话为步长值。可以设置为 False 以禁用。
                    "display": "number"}),
                "string_demo": ("STRING", {
                    "multiline": False, # 只显示一行字符串,多余部分会在 UI上隐藏
                    "default": "string_demo"
                }),
            },
            "optional":{
                "optional_image": ("IMAGE",),
            },
        }
    FUNCTION = "example_func"
    
    RETURN_TYPES = ("IMAGE",)
    RETURN_NAMES = ("image_output_demo",)
    CATEGORY = "Example"

    def example_func(self, image_demo, int_demo, float_demo, string_demo, optional_image):
        image = image / 2.0 + 1
        return (image,)

复杂例子-以EasyAnimateV3为例

由于正常的Stable Diffusion的库并不会只有一个简单的Example Node类,可能存在复杂的导入情况,在这个基础上我们进行解析。

节点文件

必要库的导入

文件结构如下,为了代码文件的干净,我们将nodes对应的python文件和json工作流放入到自己创建的名为comfyui的文件夹中。
在这里插入图片描述

除去一些pip安装的库,在导入其它EasyAnimate组件时,需要使用相对路径。

# 正常的pip库导入部分
import gc
import os

import torch
import numpy as np
from PIL import Image
from diffusers import (AutoencoderKL, DDIMScheduler,
                       DPMSolverMultistepScheduler,
                       EulerAncestralDiscreteScheduler, EulerDiscreteScheduler,
                       PNDMScheduler)
from einops import rearrange
from omegaconf import OmegaConf
from transformers import CLIPImageProcessor, CLIPVisionModelWithProjection

import comfy.model_management as mm
import folder_paths
from comfy.utils import ProgressBar, load_torch_file

# 相对目录导入easyanimate组件
from ..easyanimate.models.autoencoder_magvit import AutoencoderKLMagvit
from ..easyanimate.models.transformer3d import Transformer3DModel
from ..easyanimate.pipeline.pipeline_easyanimate_inpaint import EasyAnimateInpaintPipeline
from ..easyanimate.utils.utils import get_image_to_video_latent
from ..easyanimate.data.bucket_sampler import ASPECT_RATIO_512, get_closest_ratio

载入模型节点定义

首先定义了INPUT_TYPES,载入模型的时候我们需要指定的必要参数是:

  • model 模型的名称,分别有'EasyAnimateV3-XL-2-InP-512x512'、'EasyAnimateV3-XL-2-InP-768x768'、'EasyAnimateV3-XL-2-InP-960x960'
  • low_gpu_memory_mode 是否开启 低显存模式。
  • config 用于指定EasyAnimateV3的config。
  • precision 模型的精度,EasyAnimateV3应该用bf16。

除此之外还需要设置几个变量名:

  • RETURN_TYPES = ("EASYANIMATESMODEL",) 代表这个节点返回的内容类别,我们自定义了一个EASYANIMATESMODEL类别
  • RETURN_NAMES = ("easyanimate_model",)代表在UI上显示的节点返回的名称
  • FUNCTION = "loadmodel"对应了FUNCTION的函数名
  • CATEGORY = "EasyAnimateWrapper"代表这个节点的种类。

在loadmodel函数中,我们载入了EasyAnimateV3的pipeline,并且通过字典的方式返回回去。

class LoadEasyAnimateModel:
    @classmethod
    def INPUT_TYPES(s):
        return {
            "required": {
             	# 模型名称
                "model": (
                    [ 
                        'EasyAnimateV3-XL-2-InP-512x512',
                        'EasyAnimateV3-XL-2-InP-768x768',
                        'EasyAnimateV3-XL-2-InP-960x960'
                    ],
                    {
                        "default": 'EasyAnimateV3-XL-2-InP-768x768',
                    }
                ),
                # 是否开启 低显存模式
                "low_gpu_memory_mode":(
                    [False, True],
                    {
                        "default": False,
                    }
                ),
                # config 名称
                "config": (
                    [
                        "easyanimate_video_slicevae_motion_module_v3.yaml",
                    ],
                    {
                        "default": "easyanimate_video_slicevae_motion_module_v3.yaml",
                    }
                ),
                # 模型精度
                "precision": (
                    ['fp16', 'bf16'],
                    {
                        "default": 'bf16'
                    }
                ),
                
            },
        }
    # 定义了一个新的类别
    RETURN_TYPES = ("EASYANIMATESMODEL",)
    RETURN_NAMES = ("easyanimate_model",)
    # 对应了`FUNCTION`的函数名
    FUNCTION = "loadmodel"
    CATEGORY = "EasyAnimateWrapper"
	
	# 载入模型的函数
    def loadmodel(self, low_gpu_memory_mode, model, precision, config):
        # Init weight_dtype and device
        device          = mm.get_torch_device()
        offload_device  = mm.unet_offload_device()
        weight_dtype = {"bf16": torch.bfloat16, "fp16": torch.float16, "fp32": torch.float32}[precision]

        # Init processbar
        pbar = ProgressBar(4)

        # Load config
        config_path = f"{script_directory}/config/{config}"
        config = OmegaConf.load(config_path)

        # Detect model is existing or not 
        model_path = os.path.join(folder_paths.models_dir, "EasyAnimate", model)
      
        if not os.path.exists(model_path):
            if os.path.exists(eas_cache_dir):
                model_path = os.path.join(eas_cache_dir, 'EasyAnimate', model)
            else:
                print(f"Please download easyanimate model to: {model_path}")

        # Load vae
        if OmegaConf.to_container(config['vae_kwargs'])['enable_magvit']:
            Choosen_AutoencoderKL = AutoencoderKLMagvit
        else:
            Choosen_AutoencoderKL = AutoencoderKL
        print("Load Vae.")
        vae = Choosen_AutoencoderKL.from_pretrained(
            model_path, 
            subfolder="vae", 
        ).to(weight_dtype)
        # Update pbar
        pbar.update(1)

        # Load Sampler
        print("Load Sampler.")
        scheduler = EulerDiscreteScheduler.from_pretrained(model_path, subfolder= 'scheduler')
        # Update pbar
        pbar.update(1)
        
        # Load Transformer
        print("Load Transformer.")
        transformer = Transformer3DModel.from_pretrained(
            model_path, 
            subfolder= 'transformer', 
            transformer_additional_kwargs=OmegaConf.to_container(config['transformer_additional_kwargs'])
        ).to(weight_dtype).eval()  
        # Update pbar
        pbar.update(1) 

        # Load Transformer
        if transformer.config.in_channels == 12:
            clip_image_encoder = CLIPVisionModelWithProjection.from_pretrained(
                model_path, subfolder="image_encoder"
            ).to(device, weight_dtype)
            clip_image_processor = CLIPImageProcessor.from_pretrained(
                model_path, subfolder="image_encoder"
            )
        else:
            clip_image_encoder = None
            clip_image_processor = None   
        # Update pbar
        pbar.update(1)

        pipeline = EasyAnimateInpaintPipeline.from_pretrained(
                model_path,
                transformer=transformer,
                scheduler=scheduler,
                vae=vae,
                torch_dtype=weight_dtype,
                clip_image_encoder=clip_image_encoder,
                clip_image_processor=clip_image_processor,
        )
    
        if low_gpu_memory_mode:
            pipeline.enable_sequential_cpu_offload()
        else:
            pipeline.enable_model_cpu_offload()

        easyanimate_model = {
            'pipeline': pipeline, 
            'dtype': weight_dtype,
            'model_path': model_path,
        }
        return (easyanimate_model,)

Image to Video节点定义

EasyAnimateV3的重要功能是实现了Image to Video。我们便以这个为例进行Node的解析。

首先定义了INPUT_TYPES,载入模型的时候我们需要指定的必要参数是:

  • easyanimate_model 代表在载入模块 载入的模型,是一个特殊的种类类别EASYANIMATESMODEL。
  • prompt 代表正向提示词。在这里我们自定义了个文本框,对应的种类格式为STRING_PROMPT。
  • negative_prompt 代表负向提示词。在这里我们自定义了个文本框,对应的种类格式为STRING_PROMPT。
  • video_length 视频的长度。
  • base_resolution 基础模型的分辨率,分别有512、768、960。
  • seed种子。
  • steps预测步数。
  • cfg guidance的强度。
  • scheduler采样器名称。

可选参数是:

  • start_img 开始图片;
  • end_img 结尾图片。

除此之外还需要设置几个变量名:

  • RETURN_TYPES = ("IMAGE",) 代表这个节点返回的内容类别,返回一系列帧图片。
  • RETURN_NAMES = ("images",)代表在UI上显示的节点返回的名称
  • FUNCTION = "process"对应了FUNCTION的函数名
  • CATEGORY = "EasyAnimateWrapper"代表这个节点的种类。

在process函数中,我们预处理了输入图片,并且进行了生成,最后返回了一系列帧图片。

class EasyAnimateI2VSampler:
    @classmethod
    def INPUT_TYPES(s):
        return {
            "required": {
                "easyanimate_model": (
                    "EASYANIMATESMODEL", 
                ),
                "prompt": (
                    "STRING_PROMPT",
                ),
                "negative_prompt": (
                    "STRING_PROMPT",
                ),
                "video_length": (
                    "INT", {"default": 72, "min": 8, "max": 144, "step": 8}
                ),
                "base_resolution": (
                    [ 
                        512,
                        768,
                        960,
                    ], {"default": 768}
                ),
                "seed": (
                    "INT", {"default": 43, "min": 0, "max": 0xffffffffffffffff}
                ),
                "steps": (
                    "INT", {"default": 25, "min": 1, "max": 200, "step": 1}
                ),
                "cfg": (
                    "FLOAT", {"default": 7.0, "min": 1.0, "max": 20.0, "step": 0.01}
                ),
                "scheduler": (
                    [ 
                        "Euler",
                        "Euler A",
                        "DPM++",
                        "PNDM",
                        "DDIM",
                    ],
                    {
                        "default": 'Euler'
                    }
                )
            },
            "optional":{
                "start_img": ("IMAGE",),
                "end_img": ("IMAGE",),
            },
        }
    
    RETURN_TYPES = ("IMAGE",)
    RETURN_NAMES =("images",)
    FUNCTION = "process"
    CATEGORY = "EasyAnimateWrapper"

    def process(self, easyanimate_model, prompt, negative_prompt, video_length, base_resolution, seed, steps, cfg, scheduler, start_img=None, end_img=None):
        device = mm.get_torch_device()
        offload_device = mm.unet_offload_device()

        mm.soft_empty_cache()
        gc.collect()

        start_img = [to_pil(_start_img) for _start_img in start_img] if start_img is not None else None
        end_img = [to_pil(_end_img) for _end_img in end_img] if end_img is not None else None
        # Count most suitable height and width
        aspect_ratio_sample_size    = {key : [x / 512 * base_resolution for x in ASPECT_RATIO_512[key]] for key in ASPECT_RATIO_512.keys()}
        original_width, original_height = start_img[0].size if type(start_img) is list else Image.open(start_img).size
        closest_size, closest_ratio = get_closest_ratio(original_height, original_width, ratios=aspect_ratio_sample_size)
        height, width = [int(x / 16) * 16 for x in closest_size]
        
        # Get Pipeline
        pipeline = easyanimate_model['pipeline']
        model_path = easyanimate_model['model_path']

        # Load Sampler
        if scheduler == "DPM++":
            noise_scheduler = DPMSolverMultistepScheduler.from_pretrained(model_path, subfolder= 'scheduler')
        elif scheduler == "Euler":
            noise_scheduler = EulerDiscreteScheduler.from_pretrained(model_path, subfolder= 'scheduler')
        elif scheduler == "Euler A":
            noise_scheduler = EulerAncestralDiscreteScheduler.from_pretrained(model_path, subfolder= 'scheduler')
        elif scheduler == "PNDM":
            noise_scheduler = PNDMScheduler.from_pretrained(model_path, subfolder= 'scheduler')
        elif scheduler == "DDIM":
            noise_scheduler = DDIMScheduler.from_pretrained(model_path, subfolder= 'scheduler')
        pipeline.scheduler = noise_scheduler

        generator= torch.Generator(device).manual_seed(seed)

        with torch.no_grad():
            video_length = int(video_length // pipeline.vae.mini_batch_encoder * pipeline.vae.mini_batch_encoder) if video_length != 1 else 1
            input_video, input_video_mask, clip_image = get_image_to_video_latent(start_img, end_img, video_length=video_length, sample_size=(height, width))

            sample = pipeline(
                prompt, 
                video_length = video_length,
                negative_prompt = negative_prompt,
                height      = height,
                width       = width,
                generator   = generator,
                guidance_scale = cfg,
                num_inference_steps = steps,

                video        = input_video,
                mask_video   = input_video_mask,
                clip_image   = clip_image, 
                comfyui_progressbar = True,
            ).videos
            videos = rearrange(sample, "b c t h w -> (b t) h w c")
        return (videos,)   

节点名称映射

NODE_CLASS_MAPPINGS 的key代表comfyUI显示的节点名称,value对应了我们定义的类名。
NODE_DISPLAY_NAME_MAPPINGS 的key代表comfyUI显示的节点名称,value对应了该节点的默认名称。

在这里有两个节点未解析TextBox、EasyAnimateT2VSampler,这里两个节点较为简单,希望同学们举一反三:)

NODE_CLASS_MAPPINGS = {
    "LoadEasyAnimateModel": LoadEasyAnimateModel,
    "TextBox": TextBox,
    "EasyAnimateI2VSampler": EasyAnimateI2VSampler,
    "EasyAnimateT2VSampler": EasyAnimateT2VSampler,
}
NODE_DISPLAY_NAME_MAPPINGS = {
    "TextBox": "TextBox",
    "LoadEasyAnimateModel": "Load EasyAnimate Model",
    "EasyAnimateI2VSampler": "EasyAnimate Sampler for Image to Video",
    "EasyAnimateT2VSampler": "EasyAnimate Sampler for Text to Video",
}

init.py文件

在我们仓库的根目录,还需要一个__init__.py文件来导入这些节点:
在这里插入图片描述
具体代码为:

from .comfyui.comfyui_nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS

__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]

插件导入comfyUI

只要将这个文件移入到comfyUI的custom_nodes文件夹即可。
在这里插入图片描述
然后我们可以在ui界面中找到我们定义的这些节点。
在这里插入图片描述

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

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

相关文章

被动的机器人非线性MPC控制

MPC是一种基于数学模型的控制策略,它通过预测系统在未来一段时间内的行为,并求解优化问题来确定当前的控制输入,以实现期望的控制目标。对于非线性系统,MPC可以采用非线性模型进行预测和优化,这种方法被称为非线性模型…

JS实现:统计字符出现频率/计算文字在文本中的出现次数

要实现这个功能,JavaScript 一个非常强大的方法,那就是reduce() reduce() 它用于将数组的所有元素减少到一个单一的值。这个值可以是任何类型,包括但不限于数字、字符串、对象或数组。 reduce() 方法接收一个回调函数作为参数,这个…

【C++】设计一套基于C++与C#的视频播放软件

在开发一款集视频播放与丰富交互功能于一体的软件时,结合C的高性能与C#在界面开发上的便捷性,是一个高效且实用的选择。以下,我们将概述这样一个系统的架构设计、关键技术点以及各功能模块的详细实现思路。 一、系统架构设计 1. 架构概览 …

截图神器Snipaste

这是我作为测试这么些年来用过的最好用的截图工具,让你将截图贴回到屏幕上,最好用的截图工具,推荐给同事深受好评。 snipaste是一个简单但强大的截图工具,也可以让你将截图贴回到屏幕上。下载打开Snipaste,按下F2来开始截图&…

【已解决】sudo: apt: command not found 或者apt-get: command not found解决方案

一、问题 在CentOS7.5运行apt-get install supervisor遇到如下报错 二、原因 CentOS的软件安装工具不是apt-get ,而是yum,应该使用如下命令: yum install supervisor 后面命令换为yum就可以了 三、扩展: 一般来说linux系统…

MVC架构

MVC架构 MVC架构在软件开发中通常指的是一种设计模式,它将应用程序分为三个主要组成部分:模型(Model)、视图(View)和控制器(Controller)。这种分层结构有助于组织代码,使…

AR0132AT 1/3 英寸 CMOS 数字图像传感器可提供百万像素 HDR 图像处理(器件编号包含:AR0132AT6R、AR0132AT6C)

AR0132AT 1/3 英寸 CMOS 数字图像传感器,带 1280H x 960V 有效像素阵列。它能在线性或高动态模式下捕捉图像,且带有卷帘快门读取。它包含了多种复杂的摄像功能,如自动曝光控制、开窗,以及视频和单帧模式。它适用于低光度和高动态范…

《GroupViT: Semantic Segmentation Emerges from Text Supervision》论文解读

会议:CVPR 年份:2022 代码:https://github.com/NVlabs/GroupViT 研究背景与动机: 传统深度学习系统中,图像区域的Grouping通常是隐式通过像素级识别标签的自上而下监督来实现的。作者提出将Grouping机制重新引入深…

本地部署,AnimeGANv3: 将现实世界照片转化为动漫风格

目录 引言 技术背景 架构与原理 实验结果与分析 应用实例 本地部署 运行结果 Photo to Hayao Style Photo to Shinkai Style more suprise 支持多种风格 结论 参考文献 GitHub - TachibanaYoshino/AnimeGANv3: Use AnimeGANv3 to make your own animation works, …

在 PostgreSQL 里如何处理数据的跨库关联查询性能优化?

文章目录 一、了解跨库关联查询的挑战(一)网络延迟(二)数据量庞大(三)索引不一致(四)数据库配置差异 二、优化策略(一)合理设计数据库架构(二&…

【C++】:继承[下篇](友元静态成员菱形继承菱形虚拟继承)

目录 一,继承与友元二,继承与静态成员三,复杂的菱形继承及菱形虚拟继承四,继承的总结和反思 点击跳转上一篇文章: 【C】:继承(定义&&赋值兼容转换&&作用域&&派生类的默认成员函数…

亿康源用科技引领发展,开启大健康产业新篇章

(本台记者报)近日,杭州有一家公司凭借深厚的科技研发实力与卓越的创新能力在大健康领域屡受好评,其研发的新品一经推出便成为行业热议。为了探寻该公司的经营秘诀,我们找到了这家公司——亿康源,并有幸与亿…

C语言笔记34 •单链表经典算法OJ题-6.环形链表的约瑟夫问题•

环形链表的约瑟夫问题 1.问题 编号为 1 到 n 的 n 个人围成一圈。从编号为 1 的人开始报数,报到 m 的人离开。 下一个人继续从 1 开始报数。 n-1 轮结束以后,只剩下一个人,问最后留下的这个人编号是多少? 数据范围: 1…

postman录制设置

一、前言: ​ postman是一个很好接口调试或是测试工具,简单方便,不需要很复杂的流程与技术,并且也具备录制条件。对于接口不了解,没有明确对应的说明,但又想通过接口进行一些测试使用其录制是一个不错的办…

【YOLOv8系列】(四)YOLOv8使用自己的数据集进行模型训练,成就感满满

目录 一.数据集获取 1.使用开源数据集 2.自定义数据图片 二.数据集标注 1.标注工具介绍 2.labelme安装 3.数据标注 1.选择要标注的数据集文件夹 2.设置自动保存: 3.创建多边形标注 4.格式转换 1.安装labelme2yolo 2.格式转换 3.效果查看 4.其他格式…

腾讯开源 tlbs-map 地图组件库

腾讯宣布开源 tlbs-map,一个基于腾讯位置服务地图 API 开发的 web 端地图组件库,支持在网页绘制地图并在地图上绘制点、线、面、热力图等效果,支持 Vue2、Vue3、React 等业界主流技术栈,旨在帮助开发者低成本开发地图业务 Javascr…

大模型LLM 应用私有化部署项目

LLM 参数包含数十亿甚至万亿级的架构复杂,训练和推理涉及大量计算资源。企业客户训练资料少,在实际应用中可能表现出检索幻觉、精准度差,同时也带来了性能、安全性和效率等方面的挑战。 ChatGPT、LLaMa、Bard 等大语言模型(LLMs)取得了非常巨…

【前端】零基础学会编写CSS

一、什么是CSS CSS (Cascading Style Sheets,层叠样式表)是一种是一种用来为结构化文档(如 HTML 文档)添加样式(字体、间距和颜色等)的计算机语言,能够对网页中元素位置的排版进行像素级别的精…

论文解读--4D mmWave Radar for Autonomous Driving Perception:A Comprehensive Survey

用于自动驾驶感知的4D毫米波雷达:综合综述 摘要 自动驾驶技术的快速发展推动了感知系统的不断创新,其中4D毫米波(mmWave)雷达是关键的传感设备之一。利用其全天候操作特性和在具有挑战性的环境中的强大感知能力,4D毫米…

keil5新建stm32工程的基本

1、建立工程文件夹,keil中新建工程,选择型号; 2、工程文件夹里建立自己所需要的文件夹等,复制固件库里面的文件到工程文件夹里; 3、将工程里建立对应的同名的分组,并将文件夹内的文件添加到工程分组中。 点…