Python list not appending & variable not defined - python

I am having one small problem with my program and as I am very new to Python am struggling to find a solution. I have googled about a bit but nothing seemed to work from what I tried. My code is the following:
from PIL import Image
import numpy as np
from scipy import misc
from pandas import *
def maze_gen():
##Opens the Image
im_file = Image.open('15x15.png')
##Reads the image pixel information
arr = misc.imread('15x15.png')
##Sets the width, height and maze size variables
width = im_file.size[0]
height = im_file.size[1]
size = width * height
##Defines the mapping, start and end points array
map = np.zeros([width, height], dtype=np.int)
start_pos = np.empty(shape=[1,2])
end_pos = np.empty(shape=[1,2])
##Search image replacing white pixels with 1's in the mapping array
for x in range(width):
for y in range(height):
if 255 in arr[x,y]:
map[x,y] = 1
##Find the start and end locations
for x in range(width):
if map[0,x] > 0:
start_pos = 0,x
for x in range(width):
if map[height -1 ,x] > 0:
end_pos = height -1 ,x
return width, height, size, map, start_pos, end_pos
def check_neighbours(col, row):
width, height, size, map, start_pos, end_pos=maze_gen()
neighbours = list()
##Debugging to check cell_current values are correct
print('col =', col)
print('row =', row)
if (col >= 0 and col < height and row >= 0 and row < width):
neighbours.append((col, row))
print('Neighbours', neighbours, '\n')
if (len(neighbours) > 0):
return neighbours
else:
return None
def solver():
width, height, size, map, start_pos, end_pos=maze_gen()
##Sets cell_current to the starting position
cell_current = start_pos
path = list()
path.append((cell_current))
check_neighbours(cell_current[0]-1, cell_current[1]) ##Top neighbour
check_neighbours(cell_current[0], cell_current[1]+1) ##Right neighbour
check_neighbours(cell_current[0]+1, cell_current[1]) ##Bottom neighbour
check_neighbours(cell_current[0], cell_current[1]-1) ##Left neighbour
print('Neighbours in Solver', neighbours, '\n')
def debug():
width, height, size, map, start_pos, end_pos=maze_gen()
##Prints maze information for debugging
print ('Maze width:', width)
print ('Maze height:', height)
print ('Maze size:', size, '\n')
##Print Start and End points
print ('Start Point')
print (start_pos, '\n')
print ('End Point')
print (end_pos, '\n')
##Prints mapping array for debugging
print ('Mapping Array')
print (DataFrame(map), '\n')
if (__name__ == "__main__"):
solver()
The issues I am running into are the list neighbours isn't being added to with each successful run through of the if statement. Instead of getting the following readout:
col = -1 row = 1 Neighbours []
col = 0 row = 2 Neighbours [(0, 2)]
col = 1 row = 1 Neighbours [(1, 1)]
col = 0 row = 0 Neighbours [(0, 0)]
Any help on this problem would be amazing. Please do bare in mind that I am very new to python so any stupid mistakes or things I could change to make the code better please do say!

You need to make two changes in your code:
Make neighbours variable global.
Replace neighbours.append((col, row)) with neighbours.append([col, row]). This is because you can only append a variable or a list to an existing list.
Hope this helps.

Related

TextItem scaling in pyqtgraph

