Now we have seen most building block which you need to start with the final challenge. The challenge is the 3d reconstruction of a real world face image. The data you get is only a single image of a face. In the following sections we provide you some more hints about what you can use to adapt the model to the image. Then it is up to you to design a good compilation of the building blocks to solve the task.
The goal is to calculate a 3d reconstruction from a single 2d image. Two images are in the folder challenge
from the project repository. The indoor image is in a more controlled setting than the outdoor image. The reconstruction of the second image is harder due to the more complex illumination, pose and the cluttered background. So we suggest to start with the easier image.
We first introduce some parts that are essential to get to a first running application before in the next section we introduce more advanced topics.
You can use the landmark clicker (.jar-file) to annotate landmarks in the image. You can load your own list of landmarks by providing a valid TLMS-2d landmark file or simply use the predefined ones.
You then can load a *.tlms file using:
val landmarks = TLMSLandmarksIO.read2D(new File("path/to/file.tlms")).get
To adapt the model to the image more accurately you need to use the image colors. As for the computer graphics we need also illumination, you need to change this part of the render parameters too. You can use the following classes from the framework to handle colors:
ColorPorposal to change the albedo of the model
val colorProposal = GaussianMoMoColorProposal(0.1)
Note that you have to convert the proposals to RenderParameter
proposal using the function toParameterProposal
in order to combine model proposals and pose proposals.
ColorPrior to evaluate the model prior of the albedo
val texturePrior = GaussianTexturePrior(0.0,1.0)
IndependantPixelEvaluator to evaluate the likelihood of an image
val ipEval = IndependentPixelEvaluator(target,IsotropicGaussianPixelEvaluator(0.1),ConstantPixelEvaluator(0.4))
ImageRendererEvaluator to evaluate the likelihood of a set of parameters. The evaluator uses a renderer to produce an image and evaluates then the likelihood with the passed evaluator.
val imageEval = ImageRendererEvaluator(renderer,ipEval)
To adapt the light there are two options. Either a update sampled from a Gaussian distribution can be used, or we can optimize the light parameters given the current state:
Random updates can be done using the SHLightPerturbationProposal
:
import scalismo.faces.sampling.face.proposals.SphericalHarmonicsLightProposals._
val illuminationProposal = SHLightPerturbationProposal(0.05,fixIntensity = false)
Calculated updates can be done using the SHLightSolverProposal
:
import scalismo.faces.sampling.face.proposals.ParameterProposals.implicits._
val shlOptimizer = SphericalHarmonicsOptimizer(renderer,target)
val samplingStrategy = (mesh: TriangleMesh3D) => MeshSurfaceSampling.sampleUniformlyOnSurface(1000)(mesh)
val illuminationProposal = SHLightSolverProposal(shlOptimizer,samplingStrategy).toParameterProposal
When you use the optimization proposal you have a deterministic proposal. This proposl has not a symmetric transition ratio. Strictly mathematical speaking the transition ratio is not defined. In practice this proposal helps a lot in the beginning of the adaptation process. To use it you have however to change your algorithm and use the MetropolisHastings
class.
With these informations you should be able to already start fitting the image. Try to come up with an initial working algorithm. This will help you to develop a feeling for the problem and to select which part you would like to improve on.
We will sketch a lot of advanced topics you can address in the next section.
Here we present advanced topics which arise when working on the reconstruction problem. Note that you do not have to address all these topics and you will not have enough time to tackle them all during this summer-school.
All the mathematical guarantees that the underlying algorithm provides are only valid for very long sampling runs. Should we now invest a lot of time to design and tune the porposals and evaluators or should we simply run the algorithm long enough?
While the algorithm is non-deterministic, the evaluator has to be deterministic. So providing twice the same render parameters to an evaluator has to return the same value.
When designing proposals the important decisions are:
An important topic for more performance is caching. As we use a software renderer, the rendering takes more time compared to a hardware solution. We can overcome part of this problem using the MoMoRenderer
's caching mechanism. You can take advantage of it by simply calling cached(n: Int)
where n is the size of the cache:
val cachedRenderer: MoMoRenderer = MoMoRenderer(model).cached(10)
Further you can also cache the value of evaluators using the CachedDistributionEvaluator
. This makes sense for costly evaluations as e.g. the image evaluator which iterates over all pixels in the image. You can cache an evaluator like this:
import scalismo.faces.sampling.evaluators.CachedDistributionEvaluator.implicits._
val cachedEvaluator = evaluator.cached(10)
Another possibility to speed up the algorithm is to use step-wise Bayesian inference. Step-wise bayesian inference is not only about gaining speed. It is also useful to integrate different sources of information, as for example the landmarks and the information contained in the image.
In the software the bayesian inference or filtering is implemented using the MetropolisFilterProposal
:
import scalismo.sampling.proposals.MetropolisFilterProposal
val proposal: SymmetricProposalGenerator[RenderParameter] = ???
val evaluator: DistributionEvaluator[RenderParameter] = ???
val filteredProposals = MetropolisFilterProposal(proposal,evaluator)
Loggers provide a good starting point to get a deeper insight on a sampling run. Using an AcceptRejectLogger
, you can print out the evolution of your sampling chain during the run in the following way:
val verboseLogger = new AcceptRejectLogger[RenderParameter] {
var index = 0
override def accept(current: RenderParameter,
sample: RenderParameter,
generator: ProposalGenerator[RenderParameter],
evaluator: DistributionEvaluator[RenderParameter]): Unit = {
printMessage("A ",current,sample,generator,evaluator)
}
override def reject(current: RenderParameter,
sample: RenderParameter,
generator: ProposalGenerator[RenderParameter],
evaluator: DistributionEvaluator[RenderParameter]): Unit = {
printMessage("R ",current,sample,generator,evaluator)
}
def printMessage(prefix: String,
current: RenderParameter,
sample: RenderParameter,
generator: ProposalGenerator[RenderParameter],
evaluator: DistributionEvaluator[RenderParameter]): Unit ={
println(s"$prefix ${"%06d".format(index)} : ${evaluator.logValue(sample)}")
}
}
The AcceptRejectLogger
has to be passed as second argument to the iterator
function called on the sampling algorithm.
val algorithm = Metropolis(proposal,evaluator)
val chainIterator = algorithm.iterator(init,verboseLogger)
As an additional ChainStateLogger
you can use one that keeps track of the best sample given an evaluator.
val bestLogger = BestSampleLogger(evaluator)
/* ...
then you run your sampling application
... */
val bestSample = bestLogger.currentBestSample().get
When you want to add multiple loggers you have to combine them. Multiple loggers can be combined using logger containers:
import scalismo.sampling.loggers.ChainStateLoggerContainer
import scalismo.sampling.loggers.AcceptRejectLoggerContainer
val combinedCSLoggers = ChainStateLoggerContainer(Seq(csLoggerA, csLoggerB))
val combinedARLoggers = AcceptRejectLoggerContainer(Seq(arLoggerA, arLoggerB))
When we define the image likelihood model we have to model explicitly the background (see additional information). The best choice depends on the target image.
There are different image likelihood models you can choose from are:
The IndependentPixelEvaluator uses a PairEvaluator[RGB]
for the foreground and a DistributionEvaluator[RGB]
for the background. The evaluation at a single pixel is then the sum of the two weighted by the alpha channel, indicating if it is fore- or background.
val independentEval: PairEvaluator[PixelImage[RGBA]] = IndependentPixelEvaluator(pixelEvaluator = fgEval, bgEvaluator = bgEval)
The TrimmedIndependentPixelEvaluator uses a PairEvaluator[RGB]
for the foreground and a DistributionEvaluator[RGB]
for the background. The evaluation over the image uses only the alpha fraction of the most likely foreground pixels. The alpha has to be chosen so that it corresponds to the fraction of the image size which is coverd by the face.
val trimmedEval: PairEvaluator[PixelImage[RGBA]] = TrimmedIndependentPixelEvaluator(pixelEvaluator = fgEval, bgEvaluator = bgEval, alpha = 0.0)
The CollectiveLikelihoodEvaluator rates the average difference between the images for the rendered face area.
val cltEval: PairEvaluator[PixelImage[RGBA]] = CollectiveLikelihoodEvaluator(sigma = 0.072, relativeVariance = 9.0)
Possible choices for the foreground model are:
The IsotropicGaussianPixelEvaluator rates a pair of colors. This is the most common choice.
val gaussFG: PairEvaluator[RGB] = IsotropicGaussianPixelEvaluator(1.0)
To use different distributions you can use the package breeze.stats.distributions
. You can find a quick start guide by following this link.
Some options of the background model are:
The ConstantPixelEvaluator assumes a constant likelihood for all background pixels.
val constBG: DistributionEvaluator[RGB] = ConstantPixelEvaluator[RGB](value = 0.01)
The HistogramRGB estimates the color distribution from the target image and uses this empirical density as background model.
val histBG: DistributionEvaluator[RGB] = HistogramRGB(image = target.values.toSeq.map(_.toRGB),binsPerChannel = 10)
In the end we want a sampling chain over RenderParameter
and hence need a DistributionEvaluator[RenderParameter]
. We can transform a PairEvaluator[PixelImage[RGBA]]
to this type in two steps using the ImageRendererEvaluator
and the toDistributionEvaluator
function:
val distEval: DistributionEvaluator[PixelImage[RGBA]] = independentEval.toDistributionEvaluator(targetImage)
val imageEval = ImageRendererEvaluator(renderer = renderer, imageEvaluator = distEval)