Skip to content

Commit 6ce4217

Browse files
author
Chris Greening
committed
Refactors and adds comments
1 parent 993a83e commit 6ce4217

10 files changed

Lines changed: 386 additions & 50 deletions

File tree

IrProCapture/Camera/Camera.swift

Lines changed: 66 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -12,40 +12,63 @@ import CoreImage
1212
import CoreImage.CIFilterBuiltins
1313
import CoreGraphics
1414

15-
enum IrProError: String, Error {
16-
case noDevicesFound = "No IR camera devices found."
17-
case failedToCreateDeviceInput = "Failed to create device input."
18-
case failedToAddToSession = "Could not add to capture session."
19-
case failedToAddOutput = "Failed to set video output."
20-
}
21-
22-
class Camera: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDelegate {
23-
// Published properties for UI updates
15+
/// A camera controller class that manages thermal imaging capture, processing, and recording.
16+
///
17+
/// The `Camera` class serves as the main controller for thermal imaging operations, handling:
18+
/// - Real-time thermal image capture and processing
19+
/// - Temperature data analysis and visualization
20+
/// - Image and video recording capabilities
21+
/// - Color map and orientation management
22+
///
23+
/// This class implements the `ObservableObject` protocol for SwiftUI integration and
24+
/// `CaptureDelegate` for handling camera capture events.
25+
class Camera: NSObject, ObservableObject, CaptureDelegate {
26+
// MARK: - Published Properties
27+
28+
/// The processed thermal image ready for display
2429
@Published var resultImage: CGImage? = nil
30+
31+
/// The minimum temperature detected in the current frame
2532
@Published var minTemperature: Float = 0
33+
34+
/// The maximum temperature detected in the current frame
2635
@Published var maxTemperature: Float = 0
36+
37+
/// The temperature at the center of the frame
2738
@Published var centerTemperature: Float = 0.0
39+
40+
/// The average temperature across the entire frame
2841
@Published var averageTemperature: Float = 0
42+
43+
/// The currently selected color map for thermal visualization
2944
@Published var currentColorMap: ColorMap {
3045
didSet {
3146
UserDefaults.standard.set(colorMaps.firstIndex(of: currentColorMap)!, forKey: "currentColorMap")
3247
}
3348
}
49+
50+
/// The current orientation setting for the thermal image
3451
@Published var currentOrientation: OrientationOption {
3552
didSet {
3653
UserDefaults.standard.set(orientationOptions.firstIndex(of: currentOrientation)!, forKey: "currentRotation")
3754
}
3855
}
56+
57+
/// Indicates whether the camera is currently running
3958
@Published var isRunning = false
59+
60+
/// Indicates whether video recording is in progress
4061
@Published var isRecording = false
62+
63+
/// Temperature distribution data for histogram visualization
4164
@Published var histogram: [HistogramPoint] = []
4265

4366
// Private components
4467
private let ciContext = CIContext()
45-
private let captureSession = AVCaptureSession()
4668
private let temperatureProcessor = TemperatureProcessor()
4769
private let videoRecorder = VideoRecorder()
4870
private var isProcessing = false
71+
private var capture: Capture?
4972

5073
override init() {
5174
// Initialise any user defaults
@@ -66,51 +89,30 @@ class Camera: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDe
6689
super.init()
6790
}
6891

92+
/// Starts the thermal camera capture session.
93+
///
94+
/// - Returns: A boolean indicating whether the camera started successfully.
95+
/// - Throws: Camera initialization or permission errors.
6996
func start() throws -> Bool {
7097
if isRunning {
7198
return true
7299
}
73100

74-
// Find our USB Camera device
75-
let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.external], mediaType: .video, position: .unspecified)
76-
let devices = discoverySession.devices
77-
guard let videoCaptureDevice = devices.filter({ $0.localizedName.contains("USB Camera") }).first else {
78-
throw IrProError.noDevicesFound
79-
}
80-
81-
// Set up the session
82-
guard let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice) else {
83-
throw IrProError.failedToCreateDeviceInput
84-
}
85-
86-
if (captureSession.canAddInput(videoInput)) {
87-
captureSession.addInput(videoInput)
88-
} else {
89-
throw IrProError.failedToAddToSession
90-
}
91-
92-
let videoDataOutput = AVCaptureVideoDataOutput()
93-
if captureSession.canAddOutput(videoDataOutput) {
94-
captureSession.addOutput(videoDataOutput)
95-
videoDataOutput.setSampleBufferDelegate(self, queue: .main)
96-
videoDataOutput.alwaysDiscardsLateVideoFrames = true
97-
} else {
98-
throw IrProError.failedToAddOutput
99-
}
100-
101-
captureSession.startRunning()
102-
isRunning = true
103-
print("Camera started!")
104-
return true
101+
capture = Capture(delegate: self)
102+
isRunning = try capture?.start() ?? false
103+
return isRunning
105104
}
106105

