1. 从“画格子”到“智能排版”:布局生成的前世今生

你有没有过这样的经历?设计一个网页、制作一份PPT,或者规划一个App界面,光是调整那些按钮、图片、文字框的位置和大小,就耗去了大半天时间。对齐、间距、比例、视觉平衡……每一个细节都让人头大。这背后,其实就是“布局生成”问题——如何自动、高效、美观地安排一组视觉元素在有限空间内的位置和大小。

传统的自动化方法,比如基于规则的系统,就像给你一套死板的“设计规范”,虽然能保证不出错,但产出的东西往往千篇一律,缺乏创意和灵活性。后来,大家把目光投向了深度学习,希望AI能像人类设计师一样“理解”美感与功能。早期的尝试主要围绕两类模型:生成对抗网络(GAN)变分自编码器(VAE)

我刚开始接触这个领域时,也尝试过用GAN来做UI布局生成。GAN的思路很巧妙,它让一个“生成器”和一个“判别器”互相博弈。生成器拼命造出以假乱真的布局,判别器则努力分辨哪个是AI生成的,哪个是真实设计师的作品。理想情况下,两者在对抗中共同进步,最终生成器能产出高质量的结果。但实际用起来,坑可真不少。GAN的训练过程非常不稳定,动不动就“崩溃”或者陷入“模式坍缩”——也就是说,生成器发现只要反复输出某几种固定的、能骗过判别器的布局就能“躺赢”,于是生成的布局多样性极差,来来去去就那么几个模板。这显然不是我们想要的。

VAE则走了另一条路,它更像一个“压缩-重建”的过程,先把真实布局编码到一个潜在空间,再从这个空间里采样并解码出新布局。VAE的优点是稳定,生成多样性也更好,因为它本质上是在学习数据的分布。但问题也随之而来:它学到的分布往往过于“平滑”和“平均”,导致生成的布局虽然多样,但细节上常常模糊不清,缺乏那种让人眼前一亮的“高质量”感,视觉效果上总感觉差了口气。

所以很长一段时间里,我们都在“高质量但单一”和“多样但平庸”之间做艰难取舍。直到扩散模型(Diffusion Model)在图像生成领域大放异彩,尤其是像DALL-E 2、Stable Diffusion这样的模型,让我们看到了同时兼顾“高质量”和“高多样性”的可能。那么,一个很自然的问题就来了:这套在图像像素上玩得风生水起的技术,能不能用来“生成”非像素的、结构化的布局数据呢?

这就是LayoutDM诞生的背景。它敏锐地抓住了两个核心痛点:第一,布局数据不是规整的像素网格,而是一组无序的、混合了连续(坐标、尺寸)和离散(类别标签)属性的元素集合,传统的卷积神经网络(CNN)处理起来很别扭。第二,布局的美观与否,元素之间的相互关系(比如对齐、避让、分组)比元素本身的绝对位置更重要。基于这两点洞察,LayoutDM做了一个大胆而优雅的嫁接:用Transformer架构作为核心,去驱动一个条件扩散模型(Conditional DDPM)。这个组合拳,可以说精准地打在了传统方法的软肋上。

2. LayoutDM的核心:当Transformer遇上扩散模型

要理解LayoutDM为什么有效,我们得先拆解它的两大技术支柱:扩散模型的基本原理,以及Transformer在处理布局数据时的独特优势。

2.1 扩散模型:一场精心策划的“破坏与重建”

扩散模型的思想非常直观,它模拟了一个“加噪”和“去噪”的过程。想象一下,你有一张清晰的布局设计图(原始数据)。

  • 前向过程(破坏):我们往这张图上一点点地撒“噪声粉末”(添加高斯噪声)。每一步都比上一步撒得更多一点,经过成百上千步后,这张图就完全变成了一堆毫无意义的随机噪点。这个过程是固定的、无需学习的。
  • 反向过程(重建/生成):这才是模型要学习的核心。我们训练一个神经网络,让它学会从那一堆噪点开始,一步步地“猜”出上一步噪声更少的样子应该是什么。最终,经过同样多的步骤,它能把一堆纯噪声“还原”成一张全新的、合理的布局图。

