├── Sudoku ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── 1024.png │ │ ├── 120-1.png │ │ ├── 120.png │ │ ├── 180.png │ │ ├── 40.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 80.png │ │ ├── 87.png │ │ └── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── sudoku.imageset │ │ └── Contents.json │ └── sudokuImage.imageset │ │ ├── Contents.json │ │ └── sudoku.svg ├── Sudoku-Bridging-Header.h ├── DL │ └── model_64.mlpackage │ │ ├── Data │ │ └── com.apple.CoreML │ │ │ ├── model_64.mlmodel │ │ │ ├── Metadata.json │ │ │ └── FeatureDescriptions.json │ │ └── Manifest.json ├── Extensions │ ├── String+.swift │ ├── CALayer+.swift │ ├── UIColor+.swift │ └── UIImage+.swift ├── Localizable │ ├── zh-Hans.lproj │ │ ├── InfoPlist.strings │ │ └── Localizable.strings │ ├── zh-Hant.lproj │ │ ├── InfoPlist.strings │ │ └── Localizable.strings │ ├── ja.lproj │ │ ├── InfoPlist.strings │ │ └── Localizable.strings │ ├── ko.lproj │ │ ├── InfoPlist.strings │ │ └── Localizable.strings │ ├── en.lproj │ │ ├── InfoPlist.strings │ │ └── Localizable.strings │ ├── es.lproj │ │ ├── InfoPlist.strings │ │ └── Localizable.strings │ ├── de.lproj │ │ ├── InfoPlist.strings │ │ └── Localizable.strings │ ├── fr.lproj │ │ ├── InfoPlist.strings │ │ └── Localizable.strings │ └── it.lproj │ │ ├── InfoPlist.strings │ │ └── Localizable.strings ├── Wrapper │ ├── wrapper.h │ └── wrapper.mm ├── Info.plist ├── AppDelegate.swift ├── Base.lproj │ └── LaunchScreen.storyboard ├── sudokuCalculation.swift ├── SceneDelegate.swift ├── StoryBoard │ ├── photoSudoku.storyboard │ ├── pickerSudoku.storyboard │ ├── Base.lproj │ │ └── Main.storyboard │ └── importSudoku.storyboard └── ViewController │ ├── ViewController.swift │ ├── pickerSudokuViewController.swift │ ├── importSudokuViewController.swift │ └── photoSudokuViewController.swift ├── Framework └── opencv2.framework.zip ├── Sudoku.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── leejuhwa.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── License ├── .gitignore ├── README.md └── .gitattributes /Sudoku/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Framework/opencv2.framework.zip: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:6e6c099fcb15eaabf3d6974b9951d741914263d1f0390dc52a2681ff8cdf9971 3 | size 79277386 4 | -------------------------------------------------------------------------------- /Sudoku/Sudoku-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #include "wrapper.h" 6 | -------------------------------------------------------------------------------- /Sudoku/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f2b7991fee9de3aab35454b9af57ffdbeb7c93de5a8254a0f77bfb5f49d7abf7 3 | size 300585 4 | -------------------------------------------------------------------------------- /Sudoku/Assets.xcassets/AppIcon.appiconset/120-1.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:67665fa30a5e920014c4c42fce6e6aec9f74236e53d09e071a92109aa2a3db70 3 | size 10071 4 | -------------------------------------------------------------------------------- /Sudoku/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:67665fa30a5e920014c4c42fce6e6aec9f74236e53d09e071a92109aa2a3db70 3 | size 10071 4 | -------------------------------------------------------------------------------- /Sudoku/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:7d392311a75e370567b9b23ba8110e6743fdbaf49e495958e2307f19fe869115 3 | size 18570 4 | -------------------------------------------------------------------------------- /Sudoku/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:8fe775375f53d48f80bffdec6fedb4fe8d0e3d439f1271da38afebb2e908800f 3 | size 2592 4 | -------------------------------------------------------------------------------- /Sudoku/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:394d3a1d05cae5334dbd80810cf9e9c125c15f56c427223e21a16efa27753d9e 3 | size 3926 4 | -------------------------------------------------------------------------------- /Sudoku/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:495f54cdac5ac92ecdc52e4ddad6911ec2cba174b5e5248aa311a19d10b21343 3 | size 4065 4 | -------------------------------------------------------------------------------- /Sudoku/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:73bb7d8a8f7201d02f81c8e6d20c3ccd27edfe1f0b6808e181cd5e45f365c0cc 3 | size 6099 4 | -------------------------------------------------------------------------------- /Sudoku/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:dc51514f2fa7de74f7f00ed9683201a9283687f2c72c507d685e715120124441 3 | size 6463 4 | -------------------------------------------------------------------------------- /Sudoku/DL/model_64.mlpackage/Data/com.apple.CoreML/model_64.mlmodel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juhwa-Lee1023/SolDoKu/HEAD/Sudoku/DL/model_64.mlpackage/Data/com.apple.CoreML/model_64.mlmodel -------------------------------------------------------------------------------- /Sudoku/DL/model_64.mlpackage/Data/com.apple.CoreML/Metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "MLModelVersionStringKey" : "--", 3 | "MLModelDescriptionKey" : "--", 4 | "MLModelAuthorKey" : "--", 5 | "MLModelLicenseKey" : "--" 6 | } -------------------------------------------------------------------------------- /Sudoku.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sudoku/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sudoku/Extensions/String+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+.swift 3 | // Sudoku 4 | // 5 | // Created by 이주화 on 2022/10/09. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | var localized: String { 12 | return NSLocalizedString(self, comment: "") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sudoku/Localizable/zh-Hans.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | Sudoku 4 | 5 | Created by 이주화 on 2022/10/10. 6 | 7 | */ 8 | 9 | "NSCameraUsageDescription" = "使用该应用程序需要摄像头许可。"; 10 | "NSPhotoLibraryUsageDescription" = "申请必须获得图书馆的许可。"; 11 | "CFBundleDisplayName" = "解决数独"; 12 | -------------------------------------------------------------------------------- /Sudoku/Localizable/zh-Hant.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | Sudoku 4 | 5 | Created by 이주화 on 2022/10/10. 6 | 7 | */ 8 | 9 | "NSCameraUsageDescription" = "使用該應用程序需要攝像頭許可。"; 10 | "NSPhotoLibraryUsageDescription" = "申請必須獲得圖書館的許可。"; 11 | "CFBundleDisplayName" = "解決數獨"; 12 | -------------------------------------------------------------------------------- /Sudoku/Localizable/ja.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | Sudoku 4 | 5 | Created by 이주화 on 2022/10/10. 6 | 7 | */ 8 | 9 | "NSCameraUsageDescription" = "アプリを利用するにはカメラの許可が必要です。"; 10 | "NSPhotoLibraryUsageDescription" = "アプリケーションを使用するには、ライブラリの許可が必要です。"; 11 | "CFBundleDisplayName" = "ソルナン"; 12 | -------------------------------------------------------------------------------- /Sudoku/DL/model_64.mlpackage/Data/com.apple.CoreML/FeatureDescriptions.json: -------------------------------------------------------------------------------- 1 | { 2 | "Outputs" : { 3 | "y" : { 4 | "MLFeatureShortDescription" : "--" 5 | } 6 | }, 7 | "Inputs" : { 8 | "x" : { 9 | "MLFeatureShortDescription" : "--" 10 | } 11 | }, 12 | "TrainingInputs" : { 13 | 14 | } 15 | } -------------------------------------------------------------------------------- /Sudoku/Localizable/ko.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | Sudoku 4 | 5 | Created by 이주화 on 2022/10/10. 6 | 7 | */ 8 | 9 | "NSCameraUsageDescription" = "스도쿠 촬영을 위해서는 카메라 권한이 필요합니다."; 10 | "NSPhotoLibraryUsageDescription" = "스도쿠 사진을 불러오기 위해서는 앨범 권한이 필요합니다."; 11 | "CFBundleDisplayName" = "SolDoKu"; 12 | -------------------------------------------------------------------------------- /Sudoku.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sudoku/Assets.xcassets/sudoku.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "image 3.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sudoku/Assets.xcassets/sudokuImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "sudoku.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sudoku/Localizable/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | Sudoku 4 | 5 | Created by 이주화 on 2022/10/10. 6 | 7 | */ 8 | 9 | "NSCameraUsageDescription" = "Camera permission is required for Sudoku Shooting"; 10 | "NSPhotoLibraryUsageDescription" = "Library permission is required to retrieve Sudoku Photos."; 11 | "CFBundleDisplayName" = "SolDoKu"; 12 | -------------------------------------------------------------------------------- /Sudoku/Localizable/es.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | Sudoku 4 | 5 | Created by 이주화 on 2022/10/10. 6 | 7 | */ 8 | 9 | "NSCameraUsageDescription" = "Se requiere permiso de cámara para usar la solicitu."; 10 | "NSPhotoLibraryUsageDescription" = "Se requiere permiso de la biblioteca para usar la solicitud."; 11 | "CFBundleDisplayName" = "SolDoKu"; 12 | -------------------------------------------------------------------------------- /Sudoku/Localizable/de.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | Sudoku 4 | 5 | Created by 이주화 on 2022/10/10. 6 | 7 | */ 8 | 9 | "NSCameraUsageDescription" = "Die Genehmigung für die Nutzung der Anwendung ist erforderlich."; 10 | "NSPhotoLibraryUsageDescription" = "Die Nutzung der Anwendung bedarf der Genehmigung der Bibliothek."; 11 | "CFBundleDisplayName" = "SolDoKu"; 12 | -------------------------------------------------------------------------------- /Sudoku/Localizable/fr.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | Sudoku 4 | 5 | Created by 이주화 on 2022/10/10. 6 | 7 | */ 8 | 9 | "NSCameraUsageDescription" = "Une autorisation d'utilisation de l'application est requise."; 10 | "NSPhotoLibraryUsageDescription" = "L'autorisation de la bibliothèque est requise pour utiliser l'application."; 11 | "CFBundleDisplayName" = "SolDoKu"; 12 | -------------------------------------------------------------------------------- /Sudoku/Localizable/it.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | Sudoku 4 | 5 | Created by 이주화 on 2022/10/10. 6 | 7 | */ 8 | 9 | "NSCameraUsageDescription" = "Per l'utilizzo della domanda è necessaria l'autorizzazione della camera."; 10 | "NSPhotoLibraryUsageDescription" = "Per l'uso della domanda è necessaria l'autorizzazione della biblioteca."; 11 | "CFBundleDisplayName" = "SolDoKu"; 12 | -------------------------------------------------------------------------------- /Sudoku/Wrapper/wrapper.h: -------------------------------------------------------------------------------- 1 | // 2 | // wrapper.h 3 | // Sudoku 4 | // 5 | // Created by 이주화 on 2022/09/12. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | @interface wrapper : NSObject 12 | 13 | + (NSMutableArray *) detectRectangle: (UIImage *)image; 14 | + (NSMutableArray *) sliceImages: (UIImage *)image imageSize: (int)imageSize cutOffset: (int)cutOffset; 15 | + (NSMutableArray *) getNumImage: (UIImage *)sourceImage imageSize: (int)imageSize; 16 | + (NSArray *) detectRect: (UIImage *)image; 17 | 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /Sudoku/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Sudoku.xcodeproj/xcuserdata/leejuhwa.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SnapKitPlayground (Playground) 1.xcscheme 8 | 9 | isShown 10 | 11 | orderHint 12 | 2 13 | 14 | SnapKitPlayground (Playground) 2.xcscheme 15 | 16 | isShown 17 | 18 | orderHint 19 | 3 20 | 21 | SnapKitPlayground (Playground).xcscheme 22 | 23 | isShown 24 | 25 | orderHint 26 | 0 27 | 28 | Sudoku.xcscheme_^#shared#^_ 29 | 30 | orderHint 31 | 0 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Sudoku/DL/model_64.mlpackage/Manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileFormatVersion": "1.0.0", 3 | "itemInfoEntries": { 4 | "A0D07A9F-07A1-4AF2-9F1D-3B82D3EDA58A": { 5 | "author": "com.apple.CoreML", 6 | "description": "CoreML Model Specification", 7 | "name": "model_64.mlmodel", 8 | "path": "com.apple.CoreML/model_64.mlmodel" 9 | }, 10 | "A79B36BC-60BA-4ABC-B09E-5399A40AD923": { 11 | "author": "com.apple.CoreML", 12 | "description": "External Metadata Overlay", 13 | "name": "Metadata.json", 14 | "path": "com.apple.CoreML/Metadata.json" 15 | }, 16 | "B1A02D5A-02FA-42B3-8546-0EA27FD65B58": { 17 | "author": "com.apple.CoreML", 18 | "description": "External FeatureDescription Overlay", 19 | "name": "FeatureDescriptions.json", 20 | "path": "com.apple.CoreML/FeatureDescriptions.json" 21 | } 22 | }, 23 | "rootModelIdentifier": "A0D07A9F-07A1-4AF2-9F1D-3B82D3EDA58A" 24 | } 25 | -------------------------------------------------------------------------------- /Sudoku/Extensions/CALayer+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CALayer.swift 3 | // Sudoku 4 | // 5 | // Created by 이주화 on 2022/09/29. 6 | // 7 | import UIKit 8 | 9 | extension CALayer { 10 | func addBorder(_ arrEdge: [UIRectEdge], color: UIColor, width: CGFloat) { 11 | for edge in arrEdge { 12 | let border = CALayer() 13 | switch edge { 14 | case UIRectEdge.top: 15 | border.frame = CGRect.init(x: 0, y: 0, width: frame.width, height: width) 16 | case UIRectEdge.bottom: 17 | border.frame = CGRect.init(x: 0, y: frame.height - width, width: frame.width, height: width) 18 | case UIRectEdge.left: 19 | border.frame = CGRect.init(x: 0, y: 0, width: width, height: frame.height) 20 | case UIRectEdge.right: 21 | border.frame = CGRect.init(x: frame.width - width, y: 0, width: width, height: frame.height) 22 | default: break 23 | } 24 | border.backgroundColor = color.cgColor 25 | self.addSublayer(border) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Juhwa Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/xcode,macos 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=xcode,macos 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### macOS Patch ### 35 | # iCloud generated files 36 | *.icloud 37 | 38 | ### Xcode ### 39 | ## User settings 40 | xcuserdata/ 41 | 42 | ## Xcode 8 and earlier 43 | *.xcscmblueprint 44 | *.xccheckout 45 | 46 | ### Xcode Patch ### 47 | *.xcodeproj/* 48 | !*.xcodeproj/project.pbxproj 49 | !*.xcodeproj/xcshareddata/ 50 | !*.xcworkspace/contents.xcworkspacedata 51 | /*.gcno 52 | **/xcshareddata/WorkspaceSettings.xcsettings 53 | 54 | # End of https://www.toptal.com/developers/gitignore/api/xcode,macos 55 | -------------------------------------------------------------------------------- /Sudoku/Localizable/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "SolDoKu" = "解决数独"; 2 | "Take a Picture" = "拍照"; 3 | "Import from Album" = "从相册导入"; 4 | "Direct Input" = "直接输入"; 5 | "Shoot Again" = "再拍"; 6 | "Shooting Sudoku" = "解决数独"; 7 | "Please look where Sudoku is located" = "请看数独在哪里"; 8 | "Currently solving Sudoku" = "目前正在解决数独"; 9 | "Really want to Solve?" = "真的要解决吗?"; 10 | "Sudoku Solve requires more than 17 numbers." = "数独求解需要超过 17 个数字。"; 11 | "Yes" = "是的"; 12 | "No" = "不"; 13 | "Fail." = "失败。"; 14 | "Take a Picture Again." = "再拍一张。"; 15 | "If didn't allow the camera permission, \r\n Would like to go to the Setting Screen?" = "如果不允许相机权限,\r\n 是否要进入设置屏幕?"; 16 | "Setting" = "环境"; 17 | "Cancel" = "取消"; 18 | "Confirm" = "确认"; 19 | "Clean" = "干净的"; 20 | "Delete" = "删除"; 21 | "Solve" = "解决"; 22 | "Cannot solve Sudoku." = "无法解数独。"; 23 | "Do you want to re-enter Sudoku?" = "你想重新进入数独吗?"; 24 | "Clean Sudoku." = "干净的数独。"; 25 | "Sudoku has not Entered." = "数独没有进入。"; 26 | "Please enter Sudoku." = "请输入数独。"; 27 | "Select" = "选择"; 28 | "Album" = "专辑"; 29 | "Camera" = "相机"; 30 | "Picture hasn't been Uploaded." = "图片还没上传。"; 31 | "Want to Upload a Picture?" = "要上传图片吗?"; 32 | "Upload from Album" = "从相册上传"; 33 | "Solving Sudoku" = "解决数独"; 34 | "Really want to Solve?" = "真的要解决吗?"; 35 | "Upload another Picture?" = "上传另一张照片?"; 36 | "Soldoku is not allowed access to Album. \r\n Do you want to go to the Setting Screen?" = "Soldoku 不允许访问专辑。\r\n 你想进入设置屏幕吗?"; 37 | -------------------------------------------------------------------------------- /Sudoku/Localizable/zh-Hant.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "SolDoKu" = "解決數獨"; 2 | "Take a Picture" = "拍照"; 3 | "Import from Album" = "從相冊導入"; 4 | "Direct Input" = "直接輸入"; 5 | "Shoot Again" = "再拍"; 6 | "Shooting Sudoku" = "解決數獨"; 7 | "Please look where Sudoku is located" = "請看數獨在哪裡"; 8 | "Currently solving Sudoku" = "目前正在解決數獨"; 9 | "Really want to Solve?" = "真的要解決嗎?"; 10 | "Sudoku Solve requires more than 17 numbers." = "數獨求解需要超過 17 個數字。"; 11 | "Yes" = "是的"; 12 | "No" = "不"; 13 | "Fail." = "失敗。"; 14 | "Take a Picture Again." = "再拍一張。"; 15 | "If didn't allow the camera permission, \r\n Would like to go to the Setting Screen?" = "如果不允許相機權限,\r\n 是否要進入設置屏幕?"; 16 | "Setting" = "環境"; 17 | "Cancel" = "取消"; 18 | "Confirm" = "確認"; 19 | "Clean" = "乾淨的"; 20 | "Delete" = "刪除"; 21 | "Solve" = "解決"; 22 | "Cannot solve Sudoku." = "無法解數獨。"; 23 | "Do you want to re-enter Sudoku?" = "你想重新進入數獨嗎??"; 24 | "Clean Sudoku." = "乾淨的數獨。"; 25 | "Sudoku has not Entered." = "數獨沒有進入。"; 26 | "Please enter Sudoku." = "請輸入數獨。"; 27 | "Select" = "選擇"; 28 | "Album" = "專輯"; 29 | "Camera" = "相機"; 30 | "Picture hasn't been Uploaded." = "圖片還沒上傳。"; 31 | "Want to Upload a Picture?" = "要上傳圖片嗎?"; 32 | "Upload from Album" = "從相冊上傳"; 33 | "Solving Sudoku" = "解決數獨"; 34 | "Really want to Solve?" = "真的要解決嗎?"; 35 | "Upload another Picture?" = "上傳另一張照片?"; 36 | "Soldoku is not allowed access to Album. \r\n Do you want to go to the Setting Screen?" = "解決數獨 不允許訪問專輯。\r\n 你想進入設置屏幕嗎?"; 37 | -------------------------------------------------------------------------------- /Sudoku/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "58.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "87.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "80.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "120.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "120-1.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "180.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "1024.png", 53 | "idiom" : "ios-marketing", 54 | "scale" : "1x", 55 | "size" : "1024x1024" 56 | } 57 | ], 58 | "info" : { 59 | "author" : "xcode", 60 | "version" : 1 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sudoku/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Sudoku 4 | // 5 | // Created by 이주화 on 2022/09/06. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Sudoku/Extensions/UIColor+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor.swift 3 | // Sudoku 4 | // 5 | // Created by 이주화 on 2022/09/29. 6 | // 7 | import UIKit 8 | 9 | enum Colors { 10 | case sudokuLightPurple 11 | case sudokuDeepPurple 12 | case sudokuPurple 13 | case sudokuButton 14 | case sudokuRed 15 | case sudokuEmpty 16 | case sudokuDeepButton 17 | case sudokuLightRed 18 | } 19 | 20 | extension UIColor { 21 | static func sudokuColor(_ color: Colors) -> UIColor { 22 | switch color { 23 | case .sudokuDeepButton: 24 | return UIColor(red: 85/255, green: 85/255, blue: 103/255, alpha: 255/255) 25 | case .sudokuLightPurple: 26 | return UIColor(red: 107/255, green: 28/255, blue: 255/255, alpha: 20/100) 27 | case .sudokuDeepPurple: 28 | return UIColor(red: 107/255, green: 28/255, blue: 255/255, alpha: 100/100) 29 | case .sudokuPurple: 30 | return UIColor(red: 107/255, green: 28/255, blue: 255/255, alpha: 60/100) 31 | case .sudokuButton: 32 | return UIColor(red: 217/255, green: 217/255, blue: 217/255, alpha: 100/100) 33 | case .sudokuRed: 34 | return UIColor(red: 210/255, green: 31/255, blue: 0/255, alpha: 100/100) 35 | case .sudokuEmpty: 36 | return UIColor(red: 210/255, green: 31/255, blue: 0/255, alpha: 0/100) 37 | case .sudokuLightRed: 38 | return UIColor(red: 255/255, green: 28/255, blue: 107/255, alpha: 60/100) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sudoku/Localizable/ja.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "SolDoKu" = "ソルナン"; 2 | "Take a Picture" = "写真を撮る"; 3 | "Import from Album" = "アルバムからインポート"; 4 | "Direct Input" = "直接入力"; 5 | "Shoot Again" = "もう一度撃て"; 6 | "Shooting Sudoku" = "シューティングナンプレ"; 7 | "Please look where Sudoku is located" = "ナンプレがどこにあるか見てください"; 8 | "Currently solving Sudoku" = "現在ナンプレを解いています"; 9 | "Really want to Solve?" = "本当に解決したいですか?"; 10 | "Sudoku Solve requires more than 17 numbers." = "ナンプレ解決には 17 個以上の数字が必要です。"; 11 | "Yes" = "はい"; 12 | "No" = "いいえ"; 13 | "Fail." = "失敗。"; 14 | "Take a Picture Again." = "また写真撮って"; 15 | "If didn't allow the camera permission, \r\n Would like to go to the Setting Screen?" = "カメラの許可を許可していない場合は、\r\n設定画面に移動しますか?"; 16 | "Setting" = "設定"; 17 | "Cancel" = "キャンセル"; 18 | "Confirm" = "確認"; 19 | "Clean" = "掃除"; 20 | "Delete" = "消去"; 21 | "Solve" = "解決する"; 22 | "Cannot solve Sudoku." = "ナンプレが解けない"; 23 | "Do you want to re-enter Sudoku?" = "ナンプレを再入力しますか?"; 24 | "Clean Sudoku." = "きれいなナンプレ"; 25 | "Sudoku has not Entered." = "ナンプレは入っていません。"; 26 | "Please enter Sudoku." = "ナンプレを入力してください。"; 27 | "Select" = "選択する"; 28 | "Album" = "アルバム"; 29 | "Camera" = "カメラ"; 30 | "Picture hasn't been Uploaded." = "画像がアップロードされていません。"; 31 | "Want to Upload a Picture?" = "写真をアップロードしますか?"; 32 | "Upload from Album" = "アルバムからアップロード"; 33 | "Solving Sudoku" = "ナンプレを解く"; 34 | "Really want to Solve?" = "本当に解決したいですか?"; 35 | "Upload another Picture?" = "別の写真をアップロードしますか?"; 36 | "Soldoku is not allowed access to Album. \r\n Do you want to go to the Setting Screen?" = "ソルナンはアルバムへのアクセスを許可されていません。\r\n設定画面に移動しますか?"; 37 | -------------------------------------------------------------------------------- /Sudoku/Localizable/ko.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "SolDoKu" = "SolDoku"; 2 | "Take a Picture" = "카메라로 촬영"; 3 | "Import from Album" = "앨범에서 업로드하기"; 4 | "Direct Input" = "직접 입력하기"; 5 | "Shoot Again" = "다시 촬영"; 6 | "Shooting Sudoku" = "스도쿠 풀기"; 7 | "Please look where Sudoku is located" = "스도쿠가 위치한 곳을 향해주세요"; 8 | "Currently solving Sudoku" = "현재 스도쿠를 풀이 중입니다"; 9 | "Really want to Solve?" = "진짜 풀이를 하시겠어요?"; 10 | "Sudoku Solve requires more than 17 numbers." = "스도쿠 풀이를 하기위해서는 17개 이상의 숫자가 필요합니다."; 11 | "Yes" = "네"; 12 | "No" = "아니요"; 13 | "Fail." = "실패."; 14 | "Take a Picture Again." = "다시 촬영해주세요."; 15 | "If didn't allow the camera permission, \r\n Would like to go to the Setting Screen?" = "카메라 사용허가를 하지 않으셨으면, \r\n 설정 페이지로 가시겠어요?"; 16 | "Setting" = "설정"; 17 | "Cancel" = "취소"; 18 | "Confirm" = "확인"; 19 | "Clean" = "비우기"; 20 | "Delete" = "삭제"; 21 | "Solve" = "풀이"; 22 | "Cannot solve Sudoku." = "스도쿠를 풀이할 수 없습니다."; 23 | "Do you want to re-enter Sudoku?" = "스도쿠를 다시 입력하시겠어요?"; 24 | "Clean Sudoku." = "스도쿠 전부 비우기."; 25 | "Sudoku has not Entered." = "스도쿠가 입력되지 않았어요."; 26 | "Please enter Sudoku." = "스도쿠를 입력해주세요."; 27 | "Select" = "선택해주세요"; 28 | "Album" = "앨범에서 불러오기"; 29 | "Camera" = "카메라로 촬영하기"; 30 | "Picture hasn't been Uploaded." = "사진이 업로드 되지 않았습니다."; 31 | "Want to Upload a Picture?" = "사진을 업로드하시겠어요?"; 32 | "Upload from Album" = "앨범에서 불러오기"; 33 | "Solving Sudoku" = "스도쿠 풀이"; 34 | "Really want to Solve?" = "정말 풀이를 하시겠어요?"; 35 | "Upload another Picture?" = "다른 사진을 업로드 하시겠어요?"; 36 | "Soldoku is not allowed access to Album. \r\n Do you want to go to the Setting Screen?" = "SolDoKu가 앨범 접근 허가가 되지 않았어요. \r\n 설정 페이지로 이동하시겠어요?"; 37 | -------------------------------------------------------------------------------- /Sudoku/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 | -------------------------------------------------------------------------------- /Sudoku/Localizable/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "SolDoKu" = "SolDoKu"; 2 | "Take a Picture" = "Take a Picture"; 3 | "Import from Album" = "Import from Album"; 4 | "Direct Input" = "Direct Input"; 5 | "Shoot Again" = "Shoot Again"; 6 | "Shooting Sudoku" = "Shooting Sudoku"; 7 | "Please look where Sudoku is located" = "Please look where Sudoku is located"; 8 | "Currently solving Sudoku" = "Currently solving Sudoku"; 9 | "Really want to Solve?" = "Really want to Solve?"; 10 | "Sudoku Solve requires more than 17 numbers." = "Sudoku Solve requires more than 17 numbers."; 11 | "Yes" = "Yes"; 12 | "No" = "No"; 13 | "Fail." = "Fail."; 14 | "Take a Picture Again." = "Take a Picture Again."; 15 | "If didn't allow the camera permission, \r\n Would like to go to the Setting Screen?" = "If didn't allow the camera permission, \r\n Would like to go to the Setting Screen?"; 16 | "Setting" = "Setting"; 17 | "Cancel" = "Cancel"; 18 | "Confirm" = "Confirm"; 19 | "Clean" = "Clean"; 20 | "Delete" = "Delete"; 21 | "Solve" = "Solve"; 22 | "Cannot solve Sudoku." = "Cannot solve Sudoku."; 23 | "Do you want to re-enter Sudoku?" = "Do you want to re-enter Sudoku?"; 24 | "Clean Sudoku." = "Clean Sudoku."; 25 | "Sudoku has not Entered." = "Sudoku has not Entered."; 26 | "Please enter Sudoku." = "Please enter Sudoku."; 27 | "Select" = "Select"; 28 | "Album" = "Album"; 29 | "Camera" = "Camera"; 30 | "Picture hasn't been Uploaded." = "Picture hasn't been Uploaded."; 31 | "Want to Upload a Picture?" = "Want to Upload a Picture?"; 32 | "Upload from Album" = "Upload from Album"; 33 | "Solving Sudoku" = "Solving Sudoku"; 34 | "Really want to Solve?" = "Really want to Solve?"; 35 | "Upload another Picture?" = "Upload another Picture?"; 36 | "Soldoku is not allowed access to Album. \r\n Do you want to go to the Setting Screen?" = "Soldoku is not allowed access to Album. \r\n Do you want to go to the Setting Screen?"; 37 | -------------------------------------------------------------------------------- /Sudoku/Localizable/es.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "SolDoKu" = "SolDoKu"; 2 | "Take a Picture" = "Toma una foto"; 3 | "Import from Album" = "Importar desde álbum"; 4 | "Direct Input" = "Entrada directa"; 5 | "Shoot Again" = "Disparar de nuevo"; 6 | "Shooting Sudoku" = "Disparar Sudoku"; 7 | "Please look where Sudoku is located" = "Mira dónde se encuentra Sudoku"; 8 | "Currently solving Sudoku" = "Actualmente resolviendo Sudoku"; 9 | "Really want to Solve?" = "¿Realmente quieres Resolver?"; 10 | "Sudoku Solve requires more than 17 numbers." = "Sudoku Solve requiere más de 17 números"; 11 | "Yes" = "Sí"; 12 | "No" = "No"; 13 | "Fail." = "Fallar."; 14 | "Take a Picture Again." = "Toma una foto de nuevo"; 15 | "If didn't allow the camera permission, \r\n Would like to go to the Setting Screen?" = "Si no permitió el permiso de la cámara, \r\n ¿Le gustaría ir a la pantalla de configuración?"; 16 | "Setting" = "Ajuste"; 17 | "Cancel" = "Cancelar"; 18 | "Confirm" = "Confirmar"; 19 | "Clean" = "Limpio"; 20 | "Delete" = "Borrar"; 21 | "Solve" = "Resolver"; 22 | "Cannot solve Sudoku." = "No se puede resolver Sudoku"; 23 | "Do you want to re-enter Sudoku?" = "¿Quieres volver a entrar en Sudoku?"; 24 | "Clean Sudoku." = "Sudoku limpio"; 25 | "Sudoku has not Entered." = "Sudoku no ha entrado"; 26 | "Please enter Sudoku." = "Por favor ingrese Sudoku"; 27 | "Select" = "Seleccione"; 28 | "Album" = "Álbum"; 29 | "Camera" = "Cámara"; 30 | "Picture hasn't been Uploaded." = "La imagen no ha sido cargada"; 31 | "Want to Upload a Picture?" = "¿Quieres subir una imagen?"; 32 | "Upload from Album" = "Subir desde álbum"; 33 | "Solving Sudoku" = "Resolviendo Sudoku"; 34 | "Really want to Solve?" = "¿Realmente quieres Resolver?"; 35 | "Upload another Picture?" = "¿Subir otra imagen?"; 36 | "Soldoku is not allowed access to Album. \r\n Do you want to go to the Setting Screen?" = "Soldoku no tiene permitido el acceso al álbum. \r\n ¿Quieres ir a la pantalla de configuración?"; 37 | -------------------------------------------------------------------------------- /Sudoku/Localizable/de.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "SolDoKu" = "SolDoKu"; 2 | "Take a Picture" = "Machen Sie ein Foto"; 3 | "Import from Album" = "Aus Album importieren"; 4 | "Direct Input" = "Direkte Eingabe"; 5 | "Shoot Again" = "Noch einmal schießen"; 6 | "Shooting Sudoku" = "Sudoku schießen"; 7 | "Please look where Sudoku is located" = "Bitte schauen Sie, wo sich Sudoku befindet"; 8 | "Currently solving Sudoku" = "Derzeit Sudoku lösen"; 9 | "Really want to Solve?" = "Willst du wirklich lösen?"; 10 | "Sudoku Solve requires more than 17 numbers." = "Sudoku Solve erfordert mehr als 17 Zahlen."; 11 | "Yes" = "Ja"; 12 | "No" = "Nein"; 13 | "Fail." = "Scheitern."; 14 | "Take a Picture Again." = "Mach noch mal ein Foto."; 15 | "If didn't allow the camera permission, \r\n Would like to go to the Setting Screen?" = "Falls die Kameraberechtigung nicht erteilt wurde, \r\n möchten Sie zum Einstellungsbildschirm gehen?"; 16 | "Setting" = "Einstellung"; 17 | "Cancel" = "Absagen"; 18 | "Confirm" = "Bestätigen"; 19 | "Clean" = "Sauber"; 20 | "Delete" = "Löschen"; 21 | "Solve" = "Lösen"; 22 | "Cannot solve Sudoku." = "Kann Sudoku nicht lösen."; 23 | "Do you want to re-enter Sudoku?" = "Möchten Sie Sudoku erneut eingeben?"; 24 | "Clean Sudoku." = "Sauberes Sudoku."; 25 | "Sudoku has not Entered." = "Sudoku wurde nicht eingegeben."; 26 | "Please enter Sudoku." = "Bitte geben Sie Sudoku ein."; 27 | "Select" = "Auswählen"; 28 | "Album" = "Album"; 29 | "Camera" = "Kamera"; 30 | "Picture hasn't been Uploaded." = "Bild wurde nicht hochgeladen."; 31 | "Want to Upload a Picture?" = "Möchten Sie ein Bild hochladen?"; 32 | "Upload from Album" = "Aus Album hochladen"; 33 | "Solving Sudoku" = "Sudoku lösen"; 34 | "Really want to Solve?" = "Willst du wirklich lösen?"; 35 | "Upload another Picture?" = "Weiteres Bild hochladen?"; 36 | "Soldoku is not allowed access to Album. \r\n Do you want to go to the Setting Screen?" = "Soldoku hat keinen Zugriff auf das Album. \r\n Möchten Sie zum Einstellungsbildschirm gehen?"; 37 | -------------------------------------------------------------------------------- /Sudoku/Localizable/it.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "SolDoKu" = "SolDoKu"; 2 | "Take a Picture" = "Fai una foto"; 3 | "Import from Album" = "Importa da album"; 4 | "Direct Input" = "Input diretto"; 5 | "Shoot Again" = "Spara ancora"; 6 | "Shooting Sudoku" = "Sudoku di tiro"; 7 | "Please look where Sudoku is located" = "Per favore, guarda dove si trova Sudoku"; 8 | "Currently solving Sudoku" = "Attualmente risolvendo Sudoku"; 9 | "Really want to Solve?" = "Vuoi davvero risolvere?"; 10 | "Sudoku Solve requires more than 17 numbers." = "Sudoku Solve richiede più di 17 numeri."; 11 | "Yes" = "Sì"; 12 | "No" = "No"; 13 | "Fail." = "Fallire."; 14 | "Take a Picture Again." = "Scatta di nuovo una foto"; 15 | "If didn't allow the camera permission, \r\n Would like to go to the Setting Screen?" = "Se non è stata consentita l'autorizzazione della fotocamera, \r\n Vorresti andare alla schermata delle impostazioni?"; 16 | "Setting" = "Ambientazione"; 17 | "Cancel" = "Annulla"; 18 | "Confirm" = "Confermare"; 19 | "Clean" = "Pulire"; 20 | "Delete" = "Elimina"; 21 | "Solve" = "Risolvere"; 22 | "Cannot solve Sudoku." = "Impossibile risolvere Sudoku."; 23 | "Do you want to re-enter Sudoku?" = "Vuoi rientrare nel Sudoku?"; 24 | "Clean Sudoku." = "Pulisci Sudoku"; 25 | "Sudoku has not Entered." = "Il sudoku non è entrato."; 26 | "Please enter Sudoku." = "Per favore, inserisci Sudoku."; 27 | "Select" = "Selezionare"; 28 | "Album" = "Album"; 29 | "Camera" = "Telecamera"; 30 | "Picture hasn't been Uploaded." = "L'immagine non è stata caricata."; 31 | "Want to Upload a Picture?" = "Vuoi caricare un'immagine?"; 32 | "Upload from Album" = "Carica dall'album"; 33 | "Solving Sudoku" = "Risolvere il Sudoku"; 34 | "Really want to Solve?" = "Vuoi davvero risolvere?"; 35 | "Upload another Picture?" = "Carica un'altra immagine?"; 36 | "Soldoku is not allowed access to Album. \r\n Do you want to go to the Setting Screen?" = "A Soldoku non è consentito l'accesso all'Album. \r\n Vuoi andare alla schermata delle impostazioni?"; 37 | -------------------------------------------------------------------------------- /Sudoku/Localizable/fr.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "SolDoKu" = "SolDoKu"; 2 | "Take a Picture" = "Prendre une photo"; 3 | "Import from Album" = "Importer depuis l'album"; 4 | "Direct Input" = "Saisie directe"; 5 | "Shoot Again" = "Tirez à nouveau"; 6 | "Shooting Sudoku" = "Sudoku de tir"; 7 | "Please look where Sudoku is located" = "Veuillez regarder où se trouve Sudoku"; 8 | "Currently solving Sudoku" = "Résolution actuelle de Sudoku"; 9 | "Really want to Solve?" = "Voulez-vous vraiment résoudre ?"; 10 | "Sudoku Solve requires more than 17 numbers." = "Sudoku Solve nécessite plus de 17 numéros."; 11 | "Yes" = "Oui"; 12 | "No" = "Non"; 13 | "Fail." = "Échouer."; 14 | "Take a Picture Again." = "Prenez une photo à nouveau."; 15 | "If didn't allow the camera permission, \r\n Would like to go to the Setting Screen?" = "Si vous n'avez pas autorisé l'autorisation de la caméra, \r\n Souhaitez-vous accéder à l'écran de configuration ?"; 16 | "Setting" = "Paramètre"; 17 | "Cancel" = "Annuler"; 18 | "Confirm" = "Confirmer"; 19 | "Clean" = "Nettoyer"; 20 | "Delete" = "Effacer"; 21 | "Solve" = "Résoudre"; 22 | "Cannot solve Sudoku." = "Impossible de résoudre le Sudoku."; 23 | "Do you want to re-enter Sudoku?" = "Voulez-vous réintégrer le Sudoku?"; 24 | "Clean Sudoku." = "Sudoku propre."; 25 | "Sudoku has not Entered." = "Sudoku n'est pas entré."; 26 | "Please enter Sudoku." = "Veuillez entrer Sudoku."; 27 | "Select" = "Sélectionner"; 28 | "Album" = "Album"; 29 | "Camera" = "Caméra"; 30 | "Picture hasn't been Uploaded." = "La photo n'a pas été téléchargée."; 31 | "Want to Upload a Picture?" = "Voulez-vous télécharger une image ?"; 32 | "Upload from Album" = "Télécharger depuis l'album"; 33 | "Solving Sudoku" = "Résoudre Sudoku"; 34 | "Really want to Solve?" = "Voulez-vous vraiment résoudre ?"; 35 | "Upload another Picture?" = "Télécharger une autre photo ?"; 36 | "Soldoku is not allowed access to Album. \r\n Do you want to go to the Setting Screen?" = "Soldoku n'est pas autorisé à accéder à l'album. \r\n Voulez-vous accéder à l'écran de configuration ?"; 37 | -------------------------------------------------------------------------------- /Sudoku/sudokuCalculation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // sudokuCalculation.swift 3 | // Sudoku 4 | // 5 | // Created by 이주화 on 2022/09/11. 6 | // 7 | 8 | import Foundation 9 | 10 | // 해당하는 숫자가 들어가도 되는지 검증 11 | func isVerify(_ number: Int, _ sudoku: [[Int]], _ row: Int, _ col:Int) -> Bool { 12 | let sectorRow: Int = 3 * Int(row / 3) 13 | let sectorCol: Int = 3 * Int(col / 3) 14 | let row1 = (row + 2) % 3 15 | let row2 = (row + 4) % 3 16 | let col1 = (col + 2) % 3 17 | let col2 = (col + 4) % 3 18 | 19 | // 들어갈 숫자가 row, column에 있는 숫자와 겹치는지 확인 20 | for i in 0..<9 { 21 | if sudoku[i][col] == number { return false } 22 | if sudoku[row][i] == number { return false } 23 | } 24 | 25 | // 숫자가 들어갈 3*3의 공간에 숫자가 겹치는지 확인 26 | if sudoku[row1 + sectorRow][col1 + sectorCol] == number { return false } 27 | if sudoku[row2 + sectorRow][col1 + sectorCol] == number { return false } 28 | if sudoku[row1 + sectorRow][col2 + sectorCol] == number { return false } 29 | if sudoku[row2 + sectorRow][col2 + sectorCol] == number { return false } 30 | 31 | return true 32 | } 33 | 34 | func sudokuCalculation(_ sudoku: inout [[Int]], _ row: Int, _ col: Int, _ check: inout Int) -> Bool { 35 | 36 | if(check >= 1000000) { return false } 37 | 38 | if (row == 9) { return true } 39 | 40 | // 기존에 존재하는 숫자가 있다면 41 | if sudoku[row][col] != 0 { 42 | if (col == 8) { 43 | check += 1 44 | if (sudokuCalculation(&sudoku, row+1, 0, &check) == true) { return true } 45 | } else { 46 | check += 1 47 | if (sudokuCalculation(&sudoku, row, col+1, &check) == true) { return true } 48 | } 49 | return false 50 | } 51 | 52 | // 모든 칸을 채울 때까지 재귀함수 호출 53 | for num in 1..<10 { 54 | if isVerify(num, sudoku, row, col) == true { 55 | sudoku[row][col] = num 56 | if col == 8 { 57 | check += 1 58 | if (sudokuCalculation(&sudoku, row+1, 0, &check) == true) { return true } 59 | } else { 60 | check += 1 61 | if (sudokuCalculation(&sudoku, row, col+1, &check) == true) { return true } 62 | } 63 | // 계산이 불가능하면... 64 | sudoku[row][col] = 0 65 | } 66 | } 67 | 68 | return false 69 | } 70 | -------------------------------------------------------------------------------- /Sudoku/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Sudoku 4 | // 5 | // Created by 이주화 on 2022/09/06. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # SolDoKu 3 | |![Frame 5](https://user-images.githubusercontent.com/63584245/198891933-9802142e-f07a-4cb0-a524-e2966e08ea75.svg)|image| 4 | |:---:|:---:| 5 | 6 | 7 | _**스도쿠 사진을 찍으면 스도쿠를 대신 풀어주는 앱입니다!**_
8 | _**풀기 힘든 문제가 있다면 `솔더쿠`에게 부탁하세요!!**_ 9 | _**지원 언어 : 영어, 한글, 한자 간체, 한자 번체, 일본어, 프랑스어, 스페인어, 독일어, 이탈리아**_ 10 | 11 | 🔗App Store : SolDoKu 12 | 13 | 🔗시연 영상 : Youtube 14 | 15 | 16 | 17 | --- 18 | ### 동작화면 19 | |Case 1. 후면 카메라 실시간 풀이|Case 2. 앨범 사진 기반 풀이|Case 3. 사용자 입력 기반 풀이| 20 | |:---:|:---:|:---:| 21 | |||| 22 | 23 | 24 | --- 25 | ### :sparkles: Skills & Tech Stack 26 | * UIKit 27 | * Objective-C 28 | * AVFoundation 29 | * OpenCV 30 | * TensorFlow 31 | * Coremltools 32 | 33 | ### 🛠 Development Environment 34 | 35 | 스크린샷 2021-11-19 오후 3 52 02 스크린샷 2021-11-19 오후 3 52 02 36 | 37 | 38 | ## 기술적 도전 39 | 40 | > **OpenCV Wrapping** 41 | * UIKit는 swift를 기반으로 코딩되는데 OpenCV는 C,C++로 제작되어 직접 사용은 불가능하므로 Objective c++을 기반으로한 wrapper를 씌워 wrapper가 OpenCV를 호출하고 swift는 Objective c++로 작성된 wrapper를 부르는 방식으로 OpenCV를 사용하였습니다. 42 | 43 | > **TensorFlow로 만든 모델을 Coremltools로 변환하여 사용** 44 | * 애플에서 제공하는 createML로 모델을 만들어 사용하니 정확성이 떨어져 TensorFlow로 만든 모델을 Coremltools로 .mlmodel 로 변환하여 앱에서 사용하였다. 45 | 46 | 47 | ## Trouble Shooting 48 | 49 | > * 카메라가 비추는 것을 UIImageView 위에 올리기 50 | > * Swift로 효율적인 스도쿠 알고리즘 만들기 51 | > * 비디오 프레임이 들어오면 해당되는 프레임을 핸들링하여 UIImageView에 올라기 52 | > * OpenCV로 비디오 프레임에서 사각형 인식하여 인식한 부분만 자르기 53 | > * OpenCV와 coremltools를 이용하여 숫자 인식률 개선 54 | > * 스도쿠를 풀이할 수 없는 사진일 경우 어플이 종료되는 경우 55 | > * 정제된 이미지와 앨범에서 사진을 불러올 때 사진이 90도 회전해있는 경우 56 | > * 인식한 스도쿠 영역과 화면에 나타나는 영역이 다른경우 57 | > * 실시간으로 스도쿠 영역에서 숫자를 인식하다 앱이 종료되는 경우 58 | 59 | ### :lock_with_ink_pen: License 60 | 61 | [MIT](https://choosealicense.com/licenses/mit/) 62 | -------------------------------------------------------------------------------- /Sudoku/Extensions/UIImage+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage.swift 3 | // Sudoku 4 | // 5 | // Created by 이주화 on 2022/09/14. 6 | // 7 | import UIKit 8 | 9 | extension UIImage { 10 | // UIImage에 UIImage를 픽셀버퍼 타입으로 변환시키는 function 추가 11 | func UIImageToPixelBuffer() -> CVPixelBuffer? { 12 | 13 | let width = Int(self.size.width) 14 | let height = Int(self.size.height) 15 | 16 | let attrs = [kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue] as CFDictionary 17 | var pixelBuffer: CVPixelBuffer? 18 | let status = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32ARGB, attrs, &pixelBuffer) 19 | guard status == kCVReturnSuccess else { 20 | return nil 21 | } 22 | 23 | CVPixelBufferLockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0)) 24 | let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer!) 25 | 26 | let rgbColorSpace = CGColorSpaceCreateDeviceRGB() 27 | guard let context = CGContext(data: pixelData, width: width, height: height, bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!), space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue) else { 28 | return nil 29 | } 30 | 31 | context.translateBy(x: 0, y: CGFloat(height)) 32 | context.scaleBy(x: 1.0, y: -1.0) 33 | 34 | UIGraphicsPushContext(context) 35 | self.draw(in: CGRect(x: 0, y: 0, width: width, height: height)) 36 | UIGraphicsPopContext() 37 | CVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0)) 38 | 39 | return pixelBuffer 40 | } 41 | // UIImage에 UIImage의 방향을 확인하여 정방향으로 돌리는 function 추가 42 | func fixOrientation() -> UIImage { 43 | 44 | // 이미지의 방향이 올바를 경우 수정하지 않는다. 45 | if self.imageOrientation == UIImage.Orientation.up { 46 | return self 47 | } 48 | 49 | // 이미지를 변환시키기 위한 함수 선언 50 | var transform: CGAffineTransform = CGAffineTransform.identity 51 | 52 | // 이미지의 상태에 맞게 이미지를 돌린다. 53 | if ( self.imageOrientation == UIImage.Orientation.down || self.imageOrientation == UIImage.Orientation.downMirrored ) { 54 | transform = transform.translatedBy(x: self.size.width, y: self.size.height) 55 | transform = transform.rotated(by: CGFloat(Double.pi)) 56 | } else if ( self.imageOrientation == UIImage.Orientation.left || self.imageOrientation == UIImage.Orientation.leftMirrored ) { 57 | transform = transform.translatedBy(x: self.size.width, y: 0) 58 | transform = transform.rotated(by: CGFloat(Double.pi / 2.0)) 59 | } else if ( self.imageOrientation == UIImage.Orientation.right || self.imageOrientation == UIImage.Orientation.rightMirrored ) { 60 | transform = transform.translatedBy(x: 0, y: self.size.height) 61 | transform = transform.rotated(by: CGFloat(-Double.pi / 2.0)) 62 | } 63 | 64 | if ( self.imageOrientation == UIImage.Orientation.upMirrored || self.imageOrientation == UIImage.Orientation.downMirrored ) { 65 | transform = transform.translatedBy(x: self.size.width, y: 0) 66 | transform = transform.scaledBy(x: -1, y: 1) 67 | } else if ( self.imageOrientation == UIImage.Orientation.leftMirrored || self.imageOrientation == UIImage.Orientation.rightMirrored ) { 68 | transform = transform.translatedBy(x: self.size.height, y: 0) 69 | transform = transform.scaledBy(x: -1, y: 1) 70 | } 71 | 72 | // 이미지 변환용 값 선언 73 | let cgValue: CGContext = CGContext(data: nil, width: Int(self.size.width), height: Int(self.size.height), 74 | bitsPerComponent: self.cgImage!.bitsPerComponent, bytesPerRow: 0, 75 | space: self.cgImage!.colorSpace!, 76 | bitmapInfo: self.cgImage!.bitmapInfo.rawValue)! 77 | 78 | cgValue.concatenate(transform) 79 | 80 | if ( self.imageOrientation == UIImage.Orientation.left || 81 | self.imageOrientation == UIImage.Orientation.leftMirrored || 82 | self.imageOrientation == UIImage.Orientation.right || 83 | self.imageOrientation == UIImage.Orientation.rightMirrored ) { 84 | cgValue.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: self.size.height, height: self.size.width)) 85 | } else { 86 | cgValue.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height)) 87 | } 88 | 89 | return UIImage(cgImage: cgValue.makeImage()!) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj binary merge=union 2 | 3 | # Ensure that text files that any contributor introduces to the repository have their line endings normalized 4 | * text=auto 5 | # Normalize all files with the following extensions 6 | *.ShaderGraph text 7 | *.anim text 8 | *.asmdef text 9 | *.cginc text 10 | *.compute text 11 | *.controller text 12 | *.cs diff=csharp text 13 | *.giparams text 14 | *.hlsl text 15 | *.json text 16 | *.mask text 17 | *.mat text 18 | *.md text 19 | *.md5 text 20 | *.meta text 21 | *.mixer text 22 | *.overrideController text 23 | *.playable text 24 | *.prefab text 25 | *.shader text 26 | *.txt text 27 | # 28 | # Unity LFS 29 | # 30 | *.bytes filter=lfs diff=lfs merge=lfs -text 31 | *.cubemap filter=lfs diff=lfs merge=lfs -text 32 | *.entities filter=lfs diff=lfs merge=lfs -text 33 | *.unitypackage filter=lfs diff=lfs merge=lfs -text 34 | # 35 | # Unity Asset Files 36 | # 37 | # 38 | # 3D models 39 | # 40 | *.3dm filter=lfs diff=lfs merge=lfs -text 41 | *.3ds filter=lfs diff=lfs merge=lfs -text 42 | *.blend filter=lfs diff=lfs merge=lfs -text 43 | *.c4d filter=lfs diff=lfs merge=lfs -text 44 | *.collada filter=lfs diff=lfs merge=lfs -text 45 | *.dae filter=lfs diff=lfs merge=lfs -text 46 | *.dxf filter=lfs diff=lfs merge=lfs -text 47 | *.fbx filter=lfs diff=lfs merge=lfs -text 48 | *.jas filter=lfs diff=lfs merge=lfs -text 49 | *.lws filter=lfs diff=lfs merge=lfs -text 50 | *.lxo filter=lfs diff=lfs merge=lfs -text 51 | *.ma filter=lfs diff=lfs merge=lfs -text 52 | *.max filter=lfs diff=lfs merge=lfs -text 53 | *.mb filter=lfs diff=lfs merge=lfs -text 54 | *.obj filter=lfs diff=lfs merge=lfs -text 55 | *.ply filter=lfs diff=lfs merge=lfs -text 56 | *.skp filter=lfs diff=lfs merge=lfs -text 57 | *.stl filter=lfs diff=lfs merge=lfs -text 58 | *.ztl filter=lfs diff=lfs merge=lfs -text 59 | # 60 | # Audio 61 | # 62 | *.aif filter=lfs diff=lfs merge=lfs -text 63 | *.aiff filter=lfs diff=lfs merge=lfs -text 64 | *.it filter=lfs diff=lfs merge=lfs -text 65 | *.mod filter=lfs diff=lfs merge=lfs -text 66 | *.mp3 filter=lfs diff=lfs merge=lfs -text 67 | *.ogg filter=lfs diff=lfs merge=lfs -text 68 | *.s3m filter=lfs diff=lfs merge=lfs -text 69 | *.wav filter=lfs diff=lfs merge=lfs -text 70 | *.xm filter=lfs diff=lfs merge=lfs -text 71 | # 72 | # Video 73 | # 74 | *.asf filter=lfs diff=lfs merge=lfs -text 75 | *.avi filter=lfs diff=lfs merge=lfs -text 76 | *.flv filter=lfs diff=lfs merge=lfs -text 77 | *.mov filter=lfs diff=lfs merge=lfs -text 78 | *.mp4 filter=lfs diff=lfs merge=lfs -text 79 | *.mpeg filter=lfs diff=lfs merge=lfs -text 80 | *.mpg filter=lfs diff=lfs merge=lfs -text 81 | *.ogv filter=lfs diff=lfs merge=lfs -text 82 | *.wmv filter=lfs diff=lfs merge=lfs -text 83 | # 84 | # Fonts 85 | # 86 | *.otf filter=lfs diff=lfs merge=lfs -text 87 | *.ttf filter=lfs diff=lfs merge=lfs -text 88 | # 89 | # Images 90 | # 91 | *.bmp filter=lfs diff=lfs merge=lfs -text 92 | *.exr filter=lfs diff=lfs merge=lfs -text 93 | *.gif filter=lfs diff=lfs merge=lfs -text 94 | *.hdr filter=lfs diff=lfs merge=lfs -text 95 | *.iff filter=lfs diff=lfs merge=lfs -text 96 | *.jpeg filter=lfs diff=lfs merge=lfs -text 97 | *.jpg filter=lfs diff=lfs merge=lfs -text 98 | *.pict filter=lfs diff=lfs merge=lfs -text 99 | *.png filter=lfs diff=lfs merge=lfs -text 100 | *.psd filter=lfs diff=lfs merge=lfs -text 101 | *.sbsar filter=lfs diff=lfs merge=lfs -text 102 | *.tga filter=lfs diff=lfs merge=lfs -text 103 | *.tif filter=lfs diff=lfs merge=lfs -text 104 | *.tiff filter=lfs diff=lfs merge=lfs -text 105 | # 106 | # Compressed Archive 107 | # 108 | *.7z filter=lfs diff=lfs merge=lfs -text 109 | *.bz2 filter=lfs diff=lfs merge=lfs -text 110 | *.gz filter=lfs diff=lfs merge=lfs -text 111 | *.rar filter=lfs diff=lfs merge=lfs -text 112 | *.tar filter=lfs diff=lfs merge=lfs -text 113 | *.zip filter=lfs diff=lfs merge=lfs -text 114 | # 115 | # Compiled Dynamic Library 116 | # 117 | *.bundle filter=lfs diff=lfs merge=lfs -text 118 | *.dll filter=lfs diff=lfs merge=lfs -text 119 | *.pdb filter=lfs diff=lfs merge=lfs -text 120 | *.so filter=lfs diff=lfs merge=lfs -text 121 | # 122 | # Executable/Installer 123 | # 124 | *.apk filter=lfs diff=lfs merge=lfs -text 125 | *.exe filter=lfs diff=lfs merge=lfs -text 126 | # 127 | # Collapse Unity-generated files on GitHub 128 | # 129 | *.asset linguist-generated 130 | *.mat linguist-generated 131 | *.meta linguist-generated 132 | *.prefab linguist-generated 133 | *.unity linguist-generated 134 | -------------------------------------------------------------------------------- /Sudoku/StoryBoard/photoSudoku.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 | 44 | 45 | 46 | 47 | 61 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /Sudoku/StoryBoard/pickerSudoku.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 62 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Sudoku/Assets.xcassets/sudokuImage.imageset/sudoku.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Sudoku/ViewController/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Sudoku 4 | // 5 | // Created by 이주화 on 2022/09/06. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | @IBOutlet weak var titleLabel: UILabel! 14 | @IBOutlet weak var goPhotoView: UIButton! 15 | @IBOutlet weak var goPickerView: UIButton! 16 | @IBOutlet weak var goInsertView: UIButton! 17 | @IBOutlet weak var mainSudokuCollectionView: UICollectionView! 18 | 19 | let bounds = UIScreen.main.bounds 20 | let sudokuNum = [Int](repeating: 0, count: 81) 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | setButton() 25 | setTitleLabel() 26 | collectionViewLink() 27 | setLayout() 28 | // imageView.image = UIImage(named: "sudokuImage") 29 | // Do any additional setup after loading the view. 30 | } 31 | 32 | private func setTitleLabel() { 33 | self.view.addSubview(titleLabel) 34 | titleLabel.text = "SolDoKu".localized 35 | titleLabel.textColor = UIColor.sudokuColor(.sudokuDeepPurple) 36 | titleLabel.font = .boldSystemFont(ofSize: 60) 37 | titleLabel.minimumScaleFactor = 0.5 38 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 39 | titleLabel.topAnchor.constraint(equalTo: self.view.topAnchor, constant: bounds.height/13).isActive = true 40 | titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true 41 | } 42 | 43 | private func setButton() { 44 | goPhotoView.setTitle("Take a Picture".localized, for: .normal) 45 | goPickerView.setTitle("Import from Album".localized, for: .normal) 46 | goInsertView.setTitle("Direct Input".localized, for: .normal) 47 | [goPhotoView, goPickerView, goInsertView].forEach { 48 | $0.layer.cornerRadius = 10 49 | $0.backgroundColor = UIColor.sudokuColor(.sudokuDeepButton) 50 | $0.titleLabel?.textColor = .white 51 | $0.titleLabel?.font = .boldSystemFont(ofSize: 30) 52 | $0.titleLabel?.minimumScaleFactor = 0.5 53 | } 54 | } 55 | 56 | private func collectionViewLink() { 57 | self.mainSudokuCollectionView.delegate = self 58 | self.mainSudokuCollectionView.dataSource = self 59 | } 60 | 61 | private func setLayout() { 62 | if ((bounds.width / bounds.height) <= 9/19) { 63 | titleLabel.snp.makeConstraints() { make in 64 | make.centerX.equalToSuperview() 65 | make.top.equalTo(view.safeAreaLayoutGuide).offset(10) 66 | } 67 | 68 | mainSudokuCollectionView.snp.makeConstraints() { make in 69 | make.centerX.equalToSuperview() 70 | make.top.equalTo(titleLabel.snp.bottom).offset(bounds.height / 45) 71 | make.leading.equalTo(self.view).offset(bounds.width/20) 72 | make.trailing.equalTo(self.view).offset(-(bounds.width/20)) 73 | make.size.width.height.equalTo(bounds.width * 0.9) 74 | } 75 | 76 | goPhotoView.snp.makeConstraints() { make in 77 | make.centerX.equalToSuperview() 78 | make.top.equalTo(mainSudokuCollectionView.snp.bottom).offset(bounds.height / 35) 79 | make.leading.equalTo(self.view).offset(bounds.width/20) 80 | make.trailing.equalTo(self.view).offset(-(bounds.width/20)) 81 | make.size.height.equalTo((bounds.width * 0.9) * 1/6.5) 82 | } 83 | 84 | goPickerView.snp.makeConstraints() { make in 85 | make.centerX.equalToSuperview() 86 | make.top.equalTo(goPhotoView.snp.bottom).offset(bounds.height / 35) 87 | make.leading.equalTo(self.view).offset(bounds.width/20) 88 | make.trailing.equalTo(self.view).offset(-(bounds.width/20)) 89 | make.size.height.equalTo((bounds.width * 0.9) * 1/6.5) 90 | } 91 | 92 | goInsertView.snp.makeConstraints() { make in 93 | make.centerX.equalToSuperview() 94 | make.top.equalTo(goPickerView.snp.bottom).offset(bounds.height / 35) 95 | make.leading.equalTo(self.view).offset(bounds.width/20) 96 | make.trailing.equalTo(self.view).offset(-(bounds.width/20)) 97 | make.size.height.equalTo((bounds.width * 0.9) * 1/6.5) 98 | } 99 | } else { 100 | titleLabel.snp.makeConstraints() { make in 101 | make.centerX.equalToSuperview() 102 | make.top.equalTo(view.safeAreaLayoutGuide).offset(5) 103 | } 104 | 105 | mainSudokuCollectionView.snp.makeConstraints() { make in 106 | make.centerX.equalToSuperview() 107 | make.top.equalTo(titleLabel.snp.bottom).offset(bounds.height / 45) 108 | make.leading.equalTo(self.view).offset(bounds.width/11) 109 | make.trailing.equalTo(self.view).offset(-(bounds.width/11)) 110 | make.size.width.height.equalTo(bounds.width * 9/11) 111 | } 112 | 113 | goPhotoView.snp.makeConstraints() { make in 114 | make.centerX.equalToSuperview() 115 | make.top.equalTo(mainSudokuCollectionView.snp.bottom).offset(bounds.height / 35) 116 | make.leading.equalTo(self.view).offset(bounds.width/11) 117 | make.trailing.equalTo(self.view).offset(-(bounds.width/11)) 118 | make.size.height.equalTo((bounds.width * 9/11) * 1/6.5) 119 | } 120 | 121 | goPickerView.snp.makeConstraints() { make in 122 | make.centerX.equalToSuperview() 123 | make.top.equalTo(goPhotoView.snp.bottom).offset(bounds.height / 35) 124 | make.leading.equalTo(self.view).offset(bounds.width/11) 125 | make.trailing.equalTo(self.view).offset(-(bounds.width/11)) 126 | make.size.height.equalTo((bounds.width * 9/11) * 1/6.5) 127 | } 128 | 129 | goInsertView.snp.makeConstraints() { make in 130 | make.centerX.equalToSuperview() 131 | make.top.equalTo(goPickerView.snp.bottom).offset(bounds.height / 35) 132 | make.leading.equalTo(self.view).offset(bounds.width/11) 133 | make.trailing.equalTo(self.view).offset(-(bounds.width/11)) 134 | make.size.height.equalTo((bounds.width * 9/11) * 1/6.5) 135 | } 136 | } 137 | } 138 | } 139 | 140 | extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource { 141 | 142 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 143 | return sudokuNum.count 144 | } 145 | 146 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 147 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? mainSudokuCollectionViewCell else { return UICollectionViewCell()} 148 | 149 | cell.layer.borderWidth = 1 150 | cell.layer.borderColor = UIColor.black.cgColor 151 | 152 | switch indexPath.row / 9 { 153 | case 0 : 154 | cell.layer.addBorder([.top], color: UIColor.black, width: 4) 155 | case 3, 6 : 156 | cell.layer.addBorder([.top], color: UIColor.black, width: 2) 157 | case 8 : 158 | cell.layer.addBorder([.bottom], color: UIColor.black, width: 4) 159 | default: break 160 | } 161 | 162 | switch indexPath.row % 9 { 163 | case 0 : 164 | cell.layer.addBorder([.left], color: UIColor.black, width: 4) 165 | case 3, 6 : 166 | cell.layer.addBorder([.left], color: UIColor.black, width: 2) 167 | case 8 : 168 | cell.layer.addBorder([.right], color: UIColor.black, width: 4) 169 | default: break 170 | } 171 | 172 | return cell 173 | } 174 | 175 | func cellSet(cell: UICollectionViewCell){ 176 | cell.backgroundColor = UIColor.sudokuColor(.sudokuLightPurple) 177 | } 178 | 179 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 180 | let selectCoordinate: [Int] = [indexPath.row / 9, indexPath.row % 9] 181 | let sectorRow: Int = 3 * Int(selectCoordinate[0] / 3) 182 | let sectorCol: Int = 3 * Int(selectCoordinate[1] / 3) 183 | let row1 = (selectCoordinate[0] + 2) % 3 184 | let row2 = (selectCoordinate[0] + 4) % 3 185 | let col1 = (selectCoordinate[1] + 2) % 3 186 | let col2 = (selectCoordinate[1] + 4) % 3 187 | 188 | for i in 0..<81 { 189 | guard let cell = mainSudokuCollectionView.cellForItem(at: [0, i]) as? mainSudokuCollectionViewCell else { 190 | fatalError() 191 | } 192 | let cellCoordinate: [Int] = [i / 9, i % 9] 193 | 194 | cell.backgroundColor = .white 195 | cell.layer.borderWidth = 1 196 | cell.layer.borderColor = UIColor.black.cgColor 197 | cell.alpha = 1 198 | 199 | if cellCoordinate[0] == selectCoordinate[0] { 200 | cellSet(cell: cell) 201 | } else if cellCoordinate[1] == selectCoordinate[1] { 202 | cellSet(cell: cell) 203 | } 204 | if (row1 + sectorRow) == cellCoordinate[0] && (col1 + sectorCol) == cellCoordinate[1] { cellSet(cell: cell) } 205 | if (row2 + sectorRow) == cellCoordinate[0] && (col1 + sectorCol) == cellCoordinate[1] { cellSet(cell: cell) } 206 | if (row1 + sectorRow) == cellCoordinate[0] && (col2 + sectorCol) == cellCoordinate[1] { cellSet(cell: cell) } 207 | if (row2 + sectorRow) == cellCoordinate[0] && (col2 + sectorCol) == cellCoordinate[1] { cellSet(cell: cell) } 208 | } 209 | 210 | guard let cell = collectionView.cellForItem(at: indexPath) as? mainSudokuCollectionViewCell else { 211 | fatalError() 212 | } 213 | 214 | UIView.animate(withDuration: 0.1, 215 | animations: { 216 | cell.transform = .init(scaleX: 0.90, y: 0.90) 217 | }) { (completed) in 218 | UIView.animate(withDuration: 0.1, 219 | animations: { 220 | cell.transform = .init(scaleX: 1, y: 1) 221 | }) 222 | } 223 | cell.alpha = 1 224 | cell.backgroundColor = UIColor.sudokuColor(.sudokuPurple) 225 | } 226 | } 227 | 228 | extension ViewController: UICollectionViewDelegateFlowLayout { 229 | // cell 사이즈( 옆 라인을 고려하여 설정 ) 230 | 231 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { 232 | return 0 233 | } 234 | 235 | // 옆 간격 236 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { 237 | return 0 238 | } 239 | 240 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 241 | let doubleNum: Double = Double(mainSudokuCollectionView.frame.width) / Double(9.0) 242 | let width = CGFloat(doubleNum) 243 | let size = CGSize(width: width, height: width) 244 | return size 245 | } 246 | } 247 | 248 | class mainSudokuCollectionViewCell: UICollectionViewCell { 249 | 250 | 251 | } 252 | 253 | -------------------------------------------------------------------------------- /Sudoku/Wrapper/wrapper.mm: -------------------------------------------------------------------------------- 1 | // 2 | // wrapper.m 3 | // Sudoku 4 | // 5 | // Created by 이주화 on 2022/09/12. 6 | // 7 | 8 | 9 | #import "wrapper.h" 10 | #import 11 | #import 12 | #import 13 | 14 | #ifdef __cplusplus 15 | #undef NO 16 | #undef YES 17 | #endif 18 | 19 | 20 | 21 | @implementation wrapper 22 | 23 | 24 | // c++에서 쓰인 포인터(좌표를)를 NSArray로 변환한다. 25 | NSArray *pointToArray(std::vector vect) { 26 | NSMutableArray *resultArray = [[NSMutableArray alloc] init]; 27 | for (int i = 0; i < vect.size(); i++) 28 | { 29 | NSValue *val = [NSValue valueWithCGPoint:CGPointMake(vect[i].x, vect[i].y)]; 30 | [resultArray addObject:val]; 31 | } 32 | return resultArray; 33 | } 34 | 35 | + (NSMutableArray *) detectRectangle: (UIImage *)image { 36 | @try 37 | { 38 | // UIImage를 opevCV에서 사용하는 Mat로 변환한다. 39 | cv::Mat mat; 40 | UIImageToMat(image, mat); 41 | 42 | // grayScale을 입힌다. 43 | cv::Mat toGray; 44 | cv::cvtColor(mat, toGray, CV_BGR2GRAY); 45 | 46 | // threshold를 강조한다. 47 | cv::Mat toThresh; 48 | cv::adaptiveThreshold(toGray, toThresh, 255, CV_ADAPTIVE_THRESH_MEAN_C, CV_THRESH_BINARY_INV, 31, 31); 49 | 50 | // 테두리를 찾는다. 51 | std::vector> contours; 52 | std::vector hierarchy; 53 | cv::findContours(toThresh, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE); 54 | if (contours.size() < 1) 55 | { 56 | //테두리가 없으면... 57 | return nil; 58 | } 59 | 60 | // 제일 큰 테두리를 찾는다. 61 | double maxArea = 0; 62 | int maxContourIndex = 0; 63 | for (int i = 0; i < contours.size(); i++) 64 | { 65 | double area = cv::contourArea(contours[i]); 66 | if (area > maxArea) 67 | { 68 | maxArea = area; 69 | maxContourIndex = i; 70 | } 71 | } 72 | std::vector maxContour = contours[maxContourIndex]; 73 | 74 | // 제일 큰 테두리에서 사각형을 찾는다. 75 | std::vector sumv, diffv; 76 | for (int i = 0; i < maxContour.size(); i++) 77 | { 78 | cv::Point p = maxContour[i]; 79 | sumv.push_back(p.x + p.y); 80 | diffv.push_back(p.x - p.y); 81 | } 82 | // c++의 distance를 이용해 테두리의 각 모서리를 찾는다. 83 | auto mins = std::distance(std::begin(sumv), std::min_element(std::begin(sumv), std::end(sumv))); 84 | auto maxs = std::distance(std::begin(sumv), std::max_element(std::begin(sumv), std::end(sumv))); 85 | auto mind = std::distance(std::begin(diffv), std::min_element(std::begin(diffv), std::end(diffv))); 86 | auto maxd = std::distance(std::begin(diffv), std::max_element(std::begin(diffv), std::end(diffv))); 87 | std::vector maxRect; 88 | maxRect.push_back(maxContour[mins]); // 왼쪽 위 모서리 89 | maxRect.push_back(maxContour[mind]); // 오른쪽 위 모서리 90 | maxRect.push_back(maxContour[maxs]); // 오른쪽 아래 모서리 91 | maxRect.push_back(maxContour[maxd]); // 왼쪽 아래 모서리 92 | 93 | // 구한 모서리를 이용해 각 모서리의 좌표를 구한다. 94 | cv::Point tl = maxRect[0]; 95 | cv::Point tr = maxRect[1]; 96 | cv::Point br = maxRect[2]; 97 | cv::Point bl = maxRect[3]; 98 | // 좌표간에 거리를 계산한뒤 제곱하고 루트를 씌워 모서리 사이의 거리(변의 길이)를 구한다. 99 | double widthA = sqrt(pow((br.x - bl.x), 2) + pow((br.y - bl.y), 2)); 100 | double widthB = sqrt(pow((tr.x - tl.x), 2) + pow((tr.y - tl.y), 2)); 101 | double heightA = sqrt(pow((tr.x - br.x), 2) + pow((tr.y - br.y), 2)); 102 | double heightB = sqrt(pow((tl.x - bl.x), 2) + pow((tl.y - bl.y), 2)); 103 | // 평행하는 변 중 긴 변을 구한다. 104 | double maxWidth = fmax(int(widthA), int(widthB)); 105 | double maxHeight = fmax(int(heightA), int(heightB)); 106 | 107 | // 구한 긴 변을 기준으로 짧은 변을 늘린다. 108 | cv::Point2f dst[4] = 109 | { 110 | cv::Point2f(0, 0), 111 | cv::Point2f(maxWidth, 0), 112 | cv::Point2f(maxWidth, maxHeight), 113 | cv::Point2f(0, maxHeight) 114 | }; 115 | cv::Point2f src[4] = { tl, bl, br, tr }; 116 | cv::Mat M = cv::getPerspectiveTransform(src, dst); 117 | cv::Mat warpMat; 118 | cv::warpPerspective(mat, warpMat, M, cv::Size(maxWidth, maxHeight)); 119 | 120 | NSMutableArray *result = [[NSMutableArray alloc] init]; 121 | [result addObject:pointToArray(maxRect)]; 122 | [result addObject:MatToUIImage(warpMat)]; 123 | 124 | return result; 125 | } 126 | @catch (...) 127 | { 128 | return nil; 129 | } 130 | } 131 | 132 | // 스도쿠를 9 * 9 로 잘라서 좌표 구하기 133 | + (NSMutableArray *) sliceImages: (UIImage *)image imageSize: (int)imageSize cutOffset: (int)cutOffset { 134 | 135 | cv::Mat mat; 136 | UIImageToMat(image, mat); 137 | 138 | std::vector slicedImages; 139 | cv::Mat numImageMat = cv::Mat(imageSize * 9, imageSize * 9, CV_8UC4); 140 | 141 | double dx = (mat.size()).width / 9.0; 142 | double dy = (mat.size()).height / 9.0; 143 | 144 | for (int row = 0; row < 9; row++) 145 | { 146 | for (int col = 0; col < 9; col++) 147 | { 148 | int x = (int)(col * dx); 149 | int y = (int)(row * dy); 150 | cv::Rect r = cv::Rect(x + cutOffset, y + cutOffset, dx - cutOffset, dy - cutOffset); 151 | cv::Mat sliced = mat(r); 152 | cv::Mat resized; 153 | cv::resize(sliced, resized, cv::Size(imageSize, imageSize)); 154 | 155 | slicedImages.push_back(MatToUIImage(resized)); 156 | 157 | // 잘랐던 것들을 다시 합친다. 158 | x = col * imageSize; 159 | y = row * imageSize; 160 | r = cv::Rect(x, y, imageSize, imageSize); 161 | resized.copyTo(numImageMat(r)); 162 | } 163 | } 164 | 165 | // 잘라두었던 이미지들을 배열에 집어넣는다. 166 | NSArray *numImages = [NSArray arrayWithObjects:&slicedImages[0] count:slicedImages.size()]; 167 | 168 | 169 | 170 | NSMutableArray *result = [[NSMutableArray alloc] init]; 171 | [result addObject:numImages]; 172 | [result addObject:MatToUIImage(numImageMat)]; 173 | 174 | return result; 175 | } 176 | 177 | + (NSMutableArray *) getNumImage: (UIImage *)image imageSize: (int)imageSize { 178 | // UIImage를 cv::Mat로 변환 179 | cv::Mat mat; 180 | UIImageToMat(image, mat); 181 | 182 | // grayScale을 입힌다. 183 | cv::Mat toGray; 184 | cv::cvtColor(mat, toGray, CV_BGR2GRAY); 185 | 186 | // threshold를 강조한다. 187 | cv::Mat toThresh; 188 | cv::adaptiveThreshold(toGray, toThresh, 255, CV_ADAPTIVE_THRESH_MEAN_C, CV_THRESH_BINARY_INV, 11, 41); 189 | 190 | // 테두리를 찾는다. 191 | std::vector> contours; 192 | std::vector hierarchy; 193 | cv::findContours(toThresh, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE); 194 | if (contours.size() < 1) 195 | { 196 | //테두리가 없으면... 197 | return nil; 198 | } 199 | 200 | cv::Mat contourMat; 201 | cv::cvtColor(toThresh, contourMat, cv::COLOR_GRAY2RGB); 202 | 203 | int gx = (int)(mat.size().width * 0.1); 204 | int gy = (int)(mat.size().height * 0.1); 205 | int gw = (int)(mat.size().width * 0.7); 206 | int gh = (int)(mat.size().height * 0.8); 207 | //이미지 자르기 208 | cv::rectangle(contourMat, cv::Point(gx, gy), cv::Point(gx+gw, gy+gh), CV_RGB(0, 255, 0), 2); 209 | 210 | 211 | int maxArea = 0; 212 | int maxContourIndex = -1; 213 | cv::Rect bRect; 214 | for (int i = 0; i < contours.size(); i++) 215 | { 216 | double area = cv::contourArea(contours[i]); 217 | if (area > 10) 218 | { 219 | cv::Rect r = cv::boundingRect(contours[i]); 220 | int ox = MAX(gx, r.x); 221 | int oy = MAX(gy, r.y); 222 | int ox2 = MIN(gx+gw, r.x+r.width); 223 | int oy2 = MIN(gy+gh, r.y+r.height); 224 | int ow = ox2 - ox; 225 | int oh = oy2 - oy; 226 | int oarea = ow * oh; 227 | if (oarea == r.area()) 228 | { 229 | // 포함되는 것 중 제일 큰 것만 남긴다 230 | if (area > maxArea) 231 | { 232 | maxArea = area; 233 | maxContourIndex = i; 234 | bRect = r; 235 | } 236 | } 237 | } 238 | } 239 | 240 | // 여러 가지 정보를 묶어 return 241 | NSMutableArray *result = [[NSMutableArray alloc] init]; 242 | 243 | // maxContourIndex가 0이상이면 숫자가 있는 것 244 | if (maxContourIndex >= 0) 245 | { 246 | cv::drawContours(contourMat, contours, maxContourIndex, CV_RGB(0, 255, 0), 2); 247 | NSNumber *t = [NSNumber numberWithBool:true]; // true를 return 248 | [result addObject:t]; 249 | } 250 | else 251 | { 252 | NSNumber *f = [NSNumber numberWithBool:false]; // false를 return 253 | [result addObject:f]; 254 | } 255 | 256 | // 디버깅을 위한 정보 제공 목적 257 | // contourMat는 80% box와 안의 숫자 contour가 그려진 이미지 258 | [result addObject:MatToUIImage(contourMat)]; 259 | 260 | 261 | return result; 262 | } 263 | 264 | // 사각형 영역을 인식하여 좌표를 내보내는 함수 선언 265 | + (NSArray *) detectRect: (UIImage *)image { 266 | @try 267 | { 268 | // UIImage를 opevCV에서 사용하는 Mat로 변환한다. 269 | cv::Mat mat; 270 | UIImageToMat(image, mat); 271 | 272 | // grayScale을 입힌다. 273 | cv::Mat toGray; 274 | cv::cvtColor(mat, toGray, CV_BGR2GRAY); 275 | 276 | // threshold를 강조한다. 277 | cv::Mat toThresh; 278 | cv::adaptiveThreshold(toGray, toThresh, 255, CV_ADAPTIVE_THRESH_MEAN_C, CV_THRESH_BINARY_INV, 31, 31); 279 | 280 | // 테두리를 찾는다. 281 | std::vector> contours; 282 | std::vector hierarchy; 283 | cv::findContours(toThresh, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE); 284 | if (contours.size() < 1) 285 | { 286 | //테두리가 없으면... 287 | return nil; 288 | } 289 | 290 | // 제일 큰 테두리를 찾는다. 291 | double maxArea = 0; 292 | int maxContourIndex = 0; 293 | for (int i = 0; i < contours.size(); i++) 294 | { 295 | double area = cv::contourArea(contours[i]); 296 | if (area > maxArea) 297 | { 298 | maxArea = area; 299 | maxContourIndex = i; 300 | } 301 | } 302 | std::vector maxContour = contours[maxContourIndex]; 303 | 304 | // 제일 큰 테두리에서 사각형을 찾는다. 305 | std::vector sumv, diffv; 306 | for (int i = 0; i < maxContour.size(); i++) 307 | { 308 | cv::Point p = maxContour[i]; 309 | sumv.push_back(p.x + p.y); 310 | diffv.push_back(p.x - p.y); 311 | } 312 | // c++의 distance를 이용해 테두리의 각 모서리를 찾는다. 313 | auto mins = std::distance(std::begin(sumv), std::min_element(std::begin(sumv), std::end(sumv))); 314 | auto maxs = std::distance(std::begin(sumv), std::max_element(std::begin(sumv), std::end(sumv))); 315 | auto mind = std::distance(std::begin(diffv), std::min_element(std::begin(diffv), std::end(diffv))); 316 | auto maxd = std::distance(std::begin(diffv), std::max_element(std::begin(diffv), std::end(diffv))); 317 | std::vector maxRect; 318 | maxRect.push_back(maxContour[mins]); // 왼쪽 위 모서리 319 | maxRect.push_back(maxContour[mind]); // 오른쪽 위 모서리 320 | maxRect.push_back(maxContour[maxs]); // 오른쪽 아래 모서리 321 | maxRect.push_back(maxContour[maxd]); // 왼쪽 아래 모서리 322 | 323 | 324 | // 구한 모서리를 이용해 각 모서리의 좌표를 구한다. 325 | cv::Point tl = maxRect[0]; 326 | cv::Point tr = maxRect[1]; 327 | cv::Point br = maxRect[2]; 328 | cv::Point bl = maxRect[3]; 329 | 330 | NSArray *result = pointToArray(maxRect); 331 | 332 | return result; 333 | } 334 | @catch (...) 335 | { 336 | return nil; 337 | } 338 | } 339 | 340 | @end 341 | -------------------------------------------------------------------------------- /Sudoku/StoryBoard/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 | 32 | 43 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /Sudoku/StoryBoard/importSudoku.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 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /Sudoku/ViewController/pickerSudokuViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // pickerSudokuViewController.swift 3 | // Sudoku 4 | // 5 | // Created by 이주화 on 2022/09/14. 6 | // 7 | import UIKit 8 | import AVFoundation 9 | import CoreML 10 | import Photos 11 | 12 | class pickerSudokuViewController: UIViewController { 13 | 14 | @IBOutlet weak var photoPicker: UIButton! 15 | @IBOutlet weak var solSudoku: UIButton! 16 | @IBOutlet weak var loadingLabel: UILabel! 17 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView! 18 | @IBOutlet weak var loadingView: UIView! 19 | @IBOutlet weak var pickerImage: UIImageView! 20 | 21 | 22 | private var sudokuSolvingWorkItem: DispatchWorkItem? 23 | private var count:Int = 0 24 | private let picker = UIImagePickerController() 25 | private var ignoreSolve: Bool = false 26 | private let bounds = UIScreen.main.bounds 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | self.navigationController?.navigationBar.tintColor = .black 31 | hideIndicator() 32 | picker.delegate = self 33 | setbutton() 34 | setLayout() 35 | // Do any additional setup after loading the view. 36 | } 37 | 38 | @IBAction func shootPhotoPicker(_ sender: UIButton) { 39 | let alert = UIAlertController(title: "Select".localized, message: nil, preferredStyle: .actionSheet) 40 | let library = UIAlertAction(title: "Album".localized, style: .default) { _ in 41 | if self.PhotoAuth() { 42 | self.openLibrary() 43 | } 44 | else { 45 | self.AuthSettingOpen(AuthString: "Album") 46 | } 47 | } 48 | let camera = UIAlertAction(title: "Camera".localized, style: .default) { _ in 49 | if self.CameraAuth() { 50 | self.openCamera() 51 | } 52 | else { 53 | self.AuthSettingOpen(AuthString: "Camera") 54 | } 55 | } 56 | let cancel = UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil) 57 | 58 | alert.addAction(library) 59 | alert.addAction(camera) 60 | alert.addAction(cancel) 61 | 62 | present(alert, animated: true, completion: nil) 63 | } 64 | 65 | @IBAction func shootSolSudoku(_ sender: Any) { 66 | if pickerImage.image != nil { 67 | showIndicator() 68 | sudokuSolvingWorkItem = DispatchWorkItem(block: self.sudokuSolvingQueue) 69 | DispatchQueue.main.async(execute: sudokuSolvingWorkItem!) 70 | } else { 71 | let alert = UIAlertController(title: "Picture hasn't been Uploaded.".localized, message: "Want to Upload a Picture?".localized, preferredStyle: .alert) 72 | let yes = UIAlertAction(title: "Yes".localized, style: .default) { _ in 73 | if self.PhotoAuth() { 74 | self.openLibrary() 75 | } 76 | else { 77 | self.AuthSettingOpen(AuthString: "Album") 78 | } 79 | 80 | } 81 | let no = UIAlertAction(title: "No".localized, style: .destructive, handler: nil) 82 | alert.addAction(no) 83 | alert.addAction(yes) 84 | present(alert, animated: true, completion: nil) 85 | } 86 | } 87 | 88 | private func setLayout() { 89 | loadingLabel.text = "Currently solving Sudoku".localized 90 | 91 | pickerImage.snp.makeConstraints() { make in 92 | make.centerX.equalToSuperview() 93 | make.top.equalTo(self.view.safeAreaLayoutGuide).offset(bounds.height * 0.01) 94 | make.leading.equalTo(self.view).offset(bounds.width * 0.05) 95 | make.trailing.equalTo(self.view).offset(-(bounds.width * 0.05)) 96 | make.size.width.height.equalTo(bounds.width * 0.9) 97 | } 98 | 99 | loadingView.snp.makeConstraints() { make in 100 | make.centerX.equalToSuperview() 101 | make.top.equalTo(self.view.safeAreaLayoutGuide).offset(bounds.height * 0.01) 102 | make.leading.equalTo(self.view).offset(bounds.width * 0.05) 103 | make.trailing.equalTo(self.view).offset(-(bounds.width * 0.05)) 104 | make.size.width.height.equalTo(bounds.width * 0.9) 105 | } 106 | 107 | activityIndicator.snp.makeConstraints() { make in 108 | make.centerX.equalToSuperview() 109 | make.top.equalTo(loadingView.snp.top).offset(loadingView.frame.height * 0.45) 110 | } 111 | 112 | loadingLabel.snp.makeConstraints() { make in 113 | make.centerX.equalToSuperview() 114 | make.top.equalTo(loadingView.snp.top).offset(loadingView.frame.height * 0.45) 115 | } 116 | 117 | photoPicker.snp.makeConstraints() { make in 118 | make.centerX.equalToSuperview() 119 | make.top.equalTo(pickerImage.snp.bottom).offset(bounds.height / 35) 120 | make.leading.equalTo(self.view).offset(bounds.width * 0.05) 121 | make.trailing.equalTo(self.view).offset(-(bounds.width * 0.05)) 122 | make.size.width.equalTo(bounds.width * 0.9) 123 | make.size.height.equalTo(bounds.width * 0.9 * 1/6) 124 | } 125 | 126 | solSudoku.snp.makeConstraints() { make in 127 | make.centerX.equalToSuperview() 128 | make.top.equalTo(photoPicker.snp.bottom).offset(bounds.height / 35) 129 | make.leading.equalTo(self.view).offset(bounds.width * 0.05) 130 | make.trailing.equalTo(self.view).offset(-(bounds.width * 0.05)) 131 | make.size.width.equalTo(bounds.width * 0.9) 132 | make.size.height.equalTo(bounds.width * 0.9 * 1/6) 133 | } 134 | } 135 | 136 | private func setbutton() { 137 | photoPicker.setTitle("Upload from Album".localized, for: .normal) 138 | solSudoku.setTitle("Solving Sudoku".localized, for: .normal) 139 | 140 | [photoPicker, solSudoku].forEach { 141 | $0.layer.cornerRadius = 10 142 | $0.backgroundColor = UIColor.sudokuColor(.sudokuDeepButton) 143 | $0.titleLabel?.textColor = .white 144 | $0.titleLabel?.font = .boldSystemFont(ofSize: 30) 145 | $0.titleLabel?.minimumScaleFactor = 0.5 146 | } 147 | } 148 | 149 | private func showIndicator() { 150 | activityIndicator.startAnimating() 151 | loadingView.isHidden = false 152 | } 153 | 154 | private func hideIndicator() { 155 | activityIndicator.stopAnimating() 156 | loadingView.isHidden = true 157 | } 158 | 159 | private func sudokuSolvingQueue() { 160 | self.recognizeNum(image: pickerImage.image!) 161 | } 162 | 163 | private func recognizeNum(image: UIImage) { 164 | // get sudoku number images 165 | var sudokuArray:[[Int]] = Array(repeating: Array(repeating: 0, count: 9), count: 9) 166 | var sudokuNumbersCount: Int = 0 167 | if let UIImgaeSliceArr = wrapper.sliceImages(image, imageSize: 64, cutOffset: 0) { 168 | let numImages = UIImgaeSliceArr[0] as! NSArray 169 | for i in 0.. Bool { 312 | // 포토 라이브러리 접근 권한 313 | let authorizationStatus = PHPhotoLibrary.authorizationStatus() 314 | 315 | var isAuth = false 316 | 317 | switch authorizationStatus { 318 | case .authorized: return true // 사용자가 앱에 사진 라이브러리에 대한 액세스 권한을 명시 적으로 부여했습니다. 319 | case .denied: break // 사용자가 사진 라이브러리에 대한 앱 액세스를 명시 적으로 거부했습니다. 320 | case .limited: break // ? 321 | case .notDetermined: // 사진 라이브러리 액세스에는 명시적인 사용자 권한이 필요하지만 사용자가 아직 이러한 권한을 부여하거나 거부하지 않았습니다 322 | PHPhotoLibrary.requestAuthorization { (state) in 323 | if state == .authorized { 324 | isAuth = true 325 | } 326 | } 327 | return isAuth 328 | case .restricted: break // 앱이 사진 라이브러리에 액세스 할 수있는 권한이 없으며 사용자는 이러한 권한을 부여 할 수 없습니다. 329 | default: break 330 | } 331 | 332 | return false; 333 | } 334 | 335 | func CameraAuth() -> Bool { 336 | return AVCaptureDevice.authorizationStatus(for: .video) == AVAuthorizationStatus.authorized 337 | } 338 | 339 | 340 | func AuthSettingOpen(AuthString: String) { 341 | if let AppName = Bundle.main.infoDictionary!["CFBundleName"] as? String { 342 | let message = "Soldoku is not allowed access to Album. \r\n Do you want to go to the Setting Screen?".localized 343 | let alert = UIAlertController(title: "Setting".localized, message: message, preferredStyle: .alert) 344 | 345 | let cancle = UIAlertAction(title: "Cancel".localized, style: .default) { _ in 346 | 347 | } 348 | let confirm = UIAlertAction(title: "Confirm".localized, style: .default) { (UIAlertAction) in 349 | UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) 350 | } 351 | alert.addAction(cancle) 352 | alert.addAction(confirm) 353 | 354 | self.present(alert, animated: true, completion: nil) 355 | } 356 | } 357 | } 358 | 359 | -------------------------------------------------------------------------------- /Sudoku/ViewController/importSudokuViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // importSudokuViewController.swift 3 | // Sudoku 4 | // 5 | // Created by 이주화 on 2022/09/06. 6 | // 7 | 8 | import UIKit 9 | 10 | class importSudokuViewController: UIViewController { 11 | 12 | @IBOutlet weak var sudokuCollectionView: UICollectionView! 13 | @IBOutlet weak var buttonCollectionView: UICollectionView! 14 | @IBOutlet weak var loadingView: UIView! 15 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView! 16 | @IBOutlet weak var loadingLabel: UILabel! 17 | 18 | let bounds = UIScreen.main.bounds 19 | var selectSudoku: Int = 0 20 | var selectSudokuArr: [Int] = [] 21 | var sudokuNum = [Int](repeating: 0, count: 81) 22 | let buttonArr = ["1", "2", "3", "Clean".localized, "4", "5", "6", "Delete".localized, "7", "8", "9", "Solve".localized] 23 | var solSudokuNum: [[Int]] = Array(repeating: Array(repeating: 0, count: 9), count: 9) 24 | var selectNum: IndexPath = [] 25 | let setNumArray = [1, 2, 3, 4, 5, 6, 7, 8, 9] 26 | var count: Int = 0 27 | private var ignoreSolve: Bool = false 28 | private var sudokuSolvingWorkItem: DispatchWorkItem? 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | collectionViewLink() 33 | hideIndicator() 34 | setLayout() 35 | } 36 | 37 | private func showIndicator() { 38 | activityIndicator.startAnimating() 39 | loadingView.isHidden = false 40 | } 41 | 42 | private func hideIndicator() { 43 | activityIndicator.stopAnimating() 44 | loadingView.isHidden = true 45 | } 46 | 47 | private func setLayout() { 48 | loadingLabel.text = "Currently solving Sudoku".localized 49 | 50 | if ((bounds.width / bounds.height) <= 9/19) { 51 | sudokuCollectionView.snp.makeConstraints() { make in 52 | make.centerX.equalToSuperview() 53 | make.top.equalTo(self.view.safeAreaLayoutGuide).offset(bounds.height * 0.03) 54 | make.leading.equalTo(self.view).offset(bounds.width * 0.05) 55 | make.trailing.equalTo(self.view).offset(-(bounds.width * 0.05)) 56 | make.size.width.height.equalTo(bounds.width * 0.9) 57 | } 58 | 59 | buttonCollectionView.snp.makeConstraints() { make in 60 | make.centerX.equalToSuperview() 61 | make.top.equalTo(sudokuCollectionView.snp.bottom).offset(bounds.height * 0.03) 62 | make.leading.equalTo(self.view).offset(bounds.width * 0.05) 63 | make.trailing.equalTo(self.view).offset(-(bounds.width * 0.05)) 64 | make.size.width.height.equalTo(bounds.width * 0.9) 65 | } 66 | 67 | loadingView.snp.makeConstraints() { make in 68 | make.centerX.equalToSuperview() 69 | make.top.equalTo(self.view.safeAreaLayoutGuide).offset(bounds.height * 0.03) 70 | make.leading.equalTo(self.view).offset(bounds.width * 0.05) 71 | make.trailing.equalTo(self.view).offset(-(bounds.width * 0.05)) 72 | make.size.width.height.equalTo(bounds.width * 0.9) 73 | } 74 | 75 | activityIndicator.snp.makeConstraints() { make in 76 | make.centerX.equalToSuperview() 77 | make.top.equalTo(loadingView.snp.top).offset(loadingView.frame.height * 0.45) 78 | } 79 | 80 | loadingLabel.snp.makeConstraints() { make in 81 | make.centerX.equalToSuperview() 82 | make.top.equalTo(loadingView.snp.top).offset(loadingView.frame.height * 0.7) 83 | } 84 | } else { 85 | sudokuCollectionView.snp.makeConstraints() { make in 86 | make.centerX.equalToSuperview() 87 | make.top.equalTo(self.view.safeAreaLayoutGuide).offset(bounds.height * 0.03) 88 | make.leading.equalTo(self.view).offset(bounds.width * 0.1) 89 | make.trailing.equalTo(self.view).offset(-(bounds.width * 0.1)) 90 | make.size.width.height.equalTo(bounds.width * 0.8) 91 | } 92 | 93 | buttonCollectionView.snp.makeConstraints() { make in 94 | make.centerX.equalToSuperview() 95 | make.top.equalTo(sudokuCollectionView.snp.bottom).offset(bounds.height * 0.03) 96 | make.leading.equalTo(self.view).offset(bounds.width * 0.1) 97 | make.trailing.equalTo(self.view).offset(-(bounds.width * 0.1)) 98 | make.size.width.height.equalTo(bounds.width * 0.8) 99 | } 100 | 101 | loadingView.snp.makeConstraints() { make in 102 | make.centerX.equalToSuperview() 103 | make.top.equalTo(self.view.safeAreaLayoutGuide).offset(bounds.height * 0.03) 104 | make.leading.equalTo(self.view).offset(bounds.width * 0.1) 105 | make.trailing.equalTo(self.view).offset(-(bounds.width * 0.1)) 106 | make.size.width.height.equalTo(bounds.width * 0.8) 107 | } 108 | 109 | activityIndicator.snp.makeConstraints() { make in 110 | make.centerX.equalToSuperview() 111 | make.top.equalTo(loadingView.snp.top).offset(loadingView.frame.height * 0.45) 112 | } 113 | 114 | loadingLabel.snp.makeConstraints() { make in 115 | make.centerX.equalToSuperview() 116 | make.top.equalTo(loadingView.snp.top).offset(loadingView.frame.height * 0.7) 117 | } 118 | } 119 | 120 | 121 | } 122 | func shootSolveSudoku() { 123 | showIndicator() 124 | sudokuSolvingWorkItem = DispatchWorkItem(block: self.solveSudoku) 125 | DispatchQueue.main.async(execute: sudokuSolvingWorkItem!) 126 | } 127 | 128 | func solveSudoku() { 129 | var check: Int = 0 130 | var numCount: Int = 0 131 | for i in 0..<9 { 132 | for j in 0..<9 { 133 | if sudokuNum[check] != 0 { 134 | numCount += 1 135 | } 136 | solSudokuNum[i][j] = sudokuNum[check] 137 | check += 1 138 | } 139 | } 140 | if !ignoreSolve { 141 | if numCount < 17 { 142 | let alert = UIAlertController(title: "Really want to Solve?".localized, message: "Sudoku Solve requires more than 17 numbers.".localized, preferredStyle: .alert) 143 | let yes = UIAlertAction(title: "Yes".localized, style: .default) { _ in 144 | self.hideIndicator() 145 | self.ignoreSolve.toggle() 146 | self.solveSudoku() 147 | } 148 | let no = UIAlertAction(title: "No".localized, style: .destructive) { _ in 149 | self.hideIndicator() 150 | } 151 | alert.addAction(no) 152 | alert.addAction(yes) 153 | present(alert, animated: true, completion: nil) 154 | return 155 | } 156 | } 157 | count = 0 158 | let successCheck = sudokuCalculation(&solSudokuNum, 0, 0, &count) 159 | if !successCheck { 160 | let alert = UIAlertController(title: "Cannot solve Sudoku.".localized, message: "Do you want to re-enter Sudoku?".localized, preferredStyle: .alert) 161 | let yes = UIAlertAction(title: "Yes".localized, style: .default) { _ in 162 | for i in 0..<81 { 163 | guard let cell = self.sudokuCollectionView.cellForItem(at: [0, i]) as? sudokuCollectionViewCell else { 164 | fatalError() 165 | } 166 | 167 | cell.importNum.text = "" 168 | self.sudokuNum[i] = 0 169 | self.hideIndicator() 170 | } 171 | } 172 | let no = UIAlertAction(title: "No".localized, style: .destructive) { _ in 173 | self.hideIndicator() 174 | } 175 | alert.addAction(no) 176 | alert.addAction(yes) 177 | present(alert, animated: true, completion: nil) 178 | return 179 | } 180 | hideIndicator() 181 | drawSudoku() 182 | ignoreSolve.toggle() 183 | } 184 | 185 | private func drawSudoku() { 186 | var check: Int = 0 187 | for i in 0..<9 { 188 | for j in 0..<9 { 189 | sudokuNum[check] = solSudokuNum[i][j] 190 | check += 1 191 | } 192 | } 193 | 194 | for i in 0..<81 { 195 | guard let cell = sudokuCollectionView.cellForItem(at: [0, i]) as? sudokuCollectionViewCell else { 196 | fatalError() 197 | } 198 | 199 | cell.importNum.text = String(sudokuNum[i]) 200 | } 201 | } 202 | 203 | private func collectionViewLink() { 204 | self.sudokuCollectionView.delegate = self 205 | self.sudokuCollectionView.dataSource = self 206 | self.buttonCollectionView.delegate = self 207 | self.buttonCollectionView.dataSource = self 208 | } 209 | /* 210 | // MARK: - Navigation 211 | 212 | // In a storyboard-based application, you will often want to do a little preparation before navigation 213 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 214 | // Get the new view controller using segue.destination. 215 | // Pass the selected object to the new view controller. 216 | } 217 | */ 218 | 219 | } 220 | 221 | extension importSudokuViewController: UICollectionViewDelegate, UICollectionViewDataSource { 222 | 223 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 224 | if collectionView == sudokuCollectionView { 225 | return sudokuNum.count 226 | } else { 227 | return buttonArr.count 228 | } 229 | } 230 | 231 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 232 | // 스도쿠 영역 233 | if collectionView == sudokuCollectionView { 234 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? sudokuCollectionViewCell else { return UICollectionViewCell()} 235 | 236 | cell.importNum.text = "" 237 | cell.layer.borderWidth = 1 238 | cell.layer.borderColor = UIColor.black.cgColor 239 | 240 | // 각 행, 렬에 따라 각기 다른 테두리 굵기를 가지게 하여 스도쿠의 영역을 표시 241 | switch indexPath.row / 9 { 242 | case 0 : 243 | cell.layer.addBorder([.top], color: UIColor.black, width: 4) 244 | case 3, 6 : 245 | cell.layer.addBorder([.top], color: UIColor.black, width: 2) 246 | case 8 : 247 | cell.layer.addBorder([.bottom], color: UIColor.black, width: 4) 248 | default: break 249 | } 250 | 251 | switch indexPath.row % 9 { 252 | case 0 : 253 | cell.layer.addBorder([.left], color: UIColor.black, width: 4) 254 | case 3, 6 : 255 | cell.layer.addBorder([.left], color: UIColor.black, width: 2) 256 | case 8 : 257 | cell.layer.addBorder([.right], color: UIColor.black, width: 4) 258 | default: break 259 | } 260 | 261 | return cell 262 | } else { // 버튼 영역 263 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? buttonCollectionViewCell else { return UICollectionViewCell()} 264 | 265 | cell.importButton.text = buttonArr[indexPath.row] 266 | if (cell.importButton.text == "Clean".localized || cell.importButton.text == "Delete".localized || cell.importButton.text == "Solve".localized) { 267 | cell.importButton.font = UIFont.systemFont(ofSize: 16, weight: .semibold) 268 | cell.contentView.backgroundColor = UIColor.sudokuColor(.sudokuDeepButton) 269 | cell.importButton.textColor = .white 270 | cell.importButton.minimumScaleFactor = 0.5 271 | } else { 272 | cell.importButton.font = UIFont.boldSystemFont(ofSize: 25) 273 | cell.contentView.backgroundColor = UIColor.sudokuColor(.sudokuDeepButton) 274 | cell.importButton.textColor = .white 275 | cell.importButton.minimumScaleFactor = 0.5 276 | } 277 | cell.layer.cornerRadius = cell.frame.width / 2 278 | cell.layer.backgroundColor = UIColor.sudokuColor(.sudokuButton).cgColor 279 | 280 | return cell 281 | } 282 | } 283 | 284 | // 셀을 변경 시키는 함수 285 | func cellColorSet(cell: sudokuCollectionViewCell){ 286 | cell.backgroundColor = UIColor.sudokuColor(.sudokuLightPurple) 287 | if cell.importNum.text != "0" { 288 | selectSudoku = Int(cell.importNum.text!) ?? 0 289 | selectSudokuArr.append(selectSudoku) 290 | } 291 | } 292 | 293 | // 셀을 클릭하였을때 294 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 295 | if collectionView == sudokuCollectionView { // 클릭한 셀이 스도쿠 영역이라면 296 | selectSudokuArr.removeAll() 297 | let selectCoordinate: [Int] = [indexPath.row / 9, indexPath.row % 9] 298 | let sectorRow: Int = 3 * Int(selectCoordinate[0] / 3) 299 | let sectorCol: Int = 3 * Int(selectCoordinate[1] / 3) 300 | let row1 = (selectCoordinate[0] + 2) % 3 301 | let row2 = (selectCoordinate[0] + 4) % 3 302 | let col1 = (selectCoordinate[1] + 2) % 3 303 | let col2 = (selectCoordinate[1] + 4) % 3 304 | for i in 0..<81 { 305 | guard let cell = sudokuCollectionView.cellForItem(at: [0, i]) as? sudokuCollectionViewCell else { 306 | fatalError() 307 | } 308 | let cellNum: String = cell.importNum.text ?? "" 309 | let cellCoordinateX = i / 9 310 | let cellCoordinateY = i % 9 311 | // 셀 색상 초기화 312 | cell.backgroundColor = .white 313 | cell.layer.borderWidth = 1 314 | cell.layer.borderColor = UIColor.black.cgColor 315 | cell.alpha = 1 316 | if cellCoordinateX == selectCoordinate[0] { cellColorSet(cell: cell) } 317 | else if cellCoordinateY == selectCoordinate[1] { cellColorSet(cell: cell) } 318 | 319 | if (row1 + sectorRow) == cellCoordinateX && (col1 + sectorCol) == cellCoordinateY { cellColorSet(cell: cell) } 320 | if (row2 + sectorRow) == cellCoordinateX && (col1 + sectorCol) == cellCoordinateY { cellColorSet(cell: cell) } 321 | if (row1 + sectorRow) == cellCoordinateX && (col2 + sectorCol) == cellCoordinateY { cellColorSet(cell: cell) } 322 | if (row2 + sectorRow) == cellCoordinateX && (col2 + sectorCol) == cellCoordinateY { cellColorSet(cell: cell) } 323 | 324 | // 셀에 입력된 숫자가 있다면 325 | if cellNum != "" { 326 | let cellSectorRow: Int = 3 * Int(cellCoordinateX / 3) 327 | let cellSectorCol: Int = 3 * Int(cellCoordinateY / 3) 328 | let cellRow1 = (cellCoordinateX + 2) % 3 329 | let cellRow2 = (cellCoordinateX + 4) % 3 330 | let cellCol1 = (cellCoordinateY + 2) % 3 331 | let cellCol2 = (cellCoordinateY + 4) % 3 332 | // 다른 셀들을 확인 333 | for j in 0..<81 { 334 | guard let checkCell = sudokuCollectionView.cellForItem(at: [0, j]) as? sudokuCollectionViewCell else { 335 | fatalError() 336 | } 337 | // 셀의 값이 다른 셀의 값과 같다면 338 | if cellNum == checkCell.importNum.text { 339 | let checkCellCoordinateX = j / 9 340 | let checkCellCoordinateY = j % 9 341 | // 해당하는 숫자가 그곳이 있어도 되는지 확인하여 있으면 안되는 곳이라면 셀의 색상을 변경한다. 342 | if cellCoordinateX != checkCellCoordinateX || cellCoordinateY != checkCellCoordinateY { 343 | if cellCoordinateX == checkCellCoordinateX { 344 | cell.backgroundColor = UIColor.sudokuColor(.sudokuLightRed) 345 | } else if cellCoordinateY == checkCellCoordinateY { 346 | cell.backgroundColor = UIColor.sudokuColor(.sudokuLightRed) } 347 | if (cellRow1 + cellSectorRow) == checkCellCoordinateX && (cellCol1 + cellSectorCol) == checkCellCoordinateY { cell.backgroundColor = UIColor.sudokuColor(.sudokuLightRed) } 348 | if (cellRow2 + cellSectorRow) == checkCellCoordinateX && (cellCol1 + cellSectorCol) == checkCellCoordinateY { cell.backgroundColor = UIColor.sudokuColor(.sudokuLightRed) } 349 | if (cellRow1 + cellSectorRow) == checkCellCoordinateX && (cellCol2 + cellSectorCol) == checkCellCoordinateY { cell.backgroundColor = UIColor.sudokuColor(.sudokuLightRed) } 350 | if (cellRow2 + cellSectorRow) == checkCellCoordinateX && (cellCol2 + cellSectorCol) == checkCellCoordinateY { cell.backgroundColor = UIColor.sudokuColor(.sudokuLightRed) } 351 | } 352 | } 353 | } 354 | } 355 | } 356 | // 들어가면 안되는 숫자 버튼 색 변경 357 | for i in 0.. CGFloat { 454 | if collectionView == sudokuCollectionView { 455 | return 0 456 | } else { 457 | return buttonCollectionView.frame.height / 10 458 | } 459 | } 460 | 461 | // 옆 간격 462 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { 463 | if collectionView == sudokuCollectionView { 464 | return 0 465 | } else { 466 | return buttonCollectionView.frame.width / 20 467 | } 468 | } 469 | 470 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 471 | if collectionView == sudokuCollectionView { 472 | let doubleNum: Double = Double(sudokuCollectionView.frame.width) / Double(9.0) 473 | let width = CGFloat(doubleNum) 474 | let size = CGSize(width: width, height: width) 475 | return size 476 | } else { 477 | let width = buttonCollectionView.frame.width / 5 478 | let size = CGSize(width: width, height: width) 479 | return size 480 | } 481 | 482 | 483 | } 484 | } 485 | class sudokuCollectionViewCell: UICollectionViewCell { 486 | @IBOutlet weak var importNum: UILabel! 487 | 488 | } 489 | 490 | class buttonCollectionViewCell: UICollectionViewCell { 491 | @IBOutlet weak var importButton: UILabel! 492 | 493 | } 494 | 495 | -------------------------------------------------------------------------------- /Sudoku/ViewController/photoSudokuViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // photoSudokuViewController.swift 3 | // Sudoku 4 | // 5 | // Created by 이주화 on 2022/09/06. 6 | // 7 | 8 | import UIKit 9 | import AVFoundation 10 | import CoreML 11 | 12 | final class photoSudokuViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate { 13 | 14 | @IBOutlet weak var refinedViewLabel: UILabel! 15 | @IBOutlet weak var cameraViewLabel: UILabel! 16 | @IBOutlet weak var cameraView: UIImageView! 17 | @IBOutlet weak var refinedView: UIImageView! 18 | @IBOutlet weak var shooting: UIButton! 19 | @IBOutlet weak var loadingView: UIView! 20 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView! 21 | 22 | private var period: Int = 0 // 30프레임마다 숫자 인식을 하기위한 변수 선언 23 | private var particleLayer = CAShapeLayer() 24 | private var particlePath = UIBezierPath() 25 | private var session: AVCaptureSession? 26 | private var previewLayer: AVCaptureVideoPreviewLayer? 27 | private var count: Int = 0 28 | private var sudokuSolvingWorkItem: DispatchWorkItem? 29 | private var ignoreSolve: Bool = false 30 | private let bounds = UIScreen.main.bounds 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | self.navigationController?.navigationBar.tintColor = .black 35 | hideIndicator() 36 | preparedSession() 37 | session?.startRunning() 38 | shooting.layer.cornerRadius = 10 39 | setButton() 40 | refinedView.image = UIImage(named: "sudoku") 41 | setLayout() 42 | addRefinedViewAction() 43 | } 44 | 45 | override func viewDidAppear(_ animated: Bool) { 46 | super.viewDidAppear(animated) 47 | if !self.CameraAuth() { 48 | self.AuthSettingOpen(AuthString: "Camera") 49 | } 50 | } 51 | 52 | @IBAction func shootingAction(_ sender: Any) { 53 | if shooting.titleLabel?.text == "Shoot Again".localized { 54 | refinedView.image = UIImage(named: "sudoku") 55 | cameraStart() 56 | shooting.setTitle("Shooting Sudoku".localized, for: .normal) 57 | } else { 58 | sudokuSolvingWorkItem = DispatchWorkItem(block: sudokuSolvingQueue) 59 | DispatchQueue.main.async(execute: sudokuSolvingWorkItem!) 60 | cameraStop() 61 | shooting.setTitle("Shoot Again".localized, for: .normal) 62 | } 63 | } 64 | 65 | @objc func imageTapped(sender: UITapGestureRecognizer) { 66 | if shooting.titleLabel?.text == "Shoot Again".localized { 67 | refinedView.image = UIImage(named: "sudoku") 68 | session?.startRunning() 69 | shooting.setTitle("Shooting Sudoku".localized, for: .normal) 70 | } 71 | else { 72 | let sessionStatus = session?.isRunning ?? false 73 | if sessionStatus { 74 | cameraViewLabel.isHidden = true 75 | session?.stopRunning() 76 | } else { 77 | refinedView.image = UIImage(named: "sudoku") 78 | session?.startRunning() 79 | } 80 | } 81 | } 82 | 83 | private func addRefinedViewAction() { 84 | let tapGR = UITapGestureRecognizer(target: self, action: #selector(self.imageTapped)) 85 | refinedView.addGestureRecognizer(tapGR) 86 | refinedView.isUserInteractionEnabled = true 87 | } 88 | 89 | private func setLayout() { 90 | cameraViewLabel.text = "Please look where Sudoku is located".localized 91 | refinedViewLabel.text = "Currently solving Sudoku".localized 92 | 93 | if ((bounds.width / bounds.height) <= 9/19) { 94 | cameraView.snp.makeConstraints() { make in 95 | make.centerX.equalToSuperview() 96 | make.top.equalTo(self.view.safeAreaLayoutGuide).offset(1) 97 | make.leading.equalTo(self.view).offset(bounds.width * 0.075) 98 | make.trailing.equalTo(self.view).offset(-(bounds.width * 0.075)) 99 | make.size.width.height.equalTo(bounds.width * 0.85) 100 | } 101 | 102 | refinedView.snp.makeConstraints() { make in 103 | make.centerX.equalToSuperview() 104 | make.top.equalTo(cameraView.snp.bottom).offset(3) 105 | make.leading.equalTo(self.view).offset(bounds.width * 0.075) 106 | make.trailing.equalTo(self.view).offset(-(bounds.width * 0.075)) 107 | make.size.width.height.equalTo(bounds.width * 0.85) 108 | } 109 | 110 | loadingView.snp.makeConstraints() { make in 111 | make.centerX.equalToSuperview() 112 | make.top.equalTo(cameraView.snp.bottom).offset(3) 113 | make.leading.equalTo(self.view).offset(bounds.width * 0.075) 114 | make.trailing.equalTo(self.view).offset(-(bounds.width * 0.075)) 115 | make.size.width.height.equalTo(bounds.width * 0.85) 116 | } 117 | 118 | activityIndicator.snp.makeConstraints() { make in 119 | make.centerX.equalToSuperview() 120 | make.top.equalTo(loadingView.snp.top).offset(loadingView.frame.height * 0.45) 121 | } 122 | 123 | cameraViewLabel.snp.makeConstraints() { make in 124 | make.centerX.equalToSuperview() 125 | make.top.equalTo(cameraView.snp.top).offset(loadingView.frame.height * 0.45) 126 | } 127 | 128 | refinedViewLabel.snp.makeConstraints() { make in 129 | make.centerX.equalToSuperview() 130 | make.top.equalTo(refinedView.snp.top).offset(refinedView.frame.height * 0.7) 131 | } 132 | 133 | shooting.snp.makeConstraints() { make in 134 | make.centerX.equalToSuperview() 135 | make.top.equalTo(refinedView.snp.bottom).offset(5) 136 | make.leading.equalTo(self.view).offset(bounds.width * 0.075) 137 | make.trailing.equalTo(self.view).offset(-(bounds.width * 0.075)) 138 | make.size.width.equalTo(bounds.width * 0.85) 139 | make.size.height.equalTo(bounds.width * 0.85 * 1/7) 140 | } 141 | } else { 142 | cameraView.snp.makeConstraints() { make in 143 | make.centerX.equalToSuperview() 144 | make.top.equalTo(self.view.safeAreaLayoutGuide).offset(1) 145 | make.leading.equalTo(self.view).offset(bounds.width * 0.125) 146 | make.trailing.equalTo(self.view).offset(-(bounds.width * 0.125)) 147 | make.size.width.height.equalTo(bounds.width * 0.75) 148 | } 149 | 150 | refinedView.snp.makeConstraints() { make in 151 | make.centerX.equalToSuperview() 152 | make.top.equalTo(cameraView.snp.bottom).offset(3) 153 | make.leading.equalTo(self.view).offset(bounds.width * 0.125) 154 | make.trailing.equalTo(self.view).offset(-(bounds.width * 0.125)) 155 | make.size.width.height.equalTo(bounds.width * 0.75) 156 | } 157 | 158 | loadingView.snp.makeConstraints() { make in 159 | make.centerX.equalToSuperview() 160 | make.top.equalTo(cameraView.snp.bottom).offset(3) 161 | make.leading.equalTo(self.view).offset(bounds.width * 0.125) 162 | make.trailing.equalTo(self.view).offset(-(bounds.width * 0.125)) 163 | make.size.width.height.equalTo(bounds.width * 0.75) 164 | } 165 | 166 | activityIndicator.snp.makeConstraints() { make in 167 | make.centerX.equalToSuperview() 168 | make.top.equalTo(loadingView.snp.top).offset(loadingView.frame.height * 0.45) 169 | } 170 | 171 | cameraViewLabel.snp.makeConstraints() { make in 172 | make.centerX.equalToSuperview() 173 | make.top.equalTo(cameraView.snp.top).offset(loadingView.frame.height * 0.45) 174 | } 175 | 176 | refinedViewLabel.snp.makeConstraints() { make in 177 | make.centerX.equalToSuperview() 178 | make.top.equalTo(refinedView.snp.top).offset(refinedView.frame.height * 0.7) 179 | } 180 | 181 | shooting.snp.makeConstraints() { make in 182 | make.centerX.equalToSuperview() 183 | make.top.equalTo(refinedView.snp.bottom).offset(5) 184 | make.leading.equalTo(self.view).offset(bounds.width * 0.125) 185 | make.trailing.equalTo(self.view).offset(-(bounds.width * 0.125)) 186 | make.size.width.height.equalTo(bounds.width * 0.75) 187 | make.size.height.equalTo(bounds.width * 0.75 * 1/7) 188 | } 189 | } 190 | } 191 | 192 | private func setButton() { 193 | shooting.setTitle("Shooting Sudoku".localized, for: .normal) 194 | shooting.layer.cornerRadius = 10 195 | shooting.backgroundColor = UIColor.sudokuColor(.sudokuDeepButton) 196 | shooting.titleLabel?.textColor = .white 197 | shooting.titleLabel?.font = .boldSystemFont(ofSize: 30) 198 | shooting.titleLabel?.minimumScaleFactor = 0.5 199 | } 200 | private func showIndicator() { 201 | activityIndicator.startAnimating() 202 | loadingView.isHidden = false 203 | } 204 | 205 | private func hideIndicator() { 206 | activityIndicator.stopAnimating() 207 | loadingView.isHidden = true 208 | } 209 | 210 | private func sudokuSolvingQueue() { 211 | self.recognizeNum(image: refinedView.image!) 212 | } 213 | 214 | private func cameraStart(){ 215 | session?.startRunning() 216 | } 217 | 218 | private func cameraStop(){ 219 | cameraViewLabel.isHidden = true 220 | session?.stopRunning() 221 | showIndicator() 222 | } 223 | 224 | private func preparedSession() { 225 | let camera = AVCaptureDevice.default(for: AVMediaType.video) 226 | do { 227 | let cameraInput = try AVCaptureDeviceInput(device: camera!) 228 | 229 | session = AVCaptureSession() 230 | session?.sessionPreset = AVCaptureSession.Preset.hd1280x720 231 | //해상도 지정 232 | session?.addInput(cameraInput) 233 | 234 | let videoOutput = AVCaptureVideoDataOutput() 235 | /* 236 | https://developer.apple.com/documentation/avfoundation/avcapturevideodataoutput 237 | */ 238 | 239 | //픽셀버퍼 핸들링을 용이하게 하기위해 BGRA타입으로 변환 240 | videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as AnyHashable as! String: NSNumber(value: kCVPixelFormatType_32BGRA)] 241 | 242 | let sessionQueue = DispatchQueue(label: "camera") 243 | videoOutput.setSampleBufferDelegate(self, queue: sessionQueue) 244 | session?.addOutput(videoOutput) 245 | 246 | previewLayer = AVCaptureVideoPreviewLayer(session: session!) 247 | 248 | previewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill 249 | previewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation.portrait 250 | previewLayer?.frame = cameraView.bounds 251 | cameraView.layer.addSublayer(previewLayer!) 252 | } catch { 253 | 254 | } 255 | } 256 | 257 | // 비디오 프레임이 들어올 때마다 갱신됨 258 | /* 259 | 참고 260 | https://developer.apple.com/documentation/avfoundation/avcapturevideodataoutputsamplebufferdelegate/1385775-captureoutput 261 | */ 262 | internal func captureOutput(_ output: AVCaptureOutput, didOutput buffer: CMSampleBuffer, from connection: AVCaptureConnection) { 263 | //기기의 현재 방향에 따라 화면의 방향도 돌려준다. 264 | connection.videoOrientation = AVCaptureVideoOrientation.portrait 265 | 266 | /* 267 | https://developer.apple.com/documentation/coremedia/1489236-cmsamplebuffergetimagebuffer 268 | */ 269 | //CMSampleBuffer를 CVImageBuffer로 변환시켜준다. 270 | guard let CVimageBuffer = CMSampleBufferGetImageBuffer(buffer) else { 271 | return 272 | } 273 | 274 | /* 275 | CVPixelBufferLockBaseAddress: 276 | https://developer.apple.com/documentation/corevideo/1457128-cvpixelbufferlockbaseaddress 277 | 픽셀의 주소를 고정시켜준다. 278 | */ 279 | CVPixelBufferLockBaseAddress(CVimageBuffer, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0))) 280 | //이미지의 넓이 구하기 281 | let width = CVPixelBufferGetWidth(CVimageBuffer) 282 | let height = CVPixelBufferGetHeight(CVimageBuffer) 283 | 284 | //이미지에서 사용되는 각각의 Component가 사용하는 비트 수 선언 285 | let bitsPerComponent = 8 286 | 287 | //이미지의 row에 있는 바이트를 구한다. 288 | let bytesRow = CVPixelBufferGetBytesPerRow(CVimageBuffer) 289 | 290 | //이미지의 주소값을 구한다. 291 | guard let imageAddress = CVPixelBufferGetBaseAddress(CVimageBuffer) else { 292 | return 293 | } 294 | let colorSpace = CGColorSpaceCreateDeviceRGB() 295 | 296 | //비트 연산자 or 을 이용해 비트를 정리한다. 297 | let bitmap = CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue 298 | let context = CGContext(data: imageAddress, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesRow, space: colorSpace, bitmapInfo: bitmap) 299 | if let newContext = context { 300 | let frame = newContext.makeImage() 301 | DispatchQueue.main.async { 302 | let img = UIImage(cgImage: frame!) 303 | // crop 304 | let w = img.size.width 305 | let y = (img.size.height - w) / 2 306 | let rect = CGRect(x: 0, y: y, width: w, height: w) 307 | let imgCrop = img.cgImage?.cropping(to: rect) 308 | let refinedImage = UIImage(cgImage: imgCrop!) 309 | self.toRefinedView(refinedImage) 310 | } 311 | } 312 | //사용했던 픽셀 주소의 고정을 풀고 재사용이 가능하도록 한다. 313 | CVPixelBufferUnlockBaseAddress(CVimageBuffer, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0))) 314 | } 315 | 316 | private func toRefinedView(_ capturedImage: UIImage) { 317 | // 인식된 영역이 너무 작을 경우 숫자 인식을 하지 않기 위한 변수 선언 318 | var valueX: Float = 0 319 | var valueY: Float = 0 320 | var valueX2: Float = 0 321 | var valueY2: Float = 0 322 | 323 | if let detectRect = wrapper.detectRect(capturedImage){ 324 | let cg: [CGPoint] = detectRect as! [CGPoint] // OpenCV로 인식한 스도쿠 영역의 좌표 325 | valueX = Float(cg[0].x - cg[3].x) 326 | valueY = Float(cg[0].y - cg[1].y) 327 | valueX2 = Float(cg[1].x - cg[2].x) 328 | valueY2 = Float(cg[2].y - cg[3].y) 329 | drawRectangle(rect: cg) 330 | cameraView.layer.addSublayer(particleLayer) 331 | } 332 | period += 1 333 | 334 | // 30프레임마다 영역의 크기가 일정 이상일때 숫자 인식 모델을 통해 숫자 인식 335 | if period >= 30 && abs(valueX) > 100 && abs(valueY) > 100 && abs(valueX2) > 100 && abs(valueY2) > 100 { 336 | if let detectRectangle = wrapper.detectRectangle(capturedImage) { 337 | recognizePresentNum(image: detectRectangle[1] as! UIImage) 338 | } 339 | period = 0 340 | } 341 | 342 | } 343 | 344 | func drawRectangle(rect: [CGPoint]) { 345 | // 카메라에서 인식한 좌표와 인식한 영역을 그리는 곳의 좌표가 달라서 좌표를 계산하기 위한 변수 선언 346 | let widthSize = cameraView.bounds.width / UIScreen.main.bounds.width 347 | let widthHeight = cameraView.bounds.height / UIScreen.main.bounds.height 348 | let framesize = widthSize / widthHeight 349 | 350 | particleLayer.fillColor = UIColor.clear.cgColor 351 | particleLayer.strokeColor = UIColor.red.cgColor 352 | particleLayer.lineWidth = 5 353 | particlePath.removeAllPoints() 354 | 355 | particlePath.move(to: CGPoint(x: rect[0].x / framesize, y: rect[0].y / framesize)) 356 | particlePath.addLine(to: CGPoint(x: rect[1].x / framesize, y: rect[1].y / framesize)) 357 | particlePath.addLine(to: CGPoint(x: rect[2].x / framesize, y: rect[2].y / framesize)) 358 | particlePath.addLine(to: CGPoint(x: rect[3].x / framesize, y: rect[3].y / framesize)) 359 | particlePath.close() 360 | 361 | particleLayer.path = particlePath.cgPath 362 | 363 | } 364 | 365 | private func recognizePresentNum(image: UIImage) { 366 | // get sudoku number images 367 | var sudokuArray:[[Int]] = Array(repeating: Array(repeating: 0, count: 9), count: 9) 368 | if let UIImgaeSliceArr = wrapper.sliceImages(image, imageSize: 64, cutOffset: 0) { 369 | let numImages = UIImgaeSliceArr[0] as! NSArray 370 | for i in 0.. Bool { 549 | return AVCaptureDevice.authorizationStatus(for: .video) == AVAuthorizationStatus.authorized 550 | } 551 | 552 | private func AuthSettingOpen(AuthString: String) { 553 | if !CameraAuth(){ 554 | if let AppName = Bundle.main.infoDictionary!["CFBundleName"] as? String { 555 | let message = "If didn't allow the camera permission, \r\n Would like to go to the Setting Screen?".localized 556 | let alert = UIAlertController(title: "Setting".localized, message: message, preferredStyle: .alert) 557 | 558 | let cancle = UIAlertAction(title: "Cancel".localized, style: .default) { _ in } 559 | let confirm = UIAlertAction(title: "Confirm".localized, style: .default) { (UIAlertAction) in 560 | UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) 561 | } 562 | alert.addAction(cancle) 563 | alert.addAction(confirm) 564 | 565 | self.present(alert, animated: true, completion: nil) 566 | } 567 | } 568 | } 569 | } 570 | 571 | --------------------------------------------------------------------------------