码迷,mamicode.com
首页 > 移动开发 > 详细

我的Opencv4Android添加V4L2支持的移植记录(3)

时间:2016-07-13 16:21:35      阅读:644      评论:0      收藏:0      [点我收藏+]

标签:

博主QQ:1356438802


本文实验平台:Eclipse + Opencv 2.4.10 + MTK Android 4.4平板(这一直是我的Android实验平台)


可能各位看官,看到前面的文章会觉得很凌乱,一会儿这个平台,一会儿那个平台。

其实我的主要思路就是:opencv中的任何一个功能,首先在windows上验证成功,再到Ubuntu,然后到Android上验证!

在windows上,由于其系统通用性,各方面支持肯定更好,所以我一定能验证成功,然后我再去Android上面做这些功能。这样前面的经验可以给我做参考,在Android上调试时可以敏锐滴知道问题出在哪里,并且这个过程下来三个平台都熟悉了,对于以后我的应用程序跨平台移植也是有帮助的!


回顾这段时间的工作,主要是三个阶段:

1. 在各个平台能够正常调用opencv的函数。例如yanzi_OpenCV4Android app的JNI里面有个灰度处理的函数Java_luo_uvc_jni_ImageProc_grayProc,就只是为了验证opencv可以调用。

2. 在各个平台能够正常获取图像,实现实时预览。这个阶段前面在windows、Ubuntu和Android基本都实现了,只是效果有差别而已。

3. 对2中的函数调用升级——在各个平台使用opencv2的C++类实现预览和录像。实际上opencv2的C++类只是对opencv1的C函数做了一次封装,但是看起来代码更干净简洁,也更方便和MAT结合。windows和Ubuntu已经验证完了,可以参考前面系列文章。


接下来我们验证VideoCapture和VideoWriter在Android上的使用!


把文章《我的Opencv4Android添加V4L2支持的移植记录(2)》中的例程yanzi_OpenCV4Android拷贝过来做如下修改

yanzi_OpenCV4Android6

ImageProc.cpp

#include "ImageProc.h"

#include "cv.h"
#include "highgui.h"

#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"

#include <string>
#include <vector>

using namespace cv;
using namespace std;