这个过程妙在哪里?首先,它的训练目标非常稳定,就是让网络预测每一步添加的噪声,这是一个简单的回归任务,不像GAN那样需要精巧的平衡。其次,因为它从纯噪声开始逐步“雕刻”,所以能探索到数据分布中更广泛的区域,天生就具备强大的多样性生成能力。最后,通过控制去噪的步骤和条件,我们可以实现非常精细和可控的生成。

2.2 Transformer:为“关系”而生的架构

那么,谁来担任这个“去噪神经网络”的角色呢?在图像生成中,大家常用U-Net(一种CNN)。但布局数据是结构化的序列,每个元素(如一个按钮、一张图片)都有自己的属性(类型、坐标、宽高),元素之间没有固定的先后顺序。CNN擅长处理局部、网格化的数据,对于这种无序的、依赖长程关系的结构化数据,就显得力不从心了。

这时,Transformer的优势就凸显出来了。它的核心机制是自注意力(Self-Attention)。简单来说,在处理布局序列时,模型会让每一个元素(比如“登录按钮”)去“关注”序列中所有其他元素(比如“背景图”、“输入框”、“Logo”)。通过计算注意力权重,模型能动态地学习到:“哦,这个按钮通常应该放在输入框的下方,并且和Logo保持一定的对齐关系。” 这种对元素间复杂关系的全局建模能力,正是生成和谐、美观布局的关键。

LayoutDM的创新之处,就在于它用一个纯Transformer架构的“条件布局去噪器”(cLayoutDenoiser),完全取代了传统扩散模型中的U-Net。这个设计选择,完美适配了布局数据的特性。

2.3 LayoutDM架构全景图

让我们把这两个部分结合起来,看看LayoutDM具体是怎么工作的。整个流程可以分为数据表示、前向扩散、反向去噪三个关键环节。

1. 布局如何表示? 首先,我们需要把一张设计图“翻译”成模型能懂的语言。LayoutDM将一个布局定义为一个元素集合L = { (g1, f1), (g2, f2), ..., (gN, fN) }

  • gi = [xi, yi, wi, hi]:这是第i个元素的几何属性,即中心点x坐标、y坐标、宽度和高度。所有值都被归一化到[-1, 1]的区间,方便模型处理。
  • fi:这是第i个元素的语义属性,比如它是一个“按钮”、“标题文本”还是“图片”。通常用一个类别标签的嵌入向量(Embedding)来表示。 最关键的一点是,这个集合是无序的。交换其中任意两个元素的位置,代表的仍然是同一个布局。这决定了模型不能依赖元素输入的顺序来理解信息。

2. 前向扩散过程 这个过程是固定的。假设我们有一个干净的布局几何参数 g0。在每一步 t,我们按照一个预定义的噪声计划,向 g0 中添加一点点高斯噪声。经过足够多的步骤 T(比如1000步)后,gT 就变成了一个几乎纯高斯噪声的向量,原有的布局信息被彻底掩盖。

3. 反向去噪过程(核心) 这是cLayoutDenoiser大显身手的地方。在每一步,模型的输入是:

  • gt:当前时刻带噪声的布局几何参数。
  • f:所有元素的语义属性(条件)。
  • t:当前的时间步(告诉模型现在处于去噪的哪个阶段)。

模型的目标是预测出添加到 gt 中的噪声 ε。它的内部运作如下:

  • 嵌入层:分别将几何参数 gt 和语义属性 f 通过线性层映射到高维向量空间,得到几何嵌入和属性嵌入。同时,时间步 t 也会通过正弦位置编码转换为时间嵌入。
  • 元素Token构建:将同一个元素的几何嵌入和属性嵌入拼接起来,再通过一个全连接层融合,形成一个“元素Token”。然后,将时间嵌入加到每个元素Token上。这样,每个Token都包含了“这个元素现在在哪、是什么、处于去噪的哪个阶段”的全部信息。
  • Transformer编码器:将这些元素Token送入一个多层的Transformer编码器。这里有一个关键设计:不使用标准的位置编码(Positional Encoding)。因为布局元素是无序的,添加位置编码会强行引入虚假的顺序信息,干扰模型对真实空间关系的判断。Transformer的自注意力机制会自发地让元素之间相互“交流”,学习它们应该如何排布。
  • 输出层:经过多层Transformer处理后,模型输出对每个元素所添加噪声的预测值。

