iOS MachineLearning 系列(3)—— 靜態圖像分析之區域識別

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

專注技術,懂的熱愛,願意分享,做個朋友

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章