Metal Core Image Kernel Sampling

655 Views Asked by At

I wrote the following test kernel to understand sampling in Metal Core Image Shaders. What I want to achieve is the following. Any pixel outside the bounds(extent) of the inputImage should be black, other pixels should be pixels of inputImage as usual. But I don't see the desired output so something is wrong in my understanding of how samplers work in shaders. There is no easy way to grab the world coordinates of inputImage, only destination supports world coordinates. Here is my code.

extern "C" float4 testKernel(coreimage::sampler inputImage, coreimage::destination dest)
 {
  float2 inputCoordinate = inputImage.coord();
  float4 color = inputImage.sample(inputCoordinate);
  float2 inputOrigin = inputImage.origin();
  float2 inputSize = inputImage.size();
  float2 destCoord = dest.coord();

   if (inputCoordinate.x * inputSize.x < destCoord.x || inputCoordinate.y * inputSize.y > destCoord.y) {
       return float4(0.0, 0.0, 0.0, 1.0);
   }


   return color;
}

And here is Swift code for the filter:

 class CIMetalTestRenderer: CIFilter {
    var inputImage:CIImage?

    static var kernel:CIKernel = { () -> CIKernel in

    let bundle = Bundle.main
    let url = bundle.url(forResource: "Kernels", withExtension: "ci.metallib")!
    let data = try! Data(contentsOf: url)
    return try! CIKernel(functionName: "testKernel", fromMetalLibraryData: data)
    
  }()

override var outputImage: CIImage? {
    guard let inputImage = inputImage else {
        return nil
    }
    
    let dod = inputImage.extent.insetBy(dx: -10, dy: -10)
    return CIMetalTestRenderer.kernel.apply(extent: dod, roiCallback: { index, rect in
        return rect
    }, arguments: [inputImage])
 }
}

Update: Here is my full code of ViewController. I just have UIImageView in storyboard (which can be created in viewDidLoad as well):

class ViewController: UIViewController {

@IBOutlet weak var imageView:UIImageView!

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
   
    generateSolidImage()
}

private func generateSolidImage() {
    let renderSize = imageView.bounds.size
    
    let solidSize = CGSize(width: renderSize.width * 0.5, height: renderSize.height * 0.5)
    
    var solidImage = CIImage(color: CIColor(red: 0.3, green: 0.6, blue: 0.754, alpha: 1))
    
    var cropRect = CGRect.zero
    cropRect.size = solidSize
    solidImage = solidImage.cropped(to: cropRect)
    solidImage = solidImage.transformed(by: CGAffineTransform.init(translationX: -10, y: -10))
    
    
    let metalRenderer = CIMetalTestRenderer()
    metalRenderer.inputImage = solidImage
    
    var outputImage = metalRenderer.outputImage
    
    outputImage = outputImage?.transformed(by: CGAffineTransform.init(translationX: 20, y: 20))
    let cyanImage = CIImage(color: CIColor.cyan).cropped(to: CGRect(x: 0, y: 0, width: renderSize.width, height: renderSize.height))
    
    outputImage = outputImage?.composited(over: cyanImage)
    
    let ciContext = CIContext()
    
    let cgImage = ciContext.createCGImage(outputImage!, from: outputImage!.extent)
    
    imageView.image = UIImage(cgImage: cgImage!)
    
}

}

And here are the outputs (by commenting and uncommenting black pixel line respectively).

enter image description here

enter image description here

1

There are 1 best solutions below

6
On

I think the problem is the comparison with dest.coord() because this also changes depending on the pixel that is currently being processed.

If you just want to check whether you are currently sampling outside the bounds of inputImage, you can simply do the following:

if (inputCoordinate.x < 0.0 || inputCoordinate.x > 1.0 ||
    inputCoordinate.y < 0.0 || inputCoordinate.y > 1.0) {
    return float4(0.0, 0.0, 0.0, 1.0);
}

However, there would be a simpler way to achieve a "clamp-to-black" effect:

let clapedToBlack = solidImage.composited(over: CIImage.black)