Writing text on a grayscale low resolution image - python

I have been trying to write text to an 80x80 16-bit grayscale image and have been having some trouble getting it to work.
I am currently using:
image = im[0]/255.0 #where im is just an np array of images (which are 80x80 np arrays)
# font
font = cv2.FONT_HERSHEY_SIMPLEX
# org
org = (40, 15)
# fontScale
fontScale = 0.3
# Blue color in BGR
color = (255.0)
# Line thickness of 2 px
thickness = 1
# Using cv2.putText() method
image = cv2.putText(image, 'Out:16', org, font, fontScale, color, thickness, cv2.LINE_AA)
# Displaying the image
cv2.imshow(window_name, image)
However, not only does the text look pretty awe full and take up a lot of space (I cant go lower without it not being legible), the images becomes all black except of the text which is white.
Would there be a better way to write text to a low resolution image (make the text smaller)? And why is the image turned to all black?
EDIT:
I tried using ImageDraw() and the result is all greyed
from PIL import Image, ImageFont, ImageDraw
# creating a image object
image = Image.fromarray(im[0]/255.0)
draw = ImageDraw.Draw(image)
# specified font size
font = ImageFont.truetype('./arial.ttf', 10)
text = 'fyp:16'
# drawing text size
draw.text((5, 5), text, font = font, align ="left")

It looks like the main issue is converting image type to float.
Assume (please verify it):
im[0] is 16-bit grayscale, and im[0].dtype is dtype('uint16').
image = im[0]/255.0 implies that you want to convert the range from 16-bit grayscale to the the range of uint8.
Note: for converting the range from [0, 2^16-1] to [0, 255] you need to divide by (2**16-1)/255 = 257.0. But this is not the main issue.
The main issue is converting the type to float.
The valid range of float images in OpenCV is [0, 1].
All values above 1.0 are white pixels, and 0.5 is a gray pixel.
You can keep the image type uint16 - you don't have to convert it to uint8.
A white text color for uint16 type is 2**16-1 = 65535 (not 255).
Here is code sample that works with 16-bit grayscale (and uint16 type):
import numpy as np
import cv2
im = np.full((80, 80), 10000, np.uint16) # 16 bits grayscale synthetic image - set all pixels to 10000
cv2.circle(im, (40, 40), 10, 0, 20, cv2.LINE_8) # draw black cicle - synthetic image
#image = im[0]/255.0 #where im is just an np array of images (which are 80x80 np arrays)
image = im #where im is just an np array of images (which are 80x80 np arrays)
color = 2**16-1 # 65535 is white color for 16 bis image
# Using cv2.putText() method
image = cv2.putText(image, 'Out:16', (40, 15), cv2.FONT_HERSHEY_SIMPLEX, 0.3, color, 1, cv2.LINE_AA)
# Displaying the image
cv2.imshow("image", image)
cv2.waitKey()
The above code creates synthetic 16-bit grayscale for testing.
Converting from 16-bit grayscale to 8-bit grayscale:
# https://stackoverflow.com/questions/11337499/how-to-convert-an-image-from-np-uint16-to-np-uint8
uint8_image = cv2.convertScaleAbs(image, alpha=255.0/(2**16-1)) # Convent uint16 image to uint8 image (2**16-1 scaled to 255)
The above conversion assumes image is full range 16 bits (pixel range [0, 65535]).
About the font:
OpenCV is computer vision oriented, and text drawing limited.
Why is the image black?
It's hard to answer without knowing the values of im[0].
It could be that im[0] is not a 16-bit grayscale at all.
It could be that the values of im[0] are very small.
It could be that the type of im[0] is not uint16.
Drawing text using Pillow (PIL):
The quality of the small text is much better compared to OpenCV.
You can find for about quality text rendering here.
Continue with the uint8 image:
pil_image = Image.fromarray(uint8_image)
draw = ImageDraw.Draw(pil_image)
# specified font size
font = ImageFont.truetype('./arial.ttf', 10)
text = 'fyp:16'
# drawing text size
draw.text((5, 5), text, 255, font = font, align ="left")
pil_image.show()
Result:
I don't really know the reason your text looks wired compared to above result.

Related

How to generate an alpha image with a color range with PIL?

