当前位置: 首页 > news >正文

用【WPF+Dlib68】实现 侧脸 眼镜虚拟佩戴 - 用平面图表现空间视觉

目录

前言

眼镜3D模型的尝试

准备平面眼镜素材

在WPF中使用3D控件

3D控件的正面效果

Dlib获取人脸68点

预测人脸旋转角度           

3D控件的Y轴旋转效果

眼镜色彩校准

扩展到面具佩戴

总结

下载


前言

在 https://blog.csdn.net/LateFrames/article/details/152209080?spm=1001.2014.3001.5501 这篇文章中,我基于Dlib识别到的人脸68点,将平面眼镜图片与人物图像进行了简单的虚拟佩戴,它仅适用于 正面(包含正面歪头),但是不支持 侧脸 姿态,因为 侧脸 时脸部的空间形态变化会导致面部关键点不对称,无法直接匹配。

本文仍然基于Dlib68点,对眼镜的佩戴进行了进一步尝试,研究在WPF中如何以一种在空间形态上与面部能够更好融合的方案。


眼镜3D模型的尝试

WPF中 对3D模型的支持非常好, 因此我首先想到的方案是,在实时的图像帧上层,叠加3D模型,通过68个关键点来大致判断面部姿态的旋转方位角度,那么首先我要解决的是,要有一个理想的眼镜模型,这一步就把我卡住了,我想从3D模型网站上下载一些好看的模型,结果发现精致的模型价格竟然如此昂贵!一个眼镜模型人民币几百块钱非常常见:

如果以该方案落地,那么眼镜模型的制作成本和周期,将是一个非常大的阻碍。我暂且先找一个免费的、 不太精致的模型初步尝试一下。由于我对面部的68点并没有进行面部网格的3D重建,它们只是平面特征,因此对于 眼镜腿 部分的构造,我是不需要的,但是不容易找不到这样的模型。先假设使用下面这个模型:

我们可以看到,模型的光感很好,但是这多余的眼镜腿,对于不会制作模型的我来说,实在不知道拿它怎么办,以及模型的尺寸、人像平面投影的对齐方式,这里应该是有些复杂的,因此我暂时不深入研究这种方案,跳过。


准备平面眼镜素材

既然不使用3D模型, 那只有使用平面图了,如何让平面图产生 3D的感觉呢?这里我用到的方法是:建立一个立方体模型,把 眼镜图 贴到模型的 正前面,然后根据面部的大致旋转角度,来控制眼镜的旋转角度,这里我只处理该模型的 Y轴 旋转角度,不处理 X轴 的旋转角度,也就是只处理 摇头 方位变化,不处理 点头 方位变化。

接下来,我要找到理想的眼镜图,从百度搜索眼镜图,可以得到大量的真实的眼镜图片,但是 基本都不是理想的 正视角 方位,而且大多包含了眼镜腿,不能拿来直接用,有一些卡通风格的眼镜虽然可以直接用,但是看起来太假了,与真实人像融合到一起违和感太重,因此也不考虑:

那么我就只有把真实的眼镜图加工一下再用了,我找到了一些 正面角度 的 不受眼镜腿干扰 的更真实的图片,比如下面这些 :

我需要把 眼镜腿 P掉,还要确保 眼镜片 区域是半透明的,可是搜索到的图片,分辨率比较低,很不清晰,为了使用更高质量的眼镜图片,百度搜索出来的图片,我使用它自带的 “变清晰”功能:

它支持的AI图像处理真的非常强大,很实用。变清晰之后,可以看到质量非常好的高清眼镜图片:

但是丢失了透明度,点击“智能抠图”, 处理完成后下载即可:

下载下来的图片,需要进行以下处理:

  1. 精准的把 眼镜腿 部位移除;
  2. 对 眼镜片 区域进行半透明处理;
  3. 已抠好的图像边缘过渡性不太好,略微有白边,需要进行少量像素的羽化处理(这样与实际图像融合时更自然);
  4. 下载的眼镜图片,由于实际的拍摄角度差异,有可能两个眼镜片的中心在水平方向是不对齐的,需要对整张图像进行 细微 角度的调整;

这个时候PS要登场了, 但是PS软件太大了,我电脑没有安装,我尝试了网页的PS ,不太好用,没有处理为符合我需求的图片,因此我写了下面的小工具,它 支持 对指定的图片,模糊选取、精准圆形选取、精准矩形选取、多边形选取、全部选取、边缘羽化、指定颜色羽化、选区透明度设置、图片旋转角度,真是自己动手,风衣足食,功能精准又够用):

在多边形选取模式下,把眼镜腿区域删除,然后按住Shift键,鼠标点选眼镜片区域,并适当调整容差,使选区与边缘分割开来:

然后将透明度设置到大概150左右,可以看到眼镜片透出了背景的绿色,把两个眼镜片都进行相同的透明度调整后存储即可。

按照 https://blog.csdn.net/LateFrames/article/details/152209080?spm=1001.2014.3001.5501 这篇文章的思路继续,我仍然对眼镜的中心点进行标定后再使用,将眼睛的中心点定位在偏上,且偏内:


在WPF中使用3D控件

此时眼镜图已就绪,这时候需要把它贴到一个立体模型的正面。为了能够比较准确的控制3D模型的尺寸与实际平面图的位置对齐, 需要确保3D模型的展示尺寸与贴图的尺寸基本上要符合1:1的比例,绝对禁止因为摄像机距离的不同而导致眼镜的视觉尺寸远小于整个3D控件尺寸,也就是说,默认尺寸下,眼睛看到的3D模型的尺寸需要与2D图像的展示尺寸几乎一样, 这样在与识别的眼中心对齐的时候才能更容易控制。

因此我使用固定的相机位置:

<Viewport3D.Camera>
    <PerspectiveCamera Position="0,0,1" LookDirection="0,0,-1" UpDirection="0,1,0" FieldOfView="45"/>
</Viewport3D.Camera>

然后把眼镜图片贴图到 正前面,在进行眼睛贴图的时候 ,为了让眼镜模型与实际人脸图像的眼睛对齐,首先根据检测到的 实际两眼中心距 ,和 眼镜标定的两眼中心距 进行缩放比例计算,然后把眼镜图片按照该比例进行缩放,然后将眼镜图片贴到模型正前面,并确保1:1显示,如何确保 1:1显示呢?这里有一个非常重要的点:模型中顶点位置的选取,以及贴图的尺寸,需要完全填充铺满,理论上来说,我们需要根据眼镜图的宽高比例的不同来动态设置模型中的顶点位置,但是这会涉及到模型的显示尺寸与人脸图像的 对齐问题,这里非常可能产生差异,我经过几次转换总是对不齐,因此我按照相机的 固定位置、固定视角范围:

<PerspectiveCamera Position="0,0,1" LookDirection="0,0,-1" UpDirection="0,1,0" FieldOfView="45"/>

当默认铺满时,MeshGeometry3D点位置大致为:

Positions="-0.41418,-0.41418,0  0.41418,-0.41418,0  0.41418,0.41418,0  -0.41418,0.41418,0"

但这个面是正方形,当眼镜图片是正方形或横长的矩形时适用,但是当如果图片为纵长时则不适用,当然,纵长的图片一般不符合眼镜比例,但是可能符合 面具图 比例,因此这里我按照宽高比为 1:5 进行最大兼容(实际我觉得1:3就够用) :

Positions="-0.41418,-2.0709,0  0.41418,-2.0709,0  0.41418,2.0709,0  -0.41418,2.0709,0"

同时,把静止图片作为材质贴上去,使外容器保持1:5的比例,但是实际眼镜图仍然保持自己的比例不变,仍然使用uniform拉伸。

这里的设置有点绕,但是按照这样来进行设置后,在后台代码控制眼镜图片尺寸的时候非常方便,不需要考虑3D空间与2D投影空间转换,只需要更新材质图片路径,以及设置 3D模型控件的宽度和高度即可(这里的宽度和高度是眼镜的实际尺寸,很方便):

<GeometryModel3D.Material>

    <DiffuseMaterial x:Name="ImageMaterial">

        <DiffuseMaterial.Brush>

            <VisualBrush>

                <VisualBrush.Visual>

                    <Border Width="100" Height="500" Background="Transparent" >

                        <Image  x:Name="imgbrushglass"  Stretch="Uniform"   />

                    </Border>

                </VisualBrush.Visual>

            </VisualBrush>

        </DiffuseMaterial.Brush>

    </DiffuseMaterial>

</GeometryModel3D.Material>


3D控件的正面效果

有了 眼镜的 3D模型的控制,理论上来说,正面图(包括歪头的图像)眼镜的佩戴效果是没有明显变化的,例如下面这些平面人像图与3D模型展示融合的效果:  

我们可以看到,真实的眼镜图片进行佩戴之后,看起来效果是非常好的,这个时候 3D模型的使用并没有看出任何的优势, 因为它与2D图片的位置旋转是没有本质差异的 , 但是当 侧脸 的时候,我们就看到了 严重的问题,眼镜明显没有空间感,很假 :  

在距离较远的眼睛位置,镜片飞出去很多,非常不自然,这个时候就需要发挥3D控件的优势了:给它添加旋转角度(这里我们只对模型 Y轴 的旋转角度进行大致预测)。那么,如何根据68个关键点来大致判断人脸 左右转头 的角度呢?


Dlib获取人脸68点

获取到人脸的68个关键点,并标记在人像图中:

using (var img = Dlib.LoadImage<RgbPixel>(tempImagePath))
{ var faces = faceDetector.Operator(img); if (faces.Length > 0){Viewport3D.Visibility = Visibility.Visible;using (Graphics g = Graphics.FromImage(resultImage)){g.SmoothingMode = SmoothingMode.HighQuality;DrawingBrush redBrush = new SolidBrush(DrawingColor.Red);using (DrawingPen facePen = new DrawingPen(DrawingColor.Lime, 2))using (DrawingBrush faceOutlineBrush = new SolidBrush(DrawingColor.Cyan))using (DrawingBrush eyebrowBrush = new SolidBrush(DrawingColor.Yellow))using (DrawingBrush noseBrush = new SolidBrush(DrawingColor.Magenta))using (DrawingBrush eyeBrush = new SolidBrush(DrawingColor.Lime))using (DrawingBrush mouthBrush = new SolidBrush(DrawingColor.HotPink)){foreach (var face in faces){face_0_ps.Clear(); //68个点var shape = predictor.Detect(img, face);var ps_eye_left = new List<DlibDotNet.Point>();var ps_eye_right = new List<DlibDotNet.Point>();for (uint i = 0; i < shape.Parts; i++){var point = shape.GetPart(i);int x = (int)(point.X * scaleX);int y = (int)(point.Y * scaleY);face_0_ps.Add(new System.Drawing.Point(x, y));DrawingBrush pointBrush;if (i <= 16) pointBrush = faceOutlineBrush;else if (i <= 26) pointBrush = eyebrowBrush;else if (i <= 35) pointBrush = noseBrush;else if (i <= 47){if (i <= 41) ps_eye_left.Add(new DlibDotNet.Point(x, y));else ps_eye_right.Add(new DlibDotNet.Point(x, y));pointBrush = eyeBrush;}else pointBrush = mouthBrush;if(draw_key_ps)g.FillEllipse(pointBrush, x - 3, y - 3, 6, 6);}//眼睛的中心点 i:<=47, >36{var eye_center_x = (int)Math.Round(ps_eye_left.Average(a => a.X));var eye_center_y = (int)Math.Round(ps_eye_left.Average(a => a.Y));var eye_center = new System.Drawing.Point(eye_center_x, eye_center_y);pt_center_left_x.Add(eye_center_x);pt_center_left_y.Add(eye_center_y);// 增加平滑窗口从3帧提升到5帧,提高稳定性if (pt_center_left_x.Count > 1){pt_center_left_x.RemoveAt(0);pt_center_left_y.RemoveAt(0);}eye_center_x = (int)Math.Round(pt_center_left_x.Average());eye_center_y = (int)Math.Round(pt_center_left_y.Average());eye_cetner_left = new System.Drawing.Point(eye_center_x, eye_center_y);var rect_eye_center = new RectangleF(eye_center_x, eye_center_y, 0, 0);rect_eye_center.Inflate(5, 5);if (draw_key_ps)g.FillEllipse(redBrush, rect_eye_center);}{var eye_center_x = (int)Math.Round(ps_eye_right.Average(a => a.X));var eye_center_y = (int)Math.Round(ps_eye_right.Average(a => a.Y));var eye_center = new System.Drawing.Point(eye_center_x, eye_center_y);pt_center_right_x.Add(eye_center_x);pt_center_right_y.Add(eye_center_y);// 增加平滑窗口从3帧提升到5帧,提高稳定性if (pt_center_right_x.Count > 1){pt_center_right_x.RemoveAt(0);pt_center_right_y.RemoveAt(0);}eye_center_x = (int)Math.Round(pt_center_right_x.Average());eye_center_y = (int)Math.Round(pt_center_right_y.Average());eye_cetner_right = new System.Drawing.Point(eye_center_x, eye_center_y);var rect_eye_center = new RectangleF(eye_center_x, eye_center_y, 0, 0);rect_eye_center.Inflate(5, 5);if (draw_key_ps)g.FillEllipse(redBrush, rect_eye_center);}//标记特殊 关键点 最接近2耳的位置{var rect_eye_center = new RectangleF(face_0_ps[0].X, face_0_ps[0].Y, 0, 0);rect_eye_center.Inflate(5, 5);if (draw_key_ps)g.FillEllipse(redBrush, rect_eye_center);}{var rect_eye_center = new RectangleF(face_0_ps[16].X, face_0_ps[16].Y, 0, 0);rect_eye_center.Inflate(5, 5);if (draw_key_ps)g.FillEllipse(redBrush, rect_eye_center);}  }}} }else{Viewport3D.Visibility = Visibility.Collapsed; }
}

预测人脸旋转角度           

人的面部表情是非常容易变化的,但是鼻子和眼睛部位的关键点是相对稳定的,尤其是两眼中间的位置:

但是我们可以发现这样的规律:随着头部左右转向角度的不同,两眼中心 与 鼻梁顶部 关键点之间的距离会随之变化,假设我们把 眼中心 到 鼻梁顶点 的2个距离中 较小距离 与 较大距离 之间的比例关系作为 摇头角度 的依据,然后对测试图像手动标记最佳旋转角度,作为原始特征数据,用这种方法我标记了 66 张图数据(完整的数据在文末有链接),下面是其中的5组:

数据格式为: 短边长边比例;左眼中心到鼻梁顶点距离; 右眼中心到鼻梁顶点距离;左右眼中心距;y轴旋转角度;模型偏移X;模型偏移Y;X轴旋转角度;模型缩放比例;68个关键点

0.738;61 : 45;10;5;0;0;1;187:209,190:242,200:273,210:302,229:327,250:347,276:366,301:378,325:380,345:369,361:349,376:329,392:307,405:282,412:255,416:227,412:198,220:185,240:171,263:163,291:163,316:171,354:167,369:158,385:153,401:153,412:163,334:205,338:229,341:255,343:280,316:293,327:295,340:296,349:293,358:286,250:209,265:198,283:198,298:211,281:216,263:216,358:205,369:187,385:184,398:193,389:204,371:205,287:324,307:324,325:320,336:322,343:318,352:317,361:317,352:326,343:333,336:337,325:338,307:335,294:324,325:327,336:327,343:326,356:318,343:324,336:326,325:327
0.867;60 : 52;0;-15;0;0;1;236:180,221:209,207:240,198:273,196:304,201:335,212:362,225:386,245:406,270:415,300:411,329:404,358:393,383:380,403:362,421:344,438:322,283:180,307:174,331:182,349:196,363:214,396:238,418:245,436:256,447:273,449:293,365:255,354:276,345:296,334:317,300:313,307:322,314:331,327:337,338:340,292:214,311:218,323:227,329:244,314:235,301:225,387:278,403:280,416:287,421:298,409:296,396:287,249:315,274:327,294:338,301:346,312:351,321:362,331:369,305:377,289:373,278:368,269:358,256:340,254:320,287:342,296:351,305:357,323:368,300:362,289:357,280:349
0.761;35 : 46;-10;-5;0;0;1.1;141:273,147:296,154:317,165:335,178:351,192:366,205:377,220:386,236:388,252:382,267:371,281:355,294:337,301:315,305:291,305:267,301:242,140:255,147:245,160:244,174:245,187:251,218:245,232:235,249:229,265:225,281:231,205:267,207:286,207:304,209:324,200:331,207:335,214:337,223:333,230:329,156:265,163:258,176:258,187:269,176:273,163:273,232:264,241:249,256:245,267:251,260:260,245:264,200:357,205:353,210:351,218:353,223:349,232:351,245:353,238:364,229:368,221:369,214:368,207:364,203:357,212:357,218:357,225:355,241:353,225:355,220:357,212:357
0.613;38 : 62;-10;0;0;0;1.1;152:138,147:163,147:189,149:216,154:242,163:269,174:293,187:313,209:324,238:326,270:318,301:304,329:287,352:264,369:233,378:200,385:165,154:100,165:91,180:92,192:96,207:105,243:109,267:103,291:103,314:112,332:127,220:142,214:160,207:178,201:198,190:213,198:218,205:222,216:222,229:220,165:136,176:129,190:131,203:147,187:147,172:143,258:154,272:142,291:143,305:156,289:160,272:158,174:249,183:242,194:238,203:242,214:242,234:251,254:264,232:276,210:282,200:280,189:276,180:267,181:251,194:253,203:256,212:256,245:264,212:264,201:262,192:258
0.673;49 : 33;10;0;0;0;1;190:100,198:123,207:147,216:169,229:187,247:204,269:216,292:224,311:224,323:216,334:200,345:184,354:167,360:151,365:131,365:112,365:94,214:80,229:71,249:65,269:67,287:72,320:69,331:63,343:60,354:60,365:65,305:102,309:118,312:132,314:149,294:163,303:163,312:165,318:162,323:158,238:107,249:100,263:100,276:109,263:112,249:112,321:103,331:92,343:91,352:94,347:103,334:105,274:185,291:182,303:178,311:180,316:176,323:178,332:178,325:187,320:194,312:198,303:198,291:196,280:185,303:187,311:187,318:184,329:180,318:184,311:185,303:185

根据这些数据,我发现:短边长边比例最小可取值大约 0.3, Y轴变形度数最大为30~40,这里我取值30,禁止变形过度导致违和感加重;

由此就可以根据左眼中心点(68点中第36 - 41点取值平均点),右眼中心点(68点中第42 - 47点取值平均点)、鼻梁顶部关键点(68点中第28点),来计算头部转向的角度了:

var pctmin = 0.3;

var dgrmax = 30;

var eyeLenScale = Math.Min(dist_eye_tocenter_left, dist_eye_tocenter_right) / (double)Math.Max(dist_eye_tocenter_left, dist_eye_tocenter_right);

eyeLenScale = Math.Max(pctmin, eyeLenScale);

dgrrstY = dgrmax - (eyeLenScale - pctmin) / (double)(1 - pctmin) * dgrmax;

if (dist_eye_tocenter_left < dist_eye_tocenter_right) dgrrstY = -dgrrstY;

dgrrstY = Math.Round(dgrrstY);


3D控件的Y轴旋转效果

处理关键点:

1,根据发现的两眼中心点到鼻梁顶部的距离变化与角度旋转的关系,进行映射转换

2,使用 face_0_ps[27] (鼻梁中心点)作为眼镜定位的基准点

3,通过 pt_dist 计算距离来动态调整眼镜大小

4,使用 get_pt_to_dist 计算沿特定方向的目标点位置

核心代码为:

var model_size_init = new System.Drawing.Size(source_img_size.Width, source_img_size.Height);
var pt_from = eye_cetner_left;
var pt_to = eye_cetner_right;
//两眼中心直线距离
var pdist = pt_dist(pt_from, pt_to);if (File.Exists(selectedGlassesPath))
{if (selectedGlassesPath_lasttryon != selectedGlassesPath){imgbrushglass.Source = new BitmapImage(new Uri(selectedGlassesPath));var imgg = System.Drawing.Image.FromFile(selectedGlassesPath);glass_size_init = imgg.Size;imgg.Dispose();selectedGlassesPath_lasttryon = selectedGlassesPath; }//眼镜标定的两眼中心距离var calibration = glassesCalibrations[selectedGlassesPath];pt_glass_eye_left = calibration.LeftEyeCenter;pt_glass_eye_right = calibration.RightEyeCenter;
}//把眼镜缩放到识别的人眼中心距符合的尺寸
var scl1 = source_img_size.Width / (double)glass_size_init.Width;
var scl2 = source_img_size.Height / (double)glass_size_init.Height;
var sclmin = Math.Min(scl1, scl2);
var glass_size = new System.Drawing.Size((int)Math.Round(glass_size_init.Width * sclmin),(int)Math.Round(glass_size_init.Height * sclmin));
var sclimglarge_glass = glass_size.Width / (double)glass_size_init.Width;
var pt_glass_eye_dist = (double)Math.Abs(pt_glass_eye_left.X - pt_glass_eye_right.X);
var rec_eye_dist = pdist;
var pt_glass_eye_dist_sclto = rec_eye_dist / (double)pt_glass_eye_dist;
var pt_glass_eye_dist_sclto_wd = glass_size_init.Width * pt_glass_eye_dist_sclto;
var pt_glass_eye_dist_sclto_ht = glass_size_init.Height * pt_glass_eye_dist_sclto;//眼镜图所在的模型显示以左、上对齐,默认位置0,0
var move_x = face_0_ps[27].X - (pt_glass_eye_left.X + pt_glass_eye_right.X) / 2 * pt_glass_eye_dist_sclto;
var move_y = face_0_ps[27].Y - pt_glass_eye_left.Y * pt_glass_eye_dist_sclto;var eye_uc_ycenter = move_y + pt_glass_eye_dist_sclto_ht / 2;
mode_moveY = face_0_ps[27].Y - eye_uc_ycenter;
move_y += mode_moveY;var dist_eye_tocenter_left = (int)Math.Round(pt_dist(eye_cetner_left, face_0_ps[27]));
var dist_eye_tocenter_right = (int)Math.Round(pt_dist(eye_cetner_right, face_0_ps[27]));
//判断Y轴旋转角度
var pctmin = 0.3;
var dgrmax = 40;
dgrmax = 30;
var eyeLenScale = Math.Min(dist_eye_tocenter_left, dist_eye_tocenter_right) / (double)Math.Max(dist_eye_tocenter_left, dist_eye_tocenter_right);
eyeLenScale = Math.Max(pctmin, eyeLenScale);
var dgrrstY = 0d;
if (enable_auto_rotatey)
{dgrrstY = dgrmax - (eyeLenScale - pctmin) / (double)(1 - pctmin) * dgrmax;if (dist_eye_tocenter_left < dist_eye_tocenter_right) dgrrstY = -dgrrstY;dgrrstY = Math.Round(dgrrstY);
}
else
{dgrrstY = mode_rotateY;
}//将左上对齐的眼镜模型移动到眼睛都实际位置
var offsetmax = 5;
if (enbale_auto_move_x)
{mode_moveX = Math.Abs(dgrrstY) / (double)dgrmax * offsetmax;if (dgrrstY < 0) mode_moveX = -Math.Round(mode_moveX);else mode_moveX = Math.Round(mode_moveX);
}var set_eye_offsety = (glass_size_init.Height / 2 - pt_glass_eye_left.Y) * pt_glass_eye_dist_sclto;
var eye_offsety_targetpt = get_pt_to_dist(face_0_ps[27], face_0_ps[30], set_eye_offsety);
var eye_offsety_targetpt_offsetx = eye_offsety_targetpt.X - face_0_ps[27].X;
var eye_offsety_targetpt_offsety = eye_offsety_targetpt.Y - face_0_ps[27].Y;
trans_v3d.X = eye_offsety_targetpt_offsetx;
trans_v3d.Y = eye_offsety_targetpt_offsety;//设置歪头时的Z轴旋转角度,由两眼所在直线与水平线的夹角决定
if (enable_auto_rotatez)
{RotationZ.Angle = -latestRoll + mode_rotateZ;
}
elseRotationZ.Angle = mode_rotateZ;RotationY.Angle = dgrrstY;//更新UI
Dispatcher.Invoke(delegate
{tb_eye_left_right_dist.Text = dist_eye_tocenter_left + " : " + dist_eye_tocenter_right;Viewport3D.Width = pt_glass_eye_dist_sclto_wd;Viewport3D.Height = pt_glass_eye_dist_sclto_ht;vb.Width = source_img_size.Width;vb.Height = source_img_size.Height;gridcontent.Width = source_img_size.Width;gridcontent.Height = source_img_size.Height;Viewport3D.Margin = new Thickness(move_x, move_y, 0, 0);tbRotateY.Text = dgrrstY.ToString();});

经过Y轴旋转后的效果:

明显看到了空间变换的效果,虽然与实际的效果仍然有差异,但是比平面图的佩戴看起来自然了很多。不过,有一个奇怪的感觉,有的图片会明显感觉这图是“贴”上去了,因为:一张固定色彩的眼镜图片,在不同亮度、不同对比度的图片中表现是相同的,这是违反常理的,我发现很大的原因在于它们的色调差异太大, 因此,我尝试先检测环境图的色调,再把它应用于眼镜图片,让它们保持色调一致:


眼镜色彩校准

提取背景图色调风格:

/// <summary>
/// 分析实时图像的光照特征
/// </summary>
/// <param name="realTimeImage">实时拍摄的图像</param>
/// <returns>光照信息</returns>
public LightingInfo AnalyzeLighting(System.Drawing.Bitmap realTimeImage)
{if (realTimeImage == null)return new LightingInfo(128, 1.0, ColorTemperature.Neutral, 1.0, 1.0, 1.0, 1.0);try{var bitmapData = realTimeImage.LockBits(new System.Drawing.Rectangle(0, 0, realTimeImage.Width, realTimeImage.Height),System.Drawing.Imaging.ImageLockMode.ReadOnly,System.Drawing.Imaging.PixelFormat.Format24bppRgb);int bytesPerPixel = 3;int byteCount = bitmapData.Stride * realTimeImage.Height;byte[] pixels = new byte[byteCount];System.Runtime.InteropServices.Marshal.Copy(bitmapData.Scan0, pixels, 0, byteCount);realTimeImage.UnlockBits(bitmapData);int sampleStep = Math.Max(1, Math.Min(realTimeImage.Width, realTimeImage.Height) / 200);int[] brightnessHistogram = new int[256];long totalR = 0, totalG = 0, totalB = 0;long sampledPixels = 0;int minR = 255, maxR = 0, minG = 255, maxG = 0, minB = 255, maxB = 0;int highlightCount = 0;int shadowCount = 0;for (int y = 0; y < realTimeImage.Height; y += sampleStep){for (int x = 0; x < realTimeImage.Width; x += sampleStep){int pixelIndex = (y * bitmapData.Stride) + (x * bytesPerPixel);byte b = pixels[pixelIndex];byte g = pixels[pixelIndex + 1];byte r = pixels[pixelIndex + 2];totalR += r;totalG += g;totalB += b;sampledPixels++;minR = Math.Min(minR, r);maxR = Math.Max(maxR, r);minG = Math.Min(minG, g);maxG = Math.Max(maxG, g);minB = Math.Min(minB, b);maxB = Math.Max(maxB, b);int brightness = (r + g + b) / 3;brightnessHistogram[brightness]++;if (brightness > 220) highlightCount++;if (brightness < 35) shadowCount++;}}double avgR = (double)totalR / sampledPixels;double avgG = (double)totalG / sampledPixels;double avgB = (double)totalB / sampledPixels;double avgBrightness = (avgR + avgG + avgB) / 3.0;int medianBrightness = CalculateMedianBrightness(brightnessHistogram, (int)sampledPixels);double highlightRatio = (double)highlightCount / sampledPixels;double shadowRatio = (double)shadowCount / sampledPixels;bool isHighExposure = highlightRatio > 0.10 || avgBrightness > 160;bool isLowExposure = shadowRatio > 0.25 || avgBrightness < 90;double contrast = CalculateHistogramContrast(brightnessHistogram, (int)sampledPixels, avgBrightness);ColorTemperature colorTemp = AnalyzeAdvancedColorTemperature(avgR, avgG, avgB, pixels,bitmapData.Stride, realTimeImage.Width, realTimeImage.Height, sampleStep);double saturation = CalculateAverageSaturation(pixels, bitmapData.Stride,realTimeImage.Width, realTimeImage.Height, sampleStep);double maxChannel = Math.Max(avgR, Math.Max(avgG, avgB));double minChannel = Math.Min(avgR, Math.Min(avgR, avgB));double blendedBrightness = avgBrightness * 0.6 + medianBrightness * 0.4;double toneShiftR = (avgR - 128) / 128.0;double toneShiftG = (avgG - 128) / 128.0;double toneShiftB = (avgB - 128) / 128.0;double warmness = Math.Pow((avgR + avgG * 0.59) / (avgB + 1), 0.8);double coolness = Math.Pow(avgB / (avgR * 0.7 + avgG * 0.3 + 1), 0.8);double redChannel = maxChannel > 10 ? Math.Max(0.4, Math.Min(1.6, avgR / maxChannel)) : 1.0;double greenChannel = maxChannel > 10 ? Math.Max(0.4, Math.Min(1.6, avgG / maxChannel)) : 1.0;double blueChannel = maxChannel > 10 ? Math.Max(0.4, Math.Min(1.6, avgB / maxChannel)) : 1.0;double adjustedSaturation = saturation;if (isHighExposure){adjustedSaturation = Math.Max(0.4, Math.Min(1.1, saturation * 0.70));}else if (isLowExposure){adjustedSaturation = Math.Max(0.7, Math.Min(1.4, saturation * 1.15));}else{adjustedSaturation = Math.Max(0.6, Math.Min(1.3, saturation));}double adjustedContrast = contrast;if (isHighExposure){adjustedContrast = Math.Max(0.9, Math.Min(1.8, contrast * 1.35));}else if (isLowExposure){adjustedContrast = Math.Max(0.6, Math.Min(1.2, contrast * 0.85));}else{adjustedContrast = Math.Max(0.8, Math.Min(1.5, contrast));}return new EnhancedLightingInfo(blendedBrightness, adjustedContrast, colorTemp, adjustedSaturation,redChannel, greenChannel, blueChannel, toneShiftR, toneShiftG, toneShiftB, warmness, coolness);}catch (Exception ex){Console.WriteLine($"分析光照信息失败: {ex.Message}");return new LightingInfo(128, 1.0, ColorTemperature.Neutral, 1.0, 1.0, 1.0, 1.0);}
}

把色调风格应用到眼镜图片:

/// <summary>
/// 根据环境光照调整眼镜图像(支持透明通道)- 模拟真实阳光照射效果
/// </summary>
/// <param name="glassesImage">原始眼镜图像</param>
/// <param name="environment">环境光照信息</param>
/// <returns>调整后的眼镜图像</returns>
public System.Drawing.Bitmap AdjustGlassesToEnvironment(System.Drawing.Bitmap glassesImage, LightingInfo environment)
{if (glassesImage == null || environment == null)return glassesImage;try{bool hasAlpha = true;System.Drawing.Imaging.PixelFormat sourceFormat = hasAlpha ?System.Drawing.Imaging.PixelFormat.Format32bppArgb :System.Drawing.Imaging.PixelFormat.Format24bppRgb;System.Drawing.Imaging.PixelFormat destFormat = System.Drawing.Imaging.PixelFormat.Format32bppArgb;System.Drawing.Bitmap adjustedImage = new System.Drawing.Bitmap(glassesImage.Width, glassesImage.Height, destFormat);System.Drawing.Bitmap sourceImage = glassesImage;if (!hasAlpha){sourceImage = new System.Drawing.Bitmap(glassesImage.Width, glassesImage.Height, destFormat);using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(sourceImage)){g.Clear(System.Drawing.Color.Transparent);g.DrawImage(glassesImage, 0, 0);}}var sourceData = sourceImage.LockBits(new System.Drawing.Rectangle(0, 0, sourceImage.Width, sourceImage.Height),System.Drawing.Imaging.ImageLockMode.ReadOnly,destFormat);var destData = adjustedImage.LockBits(new System.Drawing.Rectangle(0, 0, adjustedImage.Width, adjustedImage.Height),System.Drawing.Imaging.ImageLockMode.WriteOnly,destFormat);int bytesPerPixel = 4;int sourceByteCount = sourceData.Stride * sourceImage.Height;int destByteCount = destData.Stride * adjustedImage.Height;byte[] sourcePixels = new byte[sourceByteCount];byte[] destPixels = new byte[destByteCount];System.Runtime.InteropServices.Marshal.Copy(sourceData.Scan0, sourcePixels, 0, sourceByteCount);double brightnessFactor = environment.AvgBrightness / 128.0;EnhancedLightingInfo enhancedEnv = environment as EnhancedLightingInfo;double warmShift = 0;double coolShift = 0;if (enhancedEnv != null){double warmCoolRatio = enhancedEnv.Warmness / (enhancedEnv.Coolness + 0.01);if (warmCoolRatio > 1.1){warmShift = (warmCoolRatio - 1.0) * 0.15;}else if (warmCoolRatio < 0.9){coolShift = (1.0 - warmCoolRatio) * 0.15;}}double blendStrength = ColorAdjustmentStrength;for (int y = 0; y < sourceImage.Height; y++){for (int x = 0; x < sourceImage.Width; x++){int pixelIndex = (y * sourceData.Stride) + (x * bytesPerPixel);int destPixelIndex = (y * destData.Stride) + (x * bytesPerPixel);byte originalB = sourcePixels[pixelIndex];byte originalG = sourcePixels[pixelIndex + 1];byte originalR = sourcePixels[pixelIndex + 2];byte originalA = sourcePixels[pixelIndex + 3];if (originalA == 0){destPixels[destPixelIndex] = 0;destPixels[destPixelIndex + 1] = 0;destPixels[destPixelIndex + 2] = 0;destPixels[destPixelIndex + 3] = 0;continue;}double r = originalR / 255.0;double g = originalG / 255.0;double b = originalB / 255.0;double a = originalA / 255.0;r *= brightnessFactor;g *= brightnessFactor;b *= brightnessFactor;if (warmShift > 0){r += warmShift * blendStrength;g += warmShift * 0.6 * blendStrength;b -= warmShift * 0.3 * blendStrength;}else if (coolShift > 0){r -= coolShift * 0.3 * blendStrength;g -= coolShift * 0.1 * blendStrength;b += coolShift * blendStrength;}r = Math.Max(0, Math.Min(1, r));g = Math.Max(0, Math.Min(1, g));b = Math.Max(0, Math.Min(1, b));destPixels[destPixelIndex] = (byte)(b * 255);destPixels[destPixelIndex + 1] = (byte)(g * 255);destPixels[destPixelIndex + 2] = (byte)(r * 255);destPixels[destPixelIndex + 3] = originalA;}}System.Runtime.InteropServices.Marshal.Copy(destPixels, 0, destData.Scan0, destByteCount);sourceImage.UnlockBits(sourceData);adjustedImage.UnlockBits(destData);if (!hasAlpha && sourceImage != glassesImage){sourceImage.Dispose();}return adjustedImage;}catch (Exception ex){Console.WriteLine($"调整眼镜图像失败: {ex.Message}");return glassesImage;}
}

色彩校准后的效果为:

根据 背景图 将 眼镜图 像进行色彩校准之后,发生了一些细微的变化,它们融合得更自然了一些,但是最后一张图无论是否校准色彩,佩戴的效果都不太自然,这里也是有缺陷的,这种缺陷尤其是对于彩色的可反光镜片的佩戴效果更为明显,与实拍差异还是比较大的。


扩展到面具佩戴

眼镜的佩戴功能基本到此就完成了,由此进行衍生,既然都是与眼睛的位置对齐,我们可以把眼镜扩展为 面具,同样地,先进行双眼中心标定:

然后与眼镜完全相同的佩戴方式:

我们看到大多数的佩戴效果非常漂亮,尤其是色彩校准后可以随着场景而变化,但是最后一张图侧面角度过大,效果仍然比较差。


总结

以上对 平面图 表现 3D视角 的 侧面眼镜佩戴 功能进行的一些尝试,在很多场景下表现的效果还不错,而且还可以进行一些其他扩展,比如一些动态表情图应该会更有意思;但是也存在不少问题,与真正的3D效果仍有差距。

本文的尝试,并没有把性能考虑在内,如果考虑实际落地的话,图像的色彩校准这里就需要进行严格的优化了,但是3D模型Y轴角度的旋转,可用度还是极高的,效果很棒。


下载

佩戴程序完整源码:

https://download.csdn.net/download/LateFrames/92130524

图像处理小工具exe:

https://download.csdn.net/download/LateFrames/92130584

人脸转头角度与双眼中心关系标定数据66行:

https://download.csdn.net/download/LateFrames/92130595

http://www.dtcms.com/a/482386.html

相关文章:

  • 重庆网站优化排名上海 企业
  • 网站建设的技术需要多少钱上海软件系统开发公司
  • 汽车用颗粒物传感器:市场趋势、技术革新与行业挑战
  • HICom论文阅读
  • Spring Framework源码解析——ServletContextAware
  • 苏州微网站建设公司做镜像网站
  • OpenStack 网络实现的底层细节-PORT/TAP
  • Chrome 安装失败且提示“无可用的更新” 或 “与服务器的连接意外终止”,Chrome 离线版下载安装教程
  • 02-如何使用Chrome工具排查内存泄露问题
  • 通过不同语言建立多元认知,提升创新能力
  • Tomcat 架构解析与线程池优化策略
  • springboot在DTO使用service,怎么写
  • YOLOv1 详解:实时目标检测的开山之作
  • Vue3 + SpringBoot 分片上传与断点续传方案设计
  • CTFSHOW WEB 3
  • 做个网站费用建材营销型的网站
  • POrtSwigger靶场之CSRF where token validation depends on token being present通关秘籍
  • Java 离线视频目标检测性能优化:从 Graphics2D 到 OpenCV 原生绘图的 20 倍性能提升实战
  • 基于 Informer-BiGRUGATT-CrossAttention 的风电功率预测多模型融合架构
  • 如何做旅游网站推销免费企业信息发布平台
  • 基于RBAC模型的灵活权限控制
  • C++内存管理模板深度剖析
  • 新开的公司怎么做网站手机网站设计神器
  • Bootstrap5 选择区间
  • 考研10.5笔记
  • [c++语法学习]Day 9:
  • LeetCode算法日记 - Day 71: 不同路径、不同路径II
  • 掌握string类:从基础到实战
  • 【C++】四阶龙格库塔算法实现递推轨道飞行器位置速度
  • 网站建设的费用怎么做账网站开发视频是存储的