Search code examples
iosobjective-cswiftcore-imagecifilter

Custom CIKernel Displacement Map


I'm trying to create a displacement map cikernel for iOS 8 that shifts the pixels horizontally from the map R channel and vertically from the G channel. The map pixel coordinates must be picked relative to the source image size mapPixel = ((dest.x/source.width) * map.width, (dest.y / source.height) * map.height)

The input image size that I test with is 2048 x 2048 and the map is red-green perlin noise 2560 x 2560

In Quartz Composer the cikernel works almost as expected, except that the map is not applied to the whole image

kernel vec4 coreImageKernel(sampler image, sampler displaceMap, float scaleX, float scaleY)
{
 vec2 destination = destCoord();

 vec2 imageSize = samplerSize(image);
 float xPercent = destination.x / imageSize.x;
 float yPercent = destination.y / imageSize.y;

 vec2 mapSize = samplerSize(displaceMap);
 vec2 mapCoord = vec2(mapSize.x * xPercent, mapSize.y * yPercent);

 vec4 mapPixel = sample(displaceMap, mapCoord);
 float ratioShiftX = ((mapPixel.x) * 2.0) - 1.0;
 float ratioShiftY = ((mapPixel.y) * 2.0) - 1.0;
 vec2 pixelShift = vec2(ratioShiftX * scaleX, ratioShiftY * scaleY);

 return sample(image, destination - pixelShift);
}

Here's what the filter function looks like:

function __image main(__image image, __image displaceMap, __number scaleX, __number scaleY) {
  return coreImageKernel.apply(image.definition, null, image, displaceMap, scaleX, scaleY);
}

But when I load the cikernel in CIFilter the result is far from what I see in Quartz Composer. Here's what my apply function looks like in the CIFilter

override var outputImage:CIImage? {
    if let inputImage = inputImage {
        if let inputMap = inputMap {
            let args = [inputImage as AnyObject, inputMap as AnyObject, inputScaleX, inputScaleY]

            return CIDisplacementMapFilter.kernel?.applyWithExtent(inputImage.extent, roiCallback: {
                    (index, rect) in

                    if index == 0 {
                        return rect
                    }

                    return CGRectInfinite
                }, arguments: args)
        }
    }

    return nil
}

I'm guessing the ROI is wrong and the sampler is tiled, but I can't figure it out.


Solution

  • As it turns out the kernel was wrong. Here's a kernel that does the job

    kernel vec4 displace(sampler source, sampler map, float scaleX, float scaleY)
    {
    
    vec2 d = destCoord();
    vec4 mapPixel = sample(map, samplerTransform(map, d));
    float shiftX = ((mapPixel.x * 2.0) - 1.0) * scaleX;
    float shiftY = ((mapPixel.y * 2.0) - 1.0) * scaleY;
    
    vec2 s = samplerTransform(source, d + vec2(shiftX, shiftY));
    
    return sample(source, s);
    }