织梦CMS - 轻松建站从此开始!

罗索

基于v4l2的webcam应用, 本地预监

落鹤生 发布于 2013-03-21 15:00 点击:次 
今天尝试编写了一个基于 v4l2 的摄像头应用, 目前仅仅实现从摄像头捕捉视频, 然后本地回显.照例先上效果图, 其中左侧小点为预监窗口, 右侧为经过 x264 压缩, tcp 传输, libavcodec 解压, 再用 qt 显示的效果., 延迟很低很低 :)
TAG:

今天尝试编写了一个基于 v4l2 的摄像头应用, 目前仅仅实现从摄像头捕捉视频, 然后本地回显.

照例先上效果图, 其中左侧小点为预监窗口, 右侧为经过 x264 压缩, tcp 传输, libavcodec 解压, 再用 qt 显示的效果., 延迟很低很低 :)

主要就是以下几个知识点: 

    1. v4l2接口:

    2. X11的本地回显:

    3. 使用 libswscale 进行拉伸:

    4. 使用 libx264 压缩:

1. v4l2接口: 大眼一看, 密密丫丫的 VIDIOC_XXXX, 其实静下心来, 也没多少, 很清晰, 大体流程如下:

             capture_open(name)

                      open /dev/video0         // 打开设备

                      check driver caps          // 检查一些 caps

                      VIDIOC_REQBUFS         // 使用 streaming mode,  mmap mode, 分配

                          VIDIOC_QUERYBUF       // 获取分配的buf, 并且mmap到进程空间

                          mmap

                       VIDIOC_QBUF              // buf 入列

                       VIDIOC_STREAMON      // 开始

 

使用的数据结构

  1. struct Buffer 
  2.     void *start; 
  3.     size_t length; 
  4. }; 
  5. typedef struct Buffer Buffer; 
  6. struct Ctx 
  7.     int vid; 
  8.     int width, height;  // 输出图像大小 
  9.     struct SwsContext *sws; // 用于转换 
  10.     int rows;   // 用于 sws_scale() 
  11.     int bytesperrow; // 用于cp到 pic_src 
  12.     AVPicture pic_src, pic_target;  // 用于 sws_scale 
  13.     Buffer bufs[2];     // 用于 mmap 
  14. }; 
  15. typedef struct Ctx Ctx; 