在训练时,我们让模型预测的噪声和真实添加的噪声尽可能接近(使用均方误差损失)。在生成(采样)时,我们从纯噪声 gT 开始,反复调用训练好的cLayoutDenoiser,一步步预测并减去噪声,最终得到干净的新布局 g0

下面的表格对比了传统图像DDPM和LayoutDM的关键区别:

特性传统图像DDPMLayoutDM
主干网络U-Net (基于CNN)纯Transformer架构
输入数据图像像素网格布局元素序列 (几何+属性)
噪声注入对象图像像素值布局元素的几何参数 (x, y, w, h)
条件输入类别标签、文本描述元素语义属性 (如类别标签)
核心关系建模卷积的局部性自注意力的全局交互

3. 实战:亲手运行LayoutDM生成你的第一个布局

理论说得再多,不如亲手跑一遍来得实在。下面我就带你一步步搭建环境,并用LayoutDM的官方代码(或类似实现)生成一个简单的布局。这里假设你有一定的Python和PyTorch基础。

3.1 环境准备与依赖安装

首先,我们需要一个干净的Python环境。我强烈建议使用Conda来管理,避免包冲突。

# 创建一个新的conda环境,Python版本建议3.8或3.9
conda create -n layoutdm python=3.9
conda activate layoutdm

# 安装PyTorch,请根据你的CUDA版本去PyTorch官网选择对应命令
# 例如,对于CUDA 11.8
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118

# 安装其他核心依赖
pip install pytorch-lightning  # 论文作者使用了Lightning框架简化训练
pip install einops              # 方便进行张量操作
pip install matplotlib          # 用于可视化结果
pip install scikit-learn        # 用于一些评估指标的计算
pip install tqdm                # 显示进度条

接下来,我们需要获取代码。你可以从论文作者可能开源的仓库(例如GitHub上搜索LayoutDM)克隆,或者使用社区复现的版本。这里我以一个假设的简化版项目结构为例:

git clone <假设的LayoutDM代码仓库URL>
cd LayoutDM

3.2 数据准备与预处理

LayoutDM通常在公开布局数据集上进行训练和评估,比如Rico(移动UI界面)和PublayNet(学术文档)。对于初次实验,我们可以先用一个小的示例数据集,或者从这些数据集中提取一小部分。

数据预处理的关键是将原始的布局标注(通常是JSON格式,记录了每个元素的边界框和类别)转换成模型需要的格式:即归一化的几何参数 [x_center, y_center, width, height] 和类别索引。

假设我们有一个处理好的数据文件 dataset.pkl,里面存储了多个 (geometries, attributes) 对。我们可以编写一个简单的PyTorch Dataset类来加载它:

import torch
from torch.utils.data import Dataset, DataLoader
import pickle

class LayoutDataset(Dataset):
    def __init__(self, data_path):
        with open(data_path, 'rb') as f:
            self.data = pickle.load(f) # 假设data是list of (g, f)元组

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

    def __getitem__(self, idx):
        geometries, attributes = self.data[idx] # geometries: [N, 4], attributes: [N]
        # 注意:实际中需要确保几何参数已归一化到[-1,1]
        return {
            'geometries': torch.FloatTensor(geometries),
            'attributes': torch.LongTensor(attributes)
        }

# 创建数据加载器
dataset = LayoutDataset('path/to/your/dataset.pkl')
dataloader = DataLoader(dataset, batch_size=32, shuffle=True, collate_fn=collate_fn) # 需要自定义collate_fn处理变长序列

注意:布局中的元素数量N是可变的,因此在批处理时需要用到 paddingattention mask,这是Transformer处理序列数据的常见操作。collate_fn 函数需要负责将一批不等长的序列填充到相同长度,并生成对应的注意力掩码,告诉Transformer哪些位置是真实的元素,哪些是填充的。

3.3 模型定义与关键代码解析

现在,我们来看一下cLayoutDenoiser这个核心模块的简化版实现。理解这段代码,你就能抓住LayoutDM的灵魂。

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

