视频监控实现画面缩放功能

文章目录

    • 概要
    • 一、功能说明
    • 二、核心实现代码
    • 三、技术细节

概要

在视频监控系统中,经常需要查看视频画面中的细节。通过实现区域放大、滚轮缩放和拖拽平移等功能,可以让用户更方便地观察视频细节。本文介绍如何在 Windows 系统下实现这些交互功能。

一、功能说明

框选缩放功能:按住鼠标左键,移动鼠标画出矩形框,松开左键,会将框选区域放大至整个窗口显示。点击鼠标右键,即恢复显示原画面。

示例图片

鼠标滚轮缩放功能:通过鼠标滚轮控制缩放比例,以鼠标所在位置为中心进行缩放。

示例图片

鼠标拖拽功能:在放大状态下,按住左键可以拖动视频画面,查看不同区域。

示例图片

二、核心实现代码

// 检查区域
bool IsValidZoomRc(const RECT *pRC) {return pRC != nullptr && abs(pRC->left - pRC->right) > 4 && abs(pRC->top - pRC->bottom) > 4;
}// 左上右下位置相反时对调
inline void fmtRect(RECT &rc) {if (rc.left > rc.right) std::swap(rc.left, rc.right);if (rc.top > rc.bottom) std::swap(rc.top, rc.bottom);
}// 播放类
class CPlayer {// ...省略其他代码private:bool m_bZoom;  // 是否启用缩放功能HANDLE m_hPlayer; // 调用播放库的句柄POINT m_ptDragStart;  // 拖动起始点RECT m_rcDragStart;  // 拖动开始时的显示区域AV_STREAM_INFO m_stStreamInfo;  // 存放流信息的结构体RECT m_rcZoom;  // 记录当前显示的画面区域RECT m_rcDrawRect;  // 用于画框// 在播放库回调出来的句柄上画框选的方框void OnVideoDraw(HANDLE handle, HDC hdc) {if (m_bZoom) {auto oldPen = SelectObject(hdc, m_hDrawPen);auto oldBrush = SelectObject(hdc, m_hDrawBrush);Rectangle(hdc, m_rcDrawRect.left, m_rcDrawRect.top, m_rcDrawRect.right, m_rcDrawRect.bottom);SelectObject(hdc, oldPen);SelectObject(hdc, oldBrush);}}// 画框缩放接口void DoZoom(int wndWidth, int wndHeight, const RECT *pRc, bool bLRButtonUp) {if (!m_bZoom && m_hPlayer != (void*)INFINITE) {HS_ShowRect(m_hPlayer, nullptr);  // 调用播放库接口,还原画面return;}if (pRc != nullptr) {// 如果画面已经处于放大状态,不再支持画框if (((m_rcZoom.right - m_rcZoom.left) != m_stStreamInfo.nWidth) ||((m_rcZoom.bottom - m_rcZoom.top) != m_stStreamInfo.nHeight)) {return;}}if (bLRButtonUp || pRc == nullptr) {ZeroMemory(&m_rcDrawRect, sizeof(m_rcDrawRect));} else {m_rcDrawRect = *pRc;}double fWndWidth = wndWidth;double fWndHeight = wndHeight;if (bLRButtonUp) {if (pRc == nullptr) {if (m_hPlayer != (void*)INFINITE) {HS_ShowRect(m_hPlayer, nullptr);}SetRect(&m_rcZoom, 0, 0, m_stStreamInfo.nWidth, m_stStreamInfo.nHeight);} else {RECT rc = m_rcZoom, rcPrm = *pRc;double fWidth = (double)fWndWidth / abs(m_rcZoom.left - m_rcZoom.right),fHeight = (double)fWndHeight / abs(m_rcZoom.top - m_rcZoom.bottom);fmtRect(rcPrm);rc.left += (LONG)floor((double)rcPrm.left / fWidth);rc.right = rc.left + (LONG)floor((double)abs(rcPrm.left - rcPrm.right) / fWidth);rc.top += (LONG)floor((double)rcPrm.top / fHeight);rc.bottom = rc.top + (LONG)floor((double)abs(rcPrm.top - rcPrm.bottom) / fHeight);if (m_hPlayer != (void*)INFINITE) {bool bRet = IsValidZoomRc(&rc) && 0 == HS_ShowRect(m_hPlayer, &rc);  // 将要显示的区域传给播放库if (bRet) {m_rcZoom = rc;}}}    }}// 鼠标滚轮缩放接口void DoWheelZoom(POINT ptMouse, int wndWidth, int wndHeight, short zDelta) {if (!m_bZoom || m_hPlayer == (void*)INFINITE) {return;}// 计算缩放比例 - 每次放大20%或缩小25%double scale = (zDelta > 0) ? 0.8 : 1.25;// 当前视频显示区域的宽高int currentWidth = m_rcZoom.right - m_rcZoom.left;int currentHeight = m_rcZoom.bottom - m_rcZoom.top;// 新的视频显示区域宽高int newWidth = (int)(currentWidth * scale);int newHeight = (int)(currentHeight * scale);if (zDelta > 0) {// 这里控制最多放大11次double minWidth = m_stStreamInfo.nWidth * pow(scale, 12);if (newWidth < minWidth) {return;}} else {// 这里控制只能缩小到视频原宽高if (newWidth > m_stStreamInfo.nWidth ||newHeight > m_stStreamInfo.nHeight) {newWidth = m_stStreamInfo.nWidth;newHeight = m_stStreamInfo.nHeight;}}// 计算鼠标在窗口坐标系中的相对位置(0-1范围)double mouseXRatio = ptMouse.x / (double)wndWidth;double mouseYRatio = ptMouse.y / (double)wndHeight;// 计算新的视频缩放区域,以鼠标位置为中心点RECT rcNew = {};int widthDiff = currentWidth - newWidth;int heightDiff = currentHeight - newHeight;rcNew.left = m_rcZoom.left + (int)(widthDiff * mouseXRatio);rcNew.top = m_rcZoom.top + (int)(heightDiff * mouseYRatio);rcNew.right = rcNew.left + newWidth;rcNew.bottom = rcNew.top + newHeight;// 确保不会超出视频边界if (rcNew.left < 0) {rcNew.right -= rcNew.left;rcNew.left = 0;}if (rcNew.top < 0) {rcNew.bottom -= rcNew.top;rcNew.top = 0;}if (rcNew.right > m_stStreamInfo.nWidth) {rcNew.left -= (rcNew.right - m_stStreamInfo.nWidth);rcNew.right = m_stStreamInfo.nWidth;}if (rcNew.bottom > m_stStreamInfo.nHeight) {rcNew.top -= (rcNew.bottom - m_stStreamInfo.nHeight);rcNew.bottom = m_stStreamInfo.nHeight;}// 应用新的缩放区域if (IsValidZoomRc(&rcNew)) {HS_ShowRect(m_hPlayer, &rcNew);m_rcZoom = rcNew;}}// 拖拽画面接口void DragMoveZoom(POINT ptMouse, int wndWidth, int wndHeight, bool bLBtnDown) {if (!m_bZoom || m_hPlayer == (void*)INFINITE) {return;}if (bLBtnDown) {  // 鼠标左键按下事件m_ptZoomDragStart = ptMouse;m_rcZoomDragStart = m_rcZoom;}// 画面未处于放大状态,不支持拖拽if (((m_rcZoom.right - m_rcZoom.left) == m_stStreamInfo.nWidth) &&((m_rcZoom.bottom - m_rcZoom.top) == m_stStreamInfo.nHeight)) {return;}// 计算鼠标移动的距离int deltaX = m_ptZoomDragStart.x - ptMouse.x;int deltaY = m_ptZoomDragStart.y - ptMouse.y;// 根据窗口和实际视频的比例计算实际需要移动的距离double scaleX = (double)(m_rcZoom.right - m_rcZoom.left) / wndWidth;double scaleY = (double)(m_rcZoom.bottom - m_rcZoom.top) / wndHeight;// 计算实际移动距离int actualDeltaX = (int)(deltaX * scaleX);int actualDeltaY = (int)(deltaY * scaleY);// 计算新的显示区域RECT rcNew = m_rcZoomDragStart;rcNew.left += actualDeltaX;rcNew.right += actualDeltaX;rcNew.top += actualDeltaY;rcNew.bottom += actualDeltaY;// 边界检查if (rcNew.left < 0) {rcNew.right -= rcNew.left;rcNew.left = 0;}if (rcNew.top < 0) {rcNew.bottom -= rcNew.top;rcNew.top = 0;}if (rcNew.right > m_stStreamInfo.nWidth) {rcNew.left -= (rcNew.right - m_stStreamInfo.nWidth);rcNew.right = m_stStreamInfo.nWidth;}if (rcNew.bottom > m_stStreamInfo.nHeight) {rcNew.top -= (rcNew.bottom - m_stStreamInfo.nHeight);rcNew.bottom = m_stStreamInfo.nHeight;}// 应用新的显示区域if (IsValidZoomRc(&rcNew)) {HS_ShowRect(m_hPlayer, &rcNew);m_rcZoom = rcNew;}}// ...省略其他代码
};// 窗口类
class CPlayWnd {// ...省略其他代码private:RECT m_arrBlock;  // 记录框选的区域bool m_bLBottonDowned;  // 记录鼠标左键是否处于按下状态int m_nWndWidth;int m_nWndHeight;public:// 窗口事件响应函数LRESULT ChildWinMsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {switch (msg) {// ...省略其他代码case WM_LBUTTONDOWN: {m_bLBottonDowned = true;// SetCapture(hWnd);  // 捕获鼠标POINT ptMouse = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };auto &rc = m_arrBlock;rc.left = rc.right = LOWORD(lParam);rc.top = rc.bottom = HIWORD(lParam);/* 这里通知播放类CPlayer,调用到CPlayer的缩放和拖拽接口 *//* OnDoZoom(m_nWndWidth, m_nWndHeight, &rc, false); *//* OnDragZoom(m_nWndWidth, m_nWndHeight, ptMouse, true); */} break;case WM_MOUSELEAVE: {if (m_bLBottonDowned) {m_bLBottonDowned = false;auto &rc = m_arrBlock;/* 这里通知播放类CPlayer,调用到CPlayer的缩放接口 *//* OnDoZoom(m_nWndWidth, m_nWndHeight, &rc, true); */}} break;case WM_MOUSEMOVE: {// 设置捕获鼠标离开窗口事件TRACKMOUSEEVENT trackEvent;trackEvent.cbSize = sizeof(TRACKMOUSEEVENT);trackEvent.dwFlags = TME_LEAVE;trackEvent.hwndTrack = hWnd;TrackMouseEvent(&trackEvent);if (m_bLBottonDowned) {POINT ptMouse = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };auto &rc = m_arrBlock;rc.right = LOWORD(lParam);rc.bottom = HIWORD(lParam);/* 这里通知播放类CPlayer,调用到CPlayer的缩放和拖拽接口 *//* OnDoZoom(m_nWndWidth, m_nWndHeight, &rc, false); *//* OnDragZoom(m_nWndWidth, m_nWndHeight, ptMouse, false); */}} break;case WM_MOUSEWHEEL: {POINT ptMouse;// 鼠标在屏幕的坐标ptMouse.x = GET_X_LPARAM(lParam);ptMouse.y = GET_Y_LPARAM(lParam);// 转换为相对于窗口的坐标ScreenToClient(hWnd, &ptMouse);// 获取滚轮delta值short zDelta = GET_WHEEL_DELTA_WPARAM(wParam);/* 这里通知播放类CPlayer,调用到CPlayer的滚轮缩放接口 *//* OnWheelZoom(ptMouse, m_nWndWidth, m_nWndHeight, zDelta); */} break;case WM_LBUTTONUP: {if (m_bLBottonDowned) {// ReleaseCapture();  // 释放鼠标捕获m_bLBottonDowned = false;auto &rc = m_arrBlock;rc.right = LOWORD(lParam);rc.bottom = HIWORD(lParam);/* 这里通知播放类CPlayer,调用到CPlayer的缩放接口 *//* OnDoZoom(m_nWndWidth, m_nWndHeight, &rc, true); */}} break;case WM_RBUTTONUP: {/* 这里通知播放类CPlayer,调用到CPlayer的缩放接口 *//* OnDoZoom(0, 0, nullptr, true); */} break;case WM_LBUTTONDBLCLK: {/* 这里通知播放类CPlayer,调用到CPlayer的缩放接口 *//* OnDoZoom(0, 0, nullptr, true); */} break;// ...省略其他代码}}// ...省略其他代码
};

