I'm dealing with some satellite images, consisting of 16-bit .tiff images. The color is encoded as 16-bit per channel. I would like to know how I can convert these images to normal 8-bit RGB for further CNN processing.
I have tried OpenCV (cv2.read('file',-1)) and PIL (read('file')), but these two packages cannot recognize and read 16-bit tiff images.
Generally, when you want to read or write images in Python — of any bit-depth and format — it is best to use ImageIO. As the name suggests, its singular goal is to input/output images. Only caveat: It may ignore the image's meta data. That is: It may not deal correctly with images defining a color space other than the standard sRGB, or it might fail to preserve the image's intended orientation.
You would read in the image, say example.tif, like so:
import imageio
image = imageio.imread('example.tif')
As for the conversion, that's just basic math. The data structure in which you'll receive the pixel data is a NumPy array. Introspect image.shape and image.dtype. You should expect your images to have a shape of (y, x, 3), where y is the number of pixels in the vertical, x in the horizontal direction, and 3 represents the three color channels: red, green, blue. Its dtype (data type) should be uint16, meaning unsigned 16-bit integers.
Side note: As there are three color channels, each sampled with a 16-bit resolution, the color depth of the image is more commonly described as "48 bits" (per pixel).
16-bit integer numbers range between 0 and 65535 (= 216−1). They need to be coerced to the 8-bit range: 0 to 255 (= 28−1). So divide by 256 (= 28):
image = image / 256
This will yield an array of floating-point pixel values. Its data type must be explicitly cast to 8-bit integer in order to drop any fractions.
image = image.astype('uint8')
Equivalently, and more efficiently, you may also bit-shift the 16-bit values 8 bits to the right:
image = (image >> 8).astype('uint8')
This makes the conversion faster (by a factor of 2 or so on modern hardware) as it skips the floating-point operations.
Then, either use the final image array for further processing, or save it to a new file:
imageio.imwrite('example.png', image)
If all you want is to convert, your .tiff file's color space to RGB. Then Try:-
from PIL import Image
img = Image.open(r"Path_to_tiff_image")
img = img.convert("RGB")
img.save(r"path_of_destination_image")
The above code, first opens a .tiff image, then changes its color mode to RGB. And then saves it to the destination location.
Hey I used tifffile to handle the file and a calculation that I've found in a different thread here for rescaling the 16-bit image to 8-bit.
import numpy as np
import tifffile as tif
import cv2
image = tif.imread('/home/trance/test.tiff')
# Rescale 16-bit to 8-bit
img_rescaled = 255 * (image - image.min()) / (image.max() - image.min())
# Colourising image and saving it with opencv
img_col = cv2.applyColorMap(img_rescaled.astype(np.uint8), cv2.COLORMAP_INFERNO)
cv2.imwrite('/home/trance/test.png', img_col)
Related
I have 1 tif image for each RGB colour channel, and I would like to combine the 3 images to make a single RGB image with all 3 channels in png format using python. I have tried several experiments using the PIL library but I can't get it.
I uploaded 3 sample images to Google Drive here. Does anyone know how to do this?
The answer depends on what you are really trying to achieve...
If you want an accurate merge of the 3 channels, you should probably use the tifffile module to understand the floating point values in your input files and accurately represent them in your output files. In fact, gdal would probably be even better as it understands the GeoTIFF tags in your file. PIL is unable to handle RGB float32 images.
If you want something that vaguely allows some sort of approximate visualisation as a PNG, you will need to do some work to scale your values to something sensible (but not accurate) because PNG cannot represent float data like your images contain.
Here is a more accurate merge of your channels with tifffile:
from tifffile import imread, imwrite
import numpy as np
r = imread('r.tif')
g = imread('g.tif')
b = imread('b.tif')
RGB = np.dstack((r,g,b))
imwrite('result.tif', RGB)
With PIL you would use Image.merge() but your data is float, so you will need to convert it to uint8/uint16 first to get something you can store in a PNG:
from PIL import Image
import numpy as np
# Open images
red = Image.open('red_channel.tif')
green = Image.open('green_channel.tif')
blue = Image.open('blue_channel.tif')
# Convert PIL Images to Numpy arrays
npRed = np.array(red)
npGreen = np.array(green)
npBlue = np.array(blue)
# Get rid of the pesky -3.4e+38 marker for out-of-bounds pixels
npRed[npRed < 0] = 0
npBlue[npBlue < 0] = 0
npGreen[npGreen < 0] = 0
# Find maximum across all channels for scaling
max = np.max([npRed,npGreen,npBlue])
# Scale all channels equally to range 0..255 to fit in a PNG (could use 65,535 and np.uint16 instead)
R = (npRed * 255/max).astype(np.uint8)
G = (npGreen * 255/max).astype(np.uint8)
B = (npBlue * 255/max).astype(np.uint8)
# Build a PNG
RGB = np.dstack((R,G,B))
Image.fromarray(RGB).save('result.png')
With a uint8, 3-channel image and uint8 binary mask, I have done the following in opencv and python in order to change an object on a black background into an object on a transparent background:
# Separate image into its 3 channels
b, g, r = cv2.split(img)
# Merge channels back with mask (resulting in a 4-channel image)
imgBGRA = cv2.merge((b, g, r, mask))
However, when I try doing this with a uint16, 3-channel image and uint16 binary mask, the saved result is 4-channel, but the background is still black. (I saved it as a .tiff file and viewed it in Photoshop.)
How can I make the background transparent, keeping the output image uint16?
UPDATE
Seeing #Shamshirsaz.Navid and #fmw42 comments, I tried
imgBGRA=cv2.cvtColor(imgBGR, cv2.COLOR_BGR2BGRA). Then used Numpy to add the alpha channel from the mask: imgBGRA[:,:,3]=mask. (I hadn't tried this, as I thought that cvtColor operations required an 8-bit image.) Nonetheless, my results are the same.
I think the problem is my mask. When I run numpy.amin(mask), I get 0, and for numpy.amax(mask), I get 1. What should they be? I tried multiplying the mask by 255 prior to using the split/merge technique, but the background was still black. Then I tried mask*65535, but again the background was black.
I had tried to keep the scope of my initial post narrow. But it seems that my problem does lie somewhere in the larger scope of what I'm doing and how this uint16 mask gets created.
I'm using connectedComponentsWithStats (CC) to cut out the components on a uint16 image. CC requires an 8-bit mask, which I am using as input to CC. But the cutout results need to be from my uint16 original. This has required some alterations to the way I learned to use CC on uint8 images. Note that the per-component mask (which I eventually use to try to make the background transparent) is created as uint16. Here is the whittled down version:
# img is original image, dtype=uint16
# bin is binary mask, dtype=uint8
cc = cv2.connectedComponentsWithStats(bin, connectivity, cv2.CV_32S)
num_labels = cc[0]
labels = cc[1]
for i in range(1, num_labels):
maskg = (labels == i).astype(np.uint16) # with uint8: maskg = (labels == i).astype(np.uint8) * 255
# NOTE: I don't understand why removing the `* 255` works; but after hours of experimenting, it's the only way I could get the original to appear correctly when saving 'glyph'; for all other methods I tried the colors were off in some significant way -- either grayish blue whereas the object in my original is variations of brown, or else a pixelated rainbow of colors)
glyph = img * maskg[..., np.newaxis] # with uint8: glyph = cv2.bitwise_and(img, img, mask=maskg)
b, g, r = cv2.split(glyph)
glyphBGRA = cv2.merge((b, g, r, maskg))
example (my real original image is huge and, also, I am not able share it; so I put together this example)
img (original uint16 image)
bin (input uint8 mask)
maskg (uint16 component mask created within loop)
(this is a screenshot -- it shows up all black when uploaded directly)
glyph (img with maskg applied)
glyphBGRA (result of split and merge method trying to add transparency)
(this is also a screenshot -- this one showed up all white/blank when added directly)
I hope this added info provides sufficient context for my problem.
I checked your last comment. I think an example might be better. Your code is correct; The question is, how did you use it? I attached a picture and a mask to test on them.
import sys,cv2
main = cv2.imread(sys.path[0]+'/main.png')
mask = cv2.imread(sys.path[0]+'/mask.png', cv2.IMREAD_GRAYSCALE)
mask = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY)[1]
b, g, r = cv2.split(main)
bgra = cv2.merge((b, g, r, mask))
cv2.imwrite(sys.path[0]+'/out_split_merge.png',bgra)
Main:
Mask:
Output:
If you open the final output with an image editing software, you will notice that part of it is transparent.
Diagnosis: Opencv is not able to save tiff with an alpha channel.
The following is from the opencv docs' entry for imwrite():
The function imwrite saves the image to the specified file. The image
format is chosen based on the filename extension (see cv::imread for
the list of extensions). In general, only 8-bit single-channel or
3-channel (with 'BGR' channel order) images can be saved using this
function, with these exceptions:
16-bit unsigned (CV_16U) images can be saved in the case of PNG, JPEG 2000, and TIFF formats
32-bit float (CV_32F) images can be saved in PFM, TIFF, OpenEXR, and Radiance HDR formats; 3-channel (CV_32FC3) TIFF images will be
saved using the LogLuv high dynamic range encoding (4 bytes per
pixel)
PNG images with an alpha channel can be saved using this function. To do this, create 8-bit (or 16-bit) 4-channel image BGRA, where
the alpha channel goes last. Fully transparent pixels should have
alpha set to 0, fully opaque pixels should have alpha set to
255/65535 (see the code sample below).
How I got to this point:
I manually removed the background in Photoshop and saved as png file and as tiff file. (They both look like this:)
Then I ran:
import cv2
import numpy as np
png16 = cv2.imread('c:/users/scott/desktop/python2/teststack/png16.png', cv2.IMREAD_UNCHANGED)
tif16 = cv2.imread('c:/users/scott/desktop/python2/teststack/tif16.tiff', cv2.IMREAD_UNCHANGED)
print('png16:', png16.dtype, png16.shape)
b, g, r, a = cv2.split(png16)
mmin = np.amin(a)
mmax = np.amax(a)
print('png16-a channel:', a.dtype, a.shape, mmin, mmax)
pixvals = np.unique(a.flatten()) # get all unique pixel values in a
print('png16-a channel pixel values:', pixvals)
print('tif16:', tif16.dtype, tif16.shape)
b, g, r, a = cv2.split(tif16)
mmin = np.amin(a)
mmax = np.amax(a)
print('tif16-a channel:', a.dtype, a.shape, mmin, mmax)
pixvals = np.unique(a.flatten()) # get all unique pixel values in a
print('tif16-a channel pixel values:', pixvals)
png16copy = png16.copy()
tif16copy = tif16.copy()
cv2.imwrite('c:/users/scott/desktop/python2/teststack/png16copy.png', png16copy)
cv2.imwrite('c:/users/scott/desktop/python2/teststack/tif16copy.tiff', tif16copy)
The output is all as one should expect:
png16: uint16 (312, 494, 4)
png16-a channel: uint16 (312, 494) 0 65535
png16-a channel pixel values: [ 0 65535]
tif16: uint16 (312, 494, 4)
tif16-a channel: uint16 (312, 494) 0 65535
tif16-a channel pixel values: [ 0 65535]
Back in Photoshop, the png file looked like it did before:
But the tiff file did not.
Without alpha channel visible:
With alpha channel visible:
So I knew at this point that the problem was in the saving. I reread the opencv docs for imwrite and picked up on the logic: if it's not 8-bit single-channel or 3-channel, and if it's not spelled out explicitly in the exceptions, it won't work.
I did some more searching and found something that does work. I installed tifffile and ran:
from tifffile import imsave
tif16copy2 = cv2.cvtColor(tif16copy, cv2.COLOR_BGRA2RGBA)
imsave('c:/users/scott/desktop/python2/teststack/tif16copy2.tiff', tif16copy2)
Here is the result in Photoshop:
I am doing a denoising work and I'm not very familiar with Python. I applied BM3D to get the denoised picture and I also have the original one.
Now I want to get the noise by doing this:
tmp = img - img_denoised
But it turns out to be a very strange black and white figure like this:
So how can I get a proper noise picture? What I wish to get is image like this:
Edit:
Got an image from the Internet and done the same processing.
after processing:
Edit again:
Providing a simple example:
import cv2
img = cv2.imread("path of the original image")
img_denoised = cv2.imread("path of the denoised image")
tmp = img - img_denoised
cv2.imwrite("test_noise.jpg",tmp)
In your example code, both img and img_denoised are uint8 NumPy arrays. When operating on these arrays, the output is of the same type. These operations are modulo 256. When the result of an operation exceeds 255, it wraps around back to 0, and when the result is negative, it wraps around back to 255. For example:
np.array([5], np.uint8) - np.array([10], np.uint8)
return array([251], dtype=uint8). Instead of -5, which cannot be represented in a uint8 value, we get 256 - 5 = 251.
The subtraction img - img_denoised results in some values just above zero, which look black, and some values just below zero, which will be stored as values near 255 and look white.
We can solve this in different ways. One is to force the operation to happen with floating-point values:
tmp = img.astype(float) - img_denoised.astype(float)
We now have an array of floats, about half of them negative. But a JPEG file can only store uint8 values, and casting our float values to uint8 will get us back where we started. So we need to shift the origin (the zero value) to a middle-gray (typically 128):
tmp = img.astype(float) - img_denoised.astype(float)
tmp += 128
cv2.imwrite("test_noise.jpg", tmp.astype(np.uint8))
This is very fiddly, but it works. I prefer using a library that takes care of data types for me, so I don't have to think about them when I don't want to. DIPlib is such a library (disclaimer: I'm an author):
import diplib as dip
img = dip.ImageRead("7yJS3.png")
img_denoised = dip.ImageRead("xjQIy.png")
tmp = img - img_denoised
tmp += 128
dip.ImageWrite(tmp, "test_noise.jpg")
In DIPlib, arithmetic operations automatically promote the images to a floating-point type, unless we explicitly prevent it. Saving as JPEG silently casts the image to uint8 (this is where errors will happen if the pixel values are outside the range of the uint8 type).
With limited information it is hard to pin-point the problem. Please provide input images and more code.
Looks like the result image is a binary bitmap, only white or black, no gray. Your tmp image's pixel format is probably incorrect, which might be due to your img and img_denoised are not having the same pixel format, or both are wrong. Try display your input images to see if they look normal.
Your img and img_denoised should be the same pixel format, maybe 8-bit gray scale, or 24-bit RGB, and after img-img_denoised, the result should still have the same pixel format.
It could also due to it's unsigned, try to make it signed, or + 128 to all pixels and see what happened.
What is your image data type/range? 0-1 or 0-255?
if your image is 0-1 float32, the noise image will have a data range of [-1, 1], around half of the pixels is below 0, and when displayed by cv2.imshow() as "black".
Try
noise = origin - clean
noise = (noise + 1) * 0.5
When i try to save 16-bit image data in python, pixel values are automatically scaled to 65536. For example, max value in 16-bit data is 1536. If i saved and read this data, then max value becomes 65k. How to prevent this scaling? I tried different imaging library including opencv, PIL, misc, imageio , but all of these used this scaling.
For example:
img_arr # let it contains 16-bit gray-scale image data
print np.amax(img_arr) # it prints 1536
cv2.imwrite(img_arr, "asd.png")
img_arr = cv2.imread("asd.png")
print np.amax(img_arr) # it prints 65k value
Using OpenCV, this does what you seem to want.
import cv2
import numpy as np
# Synthesize 200x200 uint16 image with 39,999 as max value
im = np.arange(0,40000,dtype=np.uint16).reshape((200,200))
# Write to disk
cv2.imwrite('result.png',im)
# Re-read from disk
rr = cv2.imread('result.png',cv2.IMREAD_UNCHANGED)
# Check maximum value
print(rr.max()) # prints 39999
PIL/Pillow doesn't support 16-bit greyscale, so you'd need to promote to 32-bit signed, as so:
from PIL import Image
import numpy as np
# Synthesize 32-bit signed image
im = np.arange(0,40000,dtype=np.int32).reshape((200,200))
# Make into PIL Image
pim = Image.fromarray(im,'I')
# Save as 16-bit PNG, or 16-bit NetPBM PGM
pim.save('result.png')
pim.save('result.pgm')
# Re-read either file for same result, namely 39999
rr = np.array(Image.open('result.pgm')).max()
rr = np.array(Image.open('result.png')).max()
I am using OpenCV 2 to do some images manipulations in YCbCr color space. For the moment I can detect some noise due to the conversion RGB -> YCbCr and then YCbCr -> RGB, but as said in the documentation:
If you use cvtColor with 8-bit images, the conversion will have some information lost. For many applications, this will not be noticeable but it is recommended to use 32-bit images in applications that need the full range of colors or that convert an image before an operation and then convert back.
So I would like to convert my image in 16 or 32 bits, but I didn't found how to do it with NumPy. Some ideas?
img = cv2.imread(imgNameIn)
# Here I want to convert img in 32 bits
cv2.cvtColor(img, cv2.COLOR_BGR2YCR_CB, img)
# Some image processing ...
cv2.cvtColor(img, cv2.COLOR_YCR_CB2BGR, img)
cv2.imwrite(imgNameOut, img, [cv2.cv.CV_IMWRITE_PNG_COMPRESSION, 0])
Thanks to #moarningsun, problem resolved:
i = cv2.imread(imgNameIn, cv2.CV_LOAD_IMAGE_COLOR) # Need to be sure to have a 8-bit input
img = np.array(i, dtype=np.uint16) # This line only change the type, not values
img *= 256 # Now we get the good values in 16 bit format
The accepted answer is not accurate. A 16-bit image has 65536 intensity levels (2^16) hence, values ranging from 0 to 65535.
If one wants to obtain a 16-bit image from an image represented as an array of float ranging from 0 to 1, one has to multiply every coefficient of this array by 65535.
Also, it is good practice to cast the type of your end result as the very last step of the operations you perform.
This is mainly for two reasons:
- If you perform divisions or multiplications by float, the result will return a float and you will need to change the type again.
- In general (in the mathematical sense of the term), a transformation from float to integer can introduce errors. Casting the type at the very end of the operations prevents error propagation.