2

I'm trying to create a stoke border around a PNG with transparent background in an NSImage.

I originally tried duplicating the image and having a larger scale and then using CIFilter to make it white - this works great for shapes like circles and other solid shapes.

However i'll have shapes like the example image below:

Tree Image

With this Image it doesn't work well at all.

I'm thinking that maybe I can do something with CIEdge and CIMorphologyMaximum but i'm not sure if i'm thinking in a good direction - would appreciate some advise if anyone has come across a similar challenge.

Hamid Yusifli
  • 9,688
  • 2
  • 24
  • 48

1 Answers1

3

Yes, CIMorphologyMaximum should be the way to go. Try this:

import CoreImage.CIFilterBuiltins

let ciImage = ...

// Apply morphology maximum to "erode" image in all direction into transparent area.
let filter = CIFilter.morphologyMaximum()
filter.inputImage = ciImage
filter.radius = 5 // border radius
let eroded = filter.outputImage!

// Turn all pixels of eroded image into desired border color.
let colorized = CIBlendKernel.sourceAtop.apply(foreground: .white, background: eroded)!.cropped(to: eroded.extent)

// Blend original image over eroded, colorized image.
let imageWithBorder = ciImage.composited(over: colorized)

And in Objective-C:

CIImage* ciImage = ...;

// Apply morphology maximum to "erode" image in all direction into transparent area.
CIFilter* erodeFilter = [CIFilter filterWithName:@"CIMorphologyMaximum"];
[erodeFilter setValue:ciImage forKey:kCIInputImageKey];
[erodeFilter setValue:@5 forKey:kCIInputRadiusKey];
CIImage* eroded = erodeFilter.outputImage;

// Turn all pixels of eroded image into desired border color.
CIImage* colorized = [[CIBlendKernel.sourceAtop applyWithForeground:[CIImage whiteImage] background:eroded] imageByCroppingToRect:eroded.extent];

// Blend original image over eroded, colorized image.
CIImage* imageWithBorder = [ciImage imageByCompositingOverImage:colorized];

Keep in mind that the border will extend the image in all directions which might result in a negative origin in working space. For example, an image with extent [0, 0, 50, 50] will have an extent of [-5, -5, 60, 60] after applying a 5 pixel border.

To compensate for that, you need to specify the extent of the resulting image when as the rect when rendering the image:

[ciContext createCGImage:imageWithBorder fromRect:imageWithBorder.extent];

Alternatively, you can move the image's origin to [0, 0] again after applying the border. But the resulting will still be larger than the input image, so keep that in mind.

imageWithBorder = [imageWithBorder imageByApplyingTransform:CGAffineTransformMakeTranslation(-ciImage.extent.origin.x, -ciImage.extent.origin.y)];

As for the border size: The inputRadius you set on the erosion filter is in pixels. That means that large images will get a smaller relative border compared to small images.

To compensate for that, you can calculate the border radius as a percentile of the image size. This should create a uniform look among differently sized images. For instance:

NSNumber* radius = @(MAX(ciImage.extent.size.width, ciImage.extent.size.height) * 0.05);
Frank Rupprecht
  • 9,191
  • 31
  • 56
  • Perfect, glad to know my thought path is in the right direction, do you know how I would do this in Obj-C sorry not familiar with swift or it's syntaxes yet. – GeraldTheGerm Aug 01 '22 at 17:20
  • Sure, see edit. – Frank Rupprecht Aug 01 '22 at 17:31
  • Perfect thanks Frank - i'll have a play around with had a small play around so far and looks good. – GeraldTheGerm Aug 01 '22 at 18:09
  • https://imgur.com/JvWDx4G https://imgur.com/a20YkAH https://imgur.com/A8OGCGi I have a couple of questions - i'm seeing many images the border is cutting off - do you know why that would be - also the border varies in size depending on the image uploaded would there be away to keep that more uniform? – GeraldTheGerm Aug 02 '22 at 07:41
  • 1
    @GeraldTheGerm I extended my answer to address your questions. – Frank Rupprecht Aug 02 '22 at 09:23
  • Thanks Frank, appreciate your time and help - i'm away from my Mac right now as soon as i'm back i'll have a tinker and digest and learn what you've told me. – GeraldTheGerm Aug 02 '22 at 10:00
  • Works absolutely perfectly, go one last question is there a simple way to convert an NSColor / CIColor to a CIImage for the border color? – GeraldTheGerm Aug 02 '22 at 15:24
  • 1
    Absolutely! You can simply initialize it with `[CIImage imageWithColor:ciColor]`. – Frank Rupprecht Aug 02 '22 at 19:36
  • Perfect I kinda got it working with filters but this is much less resource heavy as easier to manage - can I ask you how you learnt this - as i've learnt more from you then I have from Apple Documentation! Are there any resources you'd recommend me reading? One day I hope to be as epic as you :) – GeraldTheGerm Aug 03 '22 at 05:14
  • 1
    Well, through years of trial and error. I use Apple's code documentation for the CI types the most, but the (outdated) Core Image Programming Guide is also good for learning the basics. I can also recommend the free "Core Image for Swift" by @FlexMonkey on Apple Books. Though it's for Swift, all concepts are 1:1 transferrable to Objective-C. – Frank Rupprecht Aug 03 '22 at 08:41
  • Perfect really appreciate all the help you've given me - will continue to play around with everything and learn CI is really powerful especially when you understand what it can do better! – GeraldTheGerm Aug 03 '22 at 09:51