capture_open(...) 打开设备

  1. void *capture_open (const char *dev_name, int t_width, int t_height) 
  2.     int id = open(dev_name, O_RDWR); 
  3.     if (id < 0) return 0; 
  4.     Ctx *ctx = new Ctx; 
  5.     ctx->vid = id; 
  6.     // to query caps 
  7.     v4l2_capability caps; 
  8.     ioctl(id, VIDIOC_QUERYCAP, &caps); 
  9.     if (caps.capabilities & V4L2_CAP_VIDEO_CAPTURE) { 
  10.         if (caps.capabilities & V4L2_CAP_READWRITE) { 
  11.             // TODO: ... 
  12.         } 
  13.         if (caps.capabilities & V4L2_CAP_STREAMING) { 
  14.             // 检查是否支持 MMAP, 还是 USERPTR 
  15.             v4l2_requestbuffers bufs; 
  16.             memset(&bufs, 0, sizeof(bufs)); 
  17.             bufs.count = 2; 
  18.             bufs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 
  19.             bufs.memory = V4L2_MEMORY_MMAP; 
  20.             if (ioctl(id, VIDIOC_REQBUFS, &bufs) < 0) { 
  21. fprintf(stderr, "%s: don't support MEMORY_MMAP mode!/n", __func__); 
  22.                 close(id); 
  23.                 delete ctx; 
  24.                 return 0; 
  25.             } 
  26. fprintf(stderr, "%s: using MEMORY_MMAP mode, buf cnt=%d/n", __func__, bufs.count); 
  27.             // mmap 
  28.             for (int i = 0; i < 2; i++) { 
  29.                 v4l2_buffer buf; 
  30.                 memset(&buf, 0, sizeof(buf)); 
  31.                 buf.type = bufs.type; 
  32.                 buf.memory = bufs.memory; 
  33.                 if (ioctl(id, VIDIOC_QUERYBUF, &buf) < 0) { 
  34.                     fprintf(stderr, "%s: VIDIOC_QUERYBUF ERR/n", __func__); 
  35.                     close(id); 
  36.                     delete ctx; 
  37.                     return 0; 
  38.                 } 
  39.                 ctx->bufs[i].length = buf.length; 
  40.                 ctx->bufs[i].start = mmap(0, buf.length, PROT_READ|PROT_WRITE, 
  41.                         MAP_SHARED, id, buf.m.offset); 
  42.             } 
  43.         } 
  44.         else { 
  45. fprintf(stderr, "%s: can't support read()/write() mode and streaming mode/n", __func__); 
  46.             close(id); 
  47.             delete ctx; 
  48.             return 0; 
  49.         } 
  50.     } 
  51.     else { 
  52. fprintf(stderr, "%s: can't support video capture!/n", __func__); 
  53.         close(id); 
  54.         delete ctx; 
  55.         return 0; 
  56.     } 
  57.     int rc; 
  58.     // enum all support image fmt 
  59.     v4l2_fmtdesc fmt_desc; 
  60.     uint32_t index = 0; 
  61.     // 看起来, 不支持 plane fmt, 直接使用 yuyv 吧, 然后使用 libswscale 转换 
  62. #if 0    
  63.     do { 
  64.         fmt_desc.index = index; 
  65.         fmt_desc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 
  66.         rc = ioctl(id, VIDIOC_ENUM_FMT, &fmt_desc); 
  67.         if (rc >= 0) { 
  68.             fprintf(stderr, "/t support %s/n", fmt_desc.description); 
  69.         } 
  70.         index++; 
  71.     } while (rc >= 0); 
  72. #endif // 0 
  73.     v4l2_format fmt; 
  74.     fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 
  75.     rc = ioctl(id, VIDIOC_G_FMT, &fmt); 
  76.     if (rc < 0) { 
  77.         fprintf(stderr, "%s: can't VIDIOC_G_FMT.../n", __func__); 
  78.         return 0; 
  79.     } 
  80.     PixelFormat pixfmt = PIX_FMT_NONE; 
  81.     switch (fmt.fmt.pix.pixelformat) { 
  82.     case V4L2_PIX_FMT_YUYV: 
  83.         pixfmt = PIX_FMT_YUYV422; 
  84.         break
  85.     } 
  86.     if (pixfmt == PIX_FMT_NONE) { 
  87.         fprintf(stderr, "%s: can't support %4s/n"
  88. , __func__, (char*)&fmt.fmt.pix.pixelformat); 
  89.         return 0; 
  90.     } 
  91.     // 构造转换器 
  92.     fprintf(stderr, "capture_width=%d, height=%d, stride=%d/n"
  93. , fmt.fmt.pix.width, fmt.fmt.pix.height, 
  94.             fmt.fmt.pix.bytesperline); 
  95.     ctx->width = t_width; 
  96.     ctx->height = t_height; 
  97.     ctx->sws = sws_getContext(fmt.fmt.pix.width, fmt.fmt.pix.height, pixfmt, 
  98.             ctx->width, ctx->height, PIX_FMT_YUV420P,
  99.    // PIX_FMT_YUV420P 对应 X264_CSP_I420 
  100.             SWS_FAST_BILINEAR, 0, 0, 0); 
  101.     ctx->rows = fmt.fmt.pix.height; 
  102.     ctx->bytesperrow = fmt.fmt.pix.bytesperline; 
  103.     avpicture_alloc(&ctx->pic_target, PIX_FMT_YUV420P, ctx->width, ctx->height); 
  104.     // queue buf 
  105.     for (int i = 0; i < sizeof(ctx->bufs)/sizeof(Buffer); i++) { 
  106.         v4l2_buffer buf; 
  107.         memset(&buf, 0, sizeof(buf)); 
  108.         buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 
  109.         buf.memory = V4L2_MEMORY_MMAP; 
  110.         buf.index = i; 
  111.         if (ioctl(id, VIDIOC_QBUF, &buf) < 0) { 
  112.             fprintf(stderr, "%s: VIDIOC_QBUF err/n", __func__); 
  113.             exit(-1); 
  114.         } 
  115.     } 
  116.     int type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 
  117.     if (ioctl(id, VIDIOC_STREAMON, &type) < 0) { 
  118.         fprintf(stderr, "%s: VIDIOC_STREAMON err/n", __func__); 
  119.         exit(-1); 
  120.     } 
  121.     return ctx; 

