How to tint only one part of the UIImage with alpha channel - PNG (replacing color)? - ios

I have this transparent image:
My goal is to change the "ME!" parts color. Either tint only the last 3rd of the image, or replace the blue color with the new color.
Expected result after color change:
Unfortunately neither worked for me. To change the specific color I tried this: LINK, but as the documentation says, this works only without alpha channel!
Then I tried this one: LINK, but this actually does nothing, no tint or anything.
Is there any other way to tint only one part of the color or just replace a specific color?
I know I could slice the image in two parts, but I hope there is another way.

It turns out to be surprisingly complicated—you’d think you could do it in one pass with CoreGraphics blend modes, but from pretty extensive experimentation I haven’t found such a way that doesn’t mangle the alpha channel or the coloration. The solution I landed on is this:
Start with a grayscale/alpha version of your image rather than a colored one: black in the areas you don’t want tinted, white in the areas you do
Create an image context with your image’s dimensions
Fill that context with black
Draw the image into the context
Get a new image (let’s call it “the-image-over-black”) from that context
Clear the context (so you can use it again)
Fill the context with the color you want the tinted part of your image to be
Draw the-image-over-black into the context with the “multiply” blend mode
Draw the original image into the context with the “destination in” blend mode
Get your final image from the context
The reason this works is because of the combination of blend modes. What you’re doing is creating a fully-opaque black-and-white image (step 5), then multiplying it by your final color (step 8), which gives you a fully opaque black-and-your-final-color image. Then, you take the original image, which still has its alpha channel, and draw it with the “destination in” blend mode which takes the color from the black-and-your-color image and the alpha channel from the original image. The result is a tinted image with the original brightness values and alpha channel.
Objective-C
- (UIImage *)createTintedImageFromImage:(UIImage *)originalImage color:(UIColor *)desiredColor {
CGSize imageSize = originalImage.size;
CGFloat imageScale = originalImage.scale;
CGRect contextBounds = CGRectMake(0, 0, imageSize.width, imageSize.height);
UIGraphicsBeginImageContextWithOptions(imageSize, NO /* not opaque */, imageScale); // 2
[[UIColor blackColor] setFill]; // 3a
UIRectFill(contextBounds); // 3b
[originalImage drawAtPoint:CGPointZero]; // 4
UIImage *imageOverBlack = UIGraphicsGetImageFromCurrentImageContext(); // 5
CGContextClear(UIGraphicsGetCurrentImageContext()); // 6
[desiredColor setFill]; // 7a
UIRectFill(contextBounds); // 7b
[imageOverBlack drawAtPoint:CGPointZero blendMode:kCGBlendModeMultiply alpha:1]; // 8
[originalImage drawAtPoint:CGPointZero blendMode:kCGBlendModeDestinationIn alpha:1]; // 9
finalImage = UIGraphicsGetImageFromCurrentContext(); // 10
UIGraphicsEndImageContext();
return finalImage;
}
Swift 4
func createTintedImageFromImage(originalImage: UIImage, desiredColor: UIColor) -> UIImage {
let imageSize = originalImage.size
let imageScale = originalImage.scale
let contextBounds = CGRect(origin: .zero, size: imageSize)
UIGraphicsBeginImageContextWithOptions(imageSize, false /* not opaque */, imageScale) // 2
defer { UIGraphicsEndImageContext() }
UIColor.black.setFill() // 3a
UIRectFill(contextBounds) // 3b
originalImage.draw(at: .zero) // 4
guard let imageOverBlack = UIGraphicsGetImageFromCurrentImageContext() else { return originalImage } // 5
desiredColor.setFill() // 7a
UIRectFill(contextBounds) // 7b
imageOverBlack.draw(at: .zero, blendMode: .multiply, alpha: 1) // 8
originalImage.draw(at: .zero, blendMode: .destinationIn, alpha: 1) // 9
guard let finalImage = UIGraphicsGetImageFromCurrentImageContext() else { return originalImage } // 10
return finalImage
}

There are lots of ways to do this.
Core image filters come to mind as a good way to go. Since the part you want to change is a unique color, you could use the Core image CIHueAdjust filter to shift the hue from blue to red. Only the word you want to change has any color to it, so that's all it would change.
If you had an image with various colors in it and still wanted to replace ALL The colors in the image you could use CIColorCube to map the blue pixels to red without affecting other colors. There was a thread on this board last week with sample code using CIColorCube to force one color to another. Search on CIColorCube and look for the most recent post and you should be able to find it.
If you wanted to limit the change to a specific area of the screen you could probably come up with a sequence of core image filters that would limit your changes to just the target area.
You could also slice out the part you want to change, color edit it using a any of variety of techniques, and then composite it back together.

Another way is to use CoreImage filter - ColorCube.
Made category for myself when had this problem. It is for NSImage, but I think should work for UIImage after some update
https://github.com/braginets/NSImage-replace-color

Related

Fade UICollectionViewCells with overlap goes wrong

I have an UICollectionView with some UICollectionViewCells. Cells are supposed to overlap each other, but also to fade a bit based on their position. See below the result:
How can I avoid those corners to be visible? (top between 3 and 4, or 4 and 5, or all the right side between 5 and 6). They should overlap, but that should not affect the image.
In order to create a fade effect I would use an overlay like this:
Save the original image in a variable to be able to reset the process for different alpha values
Draw a shape that has same color as background (color alpha should be proportional with the item position) on top of your current image
Replace the result image with your current one
I will give you an example to illustrate better:
private UIImage baseImage;
private UIImage ChangeImageColor(UIImage image, nfloat alpha, UIColor color)
{
var alphaColor = color.ColorWithAlpha(alpha);
if(baseImage == null)
{
baseImage = image;
}
else
{
image = baseImage;
}
UIGraphics.BeginImageContextWithOptions(image.Size, false, UISCreen.MainScreen.Scale);
var context = UIGraphics.GetCurrentContext();
alphaColor.SetFill();
context.TranslateCTM(0, image.Size.Height);
context.ScaleCTM(new nfloat(1.0), new nfloat(-1.0));
context.SetBlendMode(CGBlendMode.Lighten);
var rect = new CGRect(0, 0, image.Size.Width, image.Size.Height);
context.DrawImage(rect, image.CGImage);
context.SetBlendMode(CGBlendMode.SourceAtop);
context.AddRect(rect);
context.DrawPath(CGPathDrawingMode.Fill);
image = UIGraphics.GetImageFromCurrentImageContext();
UIGraphics.EndImageContext();
return image;
}

MTKView Displaying Wide Gamut P3 Colorspace

