Spherical harmonics face relighting using OpenCV, OpenGL [w/ code]

I've been working on implementing a face image relighting algorithm using spherical harmonics, one of the most elegant methods I've seen lately.
I start up by aligning a face model with OpenGL to automatically get the canonical face normals, which brushed up my knowledge of GLSL. Then I continue to estimating real faces "spharmonics", and relighting.

Let's start!

Some mathematical background

Don't worry, it wont hurt. much.

So Spherical Harmonics, were invented to numerically express a whole bunch of things in physics like gravity and magnetic fields. But they also became very useful for computer graphics as they are perfect for modelling light falling on a spherical body.

But what ARE those mysterious spherical harmonics?

The way I see it, they are a series of "modes" or "eigenvectors" or "orthogonal components" of a base that spans the surface of a sphere.
To put it simple, they describe the surface of a sphere in increasing finer grained portions. Much like a Fourier decomposition does to a function, there is the base and there are coefficients that when multiplied with the base they recover the function.

How is that good for graphics?

People have used spherical harmonics mostly to model lighting of spherical objects. When you know the coefficients that describe the lighting, you can change them to Re-light an object, or De-light, or transfer the lighting conditions of one scene to another. Very useful!

Some good researchers, Basri and Jacobs, back in 2001 have formulated the first 9 harmonics as a function of the surface normal. On this page Basri references all his work on the subject: http://www.wisdom.weizmann.ac.il/~ronen/index_files/harmonic.html

But I like to reference a work that's easier to process than Basri's, that is the work of Wang et al from 2007. These guys made the steps to use spherical harmonics easier to follow: http://research.microsoft.com/en-us/um/people/zliu/cvpr2007.pdf.
But their algorithm is quite advanced, as it solves not only for the harmonics' coefficients but also for the normals of the object in the image. They use some fancy optimization of an energy function over a graph, that I'm not going to discuss.
But they did make the process of finding the spherical harmonics' coefficient very clear.

The bottom line

We should solve for a vector of 9 coefficients that describes the "lighting of the object" (a face in our case).
Each coefficient will tell us how much that specific harmonic is strong or weak, or in other words how lit is that certain area of the object.

Wang and Basri show a very simple method of using simultaneous linear equations to solve for the lighting coefficients, it depends only on knowing the normal of the object's surface at each pixel in the image.

Getting the normals of a canonical face

So to get the normals, I thought the best way is to use a canonical model of a face (some king of an average face), instead of trying to recover the normals from the image pixels.
For that end, I used Rhino3D to model (very roughly) a shape that resembles a human face, starting from an elongated sphere.
Now all that's left is to align the model with the face to relight, and that will supply the normals.

Cool. Then I built a small app that allows the user to move the model around until it's aligned with the face image. I used FLTK 3.0 to do it since they have a simple interface with OpenGL, they are cross platform, and lightweight.
So I set up a scene where I have the image as the background, and the model is floating above it, half transparent so the user can find the right spot. I added functions for rotating the model, and extra stuff like turning the model opaque.

To get the normal map I used a very simple GLSL shader, that simply colors the pixel with the value of the normal nX,nY,nZ -> R,G,B.
This way the result image OpenGL renders is simply the normal map of the face model. I just grab it using glReadPixels.

Estimating spherical harmonics