capture_get_pic()

                       VIDIOC_DQBUF            // 出列, 

                       sws_scale                    // 格式转换/拉伸到 PIX_FMT_YUV420P, 准备方便压缩

                       VIDIOC_QBUF              // 重新入列

capture_get_picture(...) 从摄像头得到一帧图片

  1. int capture_get_picture (void *id, Picture *pic) 
  2.     // 获取, 转换 
  3.     Ctx *ctx = (Ctx*)id; 
  4.     v4l2_buffer buf; 
  5.     memset(&buf, 0, sizeof(buf)); 
  6.     buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 
  7.     buf.memory = V4L2_MEMORY_MMAP; 
  8.     if (ioctl(ctx->vid, VIDIOC_DQBUF, &buf) < 0) { 
  9.         fprintf(stderr, "%s: VIDIOC_DQBUF err/n", __func__); 
  10.         return -1; 
  11.     } 
  12. //  _save_pic(ctx->bufs[buf.index].start, buf.length); 
  13. //  __asm("int $3"); 
  14.     ctx->pic_src.data[0] = (unsigned char*)ctx->bufs[buf.index].start; 
  15.     ctx->pic_src.data[1] = ctx->pic_src.data[2] = ctx->pic_src.data[3] = 0; 
  16.     ctx->pic_src.linesize[0] = ctx->bytesperrow; 
  17.     ctx->pic_src.linesize[1] = ctx->pic_src.linesize[2]
  18.  = ctx->pic_src.linesize[3] = 0; 
  19.     // sws_scale 
  20.     int rs = sws_scale(ctx->sws, ctx->pic_src.data, ctx->pic_src.linesize, 
  21.             0, ctx->rows, ctx->pic_target.data, ctx->pic_target.linesize); 
  22.     // out 
  23.     for (int i = 0; i < 4; i++) { 
  24.         pic->data[i] = ctx->pic_target.data[i]; 
  25.         pic->stride[i] = ctx->pic_target.linesize[i]; 
  26.     } 
  27.     // re queue buf 
  28.     if (ioctl(ctx->vid, VIDIOC_QBUF, &buf) < 0) { 
  29.         fprintf(stderr, "%s: VIDIOC_QBUF err/n", __func__); 
  30.         return -1; 
  31.     } 
  32.     return 1; 

2.  X11 的本地回显: 采用 XShm, 效率还行

              vs_open ()

                        XOpenDisplay()

                        XCreateSimpleWindow()

                        XCreateGC()

                        XMapWindow()

                        XShmCreateImage()

                        shmget()

                        shmat()

 

使用的数据结构

  1. struct Ctx 
  2.     Display *display; 
  3.     int screen; 
  4.     Window window; 
  5.     GC gc; 
  6.     XVisualInfo vinfo; 
  7.     XImage *image; 
  8.     XShmSegmentInfo segment; 
  9.     SwsContext *sws; 
  10.     PixelFormat target_pixfmt; 
  11.     AVPicture pic_target; 
  12.     int v_width, v_height; 
  13.     int curr_width, curr_height; 
  14. }; 
  15. typedef struct Ctx Ctx; 