#ifdef __cplusplus
extern "C"
{
#pragma message("------------------------ ImageProc.cpp")
#endif

#define NAMELEN	(64)

Mat frame;
VideoCapture capture;

char *prefix = NULL;
char fileName[NAMELEN] = {0};
VideoWriter writer;

int id;

JNIEXPORT jintArray JNICALL Java_luo_uvc_jni_ImageProc_grayProc(JNIEnv* env,
		jclass obj, jintArray buf, jint w, jint h)
{
	jint *cbuf;
	cbuf = env->GetIntArrayElements(buf, false);
	if (cbuf == NULL)
	{
		return 0;
	}

	Mat imgData(h, w, CV_8UC4, (unsigned char*) cbuf);

	uchar* ptr = imgData.ptr(0);
	for (int i = 0; i < w * h; i++)
	{
		//计算公式:Y(亮度) = 0.299*R + 0.587*G + 0.114*B
		//对于一个int四字节,其彩色值存储方式为:BGRA
		int grayScale = (int) (ptr[4 * i + 2] * 0.299 + ptr[4 * i + 1] * 0.587
				+ ptr[4 * i + 0] * 0.114);
		ptr[4 * i + 1] = grayScale;
		ptr[4 * i + 2] = grayScale;
		ptr[4 * i + 0] = grayScale;
	}

	int size = w * h;
	jintArray result = env->NewIntArray(size);
	env->SetIntArrayRegion(result, 0, size, cbuf);

	env->ReleaseIntArrayElements(buf, cbuf, 0);

	return result;
}

JNIEXPORT jint JNICALL Java_luo_uvc_jni_ImageProc_connectCamera(JNIEnv* env,
		jclass obj, jint device)
{

	id = device;


	if (capture.isOpened())
	{
		LOGE("camera is already opened!");
		return -2;
	}
	else
	{
		//打开一个默认的摄像头
		capture.open(-1);
		if (capture.isOpened())
		{
			LOGE("camera open success!");
			return 0;
		}
	}

//	LOGI("usb camera Java_luo_uvc_jni_ImageProc_connectCamera end ....\n");

	return -1;
}

JNIEXPORT jint JNICALL Java_luo_uvc_jni_ImageProc_releaseCamera(JNIEnv* env,
		jclass obj)
{
//	LOGI("usb camera Java_luo_uvc_jni_ImageProc_releaseCamera start ....\n");

	if (capture.isOpened())
	{
		capture.release();
		LOGE("camera release success!");
		return 0;
	}

//	LOGI("usb camera Java_luo_uvc_jni_ImageProc_releaseCamera end ....\n");

	return -1;
}

struct FrameInfoClass
{
	jfieldID width;
	jfieldID heigth;
	jfieldID imageSize;
	jfieldID pixels;
};

JNIEXPORT jobject JNICALL Java_luo_uvc_jni_ImageProc_getFrame(JNIEnv* _env,
		jclass obj)
{
//	LOGI("------------Java_luo_uvc_jni_ImageProc_getFrame\n");
	if (capture.isOpened())
	{
		LOGI("------------start capture frame\n");

		//取图片帧
		capture >> frame;
		if (frame.empty())
		{
			LOGE("capture frame empty!");
			return NULL;
		}

		//将图片写入视频文件
		if(writer.isOpened())
		{
			//录制视频用的frame不需要转格式
			writer.write(frame);
		}

		struct FrameInfoClass frameInfoClass;

		//内部类用$
		//luo/uvc/jni/ImageProc$FrameInfo
		jclass class2 = _env->FindClass("luo/uvc/jni/ImageProc$FrameInfo");

		frameInfoClass.width = _env->GetFieldID(class2, "mWidth", "I");

		frameInfoClass.heigth = _env->GetFieldID(class2, "mHeight", "I");

		frameInfoClass.imageSize = _env->GetFieldID(class2, "mImageSize", "I");

		frameInfoClass.pixels = _env->GetFieldID(class2, "mPixels", "[I");

		//
		jobject joFrame = _env->AllocObject(class2);

		LOGI("frame->cols = %d\n", frame.cols);
		LOGI("frame->rows = %d\n", frame.rows);
//		LOGI("frame->imageSize = %d\n", frame->imageSize);

		_env->SetIntField(joFrame, frameInfoClass.width, frame.cols);
		_env->SetIntField(joFrame, frameInfoClass.heigth, frame.rows);
//		_env->SetIntField(joFrame, frameInfoClass.imageSize, frame->imageSize);

		int size = frame.cols * frame.rows;
		//创建一个新的java数组(jarray),但是jarray不是C数组类型,不能直接访问jarray
		jintArray jiArr = _env->NewIntArray(size);
		jint *ji;

#if 1   //可用
		//RGB --> ARGB8888
		Mat frameARGB;
		cvtColor(frame, frameARGB, CV_RGB2RGBA);

		//JNI支持一系列的Get/Release<Type>ArrayElement 函数,允许本地代码获取一个指向基本C类型数组的元素的指针。
		ji = _env->GetIntArrayElements(jiArr, 0);

		memcpy((jbyte *) ji, frameARGB.data, 4 * size);

		_env->ReleaseIntArrayElements(jiArr, ji, 0); //可加,可不加

		_env->SetObjectField(joFrame, frameInfoClass.pixels, jiArr);


#else   //可用
		//可以使用GetIntArrayRegion函数来把一个 int数组中的所有元素复制到一个C缓冲区中
		//SetIntArrayRegion则是逆过程
		_env->SetIntArrayRegion(jiArr, 0, 2, abc);
		_env->SetObjectField(joFrame, company_class.money, jiArr);

#endif

//		LOGI("Java_luo_uvc_jni_ImageProc_getFrame end\n");

		return joFrame;

	}

//	LOGI("=================Java_luo_uvc_jni_ImageProc_getFrame failed\n");

	return 0;
}

JNIEXPORT jint JNICALL Java_luo_uvc_jni_ImageProc_startRecord
  (JNIEnv *env, jclass, jstring jstr)
{
	if(writer.isOpened() == false)
	{
		 prefix = jstringToChar(env, jstr);
		 memset(fileName, 0, NAMELEN);
		 sprintf(fileName, "/storage/sdcard0/Movies/%s.avi", prefix);
		 LOGI("fileName: %s", fileName);
		 FREE(prefix);

		 writer.open(fileName, CV_FOURCC('F', 'L', 'V', '1')/*有效*/, 30, cv::Size(640, 480),true);
//		 writer.open(fileName, CV_FOURCC('M', 'J', 'P', 'G')/*有效*/, 30, cv::Size(640, 480),true);
//		 writer.open(fileName, CV_FOURCC('D', 'I', 'V', 'X')/*有效*/, 30, cv::Size(640, 480),true);
//		 writer.open(fileName, CV_FOURCC('X', 'V', 'I', 'D')/*有效*/, 30, cv::Size(640, 480),true);
		 if(writer.isOpened())
		 {
			 LOGE("writer open successful!");
			 return 0;
		 }
		 else
		 {
			 LOGE("writer open failed!");
			 return -2;
		 }

	}
	else
	{
		//实际上已经在录像
		return -1;
	}
}

JNIEXPORT jint JNICALL Java_luo_uvc_jni_ImageProc_stopRecord
  (JNIEnv *, jclass)
{
	if (writer.isOpened())
	{
		//如果正在录像,则停止录像
		writer.release();
		LOGI("%s end record!", fileName);

		return 0;
	}

	return -1;
}

JNIEXPORT jint JNICALL Java_luo_uvc_jni_ImageProc_getWidth(JNIEnv* env,
		jclass obj)
{
	return 0;
}

JNIEXPORT jint JNICALL Java_luo_uvc_jni_ImageProc_getHeight(JNIEnv* env,
		jclass obj)
{
	return 0;
}

//jstring to char*
char* jstringToChar(JNIEnv* env, jstring jstr)
{
	char* rtn = NULL;
	jclass clsstring = env->FindClass("java/lang/String");
	jstring strencode = env->NewStringUTF("utf-8");
	jmethodID mid = env->GetMethodID(clsstring, "getBytes", "(Ljava/lang/String;)[B");
	jbyteArray barr= (jbyteArray)env->CallObjectMethod(jstr, mid, strencode);
	jsize alen = env->GetArrayLength(barr);
	jbyte* ba = env->GetByteArrayElements(barr, JNI_FALSE);

	if (alen > 0)
	{
		rtn = (char*)calloc(1, alen + 1);
		memcpy(rtn, ba, alen);
		rtn[alen] = 0;
	}

	env->ReleaseByteArrayElements(barr, ba, 0);
	return rtn;
}

#ifdef __cplusplus
}
#endif
/* end of extern */

