HarmonyOS 6.0 PixelMap像素级图片操作——马赛克/灰度/证件照换底一次讲透
图片模糊化处理、灰度转换、底片反色、证件照换底色——这些操作看似高端,底层都是像素级读写。HarmonyOS NEXT 的 PixelMap 提供了 readPixelsToBuffer / writePixelsToBuffer 两个核心 API,掌握它们就能做任何图片变换。这篇从原理到实战,把 7 种常见像素操作全部讲清楚。
PixelMap 像素操作基础
核心概念
- RGBA——每个像素 4 字节:R(红)、G(绿)、B(蓝)、A(透明度),各 1 字节 0~255
- Buffer——整张图片的像素数据是一段连续内存,大小 = width × height × 4
- readPixelsToBuffer——把像素数据读到 ArrayBuffer
- writePixelsToBuffer——把修改后的 ArrayBuffer 写回 PixelMap
import { image } from '@kit.ImageKit'

像素读写基本流程
async function processPixelMap(pixelMap: image.PixelMap): Promise<void> {
let width: number = pixelMap.getImageInfoSync().size.width
let height: number = pixelMap.getImageInfoSync().size.height
let bytesNumber: number = pixelMap.getPixelBytesNumber()
// 1. 创建 Buffer
let buffer: ArrayBuffer = new ArrayBuffer(bytesNumber)
// 2. 读取像素数据
await pixelMap.readPixelsToBuffer(buffer)
// 3. 操作 Buffer 中的像素
let view: Uint8Array = new Uint8Array(buffer)
// view[i] = R, view[i+1] = G, view[i+2] = B, view[i+3] = A
// 4. 写回 PixelMap
await pixelMap.writePixelsToBuffer(buffer)
}
要点: 像素数据是 RGBA 顺序排列,每 4 字节一组。offset = (y × width + x) × 4 就是 (x, y) 位置的像素起始偏移。
操作一:读取像素与颜色提取
interface PixelColor {
r: number;
g: number;
b: number;
a: number;
}
function getPixelAt(view: Uint8Array, width: number, x: number, y: number): PixelColor {
let offset: number = (y * width + x) * 4
return {
r: view[offset],
g: view[offset + 1],
b: view[offset + 2],
a: view[offset + 3]
}
}
function setPixelAt(view: Uint8Array, width: number, x: number, y: number, color: PixelColor): void {
let offset: number = (y * width + x) * 4
view[offset] = color.r
view[offset + 1] = color.g
view[offset + 2] = color.b
view[offset + 3] = color.a
}
要点: getPixelAt / setPixelAt 是所有像素操作的基础工具函数。
操作二:马赛克
马赛克原理:把图片分成 NxN 的小块,每块取均值覆盖。
function applyMosaic(view: Uint8Array, width: number, height: number, blockSize: number): void {
for (let by: number = 0; by < height; by += blockSize) {
for (let bx: number = 0; bx < width; bx += blockSize) {
// 计算块内均值
let sumR: number = 0
let sumG: number = 0
let sumB: number = 0
let count: number = 0
for (let y: number = by; y < Math.min(by + blockSize, height); y++) {
for (let x: number = bx; x < Math.min(bx + blockSize, width); x++) {
let offset: number = (y * width + x) * 4
sumR += view[offset]
sumG += view[offset + 1]
sumB += view[offset + 2]
count++
}
}
let avgR: number = Math.floor(sumR / count)
let avgG: number = Math.floor(sumG / count)
let avgB: number = Math.floor(sumB / count)
// 块内所有像素设为均值
for (let y: number = by; y < Math.min(by + blockSize, height); y++) {
for (let x: number = bx; x < Math.min(bx + blockSize, width); x++) {
let offset: number = (y * width + x) * 4
view[offset] = avgR
view[offset + 1] = avgG
view[offset + 2] = avgB
}
}
}
}
}
要点: blockSize 越大越模糊,10 是适中值,30 以上基本看不清细节。隐私区域打码用 15~20。
操作三:灰度化
灰度公式:Gray = R×0.299 + G×0.587 + B×0.114(人眼对绿色最敏感)
function applyGrayscale(view: Uint8Array): void {
for (let i: number = 0; i < view.length; i += 4) {
let gray: number = Math.floor(view[i] * 0.299 + view[i + 1] * 0.587 + view[i + 2] * 0.114)
view[i] = gray
view[i + 1] = gray
view[i + 2] = gray
}
}
要点: 灰度化是很多图像处理的前置步骤(如边缘检测、OCR预处理)。不要简单取 (R+G+B)/3,效果差很多。
操作四:反色(底片效果)
反色 = 255 - 原值,每个通道独立取反。
function applyInvert(view: Uint8Array): void {
for (let i: number = 0; i < view.length; i += 4) {
view[i] = 255 - view[i]
view[i + 1] = 255 - view[i + 1]
view[i + 2] = 255 - view[i + 2]
}
}
要点: 反色不改变透明度通道 A。常用于制作底片风格或辅助视觉障碍用户。
操作五:亮度调节
亮度 = 各通道加上偏移量,clamp 到 0~255。
function applyBrightness(view: Uint8Array, offset: number): void {
for (let i: number = 0; i < view.length; i += 4) {
view[i] = Math.max(0, Math.min(255, view[i] + offset))
view[i + 1] = Math.max(0, Math.min(255, view[i + 1] + offset))
view[i + 2] = Math.max(0, Math.min(255, view[i + 2] + offset))
}
}
要点: offset 正值变亮,负值变暗。建议范围 -50 ~ +50,超出容易过曝或全黑。
操作六:圆角裁剪
圆角原理:四个角的像素,计算到圆心距离,超过半径则 A=0(透明)。
function applyRoundCorner(view: Uint8Array, width: number, height: number, radius: number): void {
for (let y: number = 0; y < height; y++) {
for (let x: number = 0; x < width; x++) {
let inCorner: boolean = false
let cx: number = 0
let cy: number = 0
if (x < radius && y < radius) {
cx = radius; cy = radius; inCorner = true
} else if (x >= width - radius && y < radius) {
cx = width - radius; cy = radius; inCorner = true
} else if (x < radius && y >= height - radius) {
cx = radius; cy = height - radius; inCorner = true
} else if (x >= width - radius && y >= height - radius) {
cx = width - radius; cy = height - radius; inCorner = true
}
if (inCorner) {
let dist: number = Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy))
if (dist > radius) {
let idx: number = (y * width + x) * 4
view[idx + 3] = 0
}
}
}
}
}
要点: 只处理四个角区域,其他像素不动。圆角半径一般取图片短边的 1/8 ~ 1/4。
操作七:证件照换底色
换底色原理:取背景参考色,设定容差,与背景色接近的像素替换为目标色。
function replaceBackground(
view: Uint8Array, width: number, height: number,
targetR: number, targetG: number, targetB: number, threshold: number
): void {
// 取左上角像素作为背景参考色
let bgOffset: number = 0
let bgR: number = view[bgOffset]
let bgG: number = view[bgOffset + 1]
let bgB: number = view[bgOffset + 2]
for (let y: number = 0; y < height; y++) {
for (let x: number = 0; x < width; x++) {
let idx: number = (y * width + x) * 4
let dr: number = Math.abs(view[idx] - bgR)
let dg: number = Math.abs(view[idx + 1] - bgG)
let db: number = Math.abs(view[idx + 2] - bgB)
if (dr < threshold && dg < threshold && db < threshold) {
view[idx] = targetR
view[idx + 1] = targetG
view[idx + 2] = targetB
}
}
}
}
要点: 容差 threshold 建议 30~60,太小换不干净,太大会误伤非背景区域。蓝底→红底只需改目标色参数。更精准的做法是用多采样点取背景色均值。
PixelMap 其他常用 API
| API | 用途 | 说明 |
|---|---|---|
| scale(x, y) | 缩放 | 0~1 缩小,>1 放大 |
| rotate(angle) | 旋转 | 角度制,90/180/270 |
| flip(h, v) | 翻转 | h=水平翻转,v=垂直翻转 |
| crop(region) | 裁剪 | Region{ x, y, size } |
| getPixelBytesNumber() | 字节数 | = width × height × 4 |
| release() | 释放内存 | 用完必须调用 |
完整 Demo 代码
Demo 展示了 7 种像素操作的说明和模拟执行,包含马赛克块大小调节和证件照换底色选择。
interface PixelOp {
name: string;
desc: string;
color: string;
}
@Entry
@Component
struct PixelMapDemo {
@State selectedOp: string = ''
@State resultDesc: string = '选择像素操作查看说明'
@State isProcessing: boolean = false
@State colorPreview: string = '#2196F3'
@State pixelInfo: string = ''
@State mosaicSize: string = '10'
@State operationLog: string[] = []
private pixelOps: PixelOp[] = [
{ name: '读取像素', desc: 'readPixelsToBuffer读取RGBA数据', color: '#1a73e8' },
{ name: '马赛克', desc: '按块取均值覆盖,实现马赛克效果', color: '#F44336' },
{ name: '灰度化', desc: 'R*0.299+G*0.587+B*0.114公式', color: '#9E9E9E' },
{ name: '反色', desc: '255-R, 255-G, 255-B取反', color: '#FF9800' },
{ name: '亮度调节', desc: '各通道加上偏移量', color: '#FFEB3B' },
{ name: '圆角裁剪', desc: '四角像素alpha设为0', color: '#4CAF50' },
{ name: '证件照换底', desc: '颜色容差匹配替换背景色', color: '#9C27B0' }
]
build() {
Column({ space: 0 }) {
Row() {
Button('< 返回')
.fontSize(14)
.backgroundColor(Color.Transparent)
.fontColor('#1a73e8')
.onClick(() => { router.back(); })
Text('PixelMap 像素操作')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
.textAlign(TextAlign.Center)
Text(this.isProcessing ? '处理中' : '就绪')
.fontSize(12)
.fontColor(this.isProcessing ? '#F44336' : '#4CAF50')
}
.width('100%')
.height(56)
.padding({ left: 12, right: 12 })
.alignItems(VerticalAlign.Center)
.backgroundColor('#FFFFFF')
Scroll() {
Column({ space: 16 }) {
Column({ space: 12 }) {
Text('像素操作列表')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
ForEach(this.pixelOps, (op: PixelOp) => {
Row({ space: 12 }) {
Column()
.width(6)
.height(40)
.borderRadius(3)
.backgroundColor(op.color)
Column({ space: 2 }) {
Text(op.name)
.fontSize(15)
.fontWeight(FontWeight.Medium)
Text(op.desc)
.fontSize(12)
.fontColor('#999999')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Button('模拟')
.fontSize(12)
.height(32)
.onClick(() => this.simulateOp(op.name))
}
.width('100%')
.padding(10)
.borderRadius(8)
.backgroundColor(this.selectedOp === op.name ? '#E3F2FD' : '#FAFAFA')
}, (op: PixelOp) => op.name)
}
.width('100%')
.padding(16)
.borderRadius(12)
.backgroundColor('#FFFFFF')
Column({ space: 8 }) {
Text('操作说明')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
Text(this.resultDesc)
.fontSize(14)
.fontColor('#333333')
.width('100%')
.padding(12)
.borderRadius(8)
.backgroundColor('#F5F5F5')
}
.width('100%')
.padding(16)
.borderRadius(12)
.backgroundColor('#FFFFFF')
}
.padding(16)
}
.layoutWeight(1)
.width('100%')
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
private simulateOp(opName: string): void {
this.selectedOp = opName
this.isProcessing = true
let desc: string = ''
if (opName === '读取像素') {
desc = '1. 创建PixelMap\n2. 创建ArrayBuffer(size = width*height*4)\n3. pixelMap.readPixelsToBuffer(buffer)\n4. 解析RGBA值:每4字节为一像素(R,G,B,A)\n5. 获取指定位置颜色:offset = (y*width+x)*4'
} else if (opName === '马赛克') {
desc = '1. 读取全部像素到buffer\n2. 按块遍历(块大小=' + this.mosaicSize + ')\n3. 每块内计算R/G/B均值\n4. 块内所有像素设为均值\n5. writePixelsToBuffer写回'
} else if (opName === '灰度化') {
desc = '1. 读取像素buffer\n2. 对每个像素:gray = R*0.299 + G*0.587 + B*0.114\n3. R = G = B = gray\n4. 写回buffer'
} else if (opName === '反色') {
desc = '1. 读取像素buffer\n2. 对每个像素:R=255-R, G=255-G, B=255-B\n3. 写回buffer'
} else if (opName === '亮度调节') {
desc = '1. 读取像素buffer\n2. 对每个像素各通道加偏移:clamp(R+offset)\n3. 写回buffer\noffset>0变亮,<0变暗'
} else if (opName === '圆角裁剪') {
desc = '1. 读取像素buffer\n2. 对四个角的像素:计算到圆心距离\n3. 距离>半径则A=0(透明)\n4. 写回buffer'
} else if (opName === '证件照换底') {
desc = '1. 读取像素buffer\n2. 取左上角像素作为原背景色参考\n3. 设定颜色容差threshold\n4. 与背景色差值<threshold的像素替换为目标色\n5. 写回buffer'
}
this.resultDesc = desc
this.operationLog.push(`[${new Date().toLocaleTimeString()}] ${opName} - 模拟完成`)
this.isProcessing = false
}
}
性能优化建议
| 优化点 | 做法 | 效果 |
|---|---|---|
| 大图先缩放 | scale 到 2048 以内再处理 | 速度提升 3~5 倍 |
| 避免频繁 read/write | 一次 read → 全部修改 → 一次 write | 减少 IO 次数 |
| 区域裁剪 | 先 crop 感兴趣区域再处理 | 减少计算量 |
| Worker 线程 | 像素操作放 ThreadWorker | 不卡 UI |
| release 及时 | 处理完立即 release() | 释放内存 |
踩坑清单
| 问题 | 原因 | 解决 |
|---|---|---|
| readPixelsToBuffer 崩溃 | Buffer 大小不对 | 用 getPixelBytesNumber() 获取精确大小 |
| 写回后图片没变化 | 忘记 writePixelsToBuffer | 修改 buffer 后必须写回 |
| RGBA 顺序搞反 | 误以为是 ARGB | PixelMap 用 RGBA 顺序,不是 Android 的 ARGB |
| 马赛克边缘有黑线 | blockSize 不整除 | 处理边界时 Math.min 限制不越界 |
| 灰度图偏绿 | 用了简单平均 (R+G+B)/3 | 必须用加权公式 0.299/0.587/0.114 |
| 换底色不干净 | 容差太小 | threshold 调大到 40~60 |
| 换底色误伤头发 | 容差太大 | 减小 threshold,或用多采样点 |
| 圆角锯齿明显 | 无抗锯齿 | 边缘像素做 alpha 渐变过渡 |
| 处理大图 OOM | 未缩放直接处理 | 先 scale 到合适尺寸 |
| 亮度溢出 | offset 太大 | clamp(0, 255) 不能忘 |
更多推荐

所有评论(0)