RK3568笔记十二:Zlmedia拉流显示测试

news2024/11/15 13:44:17

若该文为原创文章,转载请注明原文出处。

Zlmediakit功能很强大,测试一下拉流,在通过解码显示。

一、环境

1、平台:rk3568

2、开发板:ATK-RK3568正点原子板子

3、环境:buildroot

测试的代码在GitHub - airockchip/rknpu2

main_video.cc主要功能是通过Zlmedia拉取RTSP流,并解码,然后重新编码保存成视频,所以直接在例子上修改程序,增加DRM显示。

二、编译

1、修改交叉工具链

修改build-linux_RK3566_RK3568.sh,

2、增加DRM显示程序

screen_test.cc

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <dlfcn.h>
#include <vector>
#include <string>

#include "rga_func.h"
#include "rknn_api.h"


#include "RgaUtils.h"
#include "im2d.h"
#include "opencv2/core/core.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/imgproc.hpp"
#include <opencv2/opencv.hpp>
#include "rga.h"
#include <xf86drm.h>
#include <xf86drmMode.h>
#include "dev.h"
#include "bo.h"

#include "screen_test.h"



static sp_dev *mDev;
#define OUTPUT_DEVICE_LCD 1

static drmModeConnectorPtr lcdConnectorPtr = nullptr;
static struct sp_crtc *lcdCRPtr;
static drmModeEncoderPtr lcdEncoderPtr = nullptr;
static uint32_t lcdCrtcId = 0;
static drmModeModeInfoPtr lcdModInfoPtr;


// 内部使用的函数原型声明
static void get_connector(uint8_t outpuDevice);
static void get_encoder(uint8_t outpuDevice);
static void get_crtc(void);
static int init_screens();



static void get_connector(uint8_t outpuDevice)
{
    int i, j = 0;
    int ret = 0;

    printf("mDev->num_connectors = %d\n", mDev->num_connectors);
    for (j = 0; j < mDev->num_connectors; j++)
    {
        // name 是分辨率信息
        printf("connector name:%d\n", j);
        printf("connector_type:%d\n", j);
        printf("connector_type_id:%d\n", j);
        printf("connector status:%d\n", j);
        // 对应不同的输出设备, 指定不同的connector跟encoder
        if (outpuDevice == OUTPUT_DEVICE_LCD)
        {
            if (mDev->connectors[j]->connector_type == DRM_MODE_CONNECTOR_DSI &&
                mDev->connectors[j]->connection == DRM_MODE_CONNECTED)
            {
                lcdConnectorPtr = mDev->connectors[j];
            }
        }
       
    }
}

static void get_encoder(uint8_t outpuDevice)
{
    int i;
    for (i = 0; i < mDev->num_encoders; i++)
    {
        if (outpuDevice == OUTPUT_DEVICE_LCD)
        {
            if (mDev->encoders[i]->encoder_type == DRM_MODE_ENCODER_DSI)
            {
                lcdEncoderPtr = mDev->encoders[i];
                lcdCrtcId = lcdEncoderPtr->crtc_id;
            }
        }
       
    }
}

static void get_crtc(void)
{
    int j;

    printf("lcd crtc id:%d\n", lcdCrtcId);

    for (j = 0; j < mDev->num_crtcs; j++)
    {

        printf("encoderPtr->crtc_id:%d\n", mDev->crtcs[j].crtc->crtc_id);
        printf("mode_valid:%d\n", mDev->crtcs[j].crtc->mode_valid);
        printf("mode_name:%s\n", mDev->crtcs[j].crtc->mode.name);
        if (mDev->crtcs[j].crtc->crtc_id == lcdCrtcId && mDev->crtcs[j].crtc->mode_valid)
        {
            lcdCRPtr = &mDev->crtcs[j];
        }
       
    }
}


static int init_screens()
{
    int ret = 0;
    
    // 获取lcd connector
    get_connector(OUTPUT_DEVICE_LCD);

    if (!lcdConnectorPtr)
    {
        printf("failed to get hdmi connector or encoder.\n");
        return -1;
    }

    printf("lcd connector id:%d\n", lcdConnectorPtr->connector_id);

    // 获取lcd encoder
    get_encoder(OUTPUT_DEVICE_LCD);


    if (!lcdEncoderPtr)
    {
        printf("failed to get encoder.\n");
        return -2;
    }

    printf("lcd encoder id:%d\n", lcdEncoderPtr->encoder_id);

    // 获取一下显示分辨率之类
    lcdModInfoPtr = &lcdConnectorPtr->modes[0];

    // 把connector的encoder id赋值为encoder的id
    lcdConnectorPtr->encoder_id = lcdEncoderPtr->encoder_id;

    // 获取lcd crtc
    get_crtc();
    if (!lcdCRPtr)
    {
        printf("failed to get crtc.\n");
        return -3;
    }

    if (lcdCRPtr->scanout)
    {
        printf("crtc already in use\n");
        return -4;
    }

    printf("lcd crtc id:%d\n", lcdCRPtr->crtc->crtc_id);

    // allset
    // 获取bo, 只需要输入分辨率即可.
    lcdCRPtr->scanout = create_sp_bo(mDev, lcdModInfoPtr->hdisplay, lcdModInfoPtr->vdisplay, 24, 32, DRM_FORMAT_XRGB8888, 0);
    if (!lcdCRPtr->scanout)
    {
        printf("failed to create new scanout bo\n");
        return -5;
    }


    printf("fill test color\n");

    fill_bo(lcdCRPtr->scanout, 0xff, 0xff, 0x0, 0x0);

    ret = drmModeSetCrtc(mDev->fd, lcdEncoderPtr->crtc_id, lcdCRPtr->scanout->fb_id, 0, 0, &lcdConnectorPtr->connector_id, 1, lcdModInfoPtr);
    if (ret)
    {
        printf("failed to set crtc mode ret=%d\n", ret);
        return -6;
    }
    lcdCRPtr->crtc = drmModeGetCrtc(mDev->fd, lcdCRPtr->crtc->crtc_id);
    memcpy(&lcdCRPtr->crtc->mode, lcdModInfoPtr, sizeof(*lcdModInfoPtr));

    return 0;
}