CameraPreview.java

package luo.uvc.jni;


import java.text.SimpleDateFormat;
import java.util.Date;

import luo.uvc.jni.ImageProc.FrameInfo;
import android.R.bool;
import android.R.string;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.Bitmap.Config;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.widget.Toast;

public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback, Runnable
{
	public static final String TAG = "UVCCameraPreview";
	protected Context context;

	private SurfaceHolder holder;
	Thread mainLoop = null;
	
	public static final int SET_PREVIEW_TEXT = 0;
	public static final int SET_RECORD_TEXT = 1;

	private boolean mIsOpened = false;
	private boolean mIsRecording = false;
	private boolean shouldStop = false;

	public callback textCallback;

	// The following variables are used to draw camera images.
	private int winWidth = 0;
	private int winHeight = 0;
	private Rect rect;
	private int dw, dh;
	private float rate;

	public CameraPreview(Context context)
	{
		super(context);
		// TODO Auto-generated constructor stub
		this.context = context;
		Log.d(TAG, "CameraPreview constructed");
		setFocusable(true);

		holder = getHolder();
		holder.addCallback(this);
		holder.setType(SurfaceHolder.SURFACE_TYPE_NORMAL);

	}

	//注意:使用findViewById获取CameraPreview,会调用这个构造函数
	public CameraPreview(Context context, AttributeSet attrs)
	{
		super(context, attrs);
		this.context = context;
		Log.d(TAG, "CameraPreview constructed");
		setFocusable(true);

		holder = getHolder();
		holder.addCallback(this);
		holder.setType(SurfaceHolder.SURFACE_TYPE_NORMAL);
		
	}	

	public void initPreview()
	{
		int index = -1;
		if (mIsOpened == false)
		{
			if (0 == ImageProc.connectCamera(index))
			{
				Log.i(TAG, "open uvc success!!!");
				mIsOpened = true;
				textCallback.setViewText(SET_PREVIEW_TEXT, "关闭");
				if (null != mainLoop)
				{
					shouldStop = false;
					Log.i(TAG, "preview mainloop starting...");
					mainLoop.start();
				}

				Toast.makeText(context.getApplicationContext(), "成功打开摄像头", Toast.LENGTH_SHORT).show();
			} else
			{
				Log.i(TAG, "open uvc fail!!!");
				mIsOpened = false;
				Toast.makeText(context.getApplicationContext(), "摄像头打开失败", Toast.LENGTH_SHORT).show();
			}

		} else
		{
			uninitPreview();
		}
	}

	public void uninitPreview()
	{
		//结束录制
		uninitRecord();
		
		//停止预览线程
		if (null != mainLoop)
		{
			Log.i(TAG, mainLoop.isAlive() ? "mainloop is alive!" : "mainloop is not alive!");
			if (mainLoop.isAlive())
			{
				shouldStop = true;
				while (shouldStop)
				{
					try
					{
						Thread.sleep(100); // wait for thread stopping
					} catch (Exception e)
					{
					}
				}
			}
		}

		//关闭camera
		if (mIsOpened)
		{
			mIsOpened = false;
			ImageProc.releaseCamera();
			textCallback.setViewText(SET_PREVIEW_TEXT, "打开");
			Log.i(TAG, "release camera...");
		}
	}
	
	public void initRecord()
	{
		if(mIsOpened)
		{
			if(mIsRecording == false)
			{
				Log.i(TAG, "init camera record!");
				Date date = new Date();
				SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmss");				
				String dateString = simpleDateFormat.format(date);
				if(null == dateString)
				{
					dateString = "luoyouren";
				}
				Log.i(TAG, dateString);
				
				if(0 == ImageProc.startRecord(dateString))
				{
					mIsRecording = true;
					textCallback.setViewText(SET_RECORD_TEXT, "停止");
					Toast.makeText(context.getApplicationContext(), "开始录制...", Toast.LENGTH_SHORT).show();
				}
				else 
				{
					mIsRecording = false;
					Log.e(TAG, "init camera record failed!");
					Toast.makeText(context.getApplicationContext(), "录制启动失败!", Toast.LENGTH_SHORT).show();
				}
				return;
			}
			else 
			{
				uninitRecord();
				return;
			}
		}
		else 
		{
			Log.e(TAG, "camera has not been opened!");
			return;
		}
	}
	
	public void uninitRecord()
	{
		if(mIsRecording)
		{
			Log.i(TAG, "camera is already recording! So we stop it.");
			ImageProc.stopRecord();
			mIsRecording = false;
			textCallback.setViewText(SET_RECORD_TEXT, "录制");
			return;
		}
	}

	public boolean isOpen()
	{
		return mIsOpened;
	}
	
	public boolean isRecording()
	{
		return mIsRecording;
	}

	@Override
	public void run()
	{
		// TODO Auto-generated method stub
		while (true && mIsOpened)
		{
			// get camera frame
			FrameInfo frame = ImageProc.getFrame();
			if (null == frame)
			{
				continue;
			}

			int w = frame.getWidth();
			int h = frame.getHeigth();
			Log.i(TAG, "frame.width = " + w + " frame.height = " + h);
			// 根据图像大小更新显示区域大小
			// 一般来说图像大小不会变化: 640x480
			updateRect(w, h);

			Bitmap resultImg = Bitmap.createBitmap(w, h, Config.ARGB_8888);

			resultImg.setPixels(frame.getPixels(), 0, w, 0, 0, w, h);

			// 刷surfaceview显示
			Canvas canvas = getHolder().lockCanvas();
			if (canvas != null)
			{
				// draw camera bmp on canvas
				canvas.drawBitmap(resultImg, null, rect, null);

				getHolder().unlockCanvasAndPost(canvas);
			}

			if (shouldStop)
			{
				shouldStop = false;
				Log.i(TAG, "mainloop will stop!");
				break;
			}
		}
		
		Log.i(TAG, "mainloop break while!");
	}

	public void updateRect(int frame_w, int frame_h)
	{
		// obtaining display area to draw a large image
		if (winWidth == 0)
		{
			winWidth = this.getWidth();
			winHeight = this.getHeight();

			if (winWidth * 3 / 4 <= winHeight)
			{
				dw = 0;
				dh = (winHeight - winWidth * 3 / 4) / 2;
				rate = ((float) winWidth) / frame_w;
				rect = new Rect(dw, dh, dw + winWidth - 1, dh + winWidth * 3 / 4 - 1);
			} else
			{
				dw = (winWidth - winHeight * 4 / 3) / 2;
				dh = 0;
				rate = ((float) winHeight) / frame_h;
				rect = new Rect(dw, dh, dw + winHeight * 4 / 3 - 1, dh + winHeight - 1);
			}
		}
	}

	@Override
	public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3)
	{
		// TODO Auto-generated method stub

	}

	@Override
	public void surfaceCreated(SurfaceHolder arg0)
	{
		// TODO Auto-generated method stub
		mainLoop = new Thread(this);
		
		updateRect(512, 512);
		// 将lena图像加载程序中并进行显示
		Bitmap resultImg = BitmapFactory.decodeResource(getResources(), R.drawable.lena);

		// 刷surfaceview显示
		Canvas canvas = getHolder().lockCanvas();
		if (canvas != null)
		{
			// draw camera bmp on canvas
			canvas.drawBitmap(resultImg, null, rect, null);

			getHolder().unlockCanvasAndPost(canvas);
		}

	}

	@Override
	public void surfaceDestroyed(SurfaceHolder arg0)
	{
		// TODO Auto-generated method stub

	}

}

