python图形差分传输实现windows桌面监视
图像的差分传输(Differential Transmission of Images)是指通过传输图像的差分信息而不是原始图像数据来实现更高效的数据传输。这种方法在图像压缩和传输中非常有用,因为它可以显著减少需要传输的数据量,从而提高传输效率和速度。
以下是图像差分传输的基本原理和步骤:
- 当前图像和参考图像:在差分传输中,通常会有一个参考图像(可能是之前传输过的图像或接收方已经拥有的图像)和一个需要传输的当前图像。参考图像和当前图像之间可能存在一定的差异。
- 计算差分图像:差分传输的核心是计算当前图像和参考图像之间的差异,即差分图像。差分图像的每个像素值表示当前图像和参考图像对应像素值的差异。例如,如果当前图像的像素值为A,参考图像的像素值为B,那么差分图像的像素值就是A - B。
- 传输差分数据:将压缩后的差分图像数据通过网络传输到接收端。由于差分图像的数据量较小,这一步通常比传输完整的原始图像更高效。
- 重建图像:接收端接收到差分图像后,会利用参考图像和差分图像重建出当前图像。重建过程是将参考图像和差分图像进行逆操作,即对每个像素值进行相加(如果差分图像的像素值为A - B,那么重建时的操作是B + (A - B) = A)。
远程桌面监控效果展示
当前图像和参考图像
在图形差分传输下,参考图像通常是上一帧图,当前图像是当前帧图,以下给出两张图用作案例演示:
上一帧 | 当前帧 |
计算差分图像
使用Python计算差分图像可以通过一些图像处理库来实现,例如OpenCV或Pillow。以下是使用OpenCV实现差分图像计算的一个简单示例,将上述两个图像进行比较,并标记差异区域:
- 注意
- 当图像文件路径存在中文时,使用
cv2.imread
和cv2.imwrite
读取和保存图像时会报错,需要使用cv2.imdecode
和cv2.imencode
替换 - OpenCV进行差异比较时需要先将图像转换成灰度图像
- 通过
cv2.threshold
修改差异阈值可以提高差异识别灵敏度,以找到更细微的差异,或者降低灵敏度,以提高执行效率 - 使用pyautogui获取当前屏幕分辨率,根据比例使用
cv2.resize
调整展示的图像尺寸
- 当图像文件路径存在中文时,使用
import cv2
import numpy as np
import pyautogui
with
open(
r"D:/图像传输/prev.png", "rb"
) as prev,
open(
r"D:/图像传输/current.png", "rb"
) as current:
# 加载上一帧图
previous_frame = cv2.imdecode(
np.frombuffer(prev.read(), np.uint8), cv2.IMREAD_GRAYSCALE
)
# 加载当前帧图
current_frame = cv2.imdecode(
np.frombuffer(current.read(), np.uint8), cv2.IMREAD_GRAYSCALE
)
# 计算两帧之间的差异
diff = cv2.absdiff(current_frame, previous_frame)
# 设置差异阈值
_, thresholded_diff = cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY)
# 找到变化区域的轮廓
contours, _ = cv2.findContours(
thresholded_diff, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
# 在当前帧上标记变化区域
marked_image = cv2.cvtColor(current_frame, cv2.COLOR_GRAY2BGR)
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
cv2.rectangle(marked_image, (x, y), (x + w, y + h), (0, 255, 0), 2)
# 保存结果
cv2.imencode(".png", marked_image)[1].tofile(
r"D:/图像传输/comp.png"
)
# 显示结果
screen_width, screen_height = pyautogui.size()
scale = screen_height / marked_image.shape[0] - 0.1
marked_image = cv2.resize(marked_image, None, fx=scale, fy=scale)
cv2.imshow("Marked Image", marked_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
- 结果图
传输差分数据
由上节可知,差分数据存储在contours
中,但是我们不需要将这个对象的全部数据传输过去,我们只需要传输差异图像、图像尺寸以及图像的坐标信息就可以支持后续的重建。
- 本节需要注意的点:
- 要传输原图数据,而不是差分计算用到的灰度图像。因此需要使用差分数据对原图进行切片,代码片段如下:
for contour in contours: x, y, w, h = cv2.boundingRect(contour) # 原图切片 slice = original[y : y + h, x : x + w]
- 当存在多个差异点时,将差异数据合并传输,可以提高传输效率。但是合并传输需要规定数据包的结构和格式,本例使用以下规定:
包结构:{<信息头><图像描述><图像内容>} 信息头:{<身份标识><切片数量>} 身份标识:16字节 切片数量:4字节,0则表示替换整个图像,>=0则差分更新上一帧 图像描述:{<切片信息><切片信息>...} 切片信息:{<切片大小><切片高度><切片宽度><X轴坐标><Y轴坐标>} 切片大小:4字节,int 切片高度:4字节,int 切片宽度:4字节,int X轴坐标:4字节,int Y轴坐标:4字节,int 图像内容:切片有序排列组合,大小=所有切片大小之和
- 参考上述规定,差分数据的发送代码段如下:
# 发送变化区域图像数据 body = b"" header = b"" desc = b"" # 添加切片数量 header = id.bytes + len(contours).to_bytes(4, byteorder="big") for contour in contours: x, y, w, h = cv2.boundingRect(contour) # 原图切片 slice = mat[y : y + h, x : x + w] # 将切片图像转换为字节流 _, slice_encoded = cv2.imencode(".png", slice) slice_bytes = slice_encoded.tobytes() # 图像大小、尺寸、位置信息 size = len(slice_bytes) slice_info = ( size.to_bytes(4, byteorder="big") + h.to_bytes(4, byteorder="big") + w.to_bytes(4, byteorder="big") + x.to_bytes(4, byteorder="big") + y.to_bytes(4, byteorder="big") ) desc += slice_info body += slice_bytes # 发送数据 sock.sendall(header) sock.sendall(desc) sock.sendall(body)
- 参考上述规定,差分数据的接收代码段如下:
# 接收消息头 header_bytes = sock.recv(20) id_bytes = header_bytes[0:16] # 解析图像数量 num_bytes = header_bytes[16:20] num = int.from_bytes(num_bytes, byteorder="big") # 解析图像大小、尺寸、位置信息 desc_bytes = sock.recv(4 * 5 * num) contours = [] body_size = 0 for i in range(num): offset = i * 20 size = int.from_bytes(desc_bytes[0 + offset : 4 + offset], byteorder="big") h = int.from_bytes(desc_bytes[4 + offset : 8 + offset], byteorder="big") w = int.from_bytes(desc_bytes[8 + offset : 12 + offset], byteorder="big") x = int.from_bytes(desc_bytes[12 + offset : 16 + offset], byteorder="big") y = int.from_bytes(desc_bytes[16 + offset : 20 + offset], byteorder="big") # 这里自定义的contours数据格式,方便后续重建图像使用 contours.append([size, h, w, x, y, b""]) body_size += size body_bytes = b"" # 接收切片数据 while len(body_bytes) < body_size: packet = sock.recv(body_size - len(body_bytes)) if not packet: break body_bytes += packet # 拆分切片数据 body_slice_begin = 0 for contour in contours: size = contour[0] body_slice_end = body_slice_begin + size contour[5] = body_bytes[body_slice_begin:body_slice_end] body_slice_begin = body_slice_end # 接收的切片数据全部存储在contours中
重建图像
使用上一步接收的contours数据进行重建
# 重建图像
next_frame = np.copy(prevframe)
for contour in contours:
h = contour[1]
w = contour[2]
x = contour[3]
y = contour[4]
# 将字节流转换成图像
slice_cv = cv2.imdecode(
np.frombuffer(contour[5], np.uint8), cv2.IMREAD_UNCHANGED
)
slice_cv = slice_cv.reshape((h, w, -1))
next_frame[y : y + h, x : x + w] = slice_cv
# 展示图像
cv2.imshow("Image", next_frame)
cv2.waitKey(0)
cv2.destroyAllWindows()
完整代码(可运行)
- 接收端
import cv2
import numpy as np
import threading
import socket
import time
import pyautogui
frames = []
previous_frame = None
def initSocket():
global frames
global previous_frame
# 监听地址和端口
server_address = ("", 8080)
# 创建TCP套接字
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定到指定地址和端口
sock.bind(server_address)
# 开始监听
sock.listen(1)
print("等待连接...")
while True:
# 接受连接
conn, addr = sock.accept()
print("连接来自:", addr)
try:
while True:
# 接收图像数量
num_bytes = conn.recv(4)
num = int.from_bytes(num_bytes, byteorder="big")
refreshAll = False
if refreshAll := (num == 0):
num = 1
# 接收图像大小、尺寸、位置
info_bytes = conn.recv(4 * 5 * num)
contours = []
for i in range(num):
offset = i * 20
size = int.from_bytes(
info_bytes[0 + offset : 4 + offset], byteorder="big"
)
h = int.from_bytes(
info_bytes[4 + offset : 8 + offset], byteorder="big"
)
w = int.from_bytes(
info_bytes[8 + offset : 12 + offset], byteorder="big"
)
x = int.from_bytes(
info_bytes[12 + offset : 16 + offset], byteorder="big"
)
y = int.from_bytes(
info_bytes[16 + offset : 20 + offset], byteorder="big"
)
contours.append([size, h, w, x, y, b""])
# 接收图像数据
for contour in contours:
size = contour[0]
frame = contour[5]
while len(frame) < size:
packet = conn.recv(size - len(frame))
if not packet:
break
frame += packet
contour[5] = frame
# 展示图像
if previous_frame is None or refreshAll:
# 将字节流转换成图像
frame_cv = cv2.imdecode(
np.frombuffer(contours[0][5], np.uint8), cv2.IMREAD_UNCHANGED
)
frames.append(frame_cv)
previous_frame = frame_cv
else:
next_frame = np.copy(previous_frame)
for contour in contours:
h = contour[1]
w = contour[2]
x = contour[3]
y = contour[4]
# 将字节流转换成图像
slice_cv = cv2.imdecode(
np.frombuffer(contour[5], np.uint8), cv2.IMREAD_UNCHANGED
)
slice_cv = slice_cv.reshape((h, w, -1))
next_frame[y : y + h, x : x + w] = slice_cv
# 展示图像
frames.append(next_frame)
previous_frame = next_frame
except:
pass
finally:
conn.close()
def show():
global frames
# 设置最大帧速率
frame_rate = 60
screen_width, screen_height = pyautogui.size()
while True:
begin = time.time()
if len(frames) > 0:
frame = frames.pop(0)
scale = screen_height / frame.shape[0]-0.1
frame = cv2.resize(frame, None, fx=scale, fy=scale)
cv2.imshow("image", frame)
cv2.resizeWindow('image', frame.shape[1], frame.shape[0])
cv2.waitKey(int(1000 / frame_rate))
end = time.time()
# 打印帧数
# print(1 / (end - begin))
if __name__ == "__main__":
thread1 = threading.Thread(target=show)
thread2 = threading.Thread(target=initSocket)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
- 发送端
import cv2
import mss
import mss.base
import mss.screenshot
import numpy as np
import time
import socket
# 截屏对象转换成OpenCV对象
def screenshot2Mat(screenshot: mss.screenshot.ScreenShot):
return np.array(screenshot)
# 打印动作帧数
def printFrameRate(original_func):
def wrapper(cls, *args, **kwrd):
begin = time.time()
result = original_func(cls, *args, **kwrd)
end = time.time()
# 打印帧数
print(1 / (end - begin))
return result
return wrapper
# 将OpenCV对象转换为字节流
def mat2Bytes(mat: cv2.typing.MatLike):
_, img_encoded = cv2.imencode(".png", mat)
return img_encoded.tobytes()
class ScreenCatcher:
# 上一帧图
prevframe: cv2.typing.MatLike = None
def __init__(self) -> None:
self.initSock()
self.initMonitor()
# 连接远程服务器
def initSock(self):
# 远程服务器的地址和端口
server_address = ("127.0.0.1", 8080)
# 创建TCP套接字
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect(server_address)
# 获取截屏区域
def initMonitor(self):
# 获取屏幕尺寸
with mss.mss() as sct:
monitor = sct.monitors[0]
screen_width = monitor["width"]
screen_height = monitor["height"]
# 设置捕获区域(这里设置为整个屏幕)
self.monitor = {
"top": 0,
"left": 0,
"width": screen_width,
"height": screen_height,
}
# 截屏动作
# @printFrameRate
def shooting(self, sct: mss.base.MSSBase):
# 获取屏幕截图
frame = sct.grab(self.monitor)
frame_mat = screenshot2Mat(frame)
if self.prevframe is None:
header, body = self.refresh(frame_mat)
else:
header, body = self.update(frame_mat)
if header is None:
return
# 发送图像大小、尺寸、位置
self.sock.sendall(header)
# 发送图像数据
self.sock.sendall(body)
self.prevframe = cv2.cvtColor(frame_mat, cv2.COLOR_BGR2GRAY)
# 生成刷新帧数据
def refresh(self, mat: cv2.typing.MatLike):
print("发送刷新帧")
body = mat2Bytes(mat)
size = len(body)
height, width, channel = mat.shape
x = y = 0
header = (
int(0).to_bytes(4, byteorder="big")
+ size.to_bytes(4, byteorder="big")
+ height.to_bytes(4, byteorder="big")
+ width.to_bytes(4, byteorder="big")
+ x.to_bytes(4, byteorder="big")
+ y.to_bytes(4, byteorder="big")
)
return header, body
# 生成更新帧数据
def update(self, mat: cv2.typing.MatLike):
# 转换图片格式
curframe = cv2.cvtColor(mat, cv2.COLOR_BGR2GRAY)
# 计算两帧之间的差异
diff = cv2.absdiff(curframe, self.prevframe)
# 应用阈值处理
_, thresholded_diff = cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY)
# 找到变化区域的轮廓
contours, _ = cv2.findContours(
thresholded_diff, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
if len(contours) == 0:
return None, None
# 切片过多则执行刷新动作
if len(contours) > 3000:
print("切片数量过多:", len(contours))
return self.refresh(mat)
# 发送变化区域图像数据
body = b""
header = b""
# 添加切片数量
header += len(contours).to_bytes(4, byteorder="big")
slice_nbytes = 0
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
# 变化区域图像切片
slice = mat[y : y + h, x : x + w]
# 将切片图像转换为字节流
_, slice_encoded = cv2.imencode(".png", slice)
slice_bytes = slice_encoded.tobytes()
# 发送图像大小、尺寸、位置
size = len(slice_bytes)
slice_info = (
size.to_bytes(4, byteorder="big")
+ h.to_bytes(4, byteorder="big")
+ w.to_bytes(4, byteorder="big")
+ x.to_bytes(4, byteorder="big")
+ y.to_bytes(4, byteorder="big")
)
header += slice_info
body += slice_bytes
slice_nbytes += slice.nbytes
# 切片过大则执行刷新动作
size_rate = slice_nbytes / mat.nbytes * 100
if size_rate > 90:
print("切片尺寸过大:", size_rate, "%")
return self.refresh(mat)
return header, body
def main():
catcher = ScreenCatcher()
with mss.mss() as sct:
while True:
catcher.shooting(sct)
cv2.destroyAllWindows()
if __name__ == "__main__":
main()