So It took me quite some time solving one problem I had in my code and I'm very interested in some details. I've written a part what exactly I was doing down at the end.
So i was reading an image which I wanted to use with
static BufferedImage img = null;
img = ImageIO.read(new File("/home/user/doggo.jpg"));
Then created a BufferedImage object to store the changes which I would have done.
BufferedImage newimg = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB);
So because some parts of the image would not be changed, I figured i'd "copy" the BufferedImage to the other one (so there wouldn't be empty places) by doing:
newimg = img;
So when I was running my code, sometimes the image would be distorted, pixelated or not what i hoped for. I knew my algorithm was correct 100% and that there should be no way why it would not work.
It turned out, at least what i lea that I was "copying" one type of BufferedImage to a different type.
img.getType() returned
1
img.getColorModel() returned:
DirectColorModel: rmask=ff0000 gmask=ff00 bmask=ff amask=0
and img.getSampleModel() returned:
java.awt.image.SinglePixelPackedSampleModel#8080b20
For the newly created BufferedImage I got:
newimg.getType() returned
5
newimg.getColorModel() returned:
ColorModel: #pixelBits = 24 numComponents = 3 color space = java.awt.color.ICC_ColorSpace#76fb509a transparency = 1 has alpha = false isAlphaPre = false
and newimg.getSampleModel() returned:
java.awt.image.PixelInterleavedSampleModel#3086002
I'm mostly interested in how does ImageIO read the image into a BufferedImage? How does it define what type of BufferedImage it will be. The image i was reading was a normal jpeg file which has RGB values, so I presumed it would not do much harm copying the BufferedImage objects like that. By now I realized that it is not as simple as I imagined but I'm still in the dark about what happened in the background. I tried reading the oracle docs but they seem maybe either too lacking or too abstract for me to comprehend.
As for the stuff I was coding, I was doing kernel convolution with images, like blur, edge detection etc. and I just wanted to copy the source image to the destination one because I didn't do any edge wrapping/clipping and I did not want the edges of the newly created image to be empty.
Related
I need to convert near white pixels to white and near black pixels to black.
I found a code snippet in python on how to do it.
hsv=cv.cvtColor(image,cv.COLOR_BGR2HSV)
# Define lower and upper limits of what we call "brown"
brown_lo=np.array([10,0,0])
brown_hi=np.array([20,255,255])
# Mask image to only select browns
mask=cv.inRange(hsv,brown_lo,brown_hi)
# Change image to red where we found brown
image[mask>0]=(0,0,255)
I have converted it java as below.
Mat temp= new Mat();
Imgproc.cvtColor(src,temp,COLOR_BGR2HSV);
Scalar low= new Scalar(10,0,0);
Scalar high= new Scalar(20,255,255);
Mat mask = new Mat();
inRange(temp,low,high,mask);
But I am facing problem converting below statement to java and there is no good opencv documentation in java with samples.
image[mask>0]=(0,0,255)
Could somebody help on how to convert above statement to java...?
I have tried setTo but it is not giving desired behaviour(attached screenshot below). Refer https://stackoverflow.com/a/50215020/12643143 for the expected result.
src.setTo(new Scalar(0,0,255),mask);
I recommend to use setTo(). This method can set all he pixels in a Mat. If an optionally mask argument is specified, then all the pixels who have a corresponding pixel with a non-zero value in the mask will be set.
Thus the python statement
image[mask>0]=(0,0,255)
can be substituted in Java by:
image.setTo(new Scalar(0, 0, 255), mask);
where image has to be a Mat object.
Answer to the question
As #Rabbid76 mentioned setTo is the correct way to do this. However if you want specific logic like image[mask>127]=(0,0,255), then do threshold (Imgproc.threshold(grey,grey, 127, 255, THRESH_BINARY);) and then use setTo.
Solution to my problem
Actually my problem was not due to setTo. Its the logic mismatch between how I read/write the Mat in my code Vs the post I referred.
I am posting the solution to the problem that I have faced so that it might help new bees like me.
Problem in reading Image
The post use Imgcodecs.imread() to read image to Mat in BGR format.
Whereas I am loading bitmap using bitmapToMat in CV_8UC4 type as below which reads the image to Mat in RGBA format.
Mat src = new Mat(bitmap.getHeight(), bitmap.getWidth(), CV_8UC4);
org.opencv.android.Utils.bitmapToMat(bitmap, src);
Fix is to convert the format properly.
Mat src = new Mat(bitmap.getHeight(), bitmap.getWidth(), CV_8UC3); //notice 3 channel
org.opencv.android.Utils.bitmapToMat(bitmap, src);
Imgproc.cvtColor(src,hsv,COLOR_RGB2HSV); //Convert RGB to HSV. COLOR_RGBA2HSV not exist, hence we load it in CV_8UC3(3 channel R,G,B).
Problem in writing the Image
Similarly as we have differences in reading between bitmapToMat and imread, the same are applicable for writing. Imgcodecs.imwrite() will write the BGR image to bitmap, where as I have to convert it back to RGB format for matToBitmap to work like Imgproc.cvtColor(rgb, rgb, Imgproc.COLOR_BGR2RGB);
I have a grayscale image ("lena" actually) which I want to experiment with. I got it as a 512x512 PNG file with 216 shades of gray.
What happens is, when I read it with Java ImageIO, like that:
String name = args[0];
File fi = new File(name);
BufferedImage img = ImageIO.read(fi);
I get a BufferedImage with only 154 colours! I only realized this, cause my processed images which looked sallow, lacking deep black.
Even more irritating, when I use XnView convert the PNG to a GIF, which is a lossless procedure in this case, read the GIF with above code, I get all 216 colours in my BufferedImage.
Is there some kind of documentation or description, what happens to my PNG, when ImageIO reads it? Are there settings to fix that? I did these experiments on a fairly recent JDK1.8. It is just that my trust in Java PNG support is lost now and I will use coloured PNG later.
Welcome to Java's "great" world of implicit color management!
For Java (at least ImageIO) everything internally is sRGB and it implicitely does color management, which often is quite counter-productive for what one actually wants to do.
For gray scale images, at least using ImageIO with most readers and at least for gray scale images without an embedded ICC profile (I haven't tested others yet), Java automatically "assigns" an ICC profile with WhitePoint=D50, Gamma=1.0. I stumbled across this as well.
And then, when you access pixels (I assume you use img.getRGB() or something similar?), you actually access sRGB values (Java's default color space on Windows).
The result is, when converting to sRGB, which has a gamma of ~2.2 (sRGB's gamma is actually a bit more complicated, but close to 2.2 overall), this affectively applies a gamma correction with (1/Gamma)=2.2 to the image, (a) making your image appear "light", and (b) due to the gamma correction from 256 to 256 discrete values, you also effectively loose some of your shades of gray.
You also can see the effect if you access your BufferedImage's data in different ways:
a) access the profile:
ColorSpace colorSpace = img.getColorModel().getColorSpace();
if ( colorSpace instanceof ICC_ColorSpace ) {
ICC_Profile profile = ((ICC_ColorSpace)colorSpace).getProfile();
if ( profile instanceof ICC_ProfileGray ) {
float gamma = ((ICC_ProfileGray)profile).getGamma();
system.out.println("Gray Profile Gamma: "+gamma); // 1.0 !
}
}
b) access some pixel values in different ways ...
//access sRGB values (Colors translated from img's ICC profile to sRGB)
System.out.println( "pixel 0,0 value (sRGB): " + Integer.toHexString(img.getRGB(0,0)) ); // getRGB() actually means "getSRGB()"
//access raw raster data, this will give you the uncorrected gray value
//as it is in the image file
Raster raster = image.getRaster();
System.out.println( "pixel 0,0 value (RAW gray value): " + Integer.toHexString(raster.getSample(0,0,0)) );
If your pixel (0,0) is not by chance 100% black or 100% white, you will see that the sRGB value is "higher" than the gray value, for example gray = d1 -> sRGB = ffeaeaea (alpha, Red, Green, Blue).
From my point of view, it does not only reduce your gray levels, but also makes your image lighter (about the same as applying gamma correction with 1/gamma value of 2.2). It would be more logical if Java for gray images without embedded ICC Profile either translates gray to sRGB with R=G=B=grayValue or would assign an ICC Gray Profile WhitePoint=D50, Gamma=2.2 (at least on Windows). The latter still would make you loose a couple of gray tones due to sRGB not being exactly Gamma 2.2.
Regarding why it works with GIF: the GIF format has no concept of "gray scales" or ICC profiles, so your image is a 256 color palette image (the 256 colors happen to be 256 shades of gray). On opening a GIF, Java assumes the RGB values are sRGB.
Solution:
Depending on what your actual use case is, the solution for you might be that you access the Raster data of each of your image's pixel (gray=raster.getSample(x,y,0)) and put it into an sRGB image setting R=G=B=gray. There might be an more elegant way, though.
Regarding your trust in java or PNG:
I'm struggling with java ImageIO in many ways due to the implicite color conversions it does. The idea is to have color management built in without the developers need much knowledge about color management. This works to some extend as long as you work with sRGB only (and your input is sRGB, too, or has no color profile and thus could legitimately considered to be sRGB). Trouble starts if you have other color spaces in your input images (for example AdobeRGB). Gray is another thing as well, especially the fact that ImageIO assumes an (unusual) Gray Profile with Gamma=1.0. Now to understand what ImageIO is doing, you don't only need to know your ABC in color management, but also need to figure out what java is doing. I didn't find this info in any documentation! Bottom line: ImageIO does things that certainly could be considered correct. It's just often not what you expect and you might to dig deeper to find out why or to change the behaviour if it isn't what you want to do.
Somehow you have converted the image from a linear grayscale (gamma=1.0) to an sRGB grayscale (gamma=1/2.2). This can be demonstrated with GraphicsMagick. Start with Lenna.png downloaded from Wikipedia, then remove the sRGB chunk to create lena.png, then
gm convert lena.png -colorspace gray -depth 8 -strip lena-gray.png
lena-gray.png has 216 colors
gm convert lena-gray.png -gamma 2.2 -depth 8 -strip lena-gray-gm22.png
lena-gray-gm22.png has 154 colors and appears washed-out or faded.
I'm using a recent beta of graphicsmagick (version 1.4) with libpng-1.6.17.
To count the colors I used ImageMagick:
identify -verbose file.png | grep Colors
I used
pngcheck -v file.png
to verify that Lenna.png contains IHDR, sRGB, IDAT, and IEND chunks, while lena-gray.png and lena-gray-gm22.png contain only IHDR, IDAT, and IEND chunks.
I am trying to write a SWT component, that is able to take and draw an instance of java.awt.BufferedImage. My problem is that SWT's Image and AWT's BufferedImage are incompatible: SWT components can't draw java.awt.Image, AWT/Swing components can't draw org.eclipse.swt.graphics.Image.
There are several approaches that try to solve this problem in other ways (which also may have some variations, but basically there are these two):
Convert between SWT Image and AWT BufferedImage
Swing/SWT Integration
They all have shortcomings and didn't satisfy my expectations:
The first approach, to convert an SWT Image to a BufferedImage, results in poor performance for large images due to the creation of a new RGB instance for every Pixel.
The second approach has several shortcomings in usability. See the "workarounds" at the end of the linked article.
This lead to the conclusion that I'd try my best to write a component (based on org.eclipse.swt.widgets.Canvas or org.eclipse.swt.widgets.Composite) which allows to draw a BufferedImage directly without any conversion of images.
My approach was to draw it pixel by pixel. Therefore I simply had to get an instance of GC, walk the source BufferedImage line by line, left-to-right and drawing the corresponding Color using GC.setForeground(Color color) and GC.drawPoint(int x, int y).
First, I created a new instance of Color for every pixel, which uses quite a lot of memory and adds an additional delay, since new Color acquires system resources and creating a new object for every pixel also takes its time.
Then I tried to pre-load all possible (24 bit) Colors into an array before drawing the image. This lead to an explosion of memory usage (>= 600 MB), which was clear before I was trying it, but I had to verify it.
Caching only the used Colors also lead to more memory consumption than would have been required.
I think there has to be a more low-level approach that doesn't require that much memory, since SWT is able to draw whole (SWT) Images without consuming that much memory.
I would appreciate any ideas or solutions.
I found out there's a way to "convert" an BufferedImage to an Image by using the original image's data buffer if it is 24 bit RGB. This is possible, since the image formats are compatible.
final BufferedImage original = ImageIO.read(new File("some-image.jpg");
final PaletteData palette =
new PaletteData(0x0000FF, 0x00FF00, 0xFF0000);
// the last argument contains the byte[] with the image data
final ImageData data = new ImageData(original.getWidth(), original.getHeight(),
24, palette, 4,
((DataBufferByte) original.getData().getDataBuffer()).getData());
final Image converted = new Image(getDevice(), data);
This way, one doesn't have to create thousands of new objects. This approach comes with the disadvantage that one needs to ensure that the original image is of type RGB 24 bit. Otherwise the image has to be converted to this format.
After that, an image can be drawn with the following code:
// get the GC of your component
gc.drawImage(image, 0, 0);
Probably other bit depths can be converted in a similar way, but this is all I need for the moment.
I merge two images using the code below. One base image without transparency, one overlay image with transparency.
The file-size of the images one there own is 20kb and 5kb, respectively.
Once I merged the two images, the resulting file-size is > 100kb, thus at least 4 times the combined size of 25kb. I expected a file-size less than 25kb.
public static void mergeTwoImages(BufferedImage base, BufferedImage overlay, String destPath, String imageName) {
// create the new image, canvas size is the max. of both image sizes
int w = Math.max(base.getWidth(), overlay.getWidth());
int h = Math.max(base.getHeight(), overlay.getHeight());
BufferedImage combined = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
// paint both images, preserving the alpha channels
Graphics2D g2 = combined.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.drawImage(base, 0, 0, null );
g2.drawImage(overlay, 0, 0, null);
g2.dispose();
// Save as new image
saveImage(combined, destPath + "/" + imageName + "_merged.png");
}
My application has to be with very good performance, thus can anyone explain me why this effect happens and how I can reduce the resulting file size?
Thanks a lot!
EDIT:
Thanks a lot for your answers. The saveImage code is:
public static void saveImage(BufferedImage src, String file) {
try {
File outputfile = new File(file);
ImageIO.write(src, "png", outputfile);
} catch (IOException e) {
e.printStackTrace();
}
}
Because PNG is a lossless format, there are only two major factors that are likely to impact the file size:
How many pixels are in the file, and
How well the format can be compressed.
Since it sounds like you're doing an overlay, I'm guessing #1 is not changing. Compare the pixel dimensions of the input and output files to double-check this.
Most likely you're seeing issues because your merged image is more complicated, so the PNG filtering algorithms have a harder time compressing the files. There's not much you can do about this, short of changing the images or switching to a lossy file format.
To explain just a bit further, let's say you have one all-white image and one all-red image. Both are 100x100 pixels. These images would be really easy to compress because you'd just need to encode: repeat red 10000 times. Now, say you merge these images in a way that every other pixel comes from a different image. Now it's checkered. If you have a good encoding mechanism set up, you might still be able to encode this well by saying: repeat [red,white] 10000 times. But you'll notice even with this ideal encoding algorithm, I've increased the size of my encoded message by quite a bit. And if you don't have an encoding format that's perfectly ideal for this sort of thing, it all goes downhill from there.
In general, the more varied and random-seeming the pixels of your image are, compared to one another, the larger the resulting file will be.
Save the image as JPEG with a higher compression ratio/lower quality. For further details see:
How to decrease image thumbnail size in java
This answer to "Java Text on Image" for an SSCCE.
I need to translate colors in bitmap loaded to BufferedImage from RGB to YCbCr (luminance and 2 channels chrominance) and back after process.
I made it with functions used like rgb2ycbcr() in main method for each pixel, but it isn't so smart solution. I should use ColorSpace and ColorModel classes to get BufferedImage with correct color space. It would be more flexible method, but I don't know how to do that.
I'm lost and I need some tips. Can somebody help me?
As I understood your question, you want to do the following:
Load RGB image -> process YCbCr image -> Use RGB image again
And you want us to help you, to make this process as seamless as possible. First and foremost you want us to give you a simple way to avoid the -> (converting) parts.
Well I looked into the BufferedImage documentation. It seems, as if there doesn't exist a way to change the ColorSpace of an once created BufferedImage.
You could create a new BufferedImage with an YCbCr color space for that you can use the predefined ICC_ColorSpace. Then you copy the data from your old image possibly via ColorSpace.fromRGB to the YCbCr color space, do the image processing and then convert again via ColorSpace.toRGB. This method requires you to fully convert the image before and after processing via existing methods. Furthermore you have to know, how ICC_ColorSpace converts your image to YCbCr color space. Otherwise you can't know, which array indices corresponds to the same pixel.
If you just want to create a wrapper around the RGB-BufferedImage that lets you manipulate this image, as if it was an YCbCr image, that isn't possible with BufferedImage.
EDIT:
To convert the color space of a BufferedImage use ColorConvertOp. The code would look something like this:
ColorConvertOp cco = new ColorConvertOp(new YCbCrColorSpace(), null);
BufferedImage ycbcrImage = cco.filter( oldRGBImage, null );
This requires you to either write your own ColorSpace class or you could download and use the classes mentioned here. If you just want to load a JPEG image you should use the predefined classes.