int drm_dis_init(void)
{
	 int ret = 0;
    int i = 0;
    printf("create sp dev\n");
    // 创建显示设备
    mDev = create_sp_dev();
    if (!mDev)
    {
        printf("failed to exec create_sp_dev.\n");
        return -10;
    }

    printf("init_screen\n");

    // 初始化屏幕
    ret = init_screens();
    if (ret != 0)
    {
        printf("failed to exec initialize_screens.\n");
        return -11;
    }
    return 0;
}

void draw_lcd_screen_rgb_960(uint8_t *data, uint32_t dataSize)
{
    uint32_t colIdx = 0;
    uint32_t rowIdx = 0;
    uint8_t *dataPtr = data;
    for (rowIdx = 0; rowIdx < 1280; rowIdx++)
    {
        uint8_t *rowPtr = (uint8_t *)lcdCRPtr->scanout->map_addr + rowIdx * lcdCRPtr->scanout->pitch;
        for (colIdx = 0; colIdx < 720; colIdx++)
        {
            uint8_t *pixel = rowPtr + colIdx * 4;   // bgr
            #if 1
            pixel[0] = *dataPtr;         
            dataPtr++;
            pixel[1] = *dataPtr;
            dataPtr++;
            pixel[2] = *dataPtr;
            dataPtr++;
            pixel[3] = 0xff;
            #else  // bgra
            pixel[0] = 0xff;   // B
            dataPtr++;
            pixel[1] = 0x00;   //G 
            dataPtr++;
            pixel[2] = 0x00;   //R
            dataPtr++;
            pixel[3] = 0xFF;
            #endif
        }
    }
}



void draw_lcd_screen_rgb_dynamic(uint8_t *data, uint32_t dataSize, uint8_t screenNum, uint8_t rows, uint8_t cols)
{
    if (rows == 0 || cols == 0 || screenNum >= rows * cols)
    {
        return; // 避免除零错误和数组越界
    }

    uint32_t startRowIdx, startColIdx;
    uint32_t screenWidth = LCD_SCREEN_WIDTH / cols;
    uint32_t screenHeight = LCD_SCREEN_HEIGHT / rows;

    // 计算起始行和列索引
    startRowIdx = (screenNum / cols) * screenHeight; // 根据屏幕编号计算起始行索引
    startColIdx = (screenNum % cols) * screenWidth;  // 根据屏幕编号计算起始列索引

    uint32_t rowIdx, colIdx;
    uint8_t *dataPtr = data;
    for (rowIdx = startRowIdx; rowIdx < startRowIdx + screenHeight; rowIdx++)
    {
        uint8_t *rowPtr = (uint8_t *)lcdCRPtr->scanout->map_addr + rowIdx * lcdCRPtr->scanout->pitch;
        for (colIdx = startColIdx; colIdx < startColIdx + screenWidth; colIdx++)
        {
            uint8_t *pixel = rowPtr + colIdx * 4;
            memcpy(pixel, dataPtr, 4);
            dataPtr += 4;
        }
    }
}


void draw_lcd_screen_rgb_nine(uint8_t *data, uint32_t dataSize, uint8_t part)
{
    uint32_t startRowIdx, startColIdx;
    uint32_t partWidth = LCD_SCREEN_WIDTH / 3;
    uint32_t partHeight = LCD_SCREEN_HEIGHT / 3;

    // 计算起始行和列索引
    startRowIdx = (part / 3) * partHeight; // 根据部分的行来计算起始行索引
    startColIdx = (part % 3) * partWidth;  // 根据部分的列来计算起始列索引

    uint32_t rowIdx, colIdx;
    uint8_t *dataPtr = data;
    for (rowIdx = startRowIdx; rowIdx < startRowIdx + partHeight; rowIdx++)
    {
        uint8_t *rowPtr = (uint8_t *)lcdCRPtr->scanout->map_addr + rowIdx * lcdCRPtr->scanout->pitch;
        for (colIdx = startColIdx; colIdx < startColIdx + partWidth; colIdx++)
        {
            uint8_t *pixel = rowPtr + colIdx * 4;
            memcpy(pixel, dataPtr, 4);
            dataPtr += 4;
        }
    }
}


void draw_lcd_screen_rgb_quarter(uint8_t *data, uint32_t dataSize, uint8_t quarter)
{
    uint32_t startRowIdx, startColIdx;
    switch (quarter)
    {
    case 0: // 左上角
        startRowIdx = 0;
        startColIdx = 0;
        break;
    case 1: // 右上角
        startRowIdx = 0;
        startColIdx = LCD_SCREEN_WIDTH / 2;
        break;
    case 2: // 左下角
        startRowIdx = LCD_SCREEN_HEIGHT / 2;
        startColIdx = 0;
        break;
    case 3: // 右下角
        startRowIdx = LCD_SCREEN_HEIGHT / 2;
        startColIdx = LCD_SCREEN_WIDTH / 2;
        break;
    default: // 默认为左上角
        startRowIdx = 0;
        startColIdx = 0;
        break;
    }

    uint32_t rowIdx, colIdx;
    uint8_t *dataPtr = data;
    for (rowIdx = startRowIdx; rowIdx < startRowIdx + LCD_SCREEN_HEIGHT / 2; rowIdx++)
    {
        uint8_t *rowPtr = (uint8_t *)lcdCRPtr->scanout->map_addr + rowIdx * lcdCRPtr->scanout->pitch;
        for (colIdx = startColIdx; colIdx < startColIdx + LCD_SCREEN_WIDTH / 2; colIdx++)
        {
            uint8_t *pixel = rowPtr + colIdx * 4;
            memcpy(pixel, dataPtr, 4);
            dataPtr += 4;
        }
    }
}

