WPF 记一个特别简单的点集滤波平滑方法

news/2024/10/7 4:29:40

本文记录我想要解决自己从窗口接收 WM_Pointer 消息时,获取到的触摸点不平滑的问题而使用的特别简单且性能垃圾的点集滤波平滑方法

我的本质错误是使用 WM_POINTER 消息的 ptPixelLocationRaw 字段而不是 ptHimetricLocationRaw 字段

由于后面在 walterlv 的帮助之下修复了触摸点收集,附带他也给 Avalonia 做了贡献,详细请看 https://github.com/AvaloniaUI/Avalonia/pull/16850

故事的开始是我用 Avalonia 使用的是 WM_Pointer 的 ptPixelLocation 字段,此字段是带预测的,这不符合我预期。为了减少 Avalonia 的干扰,我就写了一个简单的 WPF 程序去接收 WM_Pointer 消息,自己处理消息。然而这个过程里面我发现写出来的笔迹不平滑,远远不如从 Touch 事件里面收到的点平滑

以下是微软 官方文档 对 ptPixelLocation 的描述

ptPixelLocationType: POINTThe predicted screen coordinates of the pointer, in pixels.The predicted value is based on the pointer position reported by the digitizer and the motion of the pointer. This correction can compensate for visual lag due to inherent delays in sensing and processing the pointer location on the digitizer. This is applicable to pointers of type PT_TOUCH. For other pointer types, the predicted value will be the same as the non-predicted value (see ptPixelLocationRaw).

如果做一个笔迹应用,那自然带触摸的点不能作为最终的笔迹的构成,否则会出现毛刺问题。但是根据我的印象,在 Win10 或 Win11 下,笔迹预测不是在 WM_Pointer 层做的,也不知道为什么会在这里放这样的字段。印象里面是在 Ink 模块才会做预测

一般在触摸框硬件层面会做一次平滑算法,但这只是比较粗略的平滑算法,受限于触摸框的计算芯片的性能,也不会做比较复杂的平滑。在 Windows 10 或 11 的 WISP 模块也会做一次平滑,过滤一些杂点,然后就通过 WM_Pointer 扔给应用

这时候理论上应用收到的点应该就是平滑的了,只不过我错误使用了 ptPixelLocationRaw 字段,此字段是 int 类型的,丢失了精度,导致了写出来的笔迹有锯齿

本文是在此基础上进行的优化,编写了一个简单的平滑滤波算法。算法就是当前点的 X 和 Y 分开计算,当前点的 X 取前后各 5 个点的 X 的平均值,然后 Y 也取前后各 5 个点的 Y 的平均值

为了方便我编写这个简单的算法,我从 WM_Pointer 收到的触摸点信息存放到 txt 文件里面,这个文件被我放在 github 上。接下来我的代码将根据这个 output.txt 文件编写算法

算法代码十分简单,代码如下

    public static List<double> ApplyMeanFilter(List<double> list, int step){var newList = new List<double>(list.Take(step / 2));for (int i = step / 2; i < list.Count - step + step / 2; i++){newList.Add(list.Skip(i - step / 2).Take(step).Sum() / step);}newList.AddRange(list.Skip(list.Count - (step - step / 2)));return newList;}

相信这个代码大家一下就看明白了,就是传入一个 list 数组,这个数组是其中一个分量,即使 X 分量或 Y 分量。然后这个 step 在我调用代码里面会传入 10 的值,也就是中间的点应该使用前后各 5 个点的平均值,也就是核心的 list.Skip(i - step / 2).Take(step).Sum() / step 代码的含义

以上核心代码就是先使用 Skip 跳过当前的点倒数 step 的一半,也就是之前的 5 个点开始,再使用 Take 取 step 个点,也就是 10 个点,最后调用 Sum 方法获取这 step 个点的和的值,除以 step 获取平均值

前后的 var newList = new List<double>(list.Take(step / 2));newList.AddRange(list.Skip(list.Count - (step - step / 2))); 仅仅只是因为平滑每个点需要取前后 step / 2 个点,即 5 个点,在最前面的 5 个点和最后面的 5 个点没有地方可以取,于是就简单直接加入好了

再对此方法进行封装,允许传入 Point 类型,自动拆分 X 和 Y 分量,代码如下

    public static List<Point> ApplyMeanFilter(List<Point> pointList, int step = 10){var xList = ApplyMeanFilter(pointList.Select(t => t.X).ToList(), step);var yList = ApplyMeanFilter(pointList.Select(t => t.Y).ToList(), step);var newPointList = new List<Point>();for (int i = 0; i < xList.Count && i < yList.Count; i++){newPointList.Add(new Point(xList[i], yList[i]));}return newPointList;}

