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

news/2025/10/16 8:11:35/文章来源:https://www.cnblogs.com/xingrenh/p/19144701

image

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

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


眼镜3D模型的尝试

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

image

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

image

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


准备平面眼镜素材

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

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

image

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

image

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

image

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

image

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

image

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

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

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

30fd44c50b8e4d85ae28ea9030cbe761

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

ced2c03b983e4d5d8382e96790e962fc

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

按照  https://www.cnblogs.com/xingrenh/p/19144692 这篇文章的思路继续,我仍然对眼镜的中心点进行标定后再使用,将眼睛的中心点定位在偏上,且偏内:

image


在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模型展示融合的效果:

image

image

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

image

在距离较远的眼睛位置,镜片飞出去很多,非常不自然,这个时候就需要发挥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; }
}

预测人脸旋转角度

image

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

image

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

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

数据格式为: 短边长边比例;左眼中心到鼻梁顶点距离; 右眼中心到鼻梁顶点距离;左右眼中心距;y轴旋转角度;模型偏移X;模型偏移Y;X轴旋转角度;模型缩放比例;68个关键点
根据这些数据,我发现:短边长边比例最小可取值大约 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轴旋转效果

image

处理关键点:

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轴旋转后的效果:

image

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


眼镜色彩校准

提取背景图色调风格:

/// <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;}
}

色彩校准后的效果为:

image

image

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


扩展到面具佩戴

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

image

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

image

image

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


总结

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/937967.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

比 26ai 更震撼的,是 Oracle AI 向量搜索改写的生命答案

比 "26ai" 更震撼的,是 Oracle AI 向量搜索改写的生命答案2025-10-16 08:02 AlfredZhao 阅读(0) 评论(0) 收藏 举报在 Oracle AI World 上,“26ai” 的名字成为外界讨论的焦点,引发广泛好奇与热议。 …

科学背景如何赋能云计算业务战略

本文讲述了一位神经科学博士如何将科研背景应用于云计算业务发展,通过理解科研工作者需求,帮助企业客户将研究负载迁移至云端,加速从原始数据到研究成果的转化过程。科学背景如何赋能云计算业务战略 安德烈亚皮尔斯…

.netframework中自带的dll

.netframework中自带的dllSystem.Data.OracleClient.dll .netframework中自带的dll, "C:\Windows\Microsoft.NET\Framework64\v2.0.50727\System.Data.OracleClient.dll" ``

通过pypdfium2-team/ctypesgen 快速生成ctypes 代码

通过pypdfium2-team/ctypesgen 快速生成ctypes 代码以前说过ctypesgen 的作用,以下是一个简单试用 项目准备代码结构├── add.c ├── add.h ├── add.so ├── app.py ├── common.h ├── init_patch.py ├…

【GitHub每日速递 251016】23k star,Daytona:90ms内极速运行AI代码,安全弹性基础设施来袭!

原文: https://mp.weixin.qq.com/s/QkAYh9t3n41cADcQUi6FXw Daytona:90ms内极速运行AI代码,安全弹性基础设施来袭! 项目地址:https://github.com/daytonaio/daytona 主要语言:TypeScript stars: 23k仓库概述 Dayt…

用 【C# + Winform + Dlib68点】 实现静图眼镜虚拟佩戴 - 行人-

基于DlibDotNet,识别人脸68点关键点,通过眼镜标定功能,使虚拟眼镜能更自然地贴合人脸。采用.NET Framework 4.6.2开发,通过计算眼镜标定点与人眼位置的相对关系,实现眼镜对齐和缩放。关键步骤:人脸检测模型加载、…

图神经网络前沿技术与应用探索

本文深入探讨图神经网络在建模长距离依赖关系、提升计算效率以及新型因果模型方面的最新进展,涵盖算法优化、系统设计和硬件协同等多个技术层面,并介绍在知识图谱推理和多智能体系统等领域的创新应用。KDD 2023:图神…

MVCC、幻读、间隙锁与临键锁(三)

一、MVCC解决了什么问题? MVCC 解决了数据库高并发场景下的两大核心问题:读写阻塞:在传统的锁机制下,读操作可能会阻塞写操作,写操作也一定会阻塞读操作。当有大量读写操作并发时,数据库性能会急剧下降。事务隔离…

