本帖最后由 RoachWZ 于 2019-11-30 21:07 编辑
以上对此设计进行了简单介绍,以下详细说说此设计的各部分详解。 远程视频遥控详解
手机端的摄像头采集到的原始数据数据是YUV格式。建立YuvImage对象image用来存储YUV格式的原始数据。原始数据太大,需要再通过调用image.compressToJpeg()将YUV格式图像数据转为jpg格式。然后启动发送线程,通过socket将每一帧的图像发送到电脑端接收,电脑窗体再一帧一帧播放,形成视频效果。 由于时间不足,所以没有再花时间去学习相关的视频流处理原理和技术。在此使用的是动画播放原理。在基于安卓的视频遥控小车——电脑端开发也说到了,实时视频是通过电脑窗体一帧一帧播放图片,形成视频动画效果。摄像头采集到的是最低分辨率,这样每一帧图像的数据量就小了,延迟也就下去了。 手机端实时视频功能的程序流程图如下图所示。 下面来对主要步骤进行详细介绍。 对于摄像头的操作实际上是安卓自定义相机开发。直接控制相机,比调用系统相机要难一些。首先要访问相机资源,打开摄像头的语句如下。 Camera.open(id); Id表示摄像头的编号,后置摄像头为0,前置摄像头为1。在调用open()时不传入参数指定打开哪个摄像头,默认是0。 摄像头采集到的原始数据是YUV格式的数据,结构如下,其参数作用如下表所示。 YuvImage image = new YuvImage(byte[] yuv, int format, int width, int height, int[] strides);
参数 | 类型 | 作用 | yuv | byte | YUV数据。在多个图像平面的情况下,所有平面必须连接成单个字节数组。 | format | int | YUV数据格式,如ImageFormat。 | width | int | YuvImage的宽度。 | height | int | YuvImage的高度。 | strides | int | (可选)每个图像平面的行字节。如果yuv包含填充,则必须提供每个图像的步幅。如果strides为null,则该方法假定没有填充,并按格式和宽度本身派生行字节。 |
调用image.compressToJpeg()将YUV格式图像数据转为jpg格式代码如下,其参数作用如下表所示。 image.compressToJpeg(Rect rectangle, int quality, OutputStream stream);
参数 | 类型 | 作用 | rectangle | Rect | 要压缩的矩形区域。方法检查矩形是否在图像内。此外,如果矩形中的色度像素与其中的亮度像素不匹配,则该方法修改矩形。 | quality | int | 提示压缩机,范围0-100。0表示压缩小尺寸,100表示压缩以获得最高质量。 | stream | OutputStream | OutputStream写入压缩数据。 |
预览一般用SurfaceView显示摄像头采集到的画面内容。需要用到preview class。这个类需要实现android.view.SurfaceHolder.Callback接口,并用此接口把摄像头采集到的画面送到当前的预览界面。 当应用调用完摄像头之后,必须进行清理释放资源的操作。必须释放Camera对象,不然的话可能会引起其他应用程序使用Camera实例的时候发生崩溃。相应代码如下。 if (mCamera != null) { mCamera.stopPreview();//停止预览 //调用release()以释放相机以供其他应用程序使用。应用程序应在onPause()期间 //立即释放相机,并在onResume()期间重新open()。 mCamera.release(); mCamera = null; }
二、红外遥控 详见红外遥控部分
此部分代码http://www.pudn.com/Download/item/id/3913496.html
电脑端详解 Socket通信步骤如下图所示。手机采集到的图像通过Socket一帧一帧发送,电脑通过Socket接收每一帧图像。
电脑端Java程序主要代码 - /**
- *在服务器开启情况下,启动客户端,创建套接字接收图像
- */
- public class ImageServer {
- public static ServerSocket ss = null;
- public static void main(String args[]) throws Exception,IOException{
- ss = new ServerSocket(6000);
- final ImageFrame frame = new ImageFrame(ss);
- frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
- frame.setVisible(true);
- while(true){
- frame.panel.getimage();
- frame.repaint();
- }
- }
- }
建立好连接后,getimage()负责接收手机端传过来的图像,repaint()负责将接收到的图像绘制在窗体组件上。在此使用的是动画播放原理,实时视频是通过电脑窗体一帧一帧播放图片,形成视频动画效果。没有采用主流的视频压缩分包技术,而是选择牺牲画质。摄像头采集到的是最低画质,这样每一帧图像的数据量就小了,延迟也就下去了。
对小车的控制放在另一个线程中,监听按键的状态来判断要发送的命令。
主要代码如下所示
- jb.addKeyListener(new KeyAdapter() {
- ServerSocket ss;
- boolean sendFlag = false;//设置标志位,按下时只执行一次,不连续发送
- public void keyPressed(KeyEvent e) {
- int KeyCode = e.getKeyCode(); // 返回所按键对应的整数值
- String s = KeyEvent.getKeyText(KeyCode); // 返回按键的字符串描述
- System.out.print("输入的内容为:" + s + ",");
- System.out.println("对应的KeyCode为:" + KeyCode);
- if(!sendFlag) {
- try{
- ss = new ServerSocket(7788);
- send(KeyCode);
- ss.close();
- sendFlag=true;
- }catch (Exception e1) {
- e1.printStackTrace();
- }
- }
- }
- public void keyReleased(KeyEvent e) {
- int KeyCode = e.getKeyCode(); // 返回所按键对应的整数值
- if(KeyCode==38||KeyCode==40||KeyCode==37||KeyCode==39) {
- try {
- ss = new ServerSocket(7788);
- stop();
- sendFlag=false;
- } catch (Exception e1) {
- e1.printStackTrace();
- }
- }
- }
- public void send(int i) throws Exception{
- @SuppressWarnings("resource")
- ServerSocket serverSocket = ss;//new ServerSocket(7788); // 创建ServerSocket对象
- Socket client = serverSocket.accept(); // 调用ServerSocket的accept()方法接收数据
- OutputStream os = client.getOutputStream();// 获取客户端的输出流
- System.out.println("开始与客户端交互数据");
- switch (i) {
- case 38:s.write(("01").getBytes());break;//上
- case 40:s.write(("02").getBytes());break;//下
- case 37:s.write(("03").getBytes());break;//左
- case 39:s.write(("04").getBytes());break;//右
- }
- System.out.println("结束与客户端交互数据");
- os.close();
- client.close();
- }
- protected void stop() throws Exception {
- ServerSocket serverSocket = ss;// 创建ServerSocket对象
- Socket client = serverSocket.accept(); // 调用ServerSocket的accept()方法接收数据
- OutputStream os = client.getOutputStream();// 获取客户端的输出流
- os.write(("05").getBytes());//停止
- os.close();
- client.close();
- ss.close();
- }
- });
此部分源码链接http://www.pudn.com/Download/item/id/3913494.html
人脸跟随详解
基于安卓的视频遥控小车实现人脸跟随看起来好像高大上,其实是用的安卓自带的人脸检测API(FaceDetector),将其和红外发射代码结合起来,实现了小车人脸跟随功能。
人脸检测的接口为FaceDetectionListener,
- private class MyFaceDetectionListener implements Camera.FaceDetectionListener {
- @Override
- public void onFaceDetection(Camera.Face[] faces, Camera camera) {
- if (faces.length > 0){
- Log.d("FaceDetection", "face detected: "+ faces.length +
- " Face 1 Location X: " + faces[0].rect.centerX() +
- "Y: " + faces[0].rect.centerY() );
- }
- }
- }
通过Camera的setFaceDetedtionListener方法来接受底层检测到脸的回掉。
- mCamera.setFaceDetectionListener(new MyFaceDetectionListener());
在摄像机开始预览了之后调用开始检测方法
- private void startFaceDetection(){
- // Try starting Face Detection
- Camera.Parameters params = mCamera.getParameters();
- // start face detection only *after* preview has started
- if (params.getMaxNumDetectedFaces() > 0){
- // camera supports face detection, so can start it:
- mCamera.startFaceDetection();
- }
- }
以上为通用步骤,我对MyFaceDetectionListener进行了改造,
将其和红外发射代码transmit()方法结合起来,代码如下
- private class MyFaceDetectionListener implements Camera.FaceDetectionListener{
- private int faceX=0;
- private int faceY=0;
- boolean fMoveFlag = false;//设置标志位,只执行一次,不连续发送
- boolean bMoveFlag = false;
- boolean lMoveFlag = false;
- boolean rMoveFlag = false;
- Camera.Parameters parameters;
- public MyFaceDetectionListener(Camera.Parameters parameters) {
- this.parameters=parameters;
- }
- @Override
- public void onFaceDetection(Camera.Face[] faces, Camera camera) {
- if (faces.length > 0){
- Log.d("FaceDetection", "face detected: "+ faces.length +
- " Face 1 Location X: " + faces[0].rect.centerX() +
- "Y: " + faces[0].rect.centerY() );
- faceX=faces[0].rect.centerX();
- faceY=faces[0].rect.centerY();
- if(faceY<-100&&!fMoveFlag){
- mCIR.transmit(38000, pattern1);
- fMoveFlag=true;
- bMoveFlag=false;
- }
- if(faceY>100&&!bMoveFlag){
- mCIR.transmit(38000, pattern2);
- bMoveFlag=true;
- fMoveFlag=false;
- }
- if(faceX<-100&&!lMoveFlag){
- mCIR.transmit(38000, pattern3);
- lMoveFlag=true;
- rMoveFlag=false;
- }
- if(faceX>100&&!rMoveFlag){
- mCIR.transmit(38000, pattern4);
- rMoveFlag=true;
- lMoveFlag=false;
- }
- }else{
- }
- }
- }
红外发射部分详见基于安卓的视频遥控小车红外遥控部分
人脸追踪代码:http://www.pudn.com/Download/item/id/3913500.html
红外遥控详解 手机和小车之间的通信我用的不是蓝牙是红外遥控,虽然红外的遥控的控制距离只有10m左右,无法绕过障碍物进行遥控。但发射红外遥控信号的手机就架在小车上,可以将手机的红外发射器和红外接收器放在一块固定住。虽然并不是所有的安卓手机都有红外发射器,但都有3.5mm的耳机接口,红外信号的38kHz频率在音频范围内,可以用耳机接口外接的红外发光二极管发射红外遥控信号。如果使用蓝牙来完成对小车的控制,小车上需要配备蓝牙模块与手机进行配对通信。而且并不是所有的手机都支持蓝牙,早期的一些安卓智能手机就不支持蓝牙。而且蓝牙需要配对连接,红外遥控无需配对连接,省去等待时间。相比蓝牙模块,红外模块成本更低。所以采用红外遥控模式。 上边说的都是后话了,当初之所以用红外,是因为我一开始用的不是OPPO A51 ,用的是酷派8076D。那会儿A51还用着呢,这个酷派手机有WiFi但没有蓝牙,所以手机和单片机之间的通信就成了问题。 当时的小车还是这个样子
我从网上搜了好多解决方案,智能手机是开发完成的产品,留出的接口不多,也只有USB口和耳机口: 一,用手机的USB口,但我发现酷派8076D不支持OTG,然后又从网上搜说是厂家只是删除了配置文件,我试了试,还是不行,它硬件上应该也没有升压电路(手机电池一般3.7V,USB是5V供电)。这部分参考使用android IOIO和安卓手机制作视频遥控小车(控制灯的开关、实时视频传输、方向控制) 二,用耳机口,这个网上也有例子一文读懂Android/iOS手机如何通过音频接口与外设通信,他这种方案是双工通信,但这个吧,涉及到信号处理,和数学打交道,鄙人数学渣渣。再者得买个这种外设,no money啊。然后我之前研究过遥控精灵(ZaZaRemote),不支持红外遥控的手机,在耳机孔插个红外发射头(smart zaza)就行了。这种方案是单工通信,小车配套上红外一体化接收头就可以遥控小车移动。不过不同手机的耳机口驱动力不一样,有的驱动不了红外发光二极管(压降1.4V左右),我的酷派就驱动不了,我直接把二极管接在手机喇叭上。
最后,选择了音频口发射红外信号这种方案。其实造车之前,就开始在研究红外了,那会儿考四六级和期末英语考试都是用的红外耳机,就想着期末英语怎么作弊(^_−)☆,因为听力就是课本上的。教室有个红外发射器,后来查了些资料发现就是音频范围,把喇叭拆了接上红外发光二极管,就能用红外耳机听到声音。不过没用在作弊上,因为功率太小了(酷派手机喇叭改的),盖不过教室的。 音频转红外这块,我还没做好,我只是录了红外遥控信号的音频文件,然后播放。但我发现准确率大概只有八成,感觉这东西涉及到傅里叶变换,音频是正弦波,红外信号是方波,直接用音频驱动是有误差的吧,我也不是很懂,数学不好。网上我搜到这篇是用安卓实现的安卓手把手教你学习并实现 安卓耳机口音频转红外发射,但我是用底层C语言实现的,用的C4droid写的在手机上运行,参考的这篇 OpenSL ES范例,无java代码,纯C 再后来,OPPO A51不用了,就把它用在小车上。OPPO A51支持红外遥控,所以不用那么麻烦。参考这篇Android编程红外编程——红外码详析 单片机红外解码程序参考Android遥控器开发,这个后边有单片机红外解码程序。 因为Android4.4及以上才有ConsumerIrManager类用来操控红外设备,所以以下程序是基于Android 5.1系统的OPPO A51手机开发和测试的。
首先从系统服务中获取到ConsumerIrManager服务。 IR=(ConsumerIrManager)getSystemService(CONSUMER_IR_SERVICE); 然后将要发送的红外码存入数组中
//0x73 int[] pattern2 = { 9000, 4500, 560, 560, 560, 560, 560, 560, 560, 560, 560,560, 560, 560, 560, 560, 560, 560, 560, 1690, 560, 1690, 560, 1690, 560, 1690, 560, 1690, 560, 1690, 560, 1690, 560, 1690, /*0001 1000*/560, 560, 560, 560, 560, 560, 560, 1690, 560, 1690, 560, 560, 560, 560, 560, 560, 560, 1690, 560, 1690, 560, 1690, 560, 560, 560, 560, 560, 1690, 560, 1690, 560, 1690, 560, 42020, 9000, 2250, 560, 98190 };
一种交替的载波序列模式,通过毫秒测量
引导码,地址码,地址码,数据码,数据反码
第三行数据码反置,比如0x12=0001 0010反置为 0100 1000
可能和接收有关系,只有反置了之后才能接收正常
最后通过如下方法最终发送红外信号。
mCIR.transmit(hz, pattern2);//后
transmit(int carrierFrequency, int[] pattern) :此方法控制手机产生 carrierFrequency为频率的,以pattern为红外开关的时间数组,发送红外信号。(例如:transmit(38000,{100,200,300,400}) 将会产生一个频率为38KHz的红外信号,信号的电平高低为 100us高电平,200us低电平,300us高电平,400us低电平。注意pattern的数据个数要为偶数个,不然报错。)。
手机端红外发射功能的程序流程图如图所示。
|