Skip to content

Commit 144bfed

Browse files
Initial commit for Leaf Lens project
1 parent 092f4b5 commit 144bfed

11 files changed

Lines changed: 574 additions & 24 deletions

File tree

Leaf Lens.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,8 @@
256256
DEVELOPMENT_TEAM = 7RPVG8UJGR;
257257
ENABLE_PREVIEWS = YES;
258258
GENERATE_INFOPLIST_FILE = YES;
259+
INFOPLIST_KEY_NSCameraUsageDescription = "Leaf Leans needs camera access to identify plants";
260+
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Leaf Leans needs photo library access to select plant images";
259261
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
260262
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
261263
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -284,6 +286,8 @@
284286
DEVELOPMENT_TEAM = 7RPVG8UJGR;
285287
ENABLE_PREVIEWS = YES;
286288
GENERATE_INFOPLIST_FILE = YES;
289+
INFOPLIST_KEY_NSCameraUsageDescription = "Leaf Leans needs camera access to identify plants";
290+
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Leaf Leans needs photo library access to select plant images";
287291
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
288292
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
289293
INFOPLIST_KEY_UILaunchScreen_Generation = YES;

Leaf Lens/ContentView.swift

Lines changed: 0 additions & 24 deletions
This file was deleted.

Leaf Lens/Models/DataStore.swift

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//
2+
// DataStore.swift
3+
// Leaf Lens
4+
//
5+
// Created by Navin Rai on 26/06/25.
6+
//
7+
8+
import Foundation
9+
import UIKit
10+
11+
class DataStore {
12+
private let observationsKey = "savedPlantObservations"
13+
private let fileManager = FileManager.default
14+
15+
// MARK: - Observation Handling
16+
17+
func saveObservations(_ observations: [Observation]) {
18+
if let encoded = try? JSONEncoder().encode(observations) {
19+
UserDefaults.standard.set(encoded, forKey: observationsKey)
20+
}
21+
}
22+
23+
func loadObservations() -> [Observation] {
24+
if let savedData = UserDefaults.standard.data(forKey: observationsKey) {
25+
if let decodedObservations = try? JSONDecoder().decode([Observation].self, from: savedData) {
26+
return decodedObservations
27+
}
28+
}
29+
return []
30+
}
31+
32+
// MARK: - Image Handling
33+
34+
private func getDocumentsDirectory() -> URL {
35+
fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
36+
}
37+
38+
func saveImageToDocumentsDirectory(image: UIImage, withName name: String) -> Bool {
39+
guard let data = image.pngData() else { return false }
40+
let url = getDocumentsDirectory().appendingPathComponent(name)
41+
do {
42+
try data.write(to: url)
43+
return true
44+
} catch {
45+
print("Error saving image: \(error.localizedDescription)")
46+
return false
47+
}
48+
}
49+
50+
func loadImageFromDocumentsDirectory(imageName: String) -> UIImage? {
51+
let url = getDocumentsDirectory().appendingPathComponent(imageName)
52+
guard fileManager.fileExists(atPath: url.path) else {
53+
print("Image file does not exist at path: \(url.path)")
54+
return nil
55+
}
56+
do {
57+
let data = try Data(contentsOf: url)
58+
return UIImage(data: data)
59+
} catch {
60+
print("Error loading image: \(error.localizedDescription)")
61+
return nil
62+
}
63+
}
64+
65+
func deleteImageFromDocumentsDirectory(imageName: String) {
66+
let fileURL = getDocumentsDirectory().appendingPathComponent(imageName)
67+
do {
68+
try fileManager.removeItem(at: fileURL)
69+
print("Deleted image: \(fileURL.lastPathComponent)")
70+
} catch {
71+
print("Could not delete image: \(error.localizedDescription)")
72+
}
73+
}
74+
}