I have a grayscale image and I want to create an alpha layer based on a range of pixel values. I want to know how can I create a fall-off function to generate such image.
The original image is the following:
I can use the color range in photoshop to select the shadows with fuzziness of 20%
And the resultant alpha channel is the following:
With fuzziness of 100%:
How can I generate such alpha channels in python with PIL?
I thought that maybe a subtract, but it does not generates a
The code to generate the image with Numpy and PIL:
from PIL import Image
import numpy as np
img = np.arange(0,256, 0.1).astype(np.uint8)
img = np.reshape(img, (img.shape[0], 1))
img = np.repeat((img), 500, axis=1)
img = Image.fromarray(img.T)
I tried to create a fall-off function from the distance of the pixel values but it does not have the same gradient. Maybe there is a different way?
def gauss_falloff(distance, c=0.2, alpha=255):
new_value = alpha * np.exp(-1 * ((distance) ** 2) / (c**2))
new_value = new_value.clip(0,255)
return new_value.astype(np.uint8)
test = img.T / 255
test = np.abs(test - pixel)
test = gauss_falloff(test, c=0.2, alpha=255)
test = Image.fromarray(test)
With my code:
Here's how you could do that
from PIL import Image, ImageDraw
# Create a new image with a transparent background
width, height = 300, 300
image = Image.new('RGBA', (width, height), (255, 255, 255, 0))
# Create a drawing context for the image
draw = ImageDraw.Draw(image)
# Set the starting and ending colors for the gradient
start_color = (255, 0, 0)
end_color = (0, 0, 255)
# Draw a gradient line with the specified color range
for x in range(width):
color = tuple(int(start_color[i] + (end_color[i] - start_color[i]) * x / width)
for i in range(3))
draw.line((x, 0, x, height), fill=color)
# Save the image
image.save('gradient.png')
This code creates a new image with a transparent background and a drawing context for that image. Then it draws a gradient line on the image with the specified color range. Finally, it saves the image as a PNG file.
Note: The Python Imaging Library (PIL) has been replaced by the Pillow library, which is a fork of PIL. If you are using Pillow, you can use the same code as above, but you need to import the Image and ImageDraw modules from the Pillow package instead of the PIL package.

How to efficiently change colors on a lot of images?

I have a huge dataset of images like this:
I would like to change the colors on these. All white should stay white, all purple should turn white and everything else should turn black. The desired output would look like this:
I've made the code underneath and it is doing what I want, but it takes way to long to go through the amount of pictures I have. Is there another and faster way of doing this?
path = r"C:path"
for f in os.listdir(path):
f_name = (os.path.join(path,f))
if f_name.endswith(".png"):
im = Image.open(f_name)
fn, fext = os.path.splitext(f_name)
print (fn)
im =im.convert("RGBA")
for x in range(im.size[0]):
for y in range(im.size[1]):
if im.getpixel((x, y)) == (255, 255, 255, 255):
im.putpixel((x, y),(255, 255, 255,255))
elif im.getpixel((x, y)) == (128, 64, 128, 255):
im.putpixel((x, y),(255, 255, 255,255))
else:
im.putpixel((x, y),(0, 0, 0,255))
im.show()
Your images seem to be palettised as they represent segmentations, or labelled classes and there are typically fewer than 256 classes. As such, each pixel is just a label (or class number) and the actual colours are looked up in a 256-element table, i.e. the palette.
Have a look here if you are unfamiliar with palletised images.
So, you don't need to iterate over all 12 million pixels, you can instead just iterate over the palette which is only 256 elements long...
#!/usr/bin/env python3
import sys
import numpy as np
from PIL import Image
# Load image
im = Image.open('image.png')
# Check it is palettised as expected
if im.mode != 'P':
sys.exit("ERROR: Was expecting a palettised image")
# Get palette and make into Numpy array of 256 entries of 3 RGB colours
palette = np.array(im.getpalette(),dtype=np.uint8).reshape((256,3))
# Name our colours for readability
purple = [128,64,128]
white = [255,255,255]
black = [0,0,0]
# Go through palette, setting purple to white
palette[np.all(palette==purple, axis=-1)] = white
# Go through palette, setting anything not white to black
palette[~np.all(palette==white, axis=-1)] = black
# Apply our modified palette and save
im.putpalette(palette.ravel().tolist())
im.save('result.png')
That takes 290ms including loading and saving the image.
If you have many thousands of images to do, and you are on a decent OS, you can use GNU Parallel. Change the above code to accept a command-line parameter which is the name of the image, and save it as recolour.py then use:
parallel ./recolour.py {} ::: *.png
It will keep all CPU cores on your CPU busy till they are all processed.
Keywords: Image processing, Python, Numpy, PIL, Pillow, palette, getpalette, putpalette, classes, classification, label, labels, labelled image.
If you're open to use NumPy, you can heavily speed-up pixel manipulations:
from PIL import Image
import numpy as np
# Open PIL image
im = Image.open('path/to/your/image.png').convert('RGBA')
# Convert to NumPy array
pixels = np.array(im)
# Get logical indices of all white and purple pixels
idx_white = (pixels == (255, 255, 255, 255)).all(axis=2)
idx_purple = (pixels == (128, 64, 128, 255)).all(axis=2)
# Generate black image; set alpha channel to 255
out = np.zeros(pixels.shape, np.uint8)
out[:, :, 3] = 255
# Set white and purple pixels to white
out[idx_white | idx_purple] = (255, 255, 255, 255)
# Convert back to PIL image
im = Image.fromarray(out)
That code generates the desired output, and takes around 1 second on my machine, whereas your loop code needs 33 seconds.
Hope that helps!

Reading BMP RGBA using python PIL doesn't work