I'm having a problem with the font scaling of TextItems in pyqtgraph, like you can see from the following code when I zoom in/zoom out in the main graph the font of the TextItems stays the same while I'm trying to make It scale in the same exact way (rate) of the QGraphicsRectItem. I've tried to look on all the forums I know but I haven't find an answer so I really hope someone has a solution for this.
import sys
import pyqtgraph as pg
from PyQt6.QtWidgets import QApplication, QGraphicsRectItem
from pyqtgraph.Qt import QtCore
app = QApplication(sys.argv)
view = pg.GraphicsView()
l = pg.GraphicsLayout()
view.setCentralItem(l)
view.show()
view.resize(800, 600)
p0 = l.addPlot(0, 0)
p0.showGrid(x=True, y=True, alpha=1.0)
# have no x-axis tickmark below the upper plot (coordinate 0,0)
# without these lines, there will be separate coordinate systems with a gap inbetween
ay0 = p0.getAxis('left') # get handle to y-axis 0
ay0.setStyle(showValues=False) # this will remove the tick labels and reduces gap b/w plots almost to zero
# there will be a double line separating the plot rows
# ay02 = p0.getAxis('right')
# ay02.setStyle(showValues=False)
p0.hideAxis('right')
ax02 = p0.getAxis('top')
ax02.setStyle(showValues=False)
p1 = l.addPlot(0, 1)
# p1.showGrid(x=True, y=True, alpha=1.0)
p1.setYLink(p0)
l.layout.setSpacing(0.5)
l.setContentsMargins(0., 0., 0., 0.)
p1.setFixedWidth(300)
# p1.setFixedHeight(h-451)
p1.setMouseEnabled(x=False)
# ay1 = p1.getAxis('left')
# ay1.setStyle(showValues=False)
ax12 = p1.getAxis('top')
ax12.setStyle(showValues=False)
# ax1 = p1.getAxis('bottom')
# ax1.setStyle(showValues=False)
p1.showAxis('right')
p1.hideAxis('left')
p1.setXRange(0, 6, padding=0) # Then add others like 1 pip
# p1.getAxis('bottom').setTextPen('black')
board = ['123456',
'abcdef',
'ghilmn']
def draw_board(board2):
for j, row in enumerate(board2):
for i, cell in enumerate(row):
rect_w = 1
rect_h = 1
r = QGraphicsRectItem(i, -j+2, rect_w, rect_h)
r.setPen(pg.mkPen((0, 0, 0, 100)))
r.setBrush(pg.mkBrush((50, 50, 200)))
p1.addItem(r)
t_up = pg.TextItem(cell, (255, 255, 255), anchor=(0, 0))
t_up.setPos(i, -j+1+2)
p1.addItem(t_up)
draw_board(board)
if __name__ == '__main__':
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QApplication.instance().exec()
Scaling of a text item is quite difficult, as you need to consider a constant aspect ratio of the base scale, and the problems related to the way fonts are positioned and drawn relative to the origin point.
Assuming that the displayed text will always be a single character and that the characters used are standard ascii letters and numbers, the only possibility is to cycle through all possible characters, and create properly aligned paths for each of them.
So, for every character:
construct a QPainterPath;
add the letter to the path;
get the max() of that path width and the others;
get the minimum Y and maximum bottom of the bounding rectangle;
translate the path based on all other values computed above (in a separate loop);
Then, you have to set a reference size for the letter (using the maximum width above and the font metrics' height) and get the aspect ratio for that size.
The last part is implemented in the paint() function of the QGraphicsRectItem subclass, which is required to get the proper geometry of the item (if any transformation is applied to a parent item, the item will not know it), and get the maximum rectangle for the reference size based on the current rectangle size.
class NumberRectItem(QGraphicsRectItem):
textSize = None
textPaths = {}
textPath = None
def __init__(self, x, y, width, height, letter=''):
super().__init__(x, y, width, height)
if letter:
if not self.textPaths:
self._buildTextPaths()
self.textPath = self.textPaths[letter]
def _buildTextPaths(self):
from string import ascii_letters, digits
font = QApplication.font()
fm = QFontMetricsF(font)
maxWidth = 0
minY = 1000
maxY = 0
for l in ascii_letters + digits:
path = QPainterPath()
path.addText(0, 0, font, l)
br = path.boundingRect()
maxWidth = max(maxWidth, br.width())
minY = min(minY, br.y())
maxY = max(maxY, br.bottom())
self.textPaths[l] = path
self.__class__.textSize = QSizeF(maxWidth, fm.height())
self.__class__.textRatio = self.textSize.height() / self.textSize.width()
middle = minY + (maxY - minY) / 2
for path in self.textPaths.values():
path.translate(
-path.boundingRect().center().x(),
-middle)
def paint(self, qp, opt, widget=None):
super().paint(qp, opt, widget)
if not self.textPath:
return
qp.save()
qp.resetTransform()
view = widget.parent()
sceneRect = self.mapToScene(self.rect())
viewRect = view.mapFromScene(sceneRect).boundingRect()
rectSize = QSizeF(viewRect.size())
newSize = self.textSize.scaled(rectSize, Qt.KeepAspectRatio)
if newSize.width() == rectSize.width():
# width is the maximum
ratio = newSize.width() / self.textSize.width()
else:
ratio = newSize.height() / self.textSize.height()
transform = QTransform().scale(ratio, ratio)
path = transform.map(self.textPath)
qp.setRenderHint(qp.Antialiasing)
qp.setPen(Qt.NoPen)
qp.setBrush(Qt.white)
qp.drawPath(path.translated(viewRect.center()))
qp.restore()
def draw_board(board2):
for j, row in enumerate(board2):
for i, cell in enumerate(row):
rect_w = 1
rect_h = 1
r = NumberRectItem(i, -j+2, rect_w, rect_h, letter=cell)
r.setPen(pg.mkPen((150, 0, 0, 255)))
r.setBrush(pg.mkBrush((50, 50, 200, 128)))
p1.addItem(r)
Note: for PyQt6 you need to use the full enum names: Qt.GlobalColor.white, etc.

Scanning lists more efficiently in python

I have some code, which works as intended, however takes about 4 and a half hours to run, I understand that there are about 50 billion calculations my poor pc needs to do but I thought it would be worth asking!
This code gets an image, and wants to find every possible region of 331*331 pixels in the given image, and find how many black pixels there are in each, I will use this data to create a heatmap of black pixel density, and also a list of all of the values found:
image = Image.open(self.selectedFile)
pixels = list(image.getdata())
width, height = image.size
pixels = [pixels[i * width:(i+1) * width] for i in range(height)]
#print(pixels)
rightShifts = width - 331
downShifts = height - 331
self.totalRegionsLabel['text'] = f'Total Regions: {rightShifts * downShifts}'
self.blackList = [0 for i in range(0, rightShifts*downShifts)]
self.heatMap = [[] for i in range(0, downShifts)]
for x in range(len(self.heatMap)):
self.heatMap[x] = [0 for i in range(0, rightShifts)]
for x in range(rightShifts):
for y in range(downShifts):
blackCount = 0
for z in range(x + 331):
for w in range(y + 331):
if pixels[z][w] == 0:
blackCount += 1
self.blackList[x+1*y] = blackCount
self.heatMap[x][y] = blackCount
print(self.blackList)
You have several problems here, as I pointed out. Your z/w loops are always starting at the upper left, so by the time you get towards the end, you're summing the entire image, not just a 331x331 subset. You also have much confusion in your axes. In an image, [y] is first, [x] is second. An image is rows of columns. You need to remember that.
Here's an implementation as I suggested above. For each column, I do a full sum on the top 331x331 block. Then, for every row below, I just subtract the top row and add the next row below.
self.heatMap = [[0]*rightShifts for i in range(downShifts)]
for x in range(rightShifts):
# Sum up the block at the top.
blackCount = 0
for row in range(331):
for col in range(331):
if pixels[row][x+col] == 0:
blackCount += 1
self.heatMap[0][x] = blackCount
for y in range(1,downShifts):
# To do the next block down, we subtract the top row and
# add the bottom.
for col in range(331):
blackCount += pixels[y+330][x+col] - pixels[y-1][x+col]
self.heatMap[y][x] = blackCount
You could tweak this even more by alternating the columns. So, at the bottom of the first column, scoot to the right by subtracting the first column and adding the next new column. then scoot back up to the top. That's a lot more trouble.
The two innermost for-loops seem to be transformable to some numpy code if using this package is not an issue. It would give something like:
pixels = image.get_data() # it is probably already a numpy array
# Get an array filled with either True or False, with True whenever pixel is black:
pixel_is_black = (pixels[x:(x+331), y:(y+331)] == 0)
pixel_is_black *= 1 # Transform True and False to respectively 1 and 0. Maybe not needed
self.blackList[x+y] = pixel_is_black.sum() # self explanatory
This is the simplest optimization I can think of, you probably can do much better with clever numpy tricks.
I would recommend using some efficient vector computations through the numpy and opencv libraries.
First, binarize your image so that black pixels are set to zero, and any other color pixels (gray to white) are set to 1. Then, apply a 2D filter to the image of shape 331 x 331 where each value in the filter kernel is (1 / (331 x 331) - this will take the average of all the values in each 331x331 area and assign it to the center pixel.
This gives you a heatmap, where each pixel value is the proportion of non-black pixels in the surrounding 331 x 331 region. A darker pixel (value closer to zero) means more pixels in that region are black.
For some background, this approach uses image processing techniques called image binarization and box blur
Example code:
import cv2
import numpy as np
# setting up a fake image, with some white spaces, gray spaces, and black spaces
img_dim = 10000
fake_img = np.full(shape=(img_dim, img_dim), fill_value=255, dtype=np.uint8) # white
fake_img[: img_dim // 3, : img_dim // 3] = 0 # top left black
fake_img[2 * img_dim // 3 :, 2 * img_dim // 3 :] = 0 # bottom right black
fake_img[img_dim // 3 : 2 * img_dim // 3, img_dim // 3 : 2 * img_dim // 3] = 127 # center gray
# show the fake image
cv2.imshow("", fake_img)
cv2.waitKey()
cv2.destroyAllWindows()
# solution to your problem
binarized = np.where(fake_img == 0, 0, 1) # have 0 values where black, 1 values else
my_filter = np.full(shape=(331, 331), fill_value=(1 / (331 * 331))) # set up filter
heatmap = cv2.filter2D(fake_img, 1, my_filter) # apply filter, which takes average of values in 331x331 block
# show the heatmap
cv2.imshow("", heatmap)
cv2.waitKey()
cv2.destroyAllWindows()
I ran this on my laptop, with a huge (fake) image of 10000 x 10000 pixels, almost instantly.
Sorry I should have deleted this post before you all put the effort in, however, some of these workarounds are really smart and interesting, I ended up coming up with a solution independently that is the same as what Tim Robbers first suggested, I used the array I had and built a second one on which every item in a row is the number of black cells preceding it, and then for each row in a region instead of scanning every item, just scan the preceding value and the final value and you are good:
image = Image.open(self.selectedFile).convert('L') #convert to luminance mode as RGB information is irrelevant
pixels = list(image.getdata()) #get the value of every pixel in the image
width, height = image.size
pixels = [pixels[i * width:(i+1) * width] for i in range(height)] #split the pixels array into a two dimensional array with the dimensions to match the image
#This program scans every possible 331*331 square starting from the top left, so it will move right width - 331 pixels and down height - 331 pixels
rightShifts = width - 331
downShifts = height - 331
self.totalRegionsLabel['text'] = f'Total Regions: {rightShifts * downShifts}' #This wont update till the function has completed running
#The process of asigning new values to values in an array is faster than appending them so this is why I prefilled the arrays:
self.heatMap = [[] for i in range(0, downShifts)]
for x in range(len(self.heatMap)):
self.heatMap[x] = [0 for i in range(0, rightShifts)]
cumulativeMatrix = [] #The cumulative matrix replaces each value in each row with how many zeros precede it
for y in range(len(pixels)):
cumulativeMatrix.append([])
cumulativeMatrix[y].append(0)
count = 0
for x in range(len(pixels[y])):
if pixels[y][x] == 0:
count += 1
cumulativeMatrix[y].append(count)
regionCount = 0
maxValue = 0 #this is the lowest possible maximum value
minValue = 109561 #this is the largest possible minimum value
self.blackList = []
#loop through all possible regions
for y in range(downShifts):
for x in range(rightShifts):
blackPixels = 0
for regionY in range(y, y + 331):
lowerLimit = cumulativeMatrix[regionY][x]
upperLimit = cumulativeMatrix[regionY][x+332]
blackPixels += (upperLimit - lowerLimit)
if blackPixels > maxValue:
maxValue = blackPixels
if blackPixels < minValue:
minValue = blackPixels
self.blackList.append(blackPixels)
self.heatMap[y][x] = blackPixels
regionCount += 1
This brought run time to under a minute and thus solved my problem, however, thank you for your contributions I have learned a lot from reading them!
Try to look into the map() function. It uses C to streamline iterations.
You can speed up your for loops like this:
pixels = list(map(lambda i: x[i*width:(i+1)*width], range(height)))

Python Matplotlib Graph Showing Incorrect Range in X axis

Since the code is a lot of lines, I shall first show what the issue is:
I defined a simple loop and am getting the appropriate results.
Here when I attempt to plot it using matplotlib, the range shown on the x-axis is different from the range I inputted. I want 0 to 100 with a step size of 5 but I am getting 0 to 17.5 with a step size of 2.5.
Is there any issue with just the way I have coded this? If not, here is the rest of the code, thank you!:
import random
import math
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.pyplot import figure
from matplotlib.colors import ListedColormap
import sys
import decimal
sys.setrecursionlimit(4000)
n = 10 # number of rows and columns in the grid
p = 0.9 # probability that each square is open
def gridMakerN(n):
grid = (np.random.rand(n,n) < p).astype(int)
mycolormap = ListedColormap(["grey","blue"])
#plt.imshow(grid, cmap=mycolormap)
return grid
# define an exception that we will raise if percolation is detected
class percolationException(Exception): pass
# query() looks for a path from (row, col) to the bottom of the grid
# recursive function: it calls itself to determine if a path exists
def query(row, col, grid, visited):
#print("Visiting square ", row, ",", col) <- This was previously part of the code
# mark row, col as visited
visited[row,col] = 1
# is row equal to the bottom row? If so, output "path found"
(numRows,numCols) = np.shape(grid)
if row == numRows - 1:
#print("PERCOLATION FOUND!!!") <- This was previously part of the code
raise percolationException
else:
# if square below is open and unvisited, then is there a path from that square?
if grid[row+1,col] == 1 and visited[row+1,col] == 0:
query(row+1, col, grid, visited)
# if square at left is open and unvisited, then is there a path from that square?
if col > 0 and grid[row, col-1] == 1 and visited[row, col-1] == 0:
query(row, col-1, grid, visited)
# if square at right is open and unvisited, then is there a path from that square?
if col+1 < numCols and grid[row, col+1] == 1 and visited[row, col+1] == 0:
query(row, col+1, grid, visited)
# if square above is open and unvisited, then is there a path from that square?
if row > 0 and grid[row-1, col] == 1 and visited[row-1, col] == 0:
query(row-1, col, grid, visited)
# driver function to manage the whole percolation detection process
def findPercolation(grid):
# create an empty visited matrix
(numRows, numCols) = np.shape(grid)
visited = np.zeros( (numRows, numCols) )
# look for a percolation path, starting at each open square in the top row
try:
for c in range(numCols): # consider all squares in the top row
if grid[0,c] == 1:
query(0, c, grid, visited)
except percolationException:
#print("percolationException occurred") <- This was previously part of the code
return 1 # <- Here I put 1 instead of "True"
else:
#print("percolation not found") <- This was previously part of the code
return 0 # <- Here I put 0 instead of "False"
def findPercolationFixedP(n):
return findPercolation(gridMakerN(n))
def percAvgFixedP(n):
iterations = 100
results = [] #Making an Empty List
for _ in range(iterations): #Repeat the Same Step x times
results.append(findPercolationFixedP(n))
#print(results)
#print(sum(results))
return sum(results)/iterations
def avgFixedPGraph():
results = []
for x in range(10,100,5):
results.append(percAvgFixedP(x))
plt.plot(results,"c")
plt.grid()
plt.show()
avgFixedPGraph()
When plot() is only given one array:
plt.plot(results, "c")
that array is treated as the y values, and the x values default to a numeric range. In this case results has 18 values, so it plots x from 0 to 17.
To assign custom x values, pass them in explicitly, e.g.:
x = range(10, 100, 5)
results = [percAvgFixedP(value) for value in x]
plt.plot(x, results, "c")

How to code up an image stitching software for these 'simple' images?

TLDR:
Need help trying to calculate overlap region between 2 graphs.
So I'm trying to stitch these 2 images:
Since I know that the images I will be stitching definitely come from the same image, I feel that I should be able to code this up myself. Using libraries like OpenCV feels a little like overkill for me for this task.
My current idea is that I can simplify this task by doing the following steps for each image:
Load image using PIL
Convert image to black and white (PIL image mode ā€œLā€)
[Optional: crop images to overlapping region by inspection by eye]
Create vector row_sum, which is a sum of each row
[Optional: log row_sum, to reduce the size of values we're working with]
Plot row_sum.
This would reduce the (potentially) (3*2)-dimensional problem, with 3 RGB channels for each pixel on the 2D image to a (1*2)-D problem with the black and white pixel for the 2D image instead. Then, summing across the rows reduces this to a 1D problem.
I used the following code to implement the above:
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
class Stitcher():
def combine_2(self, img1, img2):
# thr1, thr2 = self.get_cropped_bw(img1, 115, img2, 80)
thr1, thr2 = self.get_cropped_bw(img1, 0, img2, 0)
row_sum1 = np.log(thr1.sum(1))
row_sum2 = np.log(thr2.sum(1))
self.plot_4x4(thr1, thr2, row_sum1, row_sum2)
def get_cropped_bw(self, img1, img1_keep_from, img2, img2_keep_till):
im1 = Image.open(img1).convert("L")
im2 = Image.open(img2).convert("L")
data1 = (np.array(im1)[img1_keep_from:]
if img1_keep_from != 0 else np.array(im1))
data2 = (np.array(im2)[:img2_keep_till]
if img2_keep_till != 0 else np.array(im2))
return data1, data2
def plot_4x4(self, thr1, thr2, row_sum1, row_sum2):
fig, ax = plt.subplots(2, 2, sharey="row", constrained_layout=True)
ax[0, 0].imshow(thr1, cmap="Greys")
ax[0, 1].imshow(thr2, cmap="Greys")
ax[1, 0].plot(row_sum1, "k.")
ax[1, 1].plot(row_sum2, "r.")
ax[1, 0].set(
xlabel="Index Value",
ylabel="Row Sum",
)
plt.show()
imgs = (r"combine\imgs\test_image_part_1.jpg",
r"combine\imgs\test_image_part_2.jpg")
s = Stitcher()
s.combine_2(*imgs)
This gave me this graph:
(I've added in those yellow boxes, to indicate the overlap regions.)
This is the bit I'm stuck at. I want to find exactly:
the index value of the left-side of the yellow box for the 1st image and
the index value of the right-side of the yellow box for the 2nd image.
I define the overlap region as the longest range for which the end of the 1st graph 'matches' the start of the 2nd graph. For the method to find the overlap region, what should I do if the row sum values aren't exactly the same (what if one is the other scaled by some factor)?
I feel like this could be a problem that could use dot products to find the similarity between the 2 graphs? But I can't think of how to implement this.
I had a lot more fun with this than I expected. I wrote this using opencv, but that's just to load and show the image. Everything else is done with numpy so swapping this to PIL shouldn't be too difficult.
I'm using a brute-force matcher. I also wrote a random-start hillclimber that runs in much less time, but I can't guarantee it'll find the correct answer since the gradient space isn't smooth. I won't include it in my code since it's long and janky, but if you really need the time efficiency I can add it back in later.
I added a random crop and some salt and pepper noise to the images to test for robustness.
The brute-force matcher operates on the idea that we don't know which section of the two images overlap, so we need to convolve the smaller image over the larger image from left to right, top to bottom. This means our search space is:
horizontal = small_width + big_width
vertical = small_height + big_height
area = horizontal * vertical
This will grow very quickly with image size. I motivate the algorithm by giving it points for having a larger overlap, but it loses more points for having differences in color for the overlapped area.
Here are some pictures from an execution of this program
import cv2
import numpy as np
import random
# randomly snips edges
def randCrop(image, maxMargin):
c = [random.randint(0,maxMargin) for a in range(4)];
return image[c[0]:-c[1], c[2]:-c[3]];
# adds noise to image
def saltPepper(image, minNoise, maxNoise):
h,w = image.shape;
randNum = random.randint(minNoise, maxNoise);
for a in range(randNum):
x = random.randint(0, w-1);
y = random.randint(0, h-1);
image[y,x] = random.randint(0, 255);
return image;
# evaluate layout
def getScore(one, two):
# do raw subtraction
left = one - two;
right = two - one;
sub = np.minimum(left, right);
return np.count_nonzero(sub);
# return 2d random position within range
def randPos(img, big_shape):
th,tw = big_shape;
h,w = img.shape;
x = random.randint(0, tw - w);
y = random.randint(0, th - h);
return [x,y];
# overlays small image onto big image
def overlay(small, big, pos):
# unpack
h,w = small.shape;
x,y = pos;
# copy and place
copy = big.copy();
copy[y:y+h, x:x+w] = small;
return copy;
# calculates overlap region
def overlap(one, two, pos_one, pos_two):
# unpack
h1,w1 = one.shape;
h2,w2 = two.shape;
x1,y1 = pos_one;
x2,y2 = pos_two;
# set edges
l1 = x1;
l2 = x2;
r1 = x1 + w1;
r2 = x2 + w2;
t1 = y1;
t2 = y2;
b1 = y1 + h1;
b2 = y2 + h2;
# go
left = max(l1, l2);
right = min(r1, r2);
top = max(t1, t2);
bottom = min(b1, b2);
return [left, right, top, bottom];
# wrapper for overlay + getScore
def fullScore(one, two, pos_one, pos_two, big_empty):
# check positions
x,y = pos_two;
h,w = two.shape;
th,tw = big_empty.shape;
if y+h > th or x+w > tw or x < 0 or y < 0:
return -99999999;
# overlay
temp_one = overlay(one, big_empty, pos_one);
temp_two = overlay(two, big_empty, pos_two);
# get overlap
l,r,t,b = overlap(one, two, pos_one, pos_two);
temp_one = temp_one[t:b, l:r];
temp_two = temp_two[t:b, l:r];
# score
diff = getScore(temp_one, temp_two);
score = (r-l) * (b-t);
score -= diff*2;
return score;
# do brute force
def bruteForce(one, two):
# calculate search space
# unpack size
h,w = one.shape;
one_size = h*w;
h,w = two.shape;
two_size = h*w;
# small and big
if one_size < two_size:
small = one;
big = two;
else:
small = two;
big = one;
# unpack size
sh, sw = small.shape;
bh, bw = big.shape;
total_width = bw + sw * 2;
total_height = bh + sh * 2;
# set up empty images
empty = np.zeros((total_height, total_width), np.uint8);
# set global best
best_score = -999999;
best_pos = None;
# start scrolling
ybound = total_height - sh;
xbound = total_width - sw;
for y in range(ybound):
print("y: " + str(y) + " || " + str(empty.shape));
for x in range(xbound):
# get score
score = fullScore(big, small, [sw,sh], [x,y], empty);
# show
# prog = overlay(big, empty, [sw,sh]);
# prog = overlay(small, prog, [x,y]);
# cv2.imshow("prog", prog);
# cv2.waitKey(1);
# compare
if score > best_score:
best_score = score;
best_pos = [x,y];
print("best_score: " + str(best_score));
return best_pos, [sw,sh], small, big, empty;
# do a step of hill climber
def hillStep(one, two, best_pos, big_empty, step):
# make a step
new_pos = best_pos[1][:];
new_pos[0] += step[0];
new_pos[1] += step[1];
# get score
return fullScore(one, two, best_pos[0], new_pos, big_empty), new_pos;
# hunt around for good position
# let's do a random-start hillclimber
def randHill(one, two, shape):
# set up empty images
big_empty = np.zeros(shape, np.uint8);
# set global best
g_best_score = -999999;
g_best_pos = None;
# lets do 200 iterations
iters = 200;
for a in range(iters):
# progress check
print(str(a) + " of " + str(iters));
# start with random position
h,w = two.shape[:2];
pos_one = [w,h];
pos_two = randPos(two, shape);
# get score
best_score = fullScore(one, two, pos_one, pos_two, big_empty);
best_pos = [pos_one, pos_two];
# hill climb (only on second image)
while True:
# end condition: no step improves score
end_flag = True;
# 8-way
for y in range(-1, 1+1):
for x in range(-1, 1+1):
if x != 0 or y != 0:
# get score and update
score, new_pos = hillStep(one, two, best_pos, big_empty, [x,y]);
if score > best_score:
best_score = score;
best_pos[1] = new_pos[:];
end_flag = False;
# end
if end_flag:
break;
else:
# show
# prog = overlay(one, big_empty, best_pos[0]);
# prog = overlay(two, prog, best_pos[1]);
# cv2.imshow("prog", prog);
# cv2.waitKey(1);
pass;
# check for new global best
if best_score > g_best_score:
g_best_score = best_score;
g_best_pos = best_pos[:];
print("top score: " + str(g_best_score));
return g_best_score, g_best_pos;
# load both images
top = cv2.imread("top.jpg");
bottom = cv2.imread("bottom.jpg");
top = cv2.cvtColor(top, cv2.COLOR_BGR2GRAY);
bottom = cv2.cvtColor(bottom, cv2.COLOR_BGR2GRAY);
# randomly crop
top = randCrop(top, 20);
bottom = randCrop(bottom, 20);
# randomly add noise
saltPepper(top, 200, 1000);
saltPepper(bottom, 200, 1000);
# set up max image (assume no overlap whatsoever)
tw = 0;
th = 0;
h, w = top.shape;
tw += w;
th += h;
h, w = bottom.shape;
tw += w*2;
th += h*2;
# do random-start hill climb
_, best_pos = randHill(top, bottom, (th, tw));
# show
empty = np.zeros((th, tw), np.uint8);
pos1, pos2 = best_pos;
image = overlay(top, empty, pos1);
image = overlay(bottom, image, pos2);
# do brute force
# small_pos, big_pos, small, big, empty = bruteForce(top, bottom);
# image = overlay(big, empty, big_pos);
# image = overlay(small, image, small_pos);
# recolor overlap
h,w = empty.shape;
color = np.zeros((h,w,3), np.uint8);
l,r,t,b = overlap(top, bottom, pos1, pos2);
color[:,:,0] = image;
color[:,:,1] = image;
color[:,:,2] = image;
color[t:b, l:r, 0] += 100;
# show images
cv2.imshow("top", top);
cv2.imshow("bottom", bottom);
cv2.imshow("overlayed", image);
cv2.imshow("Color", color);
cv2.waitKey(0);
Edit: I added in the random-start hillclimber

Making a collage in PIL

I. Am. Stuck.
I have been working on this for over a week now, and I cannot seem to get my code to run correctly. I am fairly new to PIL and Python as a whole. I am trying to make a 2x3 collage of some pictures. I have my code listed below. I am trying to get my photos to fit without any access black space in the newly created collage, however when I run my code I can only get 2 pictures to be placed into the collage, instead of the 6 I want. Any suggestions would be helpful.
*CODE EDITED
from PIL import Image
im= Image.open('Tulips.jpg')
out=im.convert("RGB", (
0.412453, 0.357580, 0.180423, 0,
0.212671, 0.715160, 0.072169, 0,
0.019334, 0.119193, 0.950227, 0 ))
out.save("Image2" + ".jpg")
out2=im.convert("RGB", (
0.9756324, 0.154789, 0.180423, 0,
0.212671, 0.715160, 0.254783, 0,
0.123456, 0.119193, 0.950227, 0 ))
out2.save("Image3" + ".jpg")
out3= im.convert("1")
out3.save("Image4"+".jpg")
out4=im.convert("RGB", (
0.986542, 0.154789, 0.756231, 0,
0.212671, 0.715160, 0.254783, 0,
0.123456, 0.119193, 0.112348, 0 ))
out4.save("Image5" + ".jpg")
out5=Image.blend(im, out4, 0.5)
out5.save("Image6" + ".jpg")
listofimages=['Tulips.jpg', 'Image2.jpg', 'Image3.jpg', 'Image4.jpg', 'Image5.jpg', 'Image6.jpg']
def create_collage(width, height, listofimages):
Picturewidth=width//3
Pictureheight=height//2
size=Picturewidth, Pictureheight
new_im=Image.new('RGB', (450, 300))
for p in listofimages:
Image.open(p)
for col in range(0,width):
for row in range(0, height):
image=Image.eval(p, lambda x: x+(col+row)/30)
new_im.paste(p, (col,row))
new_im.save("Collage"+".jpg")
create_collage(450,300,listofimages)
Here's some working code.
When you call Image.open(p), that returns an Image object, so you need to store than in a variable: im = Image.open(p).
I'm not sure what image=Image.eval(p, lambda x: x+(col+row)/30) is meant to do so I removed it.
size is the size of the thumbnails, but you're not using that variable. After opening the image, it should be resized to size.
I renamed Picturewidth and Pictureheight to thumbnail_width and thumbnail_height to make it clear what they are and follow Python naming conventions.
I also moved the number of cols and rows to variables so they can be reused without magic numbers.
The first loop opens each image into an im, thumbnails it and puts it in a list of ims.
Before the next loops we initialise i,x, andy` variables to keep track of which image we're looking at, and the x and y coordinates to paste the thumbnails into the larger canvas. They'll be updated in the next loops.
The first loop is for columns (cols), not pixels (width). (Also range(0, thing) does the same as range(thing).)
Similarly the second loop is for rows instead of pixels. Inside this loop we paste the current image at ims[i] into the big new_im at x, y. These are pixel positions, not row/cols positions.
At the end of the inner loop, increment the i counter, and add thumbnail_height to y.
Similarly, at the end of the outer loop, and add thumnnail_width to x and reset y to zero.
You only need to save new_im once, after these loops have finished.
There's no need for concatenating "Image2" + ".jpg" etc., just do "Image2.jpg".
This results in something like this:
This code could be improved. For example, if you don't need them for anything else, there's no need to save the intermediate ImageX.jpg files, and rather than putting those filenames in listofimages, put the images directly there: listofimages = [im, out1, out2, etc...], and then replace for p in listofimages: with for im in listofimages: and remove im = Image.open(p).
You could also calculate some padding for the images so the blackspace is even.
from PIL import Image
im= Image.open('Tulips.jpg')
out=im.convert("RGB", (
0.412453, 0.357580, 0.180423, 0,
0.212671, 0.715160, 0.072169, 0,
0.019334, 0.119193, 0.950227, 0 ))
out.save("Image2.jpg")
out2=im.convert("RGB", (
0.9756324, 0.154789, 0.180423, 0,
0.212671, 0.715160, 0.254783, 0,
0.123456, 0.119193, 0.950227, 0 ))
out2.save("Image3.jpg")
out3= im.convert("1")
out3.save("Image4.jpg")
out4=im.convert("RGB", (
0.986542, 0.154789, 0.756231, 0,
0.212671, 0.715160, 0.254783, 0,
0.123456, 0.119193, 0.112348, 0 ))
out4.save("Image5.jpg")
out5=Image.blend(im, out4, 0.5)
out5.save("Image6.jpg")
listofimages=['Tulips.jpg', 'Image2.jpg', 'Image3.jpg', 'Image4.jpg', 'Image5.jpg', 'Image6.jpg']
def create_collage(width, height, listofimages):
cols = 3
rows = 2
thumbnail_width = width//cols
thumbnail_height = height//rows
size = thumbnail_width, thumbnail_height
new_im = Image.new('RGB', (width, height))
ims = []
for p in listofimages:
im = Image.open(p)
im.thumbnail(size)
ims.append(im)
i = 0
x = 0
y = 0
for col in range(cols):
for row in range(rows):
print(i, x, y)
new_im.paste(ims[i], (x, y))
i += 1
y += thumbnail_height
x += thumbnail_width
y = 0
new_im.save("Collage.jpg")
create_collage(450, 300, listofimages)
I made a solution inspired by #Hugo's answer which only requires the input list of images. The function automatically creates a grid based on the number of images input.
def find_multiples(number : int):
multiples = set()
for i in range(number - 1, 1, -1):
mod = number % i
if mod == 0:
tup = (i, int(number / i))
if tup not in multiples and (tup[1], tup[0]) not in multiples:
multiples.add(tup)
if len(multiples) == 0:
mod == number % 2
div = number // 2
multiples.add((2, div + mod))
return list(multiples)
def get_smallest_multiples(number : int, smallest_first = True) -> Tuple[int, int]:
multiples = find_multiples(number)
smallest_sum = number
index = 0
for i, m in enumerate(multiples):
sum = m[0] + m[1]
if sum < smallest_sum:
smallest_sum = sum
index = i
result = list(multiples[i])
if smallest_first:
result.sort()
return result[0], result[1]
def create_collage(listofimages : List[str], n_cols : int = 0, n_rows: int = 0,
thumbnail_scale : float = 1.0, thumbnail_width : int = 0, thumbnail_height : int = 0):
n_cols = n_cols if n_cols >= 0 else abs(n_cols)
n_rows = n_rows if n_rows >= 0 else abs(n_rows)
if n_cols == 0 and n_rows != 0:
n_cols = len(listofimages) // n_rows
if n_rows == 0 and n_cols != 0:
n_rows = len(listofimages) // n_cols
if n_rows == 0 and n_cols == 0:
n_cols, n_rows = get_smallest_multiples(len(listofimages))
thumbnail_width = 0 if thumbnail_width == 0 or n_cols == 0 else round(thumbnail_width / n_cols)
thumbnail_height = 0 if thumbnail_height == 0 or n_rows == 0 else round(thumbnail_height/n_rows)
all_thumbnails : List[Image.Image] = []
for p in listofimages:
thumbnail = Image.open(p)
if thumbnail_width * thumbnail_scale < thumbnail.width:
thumbnail_width = round(thumbnail.width * thumbnail_scale)
if thumbnail_height * thumbnail_scale < thumbnail.height:
thumbnail_height = round(thumbnail.height * thumbnail_scale)
thumbnail.thumbnail((thumbnail_width, thumbnail_height))
all_thumbnails.append(thumbnail)
new_im = Image.new('RGB', (thumbnail_width * n_cols, thumbnail_height * n_rows), 'white')
i, x, y = 0, 0, 0
for col in range(n_cols):
for row in range(n_rows):
if i > len(all_thumbnails) - 1:
continue
print(i, x, y)
new_im.paste(all_thumbnails[i], (x, y))
i += 1
y += thumbnail_height
x += thumbnail_width
y = 0
extension = os.path.splitext(listofimages[0])[1]
if extension == "":
extension = ".jpg"
destination_file = os.path.join(os.path.dirname(listofimages[0]), f"Collage{extension}")
new_im.save(destination_file)
Example usage:
listofimages=['Tulips.jpg', 'Image2.jpg', 'Image3.jpg', 'Image4.jpg', 'Image5.jpg', 'Image6.jpg']
create_collage(listofimages)
In this case, because the input images are 6, the function returns a 3x2 (3 rows, 2 columns) collage of the images.
To do so, the function finds the two smallest integer multiples of the length of the input list of graphs (e.g. for 12, it returns 3 and 4 rather than 2 and 6) and creates a grid, where the first number is always the smallest of the multiples and it is taken to be the number of columns (i.e. by default the grid gets fewer columns than rows; for 12 images, you get a 4x3 matrix: 4 rows, 3 columns). This it can be customized via the smallest_first argument (only exposed in get_smallest_multiples()).
Optional arguments also allow to force a number of rows/columns.
The final image size is the sum of the sizes of the single images, but an optional thumbnail_scale argument allows to specify a percentage of scaling for all the thumbnails (defaults to 1.0, i.e. 100%, no scaling).
This function works well when the size of the images are all roughly the same. I have not covered more complex scenarios.

Categories

Resources