void draw_lcd_screen_rgb(uint8_t *data, uint32_t dataSize)
{

    uint32_t colIdx = 0;
    uint32_t rowIdx = 0;
    uint8_t *dataPtr = data;
    for (rowIdx = 0; rowIdx < LCD_SCREEN_WIDTH; rowIdx++)
    {
        uint8_t *rowPtr = (uint8_t *)lcdCRPtr->scanout->map_addr + rowIdx * lcdCRPtr->scanout->pitch;
        for (colIdx = 0; colIdx < LCD_SCREEN_HEIGHT; colIdx++)
        {
            uint8_t *pixel = rowPtr + colIdx * 4;
            memcpy(pixel, dataPtr, 4);
            dataPtr += 4;
#if 0
            uint8_t *pixel = rowPtr + colIdx * 4;
            pixel[0] = *dataPtr;
            dataPtr++;
            pixel[1] = *dataPtr;
            dataPtr++;
            pixel[2] = *dataPtr;
            dataPtr++;
            pixel[3] = 0xff;
#endif
        }
    }
}

3、格式转换显示

使用的是正点原子的5.5寸屏,在调试过程中,一直卡在显示部分,后面才发现,正点原子使用的是竖屏,横屏显示会出现问题。

修改mpp_decoder_frame_callback函数,解码后的格式是420_SP,需要转成BGRA8888格式才能显示,使用RGA转换格式

memset(&src_rect, 0, sizeof(src_rect));
  memset(&dst_rect, 0, sizeof(dst_rect));
  memset(&src1, 0, sizeof(src1));
  memset(&dst1, 0, sizeof(dst1));
  printf("resize with RGA!\n");
  resize_buf = (unsigned char *)malloc(1280 * 720 * 3);
  memset(resize_buf, 0, 1280 * 720 * 3);
  printf("=========> width_stride: %d, height_stride: %d\n", width_stride, height_stride);
  src1 = wrapbuffer_virtualaddr((unsigned char *)mpp_frame_addr, width_stride, height_stride, RK_FORMAT_YCbCr_420_SP, width_stride, height_stride);
  dst1 = wrapbuffer_virtualaddr((unsigned char*)resize_buf, 720, 1280, RK_FORMAT_BGR_888);
  ret = imcheck(src1, dst1, src_rect, dst_rect);
  if (IM_STATUS_NOERROR != ret) {
    printf("%d, check error! %s", __LINE__, imStrError((IM_STATUS)ret));
    free(resize_buf);
    return ;
  }
  IM_STATUS STATUS = imresize(src1, dst1);
	
	
  //MySaveBmp("out.bmp", (unsigned char *)resize_buf, 1280, 720);
  // 显示在屏目上。
  printf("draw_lcd_screen_rgb_960==============>\n");
  draw_lcd_screen_rgb_960((uint8_t *)resize_buf, 1280 * 720 * 3);
  printf("draw_lcd_screen_rgb_960 ok\n");
  free(resize_buf);

需要注意 dst1 = wrapbuffer_virtualaddr((unsigned char*)resize_buf, 720, 1280, RK_FORMAT_BGR_888);,原本图片是1280*720,需要对调才能正常显示。

三、推流

先准备一个1280*720的视频,使用的是FFMPEG方式推流,但直接推流是无法拉流的,所以先启动一个RTSP服务器。

服务器下载地址Releases · aler9/rtsp-simple-server · GitHub

  • 启动rtsp-simple-server

下载完成后解压缩然后执行里面的rtsp-simple-server.exe

ffmpeg推流直接使用命令

ffmpeg -re -stream_loop -1 -i test.mp4 -c copy -f rtsp rtsp://192.168.0.107:8554/stream

四、程序解析

// Copyright (c) 2023 by Rockchip Electronics Co., Ltd. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/*-------------------------------------------
                Includes
-------------------------------------------*/
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>

#include "im2d.h"
#include "rga.h"
#include "RgaUtils.h"

#include "rknn_api.h"
#include "postprocess.h"

#include "utils/mpp_decoder.h"
#include "utils/mpp_encoder.h"
#include "utils/drawing.h"
#include "im2d_buffer.h"
#include "screen_test.h"
#if defined(BUILD_VIDEO_RTSP)
#include "mk_mediakit.h"

#endif

#define OUT_VIDEO_PATH "out.h264"

#define LOG_TAG "MPP_API"

static FILE *g_save_nv12;    // nv12 

typedef struct {
  rknn_context rknn_ctx;
  rknn_input_output_num io_num;
  rknn_tensor_attr* input_attrs;
  rknn_tensor_attr* output_attrs;
  int model_channel;
  int model_width;
  int model_height;
  FILE* out_fp;
  MppDecoder* decoder;
  MppEncoder* encoder;
} rknn_app_context_t;

typedef struct {
  int width;
  int height;
  int width_stride;
  int height_stride;
  int format;
  char* virt_addr;
  int fd;
} image_frame_t;

/*-------------------------------------------
                  Functions
-------------------------------------------*/

static void dump_tensor_attr(rknn_tensor_attr* attr)
{
  printf("  index=%d, name=%s, n_dims=%d, dims=[%d, %d, %d, %d], n_elems=%d, size=%d, fmt=%s, type=%s, qnt_type=%s, "
         "zp=%d, scale=%f\n",
         attr->index, attr->name, attr->n_dims, attr->dims[0], attr->dims[1], attr->dims[2], attr->dims[3],
         attr->n_elems, attr->size, get_format_string(attr->fmt), get_type_string(attr->type),
         get_qnt_type_string(attr->qnt_type), attr->zp, attr->scale);
}

double __get_us(struct timeval t) { return (t.tv_sec * 1000000 + t.tv_usec); }

static unsigned char* load_data(FILE* fp, size_t ofst, size_t sz)
{
  unsigned char* data;
  int ret;

  data = NULL;

  if (NULL == fp) {
    return NULL;
  }

  ret = fseek(fp, ofst, SEEK_SET);
  if (ret != 0) {
    printf("blob seek failure.\n");
    return NULL;
  }

  data = (unsigned char*)malloc(sz);
  if (data == NULL) {
    printf("buffer malloc failure.\n");
    return NULL;
  }
  ret = fread(data, 1, sz, fp);
  return data;
}

static unsigned char* read_file_data(const char* filename, int* model_size)
{
  FILE* fp;
  unsigned char* data;

  fp = fopen(filename, "rb");
  if (NULL == fp) {
    printf("Open file %s failed.\n", filename);
    return NULL;
  }

  fseek(fp, 0, SEEK_END);
  int size = ftell(fp);

  data = load_data(fp, 0, size);

  fclose(fp);

  *model_size = size;
  return data;
}

