Related
I can't achieve to paste images in a square form (if I choose 9 for n_album, I should have a 3x3 collage). It only works for 1x1, if it's more it will paste the same image where another image is supposed to be.
Here is my code:
def make_montage(n_album, path):
x_offset = width #Constant resized image width
y_offset = height #Constant resized image height
c = []
x = 0
img = Image.new('RGB', (n_album*height + y_offset*2, n_album*width + x_offset*2), color = (0, 0, 0))
for file_name in os.listdir(path):
print(f"Processing {file_name}")
c.append(file_name)
print(f"root of n_album = {int(math.sqrt(n_album))}")
#Loop in square
for i in range(int(math.sqrt(n_album))):
for j in range(int(math.sqrt(n_album))):
try:
cover = Image.open(os.path.join(path, c[i + j]))
print(f"Pasting {str(c[i + j])}")
img.paste(cover, (int(i * height + y_offset), int(j * width + x_offset)))
except:
print("Je code mal mdr")
img.save(f'{path}\\{n_album}x{n_album}_musical.png')
#Clean
for file_name in os.listdir(path):
if file_name != f'{n_album}x{n_album}_musical.png':
print(f"Deleting {file_name}")
os.remove(os.path.join(path, file_name))
And here's a result for a 2x2 with images of (the order it was supposed to be pasted): Link, Mario, Princess Zelda, Peach.
I see several issues in your code:
In your method declaration, you should also pass the (desired) width and height of each image. (As is, your method assumes, that width and height are properly set outside.) This has also the advantage, that you can resize your images on-the-fly within your loop.
You don't pay attention, when to use n_album and when int(math.sqrt(n_album)). (See your 2x2 montage: You obviously initialized a 4x4 montage.) For the latter, create a variable like n_per_axis, so you don't have this math.sqrt term all the time.
In your nested loop, you have i = 1, 2, 3, and j = 1, 2, 3. Using c[i + j] then isn't the correct way to access the proper images from c. (In your 2x2 montage, you get (0 + 1) = 1 and (1 + 0) = 1, so Mario two times.) Set up a (global) image counter (let's say k), and increment it with each entering of the inner loop.
Don't post code including deleting of files, if that's not the main point of your question.
Here's your code with some modifications:
def make_montage(n_album, path, width, height): # <-- Width, height!?
x_offset = width
y_offset = height
c = []
n_per_axis = int(math.sqrt(n_album))
img = Image.new('RGB',
(n_per_axis*height + y_offset*2, # <-- n per axis!?
n_per_axis*width + x_offset*2), # <-- n per axis!?
color=(0, 0, 0))
for file_name in os.listdir(path):
print(f"Processing {file_name}")
c.append(file_name)
print(f"root of n_album = {n_per_axis}")
# Loop in square
k = -1 # <-- Image counter
for i in range(n_per_axis):
for j in range(n_per_axis):
k += 1
try:
cover = Image.open(os.path.join(path, c[k]))\
.resize((width, height)) # <-- Might be omitted here
print(f"Pasting {str(c[k])}")
img.paste(cover,
(int(i * height + y_offset),
int(j * width + x_offset)))
except:
print("Je code mal mdr")
img.save(f'{path}\\{n_per_axis}x{n_per_axis}_musical.png') # <-- n per axis!?
for file_name in os.listdir(path):
if file_name != f'{n_per_axis}x{n_per_axis}_musical.png':
print(f"Deleting {file_name}")
#os.remove(os.path.join(path, file_name))
Using
make_montage(9, 'path_with_nine_images/', 100, 100)
I get the following output:
Hope that helps!
I need to run a python script inside a folder.
the script count files ( image ) number create an image for
every 64 images.
example: if the folder containing 640 images, I will get 10
images as a combination of 64 images/outputimage
just, before adding "for" instruction, I can get a result, but manually and only with the same image ( a duplication of image ).
any idea?
so I proceed as follows:
import os
import os.path
from PIL import Image
list = os.listdir(".") # current directory
number_files = len(list)
print (number_files)
for x in range(0, number_files):
# files = ['x.png']
# opens an image: here I can't find how
im = Image.open("1.png") # here I tried to
# creates a new empty image, RGB mode, and size 800 by 800.
new_im = Image.new('RGB', (800, 800))
# Here I resize my opened image, so it is no bigger than 100,100
im.thumbnail((100, 100))
# Iterate through a 8 by 8 grid with 100 spacing, to place my image
for i in xrange(0, 800, 100):
for j in xrange(0, 800, 100):
# paste the image at location i,j:
new_im.paste(im, (i, j))
new_im.save(os.path.expanduser('outputimage.png'))
update :
import os
import os.path
from PIL import Image
def drange(start, stop, step):
while start < stop:
yield start
start += step
list = os.listdir(".") # directory path
number_files = len(list)
print (number_files)
new_im = Image.new('RGB', (800, 800))
for x in drange(0, number_files, 64):
im = Image.open(list[x])
im.thumbnail((100, 100))
for i in xrange(0, 800, 100):
for j in xrange(0, 800, 100):
new_im.paste(im, (i, j))
new_im.save(os.path.expanduser('out.png'))
other update based on Sven solution :
import os.path
from PIL import Image
fileList = [] where_to_look = "png/"
for f in os.listdir(where_to_look):
if os.path.isfile(os.path.join(where_to_look, f)):
fileList.append(f)
print (len(fileList))
target_img = None n_targets = 0 collage_saved = False
for n in range(len(fileList)):
img = Image.open(fileList[n])
img.thumbnail((100, 100))
if n % 64 == 0:
# create an empty image for a collage
target_img = Image.new("RGB", (800, 800))
n_targets += 1
collage_saved = False
# paste the image at the correct position
i = int(n / 8)
j = n % 8
target_img.paste(img, (100*i, 100*j))
if (n + 1) % 64 == 0 and target_img is not None:
# save a finished 8x8 collage
target_img.save("{0:04}.png".format(n_targets))
collage_saved = True
# save the last collage if not collage_saved:
target_img.save("{0:04}.png".format(n_targets))
Based on your update, I quickly sketched a solution. Please beware that I did not test it:
import os
from PIL import Image
image_dir = os.path.abspath("png")
# list all files in directory
files = os.listdir(image_dir)
# get all PNGs
png_files = filter(lambda x: x.endswith(".png"), files)
# make file paths absolute
image_files = map(lambda x: os.sep.join([image_dir, x]), png_files)
n_files = len(image_files)
target_img = None
n_targets = 0
collage_saved = False
for n in range(n_files):
img = Image.open(image_files[n])
img.thumbnail((100, 100))
if n % 64 == 0:
# create an empty image for a collage
target_img = Image.new("RGB", (800, 800))
n_targets += 1
collage_saved = False
# paste the image at the correct position
i = int(n / 8)
j = n % 8
target_img.paste(img, (100*i, 100*j))
if (n + 1) % 64 == 0 and target_img is not None:
# save a finished 8x8 collage
target_img.save("{0:04}.png".format(n_targets))
collage_saved = True
# save the last collage
if not collage_saved:
target_img.save("{0:04}.png".format(n_targets))
This will iterate over all your images and assemble them in a 8x8 grid (I call it collage in the script). Whenever the grid is filled, the collage is saved as a file with the naming pattern 0001.png, 0002.png and so on.
Note that there are some points to improve:
os.listdir(".") will give you all files in the directory, not only images. You would have to filter the output for image files.
This only works in the current working directory.
The output is also saved to the current working directory.
UPDATE: filter PNGs and use absolute paths
So I've got a Python script that takes a bunch of images in a folder, and puts them together into arrays (like this). I also have another script that takes the frames of a video and puts them together in arrays. The problem is, the one that takes the frames from a video creates black bars between the images.
Here is the correct image made using the first script, which uses JPEGS:
Here is the wrong image made using the second script, which uses video frames:
Here is the script that makes the correct first image:
import Image
import glob
import os
name = raw_input('What is the file name (excluding the extension) of your video that was converted using FreeVideoToJPGConverter?\n')
x_res = int(raw_input('What do you want the width of your image to be (in pixels)?\n'))
y_res = int(raw_input('What do you want the height of your image to be (in pixels)?\n'))
rows = int(raw_input('How many rows do you want?\n'))
columns = int(raw_input('How many columns do you want?\n'))
images = glob.glob('./' + name + ' (*)/' + name + '*.jpg')
new_im = Image.new('RGB', (x_res,y_res))
x_cntr = 0
y_cntr = 0
if not os.path.exists('./' + name + ' Output/'):
os.makedirs('./' + name + ' Output/')
for x in xrange(0,len(images),1):
if x%(rows*columns) == 0:
new_im.save('./' + name + ' Output/' + str(x) + '.jpg')
new_im = Image.new('RGB', (x_res,y_res))
y_cntr = 0
x_cntr = 0
print str(round(100*(float(x)/len(images)), 1)) + "% Complete"
elif x%rows == 0:
x_cntr = 0
y_cntr = y_cntr + y_res/columns
elif x%1 == 0:
x_cntr = x_cntr + x_res/rows
im = Image.open(images[x])
im = im.resize((x_res/rows + x_res%rows, y_res/columns + y_res%columns), Image.ANTIALIAS)
new_im.paste(im, (x_cntr, y_cntr))
Here is the script that makes the incorrect second image:
import cv2, Image, os
name = raw_input('Video File (With Extension): ')
x_res = int(raw_input('Image Width (Pixels): '))
y_res = int(raw_input('Image Height (Pixels): '))
rows = int(raw_input('Number of Rows: '))
columns = int(raw_input('Number of Columns: '))
vidcap = cv2.VideoCapture(name)
success,im = vidcap.read()
frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
new_im = Image.new('RGB', (x_res, y_res))
x_cntr = 0
y_cntr = 0
print str(frames) + " Frames to Join"
if not os.path.exists('./' + name + ' Output/'):
os.makedirs('./' + name + ' Output/')
for x in xrange(0,frames,1):
if x%(rows*columns) == 0:
new_im.save('./' + name + ' Output/' + str(x) + '.jpg')
new_im = Image.new('RGB', (x_res,y_res))
y_cntr = 0
x_cntr = 0
print str(round(100*(float(x)/frames), 1)) + "% Complete"
elif x%rows == 0:
x_cntr = 0
y_cntr = y_cntr + y_res/columns
elif x%1 == 0:
x_cntr = x_cntr + x_res/rows
success,cv2_im = vidcap.read()
if success == True:
cv2_im = cv2.cvtColor(cv2_im,cv2.COLOR_BGR2RGB)
im = Image.fromarray(cv2_im)
im = im.resize((x_res/rows + x_res%rows, y_res/columns + y_res%columns), Image.ANTIALIAS)
new_im.paste(im, (x_cntr, y_cntr))
elif success == False:
new_im.save('./' + name + ' Output/' + str(x) + '.jpg')
print str(round(100*(float(x)/frames), 1)) + "% Complete" #Why isn't this 100%, fix
As you can see, this specific line for resizing the image (to fit the new array of images) is exactly the same in both scripts:
im = im.resize((x_res/rows + x_res%rows, y_res/columns + y_res%columns), Image.ANTIALIAS)
...Except in the first script, the image is opened from a JPEG, and in the second script, the image is taken from a video frame using OpenCV2. If I try this with a different video, the same thing happens. It resizes as if I were using .thumbnail instead of .resize.
So why is there a different output even though they are the same script?
PS: I also don't know why there are more output images on the jpeg script than the video script, but that may be the fault of FreeVideoToJPGConverter (a software); I'm not sure though.
I'm creating a web-app that serves a dynamic image, with text.
Each string drawn may be in multiple colors.
So far I've created a parse method, and a render method. The parse method just takes the string, and parses colors from it, they are in format like this: "§aThis is green§rthis is white" (Yeah, it is Minecraft).
So this is how my font module looks like:
# Imports from pillow
from PIL import Image, ImageDraw, ImageFont
# Load the fonts
font_regular = ImageFont.truetype("static/font/regular.ttf", 24)
font_bold = ImageFont.truetype("static/font/bold.ttf", 24)
font_italics = ImageFont.truetype("static/font/italics.ttf", 24)
font_bold_italics = ImageFont.truetype("static/font/bold-italics.ttf", 24)
max_height = 21 # 9, from FONT_HEIGHT in FontRederer in MC source, multiplied by
# 3, because each virtual pixel in the font is 3 real pixels
# This number is also returned by:
# font_regular.getsize("ABCDEFGHIJKLMNOPQRSTUVWXYZ")[1]
# Create the color codes
colorCodes = [0] * 32 # Empty array, 32 slots
# This is ported from the original MC java source:
for i in range(0, 32):
j = int((i >> 3 & 1) * 85)
k = int((i >> 2 & 1) * 170 + j)
l = int((i >> 1 & 1) * 170 + j)
i1 = int((i >> 0 & 1) * 170 + j)
if i == 6:
k += 85
if i >= 16:
k = int(k/4)
l = int(l/4)
i1 = int(i1/4)
colorCodes[i] = (k & 255) << 16 | (l & 255) << 8 | i1 & 255
def _get_colour(c):
''' Get the RGB-tuple for the color
Color can be a string, one of the chars in: 0123456789abcdef
or an int in range 0 to 15, including 15
'''
if type(c) == str:
if c == 'r':
c = int('f', 16)
else:
c = int(c, 16)
c = colorCodes[c]
return ( c >> 16 , c >> 8 & 255 , c & 255 )
def _get_shadow(c):
''' Get the shadow RGB-tuple for the color
Color can be a string, one of the chars in: 0123456789abcdefr
or an int in range 0 to 15, including 15
'''
if type(c) == str:
if c == 'r':
c = int('f', 16)
else:
c = int(c, 16)
return _get_colour(c+16)
def _get_font(bold, italics):
font = font_regular
if bold and italics:
font = font_bold_italics
elif bold:
font = font_bold
elif italics:
font = font_italics
return font
def parse(message):
''' Parse the message in a format readable by render
this will return a touple like this:
[((int,int),str,str)]
so if you where to send it directly to the rederer you have to do this:
render(pos, parse(message), drawer)
'''
result = []
lastColour = 'r'
total_width = 0
bold = False
italics = False
for i in range(0,len(message)):
if message[i] == '§':
continue
elif message[i-1] == '§':
if message[i] in "01234567890abcdef":
lastColour = message[i]
if message[i] == 'l':
bold = True
if message[i] == 'o':
italics = True
if message[i] == 'r':
bold = False
italics = False
lastColour = message[i]
continue
width, height = _get_font(bold, italics).getsize(message[i])
total_width += width
result.append(((width, height), lastColour, bold, italics, message[i]))
return result
def get_width(message):
''' Calculate the width of the message
The message has to be in the format returned by the parse function
'''
return sum([i[0][0] for i in message])
def render(pos, message, drawer):
''' Render the message to the drawer
The message has to be in the format returned by the parse function
'''
x = pos[0]
y = pos[1]
for i in message:
(width, height), colour, bold, italics, char = i
font = _get_font(bold, italics)
drawer.text((x+3, y+3+(max_height-height)), char, fill=_get_shadow(colour), font=font)
drawer.text((x, y+(max_height-height)), char, fill=_get_colour(colour), font=font)
x += width
And it does work, but characters who are supposed to go below the ground line of the font, like g, y and q, are rendered on the ground line, so it looks strange, Here's an example:
Any ideas on how I can make them display corectly? Or do I have to make my own offset table, where I manually put them?
Given that you can't get the offsets from PIL, you could do this by slicing up images since PIL combines multiple characters appropriately. Here I have two approaches, but I think the first presented is better, though both are just a few lines. The first approach gives this result (it's also a zoom in on a small font which is why it's pixelated):
To explain the idea here, say I want the letter 'j', and instead of just making an image of just 'j', I make an image of ' o j' since that will keep the 'j' aligned correctly. Then I crop of the part I don't want and just keep the 'j' (by using textsize on both ' o ' and ' o j').
import Image, ImageDraw
from random import randint
make_color = lambda : (randint(50, 255), randint(50, 255), randint(50,255))
image = Image.new("RGB", (1200,20), (0,0,0)) # scrap image
draw = ImageDraw.Draw(image)
image2 = Image.new("RGB", (1200, 20), (0,0,0)) # final image
fill = " o "
x = 0
w_fill, y = draw.textsize(fill)
x_draw, x_paste = 0, 0
for c in "The quick brown fox jumps over the lazy dog.":
w_full = draw.textsize(fill+c)[0]
w = w_full - w_fill # the width of the character on its own
draw.text((x_draw,0), fill+c, make_color())
iletter = image.crop((x_draw+w_fill, 0, x_draw+w_full, y))
image2.paste(iletter, (x_paste, 0))
x_draw += w_full
x_paste += w
image2.show()
Btw, I use ' o ', rather than just 'o' since adjacent letters seem to slightly corrupt each other.
The second way is to make an image of the whole alphabet, slice it up, and then repaste this together. It's easier than it sounds. Here's an example, and both building the dictionary and concatenating into the images is only a few lines of code each:
import Image, ImageDraw
import string
A = " ".join(string.printable)
image = Image.new("RGB", (1200,20), (0,0,0))
draw = ImageDraw.Draw(image)
# make a dictionary of character images
xcuts = [draw.textsize(A[:i+1])[0] for i in range(len(A))]
xcuts = [0]+xcuts
ycut = draw.textsize(A)[1]
draw.text((0,0), A, (255,255,255))
# ichars is like {"a":(width,image), "b":(width,image), ...}
ichars = dict([(A[i], (xcuts[i+1]-xcuts[i]+1, image.crop((xcuts[i]-1, 0, xcuts[i+1], ycut)))) for i in range(len(xcuts)-1)])
# Test it...
image2 = Image.new("RGB", (400,20), (0,0,0))
x = 0
for c in "This is just a nifty text string":
w, char_image = ichars[c]
image2.paste(char_image, (x, 0))
x += w
Here's a (zoomed in) image of the resulting string:
Here's an image of the whole alphabet:
One trick here was that I had to put a space in between each character in my original alphabet image or I got the neighboring characters affecting each other.
I guess if you needed to do this a for a finite range of fonts and characters, it would be a good idea precalculate a the alphabet image dictionary.
Or, for a different approach, using a tool like numpy you could easily determine the yoffset of each character in the ichar dictionary above (eg, take the max along each horizontal row, and then find the max and min on the nonzero indices).
I simply solved this problem like this:
image = Image.new("RGB", (1000,1000), (255,255,255)) # 1000*1000 white empty image
# image = Image.fromarray(frame) # or u can get image from cv2 frame
draw = ImageDraw.Draw(image)
fontpath = "/etc/fonts/bla/bla/comic-sans.ttf"
font = ImageFont.truetype(fontpath, 35) # U can use default fonts
x = 20 # image draw start pixel x position
y = 100 # image draw start pixel y position
xDescPxl = draw.textsize("Descrition", font= font)[0]
draw.text((x, y), "Descrition" , font = font, fill = (0, 255, 0, 0)) # Green Color
draw.text((x + xDescPxl, y), ": u can do bla bla", font = font, fill = (0, 0, 0, 0)) # Black Color
Result:
Description: u can do bla bla
(20px space)---(Green Part)--(Black Part)
I'm looking at creating map tiles based on a 3D model made in blender,
The map is 16 x 16 in blender.
I've got 4 different zoom levels and each tile is 100 x 100 pixels. The entire map at the most zoomed out level is 4 x 4 tiles constructing an image of 400 x 400.
The most zoomed in level is 256 x 256 obviously constructing an image of 25600 x 25600
What I need is a script for blender that can create the tiles from the model.
I've never written in python before so I've been trying to adapt a couple of the scripts which are already there.
So far I've come up with a script, but it doesn't work very well. I'm having real difficulties getting the tiles to line up seamlessly. I'm not too concerned about changing the height of the camera as I can always create the same zoomed out tiles at 6400 x 6400 images and split the resulting images into the correct tiles.
Here is what I've got so far...
#!BPY
"""
Name: 'Export Map Tiles'
Blender: '242'
Group: 'Export'
Tip: 'Export to Map'
"""
import Blender
from Blender import Scene,sys
from Blender.Scene import Render
def init():
thumbsize = 200
CameraHeight = 4.4
YStart = -8
YMove = 4
XStart = -8
XMove = 4
ZoomLevel = 1
Path = "/Images/Map/"
Blender.drawmap = [thumbsize,CameraHeight,YStart,YMove,XStart,XMove,ZoomLevel,Path]
def show_prefs():
buttonthumbsize = Blender.Draw.Create(Blender.drawmap[0]);
buttonCameraHeight = Blender.Draw.Create(Blender.drawmap[1])
buttonYStart = Blender.Draw.Create(Blender.drawmap[2])
buttonYMove = Blender.Draw.Create(Blender.drawmap[3])
buttonXStart = Blender.Draw.Create(Blender.drawmap[4])
buttonXMove = Blender.Draw.Create(Blender.drawmap[5])
buttonZoomLevel = Blender.Draw.Create(Blender.drawmap[6])
buttonPath = Blender.Draw.Create(Blender.drawmap[7])
block = []
block.append(("Image Size", buttonthumbsize, 0, 500))
block.append(("Camera Height", buttonCameraHeight, -0, 10))
block.append(("Y Start", buttonYStart, -10, 10))
block.append(("Y Move", buttonYMove, 0, 5))
block.append(("X Start", buttonXStart,-10, 10))
block.append(("X Move", buttonXMove, 0, 5))
block.append(("Zoom Level", buttonZoomLevel, 1, 10))
block.append(("Export Path", buttonPath,0,200,"The Path to save the tiles"))
retval = Blender.Draw.PupBlock("Draw Map: Preferences" , block)
if retval:
Blender.drawmap[0] = buttonthumbsize.val
Blender.drawmap[1] = buttonCameraHeight.val
Blender.drawmap[2] = buttonYStart.val
Blender.drawmap[3] = buttonYMove.val
Blender.drawmap[4] = buttonXStart.val
Blender.drawmap[5] = buttonXMove.val
Blender.drawmap[6] = buttonZoomLevel.val
Blender.drawmap[7] = buttonPath.val
Export()
def Export():
scn = Scene.GetCurrent()
context = scn.getRenderingContext()
def cutStr(str): #cut off path leaving name
c = str.find("\\")
while c != -1:
c = c + 1
str = str[c:]
c = str.find("\\")
str = str[:-6]
return str
#variables from gui:
thumbsize,CameraHeight,YStart,YMove,XStart,XMove,ZoomLevel,Path = Blender.drawmap
XMove = XMove / ZoomLevel
YMove = YMove / ZoomLevel
Camera = Scene.GetCurrent().getCurrentCamera()
Camera.LocZ = CameraHeight / ZoomLevel
YStart = YStart + (YMove / 2)
XStart = XStart + (XMove / 2)
#Point it straight down
Camera.RotX = 0
Camera.RotY = 0
Camera.RotZ = 0
TileCount = 4**ZoomLevel
#Because the first thing we do is move the camera, start it off the map
Camera.LocY = YStart - YMove
for i in range(0,TileCount):
Camera.LocY = Camera.LocY + YMove
Camera.LocX = XStart - XMove
for j in range(0,TileCount):
Camera.LocX = Camera.LocX + XMove
Render.EnableDispWin()
context.extensions = True
context.renderPath = Path
#setting thumbsize
context.imageSizeX(thumbsize)
context.imageSizeY(thumbsize)
#could be put into a gui.
context.imageType = Render.PNG
context.enableOversampling(0)
#render
context.render()
#save image
ZasString = '%s' %(int(ZoomLevel))
XasString = '%s' %(int(j+1))
YasString = '%s' %(int((3-i)+1))
context.saveRenderedImage("Z" + ZasString + "X" + XasString + "Y" + YasString)
#close the windows
Render.CloseRenderWindow()
try:
type(Blender.drawmap)
except:
#print 'initialize extern variables'
init()
show_prefs()
This was relatively simple in the end.
I scaled up the model so that 1 tile on the map was 1 grid in blender.
Set the camera to be orthographic.
Set the scale on the camera to 1 for the highest zoom, 4 for the next one, 16 for the next one and so on.
Updated the start coordinates and move values accordingly.