├── CognitiveServices.swift ├── README.md ├── Scribble.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ ├── caroline.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ │ ├── manurink.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ │ └── micpringle.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ ├── caroline.xcuserdatad │ └── xcschemes │ │ ├── Scribble.xcscheme │ │ └── xcschememanagement.plist │ ├── manurink.xcuserdatad │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ │ ├── Scribble.xcscheme │ │ └── xcschememanagement.plist │ └── micpringle.xcuserdatad │ └── xcschemes │ ├── Scribble.xcscheme │ └── xcschememanagement.plist ├── Scribble ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Background.imageset │ │ ├── Background@2x.png │ │ └── Contents.json │ ├── Contents.json │ └── PencilTexture.imageset │ │ ├── Contents.json │ │ └── PencilTexture.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── MyDoodleCanvas.swift ├── MyNumbersModel.mlmodel ├── UIImage+Additions.swift └── ViewController.swift └── ml_things ├── convert.py ├── mycoremlmodel.mlmodel ├── mymodel.pkl └── plot_digits_classification.py /CognitiveServices.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CognitiveServices.swift 3 | // Social Tagger 4 | // 5 | // Created by Alexander Repty on 16.05.16. 6 | // Copyright © 2016 maks apps. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | 13 | /// Lowest level results for both face rectangles and emotion scores. A hit represents one face and its range of emotions. 14 | public typealias EmotionReplyHit = Dictionary 15 | /// Wrapper type for an array of hits (i.e. faces). This is the top-level JSON object. 16 | public typealias EmotionReplyType = Array 17 | 18 | /** 19 | Possible values for detected emotions in images. 20 | */ 21 | public enum CognitiveServicesEmotion: String { 22 | case Anger 23 | case Contempt 24 | case Disgust 25 | case Fear 26 | case Happiness 27 | case Neutral 28 | case Sadness 29 | case Surprise 30 | } 31 | 32 | public struct CognitiveServicesEmotionResult { 33 | public let frame: CGRect 34 | public let emotion: CognitiveServicesEmotion 35 | } 36 | 37 | /// Result closure type for emotion callbacks. 38 | public typealias EmotionResult = ([CognitiveServicesEmotionResult]?, NSError?) -> (Void) 39 | 40 | 41 | /// Result closure type for computer vision callbacks. The first parameter is an array of suitable tags for the image. 42 | public typealias CognitiveServicesTagsResult = ([String]?, NSError?) -> (Void) 43 | 44 | /// Possible results for faces callbacks 45 | public struct CognitiveServicesFacesResult { 46 | public var frame: CGRect 47 | public var faceId : String? 48 | public var landmarks: [CGPoint]? 49 | public var age : Int 50 | public var gender : String 51 | public var facialHair : String? 52 | public var glasses : String? 53 | 54 | init () { 55 | frame = CGRect(x: 0, y: 0, width: 0, height: 0) 56 | faceId = "" 57 | landmarks = nil 58 | age = 0 59 | gender = "" 60 | facialHair = "" 61 | glasses = "" 62 | } 63 | } 64 | /// Result closure type for faces results 65 | public typealias FacesResult = ([CognitiveServicesFacesResult]?, NSError?) -> (Void) 66 | 67 | 68 | /// Fill in your API key here after getting it from https://www.microsoft.com/cognitive-services/en-US/subscriptions 69 | let CognitiveServicesComputerVisionAPIKey = "xxx" 70 | let CognitiveServicesEmotionAPIKey = "xxx" 71 | let CognitiveServicesFacesAPIKey = "xxx" 72 | 73 | /// Caseless enum of available HTTP methods. 74 | /// See https://dev.projectoxford.ai/docs/services/56f91f2d778daf23d8ec6739/operations/56f91f2e778daf14a499e1fa for details 75 | enum CognitiveServicesHTTPMethod { 76 | static let POST = "POST" 77 | static let GET = "GET" 78 | } 79 | 80 | /// Caseless enum of available HTTP header keys. 81 | /// See https://dev.projectoxford.ai/docs/services/56f91f2d778daf23d8ec6739/operations/56f91f2e778daf14a499e1fa for details 82 | enum CognitiveServicesHTTPHeader { 83 | static let SubscriptionKey = "Ocp-Apim-Subscription-Key" 84 | static let ContentType = "Content-Type" 85 | } 86 | 87 | /// Caseless enum of available HTTP parameters. 88 | /// See https://dev.projectoxford.ai/docs/services/56f91f2d778daf23d8ec6739/operations/56f91f2e778daf14a499e1fa for details 89 | enum CognitiveServicesHTTPParameters { 90 | static let VisualFeatures = "visualFeatures" 91 | static let Details = "details" 92 | static let ReturnFaceId = "returnFaceId" 93 | static let ReturnFaceLandmarks = "returnFaceLandmarks" 94 | static let ReturnFaceAttributes = "returnFaceAttributes" 95 | static let Language = "language" 96 | static let Orientation = "detectOrientation" 97 | static let Handwriting = "handwriting" 98 | } 99 | 100 | /// Caseless enum of available HTTP content types. 101 | /// See https://dev.projectoxford.ai/docs/services/56f91f2d778daf23d8ec6739/operations/56f91f2e778daf14a499e1fa for details 102 | enum CognitiveServicesHTTPContentType { 103 | static let JSON = "application/json" 104 | static let OctetStream = "application/octet-stream" 105 | static let FormData = "multipart/form-data" 106 | } 107 | 108 | /// Caseless enum of available visual features to analyse the image for. 109 | /// See https://dev.projectoxford.ai/docs/services/56f91f2d778daf23d8ec6739/operations/56f91f2e778daf14a499e1fa for details 110 | enum CognitiveServicesVisualFeatures { 111 | static let Categories = "Categories" 112 | static let Tags = "Tags" 113 | static let Description = "Description" 114 | static let Faces = "Faces" 115 | static let ImageType = "ImageType" 116 | static let Color = "Color" 117 | static let Adult = "Adult" 118 | } 119 | 120 | ///Caseless enum of available face attributes returned for a sigle face 121 | enum CognitiveServicesFaceAttributes { 122 | static let Age = "age" 123 | static let Gender = "gender" 124 | static let Smile = "smile" 125 | static let FacialHair = "facialHair" 126 | static let Glasses = "glasses" 127 | static let HeadPose = "headPose" 128 | } 129 | 130 | /// Caseless enum of available JSON dictionary keys for the service's reply. 131 | /// See https://dev.projectoxford.ai/docs/services/56f91f2d778daf23d8ec6739/operations/56f91f2e778daf14a499e1fa and https://dev.projectoxford.ai/docs/services/5639d931ca73072154c1ce89/operations/563b31ea778daf121cc3a5fa for details 132 | enum CognitiveServicesKeys { 133 | static let Tags = "tags" 134 | static let Name = "name" 135 | static let Confidence = "confidence" 136 | static let FaceRectangle = "faceRectangle" 137 | static let FaceAttributes = "faceAttributes" 138 | static let FaceLandmarks = "faceLandmarks" 139 | static let FaceIdentifier = "faceId" 140 | static let Scores = "scores" 141 | static let Height = "height" 142 | static let Left = "left" 143 | static let Top = "top" 144 | static let Width = "width" 145 | static let Anger = "anger" 146 | static let Contempt = "contempt" 147 | static let Disgust = "disgust" 148 | static let Fear = "fear" 149 | static let Happiness = "happiness" 150 | static let Neutral = "neutral" 151 | static let Sadness = "sadness" 152 | static let Surprise = "surprise" 153 | static let Mustache = "mustache" 154 | static let Beard = "beard" 155 | static let Sideburns = "sideBurns" 156 | static let Regions = "regions" 157 | static let Lines = "lines" 158 | static let Words = "words" 159 | static let Text = "text" 160 | } 161 | 162 | /// Caseless enum of various configuration parameters. 163 | /// See https://dev.projectoxford.ai/docs/services/56f91f2d778daf23d8ec6739/operations/56f91f2e778daf14a499e1fa for details 164 | enum CognitiveServicesConfiguration { 165 | static let HandwrittenOcrURL = "https://westeurope.api.cognitive.microsoft.com/vision/v1.0/recognizeText" 166 | static let HandwrittenResultURL = "https://westeurope.api.cognitive.microsoft.com/vision/v1.0/textOperations/" 167 | 168 | static let AnalyzeURL = "https://westeurope.api.cognitive.microsoft.com/vision/v1.0/analyze" 169 | static let EmotionURL = "https://westus.api.cognitive.microsoft.com/emotion/v1.0/recognize" 170 | static let FaceDetectURL = "https://westeurope.api.cognitive.microsoft.com/face/v1.0/detect" 171 | 172 | static let JPEGCompressionQuality = 0.9 as CGFloat 173 | static let RequiredConfidence = 0.85 174 | } 175 | 176 | 177 | 178 | 179 | public class CognitiveServices: NSObject { 180 | 181 | /** 182 | Retrieves a list of suitable tags for a given image from Microsoft's Cognitive Services API. 183 | 184 | - parameter image: The image to analyse. 185 | - parameter completion: Callback closure. 186 | */ 187 | public func retrievePlausibleTagsForImage(_ image: UIImage, _ suggestedConfidence: Double, completion: @escaping CognitiveServicesTagsResult) { 188 | assert(CognitiveServicesComputerVisionAPIKey.count > 0, "Please set the value of the API key variable (CognitiveServicesVisualFeaturesAPIKey) before attempting to use the application.") 189 | 190 | print("i got a key - let's do this") 191 | 192 | // We need to specify that we want to retrieve tags for our image as a parameter to the URL. 193 | var urlString = CognitiveServicesConfiguration.AnalyzeURL 194 | urlString += "?\(CognitiveServicesHTTPParameters.VisualFeatures)=\("\(CognitiveServicesVisualFeatures.Tags)")" 195 | 196 | let url = URL(string: urlString) 197 | print("calling the following URL: \(String(describing: url))") 198 | 199 | let request = NSMutableURLRequest(url: url!) 200 | 201 | // The subscription key is always added as an HTTP header field. 202 | request.addValue(CognitiveServicesComputerVisionAPIKey, forHTTPHeaderField: CognitiveServicesHTTPHeader.SubscriptionKey) 203 | // We need to specify that we're sending the image as binary data, since it's possible to supply a JSON-wrapped URL instead. 204 | request.addValue(CognitiveServicesHTTPContentType.OctetStream, forHTTPHeaderField: CognitiveServicesHTTPHeader.ContentType) 205 | 206 | // Convert the image reference to a JPEG binary to submit to the service. If this ends up being over 4 MB, it'll throw an error 207 | // on the server side. In a production environment, you would check for this condition and handle it gracefully (either reduce 208 | // the quality, resize the image or prompt the user to take an action). 209 | let requestData = UIImageJPEGRepresentation(image, CognitiveServicesConfiguration.JPEGCompressionQuality) 210 | request.httpBody = requestData 211 | request.httpMethod = CognitiveServicesHTTPMethod.POST 212 | 213 | let session = URLSession.shared 214 | let task = session.dataTask(with: request as URLRequest) { (data, response, error) in 215 | print("executed task") 216 | if let error = error { 217 | // In case of an error, handle it immediately and exit without doing anything else. 218 | completion(nil, error as NSError?) 219 | return 220 | } 221 | 222 | if let data = data { 223 | print("aaand got data! -> \(data)") 224 | do { 225 | let collectionObject = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) 226 | print("the data: \(collectionObject)") 227 | var result = [String]() 228 | 229 | if let dictionary = collectionObject as? Dictionary { 230 | // Enumerate through the result tags and find those with a high enough confidence rating, disregard the rest. 231 | let tags = dictionary[CognitiveServicesKeys.Tags] 232 | if let typedTags = tags as? Array> { 233 | for tag in typedTags { 234 | let name = tag[CognitiveServicesKeys.Name] 235 | let confidence = tag[CognitiveServicesKeys.Confidence] as! Double 236 | if confidence > suggestedConfidence { 237 | result.append(name! as! String) 238 | } 239 | } 240 | } 241 | } 242 | 243 | print(result) 244 | 245 | completion(result, nil) 246 | return 247 | } 248 | catch _ { 249 | completion(nil, error as NSError?) 250 | return 251 | } 252 | } else { 253 | completion(nil, nil) 254 | return 255 | } 256 | } 257 | 258 | task.resume() 259 | } 260 | 261 | 262 | /** 263 | Retrieves the text on a given image from Microsoft's Cognitive Services API. 264 | 265 | - parameter image: The image to analyse. 266 | - parameter completion: Callback closure. 267 | */ 268 | public func retrieveTextOnImage(_ image: UIImage, completion: @escaping (String?, NSError?) -> ()) { 269 | assert(CognitiveServicesComputerVisionAPIKey.count > 0, "Please set the value of the API key variable (CognitiveServicesVisualFeaturesAPIKey) before attempting to use the application.") 270 | 271 | print("i got a key - let's do this") 272 | 273 | // We need to specify that we want to retrieve tags for our image as a parameter to the URL. 274 | var urlString = CognitiveServicesConfiguration.HandwrittenOcrURL 275 | urlString += "?\(CognitiveServicesHTTPParameters.Handwriting)=true" 276 | 277 | let url = URL(string: urlString) 278 | print("calling the following URL: \(String(describing: url))") 279 | let request = NSMutableURLRequest(url: url!) 280 | 281 | // The subscription key is always added as an HTTP header field. 282 | request.addValue(CognitiveServicesComputerVisionAPIKey, forHTTPHeaderField: CognitiveServicesHTTPHeader.SubscriptionKey) 283 | // We need to specify that we're sending the image as binary data, since it's possible to supply a JSON-wrapped URL instead. 284 | request.addValue(CognitiveServicesHTTPContentType.OctetStream, forHTTPHeaderField: CognitiveServicesHTTPHeader.ContentType) 285 | 286 | // Convert the image reference to a JPEG binary to submit to the service. If this ends up being over 4 MB, it'll throw an error 287 | // on the server side. In a production environment, you would check for this condition and handle it gracefully (either reduce 288 | // the quality, resize the image or prompt the user to take an action). 289 | let requestData = UIImageJPEGRepresentation(image, CognitiveServicesConfiguration.JPEGCompressionQuality) 290 | request.httpBody = requestData 291 | request.httpMethod = CognitiveServicesHTTPMethod.POST 292 | 293 | let session = URLSession.shared 294 | let task = session.dataTask(with: request as URLRequest) { (data, response, error) in 295 | print("executed task") 296 | if let error = error { 297 | // In case of an error, handle it immediately and exit without doing anything else. 298 | completion(nil, error as NSError?) 299 | return 300 | } 301 | 302 | let headerFields = (response as! HTTPURLResponse).allHeaderFields 303 | 304 | let operationID = headerFields["Operation-Location"] as? String 305 | if let opID = operationID { 306 | completion(opID, nil) 307 | } else { 308 | completion(nil, nil) 309 | } 310 | 311 | } 312 | 313 | task.resume() 314 | } 315 | 316 | /** 317 | Retrieves the text on a given image from Microsoft's Cognitive Services API. 318 | 319 | - parameter image: The image to analyse. 320 | - parameter completion: Callback closure. 321 | */ 322 | public func retrieveResultForOcrOperation(_ operationID: String, completion: @escaping CognitiveServicesTagsResult) { 323 | assert(CognitiveServicesComputerVisionAPIKey.count > 0, "Please set the value of the API key variable (CognitiveServicesVisualFeaturesAPIKey) before attempting to use the application.") 324 | 325 | print("i got a key - let's do this") 326 | 327 | let url = URL(string: operationID) 328 | print("calling the following URL: \(String(describing: url))") 329 | let request = NSMutableURLRequest(url: url!) 330 | 331 | // The subscription key is always added as an HTTP header field. 332 | request.addValue(CognitiveServicesComputerVisionAPIKey, forHTTPHeaderField: CognitiveServicesHTTPHeader.SubscriptionKey) 333 | 334 | // Convert the image reference to a JPEG binary to submit to the service. If this ends up being over 4 MB, it'll throw an error 335 | // on the server side. In a production environment, you would check for this condition and handle it gracefully (either reduce 336 | // the quality, resize the image or prompt the user to take an action). 337 | request.httpMethod = CognitiveServicesHTTPMethod.GET 338 | 339 | let session = URLSession.shared 340 | let task = session.dataTask(with: request as URLRequest) { (data, response, error) in 341 | print("executed task") 342 | if let error = error { 343 | // In case of an error, handle it immediately and exit without doing anything else. 344 | completion(nil, error as NSError?) 345 | return 346 | } 347 | 348 | if let data = data { 349 | print("aaand got data! -> \(data)") 350 | do { 351 | let collectionObject = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) 352 | print("the data: \(collectionObject)") 353 | 354 | var result = [String]() 355 | 356 | if let dictionary = collectionObject as? Dictionary { 357 | 358 | print("the dict: \(dictionary)") 359 | if let recognitionResult = dictionary["recognitionResult"] as? Dictionary { 360 | 361 | if let theLines = recognitionResult["lines"] as? Array { 362 | for line in theLines { 363 | let theLine = line as! Dictionary 364 | print("the line: \(theLine)") 365 | if let text = theLine["text"] as? String { 366 | print("the text: \(text)") 367 | result.append(text) 368 | 369 | } 370 | } 371 | } 372 | 373 | } 374 | 375 | } 376 | 377 | completion(result, nil) 378 | return 379 | } 380 | catch _ { 381 | completion(nil, error as NSError?) 382 | return 383 | } 384 | } else { 385 | completion(nil, nil) 386 | return 387 | } 388 | } 389 | 390 | task.resume() 391 | } 392 | 393 | 394 | 395 | /** 396 | Retrieves scores for a range of emotions and calls the completion closure with the most suitable one. 397 | 398 | - parameter image: The image to analyse. 399 | - parameter completion: Callback closure. 400 | */ 401 | public func retrievePlausibleEmotionsForImage(_ image: UIImage, completion: @escaping EmotionResult) { 402 | assert(CognitiveServicesEmotionAPIKey.count > 0, "Please set the value of the API key variable (CognitiveServicesEmotionAPIKey) before attempting to use the application.") 403 | 404 | print("i got a key - let's do this") 405 | 406 | let url = URL(string: CognitiveServicesConfiguration.EmotionURL) 407 | let request = NSMutableURLRequest(url: url!) 408 | 409 | // The subscription key is always added as an HTTP header field. 410 | request.addValue(CognitiveServicesEmotionAPIKey, forHTTPHeaderField: CognitiveServicesHTTPHeader.SubscriptionKey) 411 | // We need to specify that we're sending the image as binary data, since it's possible to supply a JSON-wrapped URL instead. 412 | request.addValue(CognitiveServicesHTTPContentType.OctetStream, forHTTPHeaderField: CognitiveServicesHTTPHeader.ContentType) 413 | 414 | // Convert the image reference to a JPEG binary to submit to the service. If this ends up being over 4 MB, it'll throw an error 415 | // on the server side. In a production environment, you would check for this condition and handle it gracefully (either reduce 416 | // the quality, resize the image or prompt the user to take an action). 417 | let requestData = UIImageJPEGRepresentation(image, 0.9) 418 | request.httpBody = requestData 419 | request.httpMethod = CognitiveServicesHTTPMethod.POST 420 | 421 | print(request) 422 | 423 | let session = URLSession.shared 424 | let task = session.dataTask(with: request as URLRequest) { (data, response, error) in 425 | print("executed task") 426 | 427 | if let error = error { 428 | // In case of an error, handle it immediately and exit without doing anything else. 429 | completion(nil, error as NSError?) 430 | return 431 | } 432 | 433 | if let data = data { 434 | print("aaand got data! -> \(data)") 435 | do { 436 | let collectionObject = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) 437 | print("the data: \(collectionObject)") 438 | var result = [CognitiveServicesEmotionResult]() 439 | 440 | if let array = collectionObject as? EmotionReplyType { 441 | // This is an array of hits, i.e. faces with associated emotions. We iterate through it and 442 | // try to get a complete set of coordinates and the most suitable emotion rating for every one. 443 | for hit in array { 444 | // See if all necessary coordinates for a rectangle are there and create a native data type 445 | // with the information. 446 | var resolvedFrame: CGRect? = nil 447 | if let 448 | frame = hit[CognitiveServicesKeys.FaceRectangle] as? Dictionary, 449 | let top = frame[CognitiveServicesKeys.Top], 450 | let left = frame[CognitiveServicesKeys.Left], 451 | let height = frame[CognitiveServicesKeys.Height], 452 | let width = frame[CognitiveServicesKeys.Width] 453 | { 454 | resolvedFrame = CGRect(x: left, y: top, width: width, height: height) 455 | } 456 | 457 | // Find all the available emotions and see which is the highest scoring one. 458 | var emotion: CognitiveServicesEmotion? = nil 459 | if let 460 | emotions = hit[CognitiveServicesKeys.Scores] as? Dictionary, 461 | let anger = emotions[CognitiveServicesKeys.Anger], 462 | let contempt = emotions[CognitiveServicesKeys.Contempt], 463 | let disgust = emotions[CognitiveServicesKeys.Disgust], 464 | let fear = emotions[CognitiveServicesKeys.Fear], 465 | let happiness = emotions[CognitiveServicesKeys.Happiness], 466 | let neutral = emotions[CognitiveServicesKeys.Neutral], 467 | let sadness = emotions[CognitiveServicesKeys.Sadness], 468 | let surprise = emotions[CognitiveServicesKeys.Surprise] 469 | { 470 | var maximumValue = 0.0 471 | for value in [anger, contempt, disgust, fear, happiness, neutral, sadness, surprise] { 472 | if value <= maximumValue { 473 | continue 474 | } 475 | 476 | maximumValue = value 477 | } 478 | 479 | if anger == maximumValue { 480 | emotion = .Anger 481 | } else if contempt == maximumValue { 482 | emotion = .Contempt 483 | } else if disgust == maximumValue { 484 | emotion = .Disgust 485 | } else if fear == maximumValue { 486 | emotion = .Fear 487 | } else if happiness == maximumValue { 488 | emotion = .Happiness 489 | } else if neutral == maximumValue { 490 | emotion = .Neutral 491 | } else if sadness == maximumValue { 492 | emotion = .Sadness 493 | } else if surprise == maximumValue { 494 | emotion = .Surprise 495 | } 496 | } 497 | 498 | // If we have both a rectangle and an emotion, we have enough information to store this as 499 | // a result set and eventually return it to the caller. 500 | if let frame = resolvedFrame, let emotion = emotion { 501 | result.append(CognitiveServicesEmotionResult(frame: frame, emotion: emotion)) 502 | } 503 | } 504 | } 505 | 506 | completion(result, nil) 507 | return 508 | } 509 | catch _ { 510 | completion(nil, error as NSError?) 511 | return 512 | } 513 | } else { 514 | completion(nil, nil) 515 | return 516 | } 517 | } 518 | 519 | task.resume() 520 | } 521 | 522 | 523 | /** 524 | Retrieves face rectangles and features of faces within a picture. 525 | 526 | - parameter image: The image to analyse. 527 | - parameter completion: Callback closure. 528 | */ 529 | public func retrieveFacesForImage(_ image: UIImage, completion: FacesResult?) { 530 | assert(CognitiveServicesFacesAPIKey.count > 0, "Please set the value of the API key variable (CognitiveServicesFacesAPIKey) before attempting to use the application.") 531 | 532 | print("i got a key - let's do this") 533 | 534 | var urlString = CognitiveServicesConfiguration.FaceDetectURL 535 | urlString += "?\(CognitiveServicesHTTPParameters.ReturnFaceId)=true&\(CognitiveServicesHTTPParameters.ReturnFaceLandmarks)=true&\(CognitiveServicesHTTPParameters.ReturnFaceAttributes)=age,gender,facialHair,glasses" 536 | 537 | let url = URL(string: urlString) 538 | let request = NSMutableURLRequest(url: url!) 539 | 540 | request.addValue(CognitiveServicesFacesAPIKey, forHTTPHeaderField: CognitiveServicesHTTPHeader.SubscriptionKey) 541 | request.addValue(CognitiveServicesHTTPContentType.OctetStream, forHTTPHeaderField: CognitiveServicesHTTPHeader.ContentType) 542 | 543 | let requestData = UIImageJPEGRepresentation(image, 0.9) 544 | request.httpBody = requestData 545 | request.httpMethod = CognitiveServicesHTTPMethod.POST 546 | 547 | print(request) 548 | 549 | let session = URLSession.shared 550 | let task = session.dataTask(with: request as URLRequest) { (data, response, error) in 551 | print("executed task") 552 | 553 | if let error = error { 554 | completion!(nil, error as NSError?) 555 | return 556 | } 557 | 558 | if let data = data { 559 | print("aaand got data! -> \(data)") 560 | do { 561 | let collectionObject = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) 562 | print("the data: \(collectionObject)") 563 | 564 | var resultArray = [CognitiveServicesFacesResult]() 565 | var result = CognitiveServicesFacesResult() 566 | 567 | let allData = collectionObject as? [Dictionary] 568 | if let dictionary = allData?[0] { 569 | 570 | let rawFaceRectangle = dictionary[CognitiveServicesKeys.FaceRectangle] 571 | if let rect = rawFaceRectangle as? Dictionary { 572 | result.frame = CGRect( 573 | x: CGFloat(rect[CognitiveServicesKeys.Left]!), 574 | y: CGFloat(rect[CognitiveServicesKeys.Top]!), 575 | width: CGFloat(rect[CognitiveServicesKeys.Width]!), 576 | height: CGFloat(rect[CognitiveServicesKeys.Height]!) 577 | ) 578 | } 579 | 580 | let rawfaceLandmarks = dictionary[CognitiveServicesKeys.FaceLandmarks] 581 | if let landmarks = rawfaceLandmarks as? Dictionary> { 582 | var landmarkPoints = [CGPoint]() 583 | for landmark in landmarks { 584 | let points = landmark.value 585 | landmarkPoints.append( CGPoint(x: points["x"]!, y: points["y"]!)) 586 | } 587 | result.landmarks = landmarkPoints 588 | } 589 | 590 | let rawAttributes = dictionary[CognitiveServicesKeys.FaceAttributes] 591 | if let attributes = rawAttributes as? Dictionary { 592 | result.age = attributes[CognitiveServicesFaceAttributes.Age] as! Int 593 | result.gender = attributes[CognitiveServicesFaceAttributes.Gender] as! String 594 | 595 | if let facialHair = attributes[CognitiveServicesFaceAttributes.FacialHair] as? Dictionary { 596 | var val : Double = 0 597 | for hair in facialHair { 598 | if hair.value > val { 599 | val = hair.value 600 | result.facialHair = hair.key 601 | } 602 | } 603 | } 604 | 605 | if let glasses = attributes[CognitiveServicesFaceAttributes.Glasses] as? String { 606 | result.glasses = glasses 607 | } 608 | } 609 | 610 | result.faceId = dictionary[CognitiveServicesKeys.FaceIdentifier] as? String 611 | } 612 | 613 | resultArray.append(result) 614 | 615 | print(resultArray) 616 | 617 | completion!(resultArray, nil) 618 | return 619 | } 620 | catch _ { 621 | completion!(nil, error as NSError?) 622 | return 623 | } 624 | } else { 625 | completion!(nil, nil) 626 | return 627 | } 628 | } 629 | task.resume() 630 | } 631 | 632 | } 633 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![license](https://img.shields.io/github/license/mashape/apistatus.svg?maxAge=2592000)]() Platform iOS Swift 3 compatible 2 | 3 | # Doodle Recognition - with Machine Learning! 4 | 5 | This very basic iOS app handles three things quite well: 6 | - Show you how drawing with the Apple Pencil works on a basic level 7 | - Show you how handwritten recognition works with Microsoft Cognitive Services 8 | - Show you how handwritten number recognition works with a self trained CoreML model 9 | 10 | The first part is covered mainly in the MyDoodleCanvas.swift file, where the touch handling, drawing and triggering of the handwritten OCR is handled. 11 | 12 | The second part is wrapped in the CognitiveServices.swift file, where two API calls are needed for getting the handwritten text within an image recognized. First you send the image to the textRecognition endpoint and get an operation_location URL back. Because the recognition can take a while, you are able to check back with this URL if the processing is already finished. If the status in the response is SUCCESS, the function just takes the result out of the text array and displays it right under your doodled text. 13 | 14 |