static int write_data_to_file(const char *path, char *data, unsigned int size) {
  FILE *fp;

  fp = fopen(path, "w");
  if(fp == NULL) {
    printf("open error: %s", path);
    return -1;
  }

  fwrite(data, 1, size, fp);
  fflush(fp);

  fclose(fp);
  return 0;
}

static int init_model(const char* model_path, rknn_app_context_t* app_ctx) {
  int ret;
  rknn_context ctx;

  /* Create the neural network */
  printf("Loading mode...\n");
  int model_data_size = 0;
  unsigned char* model_data = read_file_data(model_path, &model_data_size);
  if (model_data == NULL) {
    return -1;
  }

  ret = rknn_init(&ctx, model_data, model_data_size, 0, NULL);
  if (ret < 0) {
    printf("rknn_init error ret=%d\n", ret);
    return -1;
  }

  if (model_data) {
    free(model_data);
  }

  rknn_sdk_version version;
  ret = rknn_query(ctx, RKNN_QUERY_SDK_VERSION, &version, sizeof(rknn_sdk_version));
  if (ret < 0) {
    printf("rknn_query RKNN_QUERY_SDK_VERSION error ret=%d\n", ret);
    return -1;
  }
  printf("sdk version: %s driver version: %s\n", version.api_version, version.drv_version);

  ret = rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &app_ctx->io_num, sizeof(rknn_input_output_num));
  if (ret < 0) {
    printf("rknn_query RKNN_QUERY_IN_OUT_NUM error ret=%d\n", ret);
    return -1;
  }
  printf("model input num: %d, output num: %d\n", app_ctx->io_num.n_input, app_ctx->io_num.n_output);

  rknn_tensor_attr* input_attrs = (rknn_tensor_attr*)malloc(app_ctx->io_num.n_input * sizeof(rknn_tensor_attr));
  memset(input_attrs, 0, sizeof(input_attrs));
  for (int i = 0; i < app_ctx->io_num.n_input; i++) {
    input_attrs[i].index = i;
    ret = rknn_query(ctx, RKNN_QUERY_INPUT_ATTR, &(input_attrs[i]), sizeof(rknn_tensor_attr));
    if (ret < 0) {
      printf("rknn_query RKNN_QUERY_INPUT_ATTR error ret=%d\n", ret);
      return -1;
    }
    dump_tensor_attr(&(input_attrs[i]));
  }

  rknn_tensor_attr* output_attrs = (rknn_tensor_attr*)malloc(app_ctx->io_num.n_output * sizeof(rknn_tensor_attr));
  memset(output_attrs, 0, sizeof(output_attrs));
  for (int i = 0; i < app_ctx->io_num.n_output; i++) {
    output_attrs[i].index = i;
    ret = rknn_query(ctx, RKNN_QUERY_OUTPUT_ATTR, &(output_attrs[i]), sizeof(rknn_tensor_attr));
    if (ret < 0) {
      printf("rknn_query RKNN_QUERY_OUTPUT_ATTR error ret=%d\n", ret);
      return -1;
    }
    dump_tensor_attr(&(output_attrs[i]));
  }

  app_ctx->input_attrs = input_attrs;
  app_ctx->output_attrs = output_attrs;
  app_ctx->rknn_ctx = ctx;

  if (input_attrs[0].fmt == RKNN_TENSOR_NCHW) {
    printf("model is NCHW input fmt\n");
    app_ctx->model_channel = input_attrs[0].dims[1];
    app_ctx->model_height  = input_attrs[0].dims[2];
    app_ctx->model_width   = input_attrs[0].dims[3];
  } else {
    printf("model is NHWC input fmt\n");
    app_ctx->model_height  = input_attrs[0].dims[1];
    app_ctx->model_width   = input_attrs[0].dims[2];
    app_ctx->model_channel = input_attrs[0].dims[3];
  }
  printf("model input height=%d, width=%d, channel=%d\n", app_ctx->model_height, app_ctx->model_width, app_ctx->model_channel);

  return 0;
}

static int release_model(rknn_app_context_t* app_ctx) {
  if (app_ctx->rknn_ctx != NULL) {
    rknn_destroy(app_ctx->rknn_ctx);
  }
  free(app_ctx->input_attrs);
  free(app_ctx->output_attrs);
  deinitPostProcess();
  return 0;
}