class ConditionalLayoutDenoiser(nn.Module):
    def __init__(self, 
                 attribute_dim,      # 属性类别的数量
                 geom_dim=4,         # 几何参数维度 (x, y, w, h)
                 hidden_dim=256,     # 模型隐藏层维度
                 num_layers=8,       # Transformer层数
                 num_heads=8,        # 注意力头数
                 dropout=0.1):
        super().__init__()
        self.hidden_dim = hidden_dim
        
        # 1. 嵌入层
        self.geom_proj = nn.Linear(geom_dim, hidden_dim)      # 几何嵌入
        self.attr_embed = nn.Embedding(attribute_dim, hidden_dim) # 属性嵌入
        self.time_embed = nn.Sequential(                       # 时间步嵌入
            nn.Linear(hidden_dim, hidden_dim),
            nn.SiLU(),
            nn.Linear(hidden_dim, hidden_dim)
        )
        
        # 2. Transformer编码器
        encoder_layer = nn.TransformerEncoderLayer(
            d_model=hidden_dim,
            nhead=num_heads,
            dim_feedforward=hidden_dim*4,
            dropout=dropout,
            activation='gelu',
            batch_first=True  # 输入输出形状为 (batch, seq_len, hidden_dim)
        )
        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
        
        # 3. 输出层,预测噪声
        self.output_layer = nn.Linear(hidden_dim, geom_dim)
        
    def forward(self, noisy_geoms, attributes, timesteps, padding_mask=None):
        """
        noisy_geoms: [batch_size, num_elements, geom_dim]
        attributes:  [batch_size, num_elements]
        timesteps:   [batch_size]
        padding_mask: [batch_size, num_elements], True表示需要被mask的填充位置
        """
        batch_size, num_elements, _ = noisy_geoms.shape
        
        # 几何嵌入
        geom_emb = self.geom_proj(noisy_geoms)  # [B, N, D]
        
        # 属性嵌入
        attr_emb = self.attr_embed(attributes)  # [B, N, D]
        
        # 合并几何与属性信息
        element_emb = geom_emb + attr_emb       # [B, N, D]
        
        # 时间步嵌入,并加到每个元素上
        # 首先将时间步映射到与hidden_dim同维度的向量
        t_emb = self.time_embed(timestep_embedding(timesteps, self.hidden_dim)) # [B, D]
        t_emb = t_emb.unsqueeze(1)  # [B, 1, D]
        element_emb = element_emb + t_emb  # 广播相加 [B, N, D]
        
        # 通过Transformer编码器
        # 注意:这里没有添加任何位置编码!
        transformer_out = self.transformer(element_emb, src_key_padding_mask=padding_mask) # [B, N, D]
        
        # 预测噪声
        predicted_noise = self.output_layer(transformer_out) # [B, N, geom_dim]
        
        return predicted_noise

def timestep_embedding(t, dim):
    """ 构建正弦时间步嵌入,与原始Transformer论文中的位置编码类似 """
    half_dim = dim // 2
    emb = math.log(10000) / (half_dim - 1)
    emb = torch.exp(torch.arange(half_dim, device=t.device) * -emb)
    emb = t[:, None] * emb[None, :]
    emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=-1)
    if dim % 2 == 1:  # 如果维度是奇数,进行填充
        emb = F.pad(emb, (0, 1))
    return emb

这段代码清晰地展示了cLayoutDenoiser的工作流程:嵌入、融合、通过无位置编码的Transformer进行关系推理、最后输出噪声预测。padding_mask 确保了Transformer不会关注到那些为了批处理而填充的无效元素。

3.4 训练与采样循环

有了模型和数据,我们就可以定义训练循环了。扩散模型的训练相对直观。

def train_step(model, batch, noise_scheduler, optimizer):
    geometries = batch['geometries']  # 干净布局 [B, N, 4]
    attributes = batch['attributes']  # 属性标签 [B, N]
    padding_mask = batch['mask']      # 填充掩码 [B, N]
    
    batch_size = geometries.shape[0]
    
    # 1. 随机采样时间步和噪声
    timesteps = torch.randint(0, noise_scheduler.num_timesteps, (batch_size,), device=geometries.device).long()
    noise = torch.randn_like(geometries)
    
    # 2. 根据时间步向干净数据添加噪声 (前向扩散过程)
    noisy_geoms = noise_scheduler.add_noise(geometries, noise, timesteps)
    
    # 3. 模型预测噪声
    predicted_noise = model(noisy_geoms, attributes, timesteps, padding_mask)
    
    # 4. 计算损失(屏蔽填充部分)
    loss_mask = ~padding_mask
    loss = F.mse_loss(predicted_noise[loss_mask], noise[loss_mask])
    
    # 5. 反向传播
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    return loss.item()

