Skip to content

Commit fca0540

Browse files
authored
[Merge] #183 - CoreML을 활용한 온디바이스 AI 이미지 변환 기능 구현
2 parents 9096be5 + 29d469c commit fca0540

13 files changed

Lines changed: 865 additions & 31 deletions

File tree

Neki-iOS.xcodeproj/project.pbxproj

Lines changed: 165 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,34 @@
1515
5970335E2F2482E1000B8194 /* KakaoSDKCommon in Frameworks */ = {isa = PBXBuildFile; productRef = 5970335D2F2482E1000B8194 /* KakaoSDKCommon */; };
1616
597033602F2482E1000B8194 /* KakaoSDKUser in Frameworks */ = {isa = PBXBuildFile; productRef = 5970335F2F2482E1000B8194 /* KakaoSDKUser */; };
1717
59D1D21B2F168EE800F50EB5 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 59D1D21A2F168EE800F50EB5 /* Kingfisher */; };
18+
6C3B4B8A2F65CA4B008449C1 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6C3B4B882F65C9B0008449C1 /* WebKit.framework */; };
1819
6C4D99E52F365E1700E93BF0 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 6C4D99E42F365E1700E93BF0 /* Lottie */; };
1920
/* End PBXBuildFile section */
2021

22+
/* Begin PBXContainerItemProxy section */
23+
6C3B4B802F65C44C008449C1 /* PBXContainerItemProxy */ = {
24+
isa = PBXContainerItemProxy;
25+
containerPortal = 6C4998D52EF7EF62006BE1DB /* Project object */;
26+
proxyType = 1;
27+
remoteGlobalIDString = 6C4998DC2EF7EF62006BE1DB;
28+
remoteInfo = "Neki-iOS";
29+
};
30+
/* End PBXContainerItemProxy section */
31+
2132
/* Begin PBXFileReference section */
33+
6C3B4B7C2F65C44C008449C1 /* Neki-iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Neki-iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
34+
6C3B4B882F65C9B0008449C1 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
2235
6C4998DD2EF7EF62006BE1DB /* Neki-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Neki-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
2336
/* End PBXFileReference section */
2437

