Image Orientation


原文成于2018年12月


背景

为什么存在 Image Orientation

传统相机

  • 传统相机横向拍摄, 胶卷和照片也按横向方位固定尺寸.
  • 拍竖直方向的景的时候, 摄影师把相机旋转90度, 拍到的原始照片是横向的.
  • 要正确还原现场的情况, 需要手动把照片旋转90度.

DSLR

  • 用户的持相机方式和需求没有本质变化.
  • 仍然需要外部额外信息来帮助确定怎么旋转拍得的图像.
    • 用户使用相机上的 UI, 手动输入旋转信息.
    • 根据硬件的重力加速计自动确定方向.
  • 基于性能和效率原因, 并不会把 raw data 进行旋转生成一张新图片, 而是加上一个 tag, 这个 tag 就表示 image orientation.
  • 各端显示这样的图片时, 需要读取这个 tag, 并在渲染时给 raw data 加上一个 transform.

对图像算法的影响

当对某个图像执行某些算法时, 除了必要的图像 data 信息, 还可能需要已知的 orientation 信息. 例如很多人脸检测算法, 在有先验的 orientation 知识时, 通常能有更好的效果. 例如, Apple 的 CoreImage 支持人脸检测, 其 API 接受 orientation 参数.

但需要注意, 这个 orientation 本质上就是一个 Int 值. 但某个 orientation 值对应的标识符名称, 在不同的 API 下, 可能对应不同的含义. 通常有两种可能性:

  • 把 raw data 变换成自然方位的图像时, 应该进行的旋转
  • 自然的图像在保存成 raw data 时, 执行的旋转; 或者说成 raw data 相对于自然方位所进行的旋转

在我个人接触到的大部分 API 中, 这里的 orientation, 一般表示前者.

Orientation tag 的表示

Raw exif 信息

任意来自第三方相机或手机拍摄的图像文件, orientation 存储在 exif 信息中. 共有8个可能的取值.

Value 0th Row 0th Column
1 top left
2 top right
3 bottom right
4 bottom left
5 left top
6 right top
7 right bottom
8 left bottom

下图表述的是: 假设拍摄对象自然成 FF 图像时, 每一种 orientation 下, 存储在 raw data 中的图像姿态.

exif orientation

对表中的每一行有两种等价的解释:

  • raw data中, 0th row, 0th column, 对应合理的真实场景中的哪条边.
  • 把 raw data 通过 transform 还原后, 0th row, 0th column 被变换到哪个位置.

例如: 当 orientation 是5时, 可以发现, raw data 中第0行的 data, 对应 FF 左边的一竖, 即应该变换到 left; raw data 中第0列的 data, 对应 FF 顶上的一行, 即应该变换到 top.

Apple API

  • 在 Apple 平台上, 使用 ImageIO 这个 framework, 通过URL/file data 直接读取, 可以在kCGImagePropertyOrientation这个 property key 的位置直接读取到 tag.
  • 这个值可以直接输入许多算法, 例如上述 face detection filter.

UIImage相关 API

  • UIImageNSImage的简化版, 本身不包含任何 meta data, 当然没有 exif 信息.
    • UIImage除了包含 raw data 外, 通过一个额外的imageOrientation属性来保存等价的 orientation 信息.
    • UIImagePickerController生成UIImage时, framework 已经填充好imageOrientation.
    • 通过 AVFoundation 的AVCaptureOutput返回 data 时, 返回的 pixel buffer 没有任何 orientation 的信息. 需要手动组装:
      • 可以通过当前系统的硬件情况.
      • 或者读取由 output 随 buffer 返回的 metadata.
      • 或者通过captureConnectionvideoOrientation来 map 到一个imageOrientation.
      • isVideoMirrored是否会影响获取到的值, 需要额外验证下.
  • UIImage.Orientation的定义: 一个enum, 每个 case 的名称其实非常好理解, 他表示的就是 raw data 应该如何旋转/transform 来还原到真实的自然场景. 遇到标识符带Mirrored的, 意思是把 raw data 先按当前未旋转时的 y 轴左右镜像, 再旋转 (或者先旋转, 再按转完后的 local y 轴进行 mirror. 两者是一个意思)
    • 例如, UIImage.Orientation.right表示的就是: 把 raw data 朝右转90度(顺时针90度), 就可以还原到自然状态.
    • 又如, UIImage.Orientation.rightMirrored表示的就是, 先把 raw data 左右翻转, 再朝右转90度, 就可以还原到自然状态.
  • UIImage.Orientation的使用
    • UIImagecgImageUIImagePNGRepresentation生成的 data, 都完全不包含 orientation, 因此遇到UIImage直接进行操作/存储时, 就等于只是用/保存了 raw data, 而非矫正过朝向的图片.
    • UIImage的各个draw方法, 已经自动考虑其imageOrientation.
    • 手动操纵cgImage的 data 绘制时
      • 涉及到CGContext绘制的场合, 需要使用imageOrientation对 context 进行 transform 再进行绘制, 或先生成一张已矫正过的临时 image.
      • 如果需要传入给需要 exif orientation tag 的算法, 则应当把imageOrientation转成 exif 的 tag.
      • imageOrientationkCGImagePropertyOrientation的转化对应关系, 可以根据 exif specification 手动转换 rawValue, 也可以用 Apple 定义的 enum 直接进行同名 case 的映射 (例如Image.Orientation.right对应CGImagePropertyOrientation.right), 两者其实是一回事.

