Problem set 5 · Resampling, Warping and Morphing
Where to hand in. Submit at
https://compphoto-portal.fredopdurand.workers.dev/
Make sure your files sit in the asst directory at the root of the zip. The
Submission section at the end of this handout has the details, along with the
questions the form will ask you.
AI policy.
You are only allowed to use the class's AI assistant at
https://comp-photo-book.pages.dev/
embedded in the book, notes, slides, and problem sets.it is built to help you think, not to hand you the answer
it knows our material: chapters, notes, demos, problem sets
Fine: brainstorming, explanations, editing, debugging, learning support.
Not fine: pasting problem statements into other (general) AI tools, asking for solutions to assigned problems, or submitting AI-written text, code, equations or analysis as your own.
Keep the line visible: distinguish your own work from AI-assisted work, and mention how the AI helped you (or not).
Extra credit. Percentages are of the regular points of your own version of the class, 6.8370 or 6.8371. An extension is worth what the handout says, 5% unless marked otherwise; for 6.8371, a part marked “6.8370 only” counts as extra credit at the percentage it gives. All extra credit on one problem set together is capped at 10%, so a problem set gives at most 110%.
Questions. Ask on Piazza, where the whole class sees the answer:
https://piazza.com/mit/fall2026/683701
Summary
This problem set has several questions for extra credit. Feel free to attempt them, but do the main problems first. The maximum extra credit is 10%. The last section of this is submitting a photo of yourself, which might take some time depending on what type of person you are. So don't put it off!
Scaling using nearest-neighbor and bilinear reconstruction
Scaling using bicubic and Lanczos methods (6.8370 only, 5% extra credit for 6.8371)
Image warping according to one pair of segments
Image warping according to two lists of segments, using weighted warping
Image morphing
Take a photo of yourself
Resampling
In this section, we will rescale images, starting with simple transformations and naïve reconstructions.
See Figure 1 for examples of scaling with various methods, together with a small crop of the resulting image to highlight the differences.







Basic scaling with nearest-neighbor
The first operation we consider is the re-scaling of an image by a global scale factor $s$. If $s > 1$, the operation will enlarge the input. If $0 < s < 1$, it will shrink the input.
To implement this operation, we need to create a new Image object that is
$s$ times larger (resp.\ $\frac{1}{s}\times$ smaller if $s<1$) than the
original in both width and height. Use floor() to get an integral size
for the new image.
We now have to fill the pixel values of this new image from those of the input: this is called re-sampling. For each pixel in the new image, we look up the color value in the input image at a location that corresponds to the inverse transform (in this case a scaling by $\frac{1}{s}$). In general this location will not be on grid and we'll have to estimate the color at this location using information from the neighboring pixels.
The simplest technique to sample the new value is called nearest-neighbor re-sampling: we round-off the real coordinates to the nearest integers and use the input's color at this new location to be the value of the current output pixel.
- 1
Implement the
Image scaleNN(const Image &im, float factor)
function inbasicImageManipulation.cpp. This function should create a new image that isfactortimes the size of the input using the nearest-neighbor re-sampling method.
Scaling with bilinear interpolation
Nearest-neighbor re-sampling creates blocky artifacts and pixelated results. We will address this using a better reconstruction based on bilinear interpolation. For this, we consider the four pixels immediately around the computed real coordinates and perform two linear interpolations. We first linearly interpolate along $x$ the colors of the top and bottom pairs of pixels. Then we interpolate these two values along $y$ to get the final sample. The interpolation weight are driven by the distance from the corners (see Fig. 2).