vs_open(...) 打开设备

  1. void *vs_open (int v_width, int v_height) 
  2.     Ctx *ctx = new Ctx; 
  3.     ctx->v_width = v_width; 
  4.     ctx->v_height = v_height; 
  5.     // window 
  6.     ctx->display = XOpenDisplay(0); 
  7.     ctx->window = XCreateSimpleWindow(ctx->display, RootWindow(ctx->display, 0), 
  8.             100, 100, v_width, v_height, 0, BlackPixel(ctx->display, 0), 
  9.             WhitePixel(ctx->display, 0)); 
  10.     ctx->screen = 0; 
  11.     ctx->gc = XCreateGC(ctx->display, ctx->window, 0, 0); 
  12.      
  13.     XMapWindow(ctx->display, ctx->window); 
  14.     // current screen pix fmt 
  15.     Window root; 
  16.     unsigned int cx, cy, border, depth; 
  17.     int x, y; 
  18.     XGetGeometry(ctx->display, ctx->window, &root, &x, &y, &cx, &cy, &border, &depth); 
  19.     // visual info 
  20.     XMatchVisualInfo(ctx->display, ctx->screen, depth, DirectColor, &ctx->vinfo); 
  21.     // image 
  22.     ctx->image = XShmCreateImage(ctx->display, ctx->vinfo.visual, depth, ZPixmap, 0, 
  23.             &ctx->segment, cx, cy); 
  24.     if (!ctx->image) { 
  25.         fprintf(stderr, "%s: can't XShmCreateImage !/n", __func__); 
  26.         exit(-1); 
  27.     } 
  28.     ctx->segment.shmid = shmget(IPC_PRIVATE, 
  29.             ctx->image->bytes_per_line * ctx->image->height,  
  30.             IPC_CREAT | 0777); 
  31.     if (ctx->segment.shmid < 0) { 
  32.         fprintf(stderr, "%s: shmget err/n", __func__); 
  33.         exit(-1); 
  34.     } 
  35.     ctx->segment.shmaddr = (char*)shmat(ctx->segment.shmid, 0, 0); 
  36.     if (ctx->segment.shmaddr == (char*)-1) { 
  37.         fprintf(stderr, "%s: shmat err/n", __func__); 
  38.         exit(-1); 
  39.     } 
  40.     ctx->image->data = ctx->segment.shmaddr; 
  41.     ctx->segment.readOnly = 0; 
  42.     XShmAttach(ctx->display, &ctx->segment); 
  43.     PixelFormat target_pix_fmt = PIX_FMT_NONE; 
  44.     switch (ctx->image->bits_per_pixel) { 
  45.         case 32: 
  46.             target_pix_fmt = PIX_FMT_RGB32; 
  47.             break
  48.         case 24: 
  49.             target_pix_fmt = PIX_FMT_RGB24; 
  50.             break
  51.         default
  52.             break
  53.     } 
  54.     if (target_pix_fmt == PIX_FMT_NONE) { 
  55.         fprintf(stderr, "%s: screen depth format err/n", __func__); 
  56.         delete ctx; 
  57.         return 0; 
  58.     } 
  59.     // sws 
  60.     ctx->target_pixfmt = target_pix_fmt; 
  61.     ctx->curr_width = cx; 
  62.     ctx->curr_height = cy; 
  63.     ctx->sws = sws_getContext(v_width, v_height, PIX_FMT_YUV420P, 
  64.             cx, cy, target_pix_fmt, 
  65.             SWS_FAST_BILINEAR, 0, 0, 0); 
  66.     avpicture_alloc(&ctx->pic_target, target_pix_fmt, cx, cy); 
  67.     XFlush(ctx->display); 
  68.     return ctx; 

vs_show()

                        sws_scale()                // 拉伸到当前窗口大小, 转换格式

                        XShmPutImage()        // 显示, 呵呵, 真的很简单

 

vs_show(...) 主要代码都是处理窗口变化的

  1. int vs_show (void *ctx, unsigned char *data[4], int stride[4]) 
  2.     // 首选检查 sws 是否有效, 根据当前窗口大小决定 
  3.     Ctx *c = (Ctx*)ctx; 
  4.     Window root; 
  5.     int x, y; 
  6.     unsigned int cx, cy, border, depth; 
  7.     XGetGeometry(c->display, c->window, &root, &x, &y, &cx, &cy, &border, &depth); 
  8.     if (cx != c->curr_width || cy != c->curr_height) { 
  9.         avpicture_free(&c->pic_target); 
  10.         sws_freeContext(c->sws); 
  11.         c->sws = sws_getContext(c->v_width, c->v_height, PIX_FMT_YUV420P, 
  12.                 cx, cy, c->target_pixfmt,  
  13.                 SWS_FAST_BILINEAR, 0, 0, 0); 
  14.         avpicture_alloc(&c->pic_target, c->target_pixfmt, cx, cy); 
  15.         c->curr_width = cx; 
  16.         c->curr_height = cy; 
  17.         // re create image 
  18.         XShmDetach(c->display, &c->segment); 
  19.         shmdt(c->segment.shmaddr); 
  20.         shmctl(c->segment.shmid, IPC_RMID, 0); 
  21.         XDestroyImage(c->image); 
  22.         c->image = XShmCreateImage(c->display, c->vinfo.visual, depth, ZPixmap, 0, 
  23.             &c->segment, cx, cy); 
  24.         c->segment.shmid = shmget(IPC_PRIVATE, 
  25.                 c->image->bytes_per_line * c->image->height, 
  26.                 IPC_CREAT | 0777); 
  27.         c->segment.shmaddr = (char*)shmat(c->segment.shmid, 0, 0); 
  28.         c->image->data = c->segment.shmaddr; 
  29.         c->segment.readOnly = 0; 
  30.         XShmAttach(c->display, &c->segment); 
  31.     } 
  32.     //  
  33.     sws_scale(c->sws, data, stride, 0, c->v_height
  34. , c->pic_target.data, c->pic_target.linesize); 
  35.     // cp to image 
  36.     unsigned char *p = c->pic_target.data[0], *q = (unsigned char*)c->image->data; 
  37.     int xx = MIN(c->image->bytes_per_line, c->pic_target.linesize[0]); 
  38.     for (int i = 0; i < c->curr_height; i++) { 
  39.         memcpy(q, p, xx); 
  40.         p += c->image->bytes_per_line; 
  41.         q += c->pic_target.linesize[0]; 
  42.     } 
  43.     // 显示到 X 上 
  44.     XShmPutImage(c->display, c->window, c->gc, c->image, 0, 0
  45. , 0, 0, c->curr_width, c->curr_height, 1); 
  46.     return 1; 

