using a variable in a filename in python PIL - python

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))

Related

Grayscale Heatmap to Color Gradient Heatmap

I am trying to take a set of 256x256px 8-bit grayscale .pngs (with transparency) and convert the grayscale .png to a color .png of the same size, still retaining transparency. The palette I want to use is Zissou1 from the R wesanderson package, which I have gotten into a Python dictionary where each key corresponds to a greyscale value and each value a HEX color.
import os
from PIL import Image, ImageColor
### dic = the dictionary containing the palette in format {grayscale pixel value: "HEX COLOR"},
### created earlier in the script
with Image.open("3.png") as im:
fin = Image.new("RGBA", im.size)
px = im.load()
px1 = fin.load()
for x in range(0,256):
for y in range(0,256):
px1.putpixel(x,y,ImageColor.getcolor(dic[px.getpixel(x,y)[1]],"RGBA"))
fin.show()
I am getting the error:
px1.putpixel(x,y,ImageColor.getcolor(dic[px.getpixel(x,y)[1]],"RGBA"))
AttributeError: 'PixelAccess' object has no attribute 'putpixel'
To extend on Jason's answer:
The lookup as given by PIL
With Image.point(lookup_table, mode = 'L') you can lookup and transpose the colors of your image.
lookup_table = ...
with Image.open("3.png") as orig:
image = orig.point(lookup_table, mode = 'L')
image.show()
To see an example for using the Image.point method with the lookup_table:
Using the Image.point() method in PIL to manipulate pixel data
Your own implementation (fixed with improved naming)
or implement the lookup against your_dic yourself:
your_dic = ...
with Image.open("3.png") as orig:
image = colored_from_map(orig, your_dic)
image.show()
with this alternative function (you almost did):
def colored_from_map(orig, map_to_color):
image_in = orig.load()
image = Image.new("RGBA", im.size)
image_out = image.load()
for x in range(0,256):
for y in range(0,256):
coords = (x,y)
greyscale = image_in.getpixel(x,y)[1]
color_name = map_to_color[greyscale]
image_out.putpixel(coords, ImageColor.getcolor(color_name,"RGBA"))
return image
Preserving the alpha-channel (transparency)
See the source-code of ImageColor.getColor()
at the begin and end of its method body:
color, alpha = getrgb(color), 255 # default to no-transparency
if len(color) == 4: # if your mapped color has 4th part as alpha-channel
color, alpha = color[0:3], color[3] # then use it
# omitted lines
else:
if mode[-1] == "A": # if the last char of `RGBA` is `A`
return color + (alpha,) # then return with added alpha-channel
return color
(comments mine)
So you could simply set the fourth element of the returned color-tuple to the previous value of the original gray-scale image:
greyscale = image_in.getpixel(x,y)[1] # containing original alpha-channel
color_name = map_to_color[greyscale] # name or hex
mapped_color = ImageColor.getcolor(color_name,"RGB") # removed the A
transposed_color = mapped_color[:2] + (greyscale[3],) # first 3 for RGB + original 4th for alpha-channel (transparency)
image_out.putpixel(coords, transposed_color)
Note: because the A(lpha-channel) is provided from original image, I removed the A from the getColor invocation's last argument. Technically, you can also remove the slicing from mapped_color[:2] to result in mapped_color + (greyscale[3],).
The first parameter to PIL's PixelAccess.putpixel method expects the pixel's coordinates to be passed as a (x,y) tuple:
px1.putpixel((x,y),ImageColor.getcolor(dic[px.getpixel(x,y)[1]],"RGBA"))
Alternatively, consider using the Image.point method which takes a look up table similar to the one you already created to map an image based on pixel values. See the answer at Using the Image.point() method in PIL to manipulate pixel data for more details

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

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.

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')

Creating and combining numerous images in Python - Error: Too many open files:

So basically I've got 2 codes:
One creates a number of different coloured images with 1 pixel dimensions
The other combines all the created images into one
The first one works perfectly but in the second code I get an error: IOError: [Errno 24] Too many open files: 'Test 3161.png'
The thing is I don't necessarily want to create the files. What I really want is the combined image at the end. I'm not sure how to approach this. Any help would be greatly appreciated.
Code 1 - Creating images
from PIL import Image
import sys
im = Image.new("RGB", (1, 1))
pix = im.load()
j=0
for r in range(65,130):
for g in range(65,130):
for b in range(65,130):
for x in range(1):
for y in range(1):
axis = (r,g,b)
pix[x,y] = axis
print axis
j+=1
im.save('Test {}.png'.format(j), "PNG")
Code 2: Combining images
from PIL import Image
import sys
import glob
imgfiles = []
for file in glob.glob("*.png"):
imgfiles.append(file)
print imgfiles
#stitching images together
images = map(Image.open, imgfiles)
widths, heights = zip(*(i.size for i in images))
total_width = sum(widths)
max_height = max(heights)
new_im = Image.new('RGB', (total_width, max_height))
x_offset = 0
for im in images:
new_im.paste(im, (x_offset,0))
x_offset += im.size[0]
new_im.save('test.png')
This is somewhat the final image I'm trying to get but not with as many colours as shown in it:
The coloured images that are created from code 1 are images that are 1 pixel in width and diameter. For example like this:
Its harder to see as its one pixel right next to this. It looks like a fullstop but is the 1 pixel image in question.
I still don't understand what you expect to produce, but this should be close and a lot faster and easier:
#!/usr/local/bin/python3
from PIL import Image
import numpy as np
# Create array to hold output image
result=np.zeros([1,13*13*13,3],dtype=np.uint8)
j=0
for r in range(65,130,5):
for g in range(65,130,5):
for b in range(65,130,5):
result[0,j]= (r,g,b)
j+=1
# Convert output array to image and save
im=Image.fromarray(result)
im.save("result.jpg")
Note that the above script is intended to do the job of both of your scripts in one go.
Note that I made the result image a bit taller (fatter) so you can see it, in fact it is only 1 pixel high.
Note that I added a step of 5 to make the output image smaller because it otherwise exceeds the size limits - for JPEG at least.
Note that I coarsely guessed the array width (13*13*13) on the basis of (130-65)/5, because I don't really understand your requirements.
To solve the too-many-open-files error, you can make a little function:
def getImageDetails(imgfile):
im = Image.open(imgfile)
size = im.size
im.load() # closes the pointer after it loads the image
return size[0], size[1], im
widths, heights, images = zip(*(getImageDetails(i) for i in imgfiles))
replace these lines with the code above:
images = map(Image.open, imgfiles)
widths, heights = zip(*(i.size for i in images))

In Python, Python Image Library 1.1.6, how can I expand the canvas without resizing?