MVCC、幻读、间隙锁与临键锁

一、MVCC 解决了什么问题? 🌱 背景:并发读写冲突 当多个事务同时操作同一行时,最经典的冲突是:A 在读;B 在写;A 还没提交,B 改了数据;如何让 A 看到一致的结果?MVCC(Multi-Version Concurrency Control,多…

MVCC、幻读、间隙锁与临键锁(二)

1. MVCC 解决了什么问题? MVCC(多版本并发控制)是 MySQL InnoDB 存储引擎实现并发访问的核心机制,主要解决了读写冲突问题:在传统锁机制中,读操作需要加共享锁,写操作需要加排他锁,会导致 “读阻塞写、写阻塞读…

读AI赋能01超级能动性

读AI赋能01超级能动性1. 超级能动性 1.1. 通货膨胀已成为全球最令人担忧的问题 1.2. 科技行业仍难以摆脱广告业务放缓、投资者情绪转变以及用户参与模式变化带来的叠加影响1.2.1. 负面结果只是对科技行业在疫情期间出现…

生物聚酯塑料回收技术创新与商业应用

本文介绍了生物聚酯塑料的化学回收技术突破,包括EsterCycle低能耗甲醇解工艺和Glacier的AI视觉分拣系统,并通过商业试验验证了生物聚酯材料在零售场景中的应用效果,推动塑料循环价值链建设。更优塑料之路:进展与合…

189 轮转数组 - MKT

189 轮转数组 class Solution { public:// 通过1 time 0ms 100% space 30.mb 5% 自己 内存大void rotate1(vector<int>& nums, int k) {// 1 余数 2 是否大于边界// 10 6 16=6 12-10=2cout<<&quo…

SGD 到 AdamW 优化器的实践选型指南

在深度学习的模型训练过程中,优化器扮演着至关重要的角色。它就像一位经验丰富的向导,带领模型在复杂的参数空间中寻找最优解。从早期简单的随机梯度下降到如今广泛使用的 AdamW,优化器的发展历程充满了对效率与精度…

# ️ MySQL vs PostgreSQL架构深度对比分析报告

# ️ MySQL vs PostgreSQL架构深度对比分析报告Posted on 2025-10-16 02:32 吾以观复 阅读(1) 评论(0) 收藏 举报关联知识库:# ️ MySQL vs PostgreSQL架构深度对比分析报告️ MySQL vs PostgreSQL架构深度对比分…

# 韩国数据中心大火:647套系统因缺失双活集体宕机22小时

# 韩国数据中心大火:647套系统因缺失双活集体宕机22小时Posted on 2025-10-16 02:32 吾以观复 阅读(1) 评论(0) 收藏 举报关联知识库:# 韩国数据中心大火:647套系统因缺失双活集体宕机22小时韩国数据中心大火…

# TLP电池管理工具:Linux笔记本续航优化的终极指南

# TLP电池管理工具:Linux笔记本续航优化的终极指南Posted on 2025-10-16 02:32 吾以观复 阅读(0) 评论(0) 收藏 举报关联知识库:# TLP电池管理工具:Linux笔记本续航优化的终极指南TLP电池管理工具:Linux笔记…

LlamaIndex API Example

LlamaIndex API ExamplePosted on 2025-10-16 02:32 吾以观复 阅读(0) 评论(0) 收藏 举报关联知识库:LlamaIndex API ExampleReader and Query Engine documents = SimpleDirectoryReader(files).load_data() re…

AI中间件机遇与挑战:从Agent到组织级智能的技术演进

AI中间件机遇与挑战:从Agent到组织级智能的技术演进Posted on 2025-10-16 02:32 吾以观复 阅读(0) 评论(0) 收藏 举报关联知识库:AI中间件机遇与挑战:从Agent到组织级智能的技术演进️ AI中间件机遇与挑战:从…

# Redis日常使用与性能排查指南

# Redis日常使用与性能排查指南Posted on 2025-10-16 02:32 吾以观复 阅读(0) 评论(0) 收藏 举报关联知识库:# Redis日常使用与性能排查指南Redis日常使用与性能排查指南 草稿内容 常用命令:info指令 9大块 s…