其实改动不是很大,ImageProc.cpp中CvCapture替换成VideoCapture,CameraPreview.java增加InitRecord和UninitRecord。


编译、安装、运行后,Camera实时预览正常,和我的Opencv4Android添加V4L2支持的移植记录(2)》中一样,但是录像失败!


我们可以来看看opencv里面,对于各个平台VideoWriter功能的分发情况

/**
 * Videowriter dispatching method: it tries to find the first
 * API that can write a given stream.
 */
CV_IMPL CvVideoWriter* cvCreateVideoWriter( const char* filename, int fourcc,
                                            double fps, CvSize frameSize, int is_color )
{
    //CV_FUNCNAME( "cvCreateVideoWriter" );

    CvVideoWriter *result = 0;

#pragma message("------------------------ cvCreateVideoWriter")

    if(!fourcc || !fps)
        result = cvCreateVideoWriter_Images(filename);

#ifdef HAVE_FFMPEG
#pragma message("------------------------ HAVE_FFMPEG")
    if(!result)
        result = cvCreateVideoWriter_FFMPEG_proxy (filename, fourcc, fps, frameSize, is_color);
#endif

#ifdef HAVE_VFW
#pragma message("------------------------ HAVE_VFW")
    if(!result)
        result = cvCreateVideoWriter_VFW(filename, fourcc, fps, frameSize, is_color);
#endif

#ifdef HAVE_MSMF
#pragma message("------------------------ HAVE_MSMF")
    if (!result)
        result = cvCreateVideoWriter_MSMF(filename, fourcc, fps, frameSize, is_color);
#endif

/*  #ifdef HAVE_XINE
    if(!result)
        result = cvCreateVideoWriter_XINE(filename, fourcc, fps, frameSize, is_color);
    #endif
*/
#ifdef HAVE_AVFOUNDATION
#pragma message("------------------------ HAVE_AVFOUNDATION")
    if (! result)
        result = cvCreateVideoWriter_AVFoundation(filename, fourcc, fps, frameSize, is_color);
#endif

#if defined(HAVE_QUICKTIME) || defined(HAVE_QTKIT)
#pragma message("------- defined(HAVE_QUICKTIME) || defined(HAVE_QTKIT)")
    if(!result)
        result = cvCreateVideoWriter_QT(filename, fourcc, fps, frameSize, is_color);
#endif

#ifdef HAVE_GSTREAMER
#pragma message("------------------------ HAVE_GSTREAMER")
    if (! result)
        result = cvCreateVideoWriter_GStreamer(filename, fourcc, fps, frameSize, is_color);
#endif

#if !defined(HAVE_FFMPEG) &&     !defined(HAVE_VFW) &&     !defined(HAVE_MSMF) &&     !defined(HAVE_AVFOUNDATION) &&     !defined(HAVE_QUICKTIME) &&     !defined(HAVE_QTKIT) &&     !defined(HAVE_GSTREAMER)
// If none of the writers is used
// these statements suppress 'unused parameter' warnings.
    (void)frameSize;
    (void)is_color;
#endif

#pragma message("------------------------ cvCreateVideoWriter_Images")
    if(!result)
        result = cvCreateVideoWriter_Images(filename);

    return result;
}