2538
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
39+
6C45F2B92F65ECE900A6ABBB /* Exceptions for "Neki-iOS" folder in "Neki-iOSTests" target */ = {
40+
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
41+
membershipExceptions = (
42+
Core/Sources/ImageTransform/Data/MLModel/whiteboxcartoonization.mlmodel,
43+
);
44+
target = 6C3B4B7B2F65C44C008449C1 /* Neki-iOSTests */;
45+
};
2646
6C499D3A2F05594E006BE1DB /* Exceptions for "Neki-iOS" folder in "Neki-iOS" target */ = {
2747
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
2848
membershipExceptions = (
@@ -33,17 +53,31 @@
3353
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
3454

3555
/* Begin PBXFileSystemSynchronizedRootGroup section */
56+
6C3B4B7D2F65C44C008449C1 /* Neki-iOSTests */ = {
57+
isa = PBXFileSystemSynchronizedRootGroup;
58+
path = "Neki-iOSTests";
59+
sourceTree = "<group>";
60+
};
3661
6C4998DF2EF7EF62006BE1DB /* Neki-iOS */ = {
3762
isa = PBXFileSystemSynchronizedRootGroup;
3863
exceptions = (
3964
6C499D3A2F05594E006BE1DB /* Exceptions for "Neki-iOS" folder in "Neki-iOS" target */,
65+
6C45F2B92F65ECE900A6ABBB /* Exceptions for "Neki-iOS" folder in "Neki-iOSTests" target */,
4066
);
4167
path = "Neki-iOS";
4268
sourceTree = "<group>";
4369
};
4470
/* End PBXFileSystemSynchronizedRootGroup section */
4571

4672
/* Begin PBXFrameworksBuildPhase section */
73+
6C3B4B792F65C44C008449C1 /* Frameworks */ = {
74+
isa = PBXFrameworksBuildPhase;
75+
buildActionMask = 2147483647;
76+
files = (
77+
6C3B4B8A2F65CA4B008449C1 /* WebKit.framework in Frameworks */,
78+
);
79+
runOnlyForDeploymentPostprocessing = 0;
80+
};
4781
6C4998DA2EF7EF62006BE1DB /* Frameworks */ = {
4882
isa = PBXFrameworksBuildPhase;
4983
buildActionMask = 2147483647;
@@ -63,10 +97,20 @@
6397
/* End PBXFrameworksBuildPhase section */
6498

6599
/* Begin PBXGroup section */
100+
6C3B4B872F65C9AF008449C1 /* Frameworks */ = {
101+
isa = PBXGroup;
102+
children = (
103+
6C3B4B882F65C9B0008449C1 /* WebKit.framework */,
104+
);
105+
name = Frameworks;
106+
sourceTree = "<group>";
107+
};
66108
6C4998D42EF7EF62006BE1DB = {
67109
isa = PBXGroup;
68110
children = (
69111
6C4998DF2EF7EF62006BE1DB /* Neki-iOS */,
112+
6C3B4B7D2F65C44C008449C1 /* Neki-iOSTests */,
113+
6C3B4B872F65C9AF008449C1 /* Frameworks */,
70114
6C4998DE2EF7EF62006BE1DB /* Products */,
71115
);
72116
sourceTree = "<group>";
@@ -75,13 +119,37 @@
75119
isa = PBXGroup;
76120
children = (
77121
6C4998DD2EF7EF62006BE1DB /* Neki-iOS.app */,
122+
6C3B4B7C2F65C44C008449C1 /* Neki-iOSTests.xctest */,
78123
);
79124
name = Products;
80125
sourceTree = "<group>";
81126
};
82127
/* End PBXGroup section */
83128

84129
/* Begin PBXNativeTarget section */
130+
6C3B4B7B2F65C44C008449C1 /* Neki-iOSTests */ = {
131+
isa = PBXNativeTarget;
132+
buildConfigurationList = 6C3B4B842F65C44C008449C1 /* Build configuration list for PBXNativeTarget "Neki-iOSTests" */;
133+
buildPhases = (
134+
6C3B4B782F65C44C008449C1 /* Sources */,
135+
6C3B4B792F65C44C008449C1 /* Frameworks */,
136+
6C3B4B7A2F65C44C008449C1 /* Resources */,
137+
);
138+
buildRules = (
139+
);
140+
dependencies = (
141+
6C3B4B812F65C44C008449C1 /* PBXTargetDependency */,
142+
);
143+
fileSystemSynchronizedGroups = (
144+
6C3B4B7D2F65C44C008449C1 /* Neki-iOSTests */,
145+
);
146+
name = "Neki-iOSTests";
147+
packageProductDependencies = (
148+
);
149+
productName = "Neki-iOSTests";
150+
productReference = 6C3B4B7C2F65C44C008449C1 /* Neki-iOSTests.xctest */;
151+
productType = "com.apple.product-type.bundle.unit-test";
152+
};
85153
6C4998DC2EF7EF62006BE1DB /* Neki-iOS */ = {
86154
isa = PBXNativeTarget;
87155
buildConfigurationList = 6C4998FE2EF7EF64006BE1DB /* Build configuration list for PBXNativeTarget "Neki-iOS" */;
@@ -120,9 +188,13 @@
120188
isa = PBXProject;
121189
attributes = {
122190
BuildIndependentTargetsInParallel = 1;
123-
LastSwiftUpdateCheck = 2620;
191+
LastSwiftUpdateCheck = 2630;
124192
LastUpgradeCheck = 2620;
125193
TargetAttributes = {
194+
6C3B4B7B2F65C44C008449C1 = {
195+
CreatedOnToolsVersion = 26.3;
196+
TestTargetID = 6C4998DC2EF7EF62006BE1DB;
197+
};
126198
6C4998DC2EF7EF62006BE1DB = {
127199
CreatedOnToolsVersion = 26.2;
128200
};
@@ -150,11 +222,19 @@
150222
projectRoot = "";
151223
targets = (
152224
6C4998DC2EF7EF62006BE1DB /* Neki-iOS */,
225+
6C3B4B7B2F65C44C008449C1 /* Neki-iOSTests */,
153226
);
154227
};
155228
/* End PBXProject section */
156229

157230
/* Begin PBXResourcesBuildPhase section */
231+
6C3B4B7A2F65C44C008449C1 /* Resources */ = {
232+
isa = PBXResourcesBuildPhase;
233+
buildActionMask = 2147483647;
234+
files = (
235+
);
236+
runOnlyForDeploymentPostprocessing = 0;
237+
};
158238
6C4998DB2EF7EF62006BE1DB /* Resources */ = {
159239
isa = PBXResourcesBuildPhase;
160240
buildActionMask = 2147483647;
@@ -165,6 +245,13 @@
165245
/* End PBXResourcesBuildPhase section */
166246

167247
/* Begin PBXSourcesBuildPhase section */
248+
6C3B4B782F65C44C008449C1 /* Sources */ = {
249+
isa = PBXSourcesBuildPhase;
250+
buildActionMask = 2147483647;
251+
files = (
252+
);
253+
runOnlyForDeploymentPostprocessing = 0;
254+
};
168255
6C4998D92EF7EF62006BE1DB /* Sources */ = {
169256
isa = PBXSourcesBuildPhase;
170257
buildActionMask = 2147483647;
@@ -174,7 +261,75 @@
174261
};
175262
/* End PBXSourcesBuildPhase section */
176263

264+
/* Begin PBXTargetDependency section */
265+
6C3B4B812F65C44C008449C1 /* PBXTargetDependency */ = {
266+
isa = PBXTargetDependency;
267+
target = 6C4998DC2EF7EF62006BE1DB /* Neki-iOS */;
268+
targetProxy = 6C3B4B802F65C44C008449C1 /* PBXContainerItemProxy */;
269+
};
270+
/* End PBXTargetDependency section */
271+
177272
/* Begin XCBuildConfiguration section */
273+
6C3B4B822F65C44C008449C1 /* Debug */ = {
274+
isa = XCBuildConfiguration;
275+
baseConfigurationReferenceAnchor = 6C4998DF2EF7EF62006BE1DB /* Neki-iOS */;
276+
baseConfigurationReferenceRelativePath = APP/Sources/Resources/Debug.xcconfig;
277+
buildSettings = {
278+
BUNDLE_LOADER = "$(TEST_HOST)";
279+
CODE_SIGN_IDENTITY = "Apple Development";
280+
CODE_SIGN_STYLE = Automatic;
281+
CURRENT_PROJECT_VERSION = 1;
282+
DEVELOPMENT_TEAM = 586LZSS32L;
283+
GENERATE_INFOPLIST_FILE = YES;
284+
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
285+
MARKETING_VERSION = 1.0;
286+
PRODUCT_BUNDLE_IDENTIFIER = "com.OneTen.Neki-iOSTests";
287+
PRODUCT_NAME = "$(TARGET_NAME)";
288+
PROVISIONING_PROFILE_SPECIFIER = "";
289+
STRING_CATALOG_GENERATE_SYMBOLS = NO;
290+
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
291+
SUPPORTS_MACCATALYST = NO;
292+
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
293+
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
294+
SWIFT_APPROACHABLE_CONCURRENCY = YES;
295+
SWIFT_EMIT_LOC_STRINGS = NO;
296+
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
297+
SWIFT_VERSION = 5.0;
298+
TARGETED_DEVICE_FAMILY = 1;
299+
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Neki-iOS.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Neki-iOS";
300+
};
301+
name = Debug;
302+
};
303+
6C3B4B832F65C44C008449C1 /* Release */ = {
304+
isa = XCBuildConfiguration;
305+
baseConfigurationReferenceAnchor = 6C4998DF2EF7EF62006BE1DB /* Neki-iOS */;
306+
baseConfigurationReferenceRelativePath = APP/Sources/Resources/Release.xcconfig;
307+
buildSettings = {
308+
BUNDLE_LOADER = "$(TEST_HOST)";
309+
CODE_SIGN_IDENTITY = "Apple Development";
310+
CODE_SIGN_STYLE = Automatic;
311+
CURRENT_PROJECT_VERSION = 1;
312+
DEVELOPMENT_TEAM = 586LZSS32L;
313+
GENERATE_INFOPLIST_FILE = YES;
314+
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
315+
MARKETING_VERSION = 1.0;
316+
PRODUCT_BUNDLE_IDENTIFIER = "com.OneTen.Neki-iOSTests";
317+
PRODUCT_NAME = "$(TARGET_NAME)";
318+
PROVISIONING_PROFILE_SPECIFIER = "";
319+
STRING_CATALOG_GENERATE_SYMBOLS = NO;
320+
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
321+
SUPPORTS_MACCATALYST = NO;
322+
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
323+
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
324+
SWIFT_APPROACHABLE_CONCURRENCY = YES;
325+
SWIFT_EMIT_LOC_STRINGS = NO;
326+
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
327+
SWIFT_VERSION = 5.0;
328+
TARGETED_DEVICE_FAMILY = 1;
329+
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Neki-iOS.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Neki-iOS";
330+
};
331+
name = Release;
332+
};
178333
6C4998FC2EF7EF64006BE1DB /* Debug */ = {
179334
isa = XCBuildConfiguration;
180335
baseConfigurationReferenceAnchor = 6C4998DF2EF7EF62006BE1DB /* Neki-iOS */;
@@ -407,6 +562,15 @@
407562
/* End XCBuildConfiguration section */
408563

409564
/* Begin XCConfigurationList section */
565+
6C3B4B842F65C44C008449C1 /* Build configuration list for PBXNativeTarget "Neki-iOSTests" */ = {
566+
isa = XCConfigurationList;
567+
buildConfigurations = (
568+
6C3B4B822F65C44C008449C1 /* Debug */,
569+
6C3B4B832F65C44C008449C1 /* Release */,
570+
);
571+
defaultConfigurationIsVisible = 0;
572+
defaultConfigurationName = Release;
573+
};
410574
6C4998D82EF7EF62006BE1DB /* Build configuration list for PBXProject "Neki-iOS" */ = {
411575
isa = XCConfigurationList;
412576
buildConfigurations = (

Neki-iOS.xcodeproj/xcshareddata/xcschemes/Neki-iOS.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
parallelizable = "YES">
3636
<BuildableReference
3737
BuildableIdentifier = "primary"
38-
BlueprintIdentifier = "6C4998E92EF7EF64006BE1DB"
38+
BlueprintIdentifier = "6C3B4B7B2F65C44C008449C1"
3939
BuildableName = "Neki-iOSTests.xctest"
4040
BlueprintName = "Neki-iOSTests"
4141
ReferencedContainer = "container:Neki-iOS.xcodeproj">

Neki-iOS/Core/Sources/ImagePicker/Presentation/Extension/UIImage+.swift

Lines changed: 0 additions & 28 deletions
This file was deleted.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//
2+
// DefaultImageTransformRepository.swift
3+
// Neki-iOS
4+
//
5+
// Created by OneTen on 3/14/26.
6+
//
7+
8+
import Foundation
9+
import Vision
10+
import ComposableArchitecture
11+
import CoreImage
12+
13+
public actor DefaultImageTransformRepository: ImageTransformRepository {
14+
15+
private var cachedModel: VNCoreMLModel?
16+
private let ciContext = CIContext(options: [.cacheIntermediates: false])
17+
18+
public func transform(image inputImage: CGImage) async throws -> CGImage {
19+
20+
if self.cachedModel == nil {
21+
let config = MLModelConfiguration()
22+
config.computeUnits = .all
23+
24+
let coreML = try whiteboxcartoonization(configuration: config).model
25+
self.cachedModel = try VNCoreMLModel(for: coreML)
26+
}
27+
28+
guard let visionModel = self.cachedModel else {
29+
throw ImageTransformRepositoryError.renderingFailed
30+
}
31+
32+
let request = VNCoreMLRequest(model: visionModel)
33+
request.imageCropAndScaleOption = .scaleFit
34+
35+
let handler = VNImageRequestHandler(cgImage: inputImage)
36+
try handler.perform([request])
37+
38+
return try self.extractCGImage(from: request.results)
39+
}
40+
}
41+
42+
43+
// MARK: - DefaultImageTransformRepository Private Helpers
44+
45+
private extension DefaultImageTransformRepository {
46+
func extractCGImage(from results: [VNObservation]?) throws -> CGImage {
47+
guard let observations = results as? [VNPixelBufferObservation],
48+
let pixelBuffer = observations.first?.pixelBuffer else {
49+
throw ImageTransformRepositoryError.renderingFailed
50+
}
51+
52+
let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
53+
guard let outputCGImage = self.ciContext.createCGImage(ciImage, from: ciImage.extent) else {
54+
throw ImageTransformRepositoryError.renderingFailed
55+
}
56+
57+
return outputCGImage
58+
}
59+
}
60+
61+
private enum ImageTransformRepositoryKey: DependencyKey {
62+
static let liveValue: any ImageTransformRepository = DefaultImageTransformRepository()
63+
}
64+
65+
extension DependencyValues {
66+
public var imageTransformRepository: any ImageTransformRepository {
67+
get { self[ImageTransformRepositoryKey.self] }
68+
set { self[ImageTransformRepositoryKey.self] = newValue }
69+
}
70+
}
Binary file not shown.

0 commit comments

Comments
 (0)