- 2.a
Implement
float interpolateLin(const Image &im, float x,in
float y, int z, bool clamp)basicImageManipulation.cpp.
This function takes floating point coordinates and performs bilinear reconstruction on the given channelz. Don't forget to use smart accessors to make sure you can handle coordinates outside the image. - 2.b
Next, write an image scaling function
Image scaleLin(const Image &im, float factor)inbasicImageManipulation.cppthat rescale using linear interpolation by callinginterpolateLinwhere appropriate. Useclamp = true.
Bicubic and Lanczos (required for 6.8370, 5% Extra credit for 6.8371)
You can obtain a better interpolation by considering a larger pixel footprint and using smarter weights, such as that given by a bicubic or Lanczos functions.
You may think of resampling from the view point of convolution to better understand how bicubic/Lanczos resampling kernels work. For example, bilinear interpolation for $3\times$ upscaling would be equivalent to first scaling the image naïvely (by only taking the existing pixels from the source image and keeping the rest as blank) and then applying a $5\times 5$ convolution kernel (how does the kernel look like? A square pyramid) to the naïvely scaled image. Hint: This means that an easy implementation of bicubic and Lanczos resampling is to follow the steps of convolution and use analytical weights $k(x)$ instead of numerically discretized ones. And bilinear/bicubic/Lanczos resampling is separable, which means you can multiply horizontal and vertical weights $k(x)$ and $k(y)$ to get the 2D weight.
- 3
Implement the
Image scaleBicubic(const Image &im, float factor, float B, float C)function inbasicImageManipulation.cpp. This function should create a new image that isfactortimes the size of the input using a bicubic filter kernel. We will use the kernel parametrization from Mitchell and Netravali (“Reconstruction Filters in Computer Graphics", Mitchell and Netravali 1988):$$k(x) = \frac{1}{6}\begin{cases} (12 - 9B - 6C)|x|^3 + \\ (-18 + 12B + 6C)|x|^2 + (6 - 2B) & \text{if } |x| < 1 \\ (-B - 6C)|x|^3 + (6B + 30C)|x|^2 + \\ (-12B - 48C)|x| + (8B + 24C) & \text{if } 1 \leq |x| < 2 \\ 0 & \textrm{otherwise} \end{cases}$$A nice parameter to test your filter kernel is $B=C=\frac{1}{3}$.
- 4
Implement the
Image scaleLanczos(const Image &im, float factor, float a)function inbasicImageManipulation.cpp. This function should create a new image that isfactortimes the size of the input using a Lanczos filter kernel. The kernel is:$$k(x) = \begin{cases} \mathrm{sinc}(x) \mathrm{sinc}(x / a) & \text{if } |x| < a \\ 0 & \textrm{otherwise} \end{cases}$$where $\mathrm{sinc}(x)=\sin(\pi x)/(\pi x)$. A nice parameter to test your filter kernel is $a=3$.
Rotations (5% extra credit)
- 5
Implement the function
Image rotate(const Image &im,in
float theta)basicImageManipulation.cppthat rotates an image around its center by $\theta$ radians. Hint: use your bilinear interpolation function and the center position already present in the starter code.



rotate.Warping and morphing
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 (https://youtu.be/F2AitTPI5U0).
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. A copy of the paper is included in the handout.
Basic Vector Tools
Warping and morphing geometrically distort an input image. This requires a few
vector operation which you'll implement. We provide you a basic Vec2f
class to represent 2D vectors.
- 6.a
In
morphing.cpp, implementVec2f operator+(const Vec2f & a,to sum two vectors $\textbf{a}+\textbf{b}$.
const Vec2f & b) - 6.b
In
morphing.cpp, implementVec2f operator-(const Vec2f & a,that returns the difference $\textbf{a}-\textbf{b}$.
const Vec2f & b) - 6.c
In
morphing.cpp, implementVec2f operator*(const Vec2f & a, float f)that implements multiplication by a scalar $f\cdot \textbf{a}$. - 6.d
In
morphing.cpp, implementVec2f operator/(const Vec2f & a, float f)that implements division by a scalar $\textbf{a} / f $. - 6.e
In
morphing.cpp, implementfloat dot(const Vec2f &a, const Vec2f &b)that implements the dot product of two vectors: $a_x * b_x + a_y * b_y$. - 6.f
In
morphing.cpp, implementfloat length(const Vec2f &a)that returns the length of a vector (in the $L^2$ sense): $\|\textbf{a}\| = \sqrt{a_x^2+a_y^2}$. - 6.g
In
morphing.cpp, implementVec2f perpendicular(const Vec2f &a)that returns a vector that is perpendicular to a. Hint: either of the two possible directions is fine.
Don't forget to test your functions. We have provided some basic tests for you
in a5_main.
Segments
Now that we have some basic tools, we will implement the Segment class,
which is critical to Beier and Neely's warping.
A Segment represents a directed line segment $\overrightarrow{PQ}$. The
class holds a copy of the endpoints $P$ and $Q$, the length of the segment
$\|\overrightarrow{PQ}\|$, and a local orthonormal frame
$(\overrightarrow{e_1},\overrightarrow{e_2})$.
We define:
$\overrightarrow{e_2}$ completes the orthonormal frame (e.g. $\overrightarrow{e_2}$ is perpendicular to $\overrightarrow{e_1}$), see Figure 4.
Both frame vectors and the length of the segment need to be initialized in the class constructor and can be used directly in its class functions.

- 7
In
morphing.cpp, complete the constructorSegment::Segment(Vec2f P_, Vec2f Q_)that creates a segment from its two endpoints and initializes the data structure properly.
Now that our Segment class is usable let's implement methods to convert
from the global $(x,y)$ coordinates of a point to the local $(u,v)$ coordinates
in the reference frame as in Beier and Neely, e.g., equations (1) and
(2) in the paper, and the reverse (equation (3)).
A word of caution: although $v$ exactly corresponds to the second coordinate in the local frame, $u$ is actually rescaled (so read equation (1) carefully).
- 8.a
Implement
Vec2f Segment::XtoUV(Vec2f X) constto compute the $(u,v)$ coordinates of a 2D point $X=(x,y)$ with respect to a segment as described in the paper. - 8.b
Conversely, implement
Vec2f Segment::UVtoX(Vec2f uv) constto compute the $(x,y)$ global coordinates of a 2D point from its local $(u,v)$ coordinates. - 8.c
Implement the point to segment distance function
float Segment::distance(Vec2f X) constas described in section 3.3 of the paper.
Test these methods thoroughly as they will be used in the warping and morphing
functions in the later part of the problem set. The function
testSegment in a5_main.cpp is great place to write simple
test cases.
Warping according to one pair of segments
Now that we have a functional Segment class and an interface to specify
line segments, let the fun begin. The core component to warp images is a
method to transform an image according to the displacement of a segment.
- 9
Implement
Image warpBy1(const Image &im, const Segment &segBefore, const Segment &segAfter). This is a resampling function that warps an entire image according to one a pair of segments. Hint: Figure 1 of the Beier and Neely paper might be helpful here.


warpBy1 function on the
bear.png image using segments: before = Segment((0,0), (10,0)) and
after = Segment((10, 10), (30, 15)).The output should be an image of the same size as the input such that the
feature under segBefore is now at the location of segAfter. Use
bilinear reconstruction (with clamp = true). Again, use simple examples to test
this function, see Figure 5. Once you are done with this,
you have completed the hardest part of the assignment.
Warping according to multiple pairs of segments
In this question, 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:
where $a, b, p$ are parameters that control the interpolation. In our test, we have used $b=p=1$ and $a=10$ (roughly 5% of the image size).
- 10.a
Implement
float Segment::weight(Vec2f X, float a, float b, float p) constbased on the formula above. - 10.b
Implement
Image warp(const Image &im, const vector<Segment> &segsBefore, const vector<Segment> &segsAfter, float a=10.0,which returns a new warped image according to the list of before and after segments. Pay attention to the order of the segments, where you should loop over output pixels. Hint: section 3.3 of the paper and the above function will be helpful here.
float b=1.0, float p=1.0)
Use the provided interface, described below, to specify segments, then copy the C++ code it generates to create the corresponding segment objects.
UI
We provide an interface to specify segments in a web browser: open
morph_ui.html. It shows the two images named by path1 and
path2 at the top of that file; the “Load image” buttons, or dropping
an image file onto a picture, load others without editing it. The two images of
a morph must be the same size.
Click twice on a picture to draw a segment: the first click is its start $P$ (a dot), the second its end $Q$ (an arrowhead). Segments come in numbered pairs, and the number is shown at both ends: pair $k$ on the left corresponds to pair $k$ on the right. You do not need to draw them in the same order on both sides. A new segment joins the pair whose segment on the other picture is closest, and you can move a segment to another pair from the table below the pictures, or by clicking it and typing the pair number. Direction matters too: the start must be on the same feature in both pictures. The interface flags a pair whose two segments point in very different directions, and “flip” swaps a segment's ends. You can drag an end, or a whole segment, to adjust it, and undo with Ctrl+Z. A preview shows the segments interpolated at a time $t$ of your choice over the cross-dissolved images, where a mismatched pair shows up as a segment that swings or flips. Once you are done, copy the C++ code at the bottom into your main function. “Load from C++” reads such code back in, to edit segments you already have.

Morphing
Given your warping code, we will write a function that generates a morph sequence between two images. Again, make sure you are familiar with morphing from the article.
You are given the source and target images, and a list of segments for each image (the position in the list defines the corresponding pairs of segments, so the lists should have the same number of elements). 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's endpoints 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 warped 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, …, see testMorph in
a5_main.cpp.
- 11
Implement
vector<Image> morph(const Image &im1, const Image &im2,. It should return $N+2$ images:
const vector<Segment> &segsBefore, const vector<Segment> &segsAfter,
int N=1, float a=10.0, float b=1.0, float p=1.0)im1first,im2last, and $N$ new images in between (i.e., when called with the default value of 1, it only generates one new image for $t=0.5$). The function should check thatim1andim2have the same size, and throw an exception if not. Note that the interpolation weight should be at the scale of $[0,1]$, so we need a time constant of $1/(N+1)$ here.



Visualize Results
Aside from just looking at the images, you can explore your results at https://www.mit.edu/ adalca/tipiX/ if you wish. Click on load and load your
morphed images (make sure to respect the required naming). Then explore your
results using the pointer, or click "play" from the top-right information
panel.
There are several ways to make a video (or a .gif) out of your files
(note this is not required). You can install ffmpeg http://ffmpeg.org/ but this involves some number of dependencies. Then use it
with, e.g. ffmpeg -i tes_morph_%02d.png out.gif.
Class morph
We'll use the code implemented in this problem set to create a class morph. In order to do that, we need a photo of your face.
- 12
Submit a selfie (of yourself) as a PNG file named
myface.pngin theInputfolder of your submission. Feel free to make a silly face if you want. Make sure it's named correctly and in the right folder so that our script can pick up your image.
Make sure your photo follows the following specifications:
500 x 600 (500 width, 600 height). There are image rescaling tools online, or you can use what you implemented earlier this pset!
The background must be a single color of either white or otherwise pale color, and there shouldn't be clutter in your background.
Your entire face (everything from the neck up) should be visible. Think passport photo.
The file should be a png. If it is not, there should be plenty of conversion tools online.
In the next pset, we will ask you to run your morphing code from your own face to the face of a peer. Get excited!
Extra credit
How to hand in extra credit.
Each extension comes with a test that runs it; an extension that is described but not tested earns at most about half its value.
Put the extension's code in
extraCredit.cpp, declared inextraCredit.h. It may call your other functions.Write
testExtraCredit.cpp, a program with its ownmain()that runs the extension on real inputs (the provided images inInput/, or your own inInput/debug/), writes its results as PNG files toOutput/extra_credit/, and prints what it checks: numbers, timings, comparisons.make ecbuilds and runs it.In the submission form's extra credit question, say what the extension does, which functions implement it, and what the test shows.
make prepareas usual: it already includes both files.
extraCredit.cpp is compiled together with your required code, so if it does
not compile, every regular test fails too: submit only extra credit code that builds.
The grader keeps the images in Output/extra_credit/ (or Output/) and
ignores PNG files anywhere else.
Here are ideas for extensions you could attempt, for 5% each. At most, on the entire assignment, you can get 10% of extra credit:
Improve the javascript UI. The amount of extra credit you get is dependent on how much improvement you make. We may also keep your improvements for future classes!
Extend to movies, where segments are specified at a number of keyframes.
Morphable face models (see https://www.face-rec.org/algorithms/3D_Morph/morphmod2.pdf. Data is at https://github.com/3d-morphable-models/curated-list-of-awesome-3D-Morphable-Model-software-and-data)
Submission
Turn in your files to the online submission system and make
sure all your files are in the asst directory under the root of the zip
file. If your code compiles on the submission system, it is organized correctly.
The submission system will run code in your main function, but we will not use
this code for grading. The submission system should also show you the image your
code writes to the ./Output directory
Leave the input images we provide out of the zip: make prepare copies only
the images you made yourself (Input/debug/, or a file a question asks
for by name), the submission system
supplies the provided ones, and grading uses its own copy. Uploads are limited
to 100 MB.
In the submission system, there will be a form in which you should answer every written question this handout marks with “(Answer in the form)”, and these:
How long did the assignment take? (in minutes)
Potential issues with your solution and explanation of partial completion (for partial credit)
Any extra credit you may have implemented and their function signatures if applicable
Collaboration acknowledgment (you must write your own code)
What was most unclear/difficult?
What was most exciting?