如何用Swift编程降低PNG颜色深度至8位及以下(适配蓝牙传输)
Swift图片降色深(8位及更低)实现方案
一、转换为8位色深的两种实用方式
1. 8位灰度图(最简单的彩色转8位方案)
如果可以接受灰度显示,直接将RGB值转换为单通道灰度值,每个像素仅占1字节。使用行业标准的灰度转换公式计算:
guard let imagePixels32Bit = image.pixelData32bitRGB() else { return } var grayPixels = [UInt8]() // 预分配内存,提升效率 grayPixels.reserveCapacity(imagePixels32Bit.count / 4) for i in stride(from: 0, to: imagePixels32Bit.count, by: 4) { let r = UInt16(imagePixels32Bit[i]) let g = UInt16(imagePixels32Bit[i+1]) let b = UInt16(imagePixels32Bit[i+2]) // 标准灰度转换:灰度 = 0.299R + 0.587G + 0.114B,用整数运算避免浮点损耗 let grayValue = UInt8((r * 299 + g * 587 + b * 114) / 1000) grayPixels.append(grayValue) } let imagePixels = grayPixels
2. 8位索引色(保留彩色的256色方案)
如果需要保留彩色信息,8位色深通常采用调色板模式:先从原始图像中生成包含256种代表色的调色板,再将每个像素映射到调色板中最接近的颜色,存储对应的索引(0-255,占1字节)。
实现步骤:
// 定义RGB结构体,用于颜色计算 struct RGB: Equatable, Hashable { var r: UInt8, g: UInt8, b: UInt8 // 计算两个颜色的欧氏距离(整数版,避免浮点运算) func distance(to other: RGB) -> Int { let dr = Int(r) - Int(other.r) let dg = Int(g) - Int(other.g) let db = Int(b) - Int(other.b) return dr*dr + dg*dg + db*db } } guard let imagePixels32Bit = image.pixelData32bitRGB() else { return } // 1. 提取原始图像所有RGB颜色 var originalColors = [RGB]() for i in stride(from: 0, to: imagePixels32Bit.count, by: 4) { originalColors.append(RGB(r: imagePixels32Bit[i], g: imagePixels32Bit[i+1], b: imagePixels32Bit[i+2])) } // 2. 生成256色调色板(推荐用K-Means聚类,这里用简化抽样演示) let paletteSize = 256 var palette = Set<RGB>() // 从原始颜色中提取不重复的256种颜色 for color in originalColors { if palette.count < paletteSize { palette.insert(color) } else { break } } // 若原始颜色不足256种,补充默认黑色填充 var paletteArray = Array(palette) while paletteArray.count < paletteSize { paletteArray.append(RGB(r: 0, g: 0, b: 0)) } // 3. 将每个像素映射到调色板索引 var indexedPixels = [UInt8]() indexedPixels.reserveCapacity(originalColors.count) for color in originalColors { var minDistance = Int.max var bestIndex = 0 // 找到调色板中最接近的颜色 for (index, paletteColor) in paletteArray.enumerated() { let dist = color.distance(to: paletteColor) if dist < minDistance { minDistance = dist bestIndex = index } } indexedPixels.append(UInt8(bestIndex)) } // 最终数据:像素索引数组 + 调色板(传输时需同时发送两者) let imagePixels = indexedPixels let colorPalette = paletteArray
注意:使用索引色时,蓝牙传输需要同时发送调色板数据和像素索引数据,接收端需通过调色板还原原始颜色。
二、更低色深的可能性
可以进一步降低色深,但颜色损失会更明显,需根据场景取舍:
- 4位色深(16色):逻辑和8位索引色完全一致,仅需将调色板大小改为16,数据量仅为8位的一半。
- 1位色深(黑白二值):仅保留纯黑/纯白,适合文字、线条类简单图像。通过灰度阈值判断颜色:
guard let imagePixels32Bit = image.pixelData32bitRGB() else { return } var binaryPixels = [UInt8]() var currentByte: UInt8 = 0 var bitPosition = 0 for i in stride(from: 0, to: imagePixels32Bit.count, by: 4) { let r = UInt16(imagePixels32Bit[i]) let g = UInt16(imagePixels32Bit[i+1]) let b = UInt16(imagePixels32Bit[i+2]) let gray = (r * 299 + g * 587 + b * 114) / 1000 // 阈值设为127,高于则为白(1),否则为黑(0) let bit = gray > 127 ? 1 : 0 currentByte |= UInt8(bit) << (7 - bitPosition) bitPosition += 1 // 每8位打包成一个字节 if bitPosition == 8 { binaryPixels.append(currentByte) currentByte = 0 bitPosition = 0 } } // 处理最后不足8位的剩余数据 if bitPosition > 0 { binaryPixels.append(currentByte) } let imagePixels = binaryPixels
三、额外优化建议
- 生成调色板时,优先使用K-Means聚类算法(可自行实现或用第三方库),比抽样方式的颜色还原度高很多。
- 传输前对像素数据做无损压缩(如LZ77类算法),进一步减小蓝牙传输的数据量。
- 注意蓝牙MTU(最大传输单元)限制,拆分数据时需符合MTU大小,避免传输失败。
内容的提问来源于stack exchange,提问作者lcj
相关产品推荐
相关产品推荐