I am probably looking for the wrong thing in the handbook, but I am looking to take an image object and expand it without resizing (stretching/squishing) the original image.
Toy example: imagine a blue rectangle, 200 x 100, then I perform some operation and I have a new image object, 400 x 300, consisting of a white background upon which a 200 x 100 blue rectangle rests. Bonus if I can control in which direction this expands, or the new background color, etc.
Essentially, I have an image to which I will be adding iteratively, and I do not know what size it will be at the outset.
I suppose it would be possible for me to grab the original object, make a new, slightly larger object, paste the original on there, draw a little more, then repeat. It seems like it might be computationally expensive. However, I thought there would be a function for this, as I assume it is a common operation. Perhaps I assumed wrong.
The ImageOps.expand function will expand the image, but it adds the same amount of pixels in each direction.
The best way is simply to make a new image and paste:
newImage = Image.new(mode, (newWidth,newHeight))
newImage.paste(srcImage, (x1,y1,x1+oldWidth,y1+oldHeight))
If performance is an issue, make your original image bigger than needed and crop it after the drawing is done.
Based on interjays answer:
#!/usr/bin/env python
from PIL import Image
import math
def resize_canvas(old_image_path="314.jpg", new_image_path="save.jpg",
canvas_width=500, canvas_height=500):
"""
Resize the canvas of old_image_path.
Store the new image in new_image_path. Center the image on the new canvas.
Parameters
----------
old_image_path : str
new_image_path : str
canvas_width : int
canvas_height : int
"""
im = Image.open(old_image_path)
old_width, old_height = im.size
# Center the image
x1 = int(math.floor((canvas_width - old_width) / 2))
y1 = int(math.floor((canvas_height - old_height) / 2))
mode = im.mode
if len(mode) == 1: # L, 1
new_background = (255)
if len(mode) == 3: # RGB
new_background = (255, 255, 255)
if len(mode) == 4: # RGBA, CMYK
new_background = (255, 255, 255, 255)
newImage = Image.new(mode, (canvas_width, canvas_height), new_background)
newImage.paste(im, (x1, y1, x1 + old_width, y1 + old_height))
newImage.save(new_image_path)
resize_canvas()
You might consider a rather different approach to your image... build it out of tiles of a fixed size. That way, as you need to expand, you just add new image tiles. When you have completed all of your computation, you can determine the final size of the image, create a blank image of that size, and paste the tiles into it. That should reduce the amount of copying you're looking at for completing the task.
(You'd likely want to encapsulate such a tiled image into an object that hid the tiling aspects from the other layers of code, of course.)
This code will enlarge a smaller image, preserving aspect ratio, then center it on a standard sized canvas. Also preserves transparency, or defaults to gray background.
Tested with P mode PNG files.
Coded debug final.show() and break for testing. Remove lines and hashtag on final.save(...) to loop and save.
Could parameterize canvas ratio and improve flexibility, but it served my purpose.
"""
Resize ... and reconfigures. images in a specified directory
Use case: Images of varying size, need to be enlarged to exaxtly 1200 x 1200
"""
import os
import glob
from PIL import Image
# Source directory plus Glob file reference (Windows)
source_path = os.path.join('C:', os.sep, 'path', 'to', 'source', '*.png')
# List of UNC Image File paths
images = glob.glob(source_path)
# Destination directory of modified image (Windows)
destination_path = os.path.join('C:', os.sep, 'path', 'to', 'destination')
for image in images:
original = Image.open(image)
# Retain original attributes (ancillary chunks)
info = original.info
# Retain original mode
mode = original.mode
# Retain original palette
if original.palette is not None:
palette = original.palette.getdata()[1]
else:
palette = False
# Match original aspect ratio
dimensions = original.getbbox()
# Identify destination image background color
if 'transparency' in info.keys():
background = original.info['transparency']
else:
# Image does not have transparency set
print(image)
background = (64)
# Get base filename and extension for destination
filename, extension = os.path.basename(image).split('.')
# Calculate matched aspect ratio
if dimensions[2] > dimensions[3]:
width = int(1200)
modifier = width / dimensions[2]
length = int(dimensions[3] * modifier)
elif dimensions[3] > dimensions[2]:
length = int(1200)
modifier = length / dimensions[3]
width = int(dimensions[2] * modifier)
else:
width, length = (1200, 1200)
size = (width, length)
# Set desired final image size
canvas = (1200, 1200)
# Calculate center position
position = (
int((1200 - width)/2),
int((1200 - length)/2),
int((1200 - width)/2) + width,
int((1200 - length)/2) + length
)
# Enlarge original image proportionally
resized = original.resize(size, Image.LANCZOS)
# Then create sized canvas
final = Image.new(mode, canvas, background)
# Replicate original properties
final.info = info
# Replicate original palatte
if palette:
final.putpalette(palette)
# Cemter paste resized image to final canvas
final.paste(resized, position)
# Save final image to destination directory
final.show()
#final.save("{}\\{}.{}".format(destination_path, filename, extension))
break

Categories