So, after the model is aligned, we can assume we have the normals ready for us for each pixel in the image, and the intensity in each pixel is also known.
The first step that Wang suggests, without knowledge of the real face albedo (the real color of every pixel without any lighting effect), is to get an approximation of the 9-vector of lighting coefficients by setting a constant albedo. Easy enough, we can set the albedo to the average color in the face.
Then we can simply build a huge set of linear equations (huge as the number of pixels in the image), and solve an overdetermined system to get the 9 coefficients.

		Scalar albedo_constant = mean(face_img_hsv, smallFaceMask);

		//setup linear equation system, lighting coefficients (l) is unknown
		//I = p00 * Ht * l
		float p00 = (float)albedo_constant[2] / 255.0f;

		cout << "Build Ht("<<n<<",9)...";
		cout << "Build I("<<n<<",1)...";
		//build Ht and I
		Mat_<float> Ht(n,9);
		Mat_<float> I(n,1);
		int pos = 0;
		vector<Mat_<uchar> > face_img_chnls; split(face_img_hsv, face_img_chnls);
		for (int i=0; i<normalMapFlat.rows; i++) {
			if (smallFaceMask(i) == 0) { //is this pixel on the face?
			Ht.row(pos) = p00 * calculateSphericalHarmonicsForNormal(normalMapFlat(i));
			I(pos,0) = face_img_chnls[2](i) / 255.0f; //get V from HSV of pixel [0,1]
			pos ++;
		cout << "DONE"  << endl;

		cout << "Solve" <<endl;
		solve(Ht, I, l, DECOMP_SVD);

		cout << "initial lighting coeffs: ";
		for (int i=0; i<l.rows; i++) {

Booyah! lighting coefficients.

But this is only the first step. Now we can get an approximation of the albedo as well, using the coefficients:

		Mat_<Vec3b> face_img_v3b = face_img;

		#pragma omp parallel for schedule(dynamic)
		for (int y=0; y<face_img.rows; y++) {
			for (int x=0; x<face_img.cols; x++) {
				if (face_mask(y,x) == 0) {
					albedo(y,x) = 0;
				Mat sph = calculateSphericalHarmonicsForNormal(normalMap(y,x));
				Mat_<float> sph_l = sph * l;
				float fsph_l = sph_l(0);

				for (int cn = 0; cn<3; cn++) {
					float fimg = face_img_v3b(y,x)[cn] / 255.0f;
					albedo(y,x)[cn] = (fimg / fsph_l);

Now that we have an initial albedo, Wang suggests we compute the coefficients again to get a better approximation, and then the albedo again.
I however ran into some problems trying to do the second iteration, and the results always came out too dark... But even with the first iteration you can see a very nice change.
Look at the video from before, you can see the right side of the face, which is over-lit, was darkened and the left side was lit up.


The code for spherical harmonics analysis of images is part of a bigger project I have been working on for some time. I also spoke of it in a previous post.
Anyway it's up in GitHub: https://github.com/royshil/HeadReplacement/tree/master/HeadReplacement
You're looking for 4 files:

  • SpharmonicsUI.cpp
  • SpharmonicsUI.h
  • spherical_harmonics_analysis.cpp
  • spherical_harmonics_analysis.h

You can use the CMakeLists.txt to compile, but here's a CMakeLists.txt that should take you there in one piece (fingers crossed):

find_package(OpenCV REQUIRED)
find_package(OpenGL REQUIRED)
find_package(OpenMP REQUIRED)

######## Find and add GLEE ########
	message(STATUS "GLEE was not found")
	get_filename_component(GLEE_PATH ${GLEE_PATH} REALPATH)
	get_filename_component(GLEE_PATH ${GLEE_PATH} PATH)
	message(STATUS "Found GLEE at ${GLEE_PATH}")
	add_library(GLEE ${GLEE_PATH}/GLee.c)

############ Find FLTK ############
		message(STATUS "FLTK was not found !!!!!")
		get_filename_component(FLTK_PATH ${FLTK_PATH} REALPATH)
		get_filename_component(FLTK_PATH ${FLTK_PATH} PATH)
		message(STATUS "Found FLTK at ${FLTK_PATH}")
	get_filename_component(FLTK_PATH ${FLTK_PATH} REALPATH)
	message(STATUS "FLTK path set to ${FLTK_PATH}")

######## Relighting #######

Note that I had to resort to some very dark magic to recover the location of FLTK and GLEE... But it's a jungle out there.

The source of the photograph is: http://www.flickr.com/photos/roel1943/309048020/
It is released under Creative Commons 2.0 ShareAlike-Attribution.So attribution goes toย Roel Wijnants, andย all the results here are also CC-2.0-SA-A... ๐Ÿ™‚


  • lynn

    hi, i tried the method you offered, but sometimes i got fsph_l<0, which causes the albedo map abnormal, do you have any suggestions to me?

    "Look at the video from before, you can see the right side of the face, which is over-lit, was darkened and the left side was lit up."
    i didn't find the video you mentioned, can you give me the site as reference?


  • Roy

    I can't really say why you would get those values... you will have to debug and check the math.

    The video I refer to is about half-way through the post, it's a YouTube embed: http://www.youtube.com/watch?v=wIwAX2UM64E


    hey !
    i need help ๐Ÿ™
    i study IT and I have computer graphic with opengl lesson this term.
    the teacher say for project we must make our photo( photo of our face) with opengl.
    i dont know what should i do ๐Ÿ™ plz help me ๐Ÿ™