12#include "FgFileSystem.hpp"
13#include "FgImgDisplay.hpp"
14#include "FgCommand.hpp"
16#include "FgTestUtils.hpp"
23bool checkHeader(istream & is,
string val)
26 hdr.resize(val.size());
27 is.read(&hdr[0],hdr.size());
31Scm3SA loadEgt(String8
const & egtFile)
34 Ifstream ifs(egtFile);
35 if (!checkHeader(ifs,
"FREGT003"))
36 fgThrow(
"File is not an .EGT file",egtFile);
43 readBinRaw_(ifs,symm);
44 readBinRaw_(ifs,asym);
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);
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});
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];
70Img2F loadFim(String8
const & fimFile)
73 Ifstream ifs(fimFile);
74 if (!checkHeader(ifs,
"FIMFF001"))
75 fgThrow(
"File is not an .FIM file",fimFile);
82 size_t numPix = scast<size_t>(wid) * hgt;
83 ifs.read(
reinterpret_cast<char*
>(ret.dataPtr()),numPix*
sizeof(Vec2F));
87ImgV3F scmMac(Scm3Modes
const & modes,Floats
const & coeffs)
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();
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;
108ImgRgba8 scmAdd(ImgRgba8
const & mean,ImgV3F
const & delta,
float outInvGamma,
bool mt)
112 float outRelGamma = 2.6f / outInvGamma;
114 auto fun = [&mean,outRelGamma](
size_t xx,
size_t yy)
116 Arr4UC mn = mean.xy(xx,yy).m_c;
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);
125 return generateImg<Rgba8>(mean.dims(),fun,mt);
128 AxAffine2F meanIrcsToIucs = cIrcsToIucs<float>(mean.dims());
129 auto fun = [outRelGamma,meanIrcsToIucs,&mean,&delta](
size_t xx,
size_t yy)
131 Arr4UC mn = mean.xy(xx,yy).m_c;
132 Vec2F iucs = meanIrcsToIucs * Vec2F(xx,yy);
133 Arr3F del = sampleClampIucs(delta,iucs).m;
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);
144 return generateImg<Rgba8>(mean.dims(),fun,mt);
148Arr3D calcColorFac(ImgRgba8
const & generic,ImgRgba8
const & facegen,ImgUC
const & sampleRegion)
150 FGASSERT(!generic.empty() && !facegen.empty() && !sampleRegion.empty());
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) {
161 numer[cc] += f[cc] * gd * w;
162 denom[cc] += sqr(gd) * w;
165 return mapDiv(numer,denom);
168ImgRgba8 applyColorFac(ImgRgba8
const & genericMap,Arr3D colorFac)
170 auto fn = [=](Rgba8 in)
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));
177 return mapCall(genericMap,fn);
180Arr3D calcColorShift(ImgRgba8
const & generic,ImgRgba8
const & facegen,ImgUC
const & transition)
182 FGASSERT(!generic.empty() && !facegen.empty() && !transition.empty());
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;
197 return accDelta / accWgt;
200ImgRgba8 applyColorShift(ImgRgba8
const & img,Arr3D shift)
202 Arr3I shifti = mapRound<int>(shift);
203 auto fn = [shifti](Rgba8 in)
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)),
212 return mapCall(img,fn);
215Scm3::Scm3(String8
const & dirBase,String8
const & overlayColor)
217 if (!overlayColor.empty()) {
218 String8 dbo = dirBase+
"_"+overlayColor;
219 Strings exts = findExts(dbo,getImgExts());
221 overlay = loadImage(dbo+
"."+exts[0]);
223 Strings imgExts = findExts(dirBase,getImgExts());
228 mean = loadImage(dirBase+
"."+imgExts[0]);
229 if (pathExists(dirBase+
".egt")) {
230 modes = loadEgt(dirBase+
".egt").symm;
232 fgThrow(
"Aspect ratio of base image and EGT does not match",dirBase);
234 if (pathExists(dirBase+
".fim")) {
237 fgThrow(
"Aspect ratio of base and FIM images do not match",dirBase);
241Scm3::Scm3(ImgRgba8
const & meanClr,String8
const & egtFile,String8
const & fimFile) :
244 modes = loadEgt(egtFile).symm;
245 if (
mean.width() *
modes[0].mode.height() !=
247 fgThrow(
"Aspect ratio of base colour image does not match EGT",egtFile);
253 fgThrow(
"Aspect ratio of base colour image does not match FIM",fimFile);
256Scm3::Scm3(String8
const & imgFile,String8
const & egtFile,String8
const & fimFile)
260 loadImage_(imgFile,
mean);
263 modes = loadEgt(egtFile).symm;
264 if (
mean.width() *
modes[0].mode.height() !=
266 fgThrow(
"Aspect ratio of base colour image does not match EGT",egtFile);
272 fgThrow(
"Aspect ratio of base colour image does not match FIM",fimFile);
275static constexpr uint s_modulationBitShift = 6;
278void transformDetailImg(
const Img2F & xfmMap,ImgRgba8
const & fgDetail,ImgRgba8 & detail,
bool mt)
280 uint detailSizeRatio = fgDetail.width() / 256;
281 if (detailSizeRatio < 1)
283 if (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),
291 size_t X = detail.width(),
293 nt = cMin(thread::hardware_concurrency(),Y),
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});
304 for (CoordWgt
const & cw : ip.m) {
305 Vec2F v = xfmMap[cw.coordIrcs];
312 Rgba8 & out = detail.xy(xx,yy);
324 double rasR = double(fgDetail.height())*(1.0f - pos[1]) - 0.5;
325 double rasC = double(fgDetail.width()) * pos[0] - 0.5;
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;
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;
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);
365 Threads threads; threads.reserve(nt);
366 for (
size_t tt=0; tt<nt; ++tt) {
370 threads.emplace_back(fn,ystart,ystart+sz);
373 for (thread & t : threads)
383 if (!detail.empty() && !
detailXfm.empty()) {
384 ImgRgba8 detailInternal;
386 detailInternal = decodeJpeg(detail);
390 catch (
const FgException &) {
391 fgout << fgnl <<
"WARNING: JFIF error.";
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;
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]];
418 Floats
const & colorCoordS,
419 ImgRgba8
const & transformedDetail,
420 float detailModulation,
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);
430ImgRgba8 applyOverlay(ImgRgba8
const & base,ImgRgba8
const & overlay)
436 if (!isPow2(overlay.dims())) {
437 fgout <<
"WARNING: Overlay image not used; not an power of 2 size multiple";
441 if (base.dims()[0] < overlay.dims()[0]) {
442 ImgRgba8 om = scaleResample(base,overlay.dims());
443 return composite(overlay,om);
445 else if (base.dims()[0] > overlay.dims()[0]) {
446 ImgRgba8 ov = scaleResample(overlay,base.dims());
447 return composite(ov,base);
450 return composite(overlay,base);
455void cmd3BasisEgt(CLArgs
const & args)
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"
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) {
471 string symmStr = (symm ?
"symm_" :
"asym_");
472 Scm3Modes
const & modes = (symm ? egt.symm : egt.asym);
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]) <<
' ';
483ImgUC getSkinSampleRegion(Img2F
const & detailXfm)
485 ImgUC skinSampleRegionInternal = toUC(loadImage(dataDir()+
"main/InternalBaseFaceSkinSample.png"));
486 return resampleMap(detailXfm,skinSampleRegionInternal);
489Volume<Arr3F> loadEgtScm4(String8
const & egtFile,
size_t padZero)
491 Scm3Modes oldModes = loadEgt(egtFile).symm;
492 FGASSERT(!oldModes.empty());
493 Vec2UI dims = oldModes[0].mode.dims();
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)));
507 for (
size_t mm=0; mm<padZero; ++mm)
508 ret.push_back(Arr3F{0});
513 Vec3UI volDims {scast<uint>(oldModes.size()+padZero),dims[0],dims[1]};
514 return {volDims,fnModes()};
517void multAcc_(Volume<Arr3F>
const & modes,Floats
const & coeffs,Img3F & ret)
519 Vec3UI dims = modes.dims();
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) {
529 for (
size_t xx=0; xx<X; ++xx)
530 acc += srcPtr[xx] * coeffs[xx];
538Img3F multAcc(Volume<Arr3F>
const & modes,Floats
const & coeffs)
540 Vec3UI dims = modes.dims();
541 Img3F ret {dims[1],dims[2],Arr3F{0}};
542 multAcc_(modes,coeffs,ret);
546ScbLpp::ScbLpp(String8
const & dirBase)
548 Strings exts = findExts(dirBase,getImgExts());
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");
557ImgRgba8 ScbLpp::apply(Floats
const & coord,
float gamma)
const
559 Img3F imgF = base + multAcc(modes,coord);
560 auto fn = [gamma](Arr3F
const & a)
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);
571 return mapCall(imgF,fn);
574void testScm3(CLArgs
const &)
576 Scm3 scm3 {dataDir()+
"csam/Animate/Head/HeadHires"};
577 Face3 zuri {dataDir()+
"main/Zuri.fg"};
580 PushTimer pt {
"SCM3 original modes application to 512x512"};
581 img0 = scm3.applyFaceCoord(zuri.coord.ts(1,0),{},1.0f,
false);
583 testRegressApprox<ImgRgba8>(img0,
"main/tests/scm3-apply-zuri.png");
584 ScbLpp scm4 {dataDir()+
"csam/Animate/Head/HeadHires"};
587 PushTimer pt {
"SCM4 modes application to 512x512"};
588 img1 = scm4.apply(zuri.coord.crd.rc(1,0),2.2);
590 testRegressApprox<ImgRgba8>(img1,
"main/tests/scm4-apply-zuri.png");
594void cmdScm3Base(CLArgs
const & 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.
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.
617 String root = syn.next();
618 Svec<pair<String,String>> maps;
620 String fgen = syn.next(),
622 maps.push_back(make_pair(fgen,cust));
623 }
while (syn.more());
624 Arr3D colorShift {0};
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),
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);
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);
642 newBase = applyColorShift(custBase,colorShift);
643 saveJfif(newBase,root+
'-'+pathToBase(custName)+
".jpg");
ImgRgba8 transformDetail(Bytes const &detailJpegBlob, bool multithread=true) const
float gamma
Desired inverse gamma value for output color maps:
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:
Scm3Modes modes
SCM modes (symmetric only) encoded with gamma=1/2.6. Size is power of 2. Can be empty.