106+
/// Stops the thermal camera capture session.
107107
func stop() {
108-
if isRunning {
109-
captureSession.stopRunning()
110-
isRunning = false
111-
}
108+
capture?.stop()
109+
isRunning = false
112110
}
113111

112+
/// Saves the current thermal image to disk as a PNG file.
113+
///
114+
/// - Parameter outputURL: The URL where the image should be saved.
115+
/// - Returns: A boolean indicating whether the save operation was successful.
114116
func saveImage(outputURL: URL) -> Bool {
115117
guard let resultImage = resultImage else {
116118
print("No image to save")
@@ -141,20 +143,38 @@ class Camera: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDe
141143
}
142144
}
143145

146+
/// Begins recording thermal video to disk.
147+
///
148+
/// - Parameter outputURL: The URL where the video should be saved.
149+
/// - Returns: A boolean indicating whether recording started successfully.
144150
func startRecording(outputURL: URL) -> Bool {
145151
let (width, height) = currentOrientation.translateX(CGFloat(WIDTH), y: CGFloat(HEIGHT))
146152
isRecording = videoRecorder.startRecording(outputURL: outputURL, width: width, height: height)
147153
return isRecording
148154
}
149155

156+
/// Stops the current video recording session.
150157
func stopRecording() {
151158
isRecording = false
152159
videoRecorder.stopRecording {
153160
print("Recording finished")
154161
}
155162
}
156163