三、技术细节

  • 本文主要介绍的是如何计算出显示区域,计算出来后将结果传给播放库去显示,播放库代码不在本文展示
  • 使用GET_X_LPARAMGET_Y_LPARAM替代LOWORDHIWORD处理坐标
  • 正确处理屏幕坐标到客户区坐标的转换
  • 不使用SetCapture时,鼠标移出窗口后消息中断,拖动操作可能无法正常完成
  • 使用SetCapture后,可以跟踪整个拖动过程,即使鼠标移出窗口也能继续操作

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

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

相关文章

鸿蒙本地模拟器 模拟TCP服务端的过程

鸿蒙模拟器模拟TCP服务端的过程涉及几个关键步骤&#xff0c;主要包括创建TCPSocketServer实例、绑定IP地址和端口、监听连接请求、接收和发送数据以及处理连接事件。以下是详细的模拟过程&#xff1a; **1.创建TCPSocketServer实例&#xff1a;**首先&#xff0c;需要导入鸿蒙…

Three.js 和其他 WebGL 库 对比

在WebGL开发中&#xff0c;Three.js是一个非常流行的库&#xff0c;它简化了3D图形的创建和渲染过程。然而&#xff0c;市场上还有许多其他的WebGL库&#xff0c;如 Babylon.js、PlayCanvas、PIXI.js 和 Cesium&#xff0c;它们也有各自的特点和优势。本文将对Three.js 与这些常…

