Vertical edge detection with convolution giving transparent image as result with Swift - ios

I am currently trying to write a function which takes an image and applies a 3x3 Matrix to filter the vertical edges. For that I am using CoreImage's CIConvolution3X3 and passing the matrix used to detect vertical edges in Sobels edge detection.
Here's the code:
func verticalEdgeFilter() -> UIImage {
let inputUIImage = UIImage(named: imageName)!
let inputCIImage = CIImage(image: inputUIImage)
let context = CIContext()
let weights: [CGFloat] = [1.0, 0.0, -1.0,
2.0, 0.0, -2.0,
1.0, 0.0, -1.0]
let verticalFilter = CIFilter.convolution3X3()
verticalFilter.inputImage = inputCIImage
verticalFilter.weights = CIVector(values: weights, count: 9)
if let output = verticalFilter.outputImage{
if let cgimg = context.createCGImage(output, from: output.extent) {
let processedImage = UIImage(cgImage: cgimg)
return processedImage
}
}
print("returning original")
return inputUIImage
}
Now as a result I always get an almost fully transparent image with a 2 Pixel border like this one:
Original
Screenshot of the result (border on the left side)
Am I missing something obvious because the images are only transparent if the center value of the matrix is 0. But if I try the same kernel on some webpage, it does at least lead to a usable result. Setting a bias also just crashes the whole thing which I don't understand.
I also checked Apples documentation on this, as well as the CIFilter web page but I'm not getting anywhere, so I would really appreciate it if someone could help me with this or tell me an alternative way of doing this in Swift :)

Applying this convolution matrix to a fully opaque image will inevitably produce a fully transparent output. This is because the total sum of kernel values is 0, so after multiplying the 9 neighboring pixels and summing them up you will get 0 in the alpha component of the result. There are two ways to deal with it:
Make output opaque by using settingAlphaOne(in:) CIImage helper method.
Use CIConvolutionRGB3X3 filter that leaves the alpha component alone and applies the kernel to RGB components only.
As far as the 2 pixels border, it's also expected because when the kernel is applied to the pixels at the border it still samples all 9 pixels, and some of them happen to fall outside the image boundary (exactly 2 pixels away from the border on each side). These non existent pixels contribute as transparent black pixels 0x000000.
To get rid of the border:
Clamp image to extent to produce infinite image where the border pixels are repeated to infinity away from the border. You can either use CIClamp filter or the CIImage helper function clampedToExtent()
Apply the convolution filter
Crop resulting image to the input image extent. You can use cropped(to:) CIImage helper function for it.
With these changes here is how your code could look like.
func verticalEdgeFilter() -> UIImage {
let inputUIImage = UIImage(named: imageName)!
let inputCIImage = CIImage(image: inputUIImage)!
let context = CIContext()
let weights: [CGFloat] = [1.0, 0.0, -1.0,
2.0, 0.0, -2.0,
1.0, 0.0, -1.0]
let verticalFilter = CIFilter.convolution3X3()
verticalFilter.inputImage = inputCIImage.clampedToExtent()
verticalFilter.weights = CIVector(values: weights, count: 9)
if var output = verticalFilter.outputImage{
output = output
.cropped(to: inputCIImage.extent)
.settingAlphaOne(in: inputCIImage.extent)
if let cgimg = context.createCGImage(output, from: output.extent) {
let processedImage = UIImage(cgImage: cgimg)
return processedImage
}
}
print("returning original")
return inputUIImage
}
If you use convolutionRGB3X3 instead of convolution3X3 you don't need to do settingAlphaOne.
BTW, if you want to play with convolution filters as well as any other CIFilter out of 250, check this app out that I just published: https://apps.apple.com/us/app/filter-magic/id1594986951

Related

How to apply CIVignette as if CIImage were square?

