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

文章目录

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

概要

在视频监控系统中,经常需要查看视频画面中的细节。通过实现区域放大、滚轮缩放和拖拽平移等功能,可以让用户更方便地观察视频细节。本文介绍如何在 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_LPARAM和GET_Y_LPARAM替代LOWORD和HIWORD处理坐标正确处理屏幕坐标到客户区坐标的转换不使用SetCapture时,鼠标移出窗口后消息中断,拖动操作可能无法正常完成使用SetCapture后,可以跟踪整个拖动过程,即使鼠标移出窗口也能继续操作

Copyright © 2022 世界杯进球_国足进世界杯了吗 - fulitb.com All Rights Reserved.