How to avoid Pillow slightly editing my images after it saves them? - python

I'm trying to create a script that generate binary RGB images, all pixels must be black(0,0,0) or white(255,255,255). The problem is that when the script saves the output, some pixels will have random values of different shades of black and white such as (14,14,14), (18,18,18), (241,241,241).
#Code generated sample:
from PIL import Image
sample = Image.new('RGB', (2,2), color = (255,255,255))
#A four pixel image that will do just fine to this example
pixels = sample.load()
w, h = sample.size #width, height
str_pixels = ""
for i in range(w): #lines
for j in range(h): #columns
from random import randint
rand_bool = randint(0,1)
if rand_bool:
pixels[i,j] = (0,0,0)
str_pixels += str(pixels[i,j])
#This will be printed later as single block for readability
print("Code generated sample:") #The block above
print(str_pixels)
#Saved sample:
sample.save("sample.jpg")
saved_sample = Image.open("sample.jpg")
pixels = saved_sample.load()
w, h = saved_sample.size
str_pixels = ""
for i in range(w):
for j in range(h):
str_pixels += str(pixels[i,j])
print("Saved sample:")
print(str_pixels)
>> Code generated sample:
>>(255, 255, 255)(0, 0, 0)(0, 0, 0)(255, 255, 255)
>>Saved sample:
>>(248, 248, 248)(11, 11, 11)(14, 14, 14)(242, 242, 242)
A solution would be to create a philter that changes the values to 0 or 255 when those values will be actually on use, but hopefully there's a better one. This was tested using Windows.

This problem stems from the use of .jpg, which uses lossy spatial compression.
I recommend using .png, which is a lossless compression well suited to data like yours where you have very few distinct values. You can read about .png compression algorithms to learn more.

Related

using a variable in a filename in python PIL