I'm building a real-time photo editor based on CIFilters and MetalKit. But I'm running into an issue with displaying wide gamut images in a MTKView.
Standard sRGB images display just fine, but Display P3 images are washed out.
I've tried setting the CIContext.render colorspace as the image colorspace, and still experience the issue.
Here are snippets of the code:
guard let inputImage = CIImage(mtlTexture: sourceTexture!) else { return }
let outputImage = imageEditor.processImage(inputImage)
print(colorSpace)
context.render(outputImage,
to: currentDrawable.texture,
commandBuffer: commandBuffer,
bounds: inputImage.extent,
colorSpace: colorSpace)
commandBuffer?.present(currentDrawable)
let pickedImage = info[UIImagePickerControllerOriginalImage] as! UIImage
print(pickedImage.cgImage?.colorSpace)
if let cspace = pickedImage.cgImage?.colorSpace {
colorSpace = cspace
}
I have found a similar issue on the Apple developer forums, but without any answers: https://forums.developer.apple.com/thread/66166
In order to support the wide color gamut, you need to set the colorPixelFormat of your MTKView to either BGRA10_XR or bgra10_XR_sRGB. I suspect the colorSpace property of macOS MTKViews won't be supported on iOS because color management in iOS is not active but targeted (read Best practices for color management).
Without seeing your images and their actual values, it is hard to diagnose, but I'll explain my findings & experiments. I suggest you start like I did, by debugging a single color.
For instance, what's the reddest point in P3 color space? It can be defined through a UIColor like this:
UIColor(displayP3Red: 1, green: 0, blue: 0, alpha: 1)
Add a UIButton to your view with the background set to that color for debugging purposes. You can either get the components in code to see what those values become in sRGB,
var fRed : CGFloat = 0
var fGreen : CGFloat = 0
var fBlue : CGFloat = 0
var fAlpha : CGFloat = 0
let c = UIColor(displayP3Red: 1, green: 0, blue: 0, alpha: 1)
c.getRed(&fRed, green: &fGreen, blue: &fBlue, alpha: &fAlpha)
or you can use the Calculator in macOS Color Sync Utility,
Make sure you select Extended Range, otherwise the values will be clamped to 0 and 1.
So, as you can see, your P3(1, 0, 0) corresponds to (1.0930, -0.2267, -0.1501) in extended sRGB.
Now, back to your MTKView,
If you set the colorPixelFormat of your MTKView to .BGRA10_XR, then you obtain the brightest red if the output of your shader is,
(1.0930, -0.2267, -0.1501)
If you set the colorPixelFormat of your MTKView to .bgra10_XR_sRGB, then you obtain the brightest red if the output of your shader is,
(1.22486, -0.0420312, -0.0196301)
because you have to write a linear RGB value, since this texture format will apply the gamma correction for you. Be careful when applying the inverse gamma, since there are negative values. I use this function,
let f = {(c: Float) -> Float in
if fabs(c) <= 0.04045 {
return c / 12.92
}
return sign(c) * powf((fabs(c) + 0.055) / 1.055, 2.4)
}
The last missing piece is creating a wide gamut UIImage. Set the color space to CGColorSpace.displayP3 and copy the data over. But what data, right? The brightest red in this image will be
(1, 0, 0)
or (65535, 0, 0) in 16-bit ints.
What I do in my code is using .rgba16Unorm textures to manipulate images in displayP3 color space, where (1, 0, 0) will be the brightest red in P3. This way, I can directly copy over its contents to a UIImage. Then, for displaying, I pass a color transform to the shader to convert from P3 to extended sRGB (so, not saturating colors) before displaying. I use linear color, so my transform is just a 3x3 matrix. I set my view to .bgra10_XR_sRGB, so the gamma will be applied automatically for me.
That (column-major) matrix is,
1.2249 -0.2247 0
-0.0420 1.0419 0
-0.0197 -0.0786 1.0979
You can read about how I generated it here: Exploring the display-P3 color space
Here's an example I built using UIButtons and an MTKView, screen-captured on an iPhoneX,
The button on the left is the brightest red on sRGB, while the button on the right is using a displayP3 color. At the center, I placed an MTKView that outputs the transformed linear color as described above.
Same experiment for green,
Now, if you see this on a recent iPhone or iPad, you should see the both the square in the center and the button to the right have the same bright colors. If you see this on a Mac that can't display them, the left button will appear the same color. If you see this in a Windows machine or a browser without proper color management, the left button may also appear to be of a different color, but that's only because the whole image is interpreted as sRGB and obviously those pixels have different values... But the appearance won't be correct.
If you want more references, check the testP3UIColor unit test I added here: ColorTests.swift,
my functions to initialize the UIImage: Image.swift,
and a sample app to try out the conversions: SampleColorPalette
I haven't experimented with CIImages, but I guess the same principles apply.
I hope this information is of some help. It also took me long to figure out how to display colors properly because I couldn't find any explicit reference to displayP3 support in the Metal SDK documentation.

Adding semi-transparent images as textures in Scenekit

