DroidCamX介绍
DroidCamX是一个可以将手机摄像头作为笔记本摄像头的app,有一个客户端.我通过Wireshark抓包模拟客户端向app发送tcp报文,从而实现展示图像,打开闪光灯,聚焦,限制FPS等功能.
可以在此基础上通过cv2的模型实现了人脸识别等等操作.
因为客户端的不同会导致部分功能的缺失.
在此附上DroidCamX.apk 破解版
基于TCP连接的DroidCamX客户端
#!/usr/bin/bin python3
# -*- coding: utf-8 -*-
# @Time : 2020/8/28 11:31
# @AuThor : SuperDeng
# @Email : 1821333144@qq.com
# @file : DroidCam_TCP_Client.py
# 基于TCP协议实现的DroidCam客户端
import sys
import time
import cv2
import socket, threading
import numpy as np
import requests
from tkinter import Radiobutton, IntVar, Button, Tk, messagebox
def bytes2cv(im):
'''二进制图片转cv2
:param im: 二进制图片数据,bytes
:return: cv2图像,numpy.ndarray
'''
return cv2.imdecode(np.array(bytearray(im), dtype='uint8'), cv2.IMREAD_UNCHANGED) # 从二进制图片数据中读取
def cv2bytes(im):
'''cv2转二进制图片
:param im: cv2图像,numpy.ndarray
:return: 二进制图片数据,bytes
'''
return np.array(cv2.imencode('.png', im)[1]).tobytes()
size_dict = {
'240x320': '240x320',
'320x240': '320x240',
'352x288': '352x288',
'480x320': '480x320',
'480x360': '480x360',
'480x640': '480x640',
'640x360': '640x360',
'640x480': '640x480',
'640x640': '640x640',
'720x480': '720x480',
'864x480': '864x480',
'1280x640': '1280x640',
'1280x720': '1280x720',
'1280x960': '1280x960',
'1920x960': '1920x960',
'1920x1080': '1920x1080',
}
tcp_func = {
# 'Limit_FPS': '/cam/1/fpslimit',
'Autofocus': b'CMD /v1/ctl?8',
'Toggle_LED': b'CMD /v1/ctl?9',
'Zoom_In': b'CMD /v1/ctl?7',
'Zoom_Out': b'CMD /v1/ctl?6',
# 'Save_Photo_on_SD': '/cam/1/takepic', # url
# 音频流 udp 发送至4748服务端
'Audio': b'CMD /v2/audio', # udp 客户端向4748发送,然后获取字节
'Stop': b'CMD /v1/stop' # # udp 客户端向4748发送
}
get_url = {
'Limit_FPS': '/cam/1/fpslimit',
'Autofocus': '/cam/1/af',
'Toggle_LED': '/cam/1/led_toggle',
'Zoom_In': '/cam/1/zoomin',
'Zoom_Out': '/cam/1/zoomout',
'Save_Photo_on_SD': '/cam/1/takepic',
}
s = b'\xff\xd8'
e = b'\xff\xd9'
class DroidCam_Client:
def __init__(self, master):
self.master = master
self.master.protocol("WM_DELETE_WINDOW", self.handler)
self.playEvent = threading.Event()
self.size = size_dict['640x640']
self.v = IntVar()
self.v.set(8)
self.createWidgets()
self.PlatState = False
def set_size(self):
self.size = list(size_dict.values())[self.v.get()]
def createWidgets(self):
"""Build GUI."""
j = 0
rr = 0
cc = -1
for key in size_dict:
if cc < 6:
cc += 1
else:
rr += 1
cc = 0
Radiobutton(self.master, variable=self.v, text=key, value=j, command=self.set_size).grid(row=rr, column=cc)
j += 1
s = len(size_dict) + 1
self.setup = Button(self.master, width=15, padx=3, pady=3)
self.setup["text"] = "Play"
self.setup["command"] = self.Play
self.setup.grid(row=s, column=0, padx=2, pady=2)
self.setup = Button(self.master, width=15, padx=3, pady=3)
self.setup["text"] = "Get_Audio"
self.setup["command"] = self.Get_Audio
self.setup.grid(row=s+1, column=0, padx=2, pady=2)
c = 1
for func in get_url:
btn = Button(self.master, width=15, padx=3, pady=3)
btn["text"] = func
btn["command"] = getattr(self, func)
btn.grid(row=s, column=c, padx=2, pady=2)
c += 1
def play(self, size, event):
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_eye.xml')
smile_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_smile.xml')
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sk.connect(("192.168.124.3", 4747))
s1 = 'CMD /v2/video{}?'.format(size)
print(s1)
sk.send(s1.encode('utf-8'))
s = sk.recv(1024)
jpeg_data = b''
tmp_data = b''
while 1:
msg = sk.recv(1024)
if e not in msg:
jpeg_data += msg
else:
if msg.endswith(e):
jpeg_data += msg
else:
a, b = msg.split(e)
jpeg_data = jpeg_data + a + e
tmp_data = b
frame = bytes2cv(jpeg_data[4:])
faces = face_cascade.detectMultiScale(frame, 1.3, 2)
img = frame
for (x, y, w, h) in faces:
# 画出人脸框,蓝色,画笔宽度微
img = cv2.rectangle(img, (x, y), (x + w, y + h), (255, 0, 0), 2)
# 框选出人脸区域,在人脸区域而不是全图中进行人眼检测,节省计算资源
face_area = img[y:y + h, x:x + w]
## 人眼检测
# 用人眼级联分类器引擎在人脸区域进行人眼识别,返回的eyes为眼睛坐标列表
eyes = eye_cascade.detectMultiScale(face_area, 1.3, 10)
for (ex, ey, ew, eh) in eyes:
# 画出人眼框,绿色,画笔宽度为1
cv2.rectangle(face_area, (ex, ey), (ex + ew, ey + eh), (0, 255, 0), 1)
## 微笑检测
# 用微笑级联分类器引擎在人脸区域进行人眼识别,返回的eyes为眼睛坐标列表
smiles = smile_cascade.detectMultiScale(face_area, scaleFactor=1.16, minNeighbors=65,
minSize=(25, 25),
flags=cv2.CASCADE_SCALE_IMAGE)
for (ex, ey, ew, eh) in smiles:
# 画出微笑框,红色(BGR色彩体系),画笔宽度为1
cv2.rectangle(face_area, (ex, ey), (ex + ew, ey + eh), (0, 0, 255), 1)
cv2.putText(img, 'Smile', (x, y - 7), 3, 1.2, (0, 0, 255), 2, cv2.LINE_AA)
# 实时展示效果画面
cv2.imshow(f"DroidCam{size}", img)
# 每5毫秒监听一次键盘动作
if cv2.waitKey(5) & 0xFF == ord('q'):
break
self.s = f"{len(img[0])}x{len(img)}"
key = cv2.waitKey(1)
if key == 27:
cv2.destroyWindow(f"DroidCam{size}")
event.set()
break
jpeg_data = tmp_data
tmp_data = b''
if event.isSet():
cv2.destroyWindow(f"DroidCam{size}")
break
def Play(self):
if not self.PlatState:
threading.Thread(target=self.play, args=(self.size, self.playEvent)).start()
self.PlatState = True
else:
self.playEvent.set()
self.OverRide()
self.playEvent.clear()
time.sleep(1)
threading.Thread(target=self.play, args=(self.size, self.playEvent)).start()
self.PlatState = True
def handler(self):
"""Handler on explicitly closing the GUI window."""
if messagebox.askokcancel("Quit?", "Are you sure you want to quit?"):
self.playEvent.set()
time.sleep(1)
self.master.destroy() # Close the gui window
sys.exit(0)
else: # When the user presses cancel, resume playing.
return
def Toggle_LED(self):
ret = requests.get('http://192.168.124.3:4747/cam/1/led_toggle')
def Limit_FPS(self):
ret = requests.get('http://192.168.124.3:4747/cam/1/fpslimit')
def Autofocus(self):
ret = requests.get('http://192.168.124.3:4747/cam/1/af')
def Zoom_In(self):
ret = requests.get('http://192.168.124.3:4747/cam/1/zoomin')
def Zoom_Out(self):
ret = requests.get('http://192.168.124.3:4747/cam/1/zoomout')
def OverRide(self):
ret = requests.get('http://192.168.124.3:4747/override')
def Save_Photo_on_SD(self):
ret = requests.get('http://192.168.124.3:4747/cam/1/takepic')
def Get_Audio(self):
f = open("123.mp3","wb")
sk = socket.socket(type=socket.SOCK_DGRAM)
ipaddr = ("192.168.124.3", 4748)
sk.sendto(b"CMD /v2/audio", ipaddr)
count = 0
while 1:
count += 1
msg, _ = sk.recvfrom(1024)
if msg:
# 解析音频数据
print(count)
f.write(msg[1:])
else:
break
if count == 10000: break
print("exit")
f.flush()
f.close()
sk.close()
def run():
root = Tk()
app = DroidCam_Client(root)
app.master.title("DroidCam_TCP_Client")
root.mainloop()
if __name__ == '__main__':
run()
基于http连接的的DroidCamX客户端
#!/usr/bin/bin python3
# -*- coding: utf-8 -*-
# @Time : 2020/8/1 19:42
# @Author : SuperDeng
# @Email : 1821333144@qq.com
# @File : socket_client.py
# 基于http协议传输的视频数据流播放
import sys
import time
import cv2
import socket, threading
import numpy as np
import requests
from tkinter import Radiobutton, IntVar, Button, Tk, messagebox
def bytes2cv(im):
'''二进制图片转cv2
:param im: 二进制图片数据,bytes
:return: cv2图像,numpy.ndarray
'''
return cv2.imdecode(np.array(bytearray(im), dtype='uint8'), cv2.IMREAD_UNCHANGED) # 从二进制图片数据中读取
def cv2bytes(im):
'''cv2转二进制图片
:param im: cv2图像,numpy.ndarray
:return: 二进制图片数据,bytes
'''
return np.array(cv2.imencode('.png', im)[1]).tobytes()
size_dict = {
'240p': '320x240',
'480p': '640x480',
'720p': '960x720',
'FHD 720p': '1280x720',
'FHD 1080p': '1920x1080',
}
get_url = {
'Limit_FPS': '/cam/1/fpslimit',
'Autofocus': '/cam/1/af',
'Toggle_LED': '/cam/1/led_toggle',
'Zoom_In': '/cam/1/zoomin',
'Zoom_Out': '/cam/1/zoomout',
'Save_Photo_on_SD': '/cam/1/takepic',
}
class DroidCam_Client:
def __init__(self, master):
self.master = master
self.master.protocol("WM_DELETE_WINDOW", self.handler)
self.playEvent = threading.Event()
self.size = size_dict['480p']
self.v = IntVar()
self.v.set(1)
self.createWidgets()
self.PlatState = False
def set_size(self):
self.size = list(size_dict.values())[self.v.get()]
def createWidgets(self):
"""Build GUI."""
j = 0
for key in size_dict:
Radiobutton(self.master, variable=self.v, text=key, value=j, command=self.set_size).grid()
j += 1
self.setup = Button(self.master, width=15, padx=3, pady=3)
self.setup["text"] = "Play"
self.setup["command"] = self.Play
self.setup.grid(row=5, column=0, padx=2, pady=2)
c = 1
for func in get_url:
btn = Button(self.master, width=15, padx=3, pady=3)
btn["text"] = func
btn["command"] = getattr(self, func)
btn.grid(row=5, column=c, padx=2, pady=2)
c += 1
def play(self, size, event):
print(size)
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sk.connect(("192.168.124.3", 4747))
data = 'GET /mjpegfeed?{} HTTP/1.1\r\nHost: 192.168.124.3:4747\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3\r\nAccept-Encoding: gzip, deflate\r\nConnection: keep-alive\r\nUpgrade-Insecure-Requests: 1\r\n\r\n'.format(
size)
sk.send(data.encode('utf-8'))
msg = sk.recv(1024)
recv_dict = msg.decode('utf-8').split('\r\n')
if 'Connection: Keep-Alive' in recv_dict:
self.__play(sk,size,event)
else:
print('DroidCam is connected to another client.')
print('Disconnect the other client and Take Over')
sk.close()
def __play(self,sk,size,event):
JPEG_header = b''
JPEG = b''
while True:
msg = sk.recv(1024)
if msg:
if b'\r\n\r\n' in msg:
# 分界点到了
JPEG_header += msg.split(b'\r\n\r\n')[0]
long = int(JPEG_header.split(b'\r\n')[-1].split(b':')[-1])
JPEG_header = b''
JPEG += msg.split(b'\r\n\r\n')[1]
while True:
JPEG += sk.recv(1024)
if len(JPEG) >= long:
img = bytes2cv(JPEG[:long])
cv2.imshow(f"DroidCam{size}", img)
cv2.waitKey(1)
JPEG_header += JPEG[long:]
JPEG = b''
break
else:
JPEG_header += msg
if event.isSet():
cv2.destroyWindow(f"DroidCam{size}")
break
sk.close()
def Play(self):
if not self.PlatState:
threading.Thread(target=self.play, args=(self.size, self.playEvent)).start()
self.PlatState = True
else:
self.playEvent.set()
self.OverRide()
self.playEvent.clear()
time.sleep(1)
threading.Thread(target=self.play, args=(self.size, self.playEvent)).start()
self.PlatState = True
def handler(self):
"""Handler on explicitly closing the GUI window."""
if messagebox.askokcancel("Quit?", "Are you sure you want to quit?"):
self.playEvent.set()
time.sleep(1)
self.master.destroy() # Close the gui window
sys.exit(0)
else: # When the user presses cancel, resume playing.
return
def Toggle_LED(self):
ret = requests.get('http://192.168.124.3:4747/cam/1/led_toggle')
def Limit_FPS(self):
ret = requests.get('http://192.168.124.3:4747/cam/1/fpslimit')
def Autofocus(self):
ret = requests.get('http://192.168.124.3:4747/cam/1/af')
def Zoom_In(self):
ret = requests.get('http://192.168.124.3:4747/cam/1/zoomin')
def Zoom_Out(self):
ret = requests.get('http://192.168.124.3:4747/cam/1/zoomout')
def OverRide(self):
ret = requests.get('http://192.168.124.3:4747/override')
def Save_Photo_on_SD(self):
ret = requests.get('http://192.168.124.3:4747/cam/1/takepic')
def run():
root = Tk()
app = DroidCam_Client(root)
app.master.title("DroidCam_Client")
root.mainloop()
if __name__ == '__main__':
run()
问题
无法实现音频流的解读,我先抛砖引玉,看看诸位大佬有啥办法没.
其他应用
手机摄像头,联网,调用百度API人脸识别等等,可以试着手机安装linux deploy装ubuntu系统,模拟成一个树莓派,来进行各种操作.