采样(生成)过程是一个从噪声到清晰布局的迭代过程:

@torch.no_grad()
def sample(model, attributes, noise_scheduler, num_samples=1):
    """ 根据给定的属性条件生成布局 """
    model.eval()
    batch_size = attributes.shape[0]
    num_elements = attributes.shape[1]
    
    # 1. 从纯噪声开始
    x_t = torch.randn((batch_size, num_elements, 4), device=attributes.device)
    
    # 2. 迭代去噪
    for t in reversed(range(0, noise_scheduler.num_timesteps)):
        timesteps = torch.full((batch_size,), t, device=attributes.device, dtype=torch.long)
        
        # 预测噪声
        predicted_noise = model(x_t, attributes, timesteps)
        
        # 使用调度器计算更干净的样本
        x_t = noise_scheduler.step(predicted_noise, t, x_t)
        
    # 3. 返回最终生成的布局几何参数
    generated_layouts = x_t.clamp(-1, 1)  # 裁剪到[-1,1]范围
    return generated_layouts

运行完采样代码后,你会得到一组归一化的 [x, y, w, h]。你需要将它们反归一化到实际的画布尺寸(比如750x1334像素),然后用Matplotlib等库画出来,就能看到AI生成的布局草图了。第一次看到杂乱无章的噪声一步步变成有模有样的布局,那种感觉非常奇妙。

4. 效果对比与深入分析:LayoutDM强在哪里?

纸上得来终觉浅,我们还是要用实验数据说话。LayoutDM论文中在多个数据集上进行了详尽的定量和定性评估,结果确实令人信服。

4.1 定量指标:用数字证明实力

评估生成模型的好坏,需要综合看多个指标。LayoutDM主要使用了以下几个:

  • FID (Fréchet Inception Distance):这是衡量生成分布与真实分布距离的“金标准”之一。数值越低,说明生成的布局整体上越接近真实设计师创作的布局。LayoutDM在Rico、PublayNet等数据集上的FID得分显著低于之前的SOTA方法(如LayoutGAN++、VTN等)。这意味着它生成的布局集合,在统计特性上最像“真货”。
  • Maximum IoU:这个指标关注单个生成样本的质量。对于每个生成的布局,在真实数据集中找到与其重叠度(IoU)最高的那个真实布局,计算IoU值,然后取平均。LayoutDM在这个指标上也领先,说明它生成的单个布局的合理性和完整性很高
  • Overlap & Alignment:这两个是衡量布局美观度的“软指标”。Overlap计算元素间不必要的重叠面积,Alignment评估元素之间是否对齐良好。有趣的是,LayoutDM在这两个指标上并非总是第一,有时会略逊于某些精心设计的GAN方法。这是因为GAN的判别器可以被明确训练去惩罚重叠和对齐错误,而扩散模型是隐式学习。但这恰恰说明LayoutDM没有过度优化这些局部规则,而是更专注于学习全局的数据分布。

我复现实验时也观察到了类似趋势。用LayoutDM生成的UI布局,在FID和多样性上优势明显,但偶尔会出现两个元素轻微重叠的情况。不过在实际应用中,这通常可以通过一个快速的后处理优化步骤轻松解决,瑕不掩瑜。

4.2 定性对比:一眼就能看出的差距

数字是冰冷的,视觉对比才直观。论文中的对比图非常清晰:

  • 与LayoutGAN++对比:LayoutGAN++生成的布局有时会显得空洞、元素扎堆,或者出现一些反常识的排列(比如巨大的按钮放在角落)。而LayoutDM生成的布局,元素分布更均匀,空间利用更合理,看起来更像一个“成熟”的设计。
  • 多样性对比:当给定相同的元素类别和数量作为条件时,LayoutDM能够生成多种多样、但都合理的布局。而LayoutGAN++很容易陷入“模式坍缩”,多次生成的结果大同小异,缺乏变化。这对于需要大量设计变体供选择的实际场景(如A/B测试素材生成)来说,LayoutDM的价值巨大。

