Android 图片相识度比较(pHash)

概述

在 Android 中,要比对两张 Bitmap 图片的相似度,常见的方法有基于像素差异直方图比较、或者使用一些更高级的算法如 SSIM(结构相似性)感知哈希(pHash)

1. 基于像素的差异比较

可以逐像素比较两张 Bitmap,计算它们之间的差异。以下是一个简单的逐像素比较的例子:

public static double compareBitmaps(Bitmap bitmap1, Bitmap bitmap2) {if (bitmap1.getWidth() != bitmap2.getWidth() || bitmap1.getHeight() != bitmap2.getHeight()) {throw new IllegalArgumentException("Bitmap sizes are different!");}int width = bitmap1.getWidth();int height = bitmap1.getHeight();long diff = 0;for (int y = 0; y < height; y++) {for (int x = 0; x < width; x++) {int pixel1 = bitmap1.getPixel(x, y);int pixel2 = bitmap2.getPixel(x, y);int r1 = Color.red(pixel1);int g1 = Color.green(pixel1);int b1 = Color.blue(pixel1);int r2 = Color.red(pixel2);int g2 = Color.green(pixel2);int b2 = Color.blue(pixel2);// 计算 RGB 差异diff += Math.abs(r1 - r2);diff += Math.abs(g1 - g2);diff += Math.abs(b1 - b2);}}// 最大可能差异double maxDiff = 3L * 255 * width * height;// 返回 0 到 1 的值,越小表示相似度越高return (double) diff / maxDiff;
}

这段代码计算两张图片的 RGB 差异,返回的结果范围在 0-1 之间,数值越接近 0 表示图片越相似。

2. 基于直方图的比较

通过比较两张图片的颜色直方图来评估相似度。直方图可以捕捉图像的颜色分布,而不关心具体像素位置。

public static double compareHistograms(Bitmap bitmap1, Bitmap bitmap2) {int[] histogram1 = new int[256];int[] histogram2 = new int[256];// 计算两张图的灰度直方图for (int y = 0; y < bitmap1.getHeight(); y++) {for (int x = 0; x < bitmap1.getWidth(); x++) {int pixel1 = bitmap1.getPixel(x, y);int gray1 = (Color.red(pixel1) + Color.green(pixel1) + Color.blue(pixel1)) / 3;histogram1[gray1]++;int pixel2 = bitmap2.getPixel(x, y);int gray2 = (Color.red(pixel2) + Color.green(pixel2) + Color.blue(pixel2)) / 3;histogram2[gray2]++;}}// 计算直方图的差异double diff = 0;for (int i = 0; i < 256; i++) {diff += Math.abs(histogram1[i] - histogram2[i]);}return diff / (bitmap1.getWidth() * bitmap1.getHeight());
}

3. 使用 SSIM(结构相似性)

SSIM 是一种用来衡量两张图片结构相似性的算法,它比简单的像素差异或直方图比较更准确。Android SDK 没有内置的 SSIM 方法,但可以引入第三方库或者自己实现。SSIM 主要关注三方面:亮度、对比度和结构。

4. 感知哈希(pHash)

pHash 是一种图像哈希技术,它可以生成图片的“指纹”,然后比较两个哈希值的相似性。与传统哈希方法不同,pHash 对于图像的细微改变(例如缩放、旋转)不敏感。

可以通过第三方库实现 pHash,比如 ImageHash 库,或者自己实现基于 DCT(离散余弦变换)的算法。

// 引入第三方库 ImageHash 进行哈希比较
String hash1 = ImageHash.hash(bitmap1);
String hash2 = ImageHash.hash(bitmap2);int similarity = ImageHash.compare(hash1, hash2);

一般来说:

  • 对于简单的图像比较,基于像素差异的方式即可。
  • 如果要忽略图片的细微变动,直方图或感知哈希是更合适的选择。
  • SSIM 适用于对图像结构有更高要求的场景。

实现

图像比较的算法应用相当广泛, 本文基于感知哈希算法, 用于识别视频帧图像的左右两部分的相似度, 从而判断视频是否是一个左右眼的VR视频格式, 本文采用 感知哈希(pHash) 算法, 它非常适合处理具有细微变化的图像,如裁剪、缩放、亮度变化等。

感知哈希(pHash)是一种用于衡量图像相似度的算法,它通过将图像转换为频域信息,提取其视觉特征来生成一个哈希值。pHash 具有鲁棒性,能够忽略图像的小幅度变动、旋转和缩放等影响。下面是 pHash 算法的实现步骤及其原理。

pHash 算法的实现步骤

  1. 转换为灰度图:将图片转换为灰度图像,以便降低复杂度,并去除颜色信息的影响。

  2. 缩小尺寸:将图像缩小到一个固定的尺寸(例如 32x32),目的是去除高频细节,保留图片的整体特征。这一步骤在后续的离散余弦变换(DCT)中很重要。

  3. 离散余弦变换(DCT):对缩小后的图像执行离散余弦变换,将图像从空间域转换到频率域。这种转换能提取图像的低频信息,忽略高频噪声。

  4. 截取低频部分:只保留 DCT 结果的左上角部分(例如 8x8 的矩阵),因为这部分包含图像的主要信息。

  5. 计算均值:计算截取的低频部分的均值。

  6. 生成哈希值:将 DCT 中每个像素值与均值进行比较,生成一个二进制序列。如果某个像素值大于均值,置 1,否则置 0。最终的哈希值是由这个二进制序列构成。

参考pHash 算法实现

import android.graphics.Bitmap;
import android.graphics.Color;
import java.util.Arrays;public class ImagePHash {// 默认使用 32x32 大小private static final int SIZE = 32;// DCT 截取的大小(例如 8x8)private static final int SMALLER_SIZE = 8;public String getHash(Bitmap img) {// 1. 转换为灰度图像Bitmap grayImg = toGrayscale(img);// 2. 缩小图片Bitmap smallImg = Bitmap.createScaledBitmap(grayImg, SIZE, SIZE, false);// 3. 转换为二维数组double[][] vals = new double[SIZE][SIZE];for (int x = 0; x < SIZE; x++) {for (int y = 0; y < SIZE; y++) {vals[x][y] = Color.red(smallImg.getPixel(x, y));}}// 4. 对图像执行离散余弦变换(DCT)double[][] dctVals = applyDCT(vals);// 5. 截取 DCT 左上角的 8x8 部分double[] dctLowFreq = new double[SMALLER_SIZE * SMALLER_SIZE];for (int x = 0; x < SMALLER_SIZE; x++) {for (int y = 0; y < SMALLER_SIZE; y++) {dctLowFreq[x * SMALLER_SIZE + y] = dctVals[x][y];}}// 6. 计算均值double avg = Arrays.stream(dctLowFreq).average().orElse(0.0);// 7. 生成哈希值StringBuilder hash = new StringBuilder();for (double value : dctLowFreq) {hash.append(value > avg ? "1" : "0");}return hash.toString();}// 转换为灰度图像private Bitmap toGrayscale(Bitmap img) {int width = img.getWidth();int height = img.getHeight();Bitmap grayscaleImg = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);for (int y = 0; y < height; y++) {for (int x = 0; x < width; x++) {int pixel = img.getPixel(x, y);int red = Color.red(pixel);int green = Color.green(pixel);int blue = Color.blue(pixel);int gray = (red + green + blue) / 3;int newPixel = Color.rgb(gray, gray, gray);grayscaleImg.setPixel(x, y, newPixel);}}return grayscaleImg;}// 执行离散余弦变换(DCT)private double[][] applyDCT(double[][] f) {int N = f.length;double[][] F = new double[N][N];for (int u = 0; u < N; u++) {for (int v = 0; v < N; v++) {double sum = 0.0;for (int i = 0; i < N; i++) {for (int j = 0; j < N; j++) {sum += f[i][j] *Math.cos((2 * i + 1) * u * Math.PI / (2.0 * N)) *Math.cos((2 * j + 1) * v * Math.PI / (2.0 * N));}}double alphaU = (u == 0) ? Math.sqrt(1.0 / N) : Math.sqrt(2.0 / N);double alphaV = (v == 0) ? Math.sqrt(1.0 / N) : Math.sqrt(2.0 / N);F[u][v] = alphaU * alphaV * sum;}}return F;}// 比较两个哈希值,返回汉明距离(不同位数的个数)public int hammingDistance(String hash1, String hash2) {int distance = 0;for (int i = 0; i < hash1.length(); i++) {if (hash1.charAt(i) != hash2.charAt(i)) {distance++;}}return distance;}
}

对比效果如下(使用ListView 显示多张图片对比结果, 一帧视频图像从中间切割左右两部分, 分别显示在列表项的左右两侧, 中间的文字输出比较结果的汉明值, 值越小图像差异越小):
在这里插入图片描述
在这里插入图片描述

原始测试图片(从一个VR视频中截取出的视频帧):
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

代码分享:

test_img_diff.xml 布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:id="@+id/rlRoot"><ListView android:id="@+id/lv"android:layout_width="match_parent"android:layout_height="match_parent"/>
</RelativeLayout>

ListView 的item 布局: item_img_diff.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><ImageView android:id="@+id/ivLeft"android:layout_width="128dp"android:layout_height="72dp"/><ImageView android:id="@+id/ivRight"android:layout_width="128dp"android:layout_height="72dp"android:layout_alignParentRight="true"/><TextView android:id="@+id/tvRes"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_centerInParent="true"android:textSize="18sp"android:textColor="#FFFFFFFF"/>
</RelativeLayout>

主界面Activity: ImgDiffTester.java

public class ImgDiffTester extends Activity implements View.OnClickListener {final String TAG = "ImgDiffTester";ListView lv;ImgListAdapter adapter;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.test_img_diff);findViewById(R.id.rlRoot).setOnClickListener(this);lv = (ListView) findViewById(R.id.lv);adapter = new ImgListAdapter();lv.setAdapter(adapter);startCompare();}void startCompare(){new Thread(){@Overridepublic void run() {File[] fs = new File("/sdcard/Download/").listFiles(new FileFilter() {@Overridepublic boolean accept(File file) {return file.getName().endsWith(".png");}});for(File f : fs){Bitmap bm = BitmapFactory.decodeFile(f.getAbsolutePath());compareBitmapAndShow(bm);}lv.post(new Runnable() {@Overridepublic void run() {adapter.notifyDataSetChanged();}});}}.start();}void compareBitmapAndShow(Bitmap bm){if(bm != null && bm.getWidth() > 0 && bm.getHeight() > 0) {final Bitmap bm1 = BitmapUtils.clipBitmapWidthBounds(bm, new Rect(0, 0, bm.getWidth() / 2, bm.getHeight()));//bm1 = BitmapFactory.decodeFile("/sdcard/l.png");final Bitmap bm2 = BitmapUtils.clipBitmapWidthBounds(bm, new Rect(bm.getWidth() / 2, 0, bm.getWidth(), bm.getHeight()));//bm2 = BitmapFactory.decodeFile("/sdcard/r.png");try {Bitmap[] scaled = new Bitmap[2];//scaled[0] = Bitmap.createBitmap(pHash.DCT_LENGTH, pHash.DCT_LENGTH, Bitmap.Config.ARGB_8888);//scaled[1] = Bitmap.createBitmap(pHash.DCT_LENGTH, pHash.DCT_LENGTH, Bitmap.Config.ARGB_8888);//int cmp = pHash.compareBitmap(bm1, bm2, scaled, false);long st = SystemClock.uptimeMillis();final int cmp = ImagePHash.compareBitmap(bm1, bm2);long et = SystemClock.uptimeMillis();Log.d(TAG, "compare " + cmp + " spend " + (et - st) + " ms");Item item = new Item();item.l = bm1;item.r = bm2;item.res = "Result: " + cmp + ", spend " + (et - st) + " ms";adapter.items.add(item);} catch (Exception e) {throw new RuntimeException(e);}}}public static class ImagePHash {// 默认使用 32x32 大小private static final int SIZE = 32;// DCT 截取的大小(例如 8x8)private static final int SMALLER_SIZE = 8;public static int compareBitmap(Bitmap bm1, Bitmap bm2){String h1 = getHash(bm1);String h2 = getHash(bm2);return hammingDistance(h1, h2);}@SuppressLint("NewApi")public static String getHash(Bitmap img) {long st = SystemClock.uptimeMillis();// 1. 转换为灰度图像Bitmap grayImg = toGrayscale(img);// 2. 缩小图片Bitmap smallImg = Bitmap.createScaledBitmap(grayImg, SIZE, SIZE, false);// 3. 转换为二维数组double[][] vals = new double[SIZE][SIZE];for (int x = 0; x < SIZE; x++) {for (int y = 0; y < SIZE; y++) {vals[x][y] = Color.red(smallImg.getPixel(x, y));}}long ct1 = SystemClock.uptimeMillis();// 4. 对图像执行离散余弦变换(DCT)double[][] dctVals = applyDCT(vals);long ct2 = SystemClock.uptimeMillis();// 5. 截取 DCT 左上角的 8x8 部分double[] dctLowFreq = new double[SMALLER_SIZE * SMALLER_SIZE];for (int x = 0; x < SMALLER_SIZE; x++) {for (int y = 0; y < SMALLER_SIZE; y++) {dctLowFreq[x * SMALLER_SIZE + y] = dctVals[x][y];}}// 6. 计算均值double avg = Arrays.stream(dctLowFreq).average().orElse(0.0);long ct3 = SystemClock.uptimeMillis();// 7. 生成哈希值StringBuilder hash = new StringBuilder();for (double value : dctLowFreq) {hash.append(value > avg ? "1" : "0");}Log.d("ImgDiff", (ct1 - st) + ", " + (ct2 - ct1));return hash.toString();}// 转换为灰度图像private static Bitmap toGrayscale(Bitmap img) {int width = img.getWidth();int height = img.getHeight();Bitmap grayscaleImg = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);for (int y = 0; y < height; y++) {for (int x = 0; x < width; x++) {int pixel = img.getPixel(x, y);int red = Color.red(pixel);int green = Color.green(pixel);int blue = Color.blue(pixel);int gray = (red + green + blue) / 3;int newPixel = Color.rgb(gray, gray, gray);grayscaleImg.setPixel(x, y, newPixel);}}return grayscaleImg;}// 执行离散余弦变换(DCT)private static double[][] applyDCT(double[][] f) {int N = f.length;double[][] F = new double[N][N];for (int u = 0; u < N; u++) {for (int v = 0; v < N; v++) {double sum = 0.0;for (int i = 0; i < N; i++) {for (int j = 0; j < N; j++) {sum += f[i][j] *Math.cos((2 * i + 1) * u * Math.PI / (2.0 * N)) *Math.cos((2 * j + 1) * v * Math.PI / (2.0 * N));}}double alphaU = (u == 0) ? Math.sqrt(1.0 / N) : Math.sqrt(2.0 / N);double alphaV = (v == 0) ? Math.sqrt(1.0 / N) : Math.sqrt(2.0 / N);F[u][v] = alphaU * alphaV * sum;}}return F;}// 比较两个哈希值,返回汉明距离(不同位数的个数)public static int hammingDistance(String hash1, String hash2) {int distance = 0;for (int i = 0; i < hash1.length(); i++) {if (hash1.charAt(i) != hash2.charAt(i)) {distance++;}}return distance;}}class ImgListAdapter extends BaseAdapter{ArrayList<Item> items = new ArrayList<>();@Overridepublic int getCount() {return items.size();}@Overridepublic Object getItem(int i) {return items.get(i);}@Overridepublic long getItemId(int i) {return i;}@Overridepublic View getView(int pos, View view, ViewGroup viewGroup) {if(view == null){view = getLayoutInflater().inflate(R.layout.item_img_diff, null, false);}((ImageView)view.findViewById(R.id.ivLeft)).setImageBitmap(items.get(pos).l);((ImageView)view.findViewById(R.id.ivRight)).setImageBitmap(items.get(pos).r);((TextView)view.findViewById(R.id.tvRes)).setText(items.get(pos).res);return view;}}class Item{Bitmap l, r;String res;}
}

温馨提示
本文算法及用例仅供参考, 未经大量测试验证
请谨慎阅读参考

参考

Android Bitmap亮度调节、灰度化、二值化、相似距离实现

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

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

相关文章

基于MATLAB车牌识别系统设计

MATLAB车牌识别系统设计 实践目的 车牌是一辆汽车独一无二的信息&#xff0c;因此&#xff0c;对车辆牌照的识别技术可以作为 辨识一辆车最为有效的方法。随着ITS(智能交通系统)的高速发展&#xff0c;对车牌识别技术的研究也随之发展。从根本上讲&#xff0c;牌照识别应用了…

中缀表达式转后缀表达式(逆波兰表达式)及如何计算后缀表达式

目录 中缀、后缀表达式简介 中缀转后缀的规则 模拟中缀转后缀 中缀转后缀代码 后缀表达式求值 后缀表达式求值代码 Leetcode相关题目 中缀、后缀表达式简介 首先说说什么是中缀表达式&#xff0c;中缀表达式中&#xff0c;操作符是以中缀形式处于操作数的中间。例如&…

Linux安装Anaconda和Pytorch

又到了一年一度换环境、换服务器不断折腾的时节了&#xff0c;一通折腾后&#xff0c;重新启动遂做记录。 1. Linux安装Anaconda 1.1 离线安装模式 进入官网https://www.anaconda.com/download/success&#xff0c;如图所示&#xff1a; 选择版本进行下载即可。 1.2 在线w…

[Linux网络编程]03-TCP协议

一.TCP协议数据通信的过程 TCP数据报如下&#xff0c;数据报中的标志位双端通信的关键。 三次握手: 1.客户端向服务端发送SYN标志位&#xff0c;请求建立连接&#xff0c;同时发送空包 2.服务端向客户端回发ACK标志位(即确认标志位&#xff0c;任何一端发送数据后都需要另一端…

【VUE】【IOS】【APP】IOS Music APP播放器开发

前言 周末闲来无事&#xff0c;学习了下移动端的一些知识。了解到移动端的一些实现方式&#xff0c;先从最简单的开始。本人没有IOS swift 和Android的开发经验。抱着学习态度从简单的入手&#xff0c;经过了解&#xff0c;本人之前自己用vue的写着玩了几个小项目。看到可以用…

《使用Gin框架构建分布式应用》阅读笔记:p101-p107

《用Gin框架构建分布式应用》学习第7天&#xff0c;p101-p107总结&#xff0c;总计7页。 一、技术总结 1.StatusBadRequest vs StatusInternalServerError 写代码的时候有一个问题&#xff0c;什么时候使用 StatusBadRequest(400错误)&#xff0c;什么时候使用 StatusIntern…

1.2电子商务安全内涵

目录 1 电子商务安全的层次 2 计算机网络安全 3电子商务安全的特点 只有在你生命美丽的时候&#xff0c;世界才是美丽的。 —— 顾城 《顾城哲思录》 1 电子商务安全的层次 安全:主体没有危险的客观状态 电子商务安全是一个广泛的概念&#xff0c;它涉及到电子商务的各个方…

现今 CSS3 最强二维布局系统 Grid 网格布局

深入学习 CSS3 目前最强大的布局系统 Grid 网格布局 Grid 网格布局的基本认识 Grid 网格布局: Grid 布局是一个基于网格的二位布局系统&#xff0c;是目前 CSS 最强的布局系统&#xff0c;它可以同时对列和行进行处理&#xff08;它将网页划分成一个个网格&#xff0c;可以任…

PHP函数$_FILES详解

PHP函数$_FILES详解 在PHP中上传一个文件建一个表单要比ASP中灵活得多。具体的看代码。 <form enctype"multipart/form-data" action"upload.php" method"post"> <input type"hidden" name"MAX_FILE_SIZE" value…

嵌入式入门学习——8基于Protues仿真Arduino+SSD1306液晶显示数字时钟

0 系列文章入口 嵌入式入门学习——0快速入门&#xff0c;Let‘s Do It&#xff01; SSD1306 1 Protues查找SSD1306器件并放置在画布&#xff0c;画好电气连接&#xff08;这里VCC和GND画反了&#xff0c;后面仿真出错我才看见&#xff0c;要是现实硬件估计就烧毁了&#xf…

【时时三省】(C语言基础)函数介绍strncat

山不在高&#xff0c;有仙则名。水不在深&#xff0c;有龙则灵。 ----CSDN 时时三省 strncat 打印结果是hello wor 跟strcat不同的是他后面可以加一个参数 这个参数就是它可以根据后面的数字 来追加多少个字符 这个如果后面的参数改成10的话 就是打印hello world 不会跟strn…

Appium环境搭建、Appium连接真机

文章目录 一、安装Android SDK二、安装Appium-desktop三、安装Appium Inspector 一、安装Android SDK 首先需要安装jdk&#xff0c;这里就不演示安装jdk的过程了 SDK下载地址&#xff1a;Android SDK 下载 1、点击 Android SDK 下载 -> SKD Tools 2、选择对应的版本进行下…

诊断知识:NRC78(Response Pending)的回复时刻

文章目录 前言NRC78的使用场景客户需求解读Autosar Dcm中的定义工具链中的配置总结 前言 在项目开发过程中&#xff0c;客户变更需求&#xff0c;是关于NRC78的回复时间点的&#xff0c;该需求在Autosar Dem中也有对应的参数&#xff0c;DcmTimStrP2ServerAdjust&#xff08;针…

Cortex-A7:如何切换ARM和Thumb状态

0 参考资料 ARM Cortex-A(armV7)编程手册V4.0.pdf1 Cortex-A7&#xff1a;如何切换ARM和Thumb状态 1.1 Cortex-A7支持的指令集 Cortex-A7支持的指令集包括ARM指令集和Thumb-2&#xff08;ARM官方一般用Thumb表示&#xff09;指令集。 ARM指令集指令大小都是32位&#xff0c;…

CLion和Qt 联合开发环境配置教程(Windows和Linux版)

需要安装的工具CLion 和Qt CLion下载链接 :https://www.jetbrains.com.cn/clion/ 这个软件属于直接默认安装就行&#xff0c;很简单&#xff0c;不多做介绍了 Qt:https://mirrors.tuna.tsinghua.edu.cn/qt/official_releases/online_installers/ window 直接点exe Linux 先c…

【一种比较万能的方法删除磁盘里删除不了的文件】

一种比较万能的方法删除磁盘里删除不了的文件 只需要以下三步&#xff1a; 1、运行命令提示符&#xff08;以管理员身份打开&#xff09; 2、修复磁盘文件&#xff0c;运行命令 chkdsk 文件所在目录 /f 3、del 文件所在目录

手写Spring IOC-简易版

目录 项目结构entitydaoIUserDaoUserDaoImpl serviceIUserServiceUserServiceImpl ApplicationContext 配置文件初始化 IOC 容器RunApplication 注解初始化 IOC 容器BeanAutowired Reference 项目结构 entity User Data NoArgsConstructor AllArgsConstructor Accessors(chai…

计算DOTA文件的IOU

背景 在目标检测任务中&#xff0c;评估不同对象之间的重叠情况是至关重要的&#xff0c;而IOU&#xff08;Intersection Over Union&#xff09;是衡量这种重叠程度的重要指标。本文将介绍如何编写一个Python脚本&#xff0c;通过并行化处理DOTA格式的标注文件&#xff0c;统…

JDK17下,使用SHA1算法报Certificates do not conform to algorithm constraints错误

JDK17从17.0.5开始&#xff0c;默认不再允许使用SHA1算法&#xff0c;如果引用的jar包或代码里使用了SHA1算法&#xff0c;会报以下错误。 Caused by: javax.net.ssl.SSLHandshakeException: Certificates do not conform to algorithm constraintsat java.base/sun.security.…

演示:基于WPF的DrawingVisual开发的高刷新率示波器

一、目的&#xff1a;分享一个基于WPF的DrawingVisual开发的高刷新率示波器 二、效果演示 特此说明&#xff1a;由于Gif录制工具帧率不够&#xff0c;渲染60帧用了4.6秒&#xff0c;平均帧率在12Hz左右&#xff0c;所以展示效果不好&#xff0c;想要看好些的效果可以看文章下面…