static int inference_model(rknn_app_context_t* app_ctx, image_frame_t* img, detect_result_group_t* detect_result) {
  int ret;
  rknn_context ctx = app_ctx->rknn_ctx;
  int model_width = app_ctx->model_width;
  int model_height = app_ctx->model_height;
  int model_channel = app_ctx->model_channel;

  struct timeval start_time, stop_time;
  const float    nms_threshold      = NMS_THRESH;
  const float    box_conf_threshold = BOX_THRESH;
  // You may not need resize when src resulotion equals to dst resulotion
  void* resize_buf = nullptr;
  // init rga context
  rga_buffer_t src;
  rga_buffer_t dst;
  im_rect      src_rect;
  im_rect      dst_rect;
  memset(&src_rect, 0, sizeof(src_rect));
  memset(&dst_rect, 0, sizeof(dst_rect));
  memset(&src, 0, sizeof(src));
  memset(&dst, 0, sizeof(dst));

  printf("input image %dx%d stride %dx%d format=%d\n", img->width, img->height, img->width_stride, img->height_stride, img->format);

  float scale_w = (float)model_width / img->width;
  float scale_h = (float)model_height / img->height;

  rknn_input inputs[1];
  memset(inputs, 0, sizeof(inputs));
  inputs[0].index        = 0;
  inputs[0].type         = RKNN_TENSOR_UINT8;
  inputs[0].size         = model_width * model_height * model_channel;
  inputs[0].fmt          = RKNN_TENSOR_NHWC;
  inputs[0].pass_through = 0;

  printf("resize with RGA!\n");
  resize_buf = malloc(model_width * model_height * model_channel);
  memset(resize_buf, 0, model_width * model_height * model_channel);

  src = wrapbuffer_virtualaddr((void*)img->virt_addr, img->width, img->height, img->format, img->width_stride, img->height_stride);
  dst = wrapbuffer_virtualaddr((void*)resize_buf, model_width, model_height, RK_FORMAT_RGB_888);
  ret = imcheck(src, dst, src_rect, dst_rect);
  if (IM_STATUS_NOERROR != ret) {
    printf("%d, check error! %s", __LINE__, imStrError((IM_STATUS)ret));
    return -1;
  }
  IM_STATUS STATUS = imresize(src, dst);

  inputs[0].buf = resize_buf;

  gettimeofday(&start_time, NULL);
  rknn_inputs_set(ctx, app_ctx->io_num.n_input, inputs);

  rknn_output outputs[app_ctx->io_num.n_output];
  memset(outputs, 0, sizeof(outputs));
  for (int i = 0; i < app_ctx->io_num.n_output; i++) {
    outputs[i].want_float = 0;
  }

  ret = rknn_run(ctx, NULL);
  ret = rknn_outputs_get(ctx, app_ctx->io_num.n_output, outputs, NULL);
  gettimeofday(&stop_time, NULL);
  printf("once run use %f ms\n", (__get_us(stop_time) - __get_us(start_time)) / 1000);

  printf("post process config: box_conf_threshold = %.2f, nms_threshold = %.2f\n", box_conf_threshold, nms_threshold);

  std::vector<float> out_scales;
  std::vector<int32_t> out_zps;
  for (int i = 0; i < app_ctx->io_num.n_output; ++i) {
    out_scales.push_back(app_ctx->output_attrs[i].scale);
    out_zps.push_back(app_ctx->output_attrs[i].zp);
  }

  post_process((int8_t*)outputs[0].buf, (int8_t*)outputs[1].buf, (int8_t*)outputs[2].buf, model_height, model_width,
               box_conf_threshold, nms_threshold, scale_w, scale_h, out_zps, out_scales, detect_result);
  ret = rknn_outputs_release(ctx, app_ctx->io_num.n_output, outputs);

  if (resize_buf) {
    free(resize_buf);
  }
  return 0;
}


typedef struct                       /**** BMP file info structure ****/  
{  
    unsigned int   biSize;           /* Size of info header */  
    int            biWidth;          /* Width of image */  
    int            biHeight;         /* Height of image */  
    unsigned short biPlanes;         /* Number of color planes */  
    unsigned short biBitCount;       /* Number of bits per pixel */  
    unsigned int   biCompression;    /* Type of compression to use */  
    unsigned int   biSizeImage;      /* Size of image data */  
    int            biXPelsPerMeter;  /* X pixels per meter */  
    int            biYPelsPerMeter;  /* Y pixels per meter */  
    unsigned int   biClrUsed;        /* Number of colors used */  
    unsigned int   biClrImportant;   /* Number of important colors */  
} BITMAPINFOHEADER;

typedef struct                       /**** BMP file header structure ****/  
{  
    unsigned int   bfSize;           /* Size of file */  
    unsigned short bfReserved1;      /* Reserved */  
    unsigned short bfReserved2;      /* ... */  
    unsigned int   bfOffBits;        /* Offset to bitmap data */  
} BITMAPFILEHEADER; 


void MySaveBmp(const char *filename,unsigned char *rgbbuf,int width,int height)  
{  
    BITMAPFILEHEADER bfh;  
    BITMAPINFOHEADER bih;  
    /* 
     * Magic number for file. It does not fit in the header structure due to 
     * alignment requirements, so put it outside 
     * 文件的魔术字,由于对齐的需要,没办法将魔术字作为BITMAPFILEHEADER的成员,所以
     * 这里将魔术字放在BITMAPFILEHEADER开头外面的位置。
     */  
    unsigned short bfType=0x4d42;    //'BM'             
    bfh.bfReserved1 = 0;  
    bfh.bfReserved2 = 0;  
    bfh.bfSize = 2/* 2B魔术字 */+sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER)+width*height*3;  
    bfh.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);  
  
    bih.biSize = sizeof(BITMAPINFOHEADER);  
    bih.biWidth = width;  
    bih.biHeight = height;  
    bih.biPlanes = 1;  
    bih.biBitCount = 24;  
    bih.biCompression = 0;  
    bih.biSizeImage = 0;  
    bih.biXPelsPerMeter = 5000;  
    bih.biYPelsPerMeter = 5000;  
    bih.biClrUsed = 0;  
    bih.biClrImportant = 0;  
  
    FILE *file = fopen(filename, "wb");  
    if (!file)  
    {  
        printf("Could not write file\n");  
        return;  
    }  
  
    /*Write headers*/  
    fwrite(&bfType,sizeof(bfType),1,file);  
    fwrite(&bfh,sizeof(bfh),1, file);  
    fwrite(&bih,sizeof(bih),1, file);  
  
    fwrite(rgbbuf,width*height*3,1,file);  
    fclose(file);  
}