需要注意的是, iOS 有些时候, 例如把带 exif orientation tag 的CGImageDestination存储到某个 file path 时, 似乎会把对应的 orientation 信息消耗掉, 最终存储的是已经旋转过的图片. 之所以这么做, 我想应该是系统为将来操作进行的一种简化. 再读取该文件时, 会发现没有对应的 orientation 信息了. 而另一些时候会把 exif 存储下来, 例如使用UIImageJPEGRepresentation的时候.
总之对于我们来说, 只需要依赖 API 就好, 不要自己去 cache orientation 的信息.

使用 UIKit 获取UIImage时, 系统对imageOrientation的填充方式

虽然对我们来说, UIImagePickerController返回的UIImage如何填充imageOrientation, 很多时候是一个透明的操作: 我们只需要取出imageOrientation, 对该值分情况进行操作就好. 但如果能进一步了解 iOS 填充imageOrientation的规则, 就可以在很多地方避免常识性错误. 一个最简单的例子就是, 用手机竖着拍人的时候, 返回的imageOrientaion不是.up.

基本相对关系

当我们把相机朝向前方, 逆时针转90度时, 其实从相机的 local 视点来看, 世界是朝顺时针方向转了90度. 如果我们要把此时拍到的图片还原到现实世界的方位, 我们应该逆时针旋转拍得的照片.

后置摄像头

  • 后置相机在出厂时, 其安装正向方位对应 device 的UIDeviceOrientation.landscapeLeft(此时手机屏幕面向用户, home button 在右). 在这个方位下使用 UIKit 拍摄的UIImage, imageOrientation.up. 之所以设定这个 device 方向为标准正方位, 我想应该是为了沿袭历史.
  • 手机方位为为.portrait时, 相当于把手机从正方位顺时针旋转了90度, 按上述基本相对关系, UIKit 为生成的 image 填充的 orientation 为.right
  • 剩下2个除.faceUp.faceDown的方位同理

前置摄像头

前置摄像头有一个mirror的概念, 因为在屏幕朝向拍摄者的情况下, 如果把前置摄像头实际拍摄到的景象直接展示到屏幕上, 会发现实际中物体运动的方向和屏幕上物体运动的方向不一致, 容易使人产生误解.
因此一般情况下, 我们开发的软件会选择把拍到的图片在旋转矫正完的基础上作一次左右 mirror. (注意这里要先旋转, 再左右 mirror)


实际上, UIImagePickerController在拍摄实时预览时使用的是 mirror 后的图, 但最终输出(也就是按下拍摄按钮后弹出的确认预览时的图)的是未 mirror 的图, 两者不是统一的.


我们先看未 mirror 的情况

  • 前置相机和后置相机的安装方位相反, 对应 device 方位为.landscapeRight时, 为相机初始的正向安装方位, 此时生成的 image 未 mirror 情况下为: .up.
  • 手机方位为.portrait时, 在摄像头视角下 (不是在人眼的面朝屏幕的视角下), 相当于把相机顺时针旋转90度, 按上述分析, UIKit 生成的 image 朝向为.right.

也就是说, 无论是前置还是后置摄像头, 在没有 mirror 的情况下, 只要手机是.portrait, UIKit 生成的 image 朝向一律为.right.


如果需要 mirror, 只能是我们开发者把图像进行调整. 最快速的方法就是直接修改拿到的图像的imageOrientation. 但需要注意的是, 不能简单地在原来的 case 名字后加上"Mirrored"字样来解决. 这正是前文所说的, 因为"~Mirrored"表示的是把图像在旋转前先左右 mirror (或者等价地说, 要在旋转后的图上按新的旋转过的对称轴 mirror), 而我们预期的行为是先旋转, 最后再左右 mirror. 当左右翻转和某个特定的旋转操作结合时, mirror 发生的先后会导致不同的结果.
正确的 mirror 操作对应的 case 调整应该是:

  • up -> upMirrored
  • down -> downMirrored
  • left -> rightMirrored (Special)
  • right -> leftMirrored (Special)

Case: Face Detection 中的坐标变换

问题: 拍摄一张人像, 在屏幕上显示该图像, 并框出这个图像中的人脸区域. 在不额外产生中间 image 的情况下如何解决.

流程简单描述如下:

  • UIImagePicker生成UIImage, 其cgImage为 unadjusted raw data.
  • 执行 face detection 时, 同时传入 raw data 和 image orientation. 生成的 face bounds, 其 origin, 是在 unadjusted orientation 下, 相对于左下角的坐标.
  • 要把人头crop出来, 需要:
    • 把 face bounds 的转成在 raw data image 坐标系下, origin 相对左上角的坐标
    • 直接通过 raw data 建立 quartz image, 并用上述变换过 origin 的 bounds 执行 image crop, crop 的结果是未调整朝向的人脸 data.
  • 在 UI 上绘制图像和人脸对应的 view:
    • UIImageView渲染整张图像, 整个过程中涉及到 orientation 的地方由UIKit handle, 无难度.
    • 用一个带边框的 subview/layer 表示人脸位置, 需要计算出, 把 raw data 按 orientation 执行一次 transform 后, 此时的 face bounds 距离旋转后的图片左上角的坐标.
      • 如果图片非1:1映射到屏幕, 则需要把上述坐标按图片真实尺寸和 view 的关系进行缩放/offset, 就可以得到最终的 face view/layer 的 frame.