Skip to content

Commit b872e95

Browse files
committed
Official Release
1 parent 1c1fc53 commit b872e95

File tree

3 files changed

+39
-46
lines changed

3 files changed

+39
-46
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Version 0.3.1.0 - (2020-12-13)
1+
# Version 1.0.0 - <i> Finally! </i> - (2020-12-13)
22
## Known Bugs
33
- The GUI can sometimes become unresponsive during morphing calculations (but eventually returns to normal)
44
- QtCore.QCoreApplication.processEvents() is a potential workaround but currently produces buggy results
@@ -12,11 +12,15 @@
1212
## Removed
1313
- As of v0.3.0.1's hot pixel fix, PIM's image smoothing feature is deprecated and will now be removed
1414
- Removed Morphing.py's <b>smoothBlend()</b> method as well as the "smoothMode" parameter in <b>getImageAtAlpha()</b>
15+
- Removed Morphing.py's sub-module import for SciPy's <i>median_filter</i>
1516
- Removed all code related to smoothing in MorphingApp.py (a reduction of 77 SLOC)
1617
- Removed <b>self.smoothingBox</b> from MorphingGUI.ui and MorphingGUI.py
1718
- Comment: <i>It's likely that this checkbox will be replaced with an automatic correspondence button at some point.</i>
1819

