HarmonyOS开发中的图像裁剪缩放:智能裁剪、等比缩放、插值算法与批量处理
核心要点:掌握图像裁剪缩放的最佳实践,理解智能裁剪算法原理,深入对比不同插值算法的优劣,实现高效的批量图片处理管线
一、背景与动机
你有没有遇到过这样的场景:用户上传了一张 4:3 的照片,但你的应用需要 1:1 的头像、16:9 的封面、9:16 的故事封面?你总不能简单粗暴地拉伸吧------人脸变形、建筑歪斜,用户分分钟卸载你的应用。
裁剪和缩放看似简单------不就是"切一块"和"放大缩小"嘛?但实际做起来,问题一个接一个:裁剪框怎么选才不切到人脸?缩放后图片模糊怎么办?批量处理几百张图片怎么不卡UI?每一个都是实打实的工程问题。
今天我们就从最基础的裁剪缩放开始,一路深入到智能裁剪算法、插值算法对比、批量处理管线,把这些问题彻底搞清楚。
二、核心原理
2.1 裁剪策略对比
不同的裁剪策略适用于不同的场景:
#mermaid-svg-b7RjO7nfXVRvuP5g{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-b7RjO7nfXVRvuP5g .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-b7RjO7nfXVRvuP5g .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-b7RjO7nfXVRvuP5g .error-icon{fill:#552222;}#mermaid-svg-b7RjO7nfXVRvuP5g .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-b7RjO7nfXVRvuP5g .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-b7RjO7nfXVRvuP5g .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-b7RjO7nfXVRvuP5g .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-b7RjO7nfXVRvuP5g .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-b7RjO7nfXVRvuP5g .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-b7RjO7nfXVRvuP5g .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-b7RjO7nfXVRvuP5g .marker{fill:#333333;stroke:#333333;}#mermaid-svg-b7RjO7nfXVRvuP5g .marker.cross{stroke:#333333;}#mermaid-svg-b7RjO7nfXVRvuP5g svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-b7RjO7nfXVRvuP5g p{margin:0;}#mermaid-svg-b7RjO7nfXVRvuP5g .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-b7RjO7nfXVRvuP5g .cluster-label text{fill:#333;}#mermaid-svg-b7RjO7nfXVRvuP5g .cluster-label span{color:#333;}#mermaid-svg-b7RjO7nfXVRvuP5g .cluster-label span p{background-color:transparent;}#mermaid-svg-b7RjO7nfXVRvuP5g .label text,#mermaid-svg-b7RjO7nfXVRvuP5g span{fill:#333;color:#333;}#mermaid-svg-b7RjO7nfXVRvuP5g .node rect,#mermaid-svg-b7RjO7nfXVRvuP5g .node circle,#mermaid-svg-b7RjO7nfXVRvuP5g .node ellipse,#mermaid-svg-b7RjO7nfXVRvuP5g .node polygon,#mermaid-svg-b7RjO7nfXVRvuP5g .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-b7RjO7nfXVRvuP5g .rough-node .label text,#mermaid-svg-b7RjO7nfXVRvuP5g .node .label text,#mermaid-svg-b7RjO7nfXVRvuP5g .image-shape .label,#mermaid-svg-b7RjO7nfXVRvuP5g .icon-shape .label{text-anchor:middle;}#mermaid-svg-b7RjO7nfXVRvuP5g .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-b7RjO7nfXVRvuP5g .rough-node .label,#mermaid-svg-b7RjO7nfXVRvuP5g .node .label,#mermaid-svg-b7RjO7nfXVRvuP5g .image-shape .label,#mermaid-svg-b7RjO7nfXVRvuP5g .icon-shape .label{text-align:center;}#mermaid-svg-b7RjO7nfXVRvuP5g .node.clickable{cursor:pointer;}#mermaid-svg-b7RjO7nfXVRvuP5g .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-b7RjO7nfXVRvuP5g .arrowheadPath{fill:#333333;}#mermaid-svg-b7RjO7nfXVRvuP5g .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-b7RjO7nfXVRvuP5g .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-b7RjO7nfXVRvuP5g .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-b7RjO7nfXVRvuP5g .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-b7RjO7nfXVRvuP5g .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-b7RjO7nfXVRvuP5g .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-b7RjO7nfXVRvuP5g .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-b7RjO7nfXVRvuP5g .cluster text{fill:#333;}#mermaid-svg-b7RjO7nfXVRvuP5g .cluster span{color:#333;}#mermaid-svg-b7RjO7nfXVRvuP5g div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-b7RjO7nfXVRvuP5g .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-b7RjO7nfXVRvuP5g rect.text{fill:none;stroke-width:0;}#mermaid-svg-b7RjO7nfXVRvuP5g .icon-shape,#mermaid-svg-b7RjO7nfXVRvuP5g .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-b7RjO7nfXVRvuP5g .icon-shape p,#mermaid-svg-b7RjO7nfXVRvuP5g .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-b7RjO7nfXVRvuP5g .icon-shape .label rect,#mermaid-svg-b7RjO7nfXVRvuP5g .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-b7RjO7nfXVRvuP5g .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-b7RjO7nfXVRvuP5g .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-b7RjO7nfXVRvuP5g :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-b7RjO7nfXVRvuP5g .primary>*{fill:#4CAF50!important;stroke:#2E7D32!important;color:#fff!important;stroke-width:2px!important;}#mermaid-svg-b7RjO7nfXVRvuP5g .primary span{fill:#4CAF50!important;stroke:#2E7D32!important;color:#fff!important;stroke-width:2px!important;}#mermaid-svg-b7RjO7nfXVRvuP5g .primary tspan{fill:#fff!important;}#mermaid-svg-b7RjO7nfXVRvuP5g .warning>*{fill:#FF9800!important;stroke:#E65100!important;color:#fff!important;stroke-width:2px!important;}#mermaid-svg-b7RjO7nfXVRvuP5g .warning span{fill:#FF9800!important;stroke:#E65100!important;color:#fff!important;stroke-width:2px!important;}#mermaid-svg-b7RjO7nfXVRvuP5g .warning tspan{fill:#fff!important;}#mermaid-svg-b7RjO7nfXVRvuP5g .info>*{fill:#2196F3!important;stroke:#1565C0!important;color:#fff!important;stroke-width:2px!important;}#mermaid-svg-b7RjO7nfXVRvuP5g .info span{fill:#2196F3!important;stroke:#1565C0!important;color:#fff!important;stroke-width:2px!important;}#mermaid-svg-b7RjO7nfXVRvuP5g .info tspan{fill:#fff!important;}#mermaid-svg-b7RjO7nfXVRvuP5g .error>*{fill:#F44336!important;stroke:#C62828!important;color:#fff!important;stroke-width:2px!important;}#mermaid-svg-b7RjO7nfXVRvuP5g .error span{fill:#F44336!important;stroke:#C62828!important;color:#fff!important;stroke-width:2px!important;}#mermaid-svg-b7RjO7nfXVRvuP5g .error tspan{fill:#fff!important;}#mermaid-svg-b7RjO7nfXVRvuP5g .purple>*{fill:#9C27B0!important;stroke:#6A1B9A!important;color:#fff!important;stroke-width:2px!important;}#mermaid-svg-b7RjO7nfXVRvuP5g .purple span{fill:#9C27B0!important;stroke:#6A1B9A!important;color:#fff!important;stroke-width:2px!important;}#mermaid-svg-b7RjO7nfXVRvuP5g .purple tspan{fill:#fff!important;} 图像裁剪策略
中心裁剪
Center Crop
智能裁剪
Smart Crop
人脸优先裁剪
Face-aware Crop
内容感知裁剪
Seam Carving
最简单最常用
可能切掉重要内容
基于显著性检测
保留视觉焦点区域
检测人脸位置
人脸始终在裁剪框内
动态规划找低能量线
不丢失重要内容
2.2 智能裁剪的原理
智能裁剪的核心思路是:找到图片中"最重要"的区域,确保它被保留在裁剪框内。
判断"重要性"有几种方法:
| 方法 | 原理 | 优缺点 |
|---|---|---|
| 中心先验 | 假设重要内容在中心 | 简单但不总是对 |
| 显著性检测 | 检测视觉上"突出"的区域 | 较准确,但计算量大 |
| 边缘密度 | 边缘密集的区域通常有内容 | 中等准确,计算量适中 |
| 人脸检测 | 直接检测人脸位置 | 最准确,但依赖人脸检测API |
本文实现的是边缘密度法------通过计算图片各区域的边缘密度,找到信息量最丰富的区域作为裁剪中心。
2.3 缩放插值算法
缩放时,目标图像的像素需要从源图像中"推算"出来。不同的推算方法就是不同的插值算法:
| 算法 | 原理 | 质量 | 速度 | 适用场景 |
|---|---|---|---|---|
| 最近邻 | 取最近的源像素 | ★☆☆ | ★★★ | 缩略图、像素风 |
| 双线性 | 4个邻域加权平均 | ★★☆ | ★★☆ | 通用场景 |
| 双三次 | 16个邻域加权平均 | ★★★ | ★☆☆ | 高质量放大 |
| Lanczos | sinc函数核 | ★★★★ | ★☆☆ | 专业图像处理 |
#mermaid-svg-SyiLo1t0GdLiIlfL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SyiLo1t0GdLiIlfL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SyiLo1t0GdLiIlfL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SyiLo1t0GdLiIlfL .error-icon{fill:#552222;}#mermaid-svg-SyiLo1t0GdLiIlfL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SyiLo1t0GdLiIlfL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SyiLo1t0GdLiIlfL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SyiLo1t0GdLiIlfL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SyiLo1t0GdLiIlfL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SyiLo1t0GdLiIlfL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SyiLo1t0GdLiIlfL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SyiLo1t0GdLiIlfL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SyiLo1t0GdLiIlfL .marker.cross{stroke:#333333;}#mermaid-svg-SyiLo1t0GdLiIlfL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SyiLo1t0GdLiIlfL p{margin:0;}#mermaid-svg-SyiLo1t0GdLiIlfL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SyiLo1t0GdLiIlfL .cluster-label text{fill:#333;}#mermaid-svg-SyiLo1t0GdLiIlfL .cluster-label span{color:#333;}#mermaid-svg-SyiLo1t0GdLiIlfL .cluster-label span p{background-color:transparent;}#mermaid-svg-SyiLo1t0GdLiIlfL .label text,#mermaid-svg-SyiLo1t0GdLiIlfL span{fill:#333;color:#333;}#mermaid-svg-SyiLo1t0GdLiIlfL .node rect,#mermaid-svg-SyiLo1t0GdLiIlfL .node circle,#mermaid-svg-SyiLo1t0GdLiIlfL .node ellipse,#mermaid-svg-SyiLo1t0GdLiIlfL .node polygon,#mermaid-svg-SyiLo1t0GdLiIlfL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SyiLo1t0GdLiIlfL .rough-node .label text,#mermaid-svg-SyiLo1t0GdLiIlfL .node .label text,#mermaid-svg-SyiLo1t0GdLiIlfL .image-shape .label,#mermaid-svg-SyiLo1t0GdLiIlfL .icon-shape .label{text-anchor:middle;}#mermaid-svg-SyiLo1t0GdLiIlfL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SyiLo1t0GdLiIlfL .rough-node .label,#mermaid-svg-SyiLo1t0GdLiIlfL .node .label,#mermaid-svg-SyiLo1t0GdLiIlfL .image-shape .label,#mermaid-svg-SyiLo1t0GdLiIlfL .icon-shape .label{text-align:center;}#mermaid-svg-SyiLo1t0GdLiIlfL .node.clickable{cursor:pointer;}#mermaid-svg-SyiLo1t0GdLiIlfL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SyiLo1t0GdLiIlfL .arrowheadPath{fill:#333333;}#mermaid-svg-SyiLo1t0GdLiIlfL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SyiLo1t0GdLiIlfL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SyiLo1t0GdLiIlfL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SyiLo1t0GdLiIlfL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SyiLo1t0GdLiIlfL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SyiLo1t0GdLiIlfL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SyiLo1t0GdLiIlfL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SyiLo1t0GdLiIlfL .cluster text{fill:#333;}#mermaid-svg-SyiLo1t0GdLiIlfL .cluster span{color:#333;}#mermaid-svg-SyiLo1t0GdLiIlfL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SyiLo1t0GdLiIlfL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SyiLo1t0GdLiIlfL rect.text{fill:none;stroke-width:0;}#mermaid-svg-SyiLo1t0GdLiIlfL .icon-shape,#mermaid-svg-SyiLo1t0GdLiIlfL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SyiLo1t0GdLiIlfL .icon-shape p,#mermaid-svg-SyiLo1t0GdLiIlfL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SyiLo1t0GdLiIlfL .icon-shape .label rect,#mermaid-svg-SyiLo1t0GdLiIlfL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SyiLo1t0GdLiIlfL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SyiLo1t0GdLiIlfL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SyiLo1t0GdLiIlfL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-SyiLo1t0GdLiIlfL .primary>*{fill:#4CAF50!important;stroke:#2E7D32!important;color:#fff!important;stroke-width:2px!important;}#mermaid-svg-SyiLo1t0GdLiIlfL .primary span{fill:#4CAF50!important;stroke:#2E7D32!important;color:#fff!important;stroke-width:2px!important;}#mermaid-svg-SyiLo1t0GdLiIlfL .primary tspan{fill:#fff!important;}#mermaid-svg-SyiLo1t0GdLiIlfL .warning>*{fill:#FF9800!important;stroke:#E65100!important;color:#fff!important;stroke-width:2px!important;}#mermaid-svg-SyiLo1t0GdLiIlfL .warning span{fill:#FF9800!important;stroke:#E65100!important;color:#fff!important;stroke-width:2px!important;}#mermaid-svg-SyiLo1t0GdLiIlfL .warning tspan{fill:#fff!important;}#mermaid-svg-SyiLo1t0GdLiIlfL .info>*{fill:#2196F3!important;stroke:#1565C0!important;color:#fff!important;stroke-width:2px!important;}#mermaid-svg-SyiLo1t0GdLiIlfL .info span{fill:#2196F3!important;stroke:#1565C0!important;color:#fff!important;stroke-width:2px!important;}#mermaid-svg-SyiLo1t0GdLiIlfL .info tspan{fill:#fff!important;}#mermaid-svg-SyiLo1t0GdLiIlfL .error>*{fill:#F44336!important;stroke:#C62828!important;color:#fff!important;stroke-width:2px!important;}#mermaid-svg-SyiLo1t0GdLiIlfL .error span{fill:#F44336!important;stroke:#C62828!important;color:#fff!important;stroke-width:2px!important;}#mermaid-svg-SyiLo1t0GdLiIlfL .error tspan{fill:#fff!important;}#mermaid-svg-SyiLo1t0GdLiIlfL .purple>*{fill:#9C27B0!important;stroke:#6A1B9A!important;color:#fff!important;stroke-width:2px!important;}#mermaid-svg-SyiLo1t0GdLiIlfL .purple span{fill:#9C27B0!important;stroke:#6A1B9A!important;color:#fff!important;stroke-width:2px!important;}#mermaid-svg-SyiLo1t0GdLiIlfL .purple tspan{fill:#fff!important;} 放大
缩小
缩放操作
放大 or 缩小?
双三次/Lanczos
避免锯齿和模糊
先低通滤波
再降采样
避免混叠
HarmonyOS默认
双线性插值
高斯模糊+降采样
抗混叠
2.4 缩小图像的混叠问题
缩小图像时,如果直接丢弃像素(降采样),高频细节会产生"摩尔纹"------这就是混叠(Aliasing)。正确的做法是先做低通滤波(模糊),再降采样。
HarmonyOS 的 PixelMap.scale() 内部已经处理了这个问题,但如果手动实现缩放,必须注意抗混叠。
三、代码实战
3.1 中心裁剪与等比缩放
最基础但最常用的组合操作------中心裁剪到目标比例,再等比缩放到目标尺寸。
typescript
import { image } from '@kit.ImageKit';
import { fileIo as fs } from '@kit.CoreFileKit';
/**
* 图像裁剪缩放工具类
*/
export class CropScaleHelper {
/**
* 中心裁剪到指定宽高比
* 保留图片中心区域,裁掉多余部分
*
* @param pixelMap 目标图片
* @param targetRatio 目标宽高比 (width/height),如 16/9, 1/1, 4/3
*/
static async centerCropToRatio(pixelMap: PixelMap, targetRatio: number): Promise<void> {
const info = await pixelMap.getImageInfo();
const srcW = info.size.width;
const srcH = info.size.height;
const srcRatio = srcW / srcH;
let cropX: number, cropY: number, cropW: number, cropH: number;
if (srcRatio > targetRatio) {
// 原图更宽,裁左右
cropH = srcH;
cropW = Math.round(srcH * targetRatio);
cropX = Math.round((srcW - cropW) / 2);
cropY = 0;
} else {
// 原图更高,裁上下
cropW = srcW;
cropH = Math.round(srcW / targetRatio);
cropX = 0;
cropY = Math.round((srcH - cropH) / 2);
}
await pixelMap.crop({
x: cropX,
y: cropY,
size: { width: cropW, height: cropH }
});
}
/**
* 等比缩放到指定尺寸(Fit模式)
* 保持宽高比,图片完整显示,可能有留白
*
* @param pixelMap 目标图片
* @param maxWidth 最大宽度
* @param maxHeight 最大高度
*/
static async scaleToFit(
pixelMap: PixelMap,
maxWidth: number,
maxHeight: number
): Promise<void> {
const info = await pixelMap.getImageInfo();
const srcW = info.size.width;
const srcH = info.size.height;
// 计算等比缩放比例(取较小的,确保图片完整显示)
const scale = Math.min(maxWidth / srcW, maxHeight / srcH);
if (scale !== 1.0) {
await pixelMap.scale(scale, scale);
}
}
/**
* 等比缩放到填满指定尺寸(Fill模式)
* 保持宽高比,图片填满目标区域,可能裁掉部分
*
* @param pixelMap 目标图片
* @param targetWidth 目标宽度
* @param targetHeight 目标高度
*/
static async scaleToFill(
pixelMap: PixelMap,
targetWidth: number,
targetHeight: number
): Promise<void> {
const info = await pixelMap.getImageInfo();
const srcRatio = info.size.width / info.size.height;
const targetRatio = targetWidth / targetHeight;
// 先裁剪到目标比例
await this.centerCropToRatio(pixelMap, targetRatio);
// 再缩放到目标尺寸
const croppedInfo = await pixelMap.getImageInfo();
const scale = targetWidth / croppedInfo.size.width;
await pixelMap.scale(scale, scale);
}
/**
* 生成指定尺寸的缩略图
* 综合使用降采样解码 + 缩放,性能最优
*
* @param imageSource 图像源
* @param thumbSize 缩略图最大边长
*/
static async createThumbnail(
imageSource: image.ImageSource,
thumbSize: number
): Promise<PixelMap> {
// 先获取原始尺寸
const sourceInfo = await imageSource.getImageInfo();
const srcW = sourceInfo.size.width;
const srcH = sourceInfo.size.height;
// 计算合适的 sampleSize(2的幂次)
const maxDim = Math.max(srcW, srcH);
let sampleSize = 1;
while (maxDim / (sampleSize * 2) > thumbSize * 1.5) {
sampleSize *= 2;
}
// 使用 sampleSize 降采样解码
const pixelMap = await imageSource.createPixelMap({
sampleSize: sampleSize,
editable: true,
desiredPixelFormat: image.PixelMapFormat.RGBA_8888,
});
// 精确缩放到目标尺寸
await this.scaleToFit(pixelMap, thumbSize, thumbSize);
return pixelMap;
}
}
3.2 智能裁剪:基于边缘密度的显著性检测
typescript
import { image } from '@kit.ImageKit';
/**
* 智能裁剪算法
* 通过分析图片各区域的"信息量"来决定最佳裁剪位置
*/
export class SmartCropHelper {
/**
* 智能裁剪到指定宽高比
* 自动找到图片中信息量最丰富的区域
*
* @param pixelMap 目标图片
* @param targetRatio 目标宽高比
* @param options 裁剪选项
*/
static async smartCrop(
pixelMap: PixelMap,
targetRatio: number,
options?: SmartCropOptions
): Promise<void> {
const opts: SmartCropOptions = {
stepSize: options?.stepSize ?? 8, // 搜索步长(越大越快但越粗糙)
edgeWeight: options?.edgeWeight ?? 1.0, // 边缘密度权重
centerWeight: options?.centerWeight ?? 0.3, // 中心先验权重
...options
};
const info = await pixelMap.getImageInfo();
const srcW = info.size.width;
const srcH = info.size.height;
// 1. 计算显著性图(边缘密度图)
const saliencyMap = await this.computeSaliencyMap(pixelMap, opts.stepSize);
// 2. 确定裁剪区域尺寸
let cropW: number, cropH: number;
const srcRatio = srcW / srcH;
if (srcRatio > targetRatio) {
cropH = srcH;
cropW = Math.round(srcH * targetRatio);
} else {
cropW = srcW;
cropH = Math.round(srcW / targetRatio);
}
// 3. 滑动窗口搜索最佳裁剪位置
let bestX = 0, bestY = 0, bestScore = -Infinity;
for (let y = 0; y <= srcH - cropH; y += opts.stepSize) {
for (let x = 0; x <= srcW - cropW; x += opts.stepSize) {
// 计算该区域的显著性得分
const score = this.computeCropScore(
saliencyMap, x, y, cropW, cropH,
srcW, srcH, opts
);
if (score > bestScore) {
bestScore = score;
bestX = x;
bestY = y;
}
}
}
// 4. 执行裁剪
await pixelMap.crop({
x: bestX,
y: bestY,
size: { width: cropW, height: cropH }
});
}
/**
* 计算显著性图(基于Sobel边缘检测)
* 返回降采样后的边缘密度矩阵
*/
private static async computeSaliencyMap(
pixelMap: PixelMap,
stepSize: number
): Promise<number[][]> {
const info = await pixelMap.getImageInfo();
const width = info.size.width;
const height = info.size.height;
const bufferSize = width * height * 4;
// 读取像素
const srcBuffer = new ArrayBuffer(bufferSize);
await pixelMap.readPixelsToBuffer(srcBuffer);
const src = new Uint8Array(srcBuffer);
// 降采样后的显著性图尺寸
const mapW = Math.ceil(width / stepSize);
const mapH = Math.ceil(height / stepSize);
const saliencyMap: number[][] = [];
for (let my = 0; my < mapH; my++) {
saliencyMap[my] = [];
for (let mx = 0; mx < mapW; mx++) {
const x = mx * stepSize;
const y = my * stepSize;
// 计算该位置的Sobel边缘强度
let edgeStrength = 0;
if (x > 0 && x < width - 1 && y > 0 && y < height - 1) {
// Sobel X 核: [-1, 0, 1; -2, 0, 2; -1, 0, 1]
// Sobel Y 核: [-1, -2, -1; 0, 0, 0; 1, 2, 1]
const idx = (y * width + x) * 4;
const idxL = (y * width + x - 1) * 4;
const idxR = (y * width + x + 1) * 4;
const idxT = ((y - 1) * width + x) * 4;
const idxB = ((y + 1) * width + x) * 4;
// 灰度值
const gray = (r: number, g: number, b: number) => 0.299 * r + 0.587 * g + 0.114 * b;
const gx = gray(src[idxR], src[idxR + 1], src[idxR + 2]) -
gray(src[idxL], src[idxL + 1], src[idxL + 2]);
const gy = gray(src[idxB], src[idxB + 1], src[idxB + 2]) -
gray(src[idxT], src[idxT + 1], src[idxT + 2]);
edgeStrength = Math.sqrt(gx * gx + gy * gy);
}
saliencyMap[my][mx] = edgeStrength;
}
}
// 归一化到 0~1
let maxVal = 0;
for (const row of saliencyMap) {
for (const val of row) {
if (val > maxVal) maxVal = val;
}
}
if (maxVal > 0) {
for (let y = 0; y < mapH; y++) {
for (let x = 0; x < mapW; x++) {
saliencyMap[y][x] /= maxVal;
}
}
}
return saliencyMap;
}
/**
* 计算裁剪区域的得分
* 综合考虑边缘密度和中心先验
*/
private static computeCropScore(
saliencyMap: number[][],
cropX: number, cropY: number,
cropW: number, cropH: number,
srcW: number, srcH: number,
opts: SmartCropOptions
): number {
const stepSize = opts.stepSize;
const mapW = saliencyMap[0].length;
const mapH = saliencyMap.length;
// 裁剪区域在显著性图中的范围
const startX = Math.floor(cropX / stepSize);
const startY = Math.floor(cropY / stepSize);
const endX = Math.min(Math.ceil((cropX + cropW) / stepSize), mapW);
const endY = Math.min(Math.ceil((cropY + cropH) / stepSize), mapH);
// 计算区域内的平均显著性
let saliencySum = 0;
let count = 0;
for (let y = startY; y < endY; y++) {
for (let x = startX; x < endX; x++) {
saliencySum += saliencyMap[y][x];
count++;
}
}
const avgSaliency = count > 0 ? saliencySum / count : 0;
// 中心先验:裁剪区域越靠近图片中心,得分越高
const centerX = (cropX + cropW / 2) / srcW;
const centerY = (cropY + cropH / 2) / srcH;
const centerDist = Math.sqrt(
Math.pow(centerX - 0.5, 2) + Math.pow(centerY - 0.5, 2)
);
const centerScore = 1 - centerDist * 2; // 0~1,越靠近中心越高
// 综合得分
return avgSaliency * opts.edgeWeight + centerScore * opts.centerWeight;
}
}
/**
* 智能裁剪选项
*/
interface SmartCropOptions {
stepSize?: number; // 搜索步长
edgeWeight?: number; // 边缘密度权重
centerWeight?: number; // 中心先验权重
}
3.3 缩放插值算法对比实现
typescript
import { image } from '@kit.ImageKit';
/**
* 缩放插值算法实现
* 对比不同插值算法的效果和性能
*/
export class ScaleInterpolation {
/**
* 最近邻插值缩放
* 最快的缩放方法,但放大后会有明显锯齿
*
* 原理:目标像素直接取源图像中最近的像素值
*/
static async nearestNeighbor(
pixelMap: PixelMap,
targetWidth: number,
targetHeight: number
): Promise<PixelMap> {
const info = await pixelMap.getImageInfo();
const srcW = info.size.width;
const srcH = info.size.height;
// 读取源像素
const srcBuffer = new ArrayBuffer(srcW * srcH * 4);
await pixelMap.readPixelsToBuffer(srcBuffer);
const src = new Uint8Array(srcBuffer);
// 创建目标缓冲区
const dstBuffer = new ArrayBuffer(targetWidth * targetHeight * 4);
const dst = new Uint8Array(dstBuffer);
const xRatio = srcW / targetWidth;
const yRatio = srcH / targetHeight;
for (let y = 0; y < targetHeight; y++) {
for (let x = 0; x < targetWidth; x++) {
// 找到源图像中最近的像素坐标
const srcX = Math.min(Math.floor(x * xRatio), srcW - 1);
const srcY = Math.min(Math.floor(y * yRatio), srcH - 1);
const srcIdx = (srcY * srcW + srcX) * 4;
const dstIdx = (y * targetWidth + x) * 4;
dst[dstIdx] = src[srcIdx];
dst[dstIdx + 1] = src[srcIdx + 1];
dst[dstIdx + 2] = src[srcIdx + 2];
dst[dstIdx + 3] = src[srcIdx + 3];
}
}
// 创建新的 PixelMap
const newPixelMap = await image.createPixelMap(dstBuffer, {
size: { width: targetWidth, height: targetHeight },
pixelFormat: image.PixelMapFormat.RGBA_8888,
});
return newPixelMap;
}
/**
* 双线性插值缩放
* 质量和速度的平衡点,HarmonyOS默认使用此算法
*
* 原理:目标像素值由源图像中4个最近邻像素加权平均得到
* 权重由距离决定:越近权重越大
*/
static async bilinear(
pixelMap: PixelMap,
targetWidth: number,
targetHeight: number
): Promise<PixelMap> {
const info = await pixelMap.getImageInfo();
const srcW = info.size.width;
const srcH = info.size.height;
const srcBuffer = new ArrayBuffer(srcW * srcH * 4);
await pixelMap.readPixelsToBuffer(srcBuffer);
const src = new Uint8Array(srcBuffer);
const dstBuffer = new ArrayBuffer(targetWidth * targetHeight * 4);
const dst = new Uint8Array(dstBuffer);
const xRatio = (srcW - 1) / targetWidth;
const yRatio = (srcH - 1) / targetHeight;
for (let y = 0; y < targetHeight; y++) {
for (let x = 0; x < targetWidth; x++) {
// 源坐标(浮点)
const srcXf = x * xRatio;
const srcYf = y * yRatio;
// 4个邻域像素坐标
const x0 = Math.floor(srcXf);
const y0 = Math.floor(srcYf);
const x1 = Math.min(x0 + 1, srcW - 1);
const y1 = Math.min(y0 + 1, srcH - 1);
// 插值权重
const dx = srcXf - x0;
const dy = srcYf - y0;
// 双线性插值
for (let c = 0; c < 4; c++) {
const v00 = src[(y0 * srcW + x0) * 4 + c];
const v10 = src[(y0 * srcW + x1) * 4 + c];
const v01 = src[(y1 * srcW + x0) * 4 + c];
const v11 = src[(y1 * srcW + x1) * 4 + c];
// 水平插值 → 垂直插值
const top = v00 * (1 - dx) + v10 * dx;
const bottom = v01 * (1 - dx) + v11 * dx;
const value = top * (1 - dy) + bottom * dy;
dst[(y * targetWidth + x) * 4 + c] = Math.round(value);
}
}
}
const newPixelMap = await image.createPixelMap(dstBuffer, {
size: { width: targetWidth, height: targetHeight },
pixelFormat: image.PixelMapFormat.RGBA_8888,
});
return newPixelMap;
}
/**
* 双三次插值缩放(Bicubic)
* 质量最高的常用插值方法
* 使用16个邻域像素,通过三次多项式计算权重
*
* 原理:权重函数 W(t) = (a+2)|t|³ - (a+3)|t|² + 1 (|t|≤1)
* W(t) = a|t|³ - 5a|t|² + 8a|t| - 4a (1<|t|≤2)
* a 通常取 -0.5(Mitchell-Netravali)
*/
static async bicubic(
pixelMap: PixelMap,
targetWidth: number,
targetHeight: number
): Promise<PixelMap> {
const info = await pixelMap.getImageInfo();
const srcW = info.size.width;
const srcH = info.size.height;
const a = -0.5; // Mitchell-Netravali 参数
const srcBuffer = new ArrayBuffer(srcW * srcH * 4);
await pixelMap.readPixelsToBuffer(srcBuffer);
const src = new Uint8Array(srcBuffer);
const dstBuffer = new ArrayBuffer(targetWidth * targetHeight * 4);
const dst = new Uint8Array(dstBuffer);
const xRatio = srcW / targetWidth;
const yRatio = srcH / targetHeight;
// 三次插值权重函数
const cubicWeight = (t: number): number => {
const absT = Math.abs(t);
if (absT <= 1) {
return (a + 2) * absT * absT * absT - (a + 3) * absT * absT + 1;
} else if (absT <= 2) {
return a * absT * absT * absT - 5 * a * absT * absT + 8 * a * absT - 4 * a;
}
return 0;
};
for (let y = 0; y < targetHeight; y++) {
for (let x = 0; x < targetWidth; x++) {
const srcXf = x * xRatio - 0.5;
const srcYf = y * yRatio - 0.5;
const x0 = Math.floor(srcXf);
const y0 = Math.floor(srcYf);
for (let c = 0; c < 4; c++) {
let value = 0;
let weightSum = 0;
// 4×4邻域
for (let ky = -1; ky <= 2; ky++) {
for (let kx = -1; kx <= 2; kx++) {
const sx = Math.min(Math.max(x0 + kx, 0), srcW - 1);
const sy = Math.min(Math.max(y0 + ky, 0), srcH - 1);
const wx = cubicWeight(srcXf - (x0 + kx));
const wy = cubicWeight(srcYf - (y0 + ky));
const w = wx * wy;
value += src[(sy * srcW + sx) * 4 + c] * w;
weightSum += w;
}
}
dst[(y * targetWidth + x) * 4 + c] = Math.min(255, Math.max(0,
Math.round(value / weightSum)
));
}
}
}
const newPixelMap = await image.createPixelMap(dstBuffer, {
size: { width: targetWidth, height: targetHeight },
pixelFormat: image.PixelMapFormat.RGBA_8888,
});
return newPixelMap;
}
}
3.4 批量图片处理管线
实际开发中,经常需要批量处理图片------比如用户选了 20 张照片,都要压缩到 1080p 以内。这时候就需要一个高效的批量处理管线。
typescript
import { image } from '@kit.ImageKit';
import { fileIo as fs } from '@kit.CoreFileKit';
/**
* 批量图片处理管线
* 支持并发控制、进度回调、错误处理
*/
export class BatchImageProcessor {
private maxConcurrency: number; // 最大并发数
private activeCount: number = 0; // 当前活跃任务数
private completedCount: number = 0;
private totalCount: number = 0;
private onProgress?: (completed: number, total: number) => void;
private onError?: (filePath: string, error: Error) => void;
constructor(options: BatchProcessOptions) {
this.maxConcurrency = options.maxConcurrency ?? 3;
this.onProgress = options.onProgress;
this.onError = options.onError;
}
/**
* 批量处理图片
*
* @param filePaths 图片文件路径列表
* @param processFunc 单张图片的处理函数
* @returns 处理结果列表
*/
async processAll(
filePaths: string[],
processFunc: (pixelMap: PixelMap, filePath: string) => Promise<void>
): Promise<BatchProcessResult[]> {
this.totalCount = filePaths.length;
this.completedCount = 0;
this.activeCount = 0;
const results: BatchProcessResult[] = [];
// 使用信号量模式控制并发
const queue = [...filePaths];
return new Promise((resolve) => {
const tryNext = () => {
// 所有任务完成
if (this.completedCount >= this.totalCount) {
resolve(results);
return;
}
// 队列为空或达到并发上限
if (queue.length === 0 || this.activeCount >= this.maxConcurrency) {
return;
}
const filePath = queue.shift()!;
this.activeCount++;
// 异步处理单张图片
this.processSingle(filePath, processFunc)
.then((result) => {
results.push(result);
})
.finally(() => {
this.activeCount--;
this.completedCount++;
this.onProgress?.(this.completedCount, this.totalCount);
tryNext(); // 尝试启动下一个任务
});
};
// 启动初始任务
for (let i = 0; i < Math.min(this.maxConcurrency, filePaths.length); i++) {
tryNext();
}
});
}
/**
* 处理单张图片
*/
private async processSingle(
filePath: string,
processFunc: (pixelMap: PixelMap, filePath: string) => Promise<void>
): Promise<BatchProcessResult> {
let imageSource: image.ImageSource | null = null;
let pixelMap: PixelMap | null = null;
let file: fs.File | null = null;
try {
// 1. 打开文件
file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
// 2. 创建 ImageSource
imageSource = image.createImageSource(file.fd);
// 3. 解码
pixelMap = await imageSource.createPixelMap({
editable: true,
desiredPixelFormat: image.PixelMapFormat.RGBA_8888,
});
// 4. 执行处理函数
await processFunc(pixelMap, filePath);
// 5. 编码保存
const packer = image.createImagePacker();
const packData = await packer.packing(pixelMap, {
format: 'image/jpeg',
quality: 85,
});
// 6. 写入输出文件
const outputPath = filePath.replace(/\.\w+$/, '_processed$&');
const outFile = fs.openSync(outputPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
fs.writeSync(outFile.fd, packData);
fs.closeSync(outFile);
packer.release();
return {
inputPath: filePath,
outputPath: outputPath,
success: true,
};
} catch (error) {
this.onError?.(filePath, error as Error);
return {
inputPath: filePath,
success: false,
error: (error as Error).message,
};
} finally {
// 7. 释放资源
pixelMap?.release();
imageSource?.release();
if (file) fs.closeSync(file.fd);
}
}
}
/**
* 批量处理:统一压缩到指定尺寸
* 典型场景:用户上传多张图片,统一压缩到 1080p
*/
export async function batchCompress(
filePaths: string[],
maxWidth: number = 1920,
maxHeight: number = 1080,
quality: number = 85,
onProgress?: (completed: number, total: number) => void
): Promise<BatchProcessResult[]> {
const processor = new BatchImageProcessor({
maxConcurrency: 3,
onProgress: onProgress,
onError: (path, error) => {
console.error(`处理失败: ${path}, 错误: ${error.message}`);
},
});
return processor.processAll(filePaths, async (pixelMap, _filePath) => {
const info = await pixelMap.getImageInfo();
// 如果图片尺寸超过限制,等比缩放
if (info.size.width > maxWidth || info.size.height > maxHeight) {
const scale = Math.min(maxWidth / info.size.width, maxHeight / info.size.height);
await pixelMap.scale(scale, scale);
}
});
}
// ==================== 类型定义 ====================
interface BatchProcessOptions {
maxConcurrency?: number; // 最大并发数,默认3
onProgress?: (completed: number, total: number) => void;
onError?: (filePath: string, error: Error) => void;
}
interface BatchProcessResult {
inputPath: string;
outputPath?: string;
success: boolean;
error?: string;
}
四、踩坑与注意事项
4.1 裁剪坐标的精度问题
crop() 的坐标参数是整数,但计算过程中经常产生浮点数。如果不做取整,可能导致裁剪区域偏移 1 个像素。
typescript
// ❌ 危险:浮点坐标
const cropX = (srcW - cropW) / 2; // 可能是 499.5
// ✅ 安全:取整
const cropX = Math.round((srcW - cropW) / 2); // 500
4.2 缩放后尺寸不为整数
scale() 的参数是浮点数,缩放后的尺寸可能不是整数。比如 1000 × 0.667 = 667,但 1001 × 0.667 = 667.667。PixelMap 内部会取整,但取整方式不确定(可能四舍五入也可能截断)。
建议:如果需要精确的输出尺寸,先计算精确的缩放比例:
typescript
const targetWidth = 800;
const targetHeight = 600;
const info = await pixelMap.getImageInfo();
// 精确计算缩放比例
const scaleX = targetWidth / info.size.width;
const scaleY = targetHeight / info.size.height;
const scale = Math.min(scaleX, scaleY); // 等比缩放
await pixelMap.scale(scale, scale);
// 缩放后可能不是精确的 targetWidth × targetHeight
// 需要再做一次精确裁剪
const scaledInfo = await pixelMap.getImageInfo();
if (scaledInfo.size.width !== targetWidth || scaledInfo.size.height !== targetHeight) {
await pixelMap.crop({
x: 0,
y: 0,
size: {
width: Math.min(targetWidth, scaledInfo.size.width),
height: Math.min(targetHeight, scaledInfo.size.height)
}
});
}
4.3 智能裁剪的性能
3.2 节的智能裁剪算法使用了滑动窗口搜索,计算量与图片尺寸和搜索步长相关:
| 图片尺寸 | 步长=8 | 步长=16 | 步长=32 |
|---|---|---|---|
| 1000×1000 | ~2秒 | ~0.5秒 | ~0.1秒 |
| 4000×3000 | ~15秒 | ~4秒 | ~1秒 |
优化建议:
- 先降采样到较小尺寸再计算显著性图
- 使用更大的步长(16或32),牺牲少量精度换取大幅性能提升
- 缓存显著性图,避免重复计算
4.4 批量处理的内存峰值
批量处理时,如果并发数太高,多个 PixelMap 同时驻留内存,容易 OOM。
typescript
// ❌ 危险:10个并发,每个PixelMap占48MB = 480MB
const processor = new BatchImageProcessor({ maxConcurrency: 10 });
// ✅ 安全:3个并发 = 144MB
const processor = new BatchImageProcessor({ maxConcurrency: 3 });
4.5 createPixelMap 的 ArrayBuffer 模式
3.3 节中使用了 image.createPixelMap(buffer, options) 从 ArrayBuffer 创建新的 PixelMap。注意这个 API 的参数格式:
typescript
// ✅ 正确格式
const newPixelMap = await image.createPixelMap(dstBuffer, {
size: { width: targetWidth, height: targetHeight },
pixelFormat: image.PixelMapFormat.RGBA_8888,
});
如果 size 与 buffer 的大小不匹配(width × height × 4 ≠ buffer.byteLength),会抛异常。
4.6 缩小图片的抗混叠
直接对大图做 scale(0.1, 0.1) 缩小到 1/10,可能产生摩尔纹。更好的做法是分步缩小:
typescript
// ❌ 一步缩小到1/10,可能有摩尔纹
await pixelMap.scale(0.1, 0.1);
// ✅ 分两步缩小,每步缩小约1/3
await pixelMap.scale(0.316, 0.316); // √0.1 ≈ 0.316
await pixelMap.scale(0.316, 0.316); // 0.316 × 0.316 ≈ 0.1
五、HarmonyOS 6 适配
5.1 原生智能裁剪 API
HarmonyOS 6 新增了 image.createImagePackerV2() 和智能裁剪相关 API:
typescript
// API 14+ 智能裁剪
const smartCropOptions: image.SmartCropOptions = {
targetRatio: 16 / 9, // 目标宽高比
priority: image.CropPriority.FACE, // 人脸优先
};
await pixelMap.smartCrop(smartCropOptions);
5.2 缩放插值算法选择
HarmonyOS 6 允许在 scale() 时指定插值算法:
typescript
// API 14+ 指定插值算法
await pixelMap.scale(2.0, 2.0, {
interpolation: image.Interpolation.BICUBIC, // 双三次插值
});
5.3 版本差异速查
| 特性 | API 12 | API 13 | API 14 |
|---|---|---|---|
| 基础裁剪 crop | ✅ | ✅ | ✅ |
| 基础缩放 scale | ✅ | ✅ | ✅ |
| 智能裁剪 | ❌ | 部分 | ✅ |
| 插值算法选择 | ❌ | ❌ | ✅ |
| 批量处理API | ❌ | ❌ | 部分 |
| GPU加速缩放 | ❌ | ❌ | ✅ |
5.4 迁移指南
- 智能裁剪 :API 14 的
smartCrop()比手动实现的边缘密度法更准确(底层使用了人脸检测+显著性模型),建议优先使用 - 插值算法:放大场景使用 BICUBIC,缩小场景使用 BILINEAR + 抗混叠
- 批量处理 :API 14 新增
ImageProcessor类,内置并发控制和进度回调
六、总结
#mermaid-svg-dVDUhDG8KC823pf4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dVDUhDG8KC823pf4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dVDUhDG8KC823pf4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dVDUhDG8KC823pf4 .error-icon{fill:#552222;}#mermaid-svg-dVDUhDG8KC823pf4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dVDUhDG8KC823pf4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dVDUhDG8KC823pf4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dVDUhDG8KC823pf4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dVDUhDG8KC823pf4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dVDUhDG8KC823pf4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dVDUhDG8KC823pf4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dVDUhDG8KC823pf4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dVDUhDG8KC823pf4 .marker.cross{stroke:#333333;}#mermaid-svg-dVDUhDG8KC823pf4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dVDUhDG8KC823pf4 p{margin:0;}#mermaid-svg-dVDUhDG8KC823pf4 .edge{stroke-width:3;}#mermaid-svg-dVDUhDG8KC823pf4 .section--1 rect,#mermaid-svg-dVDUhDG8KC823pf4 .section--1 path,#mermaid-svg-dVDUhDG8KC823pf4 .section--1 circle,#mermaid-svg-dVDUhDG8KC823pf4 .section--1 polygon,#mermaid-svg-dVDUhDG8KC823pf4 .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .section--1 text{fill:#ffffff;}#mermaid-svg-dVDUhDG8KC823pf4 .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-dVDUhDG8KC823pf4 .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .edge-depth--1{stroke-width:17;}#mermaid-svg-dVDUhDG8KC823pf4 .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled,#mermaid-svg-dVDUhDG8KC823pf4 .disabled circle,#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:lightgray;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:#efefef;}#mermaid-svg-dVDUhDG8KC823pf4 .section-0 rect,#mermaid-svg-dVDUhDG8KC823pf4 .section-0 path,#mermaid-svg-dVDUhDG8KC823pf4 .section-0 circle,#mermaid-svg-dVDUhDG8KC823pf4 .section-0 polygon,#mermaid-svg-dVDUhDG8KC823pf4 .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-dVDUhDG8KC823pf4 .section-0 text{fill:black;}#mermaid-svg-dVDUhDG8KC823pf4 .node-icon-0{font-size:40px;color:black;}#mermaid-svg-dVDUhDG8KC823pf4 .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-dVDUhDG8KC823pf4 .edge-depth-0{stroke-width:14;}#mermaid-svg-dVDUhDG8KC823pf4 .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled,#mermaid-svg-dVDUhDG8KC823pf4 .disabled circle,#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:lightgray;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:#efefef;}#mermaid-svg-dVDUhDG8KC823pf4 .section-1 rect,#mermaid-svg-dVDUhDG8KC823pf4 .section-1 path,#mermaid-svg-dVDUhDG8KC823pf4 .section-1 circle,#mermaid-svg-dVDUhDG8KC823pf4 .section-1 polygon,#mermaid-svg-dVDUhDG8KC823pf4 .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .section-1 text{fill:black;}#mermaid-svg-dVDUhDG8KC823pf4 .node-icon-1{font-size:40px;color:black;}#mermaid-svg-dVDUhDG8KC823pf4 .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .edge-depth-1{stroke-width:11;}#mermaid-svg-dVDUhDG8KC823pf4 .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled,#mermaid-svg-dVDUhDG8KC823pf4 .disabled circle,#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:lightgray;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:#efefef;}#mermaid-svg-dVDUhDG8KC823pf4 .section-2 rect,#mermaid-svg-dVDUhDG8KC823pf4 .section-2 path,#mermaid-svg-dVDUhDG8KC823pf4 .section-2 circle,#mermaid-svg-dVDUhDG8KC823pf4 .section-2 polygon,#mermaid-svg-dVDUhDG8KC823pf4 .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .section-2 text{fill:#ffffff;}#mermaid-svg-dVDUhDG8KC823pf4 .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-dVDUhDG8KC823pf4 .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .edge-depth-2{stroke-width:8;}#mermaid-svg-dVDUhDG8KC823pf4 .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled,#mermaid-svg-dVDUhDG8KC823pf4 .disabled circle,#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:lightgray;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:#efefef;}#mermaid-svg-dVDUhDG8KC823pf4 .section-3 rect,#mermaid-svg-dVDUhDG8KC823pf4 .section-3 path,#mermaid-svg-dVDUhDG8KC823pf4 .section-3 circle,#mermaid-svg-dVDUhDG8KC823pf4 .section-3 polygon,#mermaid-svg-dVDUhDG8KC823pf4 .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .section-3 text{fill:black;}#mermaid-svg-dVDUhDG8KC823pf4 .node-icon-3{font-size:40px;color:black;}#mermaid-svg-dVDUhDG8KC823pf4 .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .edge-depth-3{stroke-width:5;}#mermaid-svg-dVDUhDG8KC823pf4 .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled,#mermaid-svg-dVDUhDG8KC823pf4 .disabled circle,#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:lightgray;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:#efefef;}#mermaid-svg-dVDUhDG8KC823pf4 .section-4 rect,#mermaid-svg-dVDUhDG8KC823pf4 .section-4 path,#mermaid-svg-dVDUhDG8KC823pf4 .section-4 circle,#mermaid-svg-dVDUhDG8KC823pf4 .section-4 polygon,#mermaid-svg-dVDUhDG8KC823pf4 .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .section-4 text{fill:black;}#mermaid-svg-dVDUhDG8KC823pf4 .node-icon-4{font-size:40px;color:black;}#mermaid-svg-dVDUhDG8KC823pf4 .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .edge-depth-4{stroke-width:2;}#mermaid-svg-dVDUhDG8KC823pf4 .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled,#mermaid-svg-dVDUhDG8KC823pf4 .disabled circle,#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:lightgray;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:#efefef;}#mermaid-svg-dVDUhDG8KC823pf4 .section-5 rect,#mermaid-svg-dVDUhDG8KC823pf4 .section-5 path,#mermaid-svg-dVDUhDG8KC823pf4 .section-5 circle,#mermaid-svg-dVDUhDG8KC823pf4 .section-5 polygon,#mermaid-svg-dVDUhDG8KC823pf4 .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .section-5 text{fill:black;}#mermaid-svg-dVDUhDG8KC823pf4 .node-icon-5{font-size:40px;color:black;}#mermaid-svg-dVDUhDG8KC823pf4 .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .edge-depth-5{stroke-width:-1;}#mermaid-svg-dVDUhDG8KC823pf4 .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled,#mermaid-svg-dVDUhDG8KC823pf4 .disabled circle,#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:lightgray;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:#efefef;}#mermaid-svg-dVDUhDG8KC823pf4 .section-6 rect,#mermaid-svg-dVDUhDG8KC823pf4 .section-6 path,#mermaid-svg-dVDUhDG8KC823pf4 .section-6 circle,#mermaid-svg-dVDUhDG8KC823pf4 .section-6 polygon,#mermaid-svg-dVDUhDG8KC823pf4 .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .section-6 text{fill:black;}#mermaid-svg-dVDUhDG8KC823pf4 .node-icon-6{font-size:40px;color:black;}#mermaid-svg-dVDUhDG8KC823pf4 .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .edge-depth-6{stroke-width:-4;}#mermaid-svg-dVDUhDG8KC823pf4 .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled,#mermaid-svg-dVDUhDG8KC823pf4 .disabled circle,#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:lightgray;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:#efefef;}#mermaid-svg-dVDUhDG8KC823pf4 .section-7 rect,#mermaid-svg-dVDUhDG8KC823pf4 .section-7 path,#mermaid-svg-dVDUhDG8KC823pf4 .section-7 circle,#mermaid-svg-dVDUhDG8KC823pf4 .section-7 polygon,#mermaid-svg-dVDUhDG8KC823pf4 .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .section-7 text{fill:black;}#mermaid-svg-dVDUhDG8KC823pf4 .node-icon-7{font-size:40px;color:black;}#mermaid-svg-dVDUhDG8KC823pf4 .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .edge-depth-7{stroke-width:-7;}#mermaid-svg-dVDUhDG8KC823pf4 .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled,#mermaid-svg-dVDUhDG8KC823pf4 .disabled circle,#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:lightgray;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:#efefef;}#mermaid-svg-dVDUhDG8KC823pf4 .section-8 rect,#mermaid-svg-dVDUhDG8KC823pf4 .section-8 path,#mermaid-svg-dVDUhDG8KC823pf4 .section-8 circle,#mermaid-svg-dVDUhDG8KC823pf4 .section-8 polygon,#mermaid-svg-dVDUhDG8KC823pf4 .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .section-8 text{fill:black;}#mermaid-svg-dVDUhDG8KC823pf4 .node-icon-8{font-size:40px;color:black;}#mermaid-svg-dVDUhDG8KC823pf4 .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .edge-depth-8{stroke-width:-10;}#mermaid-svg-dVDUhDG8KC823pf4 .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled,#mermaid-svg-dVDUhDG8KC823pf4 .disabled circle,#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:lightgray;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:#efefef;}#mermaid-svg-dVDUhDG8KC823pf4 .section-9 rect,#mermaid-svg-dVDUhDG8KC823pf4 .section-9 path,#mermaid-svg-dVDUhDG8KC823pf4 .section-9 circle,#mermaid-svg-dVDUhDG8KC823pf4 .section-9 polygon,#mermaid-svg-dVDUhDG8KC823pf4 .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .section-9 text{fill:black;}#mermaid-svg-dVDUhDG8KC823pf4 .node-icon-9{font-size:40px;color:black;}#mermaid-svg-dVDUhDG8KC823pf4 .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .edge-depth-9{stroke-width:-13;}#mermaid-svg-dVDUhDG8KC823pf4 .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled,#mermaid-svg-dVDUhDG8KC823pf4 .disabled circle,#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:lightgray;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:#efefef;}#mermaid-svg-dVDUhDG8KC823pf4 .section-10 rect,#mermaid-svg-dVDUhDG8KC823pf4 .section-10 path,#mermaid-svg-dVDUhDG8KC823pf4 .section-10 circle,#mermaid-svg-dVDUhDG8KC823pf4 .section-10 polygon,#mermaid-svg-dVDUhDG8KC823pf4 .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .section-10 text{fill:black;}#mermaid-svg-dVDUhDG8KC823pf4 .node-icon-10{font-size:40px;color:black;}#mermaid-svg-dVDUhDG8KC823pf4 .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .edge-depth-10{stroke-width:-16;}#mermaid-svg-dVDUhDG8KC823pf4 .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled,#mermaid-svg-dVDUhDG8KC823pf4 .disabled circle,#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:lightgray;}#mermaid-svg-dVDUhDG8KC823pf4 .disabled text{fill:#efefef;}#mermaid-svg-dVDUhDG8KC823pf4 .section-root rect,#mermaid-svg-dVDUhDG8KC823pf4 .section-root path,#mermaid-svg-dVDUhDG8KC823pf4 .section-root circle,#mermaid-svg-dVDUhDG8KC823pf4 .section-root polygon{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-dVDUhDG8KC823pf4 .section-root text{fill:#ffffff;}#mermaid-svg-dVDUhDG8KC823pf4 .section-root span{color:#ffffff;}#mermaid-svg-dVDUhDG8KC823pf4 .section-2 span{color:#ffffff;}#mermaid-svg-dVDUhDG8KC823pf4 .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-dVDUhDG8KC823pf4 .edge{fill:none;}#mermaid-svg-dVDUhDG8KC823pf4 .mindmap-node-label{dy:1em;alignment-baseline:middle;text-anchor:middle;dominant-baseline:middle;text-align:center;}#mermaid-svg-dVDUhDG8KC823pf4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 图像裁剪缩放
裁剪策略
中心裁剪
最简单最常用
可能切掉重要内容
智能裁剪
边缘密度法
Sobel边缘检测
滑动窗口搜索
人脸优先裁剪
检测人脸位置
人脸始终在框内
缩放策略
等比缩放 Fit
保持宽高比
图片完整显示
填充缩放 Fill
先裁剪再缩放
填满目标区域
缩略图
sampleSize降采样
精确缩放到目标
插值算法
最近邻
最快 锯齿明显
双线性
速度质量平衡
HarmonyOS默认
双三次
质量最高
4×4邻域
Lanczos
专业级
sinc函数核
批量处理
并发控制 信号量模式
进度回调
错误处理
内存控制 maxConcurrency
注意事项
裁剪坐标取整
缩放尺寸精度
缩小抗混叠
内存峰值控制
HarmonyOS 6
原生智能裁剪
插值算法选择
GPU加速缩放
关键知识点回顾:
| 知识点 | 要点 |
|---|---|
| 中心裁剪 | 最简单,按比例裁掉多余部分 |
| 智能裁剪 | 边缘密度法+中心先验,保留重要内容 |
| 等比缩放 | Fit模式完整显示,Fill模式填满裁剪 |
| 最近邻插值 | 最快但质量差,适合缩略图 |
| 双线性插值 | 速度质量平衡,通用场景首选 |
| 双三次插值 | 质量最高,适合放大操作 |
| 缩小抗混叠 | 分步缩小或先模糊再降采样 |
| 批量处理 | 信号量控制并发,及时释放资源 |
| 内存控制 | 并发数不超过3,大图用sampleSize降采样 |
裁剪缩放是图像处理中最常见的操作,几乎每个涉及图片的应用都绕不开。下一篇我们来看 EXIF 信息------那些藏在图片文件里的"隐藏数据",包括拍摄参数、GPS定位、以及如何做隐私脱敏。