PCL点云库入门(第18讲)——PCL库点云特征之3DSC特征描述3D shape context descriptor

一、3DSC(3D Shape Context)特征算法原理

1. 背景

3DSC 是一种描述三维点云局部形状的特征描述子,受二维 Shape Context 的启发。它用于捕捉点云某一点局部的几何分布信息,对点云配准、识别等任务非常有效。

2. 基本思想

3DSC 描述子通过统计某个关键点周围点云在空间中分布的统计直方图,编码该关键点的局部结构信息。它的核心是将关键点附近的邻域点投影到球坐标系统中,然后统计这些点在球坐标的不同区域内的分布

3. 具体原理步骤

  • 关键点选择
    3DSC一般计算在关键点上(比如角点、边缘点)【关键点提取算法会在后面章节进行讲解说明】,提高效率和鲁棒性。

  • 邻域定义
    对关键点以一定半径定义邻域(radius),找到邻域内的所有点。

  • 局部坐标系建立
    建立关键点局部参考坐标系,通常利用该点的法线方向定义z轴,通过构造局部坐标系保证描述子具有旋转不变性。

  • 球坐标系划分
    在局部坐标系中,以关键点为球心,将邻域空间划分成若干个体素(bins),通常按半径(r)、极角(θ)、方位角(φ)划分为3个维度的离散格子,如下图所示。
    在这里插入图片描述

  • 统计邻域点分布
    计算邻域中点在每个球体素内的点数,统计形成一个三维直方图。

  • 归一化
    对直方图进行归一化,得到局部几何结构的概率分布,作为特征描述子。

  • 特征匹配
    计算两个3DSC描述子之间的距离(例如χ²距离、Hellinger距离等)完成匹配。

4. 3DSC特点

  • 保持对旋转和尺度的鲁棒性(通过局部坐标系和归一化处理)
  • 能够有效捕捉局部三维结构信息
  • 特征维度较高(通常上千维),计算和存储较大,适合关键点描述

二、主要成员函数和变量

1、成员变量详解

变量名类型说明
azimuth_bins_std::size_t方位角方向划分的 bin 数(默认 12)
elevation_bins_std::size_t仰角方向划分的 bin 数(默认 11)
radius_bins_std::size_t半径方向划分的 bin 数(默认 15)
min_radius_double最小半径,表示从点开始计算特征时的起始距离(默认 0.1)
point_density_radius_double用于计算点密度的搜索半径(默认 0.2)
radii_interval_std::vector<float>半径分桶区间,用于特征球体的划分
theta_divisions_std::vector<float>方位角分桶区间
phi_divisions_std::vector<float>仰角分桶区间
volume_lut_std::vector<float>每个 bin 对应的小体积 LUT(look-up table)
descriptor_length_std::size_t每个点的描述子维度 = azimuth_bins × elevation_bins × radius_bins
rng_std::mt19937随机数生成器,用于计算局部坐标系中 X 轴的选择(不确定性)
rng_dist_std::uniform_real_distribution<float>0~1 的均匀分布,用于上述随机采样

2、 核心成员函数讲解

构造函数
ShapeContext3DEstimation(bool random = false)
  • 构造函数初始化默认参数和随机数生成器。
  • 如果 random = true,则使用当前时间作为种子,否则使用固定值 12345。

initCompute()

bool initCompute () override;
  • 初始化函数:

    • 计算并填充 radii_interval_, theta_divisions_, phi_divisions_
    • 计算每个体素的体积并填入 volume_lut_
    • 是描述子计算的前置步骤。

computePoint(...)

bool computePoint (std::size_t index, const pcl::PointCloud<PointNT>& normals, float rf[9], std::vector<float>& desc);
  • 对单个点进行特征计算。

  • 输入:

    • 点的索引
    • 法线集合
    • 点的局部参考坐标系 rf[9]
  • 输出:

    • 描述子 desc(长度为 descriptor_length_)

computeFeature(...)

void computeFeature (PointCloudOut &output) override;
  • 继承自 Feature<PointInT, PointOutT> 的虚函数。
  • 用于计算整云的 3DSC 特征。
  • 遍历所有索引点,调用 computePoint() 逐个生成描述子。