When I add a semi-transparent image (sample) as a texture for a SCNNode, how can I specify a color attribute for the node where the image is transparent. Since I am able to specify either color or image as a material property, I am unable to specify the color value to the node. Is there a way to specify both color and image for the material property or is there a workaround to this problem.
If you are assigning the image to the contents of the transparent material property, you can change the materials transparencyMode to be either .AOne or .RGBZero.
.AOne means that transparency is derived from the images alpha channel.
.RGBZero means that transparency is derived from the luminance (the total red, green, and blue) in the image.
You cannot configure an arbitrary color to be treated as transparency without a custom shader.
However, from the looks of your sample image, I would think that assigning the sample image to the transparent material properties contents and using the .AOne transparency mode would give you the result you are looking for.
I'm posting this as a new answer because it's different from the other answer.
Based on your comment, I understand that you want to want to use an image with transparency as the diffuse content of a material, but use a background color wherever the image is transparent. In other words, you won't to use a composite of the image over a color as the diffuse contents.
Using UIImage
There are a few different ways you can achieve this composited image. The easiest and likely most familiar solution is to create a new UIImage that draws the image over the color. This new image will have the same size and scale as your image, but can be opaque since it has a solid background color.
func imageByComposing(image: UIImage, over color: UIColor) -> UIImage {
UIGraphicsBeginImageContextWithOptions(image.size, true, image.scale)
defer {
UIGraphicsEndImageContext()
}
let imageRect = CGRect(origin: .zero, size: image.size)
// fill with background color
color.set()
UIRectFill(imageRect)
// draw image on top
image.drawInRect(imageRect)
return UIGraphicsGetImageFromCurrentImageContext()
}
Using this image as the contents of the diffuse material property will give you the effect that you're after.
Using Shader Modifiers
If you find yourself having to change the color very frequently (possibly animating it), you could also use custom shaders or shader modifiers to composite the image over the color.
In that case, you want to composite the image A over the color B, so that the output color (CO) is:
CO = CA + CB * (1 - ɑA)
By passing the image as the diffuse contents, and assigning the output to the diffuse content, the expression can be simplified as:
Cdiffuse = Cdiffuse + Ccolor * (1 - ɑdiffuse)
Cdiffuse += Ccolor * (1 - ɑdiffuse)
Generally the output alpha would depend on the alpha of A and B, but since B (the color) is opaque (1), the output alpha is also 1.
This can be written as a small shader modifier. Since the motivation for this solutions was to be able to change the color, the color is created as a uniform variable which can be updated in code.
// Define a color that can be set/changed from code
uniform vec3 backgroundColor;
#pragma body
// Composit A (the image) over B (the color):
// output = image + color * (1-alpha_image)
float alpha = _surface.diffuse.a;
_surface.diffuse.rgb += backgroundColor * (1.0 - alpha);
// make fully opaque (since the color is fully opaque)
_surface.diffuse.a = 1.0;
This shader modifier would then be read from the file, and set in the materials shader modifier dictionary
enum ShaderLoadingError: ErrorType {
case FileNotFound, FailedToLoad
}
func shaderModifier(named shaderName: String, fileExtension: String = "glsl") throws -> String {
guard let url = NSBundle.mainBundle().URLForResource(shaderName, withExtension: fileExtension) else {
throw ShaderLoadingError.FileNotFound
}
do {
return try String(contentsOfURL: url)
} catch {
throw ShaderLoadingError.FailedToLoad
}
}
// later, in the code that configures the material ...
do {
let modifier = try shaderModifier(named: "Composit") // the name of the shader modifier file (assuming 'glsl' file extension)
theMaterial.shaderModifiers = [SCNShaderModifierEntryPointSurface: modifier]
} catch {
// Handle the error here
print(error)
}
You would then be able to change the color by setting a new value for the "backgroundColor" of the material. Note that there is no initial value, so one would have to be set.
let backgroundColor = SCNVector3Make(1.0, 0.0, 0.7) // r, g, b
// Set the color components as an SCNVector3 wrapped in an NSValue
// for the same key as the name of the uniform variable in the sahder modifier
theMaterial.setValue(NSValue(SCNVector3: backgroundColor), forKey: "backgroundColor")
As you can see, the first solution is simpler and the one I would recommend if it suits your needs. The second solution is more complicated, but enabled the background color to be animated.
Just in case someone comes across this in the future... for some tasks, ricksters solution is likely the easiest. In my case, I wanted to display a grid on top of an image that was mapped to a sphere. I originally composited the images into one and applied them, but over time I got more fancy and this started getting complex. So I made two spheres, one inside the other. I put the grid on the inner one and the image on the outer one and presto...
let outSphereGeometry = SCNSphere(radius: 20)
outSphereGeometry.segmentCount = 100
let outSphereMaterial = SCNMaterial()
outSphereMaterial.diffuse.contents = topImage
outSphereMaterial.isDoubleSided = true
outSphereGeometry.materials = [outSphereMaterial]
outSphere = SCNNode(geometry: outSphereGeometry)
outSphere.position = SCNVector3(x: 0, y: 0, z: 0)
let sphereGeometry = SCNSphere(radius: 10)
sphereGeometry.segmentCount = 100
sphereMaterial.diffuse.contents = gridImage
sphereMaterial.isDoubleSided = true
sphereGeometry.materials = [sphereMaterial]
sphere = SCNNode(geometry: sphereGeometry)
sphere.position = SCNVector3(x: 0, y: 0, z: 0)
I was surprised that I didn't need to set sphereMaterial.transparency, it seems to get this automatically.