Leaf Lens/Models/Model.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//
2+
// Model.swift
3+
// Leaf Lens
4+
//
5+
// Created by Navin Rai on 26/06/25.
6+
//
7+
8+
import Foundation
9+
import UIKit // UIImage ke liye
10+
11+
struct Observation: Identifiable, Codable {
12+
var id = UUID() // Unique ID har observation ke liye
13+
let imageName: String // Image ko file system mein save karne ke liye naam
14+
let plantName: String
15+
let confidence: String
16+
let date: Date // Date of Scan
17+
18+
// Codable protocols ke liye custom initializers
19+
enum CodingKeys: String, CodingKey {
20+
case id, imageName, plantName, confidence, date
21+
}
22+
23+
init(imageName: String, plantName: String, confidence: String, date: Date) {
24+
self.imageName = imageName
25+
self.plantName = plantName
26+
self.confidence = confidence
27+
self.date = date
28+
}
29+
30+
init(from decoder: Decoder) throws {
31+
let container = try decoder.container(keyedBy: CodingKeys.self)
32+
id = try container.decode(UUID.self, forKey: .id)
33+
imageName = try container.decode(String.self, forKey: .imageName)
34+
plantName = try container.decode(String.self, forKey: .plantName)
35+
confidence = try container.decode(String.self, forKey: .confidence)
36+
date = try container.decode(Date.self, forKey: .date)
37+
}
38+
39+
func encode(to encoder: Encoder) throws {
40+
var container = encoder.container(keyedBy: CodingKeys.self)
41+
try container.encode(id, forKey: .id)
42+
try container.encode(imageName, forKey: .imageName)
43+
try container.encode(plantName, forKey: .plantName)
44+
try container.encode(confidence, forKey: .confidence)
45+
try container.encode(date, forKey: .date)
46+
}
47+
}
23.6 MB
Binary file not shown.
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
//
2+
// ContentViewModel.swift
3+
// Leaf Lens
4+
//
5+
// Created by Navin Rai on 26/06/25.
6+
//
7+
8+
import Foundation
9+
import SwiftUI
10+
import CoreML
11+
import Vision
12+
13+
class ContentViewModel: ObservableObject {
14+
@Published var selectedImage: UIImage?
15+
@Published var plantName: String = "Identified As: "
16+
@Published var confidence: String = "Confidence: "
17+
@Published var showingImagePicker: Bool = false
18+
@Published var showingAlert: Bool = false
19+
@Published var alertMessage: String = ""
20+
@Published var navigateToSavedObservations: Bool = false
21+
22+
private let dataStore = DataStore() // DataStore instance
23+
24+
// MARK: - Image Selection & Core ML Logic
25+
26+
func processImage() {
27+
guard let inputImage = selectedImage else {
28+
plantName = "Identified As: No image selected"
29+
confidence = "Confidence: N/A"
30+
return
31+
}
32+
33+
guard let ciImage = CIImage(image: inputImage) else {
34+
alertMessage = "Could not convert UIImage to CIImage."
35+
showingAlert = true
36+
return
37+
}
38+
39+
// Replace MobileNetV2() with your actual Core ML model name
40+
guard let model = try? VNCoreMLModel(for: MobileNetV2().model) else {
41+
alertMessage = "Failed to load Core ML model. Make sure the .mlmodel file is added correctly and target membership is set."
42+
showingAlert = true
43+
return
44+
}
45+
46+
let request = VNCoreMLRequest(model: model) { [weak self] request, error in
47+
guard let self = self else { return }
48+
49+
if let error = error {
50+
DispatchQueue.main.async {
51+
self.alertMessage = "Image classification failed: \(error.localizedDescription)"
52+
self.showingAlert = true
53+
}
54+
return
55+
}
56+
57+
guard let observations = request.results as? [VNClassificationObservation] else {
58+
DispatchQueue.main.async {
59+
self.alertMessage = "Model returned no observations."
60+
self.showingAlert = true
61+
}
62+
return
63+
}
64+
65+
if let bestResult = observations.first {
66+
let identifier = bestResult.identifier.capitalized.replacingOccurrences(of: "_", with: " ")
67+
let confidenceScore = String(format: "%.2f%%", bestResult.confidence * 100)
68+
69+
DispatchQueue.main.async {
70+
self.plantName = "Identified As: \(identifier)"
71+
self.confidence = "Confidence: \(confidenceScore)"
72+
}
73+
} else {
74+
DispatchQueue.main.async {
75+
self.plantName = "Identified As: Unknown"
76+
self.confidence = "Confidence: Low"
77+
}
78+
}
79+
}
80+
81+
let handler = VNImageRequestHandler(ciImage: ciImage)
82+
DispatchQueue.global(qos: .userInitiated).async {
83+
do {
84+
try handler.perform([request])
85+
} catch {
86+
DispatchQueue.main.async {
87+
self.alertMessage = "Failed to perform classification: \(error.localizedDescription)"
88+
self.showingAlert = true
89+
}
90+
}
91+
}
92+
}
93+
94+
// MARK: - Save Logic
95+
96+
func saveCurrentObservation() {
97+
guard let imageToSave = selectedImage,
98+
plantName != "Identified As: ",
99+
plantName != "Identified As: No image selected",
100+
plantName != "Identified As: Unknown" else {
101+
alertMessage = "No valid plant identified to save."
102+
showingAlert = true
103+
return
104+
}
105+
106+
// Image ko file system mein save karein
107+
let imageName = UUID().uuidString + ".png" // Har baar ek naya unique filename
108+
if dataStore.saveImageToDocumentsDirectory(image: imageToSave, withName: imageName) {
109+
var allObservations = dataStore.loadObservations() // Load existing
110+
let newObservation = Observation(
111+
imageName: imageName,
112+
plantName: plantName,
113+
confidence: confidence,
114+
date: Date()
115+
)
116+
allObservations.append(newObservation)
117+
dataStore.saveObservations(allObservations) // Save updated list
118+
alertMessage = "Result saved successfully!"
119+
showingAlert = true
120+
121+
// --- UI State Reset ---
122+
123+
self.selectedImage = nil
124+
self.plantName = "Identified As: "
125+
self.confidence = "Confidence: "
126+
127+
} else {
128+
alertMessage = "Failed to save image."
129+
showingAlert = true
130+
}
131+
}
132+
133+
// MARK: - UI Actions (called by View)
134+
135+
func openImagePicker() {
136+
showingImagePicker = true
137+
}
138+
139+
func imagePickedAndProcessed() {
140+
processImage()
141+
}
142+
143+
func viewSavedResults() {
144+
navigateToSavedObservations = true
145+
}
146+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//
2+
// ObservationViewModel.swift
3+
// Leaf Lens
4+
//
5+
// Created by Navin Rai on 26/06/25.
6+
//
7+
8+
import Foundation
9+
import UIKit
10+
11+
class SavedObservationsViewModel: ObservableObject {
12+
@Published var observations: [Observation] = []
13+
private let dataStore = DataStore()
14+
15+
func loadObservations() {
16+
observations = dataStore.loadObservations()
17+
}
18+
19+
func deleteObservation(at offsets: IndexSet) {
20+
// Since we display observations.reversed(), need to adjust indices for deletion on original array
21+
let originalIndicesToDelete = offsets.map { observations.count - 1 - $0 }
22+
23+
// Delete images first
24+
for index in originalIndicesToDelete {
25+
let observationToDelete = observations[index]
26+
dataStore.deleteImageFromDocumentsDirectory(imageName: observationToDelete.imageName)
27+
}
28+
29+
// Remove from the array and save
30+
var currentObservations = dataStore.loadObservations()
31+
// Filter out observations based on the IDs of those to be deleted
32+
let idsToDelete = originalIndicesToDelete.map { observations[$0].id }
33+
currentObservations.removeAll { idsToDelete.contains($0.id) }
34+
35+
dataStore.saveObservations(currentObservations)
36+
loadObservations() // Reload to reflect changes
37+
}
38+
39+
// This is for the ObservationRow to load its image
40+
func loadImageForObservation(_ observation: Observation) -> UIImage? {
41+
dataStore.loadImageFromDocumentsDirectory(imageName: observation.imageName)
42+
}
43+
}

0 commit comments

Comments
 (0)