4.3 消融实验:Transformer的作用不可替代

为了证明Transformer结构是关键,作者做了一个关键的消融实验:将Transformer主干网络替换为一个简单的多层感知机(MLP,即全连接网络)。 结果如何呢?MLP版本在简单的、元素数量固定的布局上或许还能应付,但一旦布局稍微复杂,元素间关系变得重要时,它的表现就急剧下降。生成的布局经常出现严重的元素堆叠、空间错乱,完全不符合设计规范。这是因为MLP只能独立处理每个元素,无法建模元素之间的全局依赖关系。这个实验有力地证明了,自注意力机制对于理解和生成复杂的空间布局是至关重要的

4.4 扩展应用:不止于UI和文档

LayoutDM的潜力不止在生成手机UI或文档版面。论文还展示了它在其他任务上的强大泛化能力:

  • 文本Logo生成:给定一串字符(如公司名“TechCorp”),生成一个美观的文本Logo布局。这里元素(字符)是有顺序的,所以模型重新引入了位置编码。结果它不仅能生成水平排列,还能创造出弧形、垂直、错落有致等多种富有设计感的排版,远超传统方法。
  • 自然场景布局生成:给定场景标签(如“沙滩、椰树、太阳、人”),生成这些物体在画面中的合理边界框布局。LayoutDM能够学会常识性的空间关系,比如把“太阳”放在天空区域,把“人”放在沙滩上而不是海里。这为后续的图像生成提供了强大的结构先验。

这些扩展实验表明,LayoutDM所代表的“Transformer+扩散模型”范式,对于任何需要生成结构化、关系型数据的任务,都具有巨大的启发意义。

5. 优势、局限与未来展望

经过上面的技术解析和实战演练,我们可以清晰地总结出LayoutDM的几大核心优势:

  1. 质量与多样性的黄金平衡:它摆脱了GAN的模式坍缩和VAE的模糊性,能同时产出高质量和高多样性的布局样本。
  2. 训练稳定:基于似然估计的扩散模型训练目标比GAN的对抗训练稳定得多,不再需要小心翼翼调整超参数来防止训练崩溃。
  3. 强大的关系建模能力:Transformer的自注意力机制天生适合处理布局元素间复杂的、无序的相互关系,这是生成合理布局的基石。
  4. 灵活的条件控制:可以很方便地通过属性条件(f)来控制生成布局的内容,实现可控生成。

当然,没有完美的模型,LayoutDM也有其局限性和挑战:

  • 生成速度:这是扩散模型的通病。需要迭代多步(如1000步)才能生成一个样本,相比VAE的一次前向传播要慢得多。在实际产品中应用,需要考虑使用加速采样技术(如DDIM、PLMS等),将步数缩减到50甚至20步以内,在质量和速度间取得平衡。
  • 单层画布限制:目前的模型假设所有元素都在同一个平面上,无法处理带有图层叠加、前后遮挡关系的复杂设计(比如海报设计中常见的文字叠压在图片上的效果)。未来的工作可能需要引入更复杂的数据表示(如深度通道或图层索引)。
  • 对训练数据质量要求高:模型生成的质量上限受限于训练数据。如果数据集中存在大量设计不佳的布局,模型也会学到这些坏习惯。

在我自己的项目实践中,为了解决速度问题,我通常会先用完整的1000步模型训练,得到一个高质量的检查点,然后在这个基础上用知识蒸馏或者渐进式蒸馏的方法,训练一个步数少得多但性能相近的“轻量版”模型用于部署。对于复杂布局,则可以尝试将布局生成任务分阶段进行,比如先生成主要区块的布局,再在每个区块内进行细化。

LayoutDM的出现,为AIGC在结构化设计领域的应用打开了一扇新的大门。它的思想——用适合关系建模的架构(Transformer)去驱动一个强大的生成范式(扩散模型)——完全可以迁移到其他领域,比如分子结构生成、流程图生成、室内家具布局等。随着模型加速技术的成熟和硬件算力的提升,我相信这种“慢工出细活”的扩散模型方法,会在越来越多的实时或准实时创意辅助工具中落地,真正成为设计师和开发者的得力助手。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