Skip to content

Commit

Permalink
Add interface
Browse files Browse the repository at this point in the history
  • Loading branch information
enebin committed Jun 13, 2024
1 parent 49f8c79 commit 5cb6e52
Show file tree
Hide file tree
Showing 39 changed files with 736 additions and 0 deletions.
Binary file added .DS_Store
Binary file not shown.
7 changes: 7 additions & 0 deletions .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
79 changes: 79 additions & 0 deletions .swiftpm/xcode/xcshareddata/xcschemes/Mentalist.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1530"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Mentalist"
BuildableName = "Mentalist"
BlueprintName = "Mentalist"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "MentalistTests"
BuildableName = "MentalistTests"
BlueprintName = "MentalistTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Mentalist"
BuildableName = "Mentalist"
BlueprintName = "Mentalist"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
54 changes: 54 additions & 0 deletions .swiftpm/xcode/xcshareddata/xcschemes/MentalistTests.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1530"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "MentalistTests"
BuildableName = "MentalistTests"
BlueprintName = "MentalistTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
32 changes: 32 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// swift-tools-version:5.6
import PackageDescription

let package = Package(
name: "Mentalist",
platforms: [
.macOS(.v10_13),
.iOS(.v15)
],
products: [
.library(
name: "Mentalist",
targets: ["Mentalist"]),
],
dependencies: [],
targets: [
.target(
name: "Mentalist",
dependencies: [],
resources: [
.process("Resources/FacialExpressionModel.mlpackage")
]
),
.testTarget(
name: "MentalistTests",
dependencies: ["Mentalist"],
resources: [
.process("Resources")
]
),
]
)
Binary file added Sources/.DS_Store
Binary file not shown.
Binary file added Sources/Mentalist/.DS_Store
Binary file not shown.
17 changes: 17 additions & 0 deletions Sources/Mentalist/Extension/UIView + Extension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// UIView + Extension.swift
//
//
// Created by Enebin on 6/12/24.
//

import UIKit

extension UIView {
func asUIImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
}
26 changes: 26 additions & 0 deletions Sources/Mentalist/Extension/View + Extentsion.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// View + Extentsion.swift
//
//
// Created by Enebin on 6/12/24.
//

import SwiftUI

extension View {
func asUIImage() -> UIImage {
let controller = UIHostingController(rootView: self)
controller.view.backgroundColor = .clear

controller.view.frame = CGRect(x: 0, y: CGFloat(Int.max), width: 1, height: 1)
UIApplication.shared.windows.first!.rootViewController?.view.addSubview(controller.view)

let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
controller.view.bounds = CGRect(origin: .zero, size: size)
controller.view.sizeToFit()

let image = controller.view.asUIImage()
controller.view.removeFromSuperview()
return image
}
}
103 changes: 103 additions & 0 deletions Sources/Mentalist/FaceImageTool.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//
// FaceImageTool.swift
// Mentalist
//
// Created by Enebin on 6/13/24.
//

import Vision
import CoreImage
import UIKit

struct FaceImageTool {
func preprocessImage(image: CGImage) -> CGImage? {
// Step 1: Convert to Grayscale
guard let grayscaleImage = convertToGrayscale(image: image) else { return nil }

// Step 2: Resize
guard let resizedImage = resizeImage(image: grayscaleImage, targetSize: CGSize(width: 48, height: 48)) else { return nil }

// Step 3: Normalize
guard let normalizedImage = normalizeImage(image: resizedImage) else { return nil }

return normalizedImage
}

/// Extracts faces from the given CGImage asynchronously.
/// - Parameter cgImage: The source CGImage from which to extract faces.
/// - Returns: An array of VNFaceObservation objects representing the detected faces.
/// - Throws: An error if face detection fails.
func extractFaces(from cgImage: CGImage) throws -> [VNFaceObservation] {
let request = VNDetectFaceRectanglesRequest()
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
try handler.perform([request])
let results = request.results

return results ?? []
}

/// Crops a face from the given image using the specified bounding box.
/// - Parameters:
/// - image: The source CGImage from which to crop the face.
/// - boundingBox: The CGRect representing the bounding box of the face.
/// - Returns: A CGImage representing the cropped face, or nil if cropping fails.
func cropFace(from image: CGImage, boundingBox: CGRect) -> CGImage? { let width = boundingBox.width * CGFloat(image.width)
let height = boundingBox.height * CGFloat(image.height)
let x = boundingBox.minX * CGFloat(image.width)
let y = (1 - boundingBox.minY) * CGFloat(image.height) - height

let cropRect = CGRect(x: x, y: y, width: width, height: height)

// Extract the cropped CGImage from the CGImage
guard let croppedCGImage = image.cropping(to: cropRect) else { return nil }

return croppedCGImage
}
}

private extension FaceImageTool {
func convertToGrayscale(image: CGImage) -> CGImage? {
let ciImage = CIImage(cgImage: image)

let grayscaleFilter = CIFilter(name: "CIPhotoEffectMono")
grayscaleFilter?.setValue(ciImage, forKey: kCIInputImageKey)
guard let outputCIImage = grayscaleFilter?.outputImage else { return nil }

let context = CIContext()
guard let cgImage = context.createCGImage(outputCIImage, from: outputCIImage.extent) else { return nil }

return cgImage
}

func resizeImage(image: CGImage, targetSize: CGSize) -> CGImage? {
let width = targetSize.width
let height = targetSize.height
let colorSpace = CGColorSpaceCreateDeviceGray()
let context = CGContext(data: nil, width: Int(width), height: Int(height), bitsPerComponent: 8, bytesPerRow: Int(width), space: colorSpace, bitmapInfo: CGImageAlphaInfo.none.rawValue)

context?.interpolationQuality = .high
context?.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))

return context?.makeImage()
}

func normalizeImage(image: CGImage) -> CGImage? {
let width = image.width
let height = image.height
let colorSpace = CGColorSpaceCreateDeviceGray()
var pixelData = [UInt8](repeating: 0, count: width * height)

let context = CGContext(data: &pixelData, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width, space: colorSpace, bitmapInfo: CGImageAlphaInfo.none.rawValue)
context?.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))

// Normalize pixel data
let normalizedPixelData = pixelData.map { Float($0) / 255.0 }
var denormalizedPixelData = normalizedPixelData.map { UInt8($0 * 255.0) }

// Create new CGImage
let newContext = CGContext(data: &denormalizedPixelData, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width, space: colorSpace, bitmapInfo: CGImageAlphaInfo.none.rawValue)
guard let newCgImage = newContext?.makeImage() else { return nil }

return newCgImage
}
}
29 changes: 29 additions & 0 deletions Sources/Mentalist/Mentalist.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Foundation
import SwiftUI
import Vision

@available(iOS 15.0, *)
public struct Mentalist {
static private let core = MentalistCore()

public static func analyze(cgImage: CGImage) throws -> [EmotionAnalysis] {
let mlModel = try VNCoreMLModel(for: FacialExpressionModel().model)
return try core.analyze(cgImage: cgImage, model: mlModel)
}

public static func analyze(uiImage: UIImage) throws -> [EmotionAnalysis] {
guard let cgImage = uiImage.cgImage else {
throw NSError(domain: "UIImage to CGImage conversion failed", code: 0)
}

return try analyze(cgImage: cgImage)
}

public static func analyze(image: Image) throws -> [EmotionAnalysis] {
guard let cgImage = image.asUIImage().cgImage else {
throw NSError(domain: "Image to CGImage conversion failed", code: 0)
}

return try analyze(cgImage: cgImage)
}
}
Loading

0 comments on commit 5cb6e52

Please sign in to comment.