// MPP解码回调函数
void mpp_decoder_frame_callback(void* userdata, int width_stride, int height_stride, int width, int height, int format, int fd, void* data) {

  rknn_app_context_t* ctx = (rknn_app_context_t*)userdata;

  int ret = 0;
  static int frame_index = 0;
  frame_index++;

  void* mpp_frame = NULL;
  int mpp_frame_fd = 0;
  void* mpp_frame_addr = NULL;
  int mpp_frame_size;
  int enc_data_size;

  rga_buffer_t origin;
  rga_buffer_t src;

  rga_buffer_t src1;
  rga_buffer_t dst1;
  unsigned char *resize_buf = nullptr;

  im_rect      src_rect;
  im_rect      dst_rect;

  // 编码器初始化
  if (ctx->encoder == NULL) {
    MppEncoder* mpp_encoder = new MppEncoder();
    MppEncoderParams enc_params;
    memset(&enc_params, 0, sizeof(MppEncoderParams));
    enc_params.width = width;
    enc_params.height = height;
    enc_params.hor_stride = width_stride;
    enc_params.ver_stride = height_stride;
    enc_params.fmt = MPP_FMT_YUV420SP;
    //enc_params.type = MPP_VIDEO_CodingHEVC;
    //Note: rk3562只能支持h264格式的视频流
    enc_params.type = MPP_VIDEO_CodingAVC;
    mpp_encoder->Init(enc_params, NULL);

    ctx->encoder = mpp_encoder;
  }

  int enc_buf_size = ctx->encoder->GetFrameSize();
  char* enc_data = (char*)malloc(enc_buf_size);

  // 图片格式
  image_frame_t img;
  img.width = width;
  img.height = height;
  img.width_stride = width_stride;
  img.height_stride = height_stride;
  img.fd = fd;
  img.virt_addr = (char*)data;
  img.format = RK_FORMAT_YCbCr_420_SP;
  detect_result_group_t detect_result;
  memset(&detect_result, 0, sizeof(detect_result_group_t));
  // RKNN推理
  ret = inference_model(ctx, &img, &detect_result);
  if (ret != 0) 
  {
    printf("inference model fail\n");

    if(enc_data)
      free(enc_data);

    return ;
  }

  mpp_frame = ctx->encoder->GetInputFrameBuffer();
  mpp_frame_fd = ctx->encoder->GetInputFrameBufferFd(mpp_frame);
  mpp_frame_addr = ctx->encoder->GetInputFrameBufferAddr(mpp_frame);
  mpp_frame_size = ctx->encoder->GetFrameSize();
  // 图片格式转换
  // Copy To another buffer avoid to modify mpp decoder buffer
  printf("wrapbuffer_fd==> width: %d, height: %d\n", width, height);
  origin = wrapbuffer_fd(fd, width, height, RK_FORMAT_YCbCr_420_SP, width_stride, height_stride);
  src = wrapbuffer_fd(mpp_frame_fd, width, height, RK_FORMAT_YCbCr_420_SP, width_stride, height_stride);
  imcopy(origin, src);

  // Draw objects
  for (int i = 0; i < detect_result.count; i++) {
    detect_result_t* det_result = &(detect_result.results[i]);
    printf("%s @ (%d %d %d %d) %f\n", det_result->name, det_result->box.left, det_result->box.top,
           det_result->box.right, det_result->box.bottom, det_result->prop);
    int x1 = det_result->box.left;
    int y1 = det_result->box.top;
    int x2 = det_result->box.right;
    int y2 = det_result->box.bottom;
    // 画框
    draw_rectangle_yuv420sp((unsigned char*)mpp_frame_addr, width_stride, height_stride, x1, y1, x2-x1+1, y2-y1+1, 0x00FF0000, 4);
  }
  
  //  保存原始视频流
  #if 0
        if (g_save_nv12) {
          fwrite((unsigned char*)mpp_frame_addr, 1, mpp_frame_size, g_save_nv12);
          printf("#Save len-%d to %s\n", mpp_frame_size, g_save_nv12);
        }
  #endif

  // 格式转换,420_SP转成BGR888
  memset(&src_rect, 0, sizeof(src_rect));
  memset(&dst_rect, 0, sizeof(dst_rect));
  memset(&src1, 0, sizeof(src1));
  memset(&dst1, 0, sizeof(dst1));
  printf("resize with RGA!\n");
  resize_buf = (unsigned char *)malloc(1280 * 720 * 3);
  memset(resize_buf, 0, 1280 * 720 * 3);
  printf("=========> width_stride: %d, height_stride: %d\n", width_stride, height_stride);
  src1 = wrapbuffer_virtualaddr((unsigned char *)mpp_frame_addr, width_stride, height_stride, RK_FORMAT_YCbCr_420_SP, width_stride, height_stride);
  dst1 = wrapbuffer_virtualaddr((unsigned char*)resize_buf, 720, 1280, RK_FORMAT_BGR_888);
  ret = imcheck(src1, dst1, src_rect, dst_rect);
  if (IM_STATUS_NOERROR != ret) {
    printf("%d, check error! %s", __LINE__, imStrError((IM_STATUS)ret));
    free(resize_buf);
    return ;
  }
  IM_STATUS STATUS = imresize(src1, dst1);
	
	
  //MySaveBmp("out.bmp", (unsigned char *)resize_buf, 1280, 720);
  // 显示在屏目上。
  printf("draw_lcd_screen_rgb_960==============>\n");
  draw_lcd_screen_rgb_960((uint8_t *)resize_buf, 1280 * 720 * 3);
  printf("draw_lcd_screen_rgb_960 ok\n");
  free(resize_buf);
 
  printf("waite get char to next step!\n");
  //getchar();

  printf("====>Encode\n");
  // Encode to file
  // Write header on first frame
  if (frame_index == 1) {
    enc_data_size = ctx->encoder->GetHeader(enc_data, enc_buf_size);
    fwrite(enc_data, 1, enc_data_size, ctx->out_fp);
  }
  printf("====>GetHeader ok\n");
  memset(enc_data, 0, enc_buf_size);
  enc_data_size = ctx->encoder->Encode(mpp_frame, enc_data, enc_buf_size);
  printf("====>Encode ok\n");
  fwrite(enc_data, 1, enc_data_size, ctx->out_fp);

  if (enc_data != nullptr) 
    {
      free(enc_data);
    }
  printf("fwrite ok\n");



}

int process_video_file(rknn_app_context_t* ctx, const char* path)
{
  int video_size;
  char* video_data = (char*)read_file_data(path, &video_size);
  char* video_data_end = video_data + video_size;
  printf("read video size=%d\n", video_size);

  const int SIZE = 8192;
  char* video_data_ptr = video_data;

  do {
      int pkt_eos = 0;
      int size = SIZE;
      if (video_data_ptr + size >= video_data_end) {
          pkt_eos = 1;
          size = video_data_end - video_data_ptr;
      }

      ctx->decoder->Decode((uint8_t*)video_data_ptr, size, pkt_eos);

      video_data_ptr += size;

      if (video_data_ptr >= video_data_end) {
          printf("reset decoder\n");
          break;
      }

      // LOGD("video_data_ptr=%p video_data_end=%p", video_data_ptr, video_data_end);
      // usleep(10*1000);
  } while (1);

  return 0;
}

#if defined(BUILD_VIDEO_RTSP)
void API_CALL on_track_frame_out(void *user_data, mk_frame frame) {
  rknn_app_context_t *ctx = (rknn_app_context_t *) user_data;
  printf("on_track_frame_out ctx=%p\n", ctx);
  const char* data = mk_frame_get_data(frame);
  size_t size = mk_frame_get_data_size(frame);
  printf("decoder=%p\n", ctx->decoder);
  ctx->decoder->Decode((uint8_t*)data, size, 0);
}