I have a 1080x1920 CIImage and wish to apply CIVignette as if the image were square (to mimic a camera lens).
I'm new to CoreImage and am wondering how to temporarily change the extent of my CIImage to 1920x1920. (But I'm not sure if this is needed at all.)
I could copy-paste two narrow slivers of the original image left and right and CICrop afterwards, but this seems hacky.
Any ideas?
You can use a combination of clampedToExtent (which causes the image to repeat its border pixels infinitely) and cropped to make the image square. Then you can apply the vignette and crop the result back to the original extent:
// "crop" to square, placing the image in the middle
let longerSize = max(inputImage.extent.width, inputImage.extent.height)
let xOffset = (longerSize - inputImage.extent.width) / 2.0
let yOffset = (longerSize - inputImage.extent.height) / 2.0
let squared = inputImage.clampedToExtent().cropped(to: CGRect(x: -xOffset, y: -yOffset, width: longerSize, height: longerSize))
// apply vignette
let vignetteFilter = CIFilter(name: "CIVignette")!
vignetteFilter.setValue(squared, forKey: kCIInputImageKey)
vignetteFilter.setValue(1.0, forKey: kCIInputIntensityKey)
vignetteFilter.setValue(1.0, forKey: kCIInputRadiusKey)
let withVignette = vignetteFilter.outputImage!
// crop back to original extent
let output = withVignette.cropped(to: inputImage.extent)

Antialiased pixels from Swift code is converted to unwanted black pixels by OpenCV

i am trying to make some pixels transparent using swift code by setting anti-aliasing true and remove path pixels to transparent. For further process i had sent UIimage to opencv using c++ that convert edge of path to black pixels.
i want to remove that unwanted black pixels. How can i remove those blacks pixels generated by opencv?
in opencv just converting image to mat and from mat to UIImage, the same problem occurs.
swift code:
guard let imageSize=image.size else{
return
}
UIGraphicsBeginImageContextWithOptions(imageSize,false,1.0)
guard let context = UIGraphicsGetCurrentContext() else{
return
}
context.setShouldAntialias(true)
context.setAllowsAntialiasing(true)
context.setShouldSubpixelQuantizeFonts(true)
context.interpolationQuality = .high
imgCanvas.image?.draw(in: CGRect(x: 0, y:0, width: (imgCanvas.image?.size.width)!, height: (imgCanvas.image?.size.height)!))
bezeirPath = UIBezierPath()
bezeirPath.move(to: fromPoint)
bezeirPath.addLine(to: toPoint)
bezeirPath.lineWidth=(CGFloat(widthOfLine) * scaleX )/scrollView.zoomScale
bezeirPath.lineCapStyle = CGLineCap.round
bezeirPath.lineJoinStyle=CGLineJoin.round
bezeirPath.flatness=0.0
bezeirPath.miterLimit=0.0
bezeirPath.usesEvenOddFillRule=true
UIColor.white.setStroke()
bezeirPath.stroke(with: .clear, alpha:0)
bezeirPath.close()
bezeirPath.fill()
UIColor.clear.set()
context.addPath(bezeirPath.cgPath)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
opencv code:
Mat source;
UIImageToMat(originalImage,source,true);
return MatToUIImage(source);
I tried various ways to solve this issue, looking at different sources, but none worked. I have been trying to solve this for the past 3 days. So please if anybody has even any clue related to this issue, that would be helpful!
[
I would supply the RGB image and the A mask as separate images to OpenCV. Draw your mask into a single channel image:
guard let imageSize=image.size else{
return
}
UIGraphicsBeginImageContextWithOptions(imageSize,false,1.0)
guard let context = UIGraphicsGetCurrentContext() else{
return
}
context.setShouldAntialias(true)
context.setAllowsAntialiasing(true)
context.setShouldSubpixelQuantizeFonts(true)
context.interpolationQuality = .high
context.setFillColor(UIColor.black.cgColor)
context.addRect(CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height))
context.drawPath(using: .fill)
bezeirPath = UIBezierPath()
bezeirPath.move(to: fromPoint)
bezeirPath.addLine(to: toPoint)
bezeirPath.lineWidth=(CGFloat(widthOfLine) * scaleX )/scrollView.zoomScale
bezeirPath.lineCapStyle = CGLineCap.round
bezeirPath.lineJoinStyle=CGLineJoin.round
bezeirPath.flatness=0.0
bezeirPath.miterLimit=0.0
bezeirPath.usesEvenOddFillRule=true
UIColor.white.setStroke()
bezeirPath.stroke()
bezeirPath.close()
bezeirPath.fill()
let maskImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
Then in OpenCV, you can apply the A image to your RGB image.
Mat source, sourceMask
UIImageToMat(image, source, true)
UIImageToMat(maskImage, sourceMask, true)
If your image isn't RGB, you can convert it:
cvtColor(source, source, CV_RGBA2RGB)
If your mask isn't single channel, you can convert it:
cvtColor(sourceMask, sourceMask, CV_RGBA2GRAY)
Then split the RGB image into channels:
Mat rgb[3];
split(source, rgb);
Then create RGBA image with the RGB channels and alpha channel:
Mat imgBGRA;
vector<Mat> channels = {rgb[0], rgb[1], rgb[2], sourceMask};
merge(channels, imgBGRA);
Since your mask was created with anti-aliasing, the image created above will also have anti-aliased alpha.
OpenCV can help you find edges using Canny Edge Detector You can refer for the implementation in this link
OpenCV's Canny Edge Detection in C++
Furthermore, I would like you to first do a bit of research on your own before actually asking for questions here.

