Skip to content

Commit 5cb6e52

Browse files
committed
Add interface
1 parent 49f8c79 commit 5cb6e52

File tree

39 files changed

+736
-0
lines changed

39 files changed

+736
-0
lines changed

.DS_Store

6 KB
Binary file not shown.

.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>IDEDidComputeMac32BitWarning</key>
6+
<true/>
7+
</dict>
8+
</plist>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "1530"
4+
version = "1.7">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES"
8+
buildArchitectures = "Automatic">
9+
<BuildActionEntries>
10+
<BuildActionEntry
11+
buildForTesting = "YES"
12+
buildForRunning = "YES"
13+
buildForProfiling = "YES"
14+
buildForArchiving = "YES"
15+
buildForAnalyzing = "YES">
16+
<BuildableReference
17+
BuildableIdentifier = "primary"
18+
BlueprintIdentifier = "Mentalist"
19+
BuildableName = "Mentalist"
20+
BlueprintName = "Mentalist"
21+
ReferencedContainer = "container:">
22+
</BuildableReference>
23+
</BuildActionEntry>
24+
</BuildActionEntries>
25+
</BuildAction>
26+
<TestAction
27+
buildConfiguration = "Debug"
28+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
29+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
30+
shouldUseLaunchSchemeArgsEnv = "YES"
31+
shouldAutocreateTestPlan = "YES">
32+
<Testables>
33+
<TestableReference
34+
skipped = "NO">
35+
<BuildableReference
36+
BuildableIdentifier = "primary"
37+
BlueprintIdentifier = "MentalistTests"
38+
BuildableName = "MentalistTests"
39+
BlueprintName = "MentalistTests"
40+
ReferencedContainer = "container:">
41+
</BuildableReference>
42+
</TestableReference>
43+
</Testables>
44+
</TestAction>
45+
<LaunchAction
46+
buildConfiguration = "Debug"
47+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
48+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
49+
launchStyle = "0"
50+
useCustomWorkingDirectory = "NO"
51+
ignoresPersistentStateOnLaunch = "NO"
52+
debugDocumentVersioning = "YES"
53+
debugServiceExtension = "internal"
54+
allowLocationSimulation = "YES">
55+
</LaunchAction>
56+
<ProfileAction
57+
buildConfiguration = "Release"
58+
shouldUseLaunchSchemeArgsEnv = "YES"
59+
savedToolIdentifier = ""
60+
useCustomWorkingDirectory = "NO"
61+
debugDocumentVersioning = "YES">
62+
<MacroExpansion>
63+
<BuildableReference
64+
BuildableIdentifier = "primary"
65+
BlueprintIdentifier = "Mentalist"
66+
BuildableName = "Mentalist"
67+
BlueprintName = "Mentalist"
68+
ReferencedContainer = "container:">
69+
</BuildableReference>
70+
</MacroExpansion>
71+
</ProfileAction>
72+
<AnalyzeAction
73+
buildConfiguration = "Debug">
74+
</AnalyzeAction>
75+
<ArchiveAction
76+
buildConfiguration = "Release"
77+
revealArchiveInOrganizer = "YES">
78+
</ArchiveAction>
79+
</Scheme>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "1530"
4+
version = "1.7">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES"
8+
buildArchitectures = "Automatic">
9+
</BuildAction>
10+
<TestAction
11+
buildConfiguration = "Debug"
12+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
13+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
14+
shouldUseLaunchSchemeArgsEnv = "YES"
15+
shouldAutocreateTestPlan = "YES">
16+
<Testables>
17+
<TestableReference
18+
skipped = "NO">
19+
<BuildableReference
20+
BuildableIdentifier = "primary"
21+
BlueprintIdentifier = "MentalistTests"
22+
BuildableName = "MentalistTests"
23+
BlueprintName = "MentalistTests"
24+
ReferencedContainer = "container:">
25+
</BuildableReference>
26+
</TestableReference>
27+
</Testables>
28+
</TestAction>
29+
<LaunchAction
30+
buildConfiguration = "Debug"
31+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
32+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
33+
launchStyle = "0"
34+
useCustomWorkingDirectory = "NO"
35+
ignoresPersistentStateOnLaunch = "NO"
36+
debugDocumentVersioning = "YES"
37+
debugServiceExtension = "internal"
38+
allowLocationSimulation = "YES">
39+
</LaunchAction>
40+
<ProfileAction
41+
buildConfiguration = "Release"
42+
shouldUseLaunchSchemeArgsEnv = "YES"
43+
savedToolIdentifier = ""
44+
useCustomWorkingDirectory = "NO"
45+
debugDocumentVersioning = "YES">
46+
</ProfileAction>
47+
<AnalyzeAction
48+
buildConfiguration = "Debug">
49+
</AnalyzeAction>
50+
<ArchiveAction
51+
buildConfiguration = "Release"
52+
revealArchiveInOrganizer = "YES">
53+
</ArchiveAction>
54+
</Scheme>

