Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
99
StackDex/Services/ImagePreprocessor.swift
Normal file
99
StackDex/Services/ImagePreprocessor.swift
Normal file
@@ -0,0 +1,99 @@
|
||||
import CoreImage
|
||||
import UIKit
|
||||
|
||||
struct PreparedImage {
|
||||
let normalizedImage: UIImage
|
||||
let analysisCGImage: CGImage
|
||||
let uploadJPEGData: Data
|
||||
let thumbnailJPEGData: Data?
|
||||
}
|
||||
|
||||
enum ImagePreprocessorError: LocalizedError {
|
||||
case unableToCreateImage
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .unableToCreateImage:
|
||||
return "The selected image could not be prepared for OCR."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImagePreprocessor {
|
||||
private let ciContext = CIContext()
|
||||
|
||||
func prepare(_ image: UIImage) throws -> PreparedImage {
|
||||
let upright = normalized(image)
|
||||
let resized = resizedImage(from: upright, maxDimension: 2_048)
|
||||
let enhanced = enhancedImage(from: resized) ?? resized
|
||||
|
||||
guard let cgImage = makeCGImage(from: enhanced) else {
|
||||
throw ImagePreprocessorError.unableToCreateImage
|
||||
}
|
||||
|
||||
return PreparedImage(
|
||||
normalizedImage: enhanced,
|
||||
analysisCGImage: cgImage,
|
||||
uploadJPEGData: enhanced.jpegData(compressionQuality: 0.82) ?? Data(),
|
||||
thumbnailJPEGData: resizedImage(from: enhanced, maxDimension: 240).jpegData(compressionQuality: 0.65)
|
||||
)
|
||||
}
|
||||
|
||||
private func normalized(_ image: UIImage) -> UIImage {
|
||||
guard image.imageOrientation != .up else { return image }
|
||||
|
||||
let renderer = UIGraphicsImageRenderer(size: image.size)
|
||||
return renderer.image { _ in
|
||||
image.draw(in: CGRect(origin: .zero, size: image.size))
|
||||
}
|
||||
}
|
||||
|
||||
private func resizedImage(from image: UIImage, maxDimension: CGFloat) -> UIImage {
|
||||
let largestDimension = max(image.size.width, image.size.height)
|
||||
guard largestDimension > maxDimension else { return image }
|
||||
|
||||
let scale = maxDimension / largestDimension
|
||||
let targetSize = CGSize(width: image.size.width * scale, height: image.size.height * scale)
|
||||
let renderer = UIGraphicsImageRenderer(size: targetSize)
|
||||
|
||||
return renderer.image { _ in
|
||||
image.draw(in: CGRect(origin: .zero, size: targetSize))
|
||||
}
|
||||
}
|
||||
|
||||
private func enhancedImage(from image: UIImage) -> UIImage? {
|
||||
guard let ciImage = CIImage(image: image) else { return nil }
|
||||
|
||||
let adjusted = ciImage
|
||||
.applyingFilter("CIColorControls", parameters: [
|
||||
kCIInputContrastKey: 1.08,
|
||||
kCIInputSaturationKey: 0.96,
|
||||
kCIInputBrightnessKey: 0.01,
|
||||
])
|
||||
.applyingFilter("CISharpenLuminance", parameters: [
|
||||
kCIInputSharpnessKey: 0.35,
|
||||
])
|
||||
|
||||
guard let cgImage = ciContext.createCGImage(adjusted, from: adjusted.extent) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return UIImage(cgImage: cgImage)
|
||||
}
|
||||
|
||||
private func makeCGImage(from image: UIImage) -> CGImage? {
|
||||
if let cgImage = image.cgImage {
|
||||
return cgImage
|
||||
}
|
||||
|
||||
if let ciImage = image.ciImage {
|
||||
return ciContext.createCGImage(ciImage, from: ciImage.extent)
|
||||
}
|
||||
|
||||
let renderer = UIGraphicsImageRenderer(size: image.size)
|
||||
let rendered = renderer.image { _ in
|
||||
image.draw(in: CGRect(origin: .zero, size: image.size))
|
||||
}
|
||||
return rendered.cgImage
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user