【04】MySQL数据库和数据表的基本操作详解与实例

文章目录 一、连接MySQL服务器二、数据库的基本操作2.1数据库的基本操作1. 创建数据库2. 选择数据库3. 删除数据库4.查询所有数据库5.修改数据库的字符集 2.2 数据表的基本操作1. 创建数据表2. 查看数据表结构3. 删除数据表4. 修改数据表5. 插入数据6. 查询数据7. 更新数据8. 删…

CTF-Hub SQL 报错注入(纯手动注入)

​ 当输入1时&#xff0c;发现只有查询正确&#xff0c;基本上可以判断出没有回显 开始注入(工具hackerBar) 题目是报错注入&#xff0c;方向就比较明显&#xff0c;大致说一下用到的函数和原理。 常见报错注入函数&#xff1a; 通过 floor() 报错注入通过 extractValue() …

2024 阿里云的Debian12.8,安装mariadb【图文讲解】

目录 一、安装 MariaDB Server 二、登录到MariaDB&#xff0c;记得输入密码&#xff08;注意&#xff1a;密码非明文&#xff0c;只管输入&#xff0c;完成以后回车&#xff09; 三、创建用户 root&#xff0c;并允许从任何主机连接 四、授予用户访问权限 五、刷新权限 六、…

新用户引导库-driverjs