setMinimalRadius() / getMinimalRadius()

void setMinimalRadius(double radius);
double getMinimalRadius();
  • 设置 / 获取最小半径 rmin(决定特征球从哪里开始采样)

setPointDensityRadius() / getPointDensityRadius()

void setPointDensityRadius(double radius);
double getPointDensityRadius();
  • 设置 / 获取计算点密度时的搜索半径。

getAzimuthBins(), getElevationBins(), getRadiusBins()

  • 获取当前 azimuth/elevation/radius 的 bin 数量。

rnd()

inline float rnd() { return rng_dist_(rng_); }
  • 内部随机数生成函数(0~1 均匀分布),用于随机选择局部坐标轴方向。

3、 特征维度说明

描述子维度为

descriptor_length_ = azimuth_bins_ × elevation_bins_ × radius_bins_;

默认情况下为:

12 × 11 × 15 = 1980维(对应 pcl::ShapeContext1980)

4、总结:整体流程(computeFeature)

  1. 初始化(initCompute()) → 构建球形体素划分

  2. 遍历输入点集(computeFeature()

  3. 对每个点:

    • 建立局部坐标系(通常由法线和随机 X 方向构建)
    • 邻域点转换到该局部坐标系
    • 根据方位角/仰角/半径落入 bin 中计数
    • 根据 bin 的体积归一化 → 得到直方图
  4. 存入输出点云 PointCloudOut(即每个点一个1980维向量)


三、主要实现代码注解

1、计算单点 3D 形状上下文描述子(Shape Context 3D Descriptor)

// 模板函数:计算指定点的 Shape Context 描述子
template <typename PointInT, typename PointNT, typename PointOutT> 
bool pcl::ShapeContext3DEstimation<PointInT, PointNT, PointOutT>::computePoint (std::size_t index,                          // 当前计算的点在索引列表中的索引const pcl::PointCloud<PointNT> &normals,   // 所有法线float rf[9],                                // 输出:参考坐标系(Reference Frame,3x3 矩阵展开为数组)std::vector<float> &desc)                   // 输出:形状上下文描述子
{// rf 中的三个向量组成了局部参考系 RF:x_axis | y_axis | normalEigen::Map<Eigen::Vector3f> x_axis (rf);Eigen::Map<Eigen::Vector3f> y_axis (rf + 3);Eigen::Map<Eigen::Vector3f> normal (rf + 6);// 在指定搜索半径内查找邻域点pcl::Indices nn_indices;std::vector<float> nn_dists;const std::size_t neighb_cnt = searchForNeighbors ((*indices_)[index], search_radius_, nn_indices, nn_dists);if (neighb_cnt == 0){// 若无邻居,返回 NaN 描述子和空的参考系std::fill (desc.begin (), desc.end (), std::numeric_limits<float>::quiet_NaN ());std::fill (rf, rf + 9, 0.f);return (false);}// 找到最近邻点索引(用来获取该点的法线)const auto minDistanceIt = std::min_element(nn_dists.begin (), nn_dists.end ());const auto minIndex = nn_indices[std::distance (nn_dists.begin (), minDistanceIt)];// 获取当前点的位置Vector3fMapConst origin = (*input_)[(*indices_)[index]].getVector3fMap ();// 使用最近邻的法线作为当前点的法线normal = normals[minIndex].getNormalVector3fMap ();// 初始化 x_axis 为一个随机向量,用于之后与法线正交化x_axis[0] = rnd ();x_axis[1] = rnd ();x_axis[2] = rnd ();if (!pcl::utils::equal (normal[2], 0.0f))x_axis[2] = - (normal[0]*x_axis[0] + normal[1]*x_axis[1]) / normal[2];else if (!pcl::utils::equal (normal[1], 0.0f))x_axis[1] = - (normal[0]*x_axis[0] + normal[2]*x_axis[2]) / normal[1];else if (!pcl::utils::equal (normal[0], 0.0f))x_axis[0] = - (normal[1]*x_axis[1] + normal[2]*x_axis[2]) / normal[0];x_axis.normalize ();// 断言 x_axis 与 normal 正交assert (pcl::utils::equal (x_axis.dot(normal), 0.0f, 1E-6f));// y_axis = normal × x_axis,构成正交参考系y_axis.matrix () = normal.cross (x_axis);// 遍历邻域内每个邻居点for (std::size_t ne = 0; ne < neighb_cnt; ne++){if (pcl::utils::equal (nn_dists[ne], 0.0f))continue;// 获取邻居坐标Eigen::Vector3f neighbour = (*surface_)[nn_indices[ne]].getVector3fMap ();// --- 计算邻居点的极坐标 ---float r = std::sqrt (nn_dists[ne]); // 与中心点距离// 将点投影到切平面(normal 所在平面)Eigen::Vector3f proj;pcl::geometry::project (neighbour, origin, normal, proj);proj -= origin;proj.normalize ();// phi:投影在切平面后,与 x_axis 形成的角度(0 ~ 360°)Eigen::Vector3f cross = x_axis.cross (proj);float phi = pcl::rad2deg (std::atan2 (cross.norm (), x_axis.dot (proj)));phi = cross.dot (normal) < 0.f ? (360.0f - phi) : phi;// theta:当前邻居点与法线的夹角(0 ~ 180°)Eigen::Vector3f no = neighbour - origin;no.normalize ();float theta = normal.dot (no);theta = pcl::rad2deg (std::acos (std::min (1.0f, std::max (-1.0f, theta))));// --- 计算邻居所在的直方图 Bin(j,k,l) ---const auto rad_min = std::lower_bound(std::next (radii_interval_.cbegin ()), radii_interval_.cend (), r);const auto theta_min = std::lower_bound(std::next (theta_divisions_.cbegin ()), theta_divisions_.cend (), theta);const auto phi_min = std::lower_bound(std::next (phi_divisions_.cbegin ()), phi_divisions_.cend (), phi);const auto j = std::distance(radii_interval_.cbegin (), std::prev(rad_min));const auto k = std::distance(theta_divisions_.cbegin (), std::prev(theta_min));const auto l = std::distance(phi_divisions_.cbegin (), std::prev(phi_min));// --- 计算当前邻居点的局部点密度 ---pcl::Indices neighbour_indices;std::vector<float> neighbour_distances;int point_density = searchForNeighbors (*surface_, nn_indices[ne], point_density_radius_, neighbour_indices, neighbour_distances);if (point_density == 0)continue;// 权重 w = 体素归一化体积 LUT / 邻域点数float w = (1.0f / static_cast<float> (point_density)) *volume_lut_[(l*elevation_bins_*radius_bins_) +  (k*radius_bins_) + j];assert (w >= 0.0);if (w == std::numeric_limits<float>::infinity ())PCL_ERROR ("Shape Context Error INF!\n");if (std::isnan(w))PCL_ERROR ("Shape Context Error IND!\n");// 将权重累加到对应的直方图 bin 中desc[(l*elevation_bins_*radius_bins_) + (k*radius_bins_) + j] += w;assert (desc[(l*elevation_bins_*radius_bins_) + (k*radius_bins_) + j] >= 0);}// 注意:Shape Context 3D 的参考系不具备重复性,因此输出设为 0,提示用户std::fill (rf, rf + 9, 0);return (true);
}

此函数的关键步骤为

  1. 计算局部参考坐标系(使用最近邻的法线,并构造 x/y 轴);
  2. 搜索邻域点,并将邻居映射到形状上下文的三维极坐标空间;
  3. 根据点在 (r, θ, φ) 空间中的位置,统计加权直方图
  4. 输出形状上下文描述子 desc(一维向量,长度为 radius_bins_ * elevation_bins_ * azimuth_bins_);
  5. 将 RF 置零表示其不可重复。

2、计算所有点 3D 形状上下文描述子(3DSC)

下面是你提供的 pcl::ShapeContext3DEstimation::computeFeature 函数的 逐行中文注释版,便于理解其作用和流程。


template <typename PointInT, typename PointNT, typename PointOutT> 
void pcl::ShapeContext3DEstimation<PointInT, PointNT, PointOutT>::computeFeature(PointCloudOut &output)
{// 确保描述子的长度为 1980(由半径/角度划分决定)assert (descriptor_length_ == 1980);// 假设输出点云初始是 dense(没有无效点)output.is_dense = true;// 遍历每一个需要计算描述子的点(由 indices_ 指定)for (std::size_t point_index = 0; point_index < indices_->size(); point_index++){// 如果当前点不是有限(如有 NaN 或 Inf),填充 NaN 描述子并跳过if (!isFinite((*input_)[(*indices_)[point_index]])){// 将 descriptor 数组设置为 NaNstd::fill(output[point_index].descriptor, output[point_index].descriptor + descriptor_length_,std::numeric_limits<float>::quiet_NaN());// 将局部参考帧 rf 设置为 0(表示无效)std::fill(output[point_index].rf, output[point_index].rf + 9, 0);// 标记整个输出点云为非 dense(包含无效点)output.is_dense = false;continue;}// 初始化当前点的描述子向量,长度为 descriptor_length_(1980)std::vector<float> descriptor(descriptor_length_);// 调用 computePoint 函数计算当前点的 3D Shape Context 描述子和局部参考帧 rf// 如果失败(如搜索不到邻域点),将该点视作无效点if (!computePoint(point_index, *normals_, output[point_index].rf, descriptor))output.is_dense = false;// 将计算出的 descriptor 拷贝到 output 中对应点的 descriptor 成员里std::copy(descriptor.begin(), descriptor.end(), output[point_index].descriptor);}
}

说明:

  • descriptor_length_ == 1980 是由 Shape Context 直方图的划分决定的:
    通常为:radius_bins × elevation_bins × azimuth_bins,如 5 × 6 × 66 = 1980

  • rf 是局部参考帧(Local Reference Frame),由三个正交轴组成(x, y, normal),用于保证描述子的方向不变性。

  • 如果点本身是无效的,或在 computePoint() 中无法找到有效邻域点,会将该点描述子设置为 NaN。

  • output.is_dense 是一个标志位,表示结果是否全部为有效点(无 NaN)。


四、使用代码示例

/*****************************************************************//**
* \file   PCLFeature3DSCmain.cpp
* \brief
*
* \author YZS
* \date   Jun 2025
*********************************************************************/#include <iostream>#include <pcl/io/pcd_io.h>
#include <pcl/point_types.h>
#include <pcl/features/normal_3d.h>
#include <pcl/features/3dsc.h>
#include <pcl/search/kdtree.h>int PCL3DSC(int argc, char** argv)
{// 加载点云std::string fileName = "E:/PCLlearnData/18/fragment.pcd";pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>());if (pcl::io::loadPCDFile<pcl::PointXYZ>(fileName, *cloud) == -1){std::cerr << "无法读取点云文件"<< fileName << std::endl;return -1;}std::cout << "点云加载成功,点数: " << cloud->size() << std::endl;// 计算法线pcl::PointCloud<pcl::Normal>::Ptr normals(new pcl::PointCloud<pcl::Normal>());pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> ne;ne.setInputCloud(cloud);pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZ>());ne.setSearchMethod(tree);ne.setRadiusSearch(0.05); // 法线估计半径ne.compute(*normals);std::cout << "法线估计完成。" << std::endl;// 创建ShapeContext3D特征估计器pcl::ShapeContext3DEstimation<pcl::PointXYZ, pcl::Normal, pcl::ShapeContext1980> sc3d;sc3d.setInputCloud(cloud);  // 输入点云sc3d.setInputNormals(normals); // 输入法线sc3d.setSearchMethod(tree);sc3d.setRadiusSearch(0.2);  // 特征提取的球形邻域半径// 输出描述子pcl::PointCloud<pcl::ShapeContext1980>::Ptr descriptors(new pcl::PointCloud<pcl::ShapeContext1980>());sc3d.compute(*descriptors);std::cout << "3DSC特征计算完成,特征维度: " << pcl::ShapeContext1980::descriptorSize() << std::endl;std::cout << "输出特征个数: " << descriptors->size() << std::endl;// 打印前3个点的描述子片段for (size_t i = 0; i < std::min<size_t>(3, descriptors->size()); ++i){std::cout << "点 " << i << " 描述子 (前50维): ";for (int j = 0; j < 50; ++j)std::cout << descriptors->points[i].descriptor[j] << " ";std::cout << std::endl;}return 0;
}int main(int argc, char* argv[])
{PCL3DSC(argc, argv);std::cout << "Hello World!" << std::endl;std::system("pause");return 0;
}

结果展示:
在这里插入图片描述


至此完成第十八节PCL库点云特征之3DSC特征描述,下一节我们将进入《PCL库中点云特征之GASD(Globally Aligned Spatial Distribution )特征描述》的学习,欢迎喜欢的朋友订阅。

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

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

相关文章

SpringBoot+Mysql校园跑腿服务平台系统源码

&#x1f497;博主介绍&#x1f497;&#xff1a;✌在职Java研发工程师、专注于程序设计、源码分享、技术交流、专注于Java技术领域和毕业设计✌ 温馨提示&#xff1a;文末有 CSDN 平台官方提供的老师 Wechat / QQ 名片 :) Java精品实战案例《700套》 2025最新毕业设计选题推荐…

分库分表的取舍

文章目录 大数据量下采用**水平分表**的缺点**1. 跨表查询复杂性与性能下降****2. 数据分布不均衡****3. 分布式事务与一致性问题****4. 扩展性受限****5. 查询条件限制与索引管理复杂****6. 数据迁移与维护成本高****7. 业务逻辑复杂度增加****总结** shardingJdbc分片策略**1…

Vue3解决“找不到模块@/components/xxx.vue或其相应的类型声明ts文件(2307)”

问题 1&#xff1a;如果没有这个env.d.ts文件&#xff0c;就新建 declare module "*.vue" {import { DefineComponent } from "vue";const component: DefineComponent<{}, {}, any>;export default component; }2&#xff1a;如果有tsconfig.json文…

计算机视觉与深度学习 | 基于MATLAB的图像特征提取与匹配算法总结

基于MATLAB的图像特征提取与匹配算法全面指南 图像特征提取与匹配 基于MATLAB的图像特征提取与匹配算法全面指南一、图像特征提取基础特征类型分类二、点特征提取算法1. Harris角点检测2. SIFT (尺度不变特征变换)3. SURF (加速鲁棒特征)4. FAST角点检测5. ORB (Oriented FAST …

如何通过API接口获取淘宝商品列表?操作详解

一、准备工作 注册开发者账号 访问淘宝开放平台官网/万邦开放平台&#xff0c;完成企业开发者认证&#xff08;个人账号权限受限&#xff09;&#xff0c;使用已有淘宝账号可直接登录。创建应用并填写基本信息&#xff08;如应用名称、类型等&#xff09;&#xff0c;系统生成A…

大数据驱动企业决策智能化的路径与实践

&#x1f4dd;个人主页&#x1f339;&#xff1a;慌ZHANG-CSDN博客 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; 一、引言&#xff1a;数据驱动的企业竞争力重构 在这个瞬息万变的商业时代&#xff0c;“快者胜”的竞争逻辑愈发明显。企业如何在复杂环…

DataGridView关闭默认第一列及表头居中设置

1、关闭默认第一列 如上图所示&#xff0c;C#和VB.net中的DataGridView控件都是有一个默认的第一列&#xff08;虽然是空白&#xff09;的&#xff0c;如何关闭这个默认列呢&#xff1f; 把“RowHeadersVisible”的值设置成“false”就可以了&#xff1a; 2、表头居中显示 如上…

nodejs express 打包部署

当前文件路径 webpack.config.js const path require(path); module.exports {entry: ./app.js, // Express 入口文件target: node, // 指定 Node.js 环境output: {path: path.resolve(__dirname, dist),filename: bundle.js},mode: production };打包命令 npx webpac…

CentOS 7 修改为静态 IP 地址完整指南

在企业网络环境中,服务器通常需要配置静态 IP 地址以确保网络连接的稳定性和可管理性。以下是使用 NetworkManager 工具在 CentOS 7 系统中将动态 IP 配置修改为静态 IP 的完整指南: 一、检查当前网络配置 查看网络连接状态: 使用 nmcli connection show 命令列出所有网络连…

微信小程序动态组件加载的应用场景与实现方式

动态组件加载的应用场景与实现方式 你提供的代码展示了微信小程序中动态加载组件的方法&#xff0c;但这种方式在实际开发中需要注意使用场景和实现细节。下面我来详细说明如何应用&#xff1a; 应用场景 按需加载组件&#xff1a;在某些条件满足时才加载组件动态配置组件&a…

学习记录: 使用react-router-dom

假设有一个vite创建的react项目,先npm install react-router-dom. 在app中: import { RouterProvider, createBrowserRouter } from "react-router-dom"; import Login from "./comm/Login"; import Home from "./comm/Home"; import TotalMan…

Angular中Webpack与ngx-build-plus 浅学

Webpack 在 Angular 中的概念 Webpack 是一个模块打包工具&#xff0c;用于将多个模块和资源打包成一个或多个文件。在 Angular 项目中&#xff0c;Webpack 负责将 TypeScript、HTML、CSS 等文件打包成浏览器可以理解的 JavaScript 文件。Angular CLI 默认使用 Webpack 进行项目…

java中word快速转pdf

java中word快速转pdf 网上其他方法转pdf要不转的太慢&#xff0c;要不就是损失格式&#xff0c;故而留下此方法留作备用。 文章目录 java中word快速转pdf一、依赖二、依赖包三、代码 一、依赖 <dependency><groupId>com.aspose</groupId><artifactId>…

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…

Unity使用代码分析Roslyn Analyzers

一、创建项目&#xff08;注意这里不要选netstandard2.1会有报错&#xff09; 二、NuGet上安装Microsoft.CodeAnalysis.CSharp 三、实现[Partial]特性标注的类&#xff0c;结构体&#xff0c;record必须要partial关键字修饰 需要继承DiagnosticAnalyzer 注意一定要加特性Diagn…

knife4j:4.3.0 default-flat-param-object: true 没有生效

Get 方式请求 前端接口文档中的键值对方式&#xff08;get&#xff09;发送对象参数&#xff0c;将对象请求参数展开

C++.OpenGL (15/64)Assimp(Open Asset Import Library)

Assimp(Open Asset Import Library) 3D模型加载核心流程 #mermaid-svg-cKmTZDxPpROr7ly1 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-cKmTZDxPpROr7ly1 .error-icon{fill:#552222;}#mermaid-svg-cKmTZDxPpROr…

课堂笔记:吴恩达的AI课(AI FOR EVERYONE)-第一周part2 人工智能术语人工智能公司应该怎么做

人工智能术语&人工智能公司应该怎么做 一、人工智能术语 1.机器学习&#xff1a; 让电脑能够不用开发软件&#xff0c;而自主获取某种能力的研究领域。 2.数据科学&#xff1a; 从数据中提取知识和见解的科学&#xff1b; 3.深度学习&#xff1a; 度学习是一种机器…

【服务器压力测试】本地PC电脑作为服务器运行时出现卡顿和资源紧张(Windows/Linux)

要让本地PC电脑作为服务器运行时出现卡顿和资源紧张的情况&#xff0c;可以通过以下几种方式模拟或触发&#xff1a; 1. 增加CPU负载 运行大量计算密集型任务&#xff0c;例如&#xff1a; 使用多线程循环执行复杂计算&#xff08;如数学运算、加密解密等&#xff09;。运行图…

鸿蒙开发——如何修改模拟器的显示图标/标题

1.图标 第一步&#xff1a;将你所需要的图标方到src/main/resources/base/media下 第二步&#xff1a;找到entry项目下面的src/main/module.json5 第三步&#xff1a;将原来的 "icon": "$media:layered_image", 切换成 "icon": "$media…