AI 手掌辨識
-
製作完一套具有列表的電子書,再來我們要使用 AI 來進 行手掌辨識,操作步驟如下:
- 點選畫面左方右上角的「+」號新增頁面,如下圖。
- 修改頁面的名稱,如下圖。
- 開始撰寫AI手掌辨識功能,請在 Playgounds 程式碼區輸入以下的程式:
// 手掌辨識符1 // 使用 Apple Swift Playgrounds // 圖片來源:Unsplash https://unsplash.com/photos/human-hand-qKspdY9XUzs // Created by Philip, Heman, Jean 2024/08/11 // Revised by Jean 2024/08/11 import SwiftUI import PhotosUI import Vision // 第1段 func 手掌細部辨識(_ 圖片參數: UIImage) async throws -> [CGPoint] { var 結果: [CGPoint] = [] let 工作請求 = VNDetectHumanHandPoseRequest() 工作請求.maximumHandCount = 5 if let 影像 = 圖片參數.cgImage { let 處理者 = VNImageRequestHandler(cgImage: 影像) try 處理者.perform([工作請求]) if let 處理結果 = 工作請求.results { // as? [VNHumanHandPoseObservation] print("處理結果:\(處理結果)") for 手掌 in 處理結果 { let 所有特徵點 = try 手掌.recognizedPoints(.all) // 字典(Dictionary)類型 let 手掌關節: [VNHumanHandPoseObservation.JointName] = [ .thumbTip, .thumbIP, .thumbMP, .thumbCMC, // 拇指 .indexTip, .indexDIP, .indexPIP, .indexMCP, // 食指 .middleTip, .middleDIP, .middlePIP, .middleMCP, // 中指 .ringTip, .ringDIP, .ringPIP, .ringMCP, // 無名指 .littleTip, .littleDIP, .littlePIP, .littleMCP, // 小指 .wrist // 腕關節 ] for i in 手掌關節 { if let 特徵點 = 所有特徵點[i] { if 特徵點.confidence > 0 { 結果.append(特徵點.location) // 正規化座標 } } } } } } if 結果.isEmpty { 結果.append(CGPoint.zero) } print("回傳結果:\(結果)") return 結果 } // 第2段 struct 相簿單選: View { @State var 單選: PhotosPickerItem? @Binding var 圖片: UIImage? var body: some View { PhotosPicker(selection: $單選) { if 圖片 == nil { VStack { Image(systemName: "photo") .resizable() .scaledToFit() .padding() Text("請點選相簿照片") .font(.title) } } else { Image(uiImage: 圖片!) .resizable() .scaledToFit() } } .onChange(of: 單選) { 選擇結果 in Task { do { if let 原始資料 = try await 選擇結果?.loadTransferable(type: Data.self) { if let 轉換圖片 = UIImage(data: 原始資料) { 圖片 = 轉換圖片 } } } catch { print("無法取得或轉換照片: \(error)") 圖片 = nil } } } } } // 第3段 struct 照片掃描: View { @State var 點座標陣列: [CGPoint] = [] @State var 相簿圖片: UIImage? = nil var body: some View { 網址抓圖(圖片: $相簿圖片) // 第5段 .onChange(of: 相簿圖片) { 新圖片 in 點座標陣列 = [] Task { do { 點座標陣列 = try await 手掌細部辨識(新圖片 ?? UIImage()) // 第1段 } catch { print("無法辨識圖片:\(error)") } } } Spacer() if 相簿圖片 == nil { 相簿單選(圖片: $相簿圖片) // 第2段 } else { ZStack() { Image(uiImage: 相簿圖片!) .resizable() .scaledToFit() .border(Color.secondary) .opacity(0.5) // 將圖片淡化作為底圖 .overlay(描繪特徵點(正規化點陣列: 點座標陣列)) // 第4段 .onTapGesture { 相簿圖片 = nil 點座標陣列 = [] } if 點座標陣列.isEmpty { ProgressView() .scaleEffect(2.5) } } } Spacer() } } // 第4段 struct 描繪特徵點: View { let 正規化點陣列: [CGPoint] var body: some View { Canvas { 圖層, 尺寸 in // print(尺寸) let 圖寬 = 尺寸.width let 圖高 = 尺寸.height var 畫筆 = Path() for 單點 in 正規化點陣列 { let 點座標 = CGPoint( x: 圖寬 * 單點.x, y: 圖高 - 圖高 * 單點.y) 畫筆.move(to: 點座標) 畫筆.addArc( center: 點座標, radius: 3.0, startAngle: .zero, endAngle: .degrees(360), clockwise: false) } 圖層.fill(畫筆, with: .color(.red)) } } } // 第5段 struct 網址抓圖: View { @Binding var 圖片: UIImage? @State var 網址: String = "https://images.unsplash.com/photo-1556848527-f7c548b972b2?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80" var body: some View { ZStack { Rectangle() .foregroundColor(.gray.opacity(0.5)) .frame(height: 50) HStack { Image(systemName: "photo.fill") .font(.system(size: 24)) TextField("輸入圖片網址", text: $網址, prompt: Text("https://")) .font(.system(size: 20)) .background(Color.white) .textFieldStyle(.roundedBorder) .cornerRadius(5.0) .onChange(of: 網址) { 新網址 in Task { if let myURL = URL(string: 新網址) { let (原始資料, _) = try await URLSession.shared.data(from: myURL) if let 格式轉換 = UIImage(data: 原始資料) { 圖片 = 格式轉換 } else { print("非圖片網址,請重新輸入。") } } else { print("網址格式錯誤,請重新輸入。") } } } .task { do { if let myURL = URL(string: 網址) { let (原始資料, _) = try await URLSession.shared.data(from: myURL) if let 格式轉換 = UIImage(data: 原始資料) { 圖片 = 格式轉換 } else { print("非圖片網址,請重新輸入。") } } else { print("網址格式錯誤,請重新輸入。") } } catch { print("有問題:\(error)") } } } .padding() } } } import PlaygroundSupport PlaygroundPage.current.setLiveView(照片掃描())
- 程式執行結果,如下圖。
- 在圖片上使用滑鼠左鍵按一下,會出現如下圖頁面。
你可以修改網址使用網路上的圖片,或是選擇你電腦中的圖片,或是可以使用電腦的攝影機即時拍攝一張圖片。
手掌辨識簡介
雙手是人類最靈巧的部位,也是與其他哺乳動物最大區別之一,即使在現代社會,雙手仍舊扮演非常重要的角色。在電影「鋼鐵人」中,相信大家都看過主角東尼·史塔克與人工智慧 Jarvis 互動,並以手勢操作電腦的畫面,令人印象深刻。在學過本課之後會發現,用手勢操作機器的未來,似乎離我們並不是很遠。
人的雙手有多少特徵點呢?我們每根指頭有3個關節,十指共有30個關節,再加上指尖與腕關節,雙手共計42個特徵點。
關於手掌細部辨識的程式碼如下:
// 第1段 func 手掌細部辨識(_ 圖片參數: UIImage) async throws -> [CGPoint] { var 結果: [CGPoint] = [] let 工作請求 = VNDetectHumanHandPoseRequest() 工作請求.maximumHandCount = 5 if let 影像 = 圖片參數.cgImage { let 處理者 = VNImageRequestHandler(cgImage: 影像) try 處理者.perform([工作請求]) if let 處理結果 = 工作請求.results { // as? [VNHumanHandPoseObservation] print("處理結果:\(處理結果)") for 手掌 in 處理結果 { let 所有特徵點 = try 手掌.recognizedPoints(.all) // 字典(Dictionary)類型 let 手掌關節: [VNHumanHandPoseObservation.JointName] = [ .thumbTip, .thumbIP, .thumbMP, .thumbCMC, // 拇指 .indexTip, .indexDIP, .indexPIP, .indexMCP, // 食指 .middleTip, .middleDIP, .middlePIP, .middleMCP, // 中指 .ringTip, .ringDIP, .ringPIP, .ringMCP, // 無名指 .littleTip, .littleDIP, .littlePIP, .littleMCP, // 小指 .wrist // 腕關節 ] for i in 手掌關節 { if let 特徵點 = 所有特徵點[i] { if 特徵點.confidence > 0 { 結果.append(特徵點.location) // 正規化座標 } } } } } } if 結果.isEmpty { 結果.append(CGPoint.zero) } print("回傳結果:\(結果)") return 結果 }
要辨識手掌的手勢,工作請求只須改用 VNDetectHumanHandPoseRequest,傳回的特徵點與上一節類似,同樣是字典類型,字典的「鍵」要用到手指關節名稱,如下圖:
手掌所有特徵點的索引鍵如下,每隻手掌有21個特徵點:
let 手掌關節: [VNHumanHandPoseObservation.JointName] = [ .thumbTip, .thumbIP, .thumbMP, .thumbCMC, // 拇指 .indexTip, .indexDIP, .indexPIP, .indexMCP, // 食指 .middleTip, .middleDIP, .middlePIP, .middleMCP, // 中指 .ringTip, .ringDIP, .ringPIP, .ringMCP, // 無名指 .littleTip, .littleDIP, .littlePIP, .littleMCP, // 小指 .wrist // 腕關節 ]
因此,第1段程式可改寫如下,同樣會回傳正規化的點座標陣列:
import Vision // 第1段 func 手掌細部辨識(_ 圖片參數: UIImage) async throws -> [CGPoint] { var 結果: [CGPoint] = [] let 工作請求 = VNDetectHumanHandPoseRequest() if let 影像 = 圖片參數.cgImage { let 處理者 = VNImageRequestHandler(cgImage: 影像) try 處理者.perform([工作請求]) if let 處理結果 = 工作請求.results { // as? [VNHumanHandPoseObservation] print("處理結果:\(處理結果)") for 手掌 in 處理結果 { let 所有特徵點 = try 手掌.recognizedPoints(.all) // 字典(Dictionary)類型 let 手掌關節: [VNHumanHandPoseObservation.JointName] = [ .thumbTip, .thumbIP, .thumbMP, .thumbCMC, // 拇指 .indexTip, .indexDIP, .indexPIP, .indexMCP, // 食指 .middleTip, .middleDIP, .middlePIP, .middleMCP, // 中指 .ringTip, .ringDIP, .ringPIP, .ringMCP, // 無名指 .littleTip, .littleDIP, .littlePIP, .littleMCP, // 小指 .wrist // 腕關節 ] for i in 手掌關節 { if let 特徵點 = 所有特徵點[i] { if 特徵點.confidence > 0 { 結果.append(特徵點.location) // 正規化座標 } } } } } } if 結果.isEmpty { 結果.append(CGPoint.zero) } print("回傳結果:\(結果)") return 結果 }
工作請求VNDetectHumanHandPoseRequest預設最多辨識兩隻手,若超過兩手,須修改工作請求的屬性,加一行程式:
工作請求.maximumHandCount = 5
我們試著用5隻手來做AI手掌辨識。
// 手掌辨識符2 // 使用 Apple Swift Playgrounds // 圖片來源:Unsplash https://unsplash.com/photos/human-hand-qKspdY9XUzs // Created by Philip, Heman, Jean 2024/08/11 // Revised by Jean 2024/08/11 import SwiftUI import PhotosUI import Vision // 第1段 func 手掌細部辨識(_ 圖片參數: UIImage) async throws -> [CGPoint] { var 結果: [CGPoint] = [] let 工作請求 = VNDetectHumanHandPoseRequest() 工作請求.maximumHandCount = 5 if let 影像 = 圖片參數.cgImage { let 處理者 = VNImageRequestHandler(cgImage: 影像) try 處理者.perform([工作請求]) if let 處理結果 = 工作請求.results { // as? [VNHumanHandPoseObservation] print("處理結果:\(處理結果)") for 手掌 in 處理結果 { let 所有特徵點 = try 手掌.recognizedPoints(.all) // 字典(Dictionary)類型 let 手掌關節: [VNHumanHandPoseObservation.JointName] = [ .thumbTip, .thumbIP, .thumbMP, .thumbCMC, // 拇指 .indexTip, .indexDIP, .indexPIP, .indexMCP, // 食指 .middleTip, .middleDIP, .middlePIP, .middleMCP, // 中指 .ringTip, .ringDIP, .ringPIP, .ringMCP, // 無名指 .littleTip, .littleDIP, .littlePIP, .littleMCP, // 小指 .wrist // 腕關節 ] for i in 手掌關節 { if let 特徵點 = 所有特徵點[i] { if 特徵點.confidence > 0 { 結果.append(特徵點.location) // 正規化座標 } } } } } } if 結果.isEmpty { 結果.append(CGPoint.zero) } print("回傳結果:\(結果)") return 結果 } // 第2段 struct 相簿單選: View { @State var 單選: PhotosPickerItem? @Binding var 圖片: UIImage? var body: some View { PhotosPicker(selection: $單選) { if 圖片 == nil { VStack { Image(systemName: "photo") .resizable() .scaledToFit() .padding() Text("請點選相簿照片") .font(.title) } } else { Image(uiImage: 圖片!) .resizable() .scaledToFit() } } .onChange(of: 單選) { 選擇結果 in Task { do { if let 原始資料 = try await 選擇結果?.loadTransferable(type: Data.self) { if let 轉換圖片 = UIImage(data: 原始資料) { 圖片 = 轉換圖片 } } } catch { print("無法取得或轉換照片: \(error)") 圖片 = nil } } } } } // 第3段 struct 照片掃描: View { @State var 點座標陣列: [CGPoint] = [] @State var 相簿圖片: UIImage? = nil var body: some View { 網址抓圖(圖片: $相簿圖片) // 第5段 .onChange(of: 相簿圖片) { 新圖片 in 點座標陣列 = [] Task { do { 點座標陣列 = try await 手掌細部辨識(新圖片 ?? UIImage()) // 第1段 } catch { print("無法辨識圖片:\(error)") } } } Spacer() if 相簿圖片 == nil { 相簿單選(圖片: $相簿圖片) // 第2段 } else { ZStack() { Image(uiImage: 相簿圖片!) .resizable() .scaledToFit() .border(Color.secondary) .opacity(0.5) // 將圖片淡化作為底圖 .overlay(描繪特徵點(正規化點陣列: 點座標陣列)) // 第4段 .onTapGesture { 相簿圖片 = nil 點座標陣列 = [] } if 點座標陣列.isEmpty { ProgressView() .scaleEffect(2.5) } } } Spacer() } } // 第4段 struct 描繪特徵點: View { let 正規化點陣列: [CGPoint] var body: some View { Canvas { 圖層, 尺寸 in // print(尺寸) let 圖寬 = 尺寸.width let 圖高 = 尺寸.height var 畫筆 = Path() for 單點 in 正規化點陣列 { let 點座標 = CGPoint( x: 圖寬 * 單點.x, y: 圖高 - 圖高 * 單點.y) 畫筆.move(to: 點座標) 畫筆.addArc( center: 點座標, radius: 3.0, startAngle: .zero, endAngle: .degrees(360), clockwise: false) } 圖層.fill(畫筆, with: .color(.red)) } } } // 第5段 struct 網址抓圖: View { @Binding var 圖片: UIImage? @State var 網址: String = "https://images.unsplash.com/photo-1556484687-30636164638b?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1548&q=80" var body: some View { ZStack { Rectangle() .foregroundColor(.gray.opacity(0.5)) .frame(height: 50) HStack { Image(systemName: "photo.fill") .font(.system(size: 24)) TextField("輸入圖片網址", text: $網址, prompt: Text("https://")) .font(.system(size: 20)) .background(Color.white) .textFieldStyle(.roundedBorder) .cornerRadius(5.0) .onChange(of: 網址) { 新網址 in Task { if let myURL = URL(string: 新網址) { let (原始資料, _) = try await URLSession.shared.data(from: myURL) if let 格式轉換 = UIImage(data: 原始資料) { 圖片 = 格式轉換 } else { print("非圖片網址,請重新輸入。") } } else { print("網址格式錯誤,請重新輸入。") } } } .task { do { if let myURL = URL(string: 網址) { let (原始資料, _) = try await URLSession.shared.data(from: myURL) if let 格式轉換 = UIImage(data: 原始資料) { 圖片 = 格式轉換 } else { print("非圖片網址,請重新輸入。") } } else { print("網址格式錯誤,請重新輸入。") } } catch { print("有問題:\(error)") } } } .padding() } } } import PlaygroundSupport PlaygroundPage.current.setLiveView(照片掃描())
- 程式執行結果,如下圖。