void API_CALL on_mk_play_event_func(void *user_data, int err_code, const char *err_msg, mk_track tracks[],
                                    int track_count) {
  rknn_app_context_t *ctx = (rknn_app_context_t *) user_data;
  if (err_code == 0) {
      //success
      printf("play success!");
      int i;
      for (i = 0; i < track_count; ++i) {
          if (mk_track_is_video(tracks[i])) {
              log_info("got video track: %s", mk_track_codec_name(tracks[i]));
              //监听track数据回调
              mk_track_add_delegate(tracks[i], on_track_frame_out, user_data);
          }
      }
  } else {
      printf("play failed: %d %s", err_code, err_msg);
  }
}

void API_CALL on_mk_shutdown_func(void *user_data, int err_code, const char *err_msg, mk_track tracks[], int track_count) {
  printf("play interrupted: %d %s", err_code, err_msg);
}

// 下面为Zlmeidia拉流处理
int process_video_rtsp(rknn_app_context_t* ctx, const char* url)
{
  mk_config config;
  memset(&config, 0, sizeof(mk_config));
  config.log_mask = LOG_CONSOLE;
  mk_env_init(&config);
  mk_player player = mk_player_create();
  mk_player_set_on_result(player, on_mk_play_event_func, ctx);
  mk_player_set_on_shutdown(player, on_mk_shutdown_func, ctx);
  mk_player_play(player, url);

  printf("enter any key to exit\n");
  getchar();

  if (player) {
      mk_player_release(player);
  }
  return 0;
}
#endif

/*-------------------------------------------
                  Main Functions
-------------------------------------------*/
int main(int argc, char** argv)
{
  int status = 0;
  int ret;

  if (argc != 4) {
    printf("Usage: %s <rknn_model> <video_path> <video_type 264/265> \n", argv[0]);
    return -1;
  }

  char* model_name = (char*)argv[1];
  char* video_name = argv[2];
  int video_type = atoi(argv[3]);

  rknn_app_context_t app_ctx;
  memset(&app_ctx, 0, sizeof(rknn_app_context_t));

  ret = init_model(model_name, &app_ctx);
  if (ret != 0) {
    printf("init model fail\n");
    return -1;
  }

  // MPP解码器
  if (app_ctx.decoder == NULL) {
    MppDecoder* decoder = new MppDecoder();
    decoder->Init(video_type, 30, &app_ctx);
    decoder->SetCallback(mpp_decoder_frame_callback);
    app_ctx.decoder = decoder;
  }

  // 视频保存
  if (app_ctx.out_fp == NULL) {
    FILE* fp = fopen(OUT_VIDEO_PATH, "w");
    if(fp == NULL) {
        printf("open %s error\n", OUT_VIDEO_PATH);
        return -1;
    }
    app_ctx.out_fp = fp;
  }
  
  // NV12保存
  g_save_nv12 = fopen("output.nv12", "w");
  if (!g_save_nv12)
    printf("#VENC TEST:: Open ./output.nv12 failed!\n");

  printf("app_ctx=%p decoder=%p\n", &app_ctx, app_ctx.decoder);

  // 判断是不是RTSP流还是文件
  if (strncmp(video_name, "rtsp", 4) == 0) {
#if defined(BUILD_VIDEO_RTSP)
        // DRM初始化
        drm_dis_init();
        // 拉流处理
        process_video_rtsp(&app_ctx, video_name);
#else
        printf("rtsp no support\n");
#endif
  } else {
    // 文件流处理
    process_video_file(&app_ctx, video_name);
  }

  printf("waiting finish\n");
  usleep(3*1000*1000);

  // release
  fflush(app_ctx.out_fp);
  fclose(app_ctx.out_fp);

  if (app_ctx.decoder != nullptr) {
    delete(app_ctx.decoder);
    app_ctx.decoder = nullptr;
  }
  if (app_ctx.encoder != nullptr) {
    delete(app_ctx.encoder);
    app_ctx.encoder = nullptr;
  }

  release_model(&app_ctx);

  return 0;
}

通过Zlmedia很方便的拉取RTSP流,并解码显示。使用DRM是为了多路显示。OPENCV只会显示一路,不知道怎么拼接流显示。

如有侵权,或需要完整代码,请及时联系博主。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1439071.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ubuntu20安装mongodb

方法一&#xff1a;直接安装(命令是直接从mongo官网Install MongoDB Community Edition on Ubuntu — MongoDB Manual复制的&#xff09; cat /etc/lsb-release sudo apt-get install -y gnupg curl curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | \sudo gp…

VS Code中主程序C文件引用了另一个.h头文件,编译时报错找不到函数

目录 一、问题描述二、问题原因三、解决方法四、扩展五、通过CMake进行配置 一、问题描述 VS Code中主程序C文件引用了另一个.h头文件&#xff0c;编译时报错找不到函数 主程序 main.c #include <stdio.h> #include "sumaa.h"int main(int, char**){printf(&q…

秘塔科技推出AI搜索产品「秘塔AI搜索」

近日&#xff0c;国内一家人工智能科技公司&#xff08;秘塔科技&#xff09;推出了一款AI搜索产品——秘塔AI搜索&#xff0c;能够大幅提升搜索效率&#xff0c;解决日常生活、工作学习等场景中遇到的各类搜索需求。 秘塔AI搜索官网&#xff1a;https://metaso.cn/ 相较于传统…

Java 学习和实践笔记(2)

今天的学习进度&#xff1a; 注册并下载安装好了Java 8&#xff0c;之后进行以下配置。 1&#xff09;path 是一个常见的环境变量&#xff0c;它告诉系统除了在当前的目标下妹寻找此程序外&#xff0c;还可以到path指定的目录下找。 2&#xff09;Java Home 为以后其他的软…

FastAdmin西陆房产系统(xiluHouse)全开源