157-
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
164+
// MARK: - CaptureDelegate
165+
166+
/// Processes new frames from the thermal camera.
167+
///
168+
/// This method handles:
169+
/// - Temperature data extraction
170+
/// - Image processing and colorization
171+
/// - Video recording
172+
/// - UI updates
173+
///
174+
/// - Parameters:
175+
/// - capture: The capture instance that produced the frame
176+
/// - sampleBuffer: The raw frame data buffer
177+
func capture(_ capture: Capture, didOutput sampleBuffer: CMSampleBuffer) {
158178
if isProcessing {
159179
return
160180
}

IrProCapture/Camera/ColorMaps.swift

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import CoreImage
88
import CoreImage.CIFilterBuiltins
99
import CoreGraphics
1010

11-
11+
/// Predefined color mapping for the 'Jet' colormap style
1212
let jetColormap: [(r: Float, g: Float, b: Float)] = [
1313
(0.0, 0.0, 0.5), // Dark Blue
1414
(0.0, 0.0, 1.0), // Blue
@@ -20,6 +20,7 @@ let jetColormap: [(r: Float, g: Float, b: Float)] = [
2020
(1.0, 0.0, 0.0) // Red
2121
]
2222

23+
/// Predefined color mapping for the 'Inferno' colormap style
2324
let infernoColormap: [(r: Float, g: Float, b: Float)] = [
2425
(0.0, 0.0, 0.13), // Dark Purple
2526
(0.23, 0.0, 0.38), // Deep Red
@@ -28,6 +29,7 @@ let infernoColormap: [(r: Float, g: Float, b: Float)] = [
2829
(1.0, 0.99, 0.0) // Bright Yellow
2930
]
3031

32+
/// Predefined color mapping for the 'Viridis' colormap style
3133
let viridisColormap: [(r: Float, g: Float, b: Float)] = [
3234
(0.13, 0.13, 0.38), // Dark Purple
3335
(0.24, 0.29, 0.56), // Deep Blue
@@ -36,6 +38,7 @@ let viridisColormap: [(r: Float, g: Float, b: Float)] = [
3638
(0.88, 0.98, 0.26) // Bright Yellow
3739
]
3840

41+
/// Predefined color mapping for the 'Plasma' colormap style
3942
let plasmaColormap: [(r: Float, g: Float, b: Float)] = [
4043
(0.0, 0.0, 0.13), // Dark Purple
4144
(0.26, 0.02, 0.42), // Deep Pink
@@ -44,6 +47,7 @@ let plasmaColormap: [(r: Float, g: Float, b: Float)] = [
4447
(1.0, 0.99, 0.0) // Bright Yellow
4548
]
4649

50+
/// Predefined color mapping for the 'Coolwarm' colormap style
4751
let coolwarmColormap: [(r: Float, g: Float, b: Float)] = [
4852
(0.0, 0.0, 0.5), // Dark Blue
4953
(0.0, 0.0, 1.0), // Blue
@@ -52,6 +56,7 @@ let coolwarmColormap: [(r: Float, g: Float, b: Float)] = [
5256
(1.0, 0.0, 0.0) // Red
5357
]
5458

59+
/// Predefined color mapping for the 'Magma' colormap style
5560
let magmaColormap: [(r: Float, g: Float, b: Float)] = [
5661
(0.0, 0.0, 0.13), // Dark Purple
5762
(0.25, 0.0, 0.27), // Purple
@@ -60,6 +65,7 @@ let magmaColormap: [(r: Float, g: Float, b: Float)] = [
6065
(1.0, 0.91, 0.0) // Light Yellow
6166
]
6267

68+
/// Predefined color mapping for the 'Twilight' colormap style
6369
let twilightColormap: [(r: Float, g: Float, b: Float)] = [
6470
(0.0, 0.0, 0.5), // Dark Blue
6571
(0.0, 0.0, 1.0), // Blue
@@ -68,35 +74,54 @@ let twilightColormap: [(r: Float, g: Float, b: Float)] = [
6874
(1.0, 1.0, 0.0) // Yellow
6975
]
7076

77+
/// Predefined color mapping for the 'Autumn' colormap style
7178
let autumnColormap: [(r: Float, g: Float, b: Float)] = [
7279
(1.0, 1.0, 0.0), // Yellow
7380
(1.0, 0.5, 0.0), // Orange
7481
(1.0, 0.0, 0.0) // Red
7582
]
7683

84+
/// Predefined color mapping for the 'Spring' colormap style
7785
let springColormap: [(r: Float, g: Float, b: Float)] = [
7886
(1.0, 0.0, 1.0), // Magenta
7987
(1.0, 1.0, 0.0) // Yellow
8088
]
8189

90+
/// Predefined color mapping for the 'Winter' colormap style
8291
let winterColormap: [(r: Float, g: Float, b: Float)] = [
8392
(0.0, 0.0, 1.0), // Blue
8493
(0.0, 1.0, 0.0) // Green
8594
]
8695

96+
/// A class that defines a color mapping for thermal image visualization.
97+
///
98+
/// ColorMap provides functionality to:
99+
/// - Define a named set of colors for thermal visualization
100+
/// - Convert grayscale thermal data to color using Core Image filters
101+
/// - Support comparison and hashing for collection storage
87102
class ColorMap: Hashable, Equatable {
103+
/// Compares two ColorMap instances for equality based on their names.
88104
static func == (lhs: ColorMap, rhs: ColorMap) -> Bool {
89105
return lhs.name == rhs.name
90106
}
91107

108+
/// Generates a hash value for the ColorMap based on its name.
92109
func hash(into hasher: inout Hasher) {
93110
name.hash(into: &hasher)
94111
}
95112

113+
/// The name of the color map (e.g., "Viridis", "Plasma")
96114
let name: String
115+
/// The array of RGB color values defining the color mapping
97116
let colors: [(r: Float, g: Float, b: Float)]
117+
/// The Core Image filter used to apply the color mapping
98118
let filter: CIColorCurves
99119

120+
/// Creates a new ColorMap instance.
121+
///
122+
/// - Parameters:
123+
/// - name: The name of the color map
124+
/// - colors: An array of RGB color values defining the mapping
100125
init(name: String, colors: [(r: Float, g: Float, b: Float)]) {
101126
self.name = name
102127
self.colors = colors
@@ -114,6 +139,7 @@ class ColorMap: Hashable, Equatable {
114139
}
115140
}
116141

142+
/// The collection of available color maps for thermal visualization
117143
let colorMaps = [
118144
ColorMap(name: "Viridis", colors: viridisColormap),
119145
ColorMap(name: "Plasma", colors: plasmaColormap),

0 commit comments

Comments
 (0)