1920
## Changes
21+
- Improved morphing performance (a huge <b>90%</b> speedup) by modifying Morphing.py's implementation of <b>getPoints()</b> as well as tweaking
22+
<b>interpolatePoints()</b> to utilize RectBivariateSpline's .ev() method instead of manually interpolating the image data
23+
- <b>Huge thanks to GitHub user [zhifeichen097](https://github.com/zhifeichen097) for his source code which can be found [here](https://github.com/zhifeichen097/Image-Morphing) - excellent work!</b>
2024
- Optimized the conditional logic found in MorphingApp.py's <b>displayTriangles()</b>
2125
- Optimized a query in Morphing.py's <b>getPoints()</b>
2226
- "np.where(np.array(mask) == True)" → "np.where(np.array(mask))"

Morphing/Morphing.py

Lines changed: 32 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import os
77
import copy
8-
from scipy.ndimage import median_filter
8+
from PIL import Image, ImageDraw
99
from scipy.spatial import Delaunay # pip install scipy
1010
from scipy.interpolate import RectBivariateSpline # pip install scipy
1111
from matplotlib.path import Path # pip install matplotlib
@@ -17,20 +17,12 @@
1717
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
1818

1919
def loadTriangles(leftPointFilePath: str, rightPointFilePath: str) -> tuple:
20-
leftList = []
21-
rightList = []
2220
leftTriList = []
2321
rightTriList = []
2422

25-
with open(leftPointFilePath, "r") as leftFile:
26-
for j in leftFile:
27-
leftList.append(j.split())
28-
with open(rightPointFilePath, "r") as rightFile:
29-
for k in rightFile:
30-
rightList.append(k.split())
23+
leftArray = np.loadtxt(leftPointFilePath).astype(np.float64)
24+
rightArray = np.loadtxt(rightPointFilePath).astype(np.float64)
3125

32-
leftArray = np.array(leftList, np.float64)
33-
rightArray = np.array(rightList, np.float64)
3426
delaunayTri = Delaunay(leftArray)
3527

3628
leftNP = leftArray[delaunayTri.simplices]
@@ -52,24 +44,14 @@ def __init__(self, vertices):
5244
if vertices.dtype != np.float64:
5345
raise ValueError("Input argument is not of type float64.")
5446
self.vertices = vertices
55-
self.minX = int(self.vertices[:, 0].min())
56-
self.maxX = int(self.vertices[:, 0].max())
57-
self.minY = int(self.vertices[:, 1].min())
58-
self.maxY = int(self.vertices[:, 1].max())
5947

48+
# Credit to https://github.com/zhifeichen097/Image-Morphing for the following approach (which is a bit more efficient than my own)!
6049
def getPoints(self):
61-
xList = range(self.minX, self.maxX + 1)
62-
yList = range(self.minY, self.maxY + 1)
63-
a = [xList, yList]
64-
emptyList = list(itertools.product(*a))
65-
66-
points = np.array(emptyList, np.float64)
67-
p = Path(self.vertices)
68-
grid = p.contains_points(points)
69-
mask = grid.reshape(self.maxX - self.minX + 1, self.maxY - self.minY + 1)
70-
71-
trueArray = np.where(np.array(mask))
72-
coordArray = np.vstack((trueArray[0] + self.minX, trueArray[1] + self.minY, np.ones(trueArray[0].shape[0])))
50+
width = round(max(self.vertices[:, 0]) + 2)
51+
height = round(max(self.vertices[:, 1]) + 2)
52+
mask = Image.new('P', (width, height), 0)
53+
ImageDraw.Draw(mask).polygon(tuple(map(tuple, self.vertices)), outline=255, fill=255)
54+
coordArray = np.transpose(np.nonzero(mask))
7355

7456
return coordArray
7557

@@ -94,20 +76,16 @@ def __init__(self, leftImage, leftTriangles, rightImage, rightTriangles):
9476
if isinstance(k, Triangle) == 0:
9577
raise TypeError('Element of input rightTriangles is not of Class Triangle')
9678
self.leftImage = copy.deepcopy(leftImage)
79+
self.newLeftImage = copy.deepcopy(leftImage)
9780
self.leftTriangles = leftTriangles # Not of type np.uint8
9881
self.rightImage = copy.deepcopy(rightImage)
82+
self.newRightImage = copy.deepcopy(rightImage)
9983
self.rightTriangles = rightTriangles # Not of type np.uint8
100-
self.leftInterpolation = RectBivariateSpline(np.arange(self.leftImage.shape[0]), np.arange(self.leftImage.shape[1]), self.leftImage, kx=1, ky=1)
101-
self.rightInterpolation = RectBivariateSpline(np.arange(self.rightImage.shape[0]), np.arange(self.rightImage.shape[1]), self.rightImage, kx=1, ky=1)
102-
10384

10485
def getImageAtAlpha(self, alpha):
10586
for leftTriangle, rightTriangle in zip(self.leftTriangles, self.rightTriangles):
10687
self.interpolatePoints(leftTriangle, rightTriangle, alpha)
107-
108-
blendARR = ((1 - alpha) * self.leftImage + alpha * self.rightImage)
109-
blendARR = blendARR.astype(np.uint8)
110-
return blendARR
88+
return ((1 - alpha) * self.newLeftImage + alpha * self.newRightImage).astype(np.uint8)
11189

11290
def interpolatePoints(self, leftTriangle, rightTriangle, alpha):
11391
targetTriangle = Triangle(leftTriangle.vertices + (rightTriangle.vertices - leftTriangle.vertices) * alpha)
@@ -130,12 +108,23 @@ def interpolatePoints(self, leftTriangle, rightTriangle, alpha):
130108
rightH = np.array([[righth[0][0], righth[1][0], righth[2][0]], [righth[3][0], righth[4][0], righth[5][0]], [0, 0, 1]])
131109
leftinvH = np.linalg.inv(leftH)
132110
rightinvH = np.linalg.inv(rightH)
133-
targetPoints = targetTriangle.getPoints() # TODO: ~ 17-18% of runtime
134-
135-
leftSourcePoints = np.transpose(np.matmul(leftinvH, targetPoints))
136-
rightSourcePoints = np.transpose(np.matmul(rightinvH, targetPoints))
137-
targetPoints = np.transpose(targetPoints)
138-
139-
for x, y, z in zip(targetPoints, leftSourcePoints, rightSourcePoints): # TODO: ~ 53% of runtime
140-
self.leftImage[int(x[1])][int(x[0])] = self.leftInterpolation(y[1], y[0])
141-
self.rightImage[int(x[1])][int(x[0])] = self.rightInterpolation(z[1], z[0])
111+
targetPoints = targetTriangle.getPoints()
112+
113+
# Credit to https://github.com/zhifeichen097/Image-Morphing for the following code block that I've adapted. Exceptional work on discovering
114+
# RectBivariateSpline's .ev() method! I noticed the method but didn't think much of it at the time due to the website's poor documentation..
115+
xp, yp = np.transpose(targetPoints)
116+
leftXValues = leftinvH[1, 1] * xp + leftinvH[1, 0] * yp + leftinvH[1, 2]
117+
leftYValues = leftinvH[0, 1] * xp + leftinvH[0, 0] * yp + leftinvH[0, 2]
118+
leftXParam = np.arange(np.amin(leftTriangle.vertices[:, 1]), np.amax(leftTriangle.vertices[:, 1]), 1)
119+
leftYParam = np.arange(np.amin(leftTriangle.vertices[:, 0]), np.amax(leftTriangle.vertices[:, 0]), 1)
120+
leftImageValues = self.leftImage[int(leftXParam[0]):int(leftXParam[-1] + 1), int(leftYParam[0]):int(leftYParam[-1] + 1)]
121+
122+
rightXValues = rightinvH[1, 1] * xp + rightinvH[1, 0] * yp + rightinvH[1, 2]
123+
rightYValues = rightinvH[0, 1] * xp + rightinvH[0, 0] * yp + rightinvH[0, 2]
124+
rightXParam = np.arange(np.amin(rightTriangle.vertices[:, 1]), np.amax(rightTriangle.vertices[:, 1]), 1)
125+
rightYParam = np.arange(np.amin(rightTriangle.vertices[:, 0]), np.amax(rightTriangle.vertices[:, 0]), 1)
126+
rightImageValues = self.rightImage[int(rightXParam[0]):int(rightXParam[-1] + 1), int(rightYParam[0]):int(rightYParam[-1] + 1)]
127+
128+
# This is where performance skyrockets. Again, credit goes to zhifeichen097 for discovering the .ev() method!
129+
self.newLeftImage[xp, yp] = RectBivariateSpline(leftXParam, leftYParam, leftImageValues, kx=1, ky=1).ev(leftXValues, leftYValues)
130+
self.newRightImage[xp, yp] = RectBivariateSpline(rightXParam, rightYParam, rightImageValues, kx=1, ky=1).ev(rightXValues, rightYValues)

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ python get-pip.py
5959
<p align="center"><i>Proof of Concept - Perspective Shifting</i><p align="center">
6060

6161
## Development 'To-Do' List:
62-
- <b>Change:</b> Improved Morphing Performance
63-
- <i>Currently, interpolation is the biggest bottleneck, second to the required matrix math. Some steps have already been taken but this is a complicated issue with the project that stems from the very nature of its existence - see Paragraph 2 of README.</i>
62+
- <b>Change:</b> Improved Drawing Performance
63+
- <i>There is currently a small delay with point placement when working with larger images.</i>
6464
- <b>Feature:</b> Automatic Correspondence Determination
6565
- The user may click a button to have PIM automatically generate points by scanning for similar features between images
6666
- <b>Feature:</b> Resizable Image Points

0 commit comments

Comments
 (0)