一个比好用的新用户引导的库 driverjs 在做这个功能时&#xff0c;首先要确定目标是什么样子的&#xff0c; 如果只是随意点击下一步下一步&#xff0c;那我感觉可能用图片轮播图的方式会快一点&#xff0c;更容易解决且方便&#xff0c;想要什么步骤 只需要更改图片就好&…

鸿蒙保存读取沙盒文件

鸿蒙保存读取沙盒文件 参考文件 有些时候需要保存并读取沙盒环境的文件。这样做保存一些临时文件&#xff0c;确保发送网络之前数据不会丢失&#xff0c;或者存储一些只需要在本地使用的数据等等。本文介绍一下相关的操作方式。 获取文件路径 想要保存或者读取文件&#xf…

八、利用CSS制作导航栏菜单的习题

题目一&#xff1a; 利用CSS技术&#xff0c;结合链接和样表&#xff0c;设计并实现“ 山水之间 ”页面。 运行效果&#xff1a; 代码 <!DOCTYPE html> <html><head><meta charset"utf-8" /><title>山水之间</title>&l…

ML 系列:第 31 节— 机器学习中的协方差和相关性

文章目录 一、说明二、协方差和相关性2.1 协方差的概念2.1 相关 三、有关关联的高级主题 &#xff08;有关详细信息&#xff09;3.1 相关性和独立性3.2 零相关性和依赖性示例 四、相关性和因果关系五、结论 一、说明 协方差量化了两个随机变量协同变化的程度。当一个变量的较高…

谈谈微服务的常用组件

