人工智慧程式基礎-影像辨識篇
-
人工智慧軟體的目標是希望賦予機器「人的智慧」,而人的智慧從視覺獲得的最多,因此,AI視覺除了要辨識最重要的人體之外,也要認識世間萬物,才能妥善為人類服務 — 這就是影像分類的目的。
影像分類(Image Classification)在電腦視覺或整個AI中,可能是應用最廣泛的技術,在實際生活中影像分類比人臉辨識更常用,不管是停車場的車牌辨識、工廠裏的瑕疵檢測、醫療影像協助診斷、農業病蟲害防治、無人車自動駕駛…等,在各行各業幾乎都能找到影像分類的應用場景。
但世界上萬事萬物那麼多,不管是天然的,還是人造的,AI 不可能一下子全部認得,要從哪些東西學起呢?我們先來看看 Apple 原廠教 AI 認識哪些東西。
-
內建的基本分類
用 Swift 寫AI影像分類的程式相當簡單,如前兩課一樣,只要將工作請求改用 VNClassifyImageRequest 即可,這會根據 Apple 內建的分類去判讀影像。不過,影像分類較難理解之處並非程式語法,而是執行結果。怎麼說呢?
我們先來看一個範例,才能進一步理解影像分類在做什麼。下圖取自Unsplash網站(來源網址請參考註解),你看到什麼呢?在這張照片中,AI 影像分類共找出15類物品:
這些分類跟你心裡所想的一樣嗎?一般人看到這張圖片,大概會說「在倒茶」或「茶壺」,不過 AI 影像分類並不是解釋圖片內容或動作,而是像一個初生嬰兒,試圖去認識周遭世界。
影像分類會與已知分類相比較,設法辨認影像中所有物品,標示可能有哪些物品類別(及機率)。在此例中,影像分類結果包括廚具(84.8%)、炊具(80.4%)、茶壺(80.4%)、餐具(64.3%)…等15類,我們心裡所想的「茶壺」,只排名第3。
原來,用VNClassifyImageRequest工作請求所得到的分類,是比對 Apple 原廠預先訓練好的物品集(稱為資料模型 Data Model),目前共包含1300多種分類,以樹狀結構安排大類、小類,如廚具(大類)包含炊具、餐具、茶壺…等。
所以,影像分類就像自動幫照片加上「#標籤」,替我們解讀照片內容,依照預先訓練的物品種類加以分門別類,這樣以後要找出某些內容的照片,就方便多了。
再看另外兩個執行結果,以下這張相當溫馨的室內照片分類結果有20個標籤(類別):
注意影像分類與視覺焦點無關,上圖我最喜歡的,是那張給人溫暖感覺的橘色沙發椅(排名第9,信心度才31.1%),而不是信心度99.4%的植物,也就是說,影像分類並非以重要性排序,而是正確率,這與人看東西的角度不一樣。
再看一張傍晚的城市街道(美國舊金山),分類結果如下:
戶外、道路、燈光…這些分類客觀上都非常正確,但這張照片是經典的舊金山街景,其中最吸引人的是遠方舊金山連接奧克蘭的海灣大橋,反而沒有辨識出來(或信心度太低)。
從這些執行範例可以看出,VNClassifyImageRequest 能夠辨識大部分常見的物品,例如室內家居用品、戶外城市街道、食衣住行、大自然景色…等,基本上Apple 的策略是以人為中心,先教AI認識人的周邊常出現的物品類別,這個策略相當好。
但要如何應用在實際場景呢?老實說,這個內建的影像分類並不容易應用,因為辨識出來的類別雖多,不確定的機率也高,想想看我們使用搜尋引擎的經驗,若給出太多不確定的結果,反而要花時間一一過濾,並非好事。
儘管如此,這是最簡單的影像分類方式(有現成的物品分類),程式寫起來非常容易,將第1段改寫如下,類似寫法已重複多次,應該駕輕就熟了:
import Vision struct 物品信心度 { let 名稱: String let 信心度: Float } // 第1段 func 影像分類(_ 圖片參數: UIImage) async throws -> [物品信心度] { var 回傳結果: [物品信心度] = [] let 工作請求 = VNClassifyImageRequest() // let 物品清單 = try 工作請求.supportedIdentifiers() // print(物品清單.count) if let 圖像 = 圖片參數.cgImage { let 處理者 = VNImageRequestHandler(cgImage: 圖像) try 處理者.perform([工作請求]) if let 分類結果 = 工作請求.results { // as? [VNClassificationObservation] for i in 分類結果 { if i.confidence > 0.1 { let 物品 = 物品信心度(名稱: i.identifier, 信心度: i.confidence) 回傳結果.append(物品) } } } } if 回傳結果.isEmpty { 回傳結果.append(物品信心度(名稱: "", 信心度: 0.0)) } print("回傳結果:\(回傳結果)") return 回傳結果 }
由於影像分類的處理結果,有兩個屬性需要回傳:(1) identifier 物品類別名稱 (2) confidence 信心度,因此,我們先定義一個資料類型「物品信心度」,容納這兩欄屬性(名稱、信心度)。
其實不管輸入任何圖片,VNClassifyImageRequest 的處理結果都會產生同樣1300多個標籤(分類名稱),只是每個標籤的信心度不同而已(大部分會等於0),所以我們必須篩選信心度大於一定機率(如0.1)者,組成 [物品信心度] 陣列回傳。
接下來,第4段原本畫出特徵點,在這裡改為輸出字串陣列,其中信心度用一個函式改成 % 百分比格式(如原始資料 0.914320222 轉換為 “91.4%”):
// 第4段 struct 列出物品: View { let 陣列: [物品信心度] var body: some View { VStack(alignment: .leading) { ForEach(陣列.indices, id: \.self) { i in Text("\(i + 1). \(陣列[i].名稱) (\(百分比(陣列[i].信心度)))") } } .padding() } func 百分比(_ f: Float) -> String { let f2 = f * 100.0 let p = String(format: "%.1f", f2) return p + "%" } }
最後合併成完整程式即可:
// 5-4a Image Classification // Created by Philip, Heman, Jean, 2023/03/12 // Revised by Jean, 2025/05/27 // Ref. WWDC 2019 "Understanding Images in Vision Framework" import SwiftUI import PhotosUI import Vision struct 物品信心度 { let 名稱: String let 信心度: Float } // 第1段 func 影像分類(_ 圖片參數: UIImage) async throws -> [物品信心度] { var 回傳結果: [物品信心度] = [] let 工作請求 = VNClassifyImageRequest() // let 物品清單 = try 工作請求.supportedIdentifiers() // print(物品清單.count) if let 圖像 = 圖片參數.cgImage { let 處理者 = VNImageRequestHandler(cgImage: 圖像) try 處理者.perform([工作請求]) if let 分類結果 = 工作請求.results { // as? [VNClassificationObservation] for i in 分類結果 { if i.confidence > 0.1 { let 物品 = 物品信心度(名稱: i.identifier, 信心度: i.confidence) 回傳結果.append(物品) } } } } if 回傳結果.isEmpty { 回傳結果.append(物品信心度(名稱: "", 信心度: 0.0)) } print("回傳結果:\(回傳結果)") return 回傳結果 } // 第2段 // Updated by Heman, 2024/12/24. 重新改寫 struct 相簿單選: View { @State var 單選: PhotosPickerItem? @Binding var 圖片: UIImage? var body: some View { if 圖片 == nil { PhotosPicker(selection: $單選) { VStack { Image(systemName: "barcode.viewfinder") .resizable() .scaledToFit() Text("請點選條碼照片") .font(.title) } } .photosPickerStyle(.inline) .photosPickerAccessoryVisibility(.hidden, edges: .leading) .onChange(of: 單選) { 選擇結果 in Task { do { if let 原始資料 = try await 選擇結果?.loadTransferable(type: Data.self) { if let 轉換圖片 = UIImage(data: 原始資料) { 圖片 = 轉換圖片 } } } catch { print("無法取得或轉換照片: \(error)") 圖片 = nil } } } } else { Image(uiImage: 圖片!) .resizable() .scaledToFit() } } } // 第3段 struct 照片掃描: View { @State var 物品列表: [物品信心度] = [] @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(列出物品(陣列: 物品列表), alignment: .topLeading) // 第4段 .onTapGesture { 相簿圖片 = nil 物品列表 = [] } if 物品列表.isEmpty { ProgressView() .scaleEffect(2.5) } } } Spacer() } } // 第4段 struct 列出物品: View { let 陣列: [物品信心度] var 項次: Int = 0 var body: some View { VStack(alignment: .leading) { ForEach(陣列.indices, id: \.self) { i in Text("\(i + 1). \(陣列[i].名稱) (\(百分比(陣列[i].信心度)))") } } .padding() } func 百分比(_ f: Float) -> String { let f2 = f * 100.0 let p = String(format: "%.1f", f2) return p + "%" } } // 第5段 struct 網址抓圖: View { @Binding var 圖片: UIImage? @State var 網址: String = "" 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("網址格式錯誤,請重新輸入。") } } } } .padding() } } } import PlaygroundSupport PlaygroundPage.current.setLiveView(照片掃描())
本節是常用物品的基本分類,採用 Apple 制定的1300種分類,好處是已在所有 Apple 產品內建這些分類,省去很多麻煩,辨識正確率也相當高,缺點則是無法客製化(如增加新類別或中文化)。
若想將影像分類應用到實際場景,通常須先確定影像類型以及想要什麼分類結果,例如停車場要辨認車牌的英文及數字、工廠想確認製品是否有裂紋、醫療影像(如X光片、超音波或MRI)想診斷有沒有疾病癥狀、海洋生物學家想標示每隻海豚…等,一旦確定分類結果,再去收集各類影像樣本,最後經由機器學習軟體產出資料模型,這樣才能讓影像分類的結果,符合實際需求。
💡 本節範例圖片網址
1. https://images.unsplash.com/photo-1675232348914-95f62960f4f9?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80
2. https://images.unsplash.com/photo-1618220179428-22790b461013?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=654&q=80
3. [https://images.unsplash.com/photo-1494257610566-28280a243b22?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80](https://images.unsplash.com/photo-1494257610566-28280a243b22?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=387&q=80) -
什麼是AI模型?
從上一單元了解到,影像分類若要符合各行各業的需求,必須想辦法訓練自己的的資料模型,資料模型相當於 AI 背後的知識庫,若模型有欠缺,目前的 AI 是無法自行推理補足的。
所謂訓練自己的資料模型,以影像分類為例,是指自己去蒐集相關的圖片資料合輯(稱為 data set),人工加註標籤或分類,再使用機器學習(machine learning)軟體,萃取出資料的類型特徵,最後的產出就稱為AI的資料模型(data model)。
是否一定得自己訓練才能獲得資料模型呢?也未必,最近幾年機器學習的發展非常迅速,網路已有不少前人訓練好的模型開放出來,可以從 [Model Zoo](https://modelzoo.co/) 或類似的網站下載。
不過還有個問題,網路上 AI 模型的檔案格式並不一致,Apple 目前使用的格式稱為 Core ML (副檔名為 .mlmodel,ml 是 machine learning 的縮寫),而網路上很多的是 Google 的 TensorFlow 或用於 Python 的 PyTorch 格式,模型下載後須轉換為 .mlmodel 才能使用。
[Apple 官網](https://developer.apple.com/machine-learning/models/)已有一些轉換為 .mlmodel 格式的第三方模型,其中 MobileNetV2 是 Google 開放出來的影像分類模型,輕薄短小只有24.7MB,很適合手機使用,本單元就利用這個來練習一下。
在 Swift Playgrounds 裡面如何使用 .mlmodel ?網路上似乎缺乏這種案例,經筆者一番嘗試終於成功,須經過兩個前置處理:(1) 下載模型 (2) 編譯模型,才能用於影像分類的工作請求。
我們先寫兩個函式進行前置處理,第一個「下載模型」使用檔案管理員(FileManager)物件,將下載的 .mlmodel 模型暫存到檔案裡面。
// 5-4b AI模型(with CoreML) // Created by Philip, Heman, Jean, 2023/03/17 // Revised by Jean, 2025/06/02 import Foundation // 第6段 func 下載模型(_ 網址: String) async throws -> URL? { if let myURL = URL(string: 網址) { let 檔案名稱 = myURL.lastPathComponent let 檔案管理員 = FileManager() if let 目錄 = 檔案管理員.urls( for: .cachesDirectory, in: .userDomainMask).first { let 存檔路徑 = 目錄.path + "/" + 檔案名稱 print("下載目標:\(myURL)\n存檔標的:\(存檔路徑)") if 檔案管理員.fileExists(atPath: 存檔路徑) { print("檔案已下載過") } else { let (原始資料, 錯誤碼) = try await URLSession.shared.data(from: myURL) print("下載成功:\(原始資料)", 錯誤碼) _ = 檔案管理員.createFile(atPath: 存檔路徑, contents: 原始資料) } let 回傳值 = 目錄.appendingPathComponent(檔案名稱) print("回傳模型位址:\(回傳值)") return 回傳值 } } return nil }
這個函式的輸入參數為模型網址,如:
https://ml-assets.apple.com/coreml/models/Image/ImageClassification/MobileNetV2/MobileNetV2.mlmodel
將此網址最後一部分(.lastPathComponent)抓出來指定為「檔案名稱」,此時檔案名稱為 “MobileNetV2.mlmodel”。
接下來取得檔案管理員(FileManager)物件,指定「目錄」為使用者的 .cacheDirectory,這是每個 App 的暫存目錄(App 關閉後一段時間會自動清除),用來存放模型檔案:
let 檔案管理員 = FileManager() if let 目錄 = 檔案管理員.urls( for: .cachesDirectory, in: .userDomainMask).first {...}
這是我們課程中第一次用到檔案管理員(FileManager)物件,其實使用上非常簡單,就跟平常的檔案與目錄操作類似,只是滑鼠操作改為程式呼叫。
接下來「目錄 + 檔案名稱」就是「存檔路徑」,如果檔案不存在(之前未下載過),就用 URLSession 下載檔案,再用檔案管理員.createFile() 儲存起來,並回傳存檔路徑的 URL:
let 存檔路徑 = 目錄.path + "/" + 檔案名稱 if 檔案管理員.fileExists(atPath: 存檔路徑) { print("檔案已下載過") } else { let (原始資料, 錯誤碼) = try await URLSession.shared.data(from: myURL) print("下載成功:\(原始資料)", 錯誤碼) _ = 檔案管理員.createFile(atPath: 存檔路徑, contents: 原始資料) } let 回傳值 = 目錄.appendingPathComponent(檔案名稱)
第二個函式「編譯模型」也是類似的過程,只是將下載(URLSession)的動作改成編譯(MLModel.compileModel),模型編譯需要先導入 CoreML 框架,裡面的物件名稱大多以 ML 開頭,如 MLModel。
import Foundation import CoreML // 第7段 func 編譯模型(_ 模型位址: URL) async throws -> URL? { let 檔名 = 模型位址.lastPathComponent + "c" let 編譯目錄 = 模型位址.deletingLastPathComponent() let 存檔位址 = 編譯目錄.appendingPathComponent(檔名) print("編譯目錄:\(編譯目錄)\n存檔位址:\(存檔位址)") let 檔案管理員 = FileManager() if 檔案管理員.fileExists(atPath: 存檔位址.path) { print("模型已編譯過") } else { let 編譯檔 = try await MLModel.compileModel(at: 模型位址) print("編譯成功:\(編譯檔)") try 檔案管理員.moveItem(at: 編譯檔, to: 存檔位址) } return 存檔位址 }
最後同樣回傳編譯過的存檔位址(URL),注意這個 URL 是在本機的檔案目錄中(才能導入 MLModel)。
到此就可以跟之前一樣使用這個外來的模型,有什麼不一樣呢?先看看第1段修改後的程式:
import Vision import CoreML struct 物品信心度 { let 名稱: String let 信心度: Float } // 第1段 func 影像分類v2(_ 圖片參數: UIImage, 模型參數: URL) async throws -> [物品信心度] { var 回傳結果: [物品信心度] = [] let 影像模型 = try MLModel(contentsOf: 模型參數) print(影像模型.modelDescription) let 工作請求 = VNCoreMLRequest(model: try VNCoreMLModel(for: 影像模型)) print(工作請求) if let 圖像 = 圖片參數.cgImage { let 處理者 = VNImageRequestHandler(cgImage: 圖像) try 處理者.perform([工作請求]) if let 分類結果 = 工作請求.results as? [VNClassificationObservation] { // print(分類結果) for i in 分類結果 { if i.confidence > 0.05 { let 物品 = 物品信心度(名稱: i.identifier, 信心度: i.confidence) 回傳結果.append(物品) } } } } if 回傳結果.isEmpty { 回傳結果.append(物品信心度(名稱: "", 信心度: 0.0)) } print("回傳結果:\(回傳結果)") return 回傳結果 }
「影像分類v2」多一個 URL 參數,用來承接「編譯模型()」所回傳的結果,將此 URL 參數傳入 MLModel() 轉為模型物件,再將此模型物件傳給 VNCoreMLRequest() 產出工作請求:
let 影像模型 = try MLModel(contentsOf: 模型參數) let 工作請求 = VNCoreMLRequest(model: try VNCoreMLModel(for: 影像模型))
也就是說,使用第三方(外來或客製)模型,最大的區別是工作請求須改用 VNCoreMLRequest(),處理者同樣是 VNImageRequestHandler,其他地方幾乎沒有兩樣。
到此,工作請求與處理者的關係,我們又進一步了解,第三方模型是送入工作請求(而不是處理者),所以之前的工作請求背後都是有預設模型,影像分類有影像分類的預設模型、臉部辨識有臉部辨識的預設模型、文字辨識有文字辨識的模型…等等,如下圖:
最後,只需要在第3段稍加修改,將新寫的兩個函式(第6段、第7段)整合進來即可,因為下載與編譯只須做一次,所以放在 .task 裡面,而影像分類則在每次圖片更新後都要用到,故放在 .onChange 裡面:
// 第3段 struct 照片掃描: View { @State var 相簿圖片: UIImage? @State var 物品列表: [物品信心度] = [] @State var 已編譯模型: URL? let 模型網址 = "https://ml-assets.apple.com/coreml/models/Image/ImageClassification/MobileNetV2/MobileNetV2.mlmodel" var body: some View { 網址抓圖(圖片: $相簿圖片) // 第5段 .task { do { if let 模型檔 = try await 下載模型(模型網址) { // 第6段 if let 編譯檔 = try await 編譯模型(模型檔) { // 第7段 已編譯模型 = 編譯檔 } } } catch { print("無法辨識圖片:\(error)") } } .onChange(of: 相簿圖片) { 新圖片 in 物品列表 = [] Task { do { if 已編譯模型 != nil && 新圖片 != nil { 物品列表 = try await 影像分類v2(新圖片!, 模型參數: 已編譯模型!) // 第1段 } else { print("模型尚未編譯完成") } } catch { print("無法辨識圖片:\(error)") } } } ... }
最後也將第3段主視圖與標籤文字的外觀稍加修改,執行結果如下:
待插入一張圖片
圖片中的食物辨識出甜椒(bell pepper)、小黃瓜(cucumber)、櫛瓜(zucchini)、橡果南瓜(acorn squash),只有甜椒和櫛瓜正確,另外馬鈴薯、紅蘿蔔、甜菜、番茄沒有辨認出來。
最後完整的程式碼已超過200行,共分為7段:
// 5-4b AI模型(with CoreML) // Created by Philip, Heman, Jean, 2023/03/17 // Revised by Jean, 2025/06/02 import SwiftUI import PhotosUI import Vision import CoreML struct 物品信心度 { let 名稱: String let 信心度: Float } // 第1段 func 影像分類v2(_ 圖片參數: UIImage, 模型參數: URL) async throws -> [物品信心度] { var 回傳結果: [物品信心度] = [] let 影像模型 = try MLModel(contentsOf: 模型參數) print(影像模型.modelDescription) let 工作請求 = VNCoreMLRequest(model: try VNCoreMLModel(for: 影像模型)) print(工作請求) if let 圖像 = 圖片參數.cgImage { let 處理者 = VNImageRequestHandler(cgImage: 圖像) try 處理者.perform([工作請求]) if let 分類結果 = 工作請求.results as? [VNClassificationObservation] { // print(分類結果) for i in 分類結果 { if i.confidence > 0.05 { let 物品 = 物品信心度(名稱: i.identifier, 信心度: i.confidence) 回傳結果.append(物品) } } } } if 回傳結果.isEmpty { 回傳結果.append(物品信心度(名稱: "", 信心度: 0.0)) } print("回傳結果:\(回傳結果)") return 回傳結果 } // 第2段 // Updated by Heman, 2024/12/24. 重新改寫 struct 相簿單選: View { @State var 單選: PhotosPickerItem? @Binding var 圖片: UIImage? var body: some View { if 圖片 == nil { PhotosPicker(selection: $單選) { VStack { Image(systemName: "barcode.viewfinder") .resizable() .scaledToFit() Text("請點選條碼照片") .font(.title) } } .photosPickerStyle(.inline) .photosPickerAccessoryVisibility(.hidden, edges: .leading) .onChange(of: 單選) { 選擇結果 in Task { do { if let 原始資料 = try await 選擇結果?.loadTransferable(type: Data.self) { if let 轉換圖片 = UIImage(data: 原始資料) { 圖片 = 轉換圖片 } } } catch { print("無法取得或轉換照片: \(error)") 圖片 = nil } } } } else { Image(uiImage: 圖片!) .resizable() .scaledToFit() } } } // 第3段 struct 照片掃描: View { @State var 相簿圖片: UIImage? @State var 物品列表: [物品信心度] = [] @State var 已編譯模型: URL? let 模型網址 = "https://ml-assets.apple.com/coreml/models/Image/ImageClassification/MobileNetV2/MobileNetV2.mlmodel" var body: some View { 網址抓圖(圖片: $相簿圖片) // 第5段 .task { do { if let 模型檔 = try await 下載模型(模型網址) { // 第6段 if let 編譯檔 = try await 編譯模型(模型檔) { // 第7段 已編譯模型 = 編譯檔 } } } catch { print("無法辨識圖片:\(error)") } } .onChange(of: 相簿圖片) { 新圖片 in 物品列表 = [] Task { do { if 已編譯模型 != nil && 新圖片 != nil { 物品列表 = try await 影像分類v2(新圖片!, 模型參數: 已編譯模型!) // 第1段 } else { print("模型尚未編譯完成") } } catch { print("無法辨識圖片:\(error)") } } } Spacer() if 相簿圖片 == nil { 相簿單選(圖片: $相簿圖片) // 第2段 } else { ZStack { Image(uiImage: 相簿圖片!) .resizable() .scaledToFit() // .border(Color.secondary) // .opacity(0.5) // 將圖片淡化作為底圖 .overlay(列出物品(陣列: 物品列表), alignment: .bottomLeading) // 第4段 .onTapGesture { 相簿圖片 = nil 物品列表 = [] } if 物品列表.isEmpty || 已編譯模型 == nil { ProgressView() .scaleEffect(2.5) } } } Spacer() } } // 第4段 struct 列出物品: View { let 陣列: [物品信心度] var 項次: Int = 0 var body: some View { VStack(alignment: .leading) { ForEach(陣列.indices, id: \.self) { i in Text("\(i + 1). \(陣列[i].名稱) (\(百分比(陣列[i].信心度)))") .foregroundColor(.white) .shadow(color: .indigo, radius: 3, x: .zero, y: .zero) } } .padding() } func 百分比(_ f: Float) -> String { let f2 = f * 100.0 let p = String(format: "%.1f", f2) return p + "%" } } // 第5段 struct 網址抓圖: View { @Binding var 圖片: UIImage? @State var 網址: String = "" 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("網址格式錯誤,請重新輸入。") } } } } .padding() } } } // 第6段 func 下載模型(_ 網址: String) async throws -> URL? { if let myURL = URL(string: 網址) { let 檔案名稱 = myURL.lastPathComponent let 檔案管理員 = FileManager() if let 目錄 = 檔案管理員.urls( for: .cachesDirectory, in: .userDomainMask).first { let 存檔路徑 = 目錄.path + "/" + 檔案名稱 print("下載目標:\(myURL)\n存檔標的:\(存檔路徑)") if 檔案管理員.fileExists(atPath: 存檔路徑) { print("檔案已下載過") } else { let (原始資料, 錯誤碼) = try await URLSession.shared.data(from: myURL) print("下載成功:\(原始資料)", 錯誤碼) _ = 檔案管理員.createFile(atPath: 存檔路徑, contents: 原始資料) } let 回傳值 = 目錄.appendingPathComponent(檔案名稱) print("回傳模型位址:\(回傳值)") return 回傳值 } } return nil } // 第7段 func 編譯模型(_ 模型位址: URL) async throws -> URL? { let 檔名 = 模型位址.lastPathComponent + "c" let 編譯目錄 = 模型位址.deletingLastPathComponent() let 存檔位址 = 編譯目錄.appendingPathComponent(檔名) print("編譯目錄:\(編譯目錄)\n存檔位址:\(存檔位址)") let 檔案管理員 = FileManager() if 檔案管理員.fileExists(atPath: 存檔位址.path) { print("模型已編譯過") } else { let 編譯檔 = try await MLModel.compileModel(at: 模型位址) print("編譯成功:\(編譯檔)") try 檔案管理員.moveItem(at: 編譯檔, to: 存檔位址) } return 存檔位址 } import PlaygroundSupport PlaygroundPage.current.setLiveView(照片掃描())
-
內建的基本分類