上面代码的性能是比较差的,如果大家想要使用,还请自行优化

我从文件读取了点的信息,然后应用了上面的算法,可以直接使用折线画出比较好看的界面效果

    public MainWindow(){InitializeComponent();Loaded += MainWindow_Loaded;}private void MainWindow_Loaded(object sender, RoutedEventArgs e){var file = "output.txt";var lines = System.IO.File.ReadAllLines(file);var pointList = new List<Point>();foreach (var line in lines){var match = Regex.Match(line, @"(\d+),(\d+)");if (match.Success){pointList.Add(new Point(double.Parse(match.Groups[1].ValueSpan), double.Parse(match.Groups[2].ValueSpan)));}}var applyMeanFilter = ApplyMeanFilter(pointList);var polyline = new Polyline();polyline.Stroke = Brushes.Black;polyline.StrokeThickness = 2;polyline.Points = new PointCollection(applyMeanFilter);RootCanvas.Children.Add(polyline);}

大家可以自行注释掉平滑过滤,测试前后的平滑度变化

本文代码放在 github 和 gitee 上,可以使用如下命令行拉取代码。我整个代码仓库比较庞大,使用以下命令行可以进行部分拉取,拉取速度比较快

先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 68654061ac8cced2abe6736141c37bbb6303ccdb

以上使用的是国内的 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码,将 gitee 源换成 github 源进行拉取代码。如果依然拉取不到代码,可以发邮件向我要代码

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 68654061ac8cced2abe6736141c37bbb6303ccdb

获取代码之后,进入 WPFDemo/FalwhekaylearchaKuhiyehakemchaije 文件夹,即可获取到源代码

更多触摸请看 WPF 触摸相关

更多技术博客,请参阅 博客导航

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

dotnet C# 如何在顶级语句定义属性

随着 dotnet 6 开始,现在的 C# dotnet 可以使用顶级语句非常方便创建一个小型项目,包含的代码也特别少。本文将和大家介绍如何在顶级语句里面定义属性如以下代码是传统的控制台应用程序的代码 using System;namespace Application {class Program{static void Main(string[] …

C#/.net core “hello”.IndexOf(“\0”,2)中的坑

如何规避.net core中IndexOf方法中的坑,从中又引发了哪些思考?小心这些方法!先想想看,你认为下面代码返回值是多少?"hello".IndexOf("", 2); "hello".IndexOf("\0", 2); "hello".IndexOf(\0, 2);今天和大家分享关于.ne…

最简!手把手带你完美删除Vmware虚拟机!

Vmware虚拟机最简完美删除教程你还在苦于无法完美删除Vmware虚拟机吗?你还在为自己千疮百孔的系统而烦恼吗?你还在为想要重做Vmware但没删干净各种报错而烦操吗?但今天之后这些问题都将不是问题!现在赶紧收藏吧!以备未来不时之需!接下来我们便一起来把电脑里的Vmware17删…

YuebonCore:基于.NET8开源、免费的权限管理及快速开发框架

前言 今天大姚给大家分享一款基于.NET8开源、免费(MIT License)功能强大的权限管理及快速开发框架,支持前后端分离,项目架构易于扩展,是中小企业的首选:YuebonNetCore。核心设计目标是开发迅速、代码量少、学习简单、功能强大、轻量级、易扩展,让 Web 开发更快速、简单,…

C++基于模板实现智能指针

某厂面试,当时反正是没写出来,估计是寄了,事后做个记录。 #include <iostream> #include <mutex> using namespace std;class ObjectElement { private:char *addr;int size;void release() {addr = nullptr;size = 0;}public:ObjectElement(int size) {cout<…

cf补题计划_Edu 162_E

E. Count Paths 题目简介乍一眼一看是一个很简单树上dp(实际上也是树上dp 第一看做法是考虑dp[i][j]表达为第i个节点的下面有多少个颜色为j的节点,且这个节点于i节点之间无其他的节点为j颜色,然后dp转移十分的简单,但是n是\(2\cdot10^5\),绝对超时 然后显然我们发现,一个节…

Operating Systems: Principles and Practice 2nd ed. Edition

https://recursivebooks.com/ Operating Systems: Principles and Practice 2nd ed. Editionby Thomas Anderson (Author), Michael Dahlin (Author)https://www.kea.nu/files/textbooks/ospp/

Vue 3 路由组件缓存keep-alive

Vue 3 路由组件缓存 Vue3 KeepAlive官方文档 1. keep-alive 基本介绍keep-alive 是 Vue 的内置组件,用于缓存动态组件或路由组件,避免组件被频繁销毁和重建,从而提高性能。 当组件被 keep-alive 包裹后,在路由切换时不会销毁组件,而是将其缓存起来。下次切换回来时,会直接…