3. libswscale: 用于picture格式/大小转换, 占用cpu挺高 :), 用起来很简单, 基本就是

                sws = sws_getContext(....);

                sws_scale(sws, ...)

4. libx264 压缩: 考虑主要用于互动, 所以使用 preset=fast, tune=zerolatency, 320x240, 10fps, 300kbps, jj实测延迟很低, 小于 100ms

使用的数据结构

  1. struct Ctx 
  2.     x264_t *x264; 
  3.     x264_picture_t picture; 
  4.     x264_param_t param; 
  5.     void *output;       // 用于保存编码后的完整帧 
  6.     int output_bufsize, output_datasize; 
  7.     int64_t pts;        // 输入 pts 
  8.     int64_t (*get_pts)(struct Ctx *); 
  9.     int64_t info_pts, info_dts; 
  10.     int info_key_frame; 
  11.     int info_valid; 
  12. }; 

vc_open(...) 设置必要的参数, 打开编码器

  1. void *vc_open (int width, int height) 
  2.     Ctx *ctx = new Ctx; 
  3.     // 设置编码属性 
  4.     //x264_param_default(&ctx->param); 
  5.     x264_param_default_preset(&ctx->param, "fast""zerolatency"); 
  6.     ctx->param.i_width = width; 
  7.     ctx->param.i_height = height; 
  8.     ctx->param.b_repeat_headers = 1;  // 重复SPS/PPS 放到关键帧前面 
  9.     ctx->param.b_cabac = 1; 
  10.     ctx->param.i_fps_num = 10; 
  11.     ctx->param.i_fps_den = 1; 
  12.     ctx->param.i_keyint_max = 30; 
  13.     ctx->param.i_keyint_min = 10; 
  14.     // rc 
  15.     ctx->param.rc.i_rc_method = X264_RC_CRF; 
  16.     ctx->param.rc.i_bitrate = 300; 
  17.     //ctx->param.rc.f_rate_tolerance = 0.1; 
  18.     //ctx->param.rc.i_vbv_max_bitrate = ctx->param.rc.i_bitrate * 1.3; 
  19.     //ctx->param.rc.f_rf_constant = 600; 
  20.     //ctx->param.rc.f_rf_constant_max = ctx->param.rc.f_rf_constant * 1.3; 
  21. #ifdef DEBUG 
  22.     ctx->param.i_log_level = X264_LOG_WARNING; 
  23. #else 
  24.     ctx->param.i_log_level = X264_LOG_NONE; 
  25. #endif // release 
  26.     ctx->x264 = x264_encoder_open(&ctx->param); 
  27.     if (!ctx->x264) { 
  28.         fprintf(stderr, "%s: x264_encoder_open err/n", __func__); 
  29.         delete ctx; 
  30.         return 0; 
  31.     } 
  32.     x264_picture_init(&ctx->picture); 
  33.     ctx->picture.img.i_csp = X264_CSP_I420; 
  34.     ctx->picture.img.i_plane = 3; 
  35.     ctx->output = malloc(128*1024); 
  36.     ctx->output_bufsize = 128*1024; 
  37.     ctx->output_datasize = 0; 
  38.     ctx->get_pts = first_pts; 
  39.     ctx->info_valid = 0; 
  40.     return ctx; 

