
简介面向图像处理与凸优化入门读者的MATLAB实践包围绕“图像上采样”这一典型问题示范如何借助CVX把插值权重的选择建模为凸优化问题并用PSNR衡量上采样图像与原始图像的接近程度。相比最近邻、双线性等常规插值方案更强调以优化方式抑制伪影、提升重建质量适合具备MATLAB基础、希望理解凸函数与优化建模如何落到图像任务的中级学习者。压缩包整体约22KB文件总数显示为0、主要文件类型明细暂未提供下载前可留意包内实际内容与运行环境。已有797人学习下载说明该主题在课程实验与小规模复现中具有一定参考度对课程设计、算法对比实验中的小规模验证与思路复现也较为友好。读者可据此梳理从读取图像、定义优化问题、调用CVX求解权重到插入像素、计算PSNR并迭代调参的完整流程也可将其作为高分辨率显示、图像分析与计算机视觉相关实验的入门样例。1. 上采样不是插值为什么 bicubic 之后再套一层凸优化能多出几个 dB把一张 128×128 的图放大四倍直觉上这是插值问题bicubic 一个函数调用就完事。但插值隐含了一个很强的先验——图像局部可以用光滑曲面逼近。这个假设在平坦区成立在边缘处必然过冲放大后的图在轮廓两侧出现一圈振铃。换一条路把上采样写成 y DHx 的逆问题在 matlab 里用凸优化cvx 建模求解把先验从「光滑」换成「分段光滑」同一张低分辨率输入PSNR 常常能多出 13 dB边缘也更干净。关键不在调包而在三个问题D 和 H 从哪来、凸函数在这件事里到底保证了什么、cvx 的 DCP 规则为什么逼着你换一种写法。搞清楚这三件事代码只是副产品。2. 观测模型y DHx 里的三个算子分别从哪来整条链路的起点不是优化是建模。绝大多数上采样代码翻车的第一个地方不是求解器不收敛而是 A 这个矩阵压根就不对——模型错了后面 PSNR 再高也是自欺欺人。线性观测模型写成y D H x nx 是高分辨率图拉直后的列向量H·W 维y 是低分辨率观测m·n 维H 是模糊矩阵D 是下采样矩阵n 是噪声。A DH 就是「上采样算子」的逆——它描述的是高分辨率图怎么变成低分辨率图的而我们要做的是解这个方程把 x 反推回来。2.1 为什么模型里必须先有一个模糊核 H如果只写 y DxD 取隔行隔列抽样那你等于在说低分辨率图的每个像素恰好等于高分辨率图某个像素的值其余全被扔掉。这在物理上不成立。真实成像里传感器的像元有面积光学系统有衍射极限降采样之前必然先做了一次低通。忽略这个低通模型就欠了最关键的一环。后果是可见的重建时优化器会把混叠aliasing产生的虚假高频当成真实细节去拟合。低分辨率图里一条斜边上的锯齿会被放大成摩尔纹或者一组平行条纹。这不是算法不行是模型在骗它。模糊核的取法有讲究。常用的实现思路是各向同性高斯标准差 σ 与缩放因子 s 挂钩一个常见的经验是 σ 取 0.5s 到 0.6s 之间也有人直接用 s×s 的盒式滤波等价于理想像元积分。选哪个会影响边缘的锐度高斯衰减慢重建结果偏软但稳定盒式滤波锐利但频谱有旁瓣容易在强边缘附近引入振铃。我在小图上做对比时一般先用盒式把链路跑通再换高斯微调因为盒式的物理含义更直白——像元积分不会有人质疑参数怎么来的。模糊矩阵本身是块 Toeplitz 结构但没必要真的写成 Toeplitz。可分离核高斯、盒式都可分离用 Kronecker 积拼更快% 一维高斯核长度 2r1 t -r:r; g exp(-(t.^2)/(2*sigma^2)); g g / sum(g); % 行方向模糊作用于每一行的 W 个像素 Gh kron(speye(H), toeplitz([g(ceil(end/2):end), zeros(1, W-numel(g))], ... [g(ceil(end/2):-1:1), zeros(1, W-numel(g))])); % 列方向同理作用在 H 维 Gv kron(toeplitz([g(ceil(end/2):end), zeros(1, H-numel(g))], ... [g(ceil(end/2):-1:1), zeros(1, H-numel(g))]), speye(W)); Hb Gv * Gh; % H*W × H*W 的稀疏模糊矩阵kron(speye(H), Gh_row)的意思是「把同一个一维模糊算子独立地作用到每一行」kron(Gv_col, speye(W))是「作用到每一列」。两次可分离操作合起来等价于二维卷积但非零元数量从 O(N·k²) 降到 O(N·k)这是能不能在普通机器上跑起来的分水岭。提示模糊矩阵的边界处理必须和后面差分算子的边界处理保持一致。H 用了 zero padding卷积核超出边界补零差分算子却用 Neumann 边界两者在图像最外一圈会打架重建结果边缘出现一圈暗边或者亮边PSNR 掉得莫名其妙。2.2 用 Kronecker 积把二维差分算子拼出来TV 正则需要图像的梯度梯度是线性算子也得写成矩阵。二维前向差分拆成水平和垂直两个方向function [Dh, Dv] grad_ops(H, W) % 前向差分保持方阵形式末行/末列的梯度自然为 0Neumann 边界 e ones(W,1); D1 spdiags([-e e], [0 1], W, W); % W×W第 i 行是 x(i1) - x(i) Dh kron(speye(H), D1); % 沿行方向逐行独立 e2 ones(H,1); D2 spdiags([-e2 e2], [0 1], H, H); Dv kron(D2, speye(W)); % 沿列方向逐列独立 end这里有个容易被忽略的细节spdiags([-e e], [0 1], W, W)造出来的是方阵最后一行只有一个-1因为x(W1)不存在超出的部分被丢掉了——等价于把边界梯度定义为 0。这正是 Neumann 边界条件。如果改成W-1 × W的瘦长矩阵梯度向量维度就变成 (W-1)H和后面的变量维度对不上norms那一步会直接报维度不匹配。方阵写法的代价是多了 HW 个恒为零的约束收益是Dh*x和Dv*x与x同维可以直接拼成 2×N 的矩阵交给 CVX 的norms原子。这个取舍值得做。Dh*x和Dv*x分别返回 H·W 维列向量第 (i,j) 个位置的元素就是该像素的水平/垂直差分。把它们按行拼起来G [Dh*x, Dv*x].; % 2 × (H*W)每一列是一个像素的梯度向量这个 2×N 的矩阵就是 TV 项的原料。每一列是一个二维梯度向量 (∂x/∂h, ∂x/∂v)对它取 2-范数得到梯度模长再对所有像素求和就是各向同性全变分。为什么是「各向同性」原因就在这里——先求模长再求和旋转不变如果分开对两个方向求绝对值再相加那是各向异性 TV会把斜边缘惩罚得比水平/垂直边缘更重结果斜边更容易被抹平。2.3 下采样算子的两种写法与半像素错位D 最直接的写法是挑点function D down_op(H, W, s) % 每 s 行、每 s 列取一个点等价于理想 decimation [r, c] ndgrid(1:s:H, 1:s:W); lin sub2ind([H W], r(:), c(:)); D sparse(1:numel(lin), lin, 1, numel(lin), H*W); endndgrid生成所有采样点的行列坐标sub2ind转成线性索引sparse用一个 1 的对角把这些位置挑出来。这个矩阵每行只有一个 1极度稀疏构造开销可以忽略。另一种写法是先模糊再抽样把 D 和 H 合成一个矩阵 A D*Hb。合成的好处是 CVX 里只出现一个线性映射表达式更短坏处是 A 的非零元是 Hb 每行的非零元数乘以采样率核稍大一点内存就上去了。用分离形式D*(Hb*x)在 CVX 里同样合法两个常数矩阵夹一个变量是线性表达式建模阶段不会真的把乘积展开成一个稠密矩阵。真正会翻车的是栅格对齐。imresize(img, 1/s, bicubic)的像素中心映射是(i-0.5)/s 0.5也就是低分辨率第 i 个像素的中心对到高分辨率坐标(i-0.5)s 0.5而down_op取的是1:s:H即高分辨率第 1、1s、12s… 个像素。这两套栅格差半个像素。半个像素的平移对 PSNR 的杀伤力有多大在纹理丰富的图上足以让优化后的结果比 bicubic 还低 23 dB而且肉眼看不出问题只觉得「怎么糊了」。自洽的做法是要么低分辨率图也用down_op生成做仿真实验时最稳要么把 D 的采样起点改成ceil(s/2):s:H去对齐imresize的约定。两条路选一条走到底别混用。3. 凸函数在这里救了什么从无穷多解到全局最优模型搭好接下来是解方程。A是 (m·n) × (H·W) 的矩阵行数远少于列数——四倍上采样时方程数量只有未知数的十六分之一。这是一个严重欠定的系统解集是一整个仿射子空间不是单点。3.1 零空间与病态性没有正则项时解为什么满天飞只最小化数据项min ||A x - y||²这个问题的解集是 x₀ null(A)null(A) 的维数至少是 H·W - m·n。也就是说在满足「降采样之后和观测一致」的前提下你可以往解里加任意多的高频成分而目标函数值不变。最小二乘会给一个最小范数解但那个解在视觉上很糟——它把所有不在 A 的值域里的高频都设成 0结果就是一张糊图比 bicubic 还糊。更麻烦的是数值层面。A 的条件数极大因为模糊核把高频压到了接近 0再做 decimation很多奇异值小到 1e-10 量级。直接求伪逆噪声会被放大成高频雪花。这就是典型的病态逆问题不是没解是解太多且都不稳定。正则化就是在这时候进场的min ||A x - y||² λ R(x)R(x) 编码的是「什么样的图看起来像自然图像」这个先验。λ 控制先验和数据之间的相对权重。R 的选择决定了重建结果长什么样。3.2 L2 与 TV 的凸性几何直觉和一条能自己验证的思路R(x) ||x||²Tikhonov是最省事的它让整个目标变成严格凸的二次型有闭式解(AᵀA λI)⁻¹Aᵀy一次线性求解搞定。代价是它惩罚大梯度等于在说「图像应该处处平坦」边缘被均匀地糊掉。放大后的图看着像隔着一层毛玻璃。R(x) TV(x) Σᵢ √((D_h x)ᵢ² (D_v x)ᵢ²) 换了个说法只惩罚梯度的模长不惩罚梯度的大小。平坦区梯度接近 0惩罚也接近 0边缘处梯度很大但因为是 L1 型惩罚模长的一次方不是平方代价线性增长而不是平方增长。结果就是优化器宁可保留少量大梯度边缘也要把大量小梯度噪声、纹理起伏压到 0。这就是 TV 保边的机制来源。两者都是凸函数这一点可以自己验证不用背结论2-范数||·||₂是凸的这是三角不等式直接给的仿射映射复合保凸D_h x是 x 的线性函数所以||D_h x||₂关于 x 凸非负加权和保凸Σᵢ 凸函数 凸函数。所以 TV(x) 是「凸函数 ∘ 线性映射」的和凸。数据项||Ax - y||²是凸二次函数加一个 λ0 的凸正则整体还是凸。凸性带来的不是「解更好看」而是「解可预测」。非凸目标用梯度下降换一个初值、换一个学习率、换一个 λ落到的局部极小可能完全不同调试过程变成玄学这次跑出来边缘很锐下次同样的参数跑出来糊成一团你根本不知道是代码改了还是随机种子变了。凸问题不存在这个问题——最优性由 KKT 条件完全刻画只要解满足 KKT它就是全局最优没有第二个。λ 连续变化时解沿着一条连续路径移动你可以放心地扫 λ 看趋势。TV 有个小毛病它凸但不严格凸。在整块平坦区域里把这块区域整体加一个常数TV 不变数据项因为经过模糊和下采样可能也几乎不变。所以解不唯一。实践中不影响因为目标函数值唯一PSNR 也就唯一只是求解器可能在不同次运行给出略有差异的图。3.3 λ 不是玄学用偏差原则定量纲λ 的取值经常被当成调参玄学其实它有明确的下界。如果噪声是方差为 σ² 的高斯白噪声那么在任何「合理」的解上残差应该和噪声量级相当||A x - y||² ≈ (m·n)·σ²这就是偏差原则Morozov discrepancy principle。λ 太小残差远小于噪声水平说明优化器在拟合噪声图上有雪花λ 太大残差远大于噪声水平说明数据项被忽略解趋近于「纯 TV 最小化」的结果图上出现阶梯状的色块。实操上把 y 归一化到 [0,1] 之后λ 的合理区间通常在 1e-3 到 1e-1。先用 1e-2 跑一次看残差如果||Ax-y||²比(m·n)σ²小一个数量级λ 加一个数量级反之减。两三轮就能落到合理区间。比网格搜索快得多而且每一步都有物理依据不是碰运气。注意λ 的量纲依赖 A 的尺度。如果 A 的列没有归一化或者 y 还是 0-255 的 uint8λ 的绝对数值会差好几个数量级抄别人的参数必然翻车。先把 y 归一化、把 A 的谱范数估出来再谈 λ。4. CVX 建模DCP 规则下 TV 项为什么不能直接写 sqrt模型清楚了落到 cvx 上。第一次写 TV 的人几乎都会撞同一堵墙——把数学公式原样抄进代码然后被 DCP 报错拦下来。4.1 数据项与 sum_square 的锥转换数据项||Ax - y||²在 CVX 里对应sum_square(A*x - y)。这是一个合法的凸原子CVX 内部会把它转成二阶锥约束||Ax - y||² ≤ t ⟺ ||[2(Ax-y); t-1]||₂ ≤ t1内点法求解器SDPT3、SeDuMi处理的是标准锥形式所以每一个平方项最终都会变成锥。这也是为什么 CVX 的规模上不去——锥的数量和变量维度同阶而内点法每步迭代要做一次大规模线性方程组求解复杂度随锥数量超线性增长。sum_square和square_pos(sum(...))不是一回事。前者是「先逐元素平方再求和」后者是「先求和再平方」梯度结构完全不同别混。数据项要的是前者。4.2 用 norms 原子写逐像素梯度模长数学上 TV 是sum(sqrt(dh.^2 dv.^2))。直接写tv sum(sqrt((Dh*x).^2 (Dv*x).^2)); % 会被 CVX 拒绝报错大致是Illegal operation: sqrt( {convex} )。原因在 DCP 规则sqrt在 CVX 里被登记为凹函数concave规则是「凹函数只能出现在 ≥ 约束的左边或者 maximize 的目标里」。这里 sqrt 的自变量是凸的凸函数套凹函数CVX 无法判定整体凸性直接拒绝。绕过的方法是用 CVX 专门为「范数之和」准备的原子normscvx_begin quiet cvx_solver sdpt3 variable xh(H, W) x xh(:); G [Dh*x, Dv*x].; % 2 × (H*W)每列一个梯度向量 tv norms(G, 2, 1); % 逐列 2-范数再求和 minimize( 0.5*sum_square(A*x - y) lambda*tv ) subject to xh 0; xh 1; cvx_endnorms(G, 2, 1)的语义是沿第一维把 G 切成一个个列向量分别求 2-范数再把所有范数加起来。G 是 2×N沿第一维切切出来的就是每个像素的二维梯度向量正好对上 TV 的定义。CVX 内部把它一次性转成 N 个三维二阶锥建模阶段的符号处理是向量化的速度可以接受。注意norms的维度参数在不同 CVX 版本上的行为不完全一致有的版本dim1表示按列分组有的表示按行。第一次跑如果报维度错误或者解出来的图完全没有 TV 效果梯度没被惩罚把1换成2试一次两分钟能确认。4.3 手写二阶锥约束循环写法为什么在 CVX 里会卡死想绕开norms的话可以引入辅助变量 t 手动写锥约束cvx_begin quiet variable xh(H, W) variable t(H, W) x xh(:); minimize( 0.5*sum_square(A*x - y) lambda*sum(t(:)) ) subject to for i 1:H*W norm([Dh(i,:)*x; Dv(i,:)*x]) t(i); end cvx_end数学上没错norm(·) ≤ t就是标准的二阶锥约束。但这段代码在 128×128 的图上就能让你等到怀疑人生H·W 16384 次循环每次循环 CVX 都要做一遍表达式树的 DCP 检查、注册一个新的锥约束、维护约束列表。CVX 是解释执行的建模层不是编译型 DSL单次约束注册的开销在毫秒量级一万六千次就是几十秒到几分钟而且内存占用随约束数量线性上涨。256×256 时是 65536 次基本等于挂死。向量化写法和循环写法在数学上等价在 CVX 里的建模时间差两三个数量级。这是 CVX 使用中最值得记住的一条经验能用原子就别用循环原子内部的锥转换是 C 层实现的。顺带说一句上面这段循环写法还有一个隐藏 bugDh(i,:)*x里Dh是稀疏矩阵逐行取出来做矩阵向量乘在 MATLAB 里每次都要重新索引稀疏结构比稠密矩阵慢得多。真要写循环先把Dh、Dv转成 dense但那样内存又爆了。两头堵。4.4 求解器与精度SDPT3、SeDuMi 和 cvx_precisionCVX 默认调 SDPT3。对纯 SOCP没有半定锥的问题SeDuMi 往往内存更省尤其是在锥数量很大的时候。切换只需要一行cvx_begin quiet cvx_solver sedumi cvx_precision medium ... cvx_endcvx_precision medium把内点法的终止容差从默认的 ~1e-8 放宽到 ~1e-4。对图像重建1e-4 的精度早就够了肉眼分辨不出来但迭代次数能少三到四成。这个开关在调参阶段特别值——你要跑十几个 λ 看趋势每个都高精度是浪费。如果求解器报Failed或者Infeasible先别怀疑模型。最常见的原因是约束写冲突比如同时写了xh 0和某个隐含要求负值的项其次是数据里有 NaNimresize在极端缩放下可能产生CVX 遇到 NaN 会静默失败报一个和 NaN 毫无关系的错。养成习惯进 CVX 之前先any(isnan(y))检查一次。5. 避坑与排查PSNR 算错、DCP 报错和内存爆掉时先看哪里写这类代码时间基本不花在「想出方法」上花在「为什么跑出来不对」上。下面几条是踩得最深的。5.1 PSNR 的两个隐形错误数据范围和通道PSNR 的定义简单function p psnr_metric(ref, est, peak) % ref, est: 同尺寸矩阵double 类型取值范围 [0, peak] if nargin 3, peak 1; end d double(ref) - double(est); mse mean(d(:).^2); p 10*log10(peak^2 / mse); end两个参数能让你算出的数值偏离真值几十 dB而且看不出错。第一个是peak。数据归一化到 [0,1] 时 peak1数据是 0-255 时 peak255。搞混了会差20*log10(255) ≈ 48 dB。更隐蔽的是重建结果归一化到 [0,1]参考图还是 uint8 的 0-255两者相减得到的 mse 巨大PSNR 变成负数——你会以为算法彻底失败其实是单位没统一。第二个是通道。彩色图直接对 R、G、B 三个通道求平均 PSNR数值会虚高 12 dB因为色度通道经过下采样后本来就平滑重建误差小拉高了平均值。学界惯例是转 YCbCr 只算 Y 通道ycc_ref rgb2ycbcr(ref); ycc_est rgb2ycbcr(est); p psnr_metric(ycc_ref(:,:,1), ycc_est(:,:,1), 255);还有两个细节上采样后的图外围有 s-1 圈像素是靠边界外推得到的误差偏大算 PSNR 前裁掉两张图如果有整像素平移先做位移搜索对齐再算否则纹理区错位会让 PSNR 平白掉 23 dB。5.2 五条踩坑记录一、Cannot assign values to a CVX variable现象想用 bicubic 的结果当初值xh imresize(y, s, bicubic)写在variable xh(H,W)后面直接报错。原因CVX 的变量是符号对象不是数值容器声明之后不允许赋值。CVX 也不支持热启动内点法本来也不需要初值。解决删掉赋值语句。如果确实需要初值加速比如跑 FISTA那就不该用 CVX换一阶方法。二、Illegal operation: sqrt( {convex} )现象把 TV 的数学定义原样抄进minimize。原因sqrt是凹原子套在凸表达式外面违反 DCP。解决用norms(G,2,1)或者引入辅助变量 t写norm([dh;dv]) t目标里用sum(t(:))。后者在变量维度小的时候更直观。三、解出来整张图偏灰或者几乎全黑现象PSNR 极低图看着像被压平了。原因三种可能。y 是 uint8 没转 doubleCVX 隐式转换时丢精度甚至报错λ 大了两三个数量级数据项被彻底压制变量没加 [0,1] 约束解跑到范围外显示时被 clamp 成一片。解决进 CVX 前统一y double(y)/255用 3.3 节的残差判据定 λ把xh 0; xh 1;加上——这两条不只是为了显示它们是真的在缩小可行域能显著加快收敛。四、重建 PSNR 比 bicubic 还低现象优化跑完了一算比插值还差。原因栅格错位2.3 节或者模糊核 σ 与缩放因子不匹配。σ 取太小模型以为低分辨率图里保留了更多高频重建时过度锐化σ 取太大模型以为高频全丢了重建结果偏软。解决仿真实验里用自己构造的 D、H 生成低分辨率图保证模型自洽先确认代码链路对再换真实降采样图换真实图时扫一遍 σ ∈ {0.4s, 0.5s, 0.6s, 0.7s}看 PSNR 曲线的峰值在哪。五、求解几十分钟不收敛或者报内存不足现象128×128 还能跑256×256 卡死。原因变量维度 H·W 和锥约束数量同阶内点法的每次迭代要做一次大规模稀疏分解内存随问题规模超线性增长。256×256 是 65536 维变量、65536 个三维锥SDPT3 吃掉几 GB 很正常。解决按优先级——先开cvx_precision medium换cvx_solver sedumi把图分成 64×64 的有重叠块分别解再融合重叠区加权平均权重用余弦窗都不行就换 ADMM。CVX 本来就不是给大图准备的。6. 设计取舍CVX 该停在多大规模什么时候必须换 ADMM把 CVX 用在这个问题上最需要想清楚的是它的定位。它不是生产工具是一把标尺。内点法的迭代次数对精度是对数级的看起来很美但每次迭代的代价是求解一个规模随锥数量增长的线性系统。CVX 在建模阶段把问题转成标准锥形式这个转换本身是 O(N) 的符号操作真正吃资源的是求解器内部的稀疏 Cholesky 分解。经验边界很清楚128×128 以内几十秒到几分钟能出结果256×256 开始进入「泡杯茶回来看看」的区间再往上内存和时间都不划算。替代方案是 ADMM。它把问题拆成两块min f(x) g(z) s.t. Dx zx 子问题是最小二乘在循环边界下AᵀA ρDᵀD可以被 FFT 对角化一次正反变换加逐点除法就解完复杂度 O(N log N)z 子问题是 TV 的近端算子逐像素做二维收缩把梯度向量整体往原点缩模长小于阈值就归零闭式解。每次迭代都是 O(N log N)没有矩阵分解内存只有几个 N 维向量。方法单步复杂度128×128256×256内存适合拿来干什么CVX SDPT3稀疏分解随锥数超线性秒到分钟级分钟到小时级高GB 量级验证模型、生成基准解ADMM FFTO(N log N)亚秒级秒级低百 MB 内大批量实验、调 λFISTAO(N log N)亚秒级秒级低需要单调下降的目标值时表里的时间只是量级具体数字取决于机器和核大小但相对关系是稳的同一张 256×256 的图ADMM 跑几百次迭代的总时间可能还不到 CVX 建模阶段的时间。那为什么还要用 CVX因为它是后悔药。手写 ADMM 的时候x 子问题的边界条件、z 子问题的收缩阈值、ρ 的取值、残差判据任何一处写错结果都不会报错只会悄悄变糊或者出条纹。你没法判断是「算法收敛到最优了但 λ 不对」还是「算法压根没收敛」。这时候在小图上用 CVX 跑一遍拿到目标函数的最优值和重建图就有了一个绝对参照ADMM 跑出来的目标值如果比 CVX 高 0.5%那就是没收敛如果一样但图不同那就是解不唯一TV 的老毛病不用管。这个做法我在好几个项目里都用过。有一次写一个带方向梯度的各向异性 TVADMM 跑出来边缘总有锯齿调了两天参数没结果。后来在 64×64 的小图上用 CVX 跑同样的模型发现 CVX 的结果也带锯齿——问题不在算法在模型本身各向异性 TV 对斜边就是不友好。如果没有那个基准可能还要再调两天。从那以后我每加一个新正则项都先用 CVX 在 64×64 的小图上跑一遍当基准确认目标函数和解的形状都对再换 ADMM 上大图。多花的这十分钟省下的是后面几小时的自我怀疑。资源里的脚本按「构造算子 → CVX 求解 → PSNR 评估」拆成了几段建议先单独跑算子构造那一段把 A 的维度和稀疏度打出来看一眼再往下走——很多问题在这一步就能暴露。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。