I think i've conquered 95% of this short script to make images out of individual pixels, but I'd like help with using a variable as part of a filename. the line in question is as follows (i've used {} to denote the place I want to insert the variable):
img.save("new\\{i, j}.png")
The full code is
# Importing Image from PIL package
from PIL import Image
# Creating image object
im = Image.open("C:\\Users\\joeco\\Desktop\\Python-test-image\\image.jpg")
px = im.load()
# Defining image width and height
imageSizeW, imageSizeH = im.size
# Running through each pixel coord by column then row
for i in range(1, imageSizeW):
for j in range(1, imageSizeH):
# Removing non white pixels, then saving a single pixel of the colour to a new file
if px != (255, 255, 255):
img = Image.new('RGB', (1, 1), color = (px[i, j]))
img.save("new\\{i, j}.png")
You have to use the character f before a string if you want to include variables in that manner. In your example, you would use img.save(f"new\\{i}, {j}.png")
You could also use %i to save the filename like so: img.save("new\\%i, %i.png" % (i, j))

Mapping values of pixels in an image with Python

I am working with .png files, having pixels values 1 and 255.
I have to map pixel values 255 to 0 and, in other cases, pixels with values 1 to 2.
I tried:
for img_name in good_imgs:
img = Image.open(img_name)
pixels = img.load()
for i in range(img.size[0]):
for j in range(img.size[1]):
if pixels[i,j] == 255:
pixels[i,j] = 0
img.save(img_name)
for img_name in bad_imgs:
img = Image.open(img_name)
pixels = img.load()
for i in range(img.size[0]):
for j in range(img.size[1]):
if pixels[i,j] == 255:
pixels[i,j] = 0
elif pixels[i,j] == 1:
pixels[i,j] == 2
img.save(img_name)
but the images saved in this way, have the same pixels values as the originals.
What is wrong with the above code? How can I change pixels values in this kind of images?
Updtate:
I have noticed that if I modified the pixel of a single image, the pixels are mapped correctly, i.e. now I suppose the issue is about saving images with img.save()
As I mentioned in the comments, for loops are slow, inefficient and error-prone for processing images in Python.
Here's a more efficient way of doing what you ask. Your colours of 0, 1, 2 and 255 are hard to see on StackOverflow's background and hard to differentiate from each other because 3 of them are essentially black, so I am transforming 64 and 150 into 192 and 32 but you can adapt to your own numbers:
from PIL import Image
# Load image and ensure greyscale, i.e. 'L'
im = Image.open('image.png').convert('L')
# Remap colours 150 -> 32, 64 -> 192
res = im.point((lambda p: 32 if p==150 else (192 if p==64 else 0)))
res.save('result.png')
If you want to use Numpy instead, which is also vectorised and optimised, it might look like this (other techniques are also possible):
from PIL import Image
import numpy as np
# Load image and ensure greyscale, i.e. 'L'
im = Image.open('image.png').convert('L')
# Make into Numpy array
na = np.array(im)
# Anywhere pixels are 150, make them 32
na[na==150] = 32
# Anywhere pixels are 64, make them 192
na[na==64] = 192
# Convert back to PIL Image and save
Image.fromarray(na).save('result.png')

Why is my tiled image shifted with pasting into Pillow?

I am writing a program where I chop up an image into many sub-tiles, process the tiles, then stitch them together. I am stuck at the stitching part. When I run my code, after the first row the tiles each shift one space over. I am working with 1000x1000 tiles and the image size can be variable. I also get this ugly horizontal padding that I can't figure out how to get rid of.
Here is a google drive link to the images:
https://drive.google.com/drive/folders/1HqRl29YlWUrsYoZP88TAztJe9uwgP5PS?usp=sharing
Clarification based on the comments
I take the original black and white image and crop it into 1000px x 1000px black and white tiles. These tiles are then re-colored to replace the white with a color corresponding to a density heatmap. The recolored tiles are then saved into that folder. The picture I included is one of the colored in tiles that I am trying to piece back together. When pieced together it should be the same shape but multi colored version of the black and white image
from PIL import Image
import os
stitched_image = Image.new('RGB', (large_image.width, large_image.height))
image_list = os.listdir('recolored_tiles')
current_tile = 0
for i in range(0, large_image.height, 1000):
for j in range(0, large_image.width, 1000):
p = Image.open(f'recolored_tiles/{image_list[current_tile]}')
stitched_image.paste(p, (j, i), 0)
current_tile += 1
stitched_image.save('test.png')
I am attaching the original image that I process in tiles and the current state of the output image:
An example of the tiles found in the folder recolored_tiles:
First off, the code below will create the correct image:
from PIL import Image
import os
stitched_image = Image.new('RGB', (original_image_width, original_image_height))
image_list = os.listdir('recolored_tiles')
current_tile = 0
for y in range(0, original_image_height - 1, 894):
for x in range(0, original_image_width - 1, 1008):
tile_image = Image.open(f'recolored_tiles/{image_list[current_tile]}')
print("x: {0} y: {1}".format(x, y))
stitched_image.paste(tile_image, (x, y), 0)
current_tile += 1
stitched_image.save('test.png')
Explanation
First off, you should notice, that your tiles aren't 1000x1000. They are all 1008x984 because 18145x16074 can't be divided up into 19 1000x1000 tiles each.
Therefore you will have to put the correct tile width and height in your for loops:
for y in range(0, 16074, INSERT CURRECT RECOLORED_TILE HEIGHT HERE):
for x in range(0, 18145, INSERT CURRECT RECOLORED_TILE WIDTH HERE):
Secondly, how python range works, it doesn't run on the last digit. Representation:
for i in range(0,5):
print(i)
The output for that would be:
0
1
2
3
4
Therefore the width and height of the original image will have to be minused by 1, because it thinks you have 19 tiles, but there isn't.
Hope this works and what a cool project you're working on :)

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!

A code in Python that I need explanation to it

I've tried to learn python for the last 3 weeks, and I saw a code that I need to understand what it does.
In general, the code should somehow connect to two images, and then give me a password that I need to submit.
The code is:
#env 3.7
from PIL import Image, ImageFont
import textwrap
from pathlib import Path
def find_text_in_image(imgPath):
image = Image.open(imgPath)
red_band = image.split()[0]
xSize = image.size[0]
ySize = image.size[1]
newImage = Image.new("RGB", image.size)
imagePixels = newImage.load()
for f in range(xSize):
for j in range(zSize):
if bin(red_band.getpixel((i, j)))[-1] == '0':
imagePixels[i, j] = (255, 255, 255)
else: imagePixels[i, j] = (0,0,0)
newImgPath=str(Path(imgPath).parent.absolute())
newImage.save(newImgPath+'/text.png')
It would be lovely if someone could explain it to me.
thanks!
I'll break the above snippet into parts and explain each indivdiually.
The first block are imports. PIL is usually imported by installing the Pillow library. textwrap and pathlib are two packages included in the Python Standard Library.
#env 3.7
from PIL import Image, ImageFont
import textwrap
from pathlib import Path
The next block tells you you're about to define a function that does some image processing. I'll write more in inline comments.
def find_text_in_image(imgPath):
# open the image file given and load it as an `Image` from PIL
image = Image.open(imgPath)
# this splits the image into its Red, Green, and Blue channels
# then selects the Red
red_band = image.split()[0]
# these two lines get the size of the image, width and height
xSize = image.size[0]
ySize = image.size[1]
# this constructs a new `Image` object of the same size, but blank
newImage = Image.new("RGB", image.size)
# this makes an 3-d array of the new image's pixels
imagePixels = newImage.load()
# this loops over the width, so the iterator `f` will be the column
for f in range(xSize):
# loops over the height, so `j` will be the row
for j in range(zSize): # <-- This should probably be `ySize`. `zSize` is not defined.
# this is getting a pixel at a particular (column, row) in the red channel
# and checking if it can be binarized as 0
if bin(red_band.getpixel((i, j)))[-1] == '0':
# if so, set the same spot in the new image as white
imagePixels[i, j] = (255, 255, 255)
# if not, make it black
else: imagePixels[i, j] = (0,0,0)
# now make a new path and save the image
newImgPath=str(Path(imgPath).parent.absolute())
newImage.save(newImgPath+'/text.png')
There are major problems with this code as well. In some places you refer to zSize and i despite not defining them. Also, as a matter of practice, you can create paths with pathlib objects in the idiomatic way
newPath = Path(oldPath).with_name('new_filename.ext')

Categories