应用介绍 一款基于FastAdminThinkPHPUniapp开发的西陆房产管理系统&#xff0c;支持小程序、H5、APP&#xff1b;包含房客、房东(高级授权)、经纪人(高级授权)三种身份。核心功能有&#xff1a;新盘销售、房屋租赁、地图找房、房源代理(高级授权)、在线签约(高级授权)、电子合同…

MATLAB环境下用于提取冲击信号的几种解卷积方法

卷积混合考虑了信号的时延&#xff0c;每一个单独源信号的时延信号都会和传递路径发生一 次线性瞬时混合&#xff1b;解卷积的过程就是找一个合适的滤波器&#xff0c;进行反卷积运算&#xff0c;得到源信号的近似解。 声音不可避免的会发生衍射、反射等现象&#xff0c;所以&…

(注解配置AOP)学习Spring的第十七天

基于注解配置的AOP 来看注解式开发 : 先把目标与通知放到Spring里管理 : Service("userService") public class UserServiceImpl implements UserService {Overridepublic void show1() {System.out.println("show1......");}Overridepublic void show2…

Elasticsearch:使用 LangChain 文档拆分器进行文档分块

使用 Elasticsearch 嵌套密集向量支持 这个交互式笔记本将&#xff1a; 将模型 “sentence-transformers__all-minilm-l6-v2” 从 Hugging Face 加载到 Elasticsearch ML Node 中使用 LangChain 分割器将段落分块成句子&#xff0c;并使用嵌套密集向量将它们索引到 Elasticse…

【RL】Bellman Equation (贝尔曼等式)

Lecture2: Bellman Equation State value 考虑grid-world的单步过程&#xff1a; S t → A t R t 1 , S t 1 S_t \xrightarrow[]{A_t} R_{t 1}, S_{t 1} St​At​ ​Rt1​,St1​ t t t, t 1 t 1 t1&#xff1a;时间戳 S t S_t St​&#xff1a;时间 t t t时所处的sta…

基于蒙特卡洛的电力系统可靠性分析matlab仿真,对比EDNS和LOLP

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 1.课题概述 电力系统可靠性是指电力系统按可接受的质量标准和所需数量不间断地向电力用户供应电力和电能量的能力的量度&#xff0c;包括充裕度和安全性两个方面。发电系统可靠性是指统一并网的全部发电机…

【RT-DETR有效改进】重参数化模块DiverseBranchBlock助力特征提取(附代码 + 修改教程)

&#x1f451;欢迎大家订阅本专栏&#xff0c;一起学习RT-DETR&#x1f451; 一、本文介绍 本文给大家带来的是改进机制是一种替换多元分支模块&#xff08;Diverse Branch Block&#xff09;&#xff0c;Diverse Branch Block (DBB) 是一种用于增强卷积神经网络性能的结构…

Vue前端框架--Vue工程项目问题总结{脚手架 Vue-cli}

Vue脚手架部署问题总结 我所遇到的一共两大问题 只有先执行npm install之后 才能run serve 否则会报错 vue-cli-serve不是内部或者外部的命令&#xff0c;也不是可运行的程序或者批处理文件的错误 1. 运行npm install会报错 2. 运行npm run serve报错 nodejs官网为 https://no…

算法之双指针系列1

目录 一&#xff1a;双指针的介绍 1&#xff1a;快慢指针 2&#xff1a;对撞指针 二&#xff1a;对撞指针例题讲述 一&#xff1a;双指针的介绍 在做题中常用两种指针&#xff0c;分别为对撞指针与快慢指针。 1&#xff1a;快慢指针 简称为龟兔赛跑算法&#xff0c;它的基…

Unity引擎学习笔记之【动画层操作】

动画层Animation Layer 一、动画器的三个基本状态 1. Any State&#xff08;任意状态&#xff09; “Any State”&#xff08;任意状态&#xff09;&#xff1a;这个状态可以用来连接多个状态机的任意状态转换。在动画控制器中&#xff0c;你可以使用“Any State”作为过渡条…

前端架构: 从vue-cli探究脚手架原理

从使用角度理解什么是脚手架 脚手架本质是一个操作系统的客户端 在终端中去执行一个命令&#xff0c;这个命令本身它就是一个客户端我们其实可以把脚手架理解为操作系统的一个客户端通过命令去执行它的时候&#xff0c;这个命令往往是这样的一个构造&#xff0c;如下 比如&…

CTFshow web(php命令执行 37-40)

?ceval($_GET[shy]);&shypassthru(cat flag.php); #逃逸过滤 ?cinclude%09$_GET[shy]?>&shyphp://filter/readconvert.base64-encode/resourceflag.php #文件包含 ?cinclude%0a$_GET[cmd]?>&cmdphp://filter/readconvert.base64-encode/…

npm 下载报错

报错信息 : 证书过期 (CERT_HAS_EXPIRED) D:\Apps\nodejs-v18.16.1\npx.cmd --yes create-next-app"latest" . --ts npm ERR! code CERT_HAS_EXPIRED npm ERR! errno CERT_HAS_EXPIRED npm ERR! request to https://registry.npm.taobao.org/create-next-app failed…

深入探索 Express.js 的高级特性

引言 Express.js 是一个基于 Node.js 平台的 Web 开发框架&#xff0c;旨在提供一种简单、易于使用的方式来创建 Web 应用程序。由于其灵活性和可扩展性&#xff0c;它已经成为了 Node.js 社区最受欢迎的框架之一。在本文中&#xff0c;我们将重点介绍 Express.js 的高级特性&…

157基于matlab的GVF-snake算法能自动收敛到目标区域

基于matlab的GVF-snake算法能自动收敛到目标区域。关键技术GVF snake模型算法matlab源程序&#xff0c;GVF是根据光流场原理,利用变分方法,从图像中得到的一种向量场,该向量场被称为梯度矢量流(GVF)场。 Snake模型称为动态轮廓模型&#xff08;Active Contour Model&#xff0…

【开源】JAVA+Vue+SpringBoot实现房屋出售出租系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 房屋销售模块2.2 房屋出租模块2.3 预定意向模块2.4 交易订单模块 三、系统展示四、核心代码4.1 查询房屋求租单4.2 查询卖家的房屋求购单4.3 出租意向预定4.4 出租单支付4.5 查询买家房屋销售交易单 五、免责说明 一、摘…