Package.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// swift-tools-version:5.6
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "Mentalist",
6+
platforms: [
7+
.macOS(.v10_13),
8+
.iOS(.v15)
9+
],
10+
products: [
11+
.library(
12+
name: "Mentalist",
13+
targets: ["Mentalist"]),
14+
],
15+
dependencies: [],
16+
targets: [
17+
.target(
18+
name: "Mentalist",
19+
dependencies: [],
20+
resources: [
21+
.process("Resources/FacialExpressionModel.mlpackage")
22+
]
23+
),
24+
.testTarget(
25+
name: "MentalistTests",
26+
dependencies: ["Mentalist"],
27+
resources: [
28+
.process("Resources")
29+
]
30+
),
31+
]
32+
)

Sources/.DS_Store

8 KB
Binary file not shown.

Sources/Mentalist/.DS_Store

10 KB
Binary file not shown.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//
2+
// UIView + Extension.swift
3+
//
4+
//
5+
// Created by Enebin on 6/12/24.
6+
//
7+
8+
import UIKit
9+
10+
extension UIView {
11+
func asUIImage() -> UIImage {
12+
let renderer = UIGraphicsImageRenderer(bounds: bounds)
13+
return renderer.image { rendererContext in
14+
layer.render(in: rendererContext.cgContext)
15+
}
16+
}
17+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
// View + Extentsion.swift
3+
//
4+
//
5+
// Created by Enebin on 6/12/24.
6+
//
7+
8+
import SwiftUI
9+
10+
extension View {
11+
func asUIImage() -> UIImage {
12+
let controller = UIHostingController(rootView: self)
13+
controller.view.backgroundColor = .clear
14+
15+
controller.view.frame = CGRect(x: 0, y: CGFloat(Int.max), width: 1, height: 1)
16+
UIApplication.shared.windows.first!.rootViewController?.view.addSubview(controller.view)
17+
18+
let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
19+
controller.view.bounds = CGRect(origin: .zero, size: size)
20+
controller.view.sizeToFit()
21+
22+
let image = controller.view.asUIImage()
23+
controller.view.removeFromSuperview()
24+
return image
25+
}
26+
}

Sources/Mentalist/FaceImageTool.swift

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
//
2+
// FaceImageTool.swift
3+
// Mentalist
4+
//
5+
// Created by Enebin on 6/13/24.
6+
//
7+
8+
import Vision
9+
import CoreImage
10+
import UIKit
11+
12+
struct FaceImageTool {
13+
func preprocessImage(image: CGImage) -> CGImage? {
14+
// Step 1: Convert to Grayscale
15+
guard let grayscaleImage = convertToGrayscale(image: image) else { return nil }
16+
17+
// Step 2: Resize
18+
guard let resizedImage = resizeImage(image: grayscaleImage, targetSize: CGSize(width: 48, height: 48)) else { return nil }
19+
20+
// Step 3: Normalize
21+
guard let normalizedImage = normalizeImage(image: resizedImage) else { return nil }
22+
23+
return normalizedImage
24+
}
25+
26+
/// Extracts faces from the given CGImage asynchronously.
27+
/// - Parameter cgImage: The source CGImage from which to extract faces.
28+
/// - Returns: An array of VNFaceObservation objects representing the detected faces.
29+
/// - Throws: An error if face detection fails.
30+
func extractFaces(from cgImage: CGImage) throws -> [VNFaceObservation] {
31+
let request = VNDetectFaceRectanglesRequest()
32+
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
33+
try handler.perform([request])
34+
let results = request.results
35+
36+
return results ?? []
37+
}
38+
39+
/// Crops a face from the given image using the specified bounding box.
40+
/// - Parameters:
41+
/// - image: The source CGImage from which to crop the face.
42+
/// - boundingBox: The CGRect representing the bounding box of the face.
43+
/// - Returns: A CGImage representing the cropped face, or nil if cropping fails.
44+
func cropFace(from image: CGImage, boundingBox: CGRect) -> CGImage? { let width = boundingBox.width * CGFloat(image.width)
45+
let height = boundingBox.height * CGFloat(image.height)
46+
let x = boundingBox.minX * CGFloat(image.width)
47+
let y = (1 - boundingBox.minY) * CGFloat(image.height) - height
48+
49+
let cropRect = CGRect(x: x, y: y, width: width, height: height)
50+
51+
// Extract the cropped CGImage from the CGImage
52+
guard let croppedCGImage = image.cropping(to: cropRect) else { return nil }
53+
54+
return croppedCGImage
55+
}
56+
}
57+
58+
private extension FaceImageTool {
59+
func convertToGrayscale(image: CGImage) -> CGImage? {
60+
let ciImage = CIImage(cgImage: image)
61+
62+
let grayscaleFilter = CIFilter(name: "CIPhotoEffectMono")
63+
grayscaleFilter?.setValue(ciImage, forKey: kCIInputImageKey)
64+
guard let outputCIImage = grayscaleFilter?.outputImage else { return nil }
65+
66+
let context = CIContext()
67+
guard let cgImage = context.createCGImage(outputCIImage, from: outputCIImage.extent) else { return nil }
68+
69+
return cgImage
70+
}
71+
72+
func resizeImage(image: CGImage, targetSize: CGSize) -> CGImage? {
73+
let width = targetSize.width
74+
let height = targetSize.height
75+
let colorSpace = CGColorSpaceCreateDeviceGray()
76+
let context = CGContext(data: nil, width: Int(width), height: Int(height), bitsPerComponent: 8, bytesPerRow: Int(width), space: colorSpace, bitmapInfo: CGImageAlphaInfo.none.rawValue)
77+
78+
context?.interpolationQuality = .high
79+
context?.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
80+
81+
return context?.makeImage()
82+
}
83+
84+
func normalizeImage(image: CGImage) -> CGImage? {
85+
let width = image.width
86+
let height = image.height
87+
let colorSpace = CGColorSpaceCreateDeviceGray()
88+
var pixelData = [UInt8](repeating: 0, count: width * height)
89+
90+
let context = CGContext(data: &pixelData, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width, space: colorSpace, bitmapInfo: CGImageAlphaInfo.none.rawValue)
91+
context?.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
92+
93+
// Normalize pixel data
94+
let normalizedPixelData = pixelData.map { Float($0) / 255.0 }
95+
var denormalizedPixelData = normalizedPixelData.map { UInt8($0 * 255.0) }
96+
97+
// Create new CGImage
98+
let newContext = CGContext(data: &denormalizedPixelData, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width, space: colorSpace, bitmapInfo: CGImageAlphaInfo.none.rawValue)
99+
guard let newCgImage = newContext?.makeImage() else { return nil }
100+
101+
return newCgImage
102+
}
103+
}

Sources/Mentalist/Mentalist.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import Foundation
2+
import SwiftUI
3+
import Vision
4+
5+
@available(iOS 15.0, *)
6+
public struct Mentalist {
7+
static private let core = MentalistCore()
8+
9+
public static func analyze(cgImage: CGImage) throws -> [EmotionAnalysis] {
10+
let mlModel = try VNCoreMLModel(for: FacialExpressionModel().model)
11+
return try core.analyze(cgImage: cgImage, model: mlModel)
12+
}
13+
14+
public static func analyze(uiImage: UIImage) throws -> [EmotionAnalysis] {
15+
guard let cgImage = uiImage.cgImage else {
16+
throw NSError(domain: "UIImage to CGImage conversion failed", code: 0)
17+
}
18+
19+
return try analyze(cgImage: cgImage)
20+
}
21+
22+
public static func analyze(image: Image) throws -> [EmotionAnalysis] {
23+
guard let cgImage = image.asUIImage().cgImage else {
24+
throw NSError(domain: "Image to CGImage conversion failed", code: 0)
25+
}
26+
27+
return try analyze(cgImage: cgImage)
28+
}
29+
}

0 commit comments

Comments
 (0)