FaceGen 3 SDKs Reference
Loading...
Searching...
No Matches
Fg3Scm.cpp
1//
2// Copyright (c) Singular Inversions Inc. 2012
3//
4// Authors: Andrew Beatty
5// Created: Feb 14, 2012
6//
7
8#include "stdafx.h"
9
10#include "Fg3Scm.hpp"
11#include "FgImage.hpp"
12#include "FgFileSystem.hpp"
13#include "FgImgDisplay.hpp"
14#include "FgCommand.hpp"
15#include "FgTime.hpp"
16#include "FgTestUtils.hpp"
17
18using namespace std;
19
20namespace Fg {
21
22static
23bool checkHeader(istream & is,string val)
24{
25 string hdr;
26 hdr.resize(val.size());
27 is.read(&hdr[0],hdr.size());
28 return (hdr == val);
29}
30
31Scm3SA loadEgt(String8 const & egtFile)
32{
33 Scm3SA ret;
34 Ifstream ifs(egtFile);
35 if (!checkHeader(ifs,"FREGT003"))
36 fgThrow("File is not an .EGT file",egtFile);
37 uint32 hgt,
38 wid,
39 symm,
40 asym;
41 readBinRaw_(ifs,hgt);
42 readBinRaw_(ifs,wid);
43 readBinRaw_(ifs,symm);
44 readBinRaw_(ifs,asym);
45 ifs.ignore(40);
46 ret.symm.resize(symm);
47 ret.asym.resize(asym);
48 size_t numPix = scast<size_t>(wid)*hgt;
49 for (uint ss=0; ss<2; ++ss) {
50 Scm3Modes & modes = (ss == 0) ? ret.symm : ret.asym;
51 for (Scm3Mode & mode : modes) {
52 readBinRaw_(ifs,mode.scale);
53 // File format keeps each channel separately so for faster file loading, pull these into RAM
54 // in a single read each before interleaving:
55 Schars R(numPix),G(numPix),B(numPix);
56 ifs.read(reinterpret_cast<char*>(R.data()),numPix);
57 ifs.read(reinterpret_cast<char*>(G.data()),numPix);
58 ifs.read(reinterpret_cast<char*>(B.data()),numPix);
59 mode.mode.resize(Vec2UI{wid,hgt},Arr3SC{0}); // zero fill to avoid warning
60 for (size_t ii=0; ii<numPix; ++ii) {
61 mode.mode.m_data[ii][0] = R[ii];
62 mode.mode.m_data[ii][1] = G[ii];
63 mode.mode.m_data[ii][2] = B[ii];
64 }
65 }
66 }
67 return ret;
68}
69
70Img2F loadFim(String8 const & fimFile)
71{
72 Img2F ret;
73 Ifstream ifs(fimFile);
74 if (!checkHeader(ifs,"FIMFF001"))
75 fgThrow("File is not an .FIM file",fimFile);
76 uint32 wid,
77 hgt;
78 readBinRaw_(ifs,wid);
79 readBinRaw_(ifs,hgt);
80 ifs.ignore(48);
81 ret.resize(wid,hgt);
82 size_t numPix = scast<size_t>(wid) * hgt;
83 ifs.read(reinterpret_cast<char*>(ret.dataPtr()),numPix*sizeof(Vec2F));
84 return ret;
85}
86
87ImgV3F scmMac(Scm3Modes const & modes,Floats const & coeffs)
88{
89 ImgV3F ret;
90 if (!modes.empty()) {
91 if (modes.size() != coeffs.size())
92 fgThrow("scmMac modes.size() != coeffs.size()",modes.size(),coeffs.size());
93 Vec2UI dims = modes[0].mode.dims();
94 ret.resize(dims,Vec3F(0));
95 size_t sz = ret.numPixels();
96 // Multithreading this part only made about 20% speedup in EGT calc so don't bother:
97 for (size_t cc=0; cc<modes.size(); ++cc) {
98 Scm3Mode const & mode = modes[cc];
99 FGASSERT(mode.mode.dims() == dims);
100 float fac = mode.scale * coeffs[cc];
101 for (size_t ii=0; ii<sz; ++ii)
102 ret.m_data[ii] += Vec3F(mapCast<float>(mode.mode.m_data[ii])) * fac;
103 }
104 }
105 return ret;
106}
107
108ImgRgba8 scmAdd(ImgRgba8 const & mean,ImgV3F const & delta,float outInvGamma,bool mt)
109{
110 if (mean.empty())
111 return ImgRgba8{};
112 float outRelGamma = 2.6f / outInvGamma;
113 if (delta.empty()) {
114 auto fun = [&mean,outRelGamma](size_t xx,size_t yy)
115 {
116 Arr4UC mn = mean.xy(xx,yy).m_c;
117 Rgba8 ret;
118 for (size_t ii=0; ii<3; ++ii) {
119 float const b = pow(mn[ii]/255.0f,outRelGamma) * 255.0f;
120 ret[ii] = uchar(b+0.5f);
121 }
122 ret[3] = mn[3];
123 return ret;
124 };
125 return generateImg<Rgba8>(mean.dims(),fun,mt);
126 }
127 else {
128 AxAffine2F meanIrcsToIucs = cIrcsToIucs<float>(mean.dims());
129 auto fun = [outRelGamma,meanIrcsToIucs,&mean,&delta](size_t xx,size_t yy)
130 {
131 Arr4UC mn = mean.xy(xx,yy).m_c;
132 Vec2F iucs = meanIrcsToIucs * Vec2F(xx,yy);
133 Arr3F del = sampleClampIucs(delta,iucs).m;
134 Rgba8 ret;
135 for (size_t ii=0; ii<3; ++ii) {
136 float v0 = mn[ii] + del[ii],
137 a = clamp(v0,0.0f,255.0f),
138 b = pow(a/255.0f,outRelGamma) * 255.0f;
139 ret[ii] = uchar(b+0.5f);
140 }
141 ret[3] = mn[3];
142 return ret;
143 };
144 return generateImg<Rgba8>(mean.dims(),fun,mt);
145 }
146}
147
148Arr3D calcColorFac(ImgRgba8 const & generic,ImgRgba8 const & facegen,ImgUC const & sampleRegion)
149{
150 FGASSERT(!generic.empty() && !facegen.empty() && !sampleRegion.empty());
151 Arr3D numer {0,0,0},
152 denom {0,0,0};
153 AxAffine2F transToIucs = cIrcsToIucs<float>(sampleRegion.dims());
154 for (Iter2UI it{sampleRegion.dims()}; it.valid(); it.next()) {
155 double w = scast<double>(sampleRegion[it()]) / 255;
156 Vec2F iucs = transToIucs * Vec2F{it()};
157 Arr4F g = sampleClampIucs(generic,iucs).m_c,
158 f = sampleClampIucs(facegen,iucs).m_c;
159 for (size_t cc=0; cc<3; ++cc) {
160 double gd = g[cc];
161 numer[cc] += f[cc] * gd * w;
162 denom[cc] += sqr(gd) * w;
163 }
164 }
165 return mapDiv(numer,denom);
166}
167
168ImgRgba8 applyColorFac(ImgRgba8 const & genericMap,Arr3D colorFac)
169{
170 auto fn = [=](Rgba8 in)
171 {
172 Rgba8 ret {0,0,0,255};
173 for (size_t cc=0; cc<3; ++cc)
174 ret[cc] = scast<uchar>(cMin(colorFac[cc]*in[cc],255.0));
175 return ret;
176 };
177 return mapCall(genericMap,fn);
178}
179
180Arr3D calcColorShift(ImgRgba8 const & generic,ImgRgba8 const & facegen,ImgUC const & transition)
181{
182 FGASSERT(!generic.empty() && !facegen.empty() && !transition.empty());
183 // Accumulate colors over transition region using transition weighting:
184 Arr3D accDelta {0};
185 double accWgt = 0;
186 AxAffine2F transToIucs = cIrcsToIucs<float>(transition.dims());
187 for (Iter2UI it{transition.dims()}; it.valid(); it.next()) {
188 double trans = scast<double>(transition[it()]) / 255,
189 wgt = trans * (1-trans);
190 Vec2F iucs = transToIucs * Vec2F{it()};
191 Arr4F b = sampleClampIucs(generic,iucs).m_c,
192 f = sampleClampIucs(facegen,iucs).m_c;
193 for (size_t cc=0; cc<3; ++cc)
194 accDelta[cc] += (f[cc]-b[cc]) * wgt;
195 accWgt += wgt;
196 }
197 return accDelta / accWgt;
198}
199
200ImgRgba8 applyColorShift(ImgRgba8 const & img,Arr3D shift)
201{
202 Arr3I shifti = mapRound<int>(shift);
203 auto fn = [shifti](Rgba8 in)
204 {
205 return Rgba8 {
206 scast<uchar>(clamp(int(in[0]) + shifti[0],0,255)),
207 scast<uchar>(clamp(int(in[1]) + shifti[1],0,255)),
208 scast<uchar>(clamp(int(in[2]) + shifti[2],0,255)),
209 255
210 };
211 };
212 return mapCall(img,fn);
213}
214
215Scm3::Scm3(String8 const & dirBase,String8 const & overlayColor)
216{
217 if (!overlayColor.empty()) {
218 String8 dbo = dirBase+"_"+overlayColor;
219 Strings exts = findExts(dbo,getImgExts());
220 if (!exts.empty())
221 overlay = loadImage(dbo+"."+exts[0]);
222 }
223 Strings imgExts = findExts(dirBase,getImgExts());
224 if (imgExts.empty())
225 return;
226 // TODO: refactor model sets to not rely on empty SCM3 creation (ie. Modeller):
227 // fgThrow("SCM could not find image file starting with",dirBase);
228 mean = loadImage(dirBase+"."+imgExts[0]);
229 if (pathExists(dirBase+".egt")) {
230 modes = loadEgt(dirBase+".egt").symm;
231 if (mean.width()*modes[0].mode.height() != mean.height()*modes[0].mode.width())
232 fgThrow("Aspect ratio of base image and EGT does not match",dirBase);
233 }
234 if (pathExists(dirBase+".fim")) {
235 detailXfm = loadFim(dirBase+".fim");
236 if (mean.width()*detailXfm.height() != mean.height()*detailXfm.width())
237 fgThrow("Aspect ratio of base and FIM images do not match",dirBase);
238 }
239}
240
241Scm3::Scm3(ImgRgba8 const & meanClr,String8 const & egtFile,String8 const & fimFile) :
242 mean(meanClr)
243{
244 modes = loadEgt(egtFile).symm;
245 if (mean.width() * modes[0].mode.height() !=
246 mean.height() * modes[0].mode.width())
247 fgThrow("Aspect ratio of base colour image does not match EGT",egtFile);
248 if (fimFile.empty())
249 return;
250 detailXfm = loadFim(fimFile);
251 if (mean.width() * detailXfm.height() !=
252 mean.height() * detailXfm.width())
253 fgThrow("Aspect ratio of base colour image does not match FIM",fimFile);
254}
255
256Scm3::Scm3(String8 const & imgFile,String8 const & egtFile,String8 const & fimFile)
257{
258 if (imgFile.empty())
259 return;
260 loadImage_(imgFile,mean);
261 if (egtFile.empty())
262 return;
263 modes = loadEgt(egtFile).symm;
264 if (mean.width() * modes[0].mode.height() !=
265 mean.height() * modes[0].mode.width())
266 fgThrow("Aspect ratio of base colour image does not match EGT",egtFile);
267 if (fimFile.empty())
268 return;
269 detailXfm = loadFim(fimFile);
270 if (mean.width() * detailXfm.height() !=
271 mean.height() * detailXfm.width())
272 fgThrow("Aspect ratio of base colour image does not match FIM",fimFile);
273}
274
275static constexpr uint s_modulationBitShift = 6;
276
277static
278void transformDetailImg(const Img2F & xfmMap,ImgRgba8 const & fgDetail,ImgRgba8 & detail,bool mt)
279{
280 uint detailSizeRatio = fgDetail.width() / 256;
281 if (detailSizeRatio < 1)
282 detailSizeRatio = 1;
283 if (detailSizeRatio > 8) // Do not allow sizes over 4096x4096. Will crash 32-bit and be too slow on 64.
284 detailSizeRatio = 8;
285 detail.resize(xfmMap.dims() * detailSizeRatio);
286 Vec2F maskMap(-1.0f,-1.0f);
287 uchar maskVal = 1 << s_modulationBitShift;
288 AxAffine2F dstIrcsToIucs(
289 Mat22F(-0.5f,float(detail.width())-0.5f,-0.5f,float(detail.height())-0.5f),
290 Mat22F(0,1,0,1));
291 size_t X = detail.width(),
292 Y = detail.height(),
293 nt = cMin(thread::hardware_concurrency(),Y),
294 ydiv = Y / nt,
295 ymod = Y - ydiv*nt,
296 ystart = 0;
297 auto fn = [maskMap,maskVal,dstIrcsToIucs,X,&xfmMap,&fgDetail,&detail](size_t ylo,size_t yhi) {
298 for (size_t yy=ylo; yy<yhi; ++yy) {
299 for (size_t xx=0; xx<X; ++xx) {
300 Vec2F iucs = dstIrcsToIucs * Vec2F(xx,yy);
301 Mat<CoordWgt,2,2> ip = cBlerpClampIucs(xfmMap.dims(),Vec2D{iucs});
302 Vec2F pos {0};
303 float totWgt = 0;
304 for (CoordWgt const & cw : ip.m) {
305 Vec2F v = xfmMap[cw.coordIrcs];
306 if (v != maskMap) {
307 float w = cw.wgt;
308 pos += v * w;
309 totWgt += w;
310 }
311 }
312 Rgba8 & out = detail.xy(xx,yy);
313 if (totWgt < 0.9) {
314 out[0] = maskVal;
315 out[1] = maskVal;
316 out[2] = maskVal;
317 out[3] = 255;
318 }
319 else {
320 pos /= totWgt;
321 // Do bi-linear interpolation:
322 // (itr->x1, itr->x2) is in OGL coordinate format. Convert
323 // it back to raster coordinate format.
324 double rasR = double(fgDetail.height())*(1.0f - pos[1]) - 0.5;
325 double rasC = double(fgDetail.width()) * pos[0] - 0.5;
326 // Calculate the weights for the 4 surrounding pixels:
327 double r = rasR - int(rasR);
328 double c = rasC - int(rasC);
329 if (r < 0.0) r = 1.0 + r;
330 if (c < 0.0) c = 1.0 + c;
331 double r1 = 1.0 - r;
332 double c1 = 1.0 - c;
333 double w0 = r1 * c1;
334 double w1 = r * c1;
335 double w2 = r1 * c;
336 double w3 = r * c;
337 // The four pixels that surrounds this point (yy,xx)
338 uint rowLo = (int(rasR) <= 0) ? 0 : uint(rasR);
339 rowLo = (rowLo <= fgDetail.height()-1) ? \
340 rowLo : fgDetail.height()-1;
341 uint rowHi = (rowLo+1 <= fgDetail.height()-1) ? \
342 rowLo+1 : fgDetail.height()-1;
343 uint colLo = (int(rasC) <= 0) ? 0 : uint(rasC);
344 colLo = (colLo <= fgDetail.width()-1) ? \
345 colLo : fgDetail.width()-1;
346 uint colHi = (colLo+1 <= fgDetail.width()-1) ? \
347 colLo+1 : fgDetail.width()-1;
348 Rgba8 pix[4];
349 pix[0] = fgDetail.xy(colLo,rowLo);
350 pix[1] = fgDetail.xy(colLo,rowHi);
351 pix[2] = fgDetail.xy(colHi,rowLo);
352 pix[3] = fgDetail.xy(colHi,rowHi);
353 for (int cc=0; cc<4; ++cc) {
354 double val = (double)pix[0][cc] * w0
355 + (double)pix[1][cc] * w1
356 + (double)pix[2][cc] * w2
357 + (double)pix[3][cc] * w3;
358 out[cc] = (unsigned char)(val + 0.5);
359 }
360 }
361 }
362 }
363 };
364 if (mt) {
365 Threads threads; threads.reserve(nt);
366 for (size_t tt=0; tt<nt; ++tt) {
367 size_t sz = ydiv;
368 if (tt<ymod)
369 ++sz;
370 threads.emplace_back(fn,ystart,ystart+sz);
371 ystart += sz;
372 }
373 for (thread & t : threads)
374 t.join();
375 }
376 else
377 fn(0,Y);
378}
379
380ImgRgba8 Scm3::transformDetail(Bytes const & detail,bool multithread) const
381{
382 ImgRgba8 ret;
383 if (!detail.empty() && !detailXfm.empty()) {
384 ImgRgba8 detailInternal;
385 try {
386 detailInternal = decodeJpeg(detail);
387 }
388 // Perhaps we ran out of memory, perhaps the JFIF was corrupted.
389 // In either case, ignore detail texture:
390 catch (const FgException &) {
391 fgout << fgnl << "WARNING: JFIF error.";
392 return ret;
393 }
394 if ((detailInternal.width() != detailInternal.height()) ||
395 (!isPow2(detailInternal.width())))
396 fgThrow("Detail texture has unexpected dimensions");
397 transformDetailImg(detailXfm,detailInternal,ret,multithread);
398 double detailGammaVal = 1.45 / gamma;
399 double fac = 1ULL << s_modulationBitShift;
400 uchar detailGammaLut[256];
401 for (uint xx=0; xx<256; ++xx) {
402 double dtmp = fac * pow((double)xx / fac, detailGammaVal) + 0.5;
403 if (dtmp > 255.0) dtmp = 255.0;
404 detailGammaLut[xx] = (uchar)dtmp;
405 }
406 Rgba8 *ptr = ret.dataPtr();
407 size_t sz = ret.numPixels();
408 for (size_t ii=0; ii<sz; ++ii) {
409 ptr[ii][0] = detailGammaLut[ptr[ii][0]];
410 ptr[ii][1] = detailGammaLut[ptr[ii][1]];
411 ptr[ii][2] = detailGammaLut[ptr[ii][2]];
412 }
413 }
414 return ret;
415}
416
418 Floats const & colorCoordS,
419 ImgRgba8 const & transformedDetail,
420 float detailModulation,
421 bool multithread)
422 const
423{
424 ImgV3F deltaMap = scmMac(modes,colorCoordS);
425 ImgRgba8 colorMap = scmAdd(mean,deltaMap,gamma,multithread),
426 detailMap = imgModulate(colorMap,transformedDetail,detailModulation,multithread);
427 return applyOverlay(detailMap,overlay);
428}
429
430ImgRgba8 applyOverlay(ImgRgba8 const & base,ImgRgba8 const & overlay)
431{
432 if (base.empty())
433 return overlay;
434 if (overlay.empty())
435 return base;
436 if (!isPow2(overlay.dims())) {
437 fgout << "WARNING: Overlay image not used; not an power of 2 size multiple";
438 return base;
439 }
440 else {
441 if (base.dims()[0] < overlay.dims()[0]) {
442 ImgRgba8 om = scaleResample(base,overlay.dims());
443 return composite(overlay,om);
444 }
445 else if (base.dims()[0] > overlay.dims()[0]) {
446 ImgRgba8 ov = scaleResample(overlay,base.dims());
447 return composite(ov,base);
448 }
449 else {
450 return composite(overlay,base);
451 }
452 }
453}
454
455void cmd3BasisEgt(CLArgs const & args)
456{
457 Syntax syn(args,"<in>.egt <name>\n"
458 " Output files will be saved as <name>_<rows>_<cols>_<symm>_<modeNum>.txt where:\n"
459 " * <rows> and <cols> are the image dimensions (all identical)\n"
460 " * <symm> is 'symm' or 'asym' for the saggitall symmetry of the mode\n"
461 " * <modeNum> is the PCA mode stored to this file\n"
462 " Contains signed floating point numbers in order RGB for each pixel, scaled for [0,255] "
463 "channels, in raster order, all separated by spaces"
464 );
465 Scm3SA egt = loadEgt(syn.next());
466 FGASSERT(!egt.symm.empty());
467 Vec2UI dims = egt.symm[0].mode.dims();
468 string name = syn.next() + "_" + toStr(dims[1]) + "_" + toStr(dims[0]) + "_";
469 for (uint ss=0; ss<2; ++ss) {
470 bool symm = (ss==0);
471 string symmStr = (symm ? "symm_" : "asym_");
472 Scm3Modes const & modes = (symm ? egt.symm : egt.asym);
473 size_t cnt = 0;
474 for (Scm3Mode const & mode : modes) {
475 Ofstream ofs {name + symmStr + toStrDigits(cnt++,2) + ".txt"};
476 for (Arr3SC p : mode.mode.m_data)
477 for (uint ii=0; ii<3; ++ii)
478 ofs << mode.scale * scast<float>(p[ii]) << ' ';
479 }
480 }
481}
482
483ImgUC getSkinSampleRegion(Img2F const & detailXfm)
484{
485 ImgUC skinSampleRegionInternal = toUC(loadImage(dataDir()+"main/InternalBaseFaceSkinSample.png"));
486 return resampleMap(detailXfm,skinSampleRegionInternal);
487}
488
489Volume<Arr3F> loadEgtScm4(String8 const & egtFile,size_t padZero)
490{
491 Scm3Modes oldModes = loadEgt(egtFile).symm;
492 FGASSERT(!oldModes.empty());
493 Vec2UI dims = oldModes[0].mode.dims();
494 auto fnModes = [&]()
495 {
496 size_t X = dims[0],
497 Y = dims[1],
498 M = oldModes.size();
499 Svec<Arr3F> ret; ret.reserve(X*Y*(M+padZero));
500 for (size_t yy=0; yy<Y; ++yy) {
501 for (size_t xx=0; xx<X; ++xx) {
502 for (size_t mm=0; mm<M; ++mm) {
503 Scm3Mode const & mode = oldModes[mm];
504 Arr3F rgb = mapCast<float>(mode.mode.xy(xx,yy));
505 ret.push_back(rgb * (mode.scale * (1.0f/255.0f)));
506 }
507 for (size_t mm=0; mm<padZero; ++mm)
508 ret.push_back(Arr3F{0});
509 }
510 }
511 return ret;
512 };
513 Vec3UI volDims {scast<uint>(oldModes.size()+padZero),dims[0],dims[1]}; // Volume expects X,Y,Z
514 return {volDims,fnModes()};
515}
516
517void multAcc_(Volume<Arr3F> const & modes,Floats const & coeffs,Img3F & ret)
518{
519 Vec3UI dims = modes.dims();
520 size_t X = dims[0],
521 Y = dims[1],
522 Z = dims[2];
523 FGASSERT((ret.width() == Y) && (ret.height() == Z));
524 Arr3F const *srcPtr = modes.dataPtr();
525 Arr3F *dstPtr = ret.dataPtr();
526 for (size_t zz=0; zz<Z; ++zz) {
527 for (size_t yy=0; yy<Y; ++yy) {
528 Arr3F acc{0};
529 for (size_t xx=0; xx<X; ++xx)
530 acc += srcPtr[xx] * coeffs[xx];
531 dstPtr[yy] += acc;
532 srcPtr += X;
533 }
534 dstPtr += Y;
535 }
536}
537
538Img3F multAcc(Volume<Arr3F> const & modes,Floats const & coeffs)
539{
540 Vec3UI dims = modes.dims();
541 Img3F ret {dims[1],dims[2],Arr3F{0}};
542 multAcc_(modes,coeffs,ret);
543 return ret;
544}
545
546ScbLpp::ScbLpp(String8 const & dirBase)
547{
548 Strings exts = findExts(dirBase,getImgExts());
549 if (exts.empty())
550 fgThrow("cannot find base image for ScbLpp",dirBase);
551 ImgRgba8 oldBase = loadImage(dirBase+"."+exts[0]);
552 auto fnBase = [](Rgba8 p) {return Arr3F{p[0]/255.0f,p[1]/255.0f,p[2]/255.0f}; };
553 base = mapCall(oldBase,fnBase);
554 modes = loadEgtScm4(dirBase+".egt");
555}
556
557ImgRgba8 ScbLpp::apply(Floats const & coord,float gamma) const
558{
559 Img3F imgF = base + multAcc(modes,coord);
560 auto fn = [gamma](Arr3F const & a)
561 {
562 Rgba8 ret;
563 for (size_t ii=0; ii<3; ++ii) {
564 float b = clamp(a[ii],0.0f,1.0f),
565 c = pow(b,2.6f/gamma) * (256.0f - 1.0f/1024.0f);
566 ret[ii] = scast<uchar>(c);
567 }
568 ret[3] = 255;
569 return ret;
570 };
571 return mapCall(imgF,fn);
572}
573
574void testScm3(CLArgs const &)
575{
576 Scm3 scm3 {dataDir()+"csam/Animate/Head/HeadHires"};
577 Face3 zuri {dataDir()+"main/Zuri.fg"};
578 ImgRgba8 img0;
579 {
580 PushTimer pt {"SCM3 original modes application to 512x512"};
581 img0 = scm3.applyFaceCoord(zuri.coord.ts(1,0),{},1.0f,false); // run single-threaded for 1-1 speed test
582 }
583 testRegressApprox<ImgRgba8>(img0,"main/tests/scm3-apply-zuri.png");
584 ScbLpp scm4 {dataDir()+"csam/Animate/Head/HeadHires"};
585 ImgRgba8 img1;
586 {
587 PushTimer pt {"SCM4 modes application to 512x512"};
588 img1 = scm4.apply(zuri.coord.crd.rc(1,0),2.2);
589 }
590 testRegressApprox<ImgRgba8>(img1,"main/tests/scm4-apply-zuri.png");
591 //viewImages({img0,img1});
592}
593
594void cmdScm3Base(CLArgs const & args)
595{
596 Syntax syn {args,
597 R"(<name> <FG-face>.<img> <custom>.<img> [<FG-part>.<img> <custom>.<img>]+
598 <name> - root name for new base images.
599 <FG-face> - the FaceGen-created base color map containing the face.
600 <custom> - your corresponding custom base color map. Must a square, power-of-2 pixel size.
601 <img> - )" + clOptionsStr(getImgExts()) + R"(
602 <FG-part> - FaceGen-created base color map.
603NOTES:
604 * Combines your body color maps with FaceGen SCMs created using the 'scm' command.
605 * You should have an SCM (.<img>, .egt) for each color map on the body which contains skin.
606 * <FG-face>_fade.jpg must exist. You should create this from <FG-face>_fade-gen.jpg which is
607 generated automatically during creation of the <FG-face> SCM by the the 'fg3t scm' command.
608 White areas will be filled from your custom base image, black from the FaceGen base image, and
609 greyscale is for blending between the two.
610 * For each pair of images, an output image '<name>-<custom>.jpg' will be created as a new
611 base image for the SCM.
612 * The resulting maps will remain seamless (in color transition) if the originals were seamless.
613 * This command can be used a runtime to combine your SCM with different base maps, or it can be
614 used in advance to replace your SCM mean color map.
615)"
616 };
617 String root = syn.next();
618 Svec<pair<String,String>> maps; // First is face
619 do {
620 String fgen = syn.next(),
621 cust = syn.next(); // Don't combine with below to ensure ordered
622 maps.push_back(make_pair(fgen,cust));
623 } while (syn.more());
624 Arr3D colorShift {0}; // Assigned in first iteration below
625 for (size_t ii=0; ii<maps.size(); ++ii) {
626 String const & fgName = maps[ii].first,
627 custName = maps[ii].second;
628 ImgRgba8 fgBase = loadImage(fgName),
629 custBase = loadImage(custName),
630 newBase;
631 if (custBase.dims()[0] != custBase.dims()[1])
632 syn.error("The new base image dimensions must be square",custName);
633 if (!isPow2(custBase.dims()[0]))
634 syn.error("The new base image dimensions must be a power of 2",custName);
635 if (ii == 0) {
636 ImgRgba8 faceFade4 = loadImage(pathToBase(fgName)+"_fade.jpg");
637 ImgUC faceFade = mapCall(faceFade4,[](Rgba8 p){return p[0];});
638 colorShift = calcColorShift(custBase,fgBase,faceFade);
639 newBase = customizeBase(fgBase,custBase,faceFade,colorShift);
640 }
641 else
642 newBase = applyColorShift(custBase,colorShift);
643 saveJfif(newBase,root+'-'+pathToBase(custName)+".jpg");
644 }
645}
646
647}
ImgRgba8 transformDetail(Bytes const &detailJpegBlob, bool multithread=true) const
Definition Fg3Scm.cpp:380
float gamma
Desired inverse gamma value for output color maps:
Definition Fg3Scm.hpp:85
ImgRgba8 overlay
Definition Fg3Scm.hpp:90
Img2F detailXfm
Definition Fg3Scm.hpp:83
ImgRgba8 mean
Definition Fg3Scm.hpp:77
ImgRgba8 applyFaceCoord(Floats const &ColorCoordS, ImgRgba8 const &transformedDetail, float detailModulation=1.0f, bool multithread=true) const
Returns the final color map for the given face coordinate and optional transformed detail texture:
Definition Fg3Scm.cpp:417
Scm3Modes modes
SCM modes (symmetric only) encoded with gamma=1/2.6. Size is power of 2. Can be empty.
Definition Fg3Scm.hpp:79