Search code examples
swiftcifilter

CIFilter's randomGenerator() produces patterns


I am trying to mimic a vintage film grain effect as done by apple here. the problem lies in overlaying the grain image, when I composite it and crop it to the size of the original image, I am getting a bunch of weird patterns - this is consistent across multiple images and sizes, I have applied the grain image to a plain image to help show the effect.

Photo of grain effect

As you can see, there is a consistent vertical and horizontal pattern, almost like a grid. Why is this happening and how can I prevent it from happening?

Grain generator function:

private func applyRandomGrain() {

/// generate grain & convert to grayscale
    let grainGenerator = CIFilter.randomGenerator()
    let grayscale = CIFilter.minimumComponent()
    grayscale.inputImage = grainGenerator.outputImage

/// adjust brightness of grain image
    let exposureFilter = CIFilter.exposureAdjust()
    exposureFilter.inputImage = grayscale.outputImage
    exposureFilter.ev = Float(settings.grainExposure)

/// adjust intensity of grain image with opacity
    let alphaVector = CIVector(x: 0, y: 0, z: 0, w: settings.grainOpacity)
    let alphaFilter = CIFilter.colorMatrix()
    alphaFilter.inputImage = exposureFilter.outputImage
    alphaFilter.aVector = alphaVector

    guard let inputImage = outputImage else {
        return
    }

/// apply grain image to original image
    let composite = CIFilter.sourceOverCompositing()
    composite.backgroundImage = inputImage
    composite.inputImage = alphaFilter.outputImage

    guard let imageWithGrain = composite.outputImage else {
        return
    }

    outputImage = imageWithGrain.cropped(to: inputImage)
}

EDIT: The example image is 1920x2560 - I have observed that the larger the image the more of a pattern there is. At a 100x100 image, there is no pattern.


Solution

  • The pattern generated by CIRandomGenerator is repeating. From the sample site you linked:

    The image output from CIRandomGenerator is always the same; even if you reseed your random number generator, the image output from this filter is always the same 512x512 pattern. However, it is suitable for giving the appearance of randomness.

    This means every 512 pixels the pattern repeats. That's why you don't see the effect as strongly for smaller images.

    If you want "true", non-repeating randomness, you have to implement your own filter kernel. For instance, one that implements Perlin noise.

    Alternatively, if you don't mind using an internal filter from Apple (and that's a big if), you can use the CIPhotoGrain filter that does exactly that: simulate a photo grain effect caused by high ISO values.

    let filter = CIFilter(name: "CIPhotoGrain")!
    filter.setValue(image, forKey: kCIInputImageKey)
    filter.setValue(42, forKey: "inputSeed") // random seed
    filter.setValue(1.0, forKey: "inputAmount")
    filter.setValue(2000, forKey: "inputISO")
    let output = filter.outputImage