再对比下 下面的表

enum
{
    CV_CAP_ANY      =0,     // autodetect

    CV_CAP_MIL      =100,   // MIL proprietary drivers

    CV_CAP_VFW      =200,   // platform native
    CV_CAP_V4L      =200,
    CV_CAP_V4L2     =200,

    CV_CAP_FIREWARE =300,   // IEEE 1394 drivers
    CV_CAP_FIREWIRE =300,
    CV_CAP_IEEE1394 =300,
    CV_CAP_DC1394   =300,
    CV_CAP_CMU1394  =300,

    CV_CAP_STEREO   =400,   // TYZX proprietary drivers
    CV_CAP_TYZX     =400,
    CV_TYZX_LEFT    =400,
    CV_TYZX_RIGHT   =401,
    CV_TYZX_COLOR   =402,
    CV_TYZX_Z       =403,

    CV_CAP_QT       =500,   // QuickTime

    CV_CAP_UNICAP   =600,   // Unicap drivers

    CV_CAP_DSHOW    =700,   // DirectShow (via videoInput)
    CV_CAP_MSMF     =1400,  // Microsoft Media Foundation (via videoInput)

    CV_CAP_PVAPI    =800,   // PvAPI, Prosilica GigE SDK

    CV_CAP_OPENNI   =900,   // OpenNI (for Kinect)
    CV_CAP_OPENNI_ASUS =910,   // OpenNI (for Asus Xtion)

