Swift 4, Subclassing CIFilter crashes only with “i

2019-08-02 11:54发布

问题:

How do you subclass CIFilter now? In Swift 3 I could do this as a simple example:

class CustomFilter: CIFilter {
   var inputImage: CIImage?
   var inputOrigin: CIVector?
   var inputAnotherVar: String?
}

But in Swift 4 I get an NSException. If I remove "input" from each variable it works fine. I could just do that. But I feel like I'm missing something important and I can't seem to find anything explaining this behaviour.

This compiles fine in Swift4:

class CustomFilter: CIFilter {
   var image: CIImage?
   var origin: CIVector?
   var anotherVar: String?
}

Here's the error in a Playground:

回答1:

Based on the comments, here's some Swift 4 code (unchanged from Swift 3) that both builds and executes. I'm not seeing where your issue is, so if this doesn't help you out, comment on it and I'll delete it. (If it does help, I'll edit my answer to be more specific!)

The first CIFilter uses CIColorInvert and CIHeightFieldFromMask to create a "text mask" based on the text in a UILabel. It also overrides the outputImage property of CIFilter. The second CIFilter is actually a "wrapper" around a CIKernel, using an CIImage as inputImage, the mask (from the first filter) as inputMask and also overrides outputImage like the first does.

Virtually all of this code was lifted from Core Image for Swift by Simon Gladman, nowadays available as an iBook for free. While the book was written in Swift 2, I find it to be a valuable resource for working with Core Image.

(Side note: The book combines all of this. I split out things as I was working on adapting this into an existing app as a watermark. I ended up going a different route!)

Mask.swift

public class Mask: CIFilter {
    public var inputExtent:CGRect?
    var inputRadius: Float = 15 {
        didSet {
            if oldValue != inputRadius {
                refractingImage = nil
            }
        }
    }
    private var refractingImage: CIImage?
    private var rawTextImage: CIImage?

    override public var outputImage: CIImage! {
        if refractingImage == nil {
            generateRefractingImage()
        }
        let mask = refractingImage?.applyingFilter("CIColorInvert", parameters: [:])
        return mask
    }

    func generateRefractingImage() {
        let label = UILabel(frame: inputExtent!)
        label.text = "grand canyon"
        label.font = UIFont.boldSystemFont(ofSize: 300)
        label.adjustsFontSizeToFitWidth = true
        label.textColor = UIColor.white

        UIGraphicsBeginImageContextWithOptions(
            CGSize(width: label.frame.width,
                   height: label.frame.height), true, 1)
        label.layer.render(in: UIGraphicsGetCurrentContext()!)
        let textImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        rawTextImage = CIImage(image: textImage!)!
        refractingImage = CIFilter(name: "CIHeightFieldFromMask",
                                   withInputParameters: [
                                    kCIInputRadiusKey: inputRadius,
                                    kCIInputImageKey: rawTextImage!])?.outputImage?
            .cropped(to: inputExtent!)
    }
}

Refraction.swift

public class Refraction: CIFilter {
    public var inputImage: CIImage?
    public var inputMask:CIImage?

    var inputRefractiveIndex: Float = 4.0
    var inputLensScale: Float = 50
    public var inputLightingAmount: Float = 1.5

    var inputLensBlur: CGFloat = 0
    public var inputBackgroundBlur: CGFloat = 2

    var inputRadius: Float = 15

    override public func setDefaults()
    {
        inputRefractiveIndex = 4.0
        inputLensScale = 50
        inputLightingAmount = 1.5
        inputRadius = 15
        inputLensBlur = 0
        inputBackgroundBlur = 2
    }

    override public var outputImage: CIImage! {
        guard let inputImage = inputImage, let refractingKernel = refractingKernel else {
            return nil
        }

        let extent = inputImage.extent
        let arguments = [inputImage,
                         inputMask!,
                         inputRefractiveIndex,
                         inputLensScale,
                         inputLightingAmount] as [Any]
        return refractingKernel.apply(extent: extent,
                                      roiCallback: {
                                        (index, rect) in
                                        return rect
        },
                                      arguments: arguments)!
    }

    let refractingKernel = CIKernel(source:
        "float lumaAtOffset(sampler source, vec2 origin, vec2 offset)" +
            "{" +
            " vec3 pixel = sample(source, samplerTransform(source, origin + offset)).rgb;" +
            " float luma = dot(pixel, vec3(0.2126, 0.7152, 0.0722));" +
            " return luma;" +
            "}" +


            "kernel vec4 lumaBasedRefract(sampler image, sampler refractingImage, float refractiveIndex, float lensScale, float lightingAmount) \n" +
            "{ " +
            " vec2 d = destCoord();" +

            " float northLuma = lumaAtOffset(refractingImage, d, vec2(0.0, -1.0));" +
            " float southLuma = lumaAtOffset(refractingImage, d, vec2(0.0, 1.0));" +
            " float westLuma = lumaAtOffset(refractingImage, d, vec2(-1.0, 0.0));" +
            " float eastLuma = lumaAtOffset(refractingImage, d, vec2(1.0, 0.0));" +

            " vec3 lensNormal = normalize(vec3((eastLuma - westLuma), (southLuma - northLuma), 1.0));" +

            " vec3 refractVector = refract(vec3(0.0, 0.0, 1.0), lensNormal, refractiveIndex) * lensScale; " +

            " vec3 outputPixel = sample(image, samplerTransform(image, d + refractVector.xy)).rgb;" +

            " outputPixel += (northLuma - southLuma) * lightingAmount ;" +
            " outputPixel += (eastLuma - westLuma) * lightingAmount ;" +

            " return vec4(outputPixel, 1.0);" +
        "}"
    )
}

Usage

let filterMask = Mask()
let filter = Refraction()
var imgOriginal:CIImage!
var imgMask:CIImage!
var imgEdited:CIImage!

// I have a set of sliders that update a tuple and send an action that executes the following code

filterMask.inputRadius = sliders.valuePCP.3
imgMask = filterMask.outputImage
filter.inputMask = imgMask
filter.inputRefractiveIndex = sliders.valuePCP.0
filter.inputLensScale = sliders.valuePCP.1
filter.inputLightingAmount = sliders.valuePCP.2
imgEdited = filter.outputImage

Hope this helps!



回答2:

I ran into this issue (same "error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION...") in Swift 4 while experimenting with Simon Gladman's "Core Image for Swift." I also tried running example code in an app instead of a playground. The solution for me was to add an @objc dynamic in front of var inputImage: CIImage? In your code it would look like this:

class CustomFilter: CIFilter {
    @objc dynamic var inputImage: CIImage?
    var inputOrigin: CIVector?
    var inputAnotherVar: String?
}

As I understand it, this is because Swift 4 by default minimizes inference so as to reduce binary code size. In contrast, Swift 3 implicitly infers Objc attributes. What this means in practice, is I have to add the @objc dynamic to certain variables that will take advantage of Objective-C's dynamic dispatch, such as when setting a CoreImage filter: filter.setValue(inputImage, forKey: kCIInputImageKey). Here are some resources that describe similar issues with Swift's static dispatch using Swift4 when dealing with Obj-C API's that rely on dynamic dispatch and how to deal with dispatch when you migrate from Swift 3 to 4.