Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions IrProCapture/Camera/Camera.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ class Camera: NSObject, ObservableObject, CaptureDelegate {
// Convert temperatures to a color mapped image
guard let processedImage = CIImage.fromTemperatures(
temperatures: tempResult.temperatures,
minTemp: tempResult.min,
maxTemp: tempResult.max,
minTemp: uiState.manualRangeEnabled ? uiState.manualMinTemp : tempResult.min,
maxTemp: uiState.manualRangeEnabled ? uiState.manualMaxTemp : tempResult.max,
width: 256,
height: 192,
scale: SCALE,
Expand Down
3 changes: 2 additions & 1 deletion IrProCapture/Camera/Components/ImageProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ extension CIImage {
var pixelData = [UInt8](repeating: 0, count: width * height * 4)
let range = max(maxTemp - minTemp, 1.0)
let scaledTemperatures = vDSP.multiply(255.0 / range, vDSP.add(-minTemp, temperatures))
let clippedTemperatures = vDSP.clip(scaledTemperatures, to: 0.0...255.0)
for index in 0..<width*height {
pixelData[index] = UInt8(scaledTemperatures[index])
pixelData[index] = UInt8(clippedTemperatures[index])
}
let bytesPerRow = width
let colorSpace = CGColorSpaceCreateDeviceGray()
Expand Down
31 changes: 31 additions & 0 deletions IrProCapture/Camera/UIState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,33 @@ class UIState: ObservableObject {
}
}

/// Whether to use manual temperature range for display
@Published var manualRangeEnabled: Bool {
didSet {
UserDefaults.standard.set(manualRangeEnabled, forKey: "manualRangeEnabled")
}
}

/// Manual minimum temperature for display
@Published var manualMinTemp: Float {
didSet {
if manualMinTemp >= manualMaxTemp {
manualMinTemp = manualMaxTemp - 0.1
}
UserDefaults.standard.set(manualMinTemp, forKey: "manualMinTemp")
}
}

/// Manual maximum temperature for display
@Published var manualMaxTemp: Float {
didSet {
if manualMaxTemp <= manualMinTemp {
manualMaxTemp = manualMinTemp + 0.1
}
UserDefaults.standard.set(manualMaxTemp, forKey: "manualMaxTemp")
}
}

/// Indicates whether the camera is currently running
@Published var isRunning = false

Expand Down Expand Up @@ -128,6 +155,10 @@ class UIState: ObservableObject {
self.temperatureFormat = .celsius
UserDefaults.standard.set(TemperatureFormat.celsius.rawValue, forKey: "temperatureFormat")
}

self.manualRangeEnabled = UserDefaults.standard.bool(forKey: "manualRangeEnabled")
self.manualMinTemp = UserDefaults.standard.object(forKey: "manualMinTemp") as? Float ?? 20.0
self.manualMaxTemp = UserDefaults.standard.object(forKey: "manualMaxTemp") as? Float ?? 40.0
}

/// Cycles to the next orientation option
Expand Down
8 changes: 4 additions & 4 deletions IrProCapture/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ struct ContentView: View {
Divider()
ColorMapDisplay(
colorMap: uiState.currentColorMap,
maxTemperature: model.maxTemperature,
minTemperature: model.minTemperature,
maxTemperature: uiState.manualRangeEnabled ? uiState.manualMaxTemp : model.maxTemperature,
minTemperature: uiState.manualRangeEnabled ? uiState.manualMinTemp : model.minTemperature,
format: uiState.temperatureFormat)
TemperatureHistogramChart(
histogram: model.histogram,
minTemperature: model.minTemperature,
maxTemperature: model.maxTemperature,
minTemperature: uiState.manualRangeEnabled ? uiState.manualMinTemp : model.minTemperature,
maxTemperature: uiState.manualRangeEnabled ? uiState.manualMaxTemp : model.maxTemperature,
format: uiState.temperatureFormat
)
}
Expand Down
16 changes: 16 additions & 0 deletions IrProCapture/Views/CaptureToolbar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ struct CaptureToolbar: View {
@EnvironmentObject var model: Camera
@EnvironmentObject var uiState: UIState
@State private var alertMessage: String? = nil
@State private var showRangeControls = false

var body: some View {
HStack(spacing: 20) {
Expand Down Expand Up @@ -87,6 +88,21 @@ struct CaptureToolbar: View {
.buttonStyle(.bordered)
.help("Next Orientation")

// Range control button
Button(action: {
showRangeControls.toggle()
}) {
Image(systemName: "thermometer.medium")
.font(.title)
.foregroundColor(uiState.manualRangeEnabled ? .blue : .primary)
}
.disabled(!uiState.isRunning)
.buttonStyle(.bordered)
.help("Display Range")
.popover(isPresented: $showRangeControls) {
TemperatureRangeControls()
}

Spacer()
}
.alert(
Expand Down
12 changes: 10 additions & 2 deletions IrProCapture/Views/ColorMapDisplay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,23 @@ struct ColorMapDisplay: View {
}

var body: some View {
HStack {
HStack(spacing: 8) {
VStack {
Text(format.format(format.convert(maxTemperature)))
.font(.caption)
.monospacedDigit()
Spacer()
Text(format.format(format.convert(minTemperature)))
.font(.caption)
.monospacedDigit()
}
.frame(minWidth: 50)

LinearGradient(gradient: Gradient(colors: colorMap.colors.map { Color(red: CGFloat($0.r), green: CGFloat($0.g), blue: CGFloat($0.b)) }), startPoint: .bottom, endPoint: .top)
.frame(width: 50)
.frame(width: 40)
.cornerRadius(4)
}
.padding(.vertical, 4)
}
}

Expand Down
116 changes: 116 additions & 0 deletions IrProCapture/Views/RangeSlider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import SwiftUI

struct RangeSlider: View {
@Binding var lowValue: Float
@Binding var highValue: Float
let range: ClosedRange<Float>

var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
// Background Track
Capsule()
.fill(Color.secondary.opacity(0.3))
.frame(height: 4)

// Highlighted Track
Capsule()
.fill(Color.blue)
.frame(width: CGFloat(xForValue(highValue, in: geometry.size.width) - xForValue(lowValue, in: geometry.size.width)), height: 4)
.offset(x: CGFloat(xForValue(lowValue, in: geometry.size.width)))

// Low Handle
HandleView(isFocused: focusedHandle == .low, side: .left)
.offset(x: CGFloat(xForValue(lowValue, in: geometry.size.width)) - 10)
.gesture(
DragGesture()
.onChanged { value in
focusedHandle = .low
let newValue = valueForX(Float(value.location.x), in: geometry.size.width)
lowValue = min(max(range.lowerBound, newValue), highValue - 0.1)
}
)
.focusable()
.onMoveCommand { direction in
focusedHandle = .low
switch direction {
case .left: lowValue = max(range.lowerBound, lowValue - 0.5)
case .right: lowValue = min(highValue - 0.1, lowValue + 0.5)
default: break
}
}

// High Handle
HandleView(isFocused: focusedHandle == .high, side: .right)
.offset(x: CGFloat(xForValue(highValue, in: geometry.size.width)))
.gesture(
DragGesture()
.onChanged { value in
focusedHandle = .high
let newValue = valueForX(Float(value.location.x), in: geometry.size.width)
highValue = max(min(range.upperBound, newValue), lowValue + 0.1)
}
)
.focusable()
.onMoveCommand { direction in
focusedHandle = .high
switch direction {
case .left: highValue = max(lowValue + 0.1, highValue - 0.5)
case .right: highValue = min(range.upperBound, highValue + 0.5)
default: break
}
}
}
}
.frame(height: 20)
}

enum FocusedHandle {
case low, high
}
@State private var focusedHandle: FocusedHandle? = nil

private func xForValue(_ value: Float, in width: CGFloat) -> Float {
let percentage = (value - range.lowerBound) / (range.upperBound - range.lowerBound)
return Float(width) * percentage
}

private func valueForX(_ x: Float, in width: CGFloat) -> Float {
let percentage = x / Float(width)
return range.lowerBound + percentage * (range.upperBound - range.lowerBound)
}
}

enum HandleSide {
case left, right
}

struct HandleView: View {
var isFocused: Bool
var side: HandleSide

var body: some View {
ZStack {
UnevenRoundedRectangle(
topLeadingRadius: side == .left ? 10 : 0,
bottomLeadingRadius: side == .left ? 10 : 0,
bottomTrailingRadius: side == .right ? 10 : 0,
topTrailingRadius: side == .right ? 10 : 0,
style: .circular
)
.fill(Color.white)
.frame(width: 10, height: 20)
.shadow(radius: 2)
.overlay(
UnevenRoundedRectangle(
topLeadingRadius: side == .left ? 10 : 0,
bottomLeadingRadius: side == .left ? 10 : 0,
bottomTrailingRadius: side == .right ? 10 : 0,
topTrailingRadius: side == .right ? 10 : 0,
style: .circular
)
.stroke(isFocused ? Color.blue : Color.gray.opacity(0.2), lineWidth: isFocused ? 2 : 0.5)
)
}
}
}
55 changes: 55 additions & 0 deletions IrProCapture/Views/TemperatureRangeControls.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import SwiftUI

struct TemperatureRangeControls: View {
@EnvironmentObject var uiState: UIState
@EnvironmentObject var model: Camera

var body: some View {
VStack(alignment: .leading, spacing: 10) {
Toggle("Manual Range", isOn: $uiState.manualRangeEnabled)
.toggleStyle(.checkbox)

if uiState.manualRangeEnabled {
VStack(alignment: .leading, spacing: 20) {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("Range:")
.font(.headline)
Spacer()
Text("\(uiState.temperatureFormat.format(uiState.temperatureFormat.convert(uiState.manualMinTemp))) - \(uiState.temperatureFormat.format(uiState.temperatureFormat.convert(uiState.manualMaxTemp)))")
.font(.subheadline)
.monospacedDigit()
}

RangeSlider(
lowValue: $uiState.manualMinTemp,
highValue: $uiState.manualMaxTemp,
range: -20...150
)
.padding(.horizontal, 10)
}
}
.padding(.vertical, 10)

HStack {
Button("Reset to Current") {
uiState.manualMinTemp = model.minTemperature
uiState.manualMaxTemp = model.maxTemperature
}
.buttonStyle(.link)

Spacer()

Button("Reset to Default") {
uiState.manualMinTemp = 20.0
uiState.manualMaxTemp = 40.0
}
.buttonStyle(.link)
}
.padding(.top, 10)
}
}
.padding()
.frame(width: 320)
}
}