├── 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 | ||
|
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 |
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 |
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 |
--------------------------------------------------------------------------------