'Rounding errors using PIL.Image.Transform
I have high resolution scans of old navigational maps, that I have turned into map tiles with gdal2tiles. Now I want to write code for a live video stream that records a panning movement between two random points on the maps.
Initially, I had the code working by generating an image for each videoframe, that was assembled from a grid of map tiles. These are small jpg files of 256px x 256px. The next videoframe would then show the same map, but translated a certain amount in the x and y direction.
But opening each jpg with Image.open proved to be a bottleneck. So I tried to make the process faster by reusing the opened tiles until they disappeared off the frame, and only opening fresh tiles as needed. I basically first translate the whole image like so: newframe = oldframe.transform(oldframe.size, Image.AFFINE, data). Then I add a row of fresh tiles on top (or bottom, depending on direction of the panning motion), and on the side.
The problem
I seem to run into some rounding errors (I think), because the opened tiles do not always line up well with the existing, translated content. I have thin lines appearing. But I can't figure out where the rounding errors may come from. Nor how to avoid them.
Here is some code which shows the lines appearing, using colored empty images rather than the map tiles:
import time
import math
from random import randint, uniform
from PIL import Image
import numpy as np
import cv2
FRAMESIZE = (1920, 1080)
TILESIZE = 256
class MyMap:
'''A map class, to hold all its properties'''
def __init__(self) -> None:
self.maxzoom = 8
self.dimensions = (40000, 30000)
self.route, self.zoom = self.trajectory()
pass
def trajectory(self):
'''Calculate random trajectories from map dimensions'''
factor = uniform(1, 8)
extentwidth = FRAMESIZE[0] * factor
extentheight = FRAMESIZE[1] * factor
mapwidth, mapheight = (self.dimensions)
zdiff = math.log2(factor)
newzoom = math.ceil(self.maxzoom - zdiff)
a = (randint(0 + math.ceil(extentwidth / 2), mapwidth - math.floor(extentwidth / 2)),
randint(0 + math.ceil(extentheight / 2), mapheight - math.floor(extentheight / 2))) # Starting point
b = (randint(0 + math.ceil(extentwidth / 2), mapwidth - math.floor(extentwidth / 2)),
randint(0 + math.ceil(extentheight / 2), mapheight - math.floor(extentheight / 2))) # Ending point
x_distance = b[0] - a[0]
y_distance = b[1] - a[1]
distance = math.sqrt((x_distance**2)+(y_distance**2)) # Pythagoras
speed = 3 * factor # pixels per 25th of a second (video framerate)
steps = math.floor(distance / speed)
trajectory = []
for step in range(steps):
x = a[0] + step * x_distance / steps
y = a[1] + step * y_distance / steps
trajectory.append((x, y))
return trajectory, newzoom
def CreateFrame(self, point, oldframe = None):
x = round(self.route[point][0] / (2 ** (self.maxzoom - self.zoom)))
y = round(self.route[point][1] / (2 ** (self.maxzoom - self.zoom)))
if oldframe:
xtrns = x - round(self.route[point - 1][0] / (2 ** (self.maxzoom - self.zoom)))
ytrns = y - round(self.route[point - 1][1] / (2 ** (self.maxzoom - self.zoom)))
# print(x, self.route[point - 1][0] / (2 ** (self.maxzoom - self.zoom)))
west = int(x - FRAMESIZE[0] / 2)
east = int(x + FRAMESIZE[0] / 2)
north = int(y - FRAMESIZE[1] / 2)
south = int(y + FRAMESIZE[1] / 2)
xoffset = west % TILESIZE
yoffset = north % TILESIZE
xrange = range(math.floor(west / TILESIZE), math.ceil(east / TILESIZE))
yrange = range(math.floor(north / TILESIZE), math.ceil(south / TILESIZE))
if oldframe:
data = (
1, #a
0, #b
xtrns, #c +left/-right
0, #d
1, #e
ytrns #f +up/-down
)
newframe = oldframe.transform(oldframe.size, Image.AFFINE, data)
if ytrns < 0:
singlerow_ytile = yrange.start
elif ytrns > 0:
singlerow_ytile = yrange.stop - 1
if ytrns != 0:
for xtile in xrange:
try:
tile = Image.new('RGB', (TILESIZE, TILESIZE), (130,100,10))
newframe.paste(
tile,
(
(xtile - west // TILESIZE) * TILESIZE - xoffset,
(singlerow_ytile - north // TILESIZE) * TILESIZE - yoffset
)
)
except:
tile = None
if xtrns < 0:
singlerow_xtile = xrange.start
elif xtrns > 0:
singlerow_xtile = xrange.stop - 1
if xtrns != 0:
for ytile in yrange:
try:
tile = Image.new('RGB', (TILESIZE, TILESIZE), (200, 200, 200))
newframe.paste(
tile,
(
(singlerow_xtile - west // TILESIZE) * TILESIZE - xoffset,
(ytile - north // TILESIZE) * TILESIZE - yoffset
)
)
except:
tile = None
else:
newframe = Image.new('RGB',FRAMESIZE)
for xtile in xrange:
for ytile in yrange:
try:
tile = Image.new('RGB', (TILESIZE, TILESIZE), (100, 200, 20))
newframe.paste(
tile,
(
(xtile - west // TILESIZE) * TILESIZE - xoffset,
(ytile - north // TILESIZE) * TILESIZE - yoffset
)
)
except:
tile = None
return newframe
def preparedisplay(img, ago):
quit = False
open_cv_image = np.array(img)
# Convert RGB to BGR
open_cv_image = open_cv_image[:, :, ::-1].copy()
# Write some Text
font = cv2.FONT_HERSHEY_TRIPLEX
bottomLeftCornerOfText = (10,1050)
fontScale = 1
fontColor = (2,2,2)
thickness = 2
lineType = 2
cv2.putText(open_cv_image,f"Press q to exit. Frame took {(time.process_time() - ago) * 100}",
bottomLeftCornerOfText,
font,
fontScale,
fontColor,
thickness,
lineType)
return open_cv_image
#===============================================================================
quit = False
while not quit:
currentmap = MyMap()
previousframe = None
step = 0
while step < len(currentmap.route):
start = time.process_time()
currentframe = currentmap.CreateFrame(step, previousframe)
previousframe = currentframe
cv2.imshow('maps', preparedisplay(currentframe, start))
timeleft = (time.process_time() - start)
# print(timeleft)
# if cv2.waitKey(40 - int(timeleft * 100)) == ord('q'):
if cv2.waitKey(1) == ord('q'):
# press q to terminate the loop
cv2.destroyAllWindows()
quit = True
break
step += 1
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