由于微服务给系统开发带来了一些问题和挑战&#xff0c;如服务调用的复杂性、分布式事务的处理、服务的动态管理等&#xff0c;为了更好地解决这些问题和挑战&#xff0c;各种微服务治理的组件应运而生&#xff0c;充当微服务架构的基石和支撑&#xff0c;常用组件如下表&#…

2024算法基础公选课练习七(BFS1)

一、前言 还是偏基础的bfs&#xff0c;但是有几个题不是很好写 二、题目总览 三、具体题目 3.1 问题 A: 数据结构-队列-奇怪的电梯 我的代码 可以看成求一维平面的bfs最短路 #include <bits/stdc.h> using i64 long long; using pii std::pair<int,int>; co…

探索.NET世界的无限可能——带你轻松了解.NET

前言 由于目前用到的技术栈有C#&#xff0c;而学习C#离不开.NET框架&#xff0c;正如学习Java离不开学习Spring框架一样。 .NET是微软开发的一个非常强大的框架&#xff0c;它不仅擅长桌面和移动开发&#xff0c;而且还能够支持Web开发和游戏引擎开发&#xff0c;在现在热门的…

[OpenHarmony5.0][Docker][环境]OpenHarmony5.0 Docker pull线上镜像方式构建编译环境

T. 已测试目录 主机类型主机版本Docker镜像版本结果WSL2Ubuntu22.04Ubuntu20.04PASSWSL2Ubuntu22.04Ubuntu18.04PASS R. 软硬件要求&#xff1a; 硬件&#xff1a; 设备容量备注硬盘>500G多版本系统测试&#xff0c;必须固态&#xff0c;否则编译卡死硬盘>300G单系统…

RHCE——SELinux

SELinux 什么是SELinux呢&#xff1f;其实它是【Security-Enhanced Linux】的英文缩写&#xff0c;字母上的意思就是安全强化Linux的意思。 SELinux是由美国国家安全局(NSA)开发的&#xff0c;当初开发的原因是很多企业发现&#xff0c;系统出现问题的原因大部分都在于【内部…

Python - 函数(四)

函数&#xff1a;在编写程序的过程中&#xff0c;有某一功能代码块出现多次&#xff0c; 但是为了提高编写的效率以及代码的重用&#xff0c;所以把具有独立功能的代码块组织为一个小模块&#xff0c;这就是函数 ‌Python中的函数‌是一组被命名的可执行代码&#xff0c;用于完…

代码随想录打卡DAY21

算法记录第21天 [二叉树] 1.LeetCode 538. 把二叉搜索树转换为累加树 题目描述&#xff1a; 给出二叉 搜索 树的根节点&#xff0c;该树的节点值各不相同&#xff0c;请你将其转换为累加树&#xff08;Greater Sum Tree&#xff09;&#xff0c;使每个节点 node 的新值等于原…

[在线实验]-ActiveMQ Docker镜像的下载与部署

镜像下载 下载ActiveMQ的Docker镜像文件。通常&#xff0c;这些文件会以.tar格式提供&#xff0c;例如activemq.tar。 docker的activemq镜像资源-CSDN文库 加载镜像 下载完成后&#xff0c;您可以使用以下命令将镜像文件加载到Docker中&#xff1a; docker load --input a…

VTK中对于相机camera的设置

1. 相机的核心属性 在 VTK 中&#xff0c;vtkCamera 的核心属性有默认值。如果你不设置这些属性&#xff0c;相机会使用默认值来渲染场景。 Position&#xff08;默认值&#xff1a;(0, 0, 1)&#xff09;&#xff1a; 默认情况下&#xff0c;相机位于 Z 轴正方向的 (0, 0, 1)…

学习日志017--python的几种排序算法

冒泡排序 def bubble_sort(alist):i 0while i<len(alist):j0while j<len(alist)-1:if alist[j]>alist[j1]:alist[j],alist[j1] alist[j1],alist[j]j1i1l [2,4,6,8,0,1,3,5,7,9] bubble_sort(l) print(l) 选择排序 def select_sort(alist):i 0while i<len(al…

超高流量多级缓存架构设计!

文章内容已经收录在《面试进阶之路》&#xff0c;从原理出发&#xff0c;直击面试难点&#xff0c;实现更高维度的降维打击&#xff01; 文章目录 电商-多级缓存架构设计多级缓存架构介绍多级缓存请求流程负载均衡算法的选择轮询负载均衡一致性哈希负载均衡算法选择 应用层 Ngi…