Strange CoreImage cropping issues encounter here

I have a strange issue where after I cropped a photo from my photo library, it cannot be displayed from the App. It gives me this error after I run this code:
self.correctedImageView.image = UIImage(ciImage: correctedImage)
[api] -[CIContext(CIRenderDestination) _startTaskToRender:toDestination:forPrepareRender:error:] The image extent and destination extent do not intersect.
Here is the code I used to crop and display. (inputImage is CIImage)
let imageSize = inputImage.extent.size
let correctedImage = inputImage
.cropped(to: textObvBox.boundingBox.scaled(to: imageSize) )
DispatchQueue.main.async {
self.correctedImageView.image = UIImage(ciImage: correctedImage)
}
More Info: Debug print the extent of the inputImage and correctedImage
Printing description of self.inputImage: CIImage: 0x1c42047a0 extent [0 0 3024 4032]>
crop [430 3955 31 32] extent=[430 3955 31 32]
affine [0 -1 1 0 0 4032] extent=[0 0 3024 4032] opaque
affine [1 0 0 -1 0 3024] extent=[0 0 4032 3024] opaque
colormatch "sRGB IEC61966-2.1"_to_workingspace extent=[0 0 4032 3024] opaque
IOSurface 0x1c4204790(501) seed:1 YCC420f 601 alpha_one extent=[0 0 4032 3024] opaque
Funny thing is that when I put a breakpoint, using Xcode, i was able to preview the cropped image properly. I'm not sure what this extent thing is for CIImage, but UIMageView doesn't like it when I assign the cropped image to it. Any idea what this extent thing does?
I ran into the same problem you describe. Due to some weird behavior in UIKit / CoreImage, I needed to convert the CIImage to a CGImage first. I noticed it only happened when I applied some filters to the CIImage, described below.
let image = /* my CIImage */
let goodImage = UIImage(ciImage: image)
uiImageView.image = goodImage // works great!
let image = /* my CIImage after applying 5-10 filters */
let badImage = UIImage(ciImage: image)
uiImageView.image = badImage // empty view!
This is how I solved it.
let ciContext = CIContext()
let cgImage = self.ciContext.createCGImage(image, from: image.extent)
uiImageView.image = UIImage(cgImage: cgImage) // works great!!!
As other commenters have stated beware creating a CIContext too often; its an expensive operation.
Extent in CIImage can be a bit of a pain, especially when working with cropping and translation filters. Effectively it allows the actual position of the new image to relate directly to the part of the old image it was taken from. If you crop the center out of an image, you'll find the extent's origin is non-zero and you'll get an issue like this. I believe the feature is intended for things such as making an art application that supports layers.
As noted above, you CAN convert the CIImage into a CGImage and from there to a UIImage, but this is slow and wasteful. It works because CGImage doesn't preserve extent, while UIImage can. However, as noted, it requires creating a CIContext which has a lot of overhead.
There is, fortunately, a better way. Simply create a CIAffineTransformFilter and populate it with an affine transform equal to the negative of the extent's origin, thus:
let transformFilter = CIFilter(name: "CIAffineTransform")!
let translate = CGAffineTransform(translationX: -image.extent.minX, y: -image.extent.minY)
let value = NSValue(cgAffineTransform: translate)
transformFilter.setValue(value, forKey: kCIInputTransformKey)
transformFilter.setValue(image, forKey: kCIInputImageKey)
let newImage = transformFilter.outputImage
Now, newImage should be identical to image but with an extent origin of zero. You can then pass this directly to UIView(ciimage:). I timed this and found it to be immensely faster than creating a CIContext and making a CGImage. For the sake of interest:
CGImage Method: 0.135 seconds.
CIFilter Method: 0.00002 seconds.
(Running on iPhone XS Max)
You can use advanced UIGraphicsImageRenderer to draw your image in iOS 10.X or later.
let img:CIImage = /* my ciimage */
let renderer = UIGraphicsImageRenderer(size: img.extent.size)
uiImageView.image = renderer.image { (context) in
UIImage(ciImage: img).draw(in: .init(origin: .zero, size: img.extent.size))
}

