用 【C# + Winform + MediaPipe】 实现人脸468点识别

目录

开发环境

[尝试 MediaPipe.NET 失败](#尝试 MediaPipe.NET 失败)

[封装 Python + MediaPipe](#封装 Python + MediaPipe)

[Winform 调用 Python](#Winform 调用 Python)

动态服务端口

模拟实时图像

最终程序目录

行云,我想你了


开发环境

VS2022、C#、Winform、.NetFramework 4.6.2、Python 3.9.13、MediaPipe 0.10.21

完整的程序源码:https://download.csdn.net/download/LateFrames/92063664


尝试 MediaPipe.NET 失败

我尝试使用 MediaPipe.NET来进行人脸468点的 功能实现,但是遇到了各种问题,MediaPipe.NET 对 vector<NormalizedLandmarkList>类型的支持有限,导致加载模型后也没能成功识别,遇到的问题有点多,因此没再继续尝试,更换了思路,直接把 Python 嵌入到软件目录下来进行完全隔离的调用,因此以Python的识别作为服务,来进行数据交换的识别,统一封装在一个软件中,通过一个按钮来 一键启动,进行实时识别。该演示进行了这种使用方式的尝试。


封装 Python + MediaPipe

python_embedded 这个目录是嵌入的python完整环境的目录,同时里面也包含了MediaPipe的相关依赖。MediaPipe 包含的模型分类有:face_detection、face_geometry、face_landmark、hand_landmark、holistic_landmark、iris_landmark、palm_detection、pose_detection、pose_landmark、selfie_segmentation,如下图:

这里要注意, 程序的存放路径一定是纯英文的路径, 否则会导致模型加载失败,无法成功启动服务

,python_embedded 这个目录就是所有的服务内容。


Winform 调用 Python

Winform的主窗口程序启动Python进程:

cs 复制代码
public async Task<bool> StartServiceAsync()
{
    try
    { 
        var baseDir = AppDomain.CurrentDomain.BaseDirectory;
        Console.WriteLine(string.Format("[Client] Base directory: {0}", baseDir));

        var pythonExePath = Path.Combine(baseDir, "python_embedded", "python.exe");
        if (!File.Exists(pythonExePath))
        {
            throw new FileNotFoundException("Python not found: " + pythonExePath);
        }

        var scriptPath = Path.Combine(baseDir, "mediapipe_service.py");
        if (!File.Exists(scriptPath))
        {
            throw new FileNotFoundException("Script not found: " + scriptPath);
        }

        // 直接启动Python,隐藏窗口,重定向输出
        var startInfo = new ProcessStartInfo
        {
            FileName = pythonExePath,
            Arguments = string.Format("\"{0}\" {1}", scriptPath, port),
            WorkingDirectory = baseDir,
            UseShellExecute = false,  // 必须false才能重定向输出
            CreateNoWindow = true,    // 隐藏窗口
            RedirectStandardOutput = true,  // 重定向标准输出
            RedirectStandardError = true    // 重定向错误输出
        };

        Console.WriteLine(string.Format("[Client] Launching Python in background..."));

        pythonProcess = Process.Start(startInfo);
        if (pythonProcess == null)
        {
            throw new Exception("Failed to start process");
        }
        pythonProcess.BeginOutputReadLine();
        pythonProcess.BeginErrorReadLine();

        Console.WriteLine("[Client] Process started in background, waiting 5 seconds...");
        await Task.Delay(5000);

        return await ConnectAsync();
    }
    catch (Exception ex)
    {
        Console.WriteLine(string.Format("[Client] Error: {0}", ex.Message));
        return false;
    }
}

进程启动后 通过 Tcp 连接到 服务:

cs 复制代码
public async Task<bool> ConnectAsync()
{
    try
    {
        Console.WriteLine(string.Format("[Client] Connecting to {0}:{1}...", host, port));

        tcpClient = new TcpClient();
        await tcpClient.ConnectAsync(host, port);
        stream = tcpClient.GetStream();
        isConnected = true;

        var pingResponse = await SendCommandAsync(new { command = "ping" });
        if (pingResponse != null && pingResponse["status"].ToString() == "ok")
        {
            Console.WriteLine("[Client] Connected!");
            return true;
        }

        return false;
    }
    catch (Exception ex)
    {
        Console.WriteLine(string.Format("[Client] Connection failed: {0}", ex.Message));
        isConnected = false;
        return false;
    }
}

动态服务端口

注意:服务端使用的端口有可能会被其他进程占用,因此这里动态检测端口是否可用,如果不可用 则自动切换为其他可用端口,来确保服务可以成功启动:

Python服务端动态检测可用的动态端口,处理如下 :

python 复制代码
def find_available_port(start_port=9999, max_attempts=10):
    """查找可用端口 - 从9999开始尝试10个端口"""
    for port in range(start_port, start_port + max_attempts):
        # 测试端口是否可用
        if port_is_available(port):
            return port
    return None

def main():
    # 检查默认端口9999
    if not port_available(9999):
        # 自动查找10000-10008范围内的可用端口
        alternative_port = find_available_port(10000, 10)
        if alternative_port:
            port = alternative_port
            # 保存到文件供C#读取
            with open('service_port.txt', 'w') as f:
                f.write(str(port))

Winform客户端获取实时的有效端口,处理如下:

cs 复制代码
// 启动Python服务后
await Task.Delay(5000);

// 读取Python实际使用的端口
var portFilePath = Path.Combine(baseDir, "service_port.txt");
if (File.Exists(portFilePath))
{
    var portText = File.ReadAllText(portFilePath).Trim();
    if (int.TryParse(portText, out int actualPort))
    {
        Console.WriteLine($"Service using alternative port: {actualPort}");
        port = actualPort;  // 更新客户端端口
    }
    File.Delete(portFilePath);  // 读取后删除
}

return await ConnectAsync();  // 使用正确的端口连接

模拟实时图像

Winform 截取实时图像

然后截取屏幕指定区域播放的视频来作为图像任务流,截取目标区域的图像 ,发送给服务端:

cs 复制代码
public async Task<DetectionResult> DetectFaceMeshAsync(string imagePath, List<string> enabledModels = null)
{
    var result = new DetectionResult();

    if (!isConnected)
    {
        result.Success = false;
        result.Message = "Service not connected";
        return result;
    }

    try
    {
        var stopwatch = Stopwatch.StartNew();

        // Default to face_mesh if no models specified
        if (enabledModels == null || enabledModels.Count == 0)
        {
            enabledModels = new List<string> { "face_mesh" };
        }

        var request = new
        {
            command = "detect",
            image_path = imagePath,
            enabled_models = enabledModels
        };

        var response = await SendCommandAsync(request);

        if (response != null && response["status"].ToString() == "ok")
        {
            // Store all models data
            if (response.ContainsKey("models"))
            {
                result.Models = response["models"].ToObject<Dictionary<string, object>>();

                // Legacy support - extract face_mesh landmarks to Landmarks property
                if (response["models"]["face_mesh"] != null)
                {
                    var faceMesh = response["models"]["face_mesh"] as JObject;
                    if (faceMesh != null && faceMesh.ContainsKey("landmarks"))
                    {
                        var landmarksArray = faceMesh["landmarks"] as JArray;
                        if (landmarksArray != null)
                        {
                            foreach (JObject landmark in landmarksArray)
                            {
                                var x = landmark["x"].Value<float>();
                                var y = landmark["y"].Value<float>();
                                var z = landmark["z"].Value<float>();

                                result.Landmarks.Add(new System.Drawing.PointF(x, y));
                                result.LandmarksZ.Add(z);
                            }
                        }
                    }
                }
            }

            if (response.ContainsKey("process_time"))
            {
                result.ProcessTimeMs = response["process_time"].Value<double>();
            }

            if (response.ContainsKey("image_width"))
            {
                result.ImageWidth = response["image_width"].Value<int>();
            }

            if (response.ContainsKey("image_height"))
            {
                result.ImageHeight = response["image_height"].Value<int>();
            }

            result.Success = true;
            result.Message = string.Format("Detection completed for {0} model(s)", enabledModels.Count);
        }
        else
        {
            result.Success = false;
            result.Message = response != null && response.ContainsKey("message") ?
                response["message"].ToString() : "Detection failed";
        }

        result.TotalTimeMs = stopwatch.ElapsedMilliseconds;
        return result;
    }
    catch (Exception ex)
    {
        Console.WriteLine(string.Format("[Client] Error: {0}", ex.Message));
        result.Success = false;
        result.Message = ex.Message;
        return result;
    }
}

将服务端返回的识别结果绘制到窗口:

cs 复制代码
private void DrawMultiModelResults(Bitmap frame, MediaPipeServiceClient.DetectionResult result)
{
    using (var g = Graphics.FromImage(frame))
    {
        g.SmoothingMode = SmoothingMode.AntiAlias;
        int totalPoints = 0;
        var infoLines = new List<string>();

        try
        {
            // Draw Face Mesh - 使用更明显的颜色和大小
            if (result.Models.ContainsKey("face_mesh"))
            {
                var faceMeshData = Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(
                    result.Models["face_mesh"].ToString());
                if (faceMeshData.landmarks != null)
                {
                    int faceMeshPoints = 0;
                    // 使用渐变色和发光效果
                    using (var outerBrush = new SolidBrush(Color.FromArgb(80, 0, 255, 255))) // 外圈发光
                    using (var innerBrush = new SolidBrush(Color.FromArgb(255, 0, 255, 255))) // 内圈亮青色
                    {
                        foreach (var landmark in faceMeshData.landmarks)
                        {
                            float x = landmark.x;
                            float y = landmark.y;
                            float visibility = landmark.visibility != null ? (float)landmark.visibility : 1.0f;
 
                            {
                                // 绘制外圈发光效果
                                g.FillEllipse(outerBrush, x - 3f, y - 3f, 6, 6);
                                // 绘制内圈亮点
                                g.FillEllipse(innerBrush, x - 1.5f, y - 1.5f, 3, 3);
                                faceMeshPoints++;
                                totalPoints++;
                            }
                        }
                    }
                    infoLines.Add(string.Format("Face: {0} pts", faceMeshPoints));
                }
            }  
        }
        catch (Exception ex)
        {
            Console.WriteLine(string.Format("Draw error: {0}", ex.Message));
        } 
    }
}

注意:这里需要注意2点:当对多个模型同时进行识别的时候,有的模型之间可能会有冲突;在组合模型使用的时候,使用的模型越多,识别速度也会越慢 。


最终程序目录

winform 程序编译后的目录:

该目录下包含了全部运行需要的文件,软件启动后选取一个图像画面,然后直接启动服务识别即可。


行云,我想你了

"行云,我想吃你做的饭,我想你了",太喜欢这段了,以这段为识别素材,进行演示:

完整的程序源码:https://download.csdn.net/download/LateFrames/92063664

相关推荐
人工干智能5 小时前
科普:Python 中,字典的“动态创建键”特性
开发语言·python
追逐时光者8 小时前
精选 4 款开源免费、美观实用的 MAUI UI 组件库,助力轻松构建美观且功能丰富的应用程序!
后端·.net
开心-开心急了8 小时前
主窗口(QMainWindow)如何放入文本编辑器(QPlainTextEdit)等继承自QWidget的对象--(重构版)
python·ui·pyqt
moshumu19 小时前
局域网访问Win11下的WSL中的jupyter notebook
ide·python·深度学习·神经网络·机器学习·jupyter
计算机毕设残哥10 小时前
基于Hadoop+Spark的人体体能数据分析与可视化系统开源实现
大数据·hadoop·python·scrapy·数据分析·spark·dash
R-G-B12 小时前
【14】C#实战篇——C++动态库dll 接口函数将char* strErr字符串 传给C# ,并且在winform的MessageBox和listbox中显示。C++ string 日志传给 C#
c++·c#·strerr字符串传给c#·动态库dll传递字符串给c#·string日志传给c#·c++ string传给 c#·c++底层函数日志传给c#显示
编程指南针12 小时前
2026新选题-基于Python的老年病医疗数据分析系统的设计与实现(数据采集+可视化分析)
开发语言·python·病历分析·医疗病历分析
reasonsummer13 小时前
【办公类-116-01】20250929家长会PPT(Python快速批量制作16:9PPT相册,带文件名,照片横版和竖版)
java·数据库·python·powerpoint
拉姆哥的小屋13 小时前
基于提示学习的多模态情感分析系统:从MULT到PromptModel的华丽升级
python·深度学习·学习