Create a circle or a disk with antialiasing for retina display

I have creating a circle using CGContextFillEllipseInRect from CoreGraphic.
I'm using this circle (which is actually a disk) to replace the thumbImage of an UISlider. The antialiasing is applied by default.
But the result on my iPhone 6 is clearly not good. I can clearly see the pixels ,not as much as with the antialiasing off, but way more than the pixels of a normal UISlider.
Maybe I'm doing something wrong. So my question is, is there a way to get programmatically the same nice disk than the one used by default for an UISlider?
EDIT:
Here is how I create the disk:
class func drawDisk(rgbValue:UInt32, rectForDisk: CGRect = CGRectMake(0.0, 0.0, 1.0, 1.0)) -> UIImage {
let color = uicolorFromHex(rgbValue)
UIGraphicsBeginImageContext(rectForDisk.size)
let context = UIGraphicsGetCurrentContext()
CGContextSetFillColorWithColor(context, color.CGColor)
CGContextFillEllipseInRect(context, rectForDisk)
let rectForCircle = CGRectMake(0.5, 0.5, rectForDisk.size.width - 1, rectForDisk.size.height - 1)
CGContextSetLineWidth(context, 1.0)
CGContextSetStrokeColorWithColor(context,
UIColor.blackColor().CGColor)
CGContextAddEllipseInRect(context, rectForCircle)
CGContextStrokePath(context)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
The problem is that you are creating a non-retina graphics context when using UIGraphicsBeginImageContext, as mentioned in the documentation:
This function is equivalent to calling the UIGraphicsBeginImageContextWithOptions function with the opaque parameter set to NO and a scale factor of 1.0.
Instead you should be using UIGraphicsBeginImageContextWithOptions to create your image context. You can keep passing NO for the opaque parameter if you want an image that supports transparency (same as what you are implicitly doing now).
In most cases you can pass 0.0 for the scale factor. This sets the scale factor to that of the device's main screen. Again, as mentioned in the documentation:
If you specify a value of 0.0, the scale factor is set to the scale factor of the device’s main screen.
So, in short, you should create your image context like this:
UIGraphicsBeginImageContextWithOptions(rectForDisk.size, false, 0.0) // false for Swift

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