I'm trying to read a RGBA BMP using python PIL, and it doesn't seem to work.
The following code segment shows that tensorflow bmp_decode function succeeds in this task, while PIL doesn't:
def read_image_tf(filename):
image_file = tf.read_file(filename, name='read_file')
decoded_bmp = tf.io.decode_bmp(bmp_image)
return decoded_bmp
def read_img_pil(filename):
img = np.asarray(Image.open(fh))
return img
img = K.eval(read_image_tf(<FILENAME>))
print (img.shape)
img = read_img_pil(<FILENAME>)
print (img.shape)
Output:
(3892, 3892, 4)
(3892, 3892, 3)
When trying to run imgobj.convert('RGBA') on Image.open(fh) I simply get a matrix that contains only the value of 255 (100% transparency, which is not the correct alpha value per pixel).
Is there a bug in PIL? Is there an alternative to reading RGBA using python?
PIL doesn't support 32 bit bitmap images. As the official documentation states:-
Pillow reads and writes Windows and OS/2 BMP files containing 1, L, P, or RGB data. 16-colour images are read as P images. Run-length encoding is not supported.
That's why it is generally recommended not to use Image.show() to view an image, as it converts the image to .bmp before displaying it. Therefore if the image contained alpha values (image of color mode LA, RGBA etc) the displayed image will not be display properly, and will have artifacts.
Therefore, when you try to open a .bmp image having RGBA color space in PIL, the color space gets truncated to RGB.
Example:-
from PIL import Image
# creating an red colored image with RGBA color space and full opacity
img = Image.new("RGBA", (100, 100), (255, 0, 0, 255))
# displaying the color mode of the image
print(img.mode)
# saving the image as a .bmp (bitmap)
img.save("new.bmp")
# Opening the previously saved .bmp image (having color mode RGBA)
img = Image.open("new.bmp")
# displaying the mode of the .bmp file
print(img.mode)
OUTPUT:-
RGBA
RGB

Replace Color Values in Image with Random Noise

I found something reasonably close to what I want to do here:
Python: PIL replace a single RGBA color
However, in my scenario I have images that were originally grayscale with color annotations added to the image (an x-ray with notes in color). I would like to replace any pixel that is not grayscale with random noise. My main problem is replacing values with noise and not a single color.
Edit: I figured out the random noise part, now just trying to figure out how to separate the color pixels from the pixels that were originally in grayscale.
from PIL import Image
import numpy as np
im = Image.open('test.jpg')
data = np.array(im) # "data" is a height x width x 3 numpy array
red, green, blue = data.T # Temporarily unpack the bands for readability
# Replace white with random noise...
white_areas = (red == 255) & (blue == 255) & (green == 255)
Z = random.random(data[...][white_areas.T].shape)
data[...][white_areas.T] = Z
im2 = Image.fromarray(data)
im2.show()
You could try
col_areas = np.logical_or(np.not_equal(red, blue), np.not_equal(red, green))
You could use this Pixel Editing python module
from PixelMenu import ChangePixels as cp
im = Image.open('test.jpg')
grayscalergb=(128, 128, 128) #RGB value of gray in your image
noise=(100,30,5) #You can adjust the noise based upon your requirements
outputimg=cp(im, col=grayscalergb, col2=noise, save=False,tolerance=100) #Adjust the tolerance until you get the right amount of noise in your image
Also:
I'd suggest you to use png images instead of jpg images because JPEG is designed with compression, everytime you load the image the RGB values change making it hard for your code to function perfectly everytime

getbbox method from python image library (PIL) not working

I want to crop an image to its smaller size, by cutting the white areas on the borders. I tried the solution suggested in this forum Crop a PNG image to its minimum size but the getbbox() method of pil is returning a bounding box of the same size of the image, i.e., it seems that it doesn't recognize the blank areas around. I tried the following:
>>>import Image
>>>im=Image.open("myfile.png")
>>>print im.format, im.size, im.mode
>>>print im.getbbox()
PNG (2400,1800) RGBA
(0,0,2400,1800)
I checked that my image has truly white croppable borders by cropping the image with the GIMP auto-crop. I also tried with ps and eps versions of the figure, without luck.
Any help would be highly appreciated.
Trouble is getbbox() crops off the black borders, from the docs: Calculates the bounding box of the non-zero regions in the image.
import Image
im=Image.open("flowers_white_border.jpg")
print im.format, im.size, im.mode
print im.getbbox()
# white border output:
JPEG (300, 225) RGB
(0, 0, 300, 225)
im=Image.open("flowers_black_border.jpg")
print im.format, im.size, im.mode
print im.getbbox()
# black border output:
JPEG (300, 225) RGB
(16, 16, 288, 216) # cropped as desired
We can do an easy fix for white borders, by first inverting the image using ImageOps.invert, and then use getbbox():
import ImageOps
im=Image.open("flowers_white_border.jpg")
invert_im = ImageOps.invert(im)
print invert_im.getbbox()
# output:
(16, 16, 288, 216)

Categories