Applying CIFilter to UIImage results in resized and repositioned image

After applying a CIFilter to a photo captured with the camera the image taken shrinks and repositions itself.
I was thinking that if I was able to get the original images size and orientation that it would scale accordingly and pin the imageview to the corners of the screen. However nothing is changed with this approach and not aware of a way I can properly get the image to scale to the full size of the screen.
func applyBloom() -> UIImage {
let ciImage = CIImage(image: image) // image is from UIImageView
let filteredImage = ciImage?.applyingFilter("CIBloom",
withInputParameters: [ kCIInputRadiusKey: 8,
kCIInputIntensityKey: 1.00 ])
let originalScale = image.scale
let originalOrientation = image.imageOrientation
if let image = filteredImage {
let image = UIImage(ciImage: image, scale: originalScale, orientation: originalOrientation)
return image
}
return self.image
}
Picture Description:
Photo Captured and screenshot of the image with empty spacing being a result of an image shrink.
Try something like this. Replace:
func applyBloom() -> UIImage {
let ciInputImage = CIImage(image: image) // image is from UIImageView
let ciOutputImage = ciInputImage?.applyingFilter("CIBloom",
withInputParameters: [kCIInputRadiusKey: 8, kCIInputIntensityKey: 1.00 ])
let context = CIContext()
let cgOutputImage = context.createCGImage(ciOutputImage, from: ciInputImage.extent)
return UIImage(cgImage: cgOutputImage!)
}
I remained various variables to help explain what's happening.
Obviously, depending on your code, some tweaking to optionals and unwrapping may be needed.
What's happening is this - take the filtered/output CIImage, and using a CIContext, write a CGImage the size of the input CIImage.
Be aware that a CIContext is expensive. If you already have one created, you should probably use it.
Pretty much, a UIImage size is the same as a CIImage extent. (I say pretty much because some generated CIImages can have infinite extents.)
Depending on your specific needs (and your UIImageView), you may want to use the output CIImage extent instead. Usually though, they are the same.
Last, a suggestion. If you are trying to use a CIFilter to show "near real-time" changes to an image (like a photo editor), consider the major performance improvements you'll get using CIImages and a GLKView over UIImages and a UIImageView. The former uses a devices GPU instead of the CPU.
This could also happen if a CIFilter outputs an image with dimensions different than the input image (e.g. with CIPixellate)
In which case, simply tell the CIContext to render the image in a smaller rectangle:
let cgOutputImage = context.createCGImage(ciOutputImage, from: ciInputImage.extent.insetBy(dx: 20, dy: 20))

Color Vignette Core Image filter for iOS?

I have tested the vignette filter in Core Image, while good - I am wondering whether anyone has implemented color vignette effect (instead of black edges, it soften the edges) by chaining through various Core Image filters for iOS? Or points me to a tutorial to do this?
Based on the answer below, this is my code - but does not seem to have much effect.
func colorVignette(image:UIImage) -> UIImage {
let cimage = CIImage(image:image)
let whiteImage = CIImage(image:colorImage(UIColor.whiteColor(), size:image.size))
var output1 = CIFilter(name:"CIGaussianBlur", withInputParameters:[kCIInputImageKey:cimage, kCIInputRadiusKey:5]).outputImage
var output2 = CIFilter(name:"CIVignette", withInputParameters:[kCIInputImageKey:whiteImage, kCIInputIntensityKey:vignette, kCIInputRadiusKey:1]).outputImage
var output = CIFilter(name:"CIBlendWithMask", withInputParameters:[kCIInputImageKey:cimage, kCIInputMaskImageKey:output2, kCIInputBackgroundImageKey:output1]).outputImage
return UIImage(CGImage:ctx.createCGImage(output, fromRect:cimage.extent()))
}
func colorImage(color:UIColor, size:CGSize) -> UIImage {
UIGraphicsBeginImageContextWithOptions(size, false, 0)
color.setFill()
UIRectFill(CGRect(x:0, y:0, width:size.width, height:size.height))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
You could create a filter by chaining together a Gaussian Blur, a Vignette, a Blend With Mask and the original image. First blur the input image with a CIGaussianBlur. Next, apply the CIVignette filter to a solid white image of the same size. Finally, mix the original image with the blurred image using the CIBlendWithMask filter.

Resources