15 | 16 |

17 | 18 | The third part is dealing with recognition of handwritten text - focusing on numbers - with a self trained CoreML model. The big advantage here is that it works completely offline and can be trained in a very customised way. We are using just numbers for now and take a decent Python script to get the model in shape. 19 | 20 |

21 | 22 |

23 | 24 | If you need a more detailed explanation, just visit my medium posts on this topic. There are three of them and they cover the whole doodling and text recognition API calling super extensively but in a very easy way for you to comprehend. 25 | - [The Doodling Workshop #1 - Get a grip of your Apple Pencil](https://medium.com/@codeprincess/the-doodling-workshop-1-ae955e351f7b) 26 | - [The Doodling Workshop #2 - Recognise your handwriting](https://medium.com/@codeprincess/the-doodling-workshop-2-9c763c21c92b) 27 | - [The Doodling Workshop #3 - The playground is your canvas](https://medium.com/@codeprincess/the-doodling-workshop-3-70d8e360956a) 28 | - [The Doodling Workshop #4 - Machine Learning for the noobs](https://medium.com/@codeprincess/machine-learning-in-ios-for-the-noob-6c2cdd04b00b) 29 | 30 | ## Get yourself an API Key 31 | Keep in mind to get yourself a Cognitive Services Computer Vision API Key to authenticate against the OCR API the app will use. But don’t worry. You’ll have one generated in no time and moreover get a free one for trial. 32 | 33 | Just visit the [Getting Started page](https://azure.microsoft.com/en-us/try/cognitive-services/) and right after having your API key generated, add it at line 69 in the Cognitive Services.swift file or just add it plain in your own implementation when adding the Ocp-Apim-Subscription-Key field to your request header . 34 | -------------------------------------------------------------------------------- /Scribble.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 204E56E32051763A001D162C /* MyNumbersModel.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = 204E56E22051763A001D162C /* MyNumbersModel.mlmodel */; }; 11 | 204E56E520518571001D162C /* UIImage+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 204E56E420518571001D162C /* UIImage+Additions.swift */; }; 12 | 206509081F2F66790059D614 /* CognitiveServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 206509071F2F66790059D614 /* CognitiveServices.swift */; }; 13 | 82B175511C0A7A2000A199A6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82B175501C0A7A2000A199A6 /* AppDelegate.swift */; }; 14 | 82B175531C0A7A2000A199A6 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82B175521C0A7A2000A199A6 /* ViewController.swift */; }; 15 | 82B175561C0A7A2000A199A6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 82B175541C0A7A2000A199A6 /* Main.storyboard */; }; 16 | 82B175581C0A7A2000A199A6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 82B175571C0A7A2000A199A6 /* Assets.xcassets */; }; 17 | 82B1755B1C0A7A2000A199A6 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 82B175591C0A7A2000A199A6 /* LaunchScreen.storyboard */; }; 18 | 82B175631C0A7A7D00A199A6 /* MyDoodleCanvas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82B175621C0A7A7D00A199A6 /* MyDoodleCanvas.swift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXFileReference section */ 22 | 204E56E22051763A001D162C /* MyNumbersModel.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; path = MyNumbersModel.mlmodel; sourceTree = ""; }; 23 | 204E56E420518571001D162C /* UIImage+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Additions.swift"; sourceTree = ""; }; 24 | 206509071F2F66790059D614 /* CognitiveServices.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CognitiveServices.swift; path = ../CognitiveServices.swift; sourceTree = ""; }; 25 | 82B1754D1C0A7A2000A199A6 /* Scribble.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Scribble.app; sourceTree = BUILT_PRODUCTS_DIR; }; 26 | 82B175501C0A7A2000A199A6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 27 | 82B175521C0A7A2000A199A6 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 28 | 82B175551C0A7A2000A199A6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 29 | 82B175571C0A7A2000A199A6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 30 | 82B1755A1C0A7A2000A199A6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 31 | 82B1755C1C0A7A2100A199A6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 32 | 82B175621C0A7A7D00A199A6 /* MyDoodleCanvas.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyDoodleCanvas.swift; sourceTree = ""; }; 33 | /* End PBXFileReference section */ 34 | 35 | /* Begin PBXFrameworksBuildPhase section */ 36 | 82B1754A1C0A7A2000A199A6 /* Frameworks */ = { 37 | isa = PBXFrameworksBuildPhase; 38 | buildActionMask = 2147483647; 39 | files = ( 40 | ); 41 | runOnlyForDeploymentPostprocessing = 0; 42 | }; 43 | /* End PBXFrameworksBuildPhase section */ 44 | 45 | /* Begin PBXGroup section */ 46 | 82B175441C0A7A2000A199A6 = { 47 | isa = PBXGroup; 48 | children = ( 49 | 82B1754F1C0A7A2000A199A6 /* Scribble */, 50 | 82B1754E1C0A7A2000A199A6 /* Products */, 51 | ); 52 | sourceTree = ""; 53 | }; 54 | 82B1754E1C0A7A2000A199A6 /* Products */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | 82B1754D1C0A7A2000A199A6 /* Scribble.app */, 58 | ); 59 | name = Products; 60 | sourceTree = ""; 61 | }; 62 | 82B1754F1C0A7A2000A199A6 /* Scribble */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | 82B175501C0A7A2000A199A6 /* AppDelegate.swift */, 66 | 82B175541C0A7A2000A199A6 /* Main.storyboard */, 67 | 82B175521C0A7A2000A199A6 /* ViewController.swift */, 68 | 82B175621C0A7A7D00A199A6 /* MyDoodleCanvas.swift */, 69 | 206509071F2F66790059D614 /* CognitiveServices.swift */, 70 | 204E56E22051763A001D162C /* MyNumbersModel.mlmodel */, 71 | 82B175571C0A7A2000A199A6 /* Assets.xcassets */, 72 | 82B175591C0A7A2000A199A6 /* LaunchScreen.storyboard */, 73 | 82B1755C1C0A7A2100A199A6 /* Info.plist */, 74 | 204E56E420518571001D162C /* UIImage+Additions.swift */, 75 | ); 76 | path = Scribble; 77 | sourceTree = ""; 78 | }; 79 | /* End PBXGroup section */ 80 | 81 | /* Begin PBXNativeTarget section */ 82 | 82B1754C1C0A7A2000A199A6 /* Scribble */ = { 83 | isa = PBXNativeTarget; 84 | buildConfigurationList = 82B1755F1C0A7A2100A199A6 /* Build configuration list for PBXNativeTarget "Scribble" */; 85 | buildPhases = ( 86 | 82B175491C0A7A2000A199A6 /* Sources */, 87 | 82B1754A1C0A7A2000A199A6 /* Frameworks */, 88 | 82B1754B1C0A7A2000A199A6 /* Resources */, 89 | ); 90 | buildRules = ( 91 | ); 92 | dependencies = ( 93 | ); 94 | name = Scribble; 95 | productName = Scribble; 96 | productReference = 82B1754D1C0A7A2000A199A6 /* Scribble.app */; 97 | productType = "com.apple.product-type.application"; 98 | }; 99 | /* End PBXNativeTarget section */ 100 | 101 | /* Begin PBXProject section */ 102 | 82B175451C0A7A2000A199A6 /* Project object */ = { 103 | isa = PBXProject; 104 | attributes = { 105 | LastSwiftUpdateCheck = 0710; 106 | LastUpgradeCheck = 0920; 107 | ORGANIZATIONNAME = "Caroline Begbie"; 108 | TargetAttributes = { 109 | 82B1754C1C0A7A2000A199A6 = { 110 | CreatedOnToolsVersion = 7.1.1; 111 | DevelopmentTeam = 8W62GL8FE7; 112 | LastSwiftMigration = 0920; 113 | }; 114 | }; 115 | }; 116 | buildConfigurationList = 82B175481C0A7A2000A199A6 /* Build configuration list for PBXProject "Scribble" */; 117 | compatibilityVersion = "Xcode 3.2"; 118 | developmentRegion = English; 119 | hasScannedForEncodings = 0; 120 | knownRegions = ( 121 | en, 122 | Base, 123 | ); 124 | mainGroup = 82B175441C0A7A2000A199A6; 125 | productRefGroup = 82B1754E1C0A7A2000A199A6 /* Products */; 126 | projectDirPath = ""; 127 | projectRoot = ""; 128 | targets = ( 129 | 82B1754C1C0A7A2000A199A6 /* Scribble */, 130 | ); 131 | }; 132 | /* End PBXProject section */ 133 | 134 | /* Begin PBXResourcesBuildPhase section */ 135 | 82B1754B1C0A7A2000A199A6 /* Resources */ = { 136 | isa = PBXResourcesBuildPhase; 137 | buildActionMask = 2147483647; 138 | files = ( 139 | 82B1755B1C0A7A2000A199A6 /* LaunchScreen.storyboard in Resources */, 140 | 82B175581C0A7A2000A199A6 /* Assets.xcassets in Resources */, 141 | 82B175561C0A7A2000A199A6 /* Main.storyboard in Resources */, 142 | ); 143 | runOnlyForDeploymentPostprocessing = 0; 144 | }; 145 | /* End PBXResourcesBuildPhase section */ 146 | 147 | /* Begin PBXSourcesBuildPhase section */ 148 | 82B175491C0A7A2000A199A6 /* Sources */ = { 149 | isa = PBXSourcesBuildPhase; 150 | buildActionMask = 2147483647; 151 | files = ( 152 | 82B175531C0A7A2000A199A6 /* ViewController.swift in Sources */, 153 | 82B175511C0A7A2000A199A6 /* AppDelegate.swift in Sources */, 154 | 204E56E32051763A001D162C /* MyNumbersModel.mlmodel in Sources */, 155 | 206509081F2F66790059D614 /* CognitiveServices.swift in Sources */, 156 | 82B175631C0A7A7D00A199A6 /* MyDoodleCanvas.swift in Sources */, 157 | 204E56E520518571001D162C /* UIImage+Additions.swift in Sources */, 158 | ); 159 | runOnlyForDeploymentPostprocessing = 0; 160 | }; 161 | /* End PBXSourcesBuildPhase section */ 162 | 163 | /* Begin PBXVariantGroup section */ 164 | 82B175541C0A7A2000A199A6 /* Main.storyboard */ = { 165 | isa = PBXVariantGroup; 166 | children = ( 167 | 82B175551C0A7A2000A199A6 /* Base */, 168 | ); 169 | name = Main.storyboard; 170 | sourceTree = ""; 171 | }; 172 | 82B175591C0A7A2000A199A6 /* LaunchScreen.storyboard */ = { 173 | isa = PBXVariantGroup; 174 | children = ( 175 | 82B1755A1C0A7A2000A199A6 /* Base */, 176 | ); 177 | name = LaunchScreen.storyboard; 178 | sourceTree = ""; 179 | }; 180 | /* End PBXVariantGroup section */ 181 | 182 | /* Begin XCBuildConfiguration section */ 183 | 82B1755D1C0A7A2100A199A6 /* Debug */ = { 184 | isa = XCBuildConfiguration; 185 | buildSettings = { 186 | ALWAYS_SEARCH_USER_PATHS = NO; 187 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 188 | CLANG_CXX_LIBRARY = "libc++"; 189 | CLANG_ENABLE_MODULES = YES; 190 | CLANG_ENABLE_OBJC_ARC = YES; 191 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 192 | CLANG_WARN_BOOL_CONVERSION = YES; 193 | CLANG_WARN_COMMA = YES; 194 | CLANG_WARN_CONSTANT_CONVERSION = YES; 195 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 196 | CLANG_WARN_EMPTY_BODY = YES; 197 | CLANG_WARN_ENUM_CONVERSION = YES; 198 | CLANG_WARN_INFINITE_RECURSION = YES; 199 | CLANG_WARN_INT_CONVERSION = YES; 200 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 201 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 202 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 203 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 204 | CLANG_WARN_STRICT_PROTOTYPES = YES; 205 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 206 | CLANG_WARN_UNREACHABLE_CODE = YES; 207 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 208 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 209 | COPY_PHASE_STRIP = NO; 210 | DEBUG_INFORMATION_FORMAT = dwarf; 211 | ENABLE_STRICT_OBJC_MSGSEND = YES; 212 | ENABLE_TESTABILITY = YES; 213 | GCC_C_LANGUAGE_STANDARD = gnu99; 214 | GCC_DYNAMIC_NO_PIC = NO; 215 | GCC_NO_COMMON_BLOCKS = YES; 216 | GCC_OPTIMIZATION_LEVEL = 0; 217 | GCC_PREPROCESSOR_DEFINITIONS = ( 218 | "DEBUG=1", 219 | "$(inherited)", 220 | ); 221 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 222 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 223 | GCC_WARN_UNDECLARED_SELECTOR = YES; 224 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 225 | GCC_WARN_UNUSED_FUNCTION = YES; 226 | GCC_WARN_UNUSED_VARIABLE = YES; 227 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 228 | MTL_ENABLE_DEBUG_INFO = YES; 229 | ONLY_ACTIVE_ARCH = YES; 230 | SDKROOT = iphoneos; 231 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 232 | TARGETED_DEVICE_FAMILY = 2; 233 | }; 234 | name = Debug; 235 | }; 236 | 82B1755E1C0A7A2100A199A6 /* Release */ = { 237 | isa = XCBuildConfiguration; 238 | buildSettings = { 239 | ALWAYS_SEARCH_USER_PATHS = NO; 240 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 241 | CLANG_CXX_LIBRARY = "libc++"; 242 | CLANG_ENABLE_MODULES = YES; 243 | CLANG_ENABLE_OBJC_ARC = YES; 244 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 245 | CLANG_WARN_BOOL_CONVERSION = YES; 246 | CLANG_WARN_COMMA = YES; 247 | CLANG_WARN_CONSTANT_CONVERSION = YES; 248 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 249 | CLANG_WARN_EMPTY_BODY = YES; 250 | CLANG_WARN_ENUM_CONVERSION = YES; 251 | CLANG_WARN_INFINITE_RECURSION = YES; 252 | CLANG_WARN_INT_CONVERSION = YES; 253 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 254 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 255 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 256 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 257 | CLANG_WARN_STRICT_PROTOTYPES = YES; 258 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 259 | CLANG_WARN_UNREACHABLE_CODE = YES; 260 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 261 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 262 | COPY_PHASE_STRIP = NO; 263 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 264 | ENABLE_NS_ASSERTIONS = NO; 265 | ENABLE_STRICT_OBJC_MSGSEND = YES; 266 | GCC_C_LANGUAGE_STANDARD = gnu99; 267 | GCC_NO_COMMON_BLOCKS = YES; 268 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 269 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 270 | GCC_WARN_UNDECLARED_SELECTOR = YES; 271 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 272 | GCC_WARN_UNUSED_FUNCTION = YES; 273 | GCC_WARN_UNUSED_VARIABLE = YES; 274 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 275 | MTL_ENABLE_DEBUG_INFO = NO; 276 | SDKROOT = iphoneos; 277 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 278 | TARGETED_DEVICE_FAMILY = 2; 279 | VALIDATE_PRODUCT = YES; 280 | }; 281 | name = Release; 282 | }; 283 | 82B175601C0A7A2100A199A6 /* Debug */ = { 284 | isa = XCBuildConfiguration; 285 | buildSettings = { 286 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 287 | DEVELOPMENT_TEAM = 8W62GL8FE7; 288 | INFOPLIST_FILE = Scribble/Info.plist; 289 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 290 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 291 | PRODUCT_BUNDLE_IDENTIFIER = com.razeware.Scribble; 292 | PRODUCT_NAME = "$(TARGET_NAME)"; 293 | SWIFT_SWIFT3_OBJC_INFERENCE = On; 294 | SWIFT_VERSION = 4.0; 295 | }; 296 | name = Debug; 297 | }; 298 | 82B175611C0A7A2100A199A6 /* Release */ = { 299 | isa = XCBuildConfiguration; 300 | buildSettings = { 301 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 302 | DEVELOPMENT_TEAM = 8W62GL8FE7; 303 | INFOPLIST_FILE = Scribble/Info.plist; 304 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 305 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 306 | PRODUCT_BUNDLE_IDENTIFIER = com.razeware.Scribble; 307 | PRODUCT_NAME = "$(TARGET_NAME)"; 308 | SWIFT_SWIFT3_OBJC_INFERENCE = On; 309 | SWIFT_VERSION = 4.0; 310 | }; 311 | name = Release; 312 | }; 313 | /* End XCBuildConfiguration section */ 314 | 315 | /* Begin XCConfigurationList section */ 316 | 82B175481C0A7A2000A199A6 /* Build configuration list for PBXProject "Scribble" */ = { 317 | isa = XCConfigurationList; 318 | buildConfigurations = ( 319 | 82B1755D1C0A7A2100A199A6 /* Debug */, 320 | 82B1755E1C0A7A2100A199A6 /* Release */, 321 | ); 322 | defaultConfigurationIsVisible = 0; 323 | defaultConfigurationName = Release; 324 | }; 325 | 82B1755F1C0A7A2100A199A6 /* Build configuration list for PBXNativeTarget "Scribble" */ = { 326 | isa = XCConfigurationList; 327 | buildConfigurations = ( 328 | 82B175601C0A7A2100A199A6 /* Debug */, 329 | 82B175611C0A7A2100A199A6 /* Release */, 330 | ); 331 | defaultConfigurationIsVisible = 0; 332 | defaultConfigurationName = Release; 333 | }; 334 | /* End XCConfigurationList section */ 335 | }; 336 | rootObject = 82B175451C0A7A2000A199A6 /* Project object */; 337 | } 338 | -------------------------------------------------------------------------------- /Scribble.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Scribble.xcodeproj/project.xcworkspace/xcuserdata/caroline.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codePrincess/doodlingRecognition/7aa1e4c88a8807ced2808cf82aca77b5a77ea9d2/Scribble.xcodeproj/project.xcworkspace/xcuserdata/caroline.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Scribble.xcodeproj/project.xcworkspace/xcuserdata/manurink.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codePrincess/doodlingRecognition/7aa1e4c88a8807ced2808cf82aca77b5a77ea9d2/Scribble.xcodeproj/project.xcworkspace/xcuserdata/manurink.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Scribble.xcodeproj/project.xcworkspace/xcuserdata/micpringle.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codePrincess/doodlingRecognition/7aa1e4c88a8807ced2808cf82aca77b5a77ea9d2/Scribble.xcodeproj/project.xcworkspace/xcuserdata/micpringle.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Scribble.xcodeproj/xcuserdata/caroline.xcuserdatad/xcschemes/Scribble.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Scribble.xcodeproj/xcuserdata/caroline.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Scribble.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 82B1754C1C0A7A2000A199A6 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Scribble.xcodeproj/xcuserdata/manurink.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 20 | 21 | 22 | 24 | 36 | 37 | 38 | 40 | 52 | 53 | 54 | 56 | 68 | 69 | 70 | 72 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Scribble.xcodeproj/xcuserdata/manurink.xcuserdatad/xcschemes/Scribble.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /Scribble.xcodeproj/xcuserdata/manurink.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Scribble.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 82B1754C1C0A7A2000A199A6 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Scribble.xcodeproj/xcuserdata/micpringle.xcuserdatad/xcschemes/Scribble.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Scribble.xcodeproj/xcuserdata/micpringle.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Scribble.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 82B1754C1C0A7A2000A199A6 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Scribble/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Razeware LLC 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | import UIKit 24 | 25 | @UIApplicationMain 26 | class AppDelegate: UIResponder, UIApplicationDelegate { 27 | 28 | var window: UIWindow? 29 | 30 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 31 | application.applicationSupportsShakeToEdit = true 32 | return true 33 | } 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Scribble/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "ipad", 5 | "size" : "20x20", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "ipad", 10 | "size" : "20x20", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "ipad", 15 | "size" : "29x29", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "ipad", 20 | "size" : "29x29", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "ipad", 25 | "size" : "40x40", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "ipad", 30 | "size" : "40x40", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "76x76", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "76x76", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "83.5x83.5", 46 | "scale" : "2x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } -------------------------------------------------------------------------------- /Scribble/Assets.xcassets/Background.imageset/Background@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codePrincess/doodlingRecognition/7aa1e4c88a8807ced2808cf82aca77b5a77ea9d2/Scribble/Assets.xcassets/Background.imageset/Background@2x.png -------------------------------------------------------------------------------- /Scribble/Assets.xcassets/Background.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "Background@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Scribble/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Scribble/Assets.xcassets/PencilTexture.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "PencilTexture.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Scribble/Assets.xcassets/PencilTexture.imageset/PencilTexture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codePrincess/doodlingRecognition/7aa1e4c88a8807ced2808cf82aca77b5a77ea9d2/Scribble/Assets.xcassets/PencilTexture.imageset/PencilTexture.png -------------------------------------------------------------------------------- /Scribble/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Scribble/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /Scribble/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UIRequiresFullScreen 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Scribble/MyDoodleCanvas.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | import CoreML 4 | 5 | enum RecognizeMode : Int { 6 | case cognitiveServiceOCR = 0 7 | case localMLModel 8 | } 9 | 10 | @available(iOS 11.0, *) 11 | public class MyDoodleCanvas: UIImageView { 12 | 13 | let pi = CGFloat(Double.pi) 14 | 15 | let forceSensitivity: CGFloat = 4.0 16 | var pencilTexture = UIColor(patternImage: UIImage(named: "PencilTexture")!) 17 | let minLineWidth: CGFloat = 5 18 | 19 | // current drawing rect 20 | var minX = 0 21 | var minY = 0 22 | var maxX = 0 23 | var maxY = 0 24 | 25 | var trackTimer : Timer? 26 | var lastTouchTimestamp : TimeInterval? 27 | var ocrImageRect : CGRect? 28 | var currentTextRect : CGRect? 29 | 30 | // Parameters 31 | let defaultLineWidth:CGFloat = 5 32 | 33 | var drawColor: UIColor = UIColor.black 34 | var markerColor: UIColor = UIColor.lightGray 35 | 36 | var eraserColor: UIColor { 37 | return backgroundColor ?? UIColor.white 38 | } 39 | 40 | var drawingImage: UIImage? 41 | var context : CGContext? 42 | 43 | var singleNumberModel : MyNumbersModel? 44 | var recognizeMode : RecognizeMode = .cognitiveServiceOCR 45 | 46 | public func setup () { 47 | 48 | resetDoodleRect() 49 | 50 | lastTouchTimestamp = 0 51 | 52 | trackTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { 53 | timer in 54 | 55 | let now = Date().timeIntervalSince1970 56 | 57 | if Int(self.lastTouchTimestamp!) > 0 && now - self.lastTouchTimestamp! > 1 { 58 | self.drawDoodlingRect(context: self.context) 59 | } 60 | }) 61 | 62 | singleNumberModel = MyNumbersModel() 63 | 64 | } 65 | 66 | func resetDoodleRect() { 67 | minX = Int(self.frame.width) 68 | minY = Int(self.frame.height) 69 | 70 | maxX = 0 71 | maxY = 0 72 | 73 | lastTouchTimestamp = 0 74 | 75 | UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0) 76 | context = UIGraphicsGetCurrentContext() 77 | } 78 | 79 | func updateRecognitionMode (_ mode: RecognizeMode) { 80 | recognizeMode = mode 81 | 82 | switch recognizeMode { 83 | case .cognitiveServiceOCR: 84 | print("MODE CHANGED: cognitive services OCR is active") 85 | case .localMLModel: 86 | print("MODE CHANGED: local ML is active - numbers only") 87 | } 88 | } 89 | 90 | 91 | override public func touchesMoved(_ touches: Set, with event: UIEvent?) { 92 | 93 | guard let touch = touches.first else { 94 | return 95 | } 96 | 97 | lastTouchTimestamp = Date().timeIntervalSince1970 98 | 99 | //print(touch.azimuthUnitVector(in: self)) 100 | 101 | 102 | // Draw previous image into context 103 | image?.draw(in: bounds) 104 | 105 | 106 | var touches = [UITouch]() 107 | if let coalescedTouches = event?.coalescedTouches(for: touch) { 108 | touches = coalescedTouches 109 | } else { 110 | touches.append(touch) 111 | } 112 | 113 | for touch in touches { 114 | drawStroke(context: context, touch: touch) 115 | } 116 | 117 | //drawStroke(context: context, touch: touch) 118 | 119 | /* 120 | if let predictedTouches = event?.predictedTouches(for: touch) { 121 | for touch in predictedTouches { 122 | drawStroke(context: context, touch: touch) 123 | } 124 | } 125 | */ 126 | 127 | image = UIGraphicsGetImageFromCurrentImageContext() 128 | } 129 | 130 | /* 131 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 132 | image = drawingImage 133 | } 134 | 135 | override func touchesCancelled(_ touches: Set?, with event: UIEvent?) { 136 | image = drawingImage 137 | } 138 | */ 139 | 140 | func drawStroke(context: CGContext?, touch: UITouch) { 141 | let previousLocation = touch.previousLocation(in: self) 142 | let location = touch.location(in: self) 143 | 144 | // Calculate line width for drawing stroke 145 | var lineWidth:CGFloat 146 | let tiltThreshold : CGFloat = pi/6 147 | 148 | if touch.type == .stylus { 149 | 150 | minX = min(minX, Int(location.x)) 151 | minY = min(minY, Int(location.y)) 152 | maxX = max(maxX, Int(location.x)) 153 | maxY = max(maxY, Int(location.y)) 154 | 155 | if touch.altitudeAngle < tiltThreshold { 156 | lineWidth = lineWidthForShading(context: context, touch: touch) 157 | } else { 158 | lineWidth = lineWidthForDrawing(context: context, touch: touch) 159 | } 160 | 161 | //pencilTexture.setStroke() 162 | 163 | UIColor.black.setStroke() 164 | 165 | // Configure line 166 | context!.setLineWidth(lineWidth) 167 | context!.setLineCap(.round) 168 | 169 | context?.move(to: previousLocation) 170 | context?.addLine(to: location) 171 | 172 | // Draw the stroke 173 | context!.strokePath() 174 | 175 | } 176 | /*else { 177 | lineWidth = touch.majorRadius / 2 178 | UIColor.blue.setStroke() 179 | }*/ 180 | 181 | 182 | 183 | } 184 | 185 | func drawDoodlingRect(context: CGContext?) { 186 | let inset = 5 187 | 188 | markerColor.setStroke() 189 | context!.setLineWidth(1.0) 190 | context!.setLineCap(.round) 191 | UIColor.clear.setFill() 192 | 193 | print("\(minX),\(minY),\(maxX-minX),\(maxY-minY)") 194 | 195 | ocrImageRect = CGRect(x: minX - inset, y: minY - inset, width: (maxX-minX) + inset*2, height: (maxY-minY) + 2*inset) 196 | //context!.addRect(ocrImageRect!) 197 | // Draw the stroke 198 | context!.strokePath() 199 | 200 | drawTextRect(context: context, rect: ocrImageRect!) 201 | 202 | image = UIGraphicsGetImageFromCurrentImageContext() 203 | UIGraphicsEndImageContext() 204 | 205 | fetchOCRText() 206 | 207 | resetDoodleRect() 208 | } 209 | 210 | func drawTextRect(context: CGContext?, rect: CGRect) { 211 | UIColor.lightGray.setStroke() 212 | currentTextRect = CGRect(x: rect.origin.x, y: rect.origin.y + rect.height, width: rect.width, height: 15) 213 | context!.addRect(currentTextRect!) 214 | context!.strokePath() 215 | } 216 | 217 | func addLabelForOCR(text: String) { 218 | DispatchQueue.main.async { 219 | let label = UILabel(frame: self.currentTextRect!) 220 | print(label.frame) 221 | label.text = text.count > 0 ? text : "Text not recognized" 222 | label.font = UIFont(name: "Helvetica Neue", size: 9) 223 | self.addSubview(label) 224 | } 225 | } 226 | 227 | func lineWidthForShading(context: CGContext?, touch: UITouch) -> CGFloat { 228 | 229 | let previousLocation = touch.previousLocation(in: self) 230 | let location = touch.location(in: self) 231 | 232 | let vector1 = touch.azimuthUnitVector(in: self) 233 | 234 | let vector2 = CGPoint(x: location.x - previousLocation.x, y: location.y - previousLocation.y) 235 | 236 | var angle = abs(atan2(vector2.y, vector2.x) - atan2(vector1.dy, vector1.dx)) 237 | 238 | if angle > pi { 239 | angle = 2 * pi - angle 240 | } 241 | if angle > pi / 2 { 242 | angle = pi - angle 243 | } 244 | 245 | let minAngle: CGFloat = 0 246 | let maxAngle = pi / 2 247 | let normalizedAngle = (angle - minAngle) / (maxAngle - minAngle) 248 | 249 | let maxLineWidth: CGFloat = 60 250 | var lineWidth = maxLineWidth * normalizedAngle 251 | 252 | let tiltThreshold : CGFloat = pi/6 253 | let minAltitudeAngle: CGFloat = 0.25 254 | let maxAltitudeAngle = tiltThreshold 255 | 256 | let altitudeAngle = touch.altitudeAngle < minAltitudeAngle ? minAltitudeAngle : touch.altitudeAngle 257 | 258 | let normalizedAltitude = 1 - ((altitudeAngle - minAltitudeAngle) / (maxAltitudeAngle - minAltitudeAngle)) 259 | 260 | lineWidth = lineWidth * normalizedAltitude + minLineWidth 261 | 262 | let minForce: CGFloat = 0.0 263 | let maxForce: CGFloat = 5 264 | 265 | let normalizedAlpha = (touch.force - minForce) / (maxForce - minForce) 266 | 267 | context!.setAlpha(normalizedAlpha) 268 | 269 | return lineWidth 270 | } 271 | 272 | func lineWidthForDrawing(context: CGContext?, touch: UITouch) -> CGFloat { 273 | var lineWidth = defaultLineWidth 274 | 275 | if touch.force > 0 { 276 | lineWidth = touch.force * forceSensitivity 277 | } 278 | 279 | return lineWidth 280 | } 281 | 282 | func clearCanvas(_ animated: Bool) { 283 | if animated { 284 | UIView.animate(withDuration: 0.5, animations: { 285 | self.alpha = 0 286 | }, completion: { finished in 287 | self.alpha = 1 288 | self.image = nil 289 | self.drawingImage = nil 290 | for subview in self.subviews { 291 | subview.removeFromSuperview() 292 | } 293 | UIGraphicsGetCurrentContext()?.clear(self.bounds) 294 | }) 295 | } else { 296 | image = nil 297 | drawingImage = nil 298 | } 299 | } 300 | 301 | func fetchOCRText () { 302 | let ocrImage = image!.crop(rect: ocrImageRect!) 303 | 304 | switch recognizeMode { 305 | case .cognitiveServiceOCR: 306 | recognizeWithCognitiveServiceOCR(ocrImage) 307 | case .localMLModel: 308 | recognizeWithLocalModel(ocrImage) 309 | break 310 | } 311 | } 312 | 313 | func recognizeWithCognitiveServiceOCR(_ image: UIImage) { 314 | let manager = CognitiveServices() 315 | 316 | manager.retrieveTextOnImage(image) { 317 | operationURL, error in 318 | 319 | if error != nil { 320 | self.addLabelForOCR(text: "Uhoh, error occured :( \n\(error!)") 321 | return 322 | } 323 | 324 | if #available(iOS 10.0, *) { 325 | 326 | let when = DispatchTime.now() + 2 // change 2 to desired number of seconds 327 | DispatchQueue.main.asyncAfter(deadline: when) { 328 | 329 | manager.retrieveResultForOcrOperation(operationURL!, completion: { 330 | results, error -> (Void) in 331 | 332 | if let theResult = results { 333 | print("my results \(String(describing: theResult))") 334 | var ocrText = "" 335 | for result in theResult { 336 | ocrText = "\(ocrText) \(result)" 337 | } 338 | print("ocr text: \(ocrText)") 339 | self.addLabelForOCR(text: ocrText) 340 | } else { 341 | self.addLabelForOCR(text: "No text for writing") 342 | } 343 | }) 344 | } 345 | } else { 346 | // Fallback on earlier versions 347 | } 348 | } 349 | } 350 | 351 | func recognizeWithLocalModel(_ image: UIImage) { 352 | if let data = generateMultiArrayFrom(image: image) { 353 | 354 | guard let modelOutput = try? singleNumberModel?.prediction(input: data) else { 355 | print("uhoh error happend!") 356 | return 357 | } 358 | 359 | if let result = modelOutput?.classLabel { 360 | print(result) 361 | self.addLabelForOCR(text: "\(result)") 362 | } else { 363 | print("no result available") 364 | } 365 | } 366 | } 367 | 368 | func generateMultiArrayFrom(image: UIImage) -> MLMultiArray? { 369 | 370 | guard let data = try? MLMultiArray(shape: [8,8], dataType: .double) else { 371 | print("uhoh - error on creating the multiarray") 372 | return nil 373 | } 374 | 375 | /*let imageDataArray : [[Double]] = [[0,0,0,16,12,0,0,0], 376 | [0,0,0,16,16,0,0,0], 377 | [0,16,0,3,16,0,0,0], 378 | [0,0,0,0,16,0,0,0], 379 | [0,0,0,0,16,0,0,0], 380 | [0,0,0,0,16,0,0,0], 381 | [0,0,0,0,16,0,0,0], 382 | [0,0,0,0,16,0,0,0]] 383 | */ 384 | 385 | let hTileWidth = image.size.width / 8 386 | let vTileWidth = image.size.height / 8 387 | 388 | var xPos : CGFloat = 0 389 | var yPos : CGFloat = 0 390 | 391 | for rowIndex in 0...7 { 392 | for colIndex in 0...7 { 393 | 394 | //cut the image part at the certain coordinates 395 | let imageRect = CGRect(x: xPos, y: yPos, width: hTileWidth, height: vTileWidth) 396 | let cutImage = image.crop(rect: imageRect) 397 | 398 | let avgColor = cutImage.areaAverage() 399 | var grayscale: CGFloat = 0 400 | var alpha: CGFloat = 0 401 | avgColor.getWhite(&grayscale, alpha: &alpha) 402 | 403 | xPos += hTileWidth 404 | 405 | let alphaAsNumber = NSNumber(integerLiteral: Int(alpha * 16.0)) 406 | print("[\(rowIndex)/\(colIndex)] : \(alphaAsNumber)") 407 | data[rowIndex*8 + colIndex] = alphaAsNumber 408 | } 409 | xPos = 0 410 | yPos += vTileWidth 411 | } 412 | 413 | return data 414 | } 415 | } 416 | 417 | extension UIImage { 418 | func crop( rect: CGRect) -> UIImage { 419 | var rect = rect 420 | rect.origin.x*=self.scale 421 | rect.origin.y*=self.scale 422 | rect.size.width*=self.scale 423 | rect.size.height*=self.scale 424 | 425 | let imageRef = self.cgImage!.cropping(to: rect) 426 | let image = UIImage(cgImage: imageRef!, scale: self.scale, orientation: self.imageOrientation) 427 | return image 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /Scribble/MyNumbersModel.mlmodel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codePrincess/doodlingRecognition/7aa1e4c88a8807ced2808cf82aca77b5a77ea9d2/Scribble/MyNumbersModel.mlmodel -------------------------------------------------------------------------------- /Scribble/UIImage+Additions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Foundation 3 | 4 | extension UIImage { 5 | func areaAverage() -> UIColor { 6 | var bitmap = [UInt8](repeating: 0, count: 4) 7 | 8 | if #available(iOS 9.0, *) { 9 | // Get average color. 10 | let context = CIContext() 11 | let inputImage: CIImage = ciImage ?? CoreImage.CIImage(cgImage: cgImage!) 12 | let extent = inputImage.extent 13 | let inputExtent = CIVector(x: extent.origin.x, y: extent.origin.y, z: extent.size.width, w: extent.size.height) 14 | let filter = CIFilter(name: "CIAreaAverage", withInputParameters: [kCIInputImageKey: inputImage, kCIInputExtentKey: inputExtent])! 15 | let outputImage = filter.outputImage! 16 | let outputExtent = outputImage.extent 17 | assert(outputExtent.size.width == 1 && outputExtent.size.height == 1) 18 | 19 | // Render to bitmap. 20 | context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: kCIFormatRGBA8, colorSpace: CGColorSpaceCreateDeviceRGB()) 21 | } else { 22 | // Create 1x1 context that interpolates pixels when drawing to it. 23 | let context = CGContext(data: &bitmap, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)! 24 | let inputImage = cgImage ?? CIContext().createCGImage(ciImage!, from: ciImage!.extent) 25 | 26 | // Render to bitmap. 27 | context.draw(inputImage!, in: CGRect(x: 0, y: 0, width: 1, height: 1)) 28 | } 29 | 30 | // Compute result. 31 | let result = UIColor(red: CGFloat(bitmap[0]) / 255.0, green: CGFloat(bitmap[1]) / 255.0, blue: CGFloat(bitmap[2]) / 255.0, alpha: CGFloat(bitmap[3]) / 255.0) 32 | return result 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Scribble/ViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Razeware LLC 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | import UIKit 24 | 25 | class ViewController: UIViewController { 26 | 27 | @IBOutlet weak var canvasView: MyDoodleCanvas! 28 | @IBOutlet weak var modePicker: UISegmentedControl! 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | canvasView.clearCanvas(false) 33 | } 34 | 35 | override func viewDidAppear(_ animated: Bool) { 36 | super.viewDidAppear(animated) 37 | canvasView.setup() 38 | } 39 | 40 | // Shake to clear screen 41 | override func motionEnded(_ motion: UIEventSubtype, with event: UIEvent?) { 42 | canvasView.clearCanvas(true) 43 | } 44 | 45 | @IBAction func modeValueChanged(_ sender: Any) { 46 | canvasView.updateRecognitionMode(RecognizeMode(rawValue: modePicker.selectedSegmentIndex)!) 47 | } 48 | 49 | @IBAction func clearCanvasTaped(_ sender: Any) { 50 | canvasView.clearCanvas(true) 51 | } 52 | } 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /ml_things/convert.py: -------------------------------------------------------------------------------- 1 | from coremltools.converters import sklearn as sklearn_to_ml 2 | from sklearn.externals import joblib 3 | 4 | model = joblib.load('mymodel.pkl') 5 | 6 | print('Converting model') 7 | coreml_model = sklearn_to_ml.convert(model) 8 | 9 | print('Saving CoreML model') 10 | coreml_model.save('mycoremlmodel.mlmodel') -------------------------------------------------------------------------------- /ml_things/mycoremlmodel.mlmodel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codePrincess/doodlingRecognition/7aa1e4c88a8807ced2808cf82aca77b5a77ea9d2/ml_things/mycoremlmodel.mlmodel -------------------------------------------------------------------------------- /ml_things/mymodel.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codePrincess/doodlingRecognition/7aa1e4c88a8807ced2808cf82aca77b5a77ea9d2/ml_things/mymodel.pkl -------------------------------------------------------------------------------- /ml_things/plot_digits_classification.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #print(__doc__) 3 | 4 | # Author: Gael Varoquaux 5 | # License: BSD 3 clause 6 | 7 | # Standard scientific Python imports 8 | import matplotlib.pyplot as plt 9 | import pickle 10 | 11 | # Import datasets, classifiers and performance metrics 12 | from sklearn import datasets, svm, metrics 13 | 14 | # The digits dataset 15 | digits = datasets.load_digits() 16 | 17 | # The data that we are interested in is made of 8x8 images of digits, let's 18 | # have a look at the first 4 images, stored in the `images` attribute of the 19 | # dataset. If we were working from image files, we could load them using 20 | # matplotlib.pyplot.imread. Note that each image must have the same size. For these 21 | # images, we know which digit they represent: it is given in the 'target' of 22 | # the dataset. 23 | images_and_labels = list(zip(digits.images, digits.target)) 24 | for index, (image, label) in enumerate(images_and_labels[:4]): 25 | plt.subplot(2, 4, index + 1) 26 | plt.axis('off') 27 | plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest') 28 | plt.title('Training: %i' % label) 29 | 30 | # To apply a classifier on this data, we need to flatten the image, to 31 | # turn the data in a (samples, feature) matrix: 32 | n_samples = len(digits.images) 33 | data = digits.images.reshape((n_samples, -1)) 34 | 35 | # Create a classifier: a support vector classifier 36 | classifier = svm.SVC(gamma=0.001) 37 | 38 | # We learn the digits on the first half of the digits 39 | classifier.fit(data[:n_samples // 2], digits.target[:n_samples // 2]) 40 | 41 | with open('mymodel.pkl', 'wb') as file: 42 | pickle.dump(classifier, file, protocol=pickle.HIGHEST_PROTOCOL) 43 | 44 | # Now predict the value of the digit on the second half: 45 | expected = digits.target[n_samples // 2:] 46 | predicted = classifier.predict(data[n_samples // 2:]) 47 | 48 | print("Classification report for classifier %s:\n%s\n" 49 | % (classifier, metrics.classification_report(expected, predicted))) 50 | print("Confusion matrix:\n%s" % metrics.confusion_matrix(expected, predicted)) 51 | 52 | images_and_predictions = list(zip(digits.images[n_samples // 2:], predicted)) 53 | for index, (image, prediction) in enumerate(images_and_predictions[:4]): 54 | plt.subplot(2, 4, index + 5) 55 | plt.axis('off') 56 | plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest') 57 | plt.title('Prediction: %i' % prediction) 58 | 59 | plt.show() 60 | 61 | 62 | 63 | --------------------------------------------------------------------------------