You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 12:10:21