【图像处理】框架设计——协议、值类型与工程化思维
同样是实现灰度化功能一个函数、一个类的方法、一个协议的实现结果一样设计完全不同。这一天我们来聊聊这个框架的设计决策背后的思考以及什么样的代码算是工业级的。一、从需求到设计的思维过程需求实现灰度化、亮度、对比度、阈值四个图像滤镜并支持链式调用。方案 A函数式funcapplyGrayscale(_bitmap:MLBitmap)-MLBitmap{...}funcapplyBrightness(_bitmap:MLBitmap,adjustment:Int)-MLBitmap{...}// 使用letresultapplyBrightness(applyGrayscale(bitmap),adjustment:30)// 问题嵌套调用难以阅读顺序从里到外读方案 B命令式方法extensionMLBitmap{mutatingfuncapplyGrayscale(){...}mutatingfuncapplyBrightness(adjustment:Int){...}}// 使用bitmap.applyGrayscale()bitmap.applyBrightness(adjustment:30)// 问题修改原始数据无法保留中间结果难以测试方案 C协议 链式本框架选择protocolImageFilter{funcapply(to bitmap:MLBitmap)-MLBitmap}// 使用letresultbitmap.applying(GrayscaleFilter()).applying(BrightnessFilter(adjustment:30))// 清晰、可组合、可测试二、ImageFilter 协议的设计哲学协议定向编程POPSwift 的核心设计理念之一用协议而非继承来定义行为。publicprotocolImageFilter{funcapply(to bitmap:MLBitmap)-MLBitmap}这个协议只定义一件事把一个 bitmap 转换成另一个 bitmap。极度简单但这种简单性是强大的。为什么不用继承class hierarchy// 面向对象风格不好classImageFilter{funcapply(to bitmap:MLBitmap)-MLBitmap{fatalError(Subclass must override)}}classGrayscaleFilter:ImageFilter{overridefuncapply(to bitmap:MLBitmap)-MLBitmap{...}}继承的问题强制使用class引用类型增加内存管理复杂度强耦合子类依赖父类实现扩展困难第三方代码无法继承扩展协议的优势struct实现值类型语义第三方可以轻松实现自己的 Filter组合优于继承纯函数Pure Functionfuncapply(to bitmap:MLBitmap)-MLBitmap纯函数的定义相同输入 → 永远产生相同输出确定性没有副作用不修改外部状态纯函数的好处易于测试无需 mock直接传入测试数据易于并行多个 Filter 可以并行处理不同图像没有竞争条件易于组合输出直接作为下一个的输入三、值类型Struct与 Copy-on-Write为什么 MLBitmap 是 structpublicstructMLBitmap{publicvarpixels:[UInt8]...}值类型的语义varbitmap1MLBitmap(width:10,height:10,filling:.white)varbitmap2bitmap1// 看起来像复制bitmap2[0,0].red// 修改 bitmap2// bitmap1[0, 0] 仍然是 .white// 两者完全独立如果 MLBitmap 是classclassMLBitmap{varpixels:[UInt8]}varbitmap2bitmap1// 实际上是引用复制bitmap2.pixels[0]255// bitmap1.pixels[0] 也变了图像处理中每个 Filter 应该生成新图像不影响原图。值类型的语义天然满足这个需求。Copy-on-Write写时复制Swift 的数组[UInt8]实现了 CoWvarpixels1[UInt8](repeating:0,count:1000)varpixels2pixels1// 此时不复制只是共享引用pixels2[0]255// 第一次写入时才真正复制// pixels1 不受影响这使得var result bitmap的操作几乎没有成本——只有当你真正写入result时内存才会复制。性能影响// 三次 Filter 链式调用letresultbitmap.applying(GrayscaleFilter())// 第 1 次 CoW 触发复制 bitmap.applying(BrightnessFilter())// 第 2 次 CoW 触发复制中间结果.applying(ThresholdFilter())// 第 3 次 CoW 触发复制中间结果// 共有 3 次内存复制// 对 100×100 图3 × 40 KB 120 KB几乎不可感知// 对 4K 图3 × 32 MB 96 MB链式调用时峰值内存较高工业级优化Fusion把多个 Filter 的计算合并到一次遍历。四、some ImageFiltervsany ImageFilter// MLBitmap 的链式调用方法funcapplying(_filter:someImageFilter)-MLBitmap{filter.apply(to:self)}some ProtocolOpaque Type调用时类型固定编译器知道具体类型零运行时开销不需要 existential box适合每次调用类型确定的场景any ProtocolExistential Type类型在运行时动态决定有运行时开销existential box vtable 查找适合把不同类型的 Filter 放入同一个数组// 需要存放不同 Filter 的数组时用 anyletpipeline:[anyImageFilter][GrayscaleFilter(),BrightnessFilter(adjustment:30),ThresholdFilter()]letresultpipeline.reduce(bitmap){$1.apply(to:$0)}五、Precondition vs Guard vs Throw三种防御方式框架代码中有三种处理错误的方式选择哪种取决于错误性质precondition编程错误Bug// 调用方传了不合法的参数这是 bug应该在开发阶段崩溃暴露precondition(width0height0,Width and height must be positive)precondition(factor.isFinite,factor must be finite)precondition(values.count%21,Kernel height must be odd)适用不变量被违反是调用方的编程错误。在 Debug 下崩溃暴露 bug在 Release 下行为未定义Swift 优化掉 precondition 检查。guardthrow运行时错误预期可能发生// 图像可能真的很大这不是 bug而是正常运行时的条件guardwidthmaxDimensionheightmaxDimensionelse{throwLoadError.dimensionTooLarge(width:width,height:height)}适用外部资源文件大小、内存限制、网络状态不可控调用方需要处理这些情况。return nil/ 默认值可恢复的退化// CGDataProvider 创建失败返回 nil调用方检查guardletproviderCGDataProvider(data:dataasCFData)else{returnnil}适用失败是轻量级的调用方可以通过 optional 判断处理。选择原则“这种情况不应该发生发生了说明有 bug” →precondition“这种情况可能发生调用方必须处理” →throw“这种情况可能发生调用方可以忽略” →return nil六、inline(__always)和discardableResultinline(__always)inline(__always)funcindex(x:Int,y:Int)-Int{(y*widthx)*Self.bytesPerPixel}这个函数在像素遍历的内层循环中被调用100×100 图调用 10,000 次4K 图调用 800 万次。普通函数调用有开销压栈/出栈、跳转。inline(__always)让编译器把函数体直接嵌入调用处消除调用开销。权衡内联会增加代码体积每个调用处都展开一份代码但对热路径的小函数是合理的。discardableResultdiscardableResultpublicstaticfuncprocess(_bitmap:MLBitmap,to url:URL,scenario:ExportScenario)-ExportResultSwift 默认情况下如果你忽略一个有返回值的函数的返回值编译器会给出警告。discardableResult表示忽略返回值是可以接受的。适用场景返回值提供额外信息如成功/失败详情但调用方也可能只关心副作用文件是否写出而不在乎详细的返回值。七、单一可信来源SSOT原则// ❌ 错误同样的常量在两个地方定义// ImageLoader.swiftletbitmapInfoCGImageAlphaInfo.premultipliedLast.rawValue|CGBitmapInfo.byteOrder32Big.rawValue// ImageExporter.swiftletbitmapInfoCGImageAlphaInfo.premultipliedLast.rawValue|CGBitmapInfo.byteOrder32Big.rawValue// 问题如果只改了一处另一处不同步导致颜色错乱且没有编译器提示// ✅ 正确单一定义双端引用// MLBitmap.swift单一可信来源publicstaticletbitmapInfo:CGBitmapInfoCGBitmapInfo(rawValue:CGImageAlphaInfo.premultipliedLast.rawValue|CGBitmapInfo.byteOrder32Big.rawValue)// ImageLoader.swiftletbitmapInfoMLBitmap.bitmapInfo.rawValue// 引用// ImageExporter.swiftletbitmapInfoMLBitmap.bitmapInfo// 引用SSOT 原则Single Source of Truth每个知识常量、配置、逻辑只在一个地方定义其他地方引用。八、测试驱动的工程化每一个重要功能都有对应的测试testBitmapMemoryLayout() ← 验证内存布局公式 testCoordinateOriginIsTopLeft() ← 验证坐标系约定最容易出错的地方 testGrayscaleLuminanceFormula() ← 验证 BT.709 公式精度 testBrightnessClampMax() ← 验证溢出截断不是回绕 testContrastAnchorPoint() ← 验证 128 锚点不变性 testSobelDetectsVerticalEdge() ← 验证边缘检测有效性 testAutoFormatTransparentImage() ← 验证透明度检测 testResampleReducesOversized() ← 验证等比缩放测试的价值文档化了预期行为代码即文档重构时有安全网改代码不怕破坏已有功能发现设计缺陷如testCoordinateOriginIsTopLeft暴露了坐标系 bug测试的粒度好的测试只测一件事// ❌ 测试太多失败时不知道哪里出了问题functestGrayscale(){// 测试白色、黑色、亮度公式、Alpha 保护……全放在一起}// ✅ 每个测试一个断言functestGrayscaleWhiteStaysWhite(){...}functestGrayscaleBlackStaysBlack(){...}functestGrayscaleLuminanceFormula(){...}functestGrayscaleAlphaUnchanged(){...}九、代码注释的层次本框架的注释分为三层Layer 1文件头注释解释为什么这个文件存在// ImageProcessor.swift// 工业级图像预处理管线//// 职责在导出/上传前根据场景策略对图像进行// 1. 尺寸重采样Resample// 2. 格式选择Format Selection// 3. 质量决策Quality DecisionLayer 2函数注释解释这个函数做什么参数是什么/// 将 UIImage 解码为 MLBitmapRGBA8888 / sRGB。////// 流程UIImage → CGImage → CGContext重新绘制→ [UInt8]/// 通过重新绘制确保颜色空间统一Display P3 / sRGB 均归一化为 sRGB////// - Throws: LoadError尺寸超限 / 内存超限 / CGImage 缺失publicstaticfuncload(from image:UIImage)throws-MLBitmapLayer 3关键步骤注释解释为什么这么做不是这么做会怎样// ⚠️ 不要加 translateBy/scaleBy flip// flip 会把 CGImage row 0 翻到 buffer 末尾// 反而使 bitmap[0,0] 变成视觉「左下角」。context.draw(cgImage,in:CGRect(x:0,y:0,width:width,height:height))原则注释解释为什么而不是重复做什么代码本身已经说明做什么了。十、阶段一学习完整架构回顾MLImageCore │ ├── Core/ │ └── MLBitmap.swift # 核心数据结构struct CoW │ ├── Filters/ │ ├── ImageFilter.swift # 协议定义POP │ ├── GrayscaleFilter.swift # BT.709 灰度化 │ ├── BrightnessFilter.swift # 线性亮度调整 │ ├── ThresholdFilter.swift # 二值化 │ └── ContrastFilter.swift # 对比度调整 │ ├── Algorithms/ │ ├── Convolution.swift # 2D 卷积引擎通用 │ ├── GaussianBlur.swift # 可分离高斯模糊 │ └── SobelEdge.swift # Sobel 边缘检测 │ └── IO/ ├── ImageLoader.swift # UIImage → MLBitmap颜色空间归一化 ├── ImageExporter.swift # MLBitmap → UIImage / 文件回退链 └── ImageProcessor.swift # 工业级管线重采样 格式决策 体积控制每一层都遵循单一职责原则SRPImageLoader只负责加载和格式归一化ImageExporter只负责编码和写文件ImageProcessor只负责决策和调度不操作像素Convolution只是纯数学引擎不知道 Filter 业务十一、工业级 vs 学习级代码维度学习级工业级错误处理强制解包!打印错误结构化 Errorthrow/Result日志print()os.log带级别和类别内存随意分配不考虑峰值预估峰值设置上限提前拦截边界“应该不会发生”preconditionguardthrowAPI功能正确即可命名清晰访问控制合理文档完善测试手动跑一下单元测试覆盖核心路径可扩展性直接改代码协议 预设 自定义接口常量管理魔法数字散落各处SSOT单一定义工业级代码的核心追求在 3 个月后由另一个人来维护这段代码时他能快速理解、安全修改。十二、小结与展望Phase 1 建立了图像处理的基础数据结构和坐标系约定CPU 层的完整算法实现灰度、亮度、对比度、二值化、卷积、高斯模糊、Sobel工业级的 IO 管线颜色空间归一化、格式决策、体积控制良好的代码架构和测试覆盖Phase 2 目标从手写 CPU 算法升级到调用 Apple 系统框架加速Core ImageGPU 渲染管线CIFilter 包装vImage / AccelerateSIMD 向量化更快的卷积直方图分析Otsu 自适应阈值直方图均衡颜色空间转换RGB ↔ HSV ↔ LabPhase 3 目标Metal Compute Shader真正的 GPU 并行数千个像素同时计算实时滤镜30fps 视频处理Custom Compute Kernel自定义 GPU 程序思考题如果要把MLBitmap从 struct 改为 class需要修改哪些地方会带来哪些新的问题设计一个CompositFilter它包含一个[any ImageFilter]数组调用apply时依次执行所有 Filter。写出这个类型的定义并说明它是 struct 还是 class理由是什么在 iOS 开发中如果你的图像处理需要在后台线程运行避免主线程阻塞现有的ImageFilter协议设计需要做什么改动提示Swift Concurrency 的Sendable答案2. 应该是 struct值类型因为它只是 Filter 序列的组合没有共享状态定义struct CompositFilter: ImageFilter { let filters: [any ImageFilter]; func apply(to bitmap: MLBitmap) - MLBitmap { filters.reduce(bitmap) { $1.apply(to: $0) } } }3. 需要让所有 Filter 标注Sendablestruct GrayscaleFilter: ImageFilter, Sendable {}以及让MLBitmap也标注Sendable这样才能在不同 actor 之间安全传递。♥️喜欢我的内容欢迎大家点赞、转发、关注。♥️本人专注于技术投资认知三位一体的内容分享。往期推荐一图了解图像处理中的高斯模糊为什么卷积核通常必须是奇数一图了解卷积中的边界处理一图了解几种常用卷积核一图了解卷积的核心原理一张图带你了解——卷积到底是什么一图了解饱和度控制色彩鲜艳程度的关键一图了解OCR的处理流程及相关图像处理技术一图了解二值化与阈值从灰度到黑白的决策一张图了解图像处理中的亮度、对比度与实现颜色科学与灰度化从图片到内存——你真正理解图像处理的第一天iPhone相册背后的图像处理知识下iPhone相册背后的图像处理知识中iPhone相册背后的图像处理知识上一张图了解图像处理的本质图像到底是什么图像处理技术概要图AI时代软件工程师必备概念全景图