    CV_CAP_ANDROID  =1000,  // Android
    CV_CAP_ANDROID_BACK =CV_CAP_ANDROID+99, // Android back camera
    CV_CAP_ANDROID_FRONT =CV_CAP_ANDROID+98, // Android front camera

    CV_CAP_XIAPI    =1100,   // XIMEA Camera API

    CV_CAP_AVFOUNDATION = 1200,  // AVFoundation framework for iOS (OS X Lion will have the same API)

    CV_CAP_GIGANETIX = 1300,  // Smartek Giganetix GigEVisionSDK

    CV_CAP_INTELPERC = 1500 // Intel Perceptual Computing SDK
};


cvCreateVideoWriter在windows下可以用cvCreateVideoWriter_VFW,在Ubuntu下可以用cvCreateVideoWriter_FFMPEG_proxy,可以在Android平台它用啥?没得用

看看之前的Cmake的结果对比就知道:

技术分享


Opencv在我增加V4L2后,也支持NativeCamera和V4L2两种Video I/O,而Ubuntu有FFMPEG的支持(不懂FFMPEG的去问度娘)。并且在cvCreateVideoWriter函数中也没有对NativeCamera的支持,所以我的APP录像失败。

所以如果要让yanzi_OpenCV4Android 能够录像,就要添加FFMPEG的支持!!!!


PS:

昨天我一直在纠结一个问题,VFW和DirectShow都是微软视频处理框架,DirectShow是VFW的升级版。但是在Opencv中,

cvCreateCameraCapture 读摄像头

cvCreateFileCapture 读视频文件

cvCreateVideoWriter 写视频文件

和摄像头相关的cvCreateCameraCapture 里面对DirectShow和VFW都有支持,但是和文件相关的后面两个函数只对VFW有支持。

我猜测大概是DirectShow不善于处理视频文件。


我的Opencv4Android添加V4L2支持的移植记录(3)

标签:

原文地址:http://blog.csdn.net/luoyouren/article/details/51895475

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!