iOS MachineLearning 系列(3)—— 靜態圖像分析之區域識別
本系列的前一篇文章介紹瞭如何使用iOS中自帶的API對圖片中的矩形區域進行分析。在圖像靜態分析方面,矩形區域分析是非常基礎的部分。API還提供了更多面嚮應用的分析能力,如文本區域分析,條形碼二維碼的分析,人臉區域分析,人體分析等。本篇文章主要介紹這些分析API的應用。關於矩形識別的基礎文章,鏈接如下:
https://my.oschina.net/u/2340880/blog/8671152
1 - 文本區域分析
文本區域分析相比矩形區域分析更加上層,其API接口也更加簡單。分析請求的創建示例如下:
private lazy var textDetectionRequest: VNDetectTextRectanglesRequest = {
let textDetectRequest = VNDetectTextRectanglesRequest { request, error in
DispatchQueue.main.async {
self.drawTask(request: request as! VNDetectTextRectanglesRequest)
}
}
// 是否報告字符邊框區域
textDetectRequest.reportCharacterBoxes = true
return textDetectRequest
}()
其請求的發起方式,回調結果的處理與矩形分析一文中介紹的一致,這裏就不再贅述。唯一不同的是,其分析的結果中新增了characterBoxes屬性,用來獲取每個字符的所在區域。
文本區域識別效果如下圖所示:
2 - 條形碼二維碼識別
條形碼和二維碼在生活中非常常見,Vision框架中提供的API不僅支持條碼區域的檢測,還可以直接將條碼的內容識別出來。
條碼分析請求使用VNDetectBarcodesRequest類創建,如下:
open class VNDetectBarcodesRequest : VNImageBasedRequest {
// 類屬性,獲取所支持的條碼類型
open class var supportedSymbologies: [VNBarcodeSymbology] { get }
// 設置分析時要支持的條碼類型
open var symbologies: [VNBarcodeSymbology]
// 結果列表
open var results: [VNBarcodeObservation]? { get }
}
如果我們不對symbologies屬性進行設置,則默認會嘗試識別所有支持的類型。示例代碼如下:
private lazy var barCodeDetectionRequest: VNDetectBarcodesRequest = {
let barCodeDetectRequest = VNDetectBarcodesRequest {[weak self] request, error in
guard let self else {return}
DispatchQueue.main.async {
self.drawTask(request: request as! VNDetectBarcodesRequest)
}
}
barCodeDetectRequest.revision = VNDetectBarcodesRequestRevision1
return barCodeDetectRequest
}()
需要注意,實測需要將分析所使用的算法版本revision設置爲VNDetectBarcodesRequestRevision1。默認使用的版本可能無法分析出結果。
條碼分析的結果類VNBarcodeObservation中會封裝條碼的相關數據,如下:
open class VNBarcodeObservation : VNRectangleObservation {
// 當前條碼的類型
open var symbology: VNBarcodeSymbology { get }
// 條碼的描述對象,不同類型的條碼會有不同的子類實現
open var barcodeDescriptor: CIBarcodeDescriptor? { get }
// 條碼內容
open var payloadStringValue: String? { get }
}
VNBarcodeObservation類也是繼承自VNRectangleObservation類的,因此其也可以分析出條碼所在的區域,需要注意,對於條形碼來說其只能分析出條碼的位置,對於二維碼來說,其可以準確的識別出二維碼的區域,如下圖所示:
注:互聯網上有很多可以生成條碼的工具,例如:
https://www.idcd.com/tool/barcode/encode
3 - 輪廓檢測
相比前面兩種圖像分析能力,輪廓檢測的能力要更加複雜也更加強大一些。其可以通過圖片的對比度差異來對內容輪廓進行分析。輪廓分析使用VNDetectContoursRequest類來創建請求。此類主要功能列舉如下:
open class VNDetectContoursRequest : VNImageBasedRequest {
// 輪廓檢測時的對比度設置,取值0-3之間,此值越大,檢測結果越精確(對於高對比度圖片)
open var contrastAdjustment: Float
// 作爲對比度分界的像素,取值0-1之間,默認0.5,會取居中值
open var contrastPivot: NSNumber?
// 設置檢測時是否是檢測暗色對象,默認爲true,即認爲背景色淺。設置爲false則會在暗色圖中檢測明亮的對象輪廓
open var detectsDarkOnLight: Bool
// 設置檢測圖片時的縮放,輪廓檢測會將圖片進行壓縮,此值取值範圍爲[64..NSUIntegerMax],取最大值時表示使用原圖
open var maximumImageDimension: Int
// 結果數組
open var results: [VNContoursObservation]? { get }
}
其檢測結果VNContoursObservation類中封裝了輪廓的路徑信息,在進行輪廓檢測時,最外層的輪廓可能有很多內層輪廓組成,這些信息也封裝在此類中。如下:
open class VNContoursObservation : VNObservation {
// 內部輪廓個數
open var contourCount: Int { get }
// 獲取指定的輪廓對象
open func contour(at contourIndex: Int) throws -> VNContour
// 頂級輪廓個數
open var topLevelContourCount: Int { get }
// 頂級輪廓數組
open var topLevelContours: [VNContour] { get }
// 根據indexPath獲取輪廓對象
open func contour(at indexPath: IndexPath) throws -> VNContour
// 路徑,會包含內部所有輪廓
open var normalizedPath: CGPath { get }
}
需要注意,其返回的CGPath路徑依然是以單位矩形爲參照的,我們要將其繪製出來,需要對其進行轉換,轉換其實非常簡單,現對其進行方法,並進行x軸方向的鏡像反轉,之後向下進行平移一個標準單位即可。示例如下:
private func drawTask(request: VNDetectContoursRequest) {
boxViews.forEach { v in
v.removeFromSuperview()
}
for result in request.results ?? [] {
let oriPath = result.normalizedPath
var transform = CGAffineTransform.identity.scaledBy(x: imageView.frame.width, y: -imageView.frame.height).translatedBy(x: 0, y: -1)
let layer = CAShapeLayer()
let path = oriPath.copy(using: &transform)
layer.bounds = self.imageView.bounds
layer.anchorPoint = CGPoint(x: 0, y: 0)
imageView.layer.addSublayer(layer)
layer.path = path
layer.strokeColor = UIColor.blue.cgColor
layer.backgroundColor = UIColor.white.cgColor
layer.fillColor = UIColor.gray.cgColor
layer.lineWidth = 1
}
}
原圖與繪製的輪廓圖如下所示:
原圖:
輪廓:
可以通過VNContoursObservation對象來獲取其內的所有輪廓對象,VNContour定義如下:
open class VNContour : NSObject, NSCopying, VNRequestRevisionProviding {
// indexPath
open var indexPath: IndexPath { get }
// 子輪廓個數
open var childContourCount: Int { get }
// 子輪廓對象數組
open var childContours: [VNContour] { get }
// 通過index獲取子輪廓
open func childContour(at childContourIndex: Int) throws -> VNContour
// 描述輪廓的點數
open var pointCount: Int { get }
// 輪廓路徑
open var normalizedPath: CGPath { get }
// 輪廓的縱橫比
open var aspectRatio: Float { get }
// 簡化的多邊形輪廓,參數設置簡化的閾值
open func polygonApproximation(epsilon: Float) throws -> VNContour
}
理論上說,我們對所有的子輪廓進行繪製,也能得到一樣的路徑圖像,例如:
private func drawTask(request: VNDetectContoursRequest) {
boxViews.forEach { v in
v.removeFromSuperview()
}
for result in request.results ?? [] {
for i in 0 ..< result.contourCount {
let contour = try! result.contour(at: i)
var transform = CGAffineTransform.identity.scaledBy(x: imageView.frame.width, y: -imageView.frame.height).translatedBy(x: 0, y: -1)
let layer = CAShapeLayer()
let path = contour.normalizedPath.copy(using: &transform)
layer.bounds = self.imageView.bounds
layer.anchorPoint = CGPoint(x: 0, y: 0)
imageView.layer.addSublayer(layer)
layer.path = path
layer.strokeColor = UIColor.blue.cgColor
layer.backgroundColor = UIColor.clear.cgColor
layer.fillColor = UIColor.clear.cgColor
layer.lineWidth = 1
}
}
}
效果如下圖:
4 - 文檔區域識別
文檔識別可以分析出圖片中的文本段落,使用VNDetectDocumentSegmentationRequest來創建分析請求,VNDetectDocumentSegmentationRequest沒有額外特殊的屬性,其分析結果爲一組VNRectangleObservation對象,可以獲取到文檔所在的矩形區域。這裏不再過多解說。
5 - 人臉區域識別
人臉識別在生活中也有着很廣泛的應用,在進行人臉對比識別等高級處理前,我們通常需要將人臉的區域先提取出來,Vision框架中也提供了人臉區域識別的接口,使用VNDetectFaceRectanglesRequest類來創建請求即可。VNDetectFaceRectanglesRequest類本身比較加單,繼承自VNImageBasedRequest類,無需進行額外的配置即可使用,其分析的結果爲一組VNFaceObservation對象,分析效果如下圖所示:
VNFaceObservation類本身是繼承自VNDetectedObjectObservation類的,因此我們可以直接獲取到人臉的區域。VNFaceObservation中還有許多其他有用的信息:
open class VNFaceObservation : VNDetectedObjectObservation {
// 面部特徵對象
open var landmarks: VNFaceLandmarks2D? { get }
// 人臉在z軸的旋轉度數,取值爲-PI到PI之間
open var roll: NSNumber? { get }
// 人臉在y軸的旋轉度數,取值爲-PI/2到PI/2之間
open var yaw: NSNumber? { get }
// 人臉在x軸的旋轉度數,取值爲-PI/2到PI/2之間
open var pitch: NSNumber? { get }
}
通過roll,yaw和pitch這3個屬性,我們可以獲取到人臉在空間中的角度相關信息。landmarks屬性則比較複雜,其封裝了人臉的特徵點。並且VNDetectFaceRectanglesRequest請求是不會分析面部特徵的,此屬性會爲nil,關於面部特徵,我們後續介紹。
人臉特徵分析請求使用VNDetectFaceLandmarksRequest創建,其返回的結果中會有landmarks數據,示例代碼如下:
private func drawTask(request: VNDetectFaceLandmarksRequest) {
boxViews.forEach { v in
v.removeFromSuperview()
}
for result in request.results ?? [] {
var box = result.boundingBox
// 座標系轉換
box.origin.y = 1 - box.origin.y - box.size.height
let v = UIView()
v.backgroundColor = .clear
v.layer.borderColor = UIColor.red.cgColor
v.layer.borderWidth = 2
imageView.addSubview(v)
let size = imageView.frame.size
v.frame = CGRect(x: box.origin.x * size.width, y: box.origin.y * size.height, width: box.size.width * size.width, height: box.size.height * size.height)
// 進行特徵繪製
let landmarks = result.landmarks
// 拿到所有特徵點
let allPoints = landmarks?.allPoints?.normalizedPoints
let faceRect = result.boundingBox
// 進行繪製
for point in allPoints ?? [] {
//faceRect的寬高是個比例,我們對應轉換成View上的人臉區域寬高
let rectWidth = imageView.frame.width * faceRect.width
let rectHeight = imageView.frame.height * faceRect.height
// 進行座標轉換
// 特徵點的x座標爲人臉區域的比例,
// 1. point.x * rectWidth 得到在人臉區域內的x位置
// 2. + faceRect.minX * imageView.frame.width 得到在View上的x座標
// 3. point.y * rectHeight + faceRect.minY * imageView.frame.height獲得Y座標
// 4. imageView.frame.height - 的作用是y座標進行翻轉
let tempPoint = CGPoint(x: point.x * rectWidth + faceRect.minX * imageView.frame.width, y: imageView.frame.height - (point.y * rectHeight + faceRect.minY * imageView.frame.height))
let subV = UIView()
subV.backgroundColor = .red
subV.frame = CGRect(x: tempPoint.x - 2, y: tempPoint.y - 2, width: 4, height: 4)
imageView.addSubview(subV)
}
}
}
VNFaceLandmarks2D中封裝了很多特徵信息,上面的示例代碼會將所有的特徵點進行繪製,我們也可以根據需要取部分特徵點:
open class VNFaceLandmarks2D : VNFaceLandmarks {
// 所有特徵點
open var allPoints: VNFaceLandmarkRegion2D? { get }
// 只包含面部輪廓的特徵點
open var faceContour: VNFaceLandmarkRegion2D? { get }
// 左眼位置的特徵點
open var leftEye: VNFaceLandmarkRegion2D? { get }
// 右眼位置的特徵點
open var rightEye: VNFaceLandmarkRegion2D? { get }
// 左眉特徵點
open var leftEyebrow: VNFaceLandmarkRegion2D? { get }
// 右眉特徵點
open var rightEyebrow: VNFaceLandmarkRegion2D? { get }
// 鼻子特徵點
open var nose: VNFaceLandmarkRegion2D? { get }
// 鼻尖特徵點
open var noseCrest: VNFaceLandmarkRegion2D? { get }
// 中間特徵點
open var medianLine: VNFaceLandmarkRegion2D? { get }
// 外脣特徵點
open var outerLips: VNFaceLandmarkRegion2D? { get }
// 內脣特徵點
open var innerLips: VNFaceLandmarkRegion2D? { get }
// 左瞳孔特徵點
open var leftPupil: VNFaceLandmarkRegion2D? { get }
// 右瞳孔特徵點
open var rightPupil: VNFaceLandmarkRegion2D? { get }
}
VNFaceLandmarkRegion2D類中具體封裝了特徵點位置信息,需要注意,特徵點的座標是相對人臉區域的比例值,要進行轉換。
主要提示:特徵檢測在模擬器上可能不能正常工作,可以使用真機測試。
默認人臉特徵分析會返回76個特徵點,我們可以通過設置VNDetectFaceLandmarksRequest請求實例的constellation屬性來修改使用的檢測算法,枚舉如下:
public enum VNRequestFaceLandmarksConstellation : UInt, @unchecked Sendable {
case constellationNotDefined = 0
// 使用65個特徵點的算法
case constellation65Points = 1
// 使用73個特徵點的算法
case constellation76Points = 2
}
效果如下圖:
Vision框架的靜態區域分析中與人臉分析相關的還有一種,使用VNDetectFaceCaptureQualityRequest請求可以分析當前捕獲到的人臉的質量,使用此請求分析的結果中會包含如下屬性:
extension VNFaceObservation {
// 人臉捕獲的質量
@nonobjc public var faceCaptureQuality: Float? { get }
}
faceCaptureQualit值越接近1,捕獲的人臉效果越好。
6 - 水平線識別
VNDetectHorizonReques用來創建水平線分析請求,其可以分析出圖片中的水平線位置。此請求本身比較簡單,其返回的結果對象爲VNHorizonObservation,如下:
open class VNHorizonObservation : VNObservation {
// 角度
open var angle: CGFloat { get }
}
分析結果如下圖所示:
7 - 人體相關識別
人體姿勢識別也是Vision框架非常強大的一個功能,其可以將靜態圖像中人體的關鍵節點分析出來,通過這些關鍵節點,我們可以對人體當前的姿勢進行推斷。在運動矯正,健康檢查等應用中應用廣泛。人體姿勢識別請求使用VNDetectHumanBodyPoseRequest類創建,如下:
open class VNDetectHumanBodyPoseRequest : VNImageBasedRequest {
// 獲取所支持檢查的關鍵節點
open class func supportedJointNames(forRevision revision: Int) throws -> [VNHumanBodyPoseObservation.JointName]
// 獲取所支持檢查的關鍵節組
open class func supportedJointsGroupNames(forRevision revision: Int) throws -> [VNHumanBodyPoseObservation.JointsGroupName]
// 分析結果
open var results: [VNHumanBodyPoseObservation]? { get }
}
VNHumanBodyPoseObservatio分析結果類中封裝的有各個關鍵節點的座標信息,如下:
open class VNHumanBodyPoseObservation : VNRecognizedPointsObservation {
// 可用的節點名
open var availableJointNames: [VNHumanBodyPoseObservation.JointName] { get }
// 可用的節點組名
open var availableJointsGroupNames: [VNHumanBodyPoseObservation.JointsGroupName] { get }
// 獲取某個節點座標
open func recognizedPoint(_ jointName: VNHumanBodyPoseObservation.JointName) throws -> VNRecognizedPoint
// 獲取某個節點組
open func recognizedPoints(_ jointsGroupName: VNHumanBodyPoseObservation.JointsGroupName) throws -> [VNHumanBodyPoseObservation.JointName : VNRecognizedPoint]
}
下面示例代碼演示瞭如何對身體姿勢節點進行解析:
private func drawTask(request: VNDetectHumanBodyPoseRequest) {
boxViews.forEach { v in
v.removeFromSuperview()
}
for result in request.results ?? [] {
for point in result.availableJointNames {
if let p = try? result.recognizedPoint(point) {
let v = UIView(frame: CGRect(x: p.x * imageView.bounds.width - 2, y: (1 - p.y) * imageView.bounds.height - 2.0, width: 4, height: 4))
imageView.addSubview(v)
v.backgroundColor = .red
}
}
}
}
效果如下圖:
所有支持的節點名和節點組名列舉如下:
// 節點
extension VNHumanBodyPoseObservation.JointName {
// 鼻子節點
public static let nose: VNHumanBodyPoseObservation.JointName
// 左眼節點
public static let leftEye: VNHumanBodyPoseObservation.JointName
// 右眼節點
public static let rightEye: VNHumanBodyPoseObservation.JointName
// 左耳節點
public static let leftEar: VNHumanBodyPoseObservation.JointName
// 右耳節點
public static let rightEar: VNHumanBodyPoseObservation.JointName
// 左肩節點
public static let leftShoulder: VNHumanBodyPoseObservation.JointName
// 右肩節點
public static let rightShoulder: VNHumanBodyPoseObservation.JointName
// 頸部節點
public static let neck: VNHumanBodyPoseObservation.JointName
// 左肘節點
public static let leftElbow: VNHumanBodyPoseObservation.JointName
// 右肘節點
public static let rightElbow: VNHumanBodyPoseObservation.JointName
// 左腕節點
public static let leftWrist: VNHumanBodyPoseObservation.JointName
// 右腕節點
public static let rightWrist: VNHumanBodyPoseObservation.JointName
// 左髖節點
public static let leftHip: VNHumanBodyPoseObservation.JointName
// 右髖節點
public static let rightHip: VNHumanBodyPoseObservation.JointName
// 軀幹節點
public static let root: VNHumanBodyPoseObservation.JointName
// 左膝節點
public static let leftKnee: VNHumanBodyPoseObservation.JointName
// 右膝節點
public static let rightKnee: VNHumanBodyPoseObservation.JointName
// 左踝節點
public static let leftAnkle: VNHumanBodyPoseObservation.JointName
// 右踝節點
public static let rightAnkle: VNHumanBodyPoseObservation.JointName
}
// 節點組
extension VNHumanBodyPoseObservation.JointsGroupName {
// 面部節點組
public static let face: VNHumanBodyPoseObservation.JointsGroupName
// 軀幹節點組
public static let torso: VNHumanBodyPoseObservation.JointsGroupName
// 左臂節點組
public static let leftArm: VNHumanBodyPoseObservation.JointsGroupName
// 右臂節點組
public static let rightArm: VNHumanBodyPoseObservation.JointsGroupName
// 左腿節點組
public static let leftLeg: VNHumanBodyPoseObservation.JointsGroupName
// 右腿節點組
public static let rightLeg: VNHumanBodyPoseObservation.JointsGroupName
// 所有節點
public static let all: VNHumanBodyPoseObservation.JointsGroupName
}
與人體姿勢識別類似,VNDetectHumanHandPoseRequest用來對手勢進行識別,VNDetectHumanHandPoseRequest定義如下:
open class VNDetectHumanHandPoseRequest : VNImageBasedRequest {
// 支持的手勢節點
open class func supportedJointNames(forRevision revision: Int) throws -> [VNHumanHandPoseObservation.JointName]
// 支持的手勢節點組
open class func supportedJointsGroupNames(forRevision revision: Int) throws -> [VNHumanHandPoseObservation.JointsGroupName]
// 設置最大支持的檢測人手數量,默認2,最大6
open var maximumHandCount: Int
// 識別結果
open var results: [VNHumanHandPoseObservation]? { get }
}
VNHumanHandPoseObservation類的定義如下:
open class VNHumanHandPoseObservation : VNRecognizedPointsObservation {
// 可用的節點名
open var availableJointNames: [VNHumanHandPoseObservation.JointName] { get }
// 可用的節點組名
open var availableJointsGroupNames: [VNHumanHandPoseObservation.JointsGroupName] { get }
// 獲取座標點
open func recognizedPoint(_ jointName: VNHumanHandPoseObservation.JointName) throws -> VNRecognizedPoint
open func recognizedPoints(_ jointsGroupName: VNHumanHandPoseObservation.JointsGroupName) throws -> [VNHumanHandPoseObservation.JointName : VNRecognizedPoint]
// 獲取手性
open var chirality: VNChirality { get }
}
chiralit屬性用來識別左右手,枚舉如下:
@frozen public enum VNChirality : Int, @unchecked Sendable {
// 未知
case unknown = 0
// 左手
case left = -1
// 右手
case right = 1
}
在手勢識別中,可用的節點名列舉如下:
extension VNHumanHandPoseObservation.JointName {
// 手腕節點
public static let wrist: VNHumanHandPoseObservation.JointName
// 拇指關節節點
public static let thumbCMC: VNHumanHandPoseObservation.JointName
public static let thumbMP: VNHumanHandPoseObservation.JointName
public static let thumbIP: VNHumanHandPoseObservation.JointName
public static let thumbTip: VNHumanHandPoseObservation.JointName
// 食指關節節點
public static let indexMCP: VNHumanHandPoseObservation.JointName
public static let indexPIP: VNHumanHandPoseObservation.JointName
public static let indexDIP: VNHumanHandPoseObservation.JointName
public static let indexTip: VNHumanHandPoseObservation.JointName
// 中指關節節點
public static let middleMCP: VNHumanHandPoseObservation.JointName
public static let middlePIP: VNHumanHandPoseObservation.JointName
public static let middleDIP: VNHumanHandPoseObservation.JointName
public static let middleTip: VNHumanHandPoseObservation.JointName
// 無名指關節節點
public static let ringMCP: VNHumanHandPoseObservation.JointName
public static let ringPIP: VNHumanHandPoseObservation.JointName
public static let ringDIP: VNHumanHandPoseObservation.JointName
public static let ringTip: VNHumanHandPoseObservation.JointName
// 小指關節節點
public static let littleMCP: VNHumanHandPoseObservation.JointName
public static let littlePIP: VNHumanHandPoseObservation.JointName
public static let littleDIP: VNHumanHandPoseObservation.JointName
public static let littleTip: VNHumanHandPoseObservation.JointName
}
extension VNHumanHandPoseObservation.JointsGroupName {
// 拇指
public static let thumb: VNHumanHandPoseObservation.JointsGroupName
// 食指
public static let indexFinger: VNHumanHandPoseObservation.JointsGroupName
// 中指
public static let middleFinger: VNHumanHandPoseObservation.JointsGroupName
// 無名指
public static let ringFinger: VNHumanHandPoseObservation.JointsGroupName
// 小指
public static let littleFinger: VNHumanHandPoseObservation.JointsGroupName
// 全部
public static let all: VNHumanHandPoseObservation.JointsGroupName
}
效果如下圖:
如果我們只需要識別人體的軀幹部位,則使用VNDetectHumanRectanglesRequest會非常方便,VNDetectHumanRectanglesRequest定義如下:
open class VNDetectHumanRectanglesRequest : VNImageBasedRequest {
// 設置是否僅僅檢測上半身,默認爲true
open var upperBodyOnly: Bool
// 分析結果
open var results: [VNHumanObservation]? { get }
}
人體軀幹識別的結果用法與矩形識別類似,效果如下:
需要注意:人體姿勢識別和手勢識別的API在模擬器上可能無法正常的工作。
本篇文章,我們介紹了許多關於靜態圖像區域分析和識別的API,這些接口功能強大,且設計的非常簡潔。文本中所涉及到的代碼,都可以在如下Demo中找到:
https://github.com/ZYHshao/MachineLearnDemo
專注技術,懂的熱愛,願意分享,做個朋友