parent
d2f8869823
commit
4e7529c83d
After Width: | Height: | Size: 132 KiB |
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "alcove_00003905.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 771 KiB |
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "image_masked.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 161 KiB |
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "alcove_00003905.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 170 KiB |
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "mask_.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 1.7 KiB |
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "maskBackGround.jpg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 8.1 KiB |
@ -0,0 +1,546 @@
|
|||||||
|
//
|
||||||
|
// CoreMLHelpers.swift
|
||||||
|
// Inpating
|
||||||
|
//
|
||||||
|
// Created by 間嶋大輔 on 2023/01/12.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Accelerate
|
||||||
|
import CoreML
|
||||||
|
|
||||||
|
public protocol MultiArrayType: Comparable {
|
||||||
|
static var multiArrayDataType: MLMultiArrayDataType { get }
|
||||||
|
static func +(lhs: Self, rhs: Self) -> Self
|
||||||
|
static func -(lhs: Self, rhs: Self) -> Self
|
||||||
|
static func *(lhs: Self, rhs: Self) -> Self
|
||||||
|
static func /(lhs: Self, rhs: Self) -> Self
|
||||||
|
init(_: Int)
|
||||||
|
var toUInt8: UInt8 { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Double: MultiArrayType {
|
||||||
|
public static var multiArrayDataType: MLMultiArrayDataType { return .double }
|
||||||
|
public var toUInt8: UInt8 { return UInt8(self) }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Float: MultiArrayType {
|
||||||
|
public static var multiArrayDataType: MLMultiArrayDataType { return .float32 }
|
||||||
|
public var toUInt8: UInt8 { return UInt8(self) }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Int32: MultiArrayType {
|
||||||
|
public static var multiArrayDataType: MLMultiArrayDataType { return .int32 }
|
||||||
|
public var toUInt8: UInt8 { return UInt8(self) }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MLMultiArray {
|
||||||
|
/**
|
||||||
|
Converts the multi-array to a CGImage.
|
||||||
|
The multi-array must have at least 2 dimensions for a grayscale image, or
|
||||||
|
at least 3 dimensions for a color image.
|
||||||
|
The default expected shape is (height, width) or (channels, height, width).
|
||||||
|
However, you can change this using the `axes` parameter. For example, if
|
||||||
|
the array shape is (1, height, width, channels), use `axes: (3, 1, 2)`.
|
||||||
|
If `channel` is not nil, only converts that channel to a grayscale image.
|
||||||
|
This lets you visualize individual channels from a multi-array with more
|
||||||
|
than 4 channels.
|
||||||
|
Otherwise, converts all channels. In this case, the number of channels in
|
||||||
|
the multi-array must be 1 for grayscale, 3 for RGB, or 4 for RGBA.
|
||||||
|
Use the `min` and `max` parameters to put the values from the array into
|
||||||
|
the range [0, 255], if not already:
|
||||||
|
- `min`: should be the smallest value in the data; this will be mapped to 0.
|
||||||
|
- `max`: should be the largest value in the data; will be mapped to 255.
|
||||||
|
For example, if the range of the data in the multi-array is [-1, 1], use
|
||||||
|
`min: -1, max: 1`. If the range is already [0, 255], then use the defaults.
|
||||||
|
*/
|
||||||
|
public func cgImage(min: Double = 0,
|
||||||
|
max: Double = 255,
|
||||||
|
channel: Int? = nil,
|
||||||
|
axes: (Int, Int, Int)? = nil) -> CGImage? {
|
||||||
|
switch self.dataType {
|
||||||
|
case .double:
|
||||||
|
return _image(min: min, max: max, channel: channel, axes: axes)
|
||||||
|
case .float32:
|
||||||
|
return _image(min: Float(min), max: Float(max), channel: channel, axes: axes)
|
||||||
|
case .int32:
|
||||||
|
return _image(min: Int32(min), max: Int32(max), channel: channel, axes: axes)
|
||||||
|
@unknown default:
|
||||||
|
fatalError("Unsupported data type \(dataType.rawValue)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Helper function that allows us to use generics. The type of `min` and `max`
|
||||||
|
is also the dataType of the MLMultiArray.
|
||||||
|
*/
|
||||||
|
private func _image<T: MultiArrayType>(min: T,
|
||||||
|
max: T,
|
||||||
|
channel: Int?,
|
||||||
|
axes: (Int, Int, Int)?) -> CGImage? {
|
||||||
|
if let (b, w, h, c) = toRawBytes(min: min, max: max, channel: channel, axes: axes) {
|
||||||
|
if c == 1 {
|
||||||
|
return CGImage.fromByteArrayGray(b, width: w, height: h)
|
||||||
|
} else {
|
||||||
|
return CGImage.fromByteArrayRGBA(b, width: w, height: h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Converts the multi-array into an array of RGBA or grayscale pixels.
|
||||||
|
- Note: This is not particularly fast, but it is flexible. You can change
|
||||||
|
the loops to convert the multi-array whichever way you please.
|
||||||
|
- Note: The type of `min` and `max` must match the dataType of the
|
||||||
|
MLMultiArray object.
|
||||||
|
- Returns: tuple containing the RGBA bytes, the dimensions of the image,
|
||||||
|
and the number of channels in the image (1, 3, or 4).
|
||||||
|
*/
|
||||||
|
public func toRawBytes<T: MultiArrayType>(min: T,
|
||||||
|
max: T,
|
||||||
|
channel: Int? = nil,
|
||||||
|
axes: (Int, Int, Int)? = nil)
|
||||||
|
-> (bytes: [UInt8], width: Int, height: Int, channels: Int)? {
|
||||||
|
// MLMultiArray with unsupported shape?
|
||||||
|
if shape.count < 2 {
|
||||||
|
print("Cannot convert MLMultiArray of shape \(shape) to image")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Figure out which dimensions to use for the channels, height, and width.
|
||||||
|
let channelAxis: Int
|
||||||
|
let heightAxis: Int
|
||||||
|
let widthAxis: Int
|
||||||
|
if let axes = axes {
|
||||||
|
channelAxis = axes.0
|
||||||
|
heightAxis = axes.1
|
||||||
|
widthAxis = axes.2
|
||||||
|
guard channelAxis >= 0 && channelAxis < shape.count &&
|
||||||
|
heightAxis >= 0 && heightAxis < shape.count &&
|
||||||
|
widthAxis >= 0 && widthAxis < shape.count else {
|
||||||
|
print("Invalid axes \(axes) for shape \(shape)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else if shape.count == 2 {
|
||||||
|
// Expected shape for grayscale is (height, width)
|
||||||
|
heightAxis = 0
|
||||||
|
widthAxis = 1
|
||||||
|
channelAxis = -1 // Never be used
|
||||||
|
} else {
|
||||||
|
// Expected shape for color is (channels, height, width)
|
||||||
|
channelAxis = 0
|
||||||
|
heightAxis = 1
|
||||||
|
widthAxis = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
let height = self.shape[heightAxis].intValue
|
||||||
|
let width = self.shape[widthAxis].intValue
|
||||||
|
let yStride = self.strides[heightAxis].intValue
|
||||||
|
let xStride = self.strides[widthAxis].intValue
|
||||||
|
|
||||||
|
let channels: Int
|
||||||
|
let cStride: Int
|
||||||
|
let bytesPerPixel: Int
|
||||||
|
let channelOffset: Int
|
||||||
|
|
||||||
|
// MLMultiArray with just two dimensions is always grayscale. (We ignore
|
||||||
|
// the value of channelAxis here.)
|
||||||
|
if shape.count == 2 {
|
||||||
|
channels = 1
|
||||||
|
cStride = 0
|
||||||
|
bytesPerPixel = 1
|
||||||
|
channelOffset = 0
|
||||||
|
|
||||||
|
// MLMultiArray with more than two dimensions can be color or grayscale.
|
||||||
|
} else {
|
||||||
|
let channelDim = self.shape[channelAxis].intValue
|
||||||
|
if let channel = channel {
|
||||||
|
if channel < 0 || channel >= channelDim {
|
||||||
|
print("Channel must be -1, or between 0 and \(channelDim - 1)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
channels = 1
|
||||||
|
bytesPerPixel = 1
|
||||||
|
channelOffset = channel
|
||||||
|
} else if channelDim == 1 {
|
||||||
|
channels = 1
|
||||||
|
bytesPerPixel = 1
|
||||||
|
channelOffset = 0
|
||||||
|
} else {
|
||||||
|
if channelDim != 3 && channelDim != 4 {
|
||||||
|
print("Expected channel dimension to have 1, 3, or 4 channels, got \(channelDim)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
channels = channelDim
|
||||||
|
bytesPerPixel = 4
|
||||||
|
channelOffset = 0
|
||||||
|
}
|
||||||
|
cStride = self.strides[channelAxis].intValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate storage for the RGBA or grayscale pixels. Set everything to
|
||||||
|
// 255 so that alpha channel is filled in if only 3 channels.
|
||||||
|
let count = height * width * bytesPerPixel
|
||||||
|
var pixels = [UInt8](repeating: 255, count: count)
|
||||||
|
|
||||||
|
// Grab the pointer to MLMultiArray's memory.
|
||||||
|
var ptr = UnsafeMutablePointer<T>(OpaquePointer(self.dataPointer))
|
||||||
|
ptr = ptr.advanced(by: channelOffset * cStride)
|
||||||
|
|
||||||
|
// Loop through all the pixels and all the channels and copy them over.
|
||||||
|
for c in 0..<channels {
|
||||||
|
for y in 0..<height {
|
||||||
|
for x in 0..<width {
|
||||||
|
let value = ptr[c*cStride + y*yStride + x*xStride]
|
||||||
|
let scaled = (value - min) * T(255) / (max - min)
|
||||||
|
let pixel = clamp(scaled, min: T(0), max: T(255)).toUInt8
|
||||||
|
pixels[(y*width + x)*bytesPerPixel + c] = pixel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (pixels, width, height, channels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Fast conversion from MLMultiArray to CGImage using the vImage framework.
|
||||||
|
- Parameters:
|
||||||
|
- features: A multi-array with data type FLOAT32 and three dimensions
|
||||||
|
(3, height, width).
|
||||||
|
- min: The smallest value in the multi-array. This value, as well as any
|
||||||
|
smaller values, will be mapped to 0 in the output image.
|
||||||
|
- max: The largest value in the multi-array. This and any larger values
|
||||||
|
will be will be mapped to 255 in the output image.
|
||||||
|
- Returns: a new CGImage or nil if the conversion fails
|
||||||
|
*/
|
||||||
|
public func createCGImage(fromFloatArray features: MLMultiArray,
|
||||||
|
min: Float = 0,
|
||||||
|
max: Float = 255) -> CGImage? {
|
||||||
|
assert(features.dataType == .float32)
|
||||||
|
assert(features.shape.count == 3)
|
||||||
|
|
||||||
|
let ptr = UnsafeMutablePointer<Float>(OpaquePointer(features.dataPointer))
|
||||||
|
|
||||||
|
let height = features.shape[1].intValue
|
||||||
|
let width = features.shape[2].intValue
|
||||||
|
let channelStride = features.strides[0].intValue
|
||||||
|
let rowStride = features.strides[1].intValue
|
||||||
|
let srcRowBytes = rowStride * MemoryLayout<Float>.stride
|
||||||
|
|
||||||
|
var blueBuffer = vImage_Buffer(data: ptr,
|
||||||
|
height: vImagePixelCount(height),
|
||||||
|
width: vImagePixelCount(width),
|
||||||
|
rowBytes: srcRowBytes)
|
||||||
|
var greenBuffer = vImage_Buffer(data: ptr.advanced(by: channelStride),
|
||||||
|
height: vImagePixelCount(height),
|
||||||
|
width: vImagePixelCount(width),
|
||||||
|
rowBytes: srcRowBytes)
|
||||||
|
var redBuffer = vImage_Buffer(data: ptr.advanced(by: channelStride * 2),
|
||||||
|
height: vImagePixelCount(height),
|
||||||
|
width: vImagePixelCount(width),
|
||||||
|
rowBytes: srcRowBytes)
|
||||||
|
|
||||||
|
let destRowBytes = width * 4
|
||||||
|
var pixels = [UInt8](repeating: 0, count: height * destRowBytes)
|
||||||
|
var destBuffer = vImage_Buffer(data: &pixels,
|
||||||
|
height: vImagePixelCount(height),
|
||||||
|
width: vImagePixelCount(width),
|
||||||
|
rowBytes: destRowBytes)
|
||||||
|
|
||||||
|
let error = vImageConvert_PlanarFToBGRX8888(&blueBuffer,
|
||||||
|
&greenBuffer,
|
||||||
|
&redBuffer,
|
||||||
|
Pixel_8(255),
|
||||||
|
&destBuffer,
|
||||||
|
[max, max, max],
|
||||||
|
[min, min, min],
|
||||||
|
vImage_Flags(0))
|
||||||
|
if error == kvImageNoError {
|
||||||
|
return CGImage.fromByteArrayRGBA(pixels, width: width, height: height)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(UIKit)
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension MLMultiArray {
|
||||||
|
public func image(min: Double = 0,
|
||||||
|
max: Double = 255,
|
||||||
|
channel: Int? = nil,
|
||||||
|
axes: (Int, Int, Int)? = nil) -> UIImage? {
|
||||||
|
let cgImg = cgImage(min: min, max: max, channel: channel, axes: axes)
|
||||||
|
return cgImg.map { UIImage(cgImage: $0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func createUIImage(fromFloatArray features: MLMultiArray,
|
||||||
|
min: Float = 0,
|
||||||
|
max: Float = 255) -> UIImage? {
|
||||||
|
let cgImg = createCGImage(fromFloatArray: features, min: min, max: max)
|
||||||
|
return cgImg.map { UIImage(cgImage: $0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public func clamp<T: Comparable>(_ x: T, min: T, max: T) -> T {
|
||||||
|
if x < min { return min }
|
||||||
|
if x > max { return max }
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
import CoreGraphics
|
||||||
|
|
||||||
|
extension CGImage {
|
||||||
|
/**
|
||||||
|
Converts the image into an array of RGBA bytes.
|
||||||
|
*/
|
||||||
|
@nonobjc public func toByteArrayRGBA() -> [UInt8] {
|
||||||
|
var bytes = [UInt8](repeating: 0, count: width * height * 4)
|
||||||
|
bytes.withUnsafeMutableBytes { ptr in
|
||||||
|
if let colorSpace = colorSpace,
|
||||||
|
let context = CGContext(
|
||||||
|
data: ptr.baseAddress,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
bitsPerComponent: bitsPerComponent,
|
||||||
|
bytesPerRow: bytesPerRow,
|
||||||
|
space: colorSpace,
|
||||||
|
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) {
|
||||||
|
let rect = CGRect(x: 0, y: 0, width: width, height: height)
|
||||||
|
context.draw(self, in: rect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Creates a new CGImage from an array of RGBA bytes.
|
||||||
|
*/
|
||||||
|
@nonobjc public class func fromByteArrayRGBA(_ bytes: [UInt8],
|
||||||
|
width: Int,
|
||||||
|
height: Int) -> CGImage? {
|
||||||
|
return fromByteArray(bytes, width: width, height: height,
|
||||||
|
bytesPerRow: width * 4,
|
||||||
|
colorSpace: CGColorSpaceCreateDeviceRGB(),
|
||||||
|
alphaInfo: .premultipliedLast)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Creates a new CGImage from an array of grayscale bytes.
|
||||||
|
*/
|
||||||
|
@nonobjc public class func fromByteArrayGray(_ bytes: [UInt8],
|
||||||
|
width: Int,
|
||||||
|
height: Int) -> CGImage? {
|
||||||
|
return fromByteArray(bytes, width: width, height: height,
|
||||||
|
bytesPerRow: width,
|
||||||
|
colorSpace: CGColorSpaceCreateDeviceGray(),
|
||||||
|
alphaInfo: .none)
|
||||||
|
}
|
||||||
|
|
||||||
|
@nonobjc class func fromByteArray(_ bytes: [UInt8],
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
bytesPerRow: Int,
|
||||||
|
colorSpace: CGColorSpace,
|
||||||
|
alphaInfo: CGImageAlphaInfo) -> CGImage? {
|
||||||
|
return bytes.withUnsafeBytes { ptr in
|
||||||
|
let context = CGContext(data: UnsafeMutableRawPointer(mutating: ptr.baseAddress!),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
bitsPerComponent: 8,
|
||||||
|
bytesPerRow: bytesPerRow,
|
||||||
|
space: colorSpace,
|
||||||
|
bitmapInfo: alphaInfo.rawValue)
|
||||||
|
return context?.makeImage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension CGImage {
|
||||||
|
var frame: CGRect {
|
||||||
|
return CGRect(x: 0, y: 0, width: self.width, height: self.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toBGR()->CGImage{
|
||||||
|
let ciImage = CIImage(cgImage: self)
|
||||||
|
let kernelStr: String = """
|
||||||
|
kernel vec4 swapRedAndGreenAmount(__sample s) {
|
||||||
|
return s.bgra;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
let ctx = CIContext(options: nil)
|
||||||
|
let swapKernel = CIColorKernel( source:
|
||||||
|
"kernel vec4 swapRedAndGreenAmount(__sample s) {" +
|
||||||
|
"return s.bgra;" +
|
||||||
|
"}"
|
||||||
|
)
|
||||||
|
let ciOutput = swapKernel?.apply(extent: (ciImage.extent), arguments: [ciImage as Any])
|
||||||
|
let cgOut:CGImage = ctx.createCGImage(ciOutput!, from: ciOutput!.extent)!
|
||||||
|
return cgOut
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension UIImage {
|
||||||
|
func mlMultiArray(scale preprocessScale:Double=1/255, rBias preprocessRBias:Double=0, gBias preprocessGBias:Double=0, bBias preprocessBBias:Double=0) -> MLMultiArray {
|
||||||
|
let imagePixel = self.getPixelRgb(scale: preprocessScale, rBias: preprocessRBias, gBias: preprocessGBias, bBias: preprocessBBias)
|
||||||
|
// let size = self.size
|
||||||
|
let imagePointer : UnsafePointer<Double> = UnsafePointer(imagePixel)
|
||||||
|
let mlArray = try! MLMultiArray(shape: [1,3, NSNumber(value: Float(512)), NSNumber(value: Float(512))], dataType: MLMultiArrayDataType.double)
|
||||||
|
mlArray.dataPointer.initializeMemory(as: Double.self, from: imagePointer, count: imagePixel.count)
|
||||||
|
|
||||||
|
return mlArray
|
||||||
|
}
|
||||||
|
|
||||||
|
func mlMultiArrayGrayScale(scale preprocessScale:Double=1/255,bias preprocessBias:Double=0) -> MLMultiArray {
|
||||||
|
let imagePixel = self.getPixelGrayScale(scale: preprocessScale, bias: preprocessBias)
|
||||||
|
// let size = self.size
|
||||||
|
let imagePointer : UnsafePointer<Double> = UnsafePointer(imagePixel)
|
||||||
|
let mlArray = try! MLMultiArray(shape: [1,1, NSNumber(value: Float(512)), NSNumber(value: Float(512))], dataType: MLMultiArrayDataType.double)
|
||||||
|
mlArray.dataPointer.initializeMemory(as: Double.self, from: imagePointer, count: imagePixel.count)
|
||||||
|
return mlArray
|
||||||
|
}
|
||||||
|
|
||||||
|
func mlMultiArrayComposite(outImage out:UIImage, inputImage input:UIImage, maskImage mask: UIImage, scale preprocessScale:Double=1/255, rBias preprocessRBias:Double=0, gBias preprocessGBias:Double=0, bBias preprocessBBias:Double=0) -> MLMultiArray {
|
||||||
|
let imagePixel = self.getMaskedPixelRgb(out: out, input: input, mask: mask)
|
||||||
|
// let size = self.size
|
||||||
|
let imagePointer : UnsafePointer<Double> = UnsafePointer(imagePixel)
|
||||||
|
let mlArray = try! MLMultiArray(shape: [1,3, NSNumber(value: Float(512)), NSNumber(value: Float(512))], dataType: MLMultiArrayDataType.double)
|
||||||
|
mlArray.dataPointer.initializeMemory(as: Double.self, from: imagePointer, count: imagePixel.count)
|
||||||
|
|
||||||
|
return mlArray
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMaskedPixelRgb(out: UIImage,input: UIImage, mask:UIImage, scale preprocessScale:Double=1, rBias preprocessRBias:Double=0, gBias preprocessGBias:Double=0, bBias preprocessBBias:Double=0) -> [Double]
|
||||||
|
{
|
||||||
|
guard let outCGImage = out.cgImage?.resize(size: CGSize(width: 512, height: 512)) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let outbytesPerRow = outCGImage.bytesPerRow
|
||||||
|
let outwidth = outCGImage.width
|
||||||
|
let outheight = outCGImage.height
|
||||||
|
let outbytesPerPixel = 4
|
||||||
|
let outpixelData = outCGImage.dataProvider!.data! as Data
|
||||||
|
|
||||||
|
guard let inputCGImage = input.cgImage?.resize(size: CGSize(width: 512, height: 512)) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let inputpixelData = inputCGImage.dataProvider!.data! as Data
|
||||||
|
|
||||||
|
guard let maskCgImage = mask.cgImage?.resize(size: CGSize(width: 512, height: 512)) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let maskBytesPerRow = maskCgImage.bytesPerRow
|
||||||
|
let maskBytesPerPixel = 4
|
||||||
|
let maskPixelData = maskCgImage.dataProvider!.data! as Data
|
||||||
|
|
||||||
|
var r_buf : [Double] = []
|
||||||
|
var g_buf : [Double] = []
|
||||||
|
var b_buf : [Double] = []
|
||||||
|
|
||||||
|
for j in 0..<outheight {
|
||||||
|
for i in 0..<outwidth {
|
||||||
|
let pixelInfo = outbytesPerRow * j + i * outbytesPerPixel
|
||||||
|
let maskPixelInfo = maskBytesPerRow * j + i * maskBytesPerPixel
|
||||||
|
let v = Double(maskPixelData[maskPixelInfo])
|
||||||
|
|
||||||
|
let r = Double(outpixelData[pixelInfo])
|
||||||
|
let g = Double(outpixelData[pixelInfo+1])
|
||||||
|
let b = Double(outpixelData[pixelInfo+2])
|
||||||
|
let bgr = Double(inputpixelData[pixelInfo+1])
|
||||||
|
let bgg = Double(inputpixelData[pixelInfo+2])
|
||||||
|
let bgb = Double(inputpixelData[pixelInfo+3])
|
||||||
|
if v > 0 {
|
||||||
|
r_buf.append(Double(r*preprocessScale)+preprocessRBias)
|
||||||
|
g_buf.append(Double(g*preprocessScale)+preprocessGBias)
|
||||||
|
b_buf.append(Double(b*preprocessScale)+preprocessBBias)
|
||||||
|
} else {
|
||||||
|
r_buf.append(Double(bgr*preprocessScale)+preprocessRBias)
|
||||||
|
g_buf.append(Double(bgg*preprocessScale)+preprocessGBias)
|
||||||
|
b_buf.append(Double(bgb*preprocessScale)+preprocessBBias)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ((r_buf + g_buf) + b_buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPixelRgb(scale preprocessScale:Double=1/255, rBias preprocessRBias:Double=0, gBias preprocessGBias:Double=0, bBias preprocessBBias:Double=0) -> [Double]
|
||||||
|
{
|
||||||
|
guard let cgImage = self.cgImage?.resize(size: CGSize(width: 512, height: 512)) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let bytesPerRow = cgImage.bytesPerRow
|
||||||
|
let width = cgImage.width
|
||||||
|
let height = cgImage.height
|
||||||
|
let bytesPerPixel = 4
|
||||||
|
let pixelData = cgImage.dataProvider!.data! as Data
|
||||||
|
|
||||||
|
var r_buf : [Double] = []
|
||||||
|
var g_buf : [Double] = []
|
||||||
|
var b_buf : [Double] = []
|
||||||
|
|
||||||
|
for j in 0..<height {
|
||||||
|
for i in 0..<width {
|
||||||
|
let pixelInfo = bytesPerRow * j + i * bytesPerPixel
|
||||||
|
let r = Double(pixelData[pixelInfo+1])
|
||||||
|
let g = Double(pixelData[pixelInfo+2])
|
||||||
|
let b = Double(pixelData[pixelInfo+3])
|
||||||
|
r_buf.append(Double(r*preprocessScale)+preprocessRBias)
|
||||||
|
g_buf.append(Double(g*preprocessScale)+preprocessGBias)
|
||||||
|
b_buf.append(Double(b*preprocessScale)+preprocessBBias)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ((r_buf + g_buf) + b_buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPixelGrayScale(scale preprocessScale:Double=1/255, bias preprocessBias:Double=0) -> [Double]
|
||||||
|
{
|
||||||
|
guard let cgImage = self.cgImage?.resize(size: CGSize(width: 512, height: 512)) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let bytesPerRow = cgImage.bytesPerRow
|
||||||
|
let width = cgImage.width
|
||||||
|
let height = cgImage.height
|
||||||
|
let bytesPerPixel = 4
|
||||||
|
let pixelData = cgImage.dataProvider!.data! as Data
|
||||||
|
|
||||||
|
var buf : [Double] = []
|
||||||
|
|
||||||
|
for j in 0..<height {
|
||||||
|
for i in 0..<width {
|
||||||
|
let pixelInfo = bytesPerRow * j + i * bytesPerPixel
|
||||||
|
let v = Double(pixelData[pixelInfo])
|
||||||
|
buf.append(Double(v*preprocessScale)+preprocessBias)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CGImage {
|
||||||
|
func resize(size:CGSize) -> CGImage? {
|
||||||
|
let width: Int = Int(size.width)
|
||||||
|
let height: Int = Int(size.height)
|
||||||
|
|
||||||
|
let bytesPerPixel = self.bitsPerPixel / self.bitsPerComponent
|
||||||
|
let destBytesPerRow = width * bytesPerPixel
|
||||||
|
|
||||||
|
|
||||||
|
guard let colorSpace = self.colorSpace else { return nil }
|
||||||
|
guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: self.bitsPerComponent, bytesPerRow: destBytesPerRow, space: colorSpace, bitmapInfo: self.alphaInfo.rawValue) else { return nil }
|
||||||
|
|
||||||
|
context.interpolationQuality = .high
|
||||||
|
context.draw(self, in: CGRect(x: 0, y: 0, width: width, height: height))
|
||||||
|
|
||||||
|
return context.makeImage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
|||||||
|
//
|
||||||
|
// Utils.swift
|
||||||
|
// AOT-GAN-for-Inpainting-CoreML
|
||||||
|
//
|
||||||
|
// Created by 間嶋大輔 on 2023/02/07.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
@ -1,19 +1,269 @@
|
|||||||
//
|
|
||||||
// ViewController.swift
|
|
||||||
// AOT-GAN-for-Inpainting-CoreML
|
|
||||||
//
|
|
||||||
// Created by 間嶋大輔 on 2023/02/03.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import Vision
|
||||||
|
import CoreML
|
||||||
|
import Drawsana
|
||||||
|
import PhotosUI
|
||||||
|
|
||||||
class ViewController: UIViewController {
|
class ViewController: UIViewController,PHPickerViewControllerDelegate, UIPickerViewDelegate {
|
||||||
|
|
||||||
|
var imageView:UIImageView!
|
||||||
|
let drawingView = DrawsanaView()
|
||||||
|
let penTool = PenTool()
|
||||||
|
let undoButton = UIButton()
|
||||||
|
let runButton = UIButton()
|
||||||
|
let selectPhotoButton = UIButton()
|
||||||
|
let superResolutionButton = UIButton()
|
||||||
|
let brushButton = UIButton()
|
||||||
|
var inputImage: UIImage?
|
||||||
|
let ciContext = CIContext()
|
||||||
|
lazy var maskBackGroundImage: UIImage = {
|
||||||
|
guard let image = UIImage(named: "maskBackGround") else { fatalError("Please set black image ") }
|
||||||
|
return image
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var model: MLModel? = {
|
||||||
|
do {
|
||||||
|
let config = MLModelConfiguration()
|
||||||
|
config.computeUnits = .cpuAndGPU
|
||||||
|
let model = try aotgan(configuration: config).model
|
||||||
|
return model
|
||||||
|
} catch let error {
|
||||||
|
print(error)
|
||||||
|
fatalError("model initialize error")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var srRequest: VNCoreMLRequest = {
|
||||||
|
do {
|
||||||
|
let config = MLModelConfiguration()
|
||||||
|
config.computeUnits = .cpuAndGPU
|
||||||
|
let model = try realesrgangeneral512(configuration: config).model
|
||||||
|
let vnModel = try VNCoreMLModel(for: model)
|
||||||
|
let request = VNCoreMLRequest(model: vnModel)
|
||||||
|
request.imageCropAndScaleOption = .scaleFill
|
||||||
|
return request
|
||||||
|
} catch let error {
|
||||||
|
print(error)
|
||||||
|
fatalError("model initialize error")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
// Do any additional setup after loading the view.
|
setupView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func inference(maskedImage inputImage:UIImage, maskImage mask:UIImage) {
|
||||||
|
guard let model = model else { fatalError("Model initialize error.") }
|
||||||
|
do {
|
||||||
|
let mask_1 = mask.mlMultiArrayGrayScale(scale: 1/255)
|
||||||
|
|
||||||
|
// input
|
||||||
|
|
||||||
|
let x_1 = inputImage.mlMultiArray(scale: 1/127.5,rBias: -1,gBias: -1, bBias: -1)
|
||||||
|
|
||||||
|
let inputTensor = MLMultiArray(concatenating: [x_1, mask_1],
|
||||||
|
axis: 1,
|
||||||
|
dataType: .float32)
|
||||||
|
|
||||||
|
let input = aotganInput(x_1: inputTensor)
|
||||||
|
|
||||||
|
// run
|
||||||
|
let start = Date()
|
||||||
|
let out = try model.prediction(from: input)
|
||||||
|
let timeElapsed = -start.timeIntervalSinceNow
|
||||||
|
print(timeElapsed)
|
||||||
|
let outArray = out.featureValue(for: "var_915")?.multiArrayValue
|
||||||
|
let outImage = outArray!.cgImage(min: -1,max: 1, axes: (1,2,3))
|
||||||
|
let uiOut = UIImage(cgImage: outImage!)
|
||||||
|
let originalSize = self.inputImage?.size
|
||||||
|
|
||||||
|
let comp = uiOut.mlMultiArrayComposite(outImage: uiOut, inputImage: inputImage, maskImage: mask).cgImage(axes: (1,2,3))?.resize(size: originalSize!)
|
||||||
|
let final = UIImage(cgImage: comp!)
|
||||||
|
|
||||||
|
self.inputImage = final
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
|
||||||
|
self.resetDrawingView()
|
||||||
|
self.imageView.image = final
|
||||||
|
}
|
||||||
|
print("Done")
|
||||||
|
} catch let error {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetDrawingView() {
|
||||||
|
if drawingView.drawing.shapes.count != 0 {
|
||||||
|
for _ in 0...drawingView.drawing.shapes.count-1 {
|
||||||
|
drawingView.operationStack.undo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupView() {
|
||||||
|
imageView = UIImageView(frame: view.bounds)
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
view.addSubview(imageView)
|
||||||
|
inputImage = UIImage(named: "input")
|
||||||
|
imageView.image = inputImage
|
||||||
|
|
||||||
|
drawingView.set(tool: penTool)
|
||||||
|
drawingView.userSettings.strokeWidth = 20
|
||||||
|
drawingView.userSettings.strokeColor = .white
|
||||||
|
drawingView.userSettings.fillColor = .black
|
||||||
|
|
||||||
|
drawingView.userSettings.fontSize = 24
|
||||||
|
drawingView.userSettings.fontName = "Marker Felt"
|
||||||
|
drawingView.frame = imageView.frame
|
||||||
|
view.addSubview(drawingView)
|
||||||
|
let buttonWidth = view.bounds.width*0.3
|
||||||
|
undoButton.frame = CGRect(x: view.bounds.width*0.025, y: 50, width: buttonWidth, height: 50)
|
||||||
|
superResolutionButton.frame = CGRect(x: view.bounds.maxX-view.bounds.width*0.025-buttonWidth, y: 50, width: buttonWidth, height: 50)
|
||||||
|
brushButton.frame = CGRect(x: undoButton.frame.maxX + view.bounds.width * 0.025, y: 50, width: buttonWidth, height: 50)
|
||||||
|
selectPhotoButton.frame = CGRect(x: view.bounds.width*0.1, y: view.bounds.maxY - 100, width: buttonWidth, height: 50)
|
||||||
|
runButton.frame = CGRect(x: view.bounds.maxX - view.bounds.width*0.1 - buttonWidth, y: view.bounds.maxY - 100, width: buttonWidth, height: 50)
|
||||||
|
undoButton.setTitle("undoDraw", for: .normal)
|
||||||
|
selectPhotoButton.setTitle("select Photo", for: .normal)
|
||||||
|
superResolutionButton.setTitle("SR", for: .normal)
|
||||||
|
brushButton.setTitle("brushWidth", for: .normal)
|
||||||
|
runButton.setTitle("run", for: .normal)
|
||||||
|
undoButton.backgroundColor = .gray
|
||||||
|
undoButton.setTitleColor(.white, for: .normal)
|
||||||
|
selectPhotoButton.backgroundColor = .gray
|
||||||
|
selectPhotoButton.setTitleColor(.white, for: .normal)
|
||||||
|
superResolutionButton.backgroundColor = .gray
|
||||||
|
superResolutionButton.setTitleColor(.white, for: .normal)
|
||||||
|
brushButton.backgroundColor = .gray
|
||||||
|
brushButton.setTitleColor(.white, for: .normal)
|
||||||
|
runButton.backgroundColor = .gray
|
||||||
|
runButton.setTitleColor(.white, for: .normal)
|
||||||
|
selectPhotoButton.addTarget(self, action: #selector(presentPhPicker), for: .touchUpInside)
|
||||||
|
superResolutionButton.addTarget(self, action: #selector(sr), for: .touchUpInside)
|
||||||
|
brushButton.addTarget(self, action: #selector(brushWidth), for: .touchUpInside)
|
||||||
|
|
||||||
|
undoButton.addTarget(self, action: #selector(undo), for: .touchUpInside)
|
||||||
|
runButton.addTarget(self, action: #selector(run), for: .touchUpInside)
|
||||||
|
view.addSubview(selectPhotoButton)
|
||||||
|
view.addSubview(superResolutionButton)
|
||||||
|
view.addSubview(undoButton)
|
||||||
|
view.addSubview(brushButton)
|
||||||
|
|
||||||
|
view.addSubview(runButton)
|
||||||
|
adjustDrawingViewSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func brushWidth(){
|
||||||
|
let ac = UIAlertController(title: "SelectBrush", message: "", preferredStyle: .alert)
|
||||||
|
ac.addAction(UIAlertAction(title: "thin", style: .default,handler: { action in
|
||||||
|
self.drawingView.userSettings.strokeWidth = 5
|
||||||
|
}))
|
||||||
|
ac.addAction(UIAlertAction(title: "medium", style: .default,handler: { action in
|
||||||
|
self.drawingView.userSettings.strokeWidth = 20
|
||||||
|
}))
|
||||||
|
ac.addAction(UIAlertAction(title: "thin", style: .default,handler: { action in
|
||||||
|
self.drawingView.userSettings.strokeWidth = 40
|
||||||
|
}))
|
||||||
|
present(ac, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func undo() {
|
||||||
|
drawingView.operationStack.undo()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func run() {
|
||||||
|
guard let overlapImage:UIImage = drawingView.render(over:inputImage),
|
||||||
|
let maskImage:UIImage = drawingView.render()
|
||||||
|
else { fatalError("Mask overlap error") }
|
||||||
|
inference(maskedImage: overlapImage, maskImage: maskImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func presentPhPicker(){
|
||||||
|
var configuration = PHPickerConfiguration()
|
||||||
|
configuration.selectionLimit = 1
|
||||||
|
configuration.filter = .images
|
||||||
|
let picker = PHPickerViewController(configuration: configuration)
|
||||||
|
picker.delegate = self
|
||||||
|
present(picker, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func sr() {
|
||||||
|
let handler = VNImageRequestHandler(ciImage: CIImage(image: inputImage!)!)
|
||||||
|
do {
|
||||||
|
try handler.perform([srRequest])
|
||||||
|
guard let result = srRequest.results?.first as? VNPixelBufferObservation else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let srCIImage = CIImage(cvPixelBuffer: result.pixelBuffer)
|
||||||
|
let resizedCGImage = ciContext.createCGImage(srCIImage, from: srCIImage.extent)?.resize(size: CGSize(width: inputImage!.size.width, height: inputImage!.size.height))
|
||||||
|
let srUIImage = UIImage(cgImage: resizedCGImage!)
|
||||||
|
inputImage = srUIImage
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.imageView.image = srUIImage
|
||||||
|
self.adjustDrawingViewSize()
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||||
|
picker.dismiss(animated: true)
|
||||||
|
guard let result = results.first else { return }
|
||||||
|
if result.itemProvider.canLoadObject(ofClass: UIImage.self) {
|
||||||
|
result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
|
||||||
|
if let image = image as? UIImage, let safeSelf = self {
|
||||||
|
let correctOrientImage = safeSelf.getCorrectOrientationUIImage(uiImage: image)
|
||||||
|
safeSelf.inputImage = correctOrientImage
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
safeSelf.resetDrawingView()
|
||||||
|
safeSelf.imageView.image = correctOrientImage
|
||||||
|
safeSelf.adjustDrawingViewSize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCorrectOrientationUIImage(uiImage:UIImage) -> UIImage {
|
||||||
|
var newImage = UIImage()
|
||||||
|
let ciContext = CIContext()
|
||||||
|
switch uiImage.imageOrientation.rawValue {
|
||||||
|
case 1:
|
||||||
|
guard let orientedCIImage = CIImage(image: uiImage)?.oriented(CGImagePropertyOrientation.down),
|
||||||
|
let cgImage = ciContext.createCGImage(orientedCIImage, from: orientedCIImage.extent) else { return uiImage}
|
||||||
|
|
||||||
|
newImage = UIImage(cgImage: cgImage)
|
||||||
|
case 3:
|
||||||
|
guard let orientedCIImage = CIImage(image: uiImage)?.oriented(CGImagePropertyOrientation.right),
|
||||||
|
let cgImage = ciContext.createCGImage(orientedCIImage, from: orientedCIImage.extent) else { return uiImage}
|
||||||
|
newImage = UIImage(cgImage: cgImage)
|
||||||
|
default:
|
||||||
|
newImage = uiImage
|
||||||
|
}
|
||||||
|
return newImage
|
||||||
|
}
|
||||||
|
|
||||||
|
func adjustDrawingViewSize() {
|
||||||
|
let displayAspect = imageView.frame.height / imageView.frame.width
|
||||||
|
let imageSize = imageView.image!.size
|
||||||
|
let imageAspect = imageSize.height / imageSize.width
|
||||||
|
if imageAspect <= displayAspect {
|
||||||
|
// fit to width
|
||||||
|
let minX = imageView.frame.minX
|
||||||
|
let minY = imageView.center.y - (imageView.frame.width * imageAspect / 2)
|
||||||
|
let width = imageView.frame.width
|
||||||
|
let height = imageView.frame.width * imageAspect
|
||||||
|
drawingView.frame = CGRect(x: minX, y: minY, width: width, height: height)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// fit to height
|
||||||
|
let aspect = imageSize.width / imageSize.height
|
||||||
|
drawingView.frame = CGRect(x: imageView.center.x - (imageView.frame.height * aspect / 2), y: imageView.frame.minY, width: imageView.frame.height * aspect, height: imageView.frame.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue