In this section, we will scale and rotate images, starting with simple transformations and naïve reconstruction. Below we show an example of scaling with various methods, and a small crop of the resulting image to highlight the differences.
We first consider scaling the image by a constant scale factor.
FloatImage scaleNN(const FloatImage &im, float factor)
: This function should create a new image that is factor
times the size of the input. For this, you need to create a new FloatImage
object that is larger than the original by a factor in each dimension (use floor() to determine the size of the new image). You then loop over all the pixels of the new image, and assign them a color by looking up the appropriate coordinates in the input image. In this simple function, we will use nearest-neighbor reconstruction, which means that all you need to do is round floating point coordinates to the nearest integers.Nearest-neighbor reconstruction leads to blocky artifacts and pixelated results. You will address this using a better reconstruction based on bilinear interpolation. For this, consider the four pixels around the computed coordinate location and perform bilinear reconstruction by first performing two linear interpolations along \(x\) for the top and bottom pairs of pixels, and then another interpolation along \(y\) for the two results. In each case, the interpolation weights are given by the fractional \(x\) and \(y\) coordinates.
float interpolateLin(const FloatImage &im, float x, float y, int z, bool clamp)
: This function takes floating point coordinates and performs bilinear reconstruction. Don't forget to use smartAccessor()
to make sure you can handle coordinates outside the image.FloatImage scaleLin(const FloatImage &im, float factor)
: that uses linear interpolation by calling interpolateLin
appropriately.
rotate(const FloatImage &im, float theta)
: that rotates an image around its center by \(\theta\). Use the center position already in your starter code.
You can obtain better reconstruction by considering more pixels and using smarter weights, such as that given by a bicubic or Lanczos functions.
In what follows, you will implement image warping and morphing according to Beier and Neely's method, which was used for special effects such as those of Michael Jackson's Black or White music video. We highly recommend that you read the provided original article, which is well written and includes important references such as Ghost Busters: Beier, Thaddeus, and Shawn Neely. "Feature-based image metamorphosis." ACM SIGGRAPH Computer Graphics. Vol. 26. No. 2. ACM, 1992.
The full method for warping and morphing includes a number of technical components and it is critical that you debug them as you implement each individual one.
We provide you with a simple (to say the least) interface to specify segment location from a web browser. It is based on javascript and the raphael library (http://raphaeljs.com/). Copy click.html and raphael-min.js to a location with your two input images. The two images should have the same size. For the warp part, you can use the same image on both sides. To change the image, modify the beginning of click.html and update imagewidth, imageheight, path1 and path2 according to your images.
You must click on the segment endpoints in the same order on the left and on the right. Unfortunately, you cannot edit the locations after you have clicked.
Yes, this is primitive. Once you are done, simply copy the C++ code below each image into your main function to create the corresponding segments.
We will start by implementing the Segment
class, which is critical to warping. Test these methods thoroughly as they will be used in warping and morphing functions below.
Segment
class:
Segment::Segment(float x1, float y1, float x2, float y2)
that takes in four floating point values corresponding to the ends of a segment. In particular, this is the syntax that our (hacky) UI expects. Look in the comments to figure out what you need to implement in the constructor. You may find the functions Segment::subtract
, Segment::dot
, and Segment:scalorMult
useful.float Segment::getU(float x, float y)
and float Segment::getV(float x, float y)
. Given these coordinates, you can then compute the new \(x\),\(y\) position of this point given the location of the other segment in vector<float>Segment::UVtoX(float u, float v)
.
float Segment::dist(float x, float y)
, remembering the three cases discussed in class.The core component is a method to transform an image according to the displacement of a segment.
Segment
class above. The output should be a warped image of the same size as the input. Use bilinear reconstruction. Again, use simple examples to test this function. Once you are done with this, you have completed the hardest part of the assignment.
You can use the javascript UI to specify the segments, using the same image on both side for reference.
In this section, you will extend you warp code to perform transformations according to multiple pairs of segments. For each pixel, transform its 2D coordinates according to each pair of segments and take a weighted average according to the length of each segment and the distance of the pixel to the segments. Specifically, the weight is given according to Beier and Neely: \[ \text{weight} = {\left( \frac{\text{length}^p}{a + \text{dist}} \right)}^b \] where \(a, b, p\) are parameters that control the interpolation. In our test, we have used \(b = p = 1\) and \(a\) equal to roughly 5% of the image.
float Segment::weight(float x, float y, float
a, float b, float p)
based on the formula above.FloatImage warp(const Image &im, listSegmentsBefore, listSegmentsAfter, a, b, p)
, which returns a new warped image according to the list of before and after segments, using the Segment::weight
function.Use the provided javascript UI to specify segments. The points must be entered in the same order on the left and right image. You can then copy-paste the C++ code generated below the images to create the corresponding segment objects.
Given your warping code, you will write a function that generates a morph sequence between two images. Again, make sure you are familiar with morphing from the lecture slides and the article.
You are given the two images and a list of corresponding segments for each image. You must generate N images morphing from the first input image to the second input image.
For each image, compute its corresponding time fraction t. Then linearly interpolate the position of each segment endpoint according to t, where t = 0 corresponds to the position in the first image and t = 1 is the position in the last image. You might want to visualize the results for debugging.
You now need to warp both the first and last image so that their segment locations are moved to the segment location at t, which will align the features of the images. We suggest that you write these two images to disk and verify that the images align and that, as t increases, the images get warp from the configuration of the first image all the way to that of the last one.
Finally, for each t, perform a linear interpolation between the pixel values of the two warped images.
Your function should return a sequence of images. For debugging you can use your main function to write your images to disk using a sequence of names such as morph_1.png, morph_2.png, ...
morph(im0, im1, listSegmentsBefore, listSegmentsAfter, N, a, b, p)
. It should return a sequence of \(N\) images in addition to the two inputs (i.e., when called with the default value of 1, it only generates one new image for \(t = 0.5\)). The function should check that im0
and im1
have the same size.There are several ways to make a video out of your files (note this is not required). You can install ffmpeg but this involves some number of dependencies. You can use it as follows:
ffmpeg -r 20 -b 1800 -y -i morph%03d.png out.mp4A windows-only alternative appears to be http://home.hccnet.nl/s.vd.palen/index.html but we haven't tried it. See also: http://www.videohelp.com/tools/Virtualdub. MATLAB also has some decent tools for this.
Create a new function classMorph()
in a6_main.cpp that uses your morphing code to morph between your face and that of the next student in the list. Turn in 15 PNG frames of size \(400 \times 400\).
Turn in your files using Canvas and make sure all your files are in the a6 directory under the root of the zip file. Include all sources (.cpp and .h) files, any of your images, and the output of your program. Don't include your actual executable (we don't need your _build directory), and remove any superfluous files before submission.
In your readme.txt file, you should also answer the following questions: