視頻縮放及像素格式轉(zhuǎn)換ffmpg(十二)

前言

1、視頻縮放是指將視頻放大或者縮小,放大或者縮小對應(yīng)著不同的縮放算法芳室,每一種算法性能和效果也不一致。視頻縮小也是很常見的需求刹勃,各個點播平臺基本上都會提供不同分辨率(超清1080P堪侯,高清720P,標(biāo)清360P)的視頻資源以適應(yīng)用戶不同網(wǎng)絡(luò)條件的需求荔仁。
2伍宦、視頻像素格式轉(zhuǎn)換芽死;安卓平臺碎片化的特性,安卓手機錄制的視頻可能有多種不同像素格式雹拄,比如NV12,NV21等等收奔,雖然他們都是YUV顏色空間掌呜,但是轉(zhuǎn)換成RGB的方式和方法卻不一致滓玖。

視頻縮放及像素格式轉(zhuǎn)換流程

image.png

視頻縮放及像素格式轉(zhuǎn)換相關(guān)命令行介紹

  • 1、MP4文件中提取視頻并轉(zhuǎn)換為YUV

ffmpeg -i test_1280x720.MP4 -ss 00:00:00 -t 00:00:05 -pixel_format yuv420p -vf scale=640:360 -f rawvideo test_640x360_yuv420p.yuv

備注:pixel_format指定輸出的YUV的格式质蕉。vf scale=640:360表示壓縮視頻濾鏡势篡,最終轉(zhuǎn)換后的視頻分辨率為640x360

  • 2、播放YUV

ffplay -i test.yuv -f rawvideo -pixel_format nv12 -video_size 320x180 -framerate 50

備注:-pixel_format代表yuv的格式模暗,如果不指定默認yuv420p;framerate代表播放時視頻的幀率禁悠,如果不指定默認25

視頻縮放及像素格式轉(zhuǎn)換流程相關(guān)函數(shù)介紹

1、SwsContext

視頻進行縮放和格式轉(zhuǎn)換的上下文

2兑宇、struct SwsContext *sws_getContext(
int srcW, int srcH, enum AVPixelFormat srcFormat,
int dstW, int dstH, enum AVPixelFormat dstFormat,
int flags, SwsFilter *srcFilter,SwsFilter *dstFilter,
const double *param);

創(chuàng)建SwsContext上下文碍侦,并將函數(shù)中參數(shù)賦值到上下文中
1、參數(shù)1 2 3:轉(zhuǎn)換源數(shù)據(jù)寬隶糕,高瓷产,像素格式
2、參數(shù)4 5 6:轉(zhuǎn)換后數(shù)據(jù)寬枚驻,高濒旦,像素格式
3、參數(shù)7:如果進行縮放再登,所采用的的縮放算法
4尔邓、參數(shù)8:轉(zhuǎn)換源數(shù)據(jù)的 SwsFilter 參數(shù)
5、參數(shù)9:轉(zhuǎn)換后數(shù)據(jù)的 SwsFilter 參數(shù)
6锉矢、參數(shù)10:轉(zhuǎn)換縮放算法的相關(guān)參數(shù) const double* 類型數(shù)組
7梯嗽、8 9 10三個參數(shù)一般傳遞NULL,即采用默認值即可
8沽损、SWS_BICUBIC性能比較好慷荔,SWS_FAST_BILINEAR在性能和速度之間有一個比好好的平衡,SWS_POINT的效果比較差缠俺。

struct SwsContext *sws_alloc_context(void)

創(chuàng)建SwsContext上下文显晶,并賦值默認的參數(shù)

av_opt_set_xxx()給SwsContext上下文設(shè)置對應(yīng)的參數(shù)

int sws_init_context(struct SwsContext *sws_context, SwsFilter *srcFilter, SwsFilter *dstFilter);

初始化SwsContext上下文

int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
const int srcStride[], int srcSliceY, int srcSliceH,
uint8_t *const dst[], const int dstStride[]);

進行縮放和格式轉(zhuǎn)換

ffmpeg實現(xiàn)代碼

頭文件

#ifndef videoresample_hpp
#define videoresample_hpp

#include <stdio.h>
#include <string>
#include "cppcommon/CLog.h"

extern "C" {
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
#include <libavutil/opt.h>
}
using namespace std;

typedef enum
{
    FormatTypeYUV420P,
    FormattypeRGB24
}FormatType;

class VideoScale {
public:
    VideoScale();
    ~VideoScale();
    
    /** sws_scale()函數(shù)主要用來進行視頻的縮放和視頻數(shù)據(jù)的格式轉(zhuǎn)換。對于縮放壹士,有不同的算法磷雇,每個算法的性能不一樣
     */
    void doScale();
};


#endif /* videoresample_hpp */

實現(xiàn)文件

VideoScale::VideoScale(){
    
}

VideoScale::~VideoScale()
{
    
}

const char* nameForOptionType(AVOptionType type) {
    const char *names[19] = {
        "AV_OPT_TYPE_FLAGS",
        "AV_OPT_TYPE_INT",
        "AV_OPT_TYPE_INT64",
        "AV_OPT_TYPE_DOUBLE",
        "AV_OPT_TYPE_FLOAT",
        "AV_OPT_TYPE_STRING",
        "AV_OPT_TYPE_RATIONAL",
        "AV_OPT_TYPE_BINARY",
        "AV_OPT_TYPE_DICT",
        "AV_OPT_TYPE_UINT64",
        "AV_OPT_TYPE_CONST",
        "AV_OPT_TYPE_IMAGE_SIZE",
        "AV_OPT_TYPE_PIXEL_FMT",
        "AV_OPT_TYPE_SAMPLE_FMT",
        "AV_OPT_TYPE_VIDEO_RATE",
        "AV_OPT_TYPE_DURATION",
        "AV_OPT_TYPE_COLOR",
        "AV_OPT_TYPE_CHANNEL_LAYOUT",
        "AV_OPT_TYPE_BOOL"
    };
    
    return names[type];
}


/** 視頻像素格式存儲方式
 *  YUV420P:三個plane,按照 YYYY........U........V..........分別存儲于各個plane通道
 *  RGB24:一個plane躏救,按照RGBRGB....順序存儲在一個planne中
 *  NV12:兩個plane唯笙,一個存儲YYYYYY螟蒸,另一個按照UV的順序交叉存儲
 *  NV21:兩個plane,一個存儲YYYYYY崩掘,另一個按照VU的順序交叉存儲
 */
/** 播放yuv的ffplay 命令
 *  ffplay -i  test_320x180_nv12.yuv -f rawvideo --pixel_format nv12 -video_size 320x180 -framerate 50
 */
/** 遇到問題:源MP4文件視頻的幀率為50fps七嫌,解碼成yuv之后用ffplay播放rawvideo速度變慢
 *  原因分析:ffplay播放rawvideo的默認幀率為25fps;如上添加-framerate 50 代表以50的fps播放
 */
void VideoScale::doScale()
{
    string curFile(__FILE__);
    unsigned long pos = curFile.find("1-video_encode_decode");
    if (pos == string::npos) {
        LOGD("1-video_encode_decode file fail");
        return;
    }
    string resourceDir = curFile.substr(0,pos)+"filesources/";
    string srcyuvPath = resourceDir + "test_640x360_yuv420p.yuv";
    string dstyuvPath = "test.yuv";
    enum AVPixelFormat src_pix_fmt = AV_PIX_FMT_YUV420P;
    enum AVPixelFormat dst_pix_fmt = AV_PIX_FMT_NV12;
    int src_w = 640,src_h = 360;
    int dst_w = 320,dst_h = 180;
    
    /** 1、因為視頻的planner個數(shù)最多不超過4個苞慢,所以這里的src_data數(shù)組和linesize數(shù)組長度都是4
     *  2诵原、linesize數(shù)組存儲的是各個planner的寬,對應(yīng)"視頻的寬"挽放,由于字節(jié)對齊可能會大于等于對應(yīng)"視頻的寬"
    */
    uint8_t *src_buffer[4],*dst_buffer[4];
    int     src_linesize[4],dst_linesize[4];
    const AVPixFmtDescriptor *pixfmtDest = av_pix_fmt_desc_get(src_pix_fmt);
    
    // 一绍赛、獲取視頻轉(zhuǎn)換上下文
    /**
     * 方式一:
     *  1、參數(shù)1 2 3:轉(zhuǎn)換源數(shù)據(jù)寬辑畦,高吗蚌,像素格式
     *  2、參數(shù)4 5 6:轉(zhuǎn)換后數(shù)據(jù)寬纯出,高蚯妇,像素格式
     *  3、參數(shù)7:如果進行縮放暂筝,所采用的的縮放算法
     *  4箩言、參數(shù)8:轉(zhuǎn)換源數(shù)據(jù)的 SwsFilter 參數(shù)
     *  5、參數(shù)9:轉(zhuǎn)換后數(shù)據(jù)的 SwsFilter 參數(shù)
     *  6乖杠、參數(shù)10:轉(zhuǎn)換縮放算法的相關(guān)參數(shù) const double* 類型數(shù)組
     *  7分扎、8 9 10三個參數(shù)一般傳遞NULL,即采用默認值即可
     *  8胧洒、SWS_BICUBIC性能比較好畏吓,SWS_FAST_BILINEAR在性能和速度之間有一個比好好的平衡,SWS_POINT的效果比較差卫漫。
     */
    SwsContext *swsCtx = NULL;
#if 1
    swsCtx = sws_getContext(src_w,src_h,src_pix_fmt,
                                dst_w,dst_h,dst_pix_fmt,
                                SWS_BICUBIC,
                                NULL,NULL,
                                NULL
                                );
    if (swsCtx == NULL) {
        LOGD("sws_getContext fail");
        return;
    }
#else
    // 方式二菲饼;此方式可以設(shè)置更多的轉(zhuǎn)換屬性,比如設(shè)置YUV的顏色取值范圍是MPEG還是JPEG
    swsCtx = sws_alloc_context();
//    const AVOption *opt = NULL;
//    LOGD("輸出 AVOption的所有值===>");
    /** 基于AVClass的結(jié)構(gòu)體列赎。
     *  1宏悦、所有基于AVClass的結(jié)構(gòu)體都可以用av_opt_set_xxx()函數(shù)和av_opt_get_xxx()函數(shù)來設(shè)置和獲取結(jié)構(gòu)體對應(yīng)的屬性,注意:AVClass
     *  必須是該結(jié)構(gòu)體的第一個屬性包吝。這個結(jié)構(gòu)體的每一個屬性是AVOption結(jié)構(gòu)體饼煞,包含了屬性名,屬性值等等诗越。存儲于AVClass的
     *  const struct AVOption *option中
     *  2砖瞧、ffmpeg的常用結(jié)構(gòu)體都是基于AVClass的,比如AVCodec嚷狞,SwsContext等等块促。這些結(jié)構(gòu)體對應(yīng)屬性的屬性名在各個目錄的
     *  options.c中荣堰,比如SwsContext的就在libswscale目錄下的options.c文件中定義,并且有默認值
     *  3竭翠、av_opt_next()可以遍歷基于AVClass的結(jié)構(gòu)體的屬性對應(yīng)的AVOption結(jié)構(gòu)體
     */
//    while ((opt = av_opt_next(swsCtx, opt)) != NULL) {
//        LOGD("name:%s help:%s type %s",opt->name,opt->help,nameForOptionType(opt->type));
//    }
    
    /** 遇到問題:視頻進行壓縮后變形
     *  原因分析:因為下面dstw的參數(shù)寫成了src_w導(dǎo)致變形振坚,設(shè)置爭取即可
     */
    av_opt_set_pixel_fmt(swsCtx,"src_format",src_pix_fmt,AV_OPT_SEARCH_CHILDREN);
    av_opt_set_int(swsCtx, "srcw", src_w, AV_OPT_SEARCH_CHILDREN);
    av_opt_set_int(swsCtx,"srch",src_h,AV_OPT_SEARCH_CHILDREN);
    /** 設(shè)置源的AVColorRange
     *  AVCOL_RANGE_JPEG:以JPEG為代表的標(biāo)準(zhǔn)中,Y斋扰、U渡八、V的取值范圍都是0-255。FFmpeg中稱之為“jpeg” 范圍褥实。
     *  AVCOL_RANGE_MPEG:一般用于以Rec.601為代表(還包括BT.709 / BT.2020)的廣播電視標(biāo)準(zhǔn)Y的取值范圍是16-235呀狼,
     *  U裂允、V的取值范圍是16-240损离。FFmpeg中稱之為“mpeg”范圍。
     *  默認是MPEG范圍绝编,對于源來說 要與實際情況一致僻澎,這里源的實際范圍是MPEG范圍
     */
    av_opt_set_int(swsCtx,"src_range",0,AV_OPT_SEARCH_CHILDREN);
    
    av_opt_set_pixel_fmt(swsCtx,"dst_format",dst_pix_fmt,AV_OPT_SEARCH_CHILDREN);
    av_opt_set_int(swsCtx,"dstw",dst_w,AV_OPT_SEARCH_CHILDREN);
    av_opt_set_int(swsCtx,"dsth",dst_h,AV_OPT_SEARCH_CHILDREN);
    av_opt_set_int(swsCtx,"dst_range",1,AV_OPT_SEARCH_CHILDREN);
    av_opt_set_int(swsCtx,"sws_flags",SWS_BICUBIC,AV_OPT_SEARCH_CHILDREN);
    if (sws_init_context(swsCtx, NULL, NULL) < 0) {
        LOGD("sws_init_context fail");
        sws_freeContext(swsCtx);
        return;
    }
#endif
    
    LOGD("src_fmt %s dst_fmt %s",av_get_pix_fmt_name(src_pix_fmt),av_get_pix_fmt_name(dst_pix_fmt));
    
    // 二、分配內(nèi)存 轉(zhuǎn)換前和轉(zhuǎn)換后的
    /** av_image_alloc()返回分配內(nèi)存的大小
    *  1十饥、該大小大于等于視頻數(shù)據(jù)實際占用的大小窟勃,當(dāng)最后一個參數(shù)為1時或者視頻長寬剛好滿足字節(jié)對齊時
    * 兩者會相等。
    *  2逗堵、linesize對應(yīng)視頻的寬秉氧,它的值會大于等于視頻的寬,在將視頻數(shù)據(jù)讀入這個內(nèi)存或者從這個內(nèi)存取出數(shù)據(jù)時都要用實際的寬的值
    *  3蜒秤、該函數(shù)分配的內(nèi)存實際上一塊連續(xù)的內(nèi)存汁咏,只不過src_data數(shù)組的指針分別指向了這塊連續(xù)內(nèi)存上對應(yīng)各個planner
    *  上的首地址
    */
    int src_size = av_image_alloc(src_buffer,src_linesize,src_w,src_h,src_pix_fmt,16);
    LOGD("size %d linesize[0] %d linesize[1] %d linesize[2] %d log2_chroma_h %d",src_size,src_linesize[0],src_linesize[1],src_linesize[2],pixfmtDest->log2_chroma_h);
    if (src_size < 0) {
        LOGD("src image_alloc <0 %d",src_size);
        return;
    }
    int dst_size = av_image_alloc(dst_buffer,dst_linesize,dst_w,dst_h,dst_pix_fmt,16);
    if (dst_size < 0) {
        LOGD("dst_size < 0 %d",dst_size);
        return;
    }
    
    // 三、進行轉(zhuǎn)換
    FILE *srcFile = fopen(srcyuvPath.c_str(), "rb");
    if (srcFile == NULL) {
        LOGD("srcFile is NULL");
        return;
    }
    FILE *dstFile = fopen(dstyuvPath.c_str(), "wb+");
    if (dstFile == NULL) {
        LOGD("dstFile is NULL");
        return;
    }
    
    while (true) {
        
        // 讀取YUV420P的方式;注意這里用的是src_w而非linesize中的值作媚;yuv中uv的寬高為實際寬高的一半
        if (src_pix_fmt == AV_PIX_FMT_YUV420P) {
            size_t size_y = fread(src_buffer[0],1,src_w*src_h,srcFile);
            size_t size_u = fread(src_buffer[1],1,src_w/2*src_h/2,srcFile);
            size_t size_v = fread(src_buffer[2],1,src_w/2*src_h/2,srcFile);
            if (size_y <= 0 || size_u <= 0 || size_v <= 0) {
                LOGD("size_y %d size_u %d size_v %d",size_y,size_u,size_v);
                break;
            }
        } else if (src_pix_fmt == AV_PIX_FMT_RGB24) {
            size_t size = fread(src_buffer[0],1,src_w*src_h*3,srcFile);
            if (size <= 0) {
                LOGD("size %d",size);
                break;
            }
        } else if (src_pix_fmt == AV_PIX_FMT_NV12) {
            size_t size_y = fread(src_buffer[0],1,src_w * src_h,srcFile);
            size_t size_uv = fread(src_buffer[1],1,src_w * src_h/2,srcFile);
            if (size_y <= 0 || size_uv <= 0) {
                LOGD("size_y %d size_uv %d",size_y,size_uv);
                break;
            }
        } else if (src_pix_fmt == AV_PIX_FMT_NV21) {
            size_t size_y = fread(src_buffer[0],1,src_w * src_h,srcFile);
            size_t size_vu = fread(src_buffer[1],1,src_w * src_h/2,srcFile);
            if (size_y <= 0 || size_vu <= 0) {
                LOGD("size_y %d size_vu %d",size_y,size_vu);
                break;
            }
        }
        
        /** 參數(shù)1:轉(zhuǎn)換上下文
        *  參數(shù)2:轉(zhuǎn)換源數(shù)據(jù)內(nèi)存地址
        *  參數(shù)3:轉(zhuǎn)換源的linesize數(shù)組
        *  參數(shù)4:從源數(shù)據(jù)的撒位置起開始處理數(shù)據(jù)攘滩,一般是處理整個數(shù)據(jù),傳0
        *  參數(shù)5:換換源數(shù)據(jù)的height(視頻的高)
        *  參數(shù)6:轉(zhuǎn)換目的數(shù)據(jù)內(nèi)存地址
        *  參數(shù)7:轉(zhuǎn)換目的linesize數(shù)組
        *  返回:轉(zhuǎn)換后的目的數(shù)據(jù)的height(轉(zhuǎn)換后的視頻的實際高)
        */
        int size = sws_scale(swsCtx,src_buffer,src_linesize,0,src_h,
                  dst_buffer,dst_linesize);
        LOGD("size == %d",size);
        
        if (dst_pix_fmt == AV_PIX_FMT_YUV420P) {
            fwrite(dst_buffer[0],1,dst_w*dst_h,dstFile);
            fwrite(dst_buffer[1],1,dst_w/2*dst_h/2,dstFile);
            fwrite(dst_buffer[2],1,dst_w/2*dst_h/2,dstFile);
        } else if (dst_pix_fmt == AV_PIX_FMT_RGB24) {
            fwrite(dst_buffer[0],1,dst_w*dst_h*3,dstFile);
        } else if (dst_pix_fmt == AV_PIX_FMT_NV12) {
            fwrite(dst_buffer[0],1,dst_w*dst_h,dstFile);
            fwrite(dst_buffer[1],1,dst_w*dst_h/2,dstFile);
        } else if (dst_pix_fmt == AV_PIX_FMT_NV21) {
           fwrite(dst_buffer[0],1,dst_w*dst_h,dstFile);
           fwrite(dst_buffer[1],1,dst_w*dst_h/2,dstFile);
        }
    }
    
    
    sws_freeContext(swsCtx);
    fclose(srcFile);
    fclose(dstFile);
    av_freep(&src_buffer[0]);
    av_freep(&dst_buffer[0]);
}

項目代碼:
示例地址

示例代碼位于cppsrc目錄下文件
videoresample.hpp
videoresample.cpp

項目下示例可運行于iOS/android/mac平臺纸泡,工程分別位于demo-ios/demo-android/demo-mac三個目錄下漂问,可根據(jù)需要選擇不同平臺

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市女揭,隨后出現(xiàn)的幾起案子蚤假,更是在濱河造成了極大的恐慌,老刑警劉巖吧兔,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件磷仰,死亡現(xiàn)場離奇詭異,居然都是意外死亡掩驱,警方通過查閱死者的電腦和手機芒划,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進店門冬竟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人民逼,你說我怎么就攤上這事泵殴。” “怎么了拼苍?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵笑诅,是天一觀的道長。 經(jīng)常有香客問我疮鲫,道長吆你,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任俊犯,我火速辦了婚禮妇多,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘燕侠。我一直安慰自己者祖,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布绢彤。 她就那樣靜靜地躺著七问,像睡著了一般。 火紅的嫁衣襯著肌膚如雪茫舶。 梳的紋絲不亂的頭發(fā)上械巡,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天,我揣著相機與錄音饶氏,去河邊找鬼讥耗。 笑死,一個胖子當(dāng)著我的面吹牛嚷往,可吹牛的內(nèi)容都是我干的葛账。 我是一名探鬼主播,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼皮仁,長吁一口氣:“原來是場噩夢啊……” “哼籍琳!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起贷祈,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤趋急,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后势誊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體呜达,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年粟耻,在試婚紗的時候發(fā)現(xiàn)自己被綠了查近。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片眉踱。...
    茶點故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖霜威,靈堂內(nèi)的尸體忽然破棺而出谈喳,到底是詐尸還是另有隱情,我是刑警寧澤戈泼,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布婿禽,位于F島的核電站,受9級特大地震影響大猛,放射性物質(zhì)發(fā)生泄漏扭倾。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一挽绩、第九天 我趴在偏房一處隱蔽的房頂上張望膛壹。 院中可真熱鬧,春花似錦琼牧、人聲如沸恢筝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至此改,卻和暖如春趾撵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背共啃。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工占调, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人移剪。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓究珊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親纵苛。 傳聞我的和親對象是個殘疾皇子剿涮,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,864評論 2 354