Skip to content
This repository has been archived by the owner on Dec 18, 2024. It is now read-only.

Commit

Permalink
support up to 3 images
Browse files Browse the repository at this point in the history
  • Loading branch information
avojak committed May 3, 2020
1 parent 0f6d24e commit a3aa713
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 36 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.0
1.1.0
4 changes: 3 additions & 1 deletion images/inputs/photo_attribution.rtf
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ sidius: By Source (WP:NFCC#4), Fair use, https://en.wikipedia.org/w/index.php?cu

Young Anakin: Comic Vine (https://comicvine1.cbsistatic.com/uploads/original/11125/111250671/4775035-a1.jpg)

Old Anakin: Fandom (https://vignette.wikia.nocookie.net/swfanon/images/8/89/AnakinEstGrumpy.jpg/revision/latest/top-crop/width/360/height/450?cb=20120219201211)
Old Anakin: Fandom (https://vignette.wikia.nocookie.net/swfanon/images/8/89/AnakinEstGrumpy.jpg/revision/latest/top-crop/width/360/height/450?cb=20120219201211)

vader: BGR (https://boygeniusreport.files.wordpress.com/2015/08/darth-vader.jpg?quality=98&strip=all)
Binary file added images/inputs/vader.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
75 changes: 57 additions & 18 deletions lib/libmorphing/morphing.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@

class ImageMorph:

def __init__(self, source_img_path: str, target_img_path: str, source_points: list, target_points: list,
output_dir: str, gif_duration: int, gif_fps: int) -> None:
def __init__(self, source_img_path: str, middle_img_path: str, target_img_path: str, source_points: list,
middle_points: list, target_points: list, output_dir: str, gif_duration: int, gif_fps: int) -> None:
"""
Create an animated GIF which morphs the source image into the target image.
Expand All @@ -34,22 +34,35 @@ def __init__(self, source_img_path: str, target_img_path: str, source_points: li
correspond to points in the source image.
- output_dir: The location where result images will be placed.
"""
self.has_middle_img = middle_img_path is not None

self.source_img = cv2.imread(source_img_path)
if self.has_middle_img:
self.middle_img = cv2.imread(middle_img_path)
self.target_img = cv2.imread(target_img_path)

assert len(source_points) == len(target_points)
if self.has_middle_img:
assert len(source_points) == len(middle_points)

self.source_points = source_points.copy()
if self.has_middle_img:
self.middle_points = middle_points.copy()
self.target_points = target_points.copy()

self.output_dir = output_dir
os.makedirs(self.output_dir, exist_ok=True)

assert self.source_img.shape == self.target_img.shape
if self.has_middle_img:
assert self.source_img.shape == self.middle_img.shape

H, W, C = self.source_img.shape

# Add the points at the corners of the images
self.source_points.extend([[0, 0], [0, W - 1], [H - 1, 0], [H - 1, W - 1]])
if self.has_middle_img:
self.middle_points.extend([[0, 0], [0, W - 1], [H - 1, 0], [H - 1, W - 1]])
self.target_points.extend([[0, 0], [0, W - 1], [H - 1, 0], [H - 1, W - 1]])

# Define the configuration
Expand All @@ -60,6 +73,8 @@ def __init__(self, source_img_path: str, target_img_path: str, source_points: li
logger.info('GIF duration: {}'.format(self.gif_duration))
logger.info('GIF FPS: {}'.format(self.gif_fps))
logger.info('Source image: {} ({}x{})'.format(source_img_path, W, H))
if self.has_middle_img:
logger.info('Middle image: {} ({}x{})'.format(middle_img_path, W, H))
logger.info('Target image: {} ({}x{})'.format(target_img_path, W, H))

self._morph()
Expand All @@ -68,19 +83,27 @@ def _morph(self):
"""
Performs the morphing.
"""
logger.debug('Writing mapping image...')
logger.debug('Writing mapping images...')
if self.has_middle_img:
io.write_mapping_img(self.source_img, self.middle_img, self.source_points, self.middle_points,
os.path.join(self.output_dir, 'source-middle-mapping.png'))
io.write_mapping_img(self.source_img, self.target_img, self.source_points, self.target_points,
os.path.join(self.output_dir, 'mapping.png'))
os.path.join(self.output_dir, 'source-target-mapping.png'))
logger.debug('Creating Delaunay triangulation...')
triangulation = Delaunay(self.source_points)

# From this point on, the points must be a NumPy array
self.source_points = np.array(self.source_points)
if self.has_middle_img:
self.middle_points = np.array(self.middle_points)
self.target_points = np.array(self.target_points)

logger.debug('Writing triangulation images...')
io.write_triangulation_img(triangulation, self.source_img, self.source_points,
os.path.join(self.output_dir, 'source_triangulation.png'))
if self.has_middle_img:
io.write_triangulation_img(triangulation, self.middle_img, self.middle_points,
os.path.join(self.output_dir, 'middle_triangulation.png'))
io.write_triangulation_img(triangulation, self.target_img, self.target_points,
os.path.join(self.output_dir, 'target_triangulation.png'))

Expand All @@ -93,7 +116,23 @@ def _morph(self):
results = []
for frame_num in range(0, num_frames + 1):
t = frame_num / num_frames
res = pool.apply_async(self._process_func, (triangulation, t, frame_num, (H, W, C)))
if self.has_middle_img:
if t <= 0.5:
scaled_t = t / 0.5
res = pool.apply_async(self._process_func,
(triangulation, scaled_t, frame_num, (H, W, C), self.source_img,
self.middle_img, self.source_points,
self.middle_points))
else:
scaled_t = (t - 0.5) / 0.5
res = pool.apply_async(self._process_func,
(triangulation, scaled_t, frame_num, (H, W, C), self.middle_img,
self.target_img, self.middle_points,
self.target_points))
else:
res = pool.apply_async(self._process_func, (triangulation, t, frame_num, (H, W, C), self.source_img,
self.target_img, self.source_points,
self.target_points))
results.append(res)

for res in results:
Expand All @@ -104,7 +143,7 @@ def _morph(self):
logger.debug('Creating GIF...')
io.write_gif(frame_dir, filename, self.gif_fps)

def _compute_frame(self, triangulation, t, shape):
def _compute_frame(self, triangulation, t, shape, source_img, target_img, source_points, target_points):
"""
Computes a frame of the image morph.
Expand All @@ -127,20 +166,20 @@ def _compute_frame(self, triangulation, t, shape):
simplices = triangulation.simplices[triangle_index]
for v in range(0, 3):
simplex = triangulation.simplices[triangle_index][v]
P = self.source_points[simplex]
Q = self.target_points[simplex]
P = source_points[simplex]
Q = target_points[simplex]
average_triangles[triangle_index][v] = P + t * (Q - P)

# Compute the affine projection to the source and target triangles
source_triangle = np.float32([
self.source_points[simplices[0]],
self.source_points[simplices[1]],
self.source_points[simplices[2]]
source_points[simplices[0]],
source_points[simplices[1]],
source_points[simplices[2]]
])
target_triangle = np.float32([
self.target_points[simplices[0]],
self.target_points[simplices[1]],
self.target_points[simplices[2]]
target_points[simplices[0]],
target_points[simplices[1]],
target_points[simplices[2]]
])
average_triangle = np.float32(average_triangles[triangle_index])
source_transform = cv2.getAffineTransform(average_triangle, source_triangle)
Expand All @@ -157,12 +196,12 @@ def _compute_frame(self, triangulation, t, shape):

# Perform a weighted average per-channel
for c in range(0, shape[2]):
source_val = self.source_img[int(source_point[1]), int(source_point[0]), c]
target_val = self.target_img[int(target_point[1]), int(target_point[0]), c]
source_val = source_img[int(source_point[1]), int(source_point[0]), c]
target_val = target_img[int(target_point[1]), int(target_point[0]), c]
frame[point[1], point[0], c] = round((1 - t) * source_val + t * target_val)
return frame

def _process_func(self, triangulation, t, frame_num, shape):
frame = self._compute_frame(triangulation, t, shape)
def _process_func(self, triangulation, t, frame_num, shape, source_img, target_img, source_points, target_points):
frame = self._compute_frame(triangulation, t, shape, source_img, target_img, source_points, target_points)
io.write_frame(frame, frame_num, os.path.join(self.output_dir, 'frames'))
logger.debug('Created frame {}'.format(str(frame_num)))
3 changes: 2 additions & 1 deletion lib/tests/test0.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ def test_1():
[233.0, 306.0], [240.0, 8.0], [227.0, 458.0]]
target_points = [[162.0, 209.0], [305.0, 209.0], [28.0, 15.0], [450.0, 18.0], [198.0, 346.0], [270.0, 345.0],
[238.0, 293.0], [233.0, 59.0], [237.0, 382.0]]
ImageMorph(source_img_path, target_img_path, source_points, target_points, output_dir, gif_duration=3, gif_fps=10)
ImageMorph(source_img_path, None, target_img_path, source_points, None, target_points, output_dir, gif_duration=3,
gif_fps=10)
60 changes: 52 additions & 8 deletions web/webmorphing/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ def home():
@bp.route('/morph', methods=['POST', 'GET'])
def morph():
if request.method == 'POST':
has_middle_image = False
if 'middle-img' in request.files and request.files['middle-img'].filename != '':
has_middle_image = True

# Ensure that we've actually received both image files
if 'source-img' not in request.files or request.files['source-img'].filename == '':
flash('No source image.')
Expand All @@ -40,16 +44,25 @@ def morph():
if not allowed_file(request.files['source-img'].filename):
flash('Source image file type is not allowed.')
return redirect(request.url)
if has_middle_image and not allowed_file(request.files['middle-img'].filename):
flash('Middle image file type is not allowed.')
return redirect(request.url)
if not allowed_file(request.files['target-img'].filename):
flash('Target image file type is not allowed.')
return redirect(request.url)

# Ensure that we've received the source and target points
source_points = json.loads(request.form['source_points'])
middle_points = None
if has_middle_image:
middle_points = json.loads(request.form['middle_points'])
target_points = json.loads(request.form['target_points'])
if len(source_points) == 0:
flash('No source points selected.')
return redirect(request.url)
if has_middle_image and len(middle_points) == 0:
flash('No middle points selected.')
return redirect(request.url)
if len(target_points) == 0:
flash('No target points selected.')
return redirect(request.url)
Expand All @@ -76,27 +89,44 @@ def morph():

# Extract the image files from the request and save them in the upload folder
source_img = request.files['source-img']
middle_img = None
if has_middle_image:
middle_img = request.files['middle-img']
target_img = request.files['target-img']
source_img_path = os.path.join(req_dir, 'source_img')
middle_img_path = None
if has_middle_image:
middle_img_path = os.path.join(req_dir, 'middle_img')
target_img_path = os.path.join(req_dir, 'target_img')
source_img.save(source_img_path)
if has_middle_image:
middle_img.save(middle_img_path)
target_img.save(target_img_path)

# Verify image dimensions
source_shape = cv2.imread(source_img_path).shape
middle_shape = None
if has_middle_image:
middle_shape = cv2.imread(middle_img_path).shape
target_shape = cv2.imread(target_img_path).shape
if source_shape[0] > 600 or source_shape[1] > 600 or target_shape[0] > 600 or target_shape[1] > 600:
flash('Maximum image resolution is 600x600.')
return redirect(request.url)
if has_middle_image and (middle_shape[0] > 600 or middle_shape[1] > 600):
flash('Maximum image resolution is 600x600.')
return redirect(request.url)
if source_shape != target_shape:
flash('Source and target image have mismatching resolutions.')
return redirect(request.url)
if has_middle_image and source_shape != middle_shape:
flash('Source and middle image have mismatching resolutions.')
return redirect(request.url)

Thread(target=thread_func,
args=(source_img_path, target_img_path, source_points, target_points, res_dir, gif_duration, gif_fps)
).start()
args=(source_img_path, middle_img_path, target_img_path, source_points, middle_points, target_points,
res_dir, gif_duration, gif_fps)).start()

return redirect(url_for('home.morph_result', req_id=req_id))
return redirect(url_for('home.morph_result', req_id=req_id, has_middle_image=has_middle_image))
else:
return render_template('home.html')

Expand Down Expand Up @@ -126,10 +156,16 @@ def get_target_image(req_id):
return get_image(os.path.join(get_req_dir(req_id), 'target_img'))


@bp.route('/results/<req_id>/point-mapping-image', methods=['GET'])
def get_point_mapping_image(req_id):
@bp.route('/results/<req_id>/source-middle-mapping-image', methods=['GET'])
def get_source_middle_mapping_image(req_id):
validate_request_id(req_id)
return get_image(os.path.join(get_res_dir(req_id), 'mapping.png'))
return get_image(os.path.join(get_res_dir(req_id), 'source-middle-mapping.png'))


@bp.route('/results/<req_id>/source-target-mapping-image', methods=['GET'])
def get_source_target_mapping_image(req_id):
validate_request_id(req_id)
return get_image(os.path.join(get_res_dir(req_id), 'source-target-mapping.png'))


@bp.route('/results/<req_id>/source-triangulation-image', methods=['GET'])
Expand All @@ -138,6 +174,12 @@ def get_source_triangulation_image(req_id):
return get_image(os.path.join(get_res_dir(req_id), 'source_triangulation.png'))


@bp.route('/results/<req_id>/middle-triangulation-image', methods=['GET'])
def get_middle_triangulation_image(req_id):
validate_request_id(req_id)
return get_image(os.path.join(get_res_dir(req_id), 'middle_triangulation.png'))


@bp.route('/results/<req_id>/target-triangulation-image', methods=['GET'])
def get_target_triangulation_image(req_id):
validate_request_id(req_id)
Expand Down Expand Up @@ -179,5 +221,7 @@ def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in current_app.config['ALLOWED_EXTENSIONS']


def thread_func(source_img_path, target_img_path, source_points, target_points, output_dir, gif_duration, gif_fps):
ImageMorph(source_img_path, target_img_path, source_points, target_points, output_dir, gif_duration, gif_fps)
def thread_func(source_img_path, middle_img_path, target_img_path, source_points, middle_points, target_points,
output_dir, gif_duration, gif_fps):
ImageMorph(source_img_path, middle_img_path, target_img_path, source_points, middle_points, target_points,
output_dir, gif_duration, gif_fps)
Loading

0 comments on commit a3aa713

Please sign in to comment.