vc_compress(...) 压缩, 如果成功, 得到串流

  1. static int encode_nals (Ctx *c, x264_nal_t *nals, int nal_cnt) 
  2.     char *pout = (char*)c->output; 
  3.     c->output_datasize = 0; 
  4.     for (int i = 0; i < nal_cnt; i++) { 
  5.         if (c->output_datasize + nals[i].i_payload > c->output_bufsize) { 
  6.             // 扩展 
  7.             c->output_bufsize = (c->output_datasize+nals[i].i_payload+4095)/4096*4096; 
  8.             c->output = realloc(c->output, c->output_bufsize); 
  9.         } 
  10.         memcpy(pout+c->output_datasize, nals[i].p_payload, nals[i].i_payload); 
  11.         c->output_datasize += nals[i].i_payload; 
  12.     } 
  13.     return c->output_datasize; 
  14. int vc_compress (void *ctx, unsigned char *data[4]
  15. int stride[4], const void **out, int *len) 
  16.     Ctx *c = (Ctx*)ctx; 
  17.      
  18.     // 设置 picture 数据 
  19.     for (int i = 0; i < 4; i++) { 
  20.         c->picture.img.plane[i] = data[i]; 
  21.         c->picture.img.i_stride[i] = stride[i]; 
  22.     } 
  23.     // encode 
  24.     x264_nal_t *nals; 
  25.     int nal_cnt; 
  26.     x264_picture_t pic_out; 
  27.      
  28.     c->picture.i_pts = c->get_pts(c); 
  29. #ifdef DEBUG_MORE 
  30.     static int64_t _last_pts = c->picture.i_pts; 
  31.     fprintf(stderr, "DBG: pts delta = %lld/n", c->picture.i_pts - _last_pts); 
  32.     _last_pts = c->picture.i_pts; 
  33. #endif // 
  34.     x264_picture_t *pic = &c->picture; 
  35.     do { 
  36.         // 这里努力消耗掉 delayed frames ??? 
  37.         // 实际使用 zerolatency preset 时, 效果足够好了 
  38.         int rc = x264_encoder_encode(c->x264, &nals, &nal_cnt, pic, &pic_out); 
  39.         if (rc < 0) return -1; 
  40.         encode_nals(c, nals, nal_cnt); 
  41.     } while (0); 
  42.     *out = c->output; 
  43.     *len = c->output_datasize; 
  44.     if (nal_cnt > 0) { 
  45.         c->info_valid = 1; 
  46.         c->info_key_frame = pic_out.b_keyframe; 
  47.         c->info_pts = pic_out.i_pts; 
  48.         c->info_dts = pic_out.i_dts; 
  49.     } 
  50.     else { 
  51.         fprintf(stderr, "."); 
  52.         return 0; // 继续 
  53.     } 
  54.  
  55. #ifdef DEBUG_MORE 
  56.     static size_t _seq = 0; 
  57.     fprintf(stderr, "#%lu: [%c] frame type=%d, size=%d/n", _seq,  
  58.             pic_out.b_keyframe ? '*' : '.',  
  59.             pic_out.i_type, c->output_datasize); 
  60.     _seq++; 
  61. #endif // debug 
  62.     return 1; 

附上源码: 唉, 源码是不停更新的, csdn居然没有一个类似 git, svn 之类的仓库, 算了, 如果有人要, email吧.

      main.cpp          主流程

      capture.cpp, capture.h    获取 v4l2 的图像帧

      vcompress.cpp vcompress.h 实现 x264 的压缩

      vshow.cpp vsho.h   用 X11 显示实时图像

(sunkwei)
本站文章除注明转载外,均为本站原创或编译欢迎任何形式的转载,但请务必注明出处,尊重他人劳动,同学习共成长。转载请注明:文章转载自:罗索实验室 [http://www.rosoo.net/a/201303/16558.html]
本文出处:CSDN博客 作者:sunkwei 原文
顶一下
(6)
100%
踩一下
(0)
0%
------分隔线----------------------------
发表评论
请自觉遵守互联网相关的政策法规,严禁发布色情、暴力、反动的言论。
评价:
表情:
用户名: 验证码:点击我更换图片
栏目列表
将本文分享到微信
织梦二维码生成器
推荐内容