数据专栏

智能大数据搬运工,你想要的我们都有

科技资讯:

科技学院:

科技百科:

科技书籍:

网站大全:

软件大全:

「深度学习福利」大神带你进阶工程师,立即查看>>>
参考: https://blog.csdn.net/elf001/article/details/8968442
1、自己实现一个类Scene类继承QGraphicsScene,定义信号和事件。当鼠标按下事件发生时,发射信号message:添加C class文件
1.1、Scene.h中: #ifndef SCENE_H #define SCENE_H class QGraphicsSceneMouseEvent; #include class Scene : public QGraphicsScene { Q_OBJECT public: Scene( ); // constructor signals: void message( QString ); // 文本消息信号 protected: void mousePressEvent( QGraphicsSceneMouseEvent* ); // 接收鼠标按下事件 }; #endif // SCENE_H
1.2、Scene.cpp中: #include "scene.h" #include Scene::Scene( ) : QGraphicsScene() { } /********************************** mousePressEvent **********************************/ void Scene::mousePressEvent( QGraphicsSceneMouseEvent* event ) { // 判断用户是否按下鼠标左键 if ( event->button() != Qt::LeftButton ) return; // 发送信息信号 qreal x = event->scenePos().x(); qreal y = event->scenePos().y(); emit message( QString("Clicked at %1,%2").arg(x).arg(y) ); }
2、在GUI中mainwindows中
2.1、mainwindow.h中 #ifndef MAINWINDOW_H #define MAINWINDOW_H #include #include "scene.h" class Scene; class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = 0); ~MainWindow(); private: Scene* m_scene; public slots: //添加槽方法的定义 void showMessage( QString ); // 在状态栏上显示消息 }; #endif // MAINWINDOW_H
2.2、mainwhidow.cpp中 1、实例❀一个Scene,给mainwindow创建一个场景和视图:将视图放入场景,编写槽函数 #include "mainwindow.h" #include #include #include MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { menuBar()->addMenu( "&File" ); menuBar()->addMenu( "&Edit" ); menuBar()->addMenu( "&View" ); menuBar()->addMenu( "&Simulate" ); menuBar()->addMenu( "&Help" ); statusBar()->showMessage("QSimulate has started"); // 创建场景和显示场景的中央视图部件 m_scene = new Scene(); QGraphicsView* view = new QGraphicsView( m_scene ); view->setAlignment( Qt::AlignLeft | Qt::AlignTop ); view->setFrameStyle( 0 ); setCentralWidget( view ); //将信号与槽关联:m_scene发射message信号时,MainWindow接收信号,执行showMessage槽函数 connect( m_scene, SIGNAL(message(QString)), this, SLOT(showMessage(QString)) ); } MainWindow::~MainWindow() { } void MainWindow::showMessage( QString msg ) { statusBar()->showMessage( msg ); // 在主窗口状态栏上显示消息 }
2.3、main.cpp中 #include "mainwindow.h" #include int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); return a.exec(); }
多媒体
2018-09-04 08:46:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
重写画框函数时:必须用事件过滤器在label上重写而不是直接重写mainwindws的鼠标事件,否则得到的是相对于windows的坐标,而不是相对于label的坐标,导致画框函数的坐标不对劲
多媒体
2018-08-31 09:52:00
「深度学习福利」大神带你进阶工程师,立即查看>>> this->setCentralWidget(ui->scrollArea); //设置中心布局组件 //在UI设计中添加QLabel,改变QLabel的底色 QPalette palette; palette.setColor(QPalette::Background, QColor(255, 255, 255)); ui->label->setAutoFillBackground(true); ui->label->setPalette(palette); ui->label->resize(labelrx, labelry);


-
多媒体
2018-08-31 09:18:00
「深度学习福利」大神带你进阶工程师,立即查看>>>

第一:对目标对象调用installEventFilter()来注册监视对象(事件过滤器);
第二:重写监视对象的eventFilter()函数处理目标对象的事件。

1、在mainwindows中添加: public: bool eventFilter(QObject *watched, QEvent *event); private: bool mouseIsdown; QPointF startPos; QPointF endPos;
2、构造函数中安装事件过滤器: mouseIsdown = false; ui -> label ->installEventFilter( this );
3、在 mainwindows.cpp中添加 #include #include bool MainWindow::eventFilter(QObject *watched, QEvent *event) { if(watched == ui->label ) { if(event->type() == QEvent::MouseButtonPress) { QMouseEvent *mouseEvent = (QMouseEvent *)event; if(mouseEvent->buttons()&Qt::LeftButton) { mouseIsdown = true; startPos = mouseEvent->pos(); } } if(event->type() == QEvent::MouseMove) { QMouseEvent *mouseEvent=(QMouseEvent *)event; if(mouseIsdown) { endPos = mouseEvent->pos(); ui->label->update(); } } if(event->type() == QEvent::MouseButtonRelease) { QMouseEvent *mouseEvent=(QMouseEvent *)event; endPos = mouseEvent->pos(); mouseIsdown = false; } if(event->type() == QEvent::Paint) { magicTime(); QPainter painter(ui->label); QPen pen = painter.pen(); pen.setWidth(3); pen.setColor(QColor(173, 255, 47)); painter.setRenderHint(QPainter::Antialiasing, true); painter.setPen(pen); QRect rect(startPos.x(),startPos.y(),endPos.x(), endPos.y()); painter.drawRect(rect); } } return QMainWindow::eventFilter(watched, event); //将事件传递给父类 }
参考: https://blog.csdn.net/qq_31607215/article/details/53964594
多媒体
2018-08-31 09:07:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
取决于通道数
1、灰度图像

2、多通道:矩阵的类会包含多个子阵:比如RGB[注意opencv中子列的通道顺序是反过来的是BGR而不是RGB]
3、如果内存足够大,可以实现连续存储,因此,图像中的各行就能一行一行的连接起来,形成一个长行。连续存储有助于提升扫描速度,可以用isContinuous()判断是不是连续存储









多媒体
2018-08-30 18:15:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
1、imwrite
imwrite函数用于将图像保存到指定的文件。
CV_EXPORTS_W bool imwrite( const String& filename, InputArray img, const std::vector& params = std::vector());
1、filename保存路径
2、源图像
3、第三个参数:const std::vector&类型的params,表示为特定格式保存的参数编码,它有默认值std::vector(),所以一般情况下不需要填写。如果更改的话,对于不同的图片格式,其对应的值不同功能不同,如下:
对于JPEG格式的图片,这个参数表示从0-100的图片质量(CV_IMWRITE_JPEG_QUALITY),默认值是95.
对于PNG格式的图片,这个参数表示压缩级别(CV_IMWRITE_PNG_COMPRESSION)从0-9.较高的值意味着更小的尺寸和更长的压缩时间而默认值是3.
对于PPM,PGM或PBM格式的图片,这个参数表示一个二进制格式标志(CV_IMWRITE_PXM_BINARY),取值为0或1,而默认值为1.
2、Vec4b

typedef Vec Vec2b;
Vec2b—表示每个Vec2b对象中,可以存储2个char(字符型)数据
Vec3b—表示每一个Vec3b对象中,可以存储3个char(字符型)数据,比如可以用这样的对象,去存储RGB图像中的
Vec4b—表示每一个Vec4b对象中,可以存储4个字符型数据,可以用这样的类对象去存储—4通道RGB+Alpha的图
3、saturate_cast
就是防止数据溢出,具体的原理可以大致描述如下:
if(data<0)
data=0;
if(data>255)
data=255
实例:
pResultData[j]= cv::saturate_cast(255 - pSrcData[j]);

4、 例子
#include "mainwindow.h" #include #include #include using namespace cv; using namespace std; void createAlphaMat(Mat &mat) { for(int i = 0; i < mat.rows; ++i) { for(int j = 0; j < mat.cols; ++j) { Vec4b &rgba = mat.at(i, j); //每一个Vec4b对象中,可以存储4个字符型数据,可以用这样的类对象去存储—4通道RGB+Alpha的图 rgba[0] = UCHAR_MAX; rgba[1] = saturate_cast((float (mat.cols - j))/ ((float)mat.cols) * UCHAR_MAX); rgba[2] = saturate_cast((float (mat.rows - j))/ ((float)mat.rows) * UCHAR_MAX); rgba[3] = saturate_cast(0.5 * (rgba[1] + rgba[2])); } } } int main(int argc, char *argv[]) { QApplication a(argc, argv); //创建带Alpha通道的Mat Mat mat(480, 640, CV_8UC4); createAlphaMat(mat); vectorcompression_params; compression_params.push_back(IMWRITE_PNG_COMPRESSION); compression_params.push_back(9); try{ imwrite("Alpha.png", mat, compression_params); imshow("created png", mat); fprintf(stdout, "PNG图片文件的alpha数据保存完毕!可以在工程目录下查看\n"); waitKey(0); }catch(runtime_error & ex){ fprintf(stderr, "图像转换成gng格式发生错误", ex.what()); return 1; } return a.exec(); }

多媒体
2018-08-30 17:00:00
「深度学习福利」大神带你进阶工程师,立即查看>>> #include "mainwindow.h" #include #include #include #include using namespace cv; #define WINDOW_NAME "[图像]" #define FuSi_NAME "[腐蚀]" int main(int argc, char *argv[]) { QApplication a(argc, argv); system("chcp 65001"); String imgpath = "F:\\MyCode\\dcq.jpg"; Mat srcImage = imread(imgpath, 1); // Mat tmpImage = srcImage; if(!srcImage.data){printf("读取失败");return -1;} namedWindow(WINDOW_NAME, WINDOW_AUTOSIZE); imshow(WINDOW_NAME, srcImage); Mat element = getStructuringElement(MORPH_RECT, Size(15, 15)); Mat dstImage; erode(srcImage, dstImage, element); //腐蚀 namedWindow(FuSi_NAME, WINDOW_AUTOSIZE); imshow(FuSi_NAME, dstImage); waitKey(0); return a.exec(); }
多媒体
2018-08-30 15:37:00
「深度学习福利」大神带你进阶工程师,立即查看>>> #include "mainwindow.h" #include #include #include #include using namespace cv; using namespace std; #define WINDOW_NAME "[图像]" int main(int argc, char *argv[]) { QApplication a(argc, argv); system("chcp 65001"); String imgpath = "F:\\MyCode\\dcq.jpg"; Mat srcImage = imread(imgpath, 1); // Mat tmpImage = srcImage; if(!srcImage.data){printf("读取失败");return -1;} resize(srcImage, srcImage, Size(250, 125)); namedWindow(WINDOW_NAME, WINDOW_AUTOSIZE); imshow(WINDOW_NAME, srcImage); int key = 0; Mat dstImage1 ; while(1) { key = waitKey(9); //读取键值到key种 switch(key) { case 27: //ESC return 0; break; case 'q': //键值Q return 0; break; case 'a': pyrUp(srcImage, dstImage1, Size(srcImage.cols*2, srcImage.rows*2)); printf("pyrUp图像尺寸*2"); imshow("pyrUp", dstImage1); break; case 'w': resize(srcImage, dstImage1, Size(srcImage.cols*2, srcImage.rows*2), INTER_LINEAR); printf("pyrUp图像尺寸*2"); imshow("pyrUp", dstImage1); break; case '1': resize(dstImage1, dstImage1, Size(dstImage1.cols*1.2, dstImage1.rows*1.2), INTER_LINEAR); printf("pyrUp图像尺寸*2"); imshow("resize", dstImage1); break; case 'd': resize(srcImage, dstImage1, Size(dstImage1.cols/2, dstImage1.rows/2)); imshow("sXresize", dstImage1); break; case 's': pyrDown(dstImage1, dstImage1, Size(dstImage1.cols/2, dstImage1.rows/2)); imshow("sXresize", dstImage1); break; } } pyrDown(srcImage, dstImage1, Size(srcImage.cols/2, srcImage.rows/2)); imshow("pyrDown", dstImage1); return a.exec(); }
多媒体
2018-08-30 15:25:00
「深度学习福利」大神带你进阶工程师,立即查看>>> INCLUDEPATH += E:/software/opencv-3.4.2/build/install/include/opencv \ E:/software/opencv-3.4.2/build/install/include/opencv2 \ E:/software/opencv-3.4.2/build/install/include LIBS += -L E:/software/opencv-3.4.2/build/install/x86/mingw/bin/libopencv_*.dll #include "mainwindow.h" #include #include #include using namespace cv; int main(int argc, char *argv[]) { QApplication a(argc, argv); Mat img = imread("F:\\MyCode\\dcq.jpg", CV_LOAD_IMAGE_UNCHANGED); if (img.empty()) { //cout << "图像加载失败!" << endl; return -1; } Mat dstImage, GryImage; cvtColor(img, dstImage, COLOR_BGR2Lab); cvtColor(img, GryImage, COLOR_BGR2GRAY); imshow("COLOR_BGR2Lab", dstImage); imshow("COLOR_BGR2GRAY", GryImage); imshow("MyWindow", img);//在MyWindow的窗中中显示存储在img中的图片 waitKey(0); //等待直到有键按下 return a.exec(); }
多媒体
2018-08-30 12:31:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
Point_ <--> Point2f
Point_ <--> Point2i <--> Point
Point表示二维坐标系下的点 #include #include using namespace cv; using namespace std; int main() { system("chcp 65001"); //控制台中文支持 Point2i point; point.x = 10; point.y = 8; Point point2 = Point(10, 8); return 0; }
多媒体
2018-08-30 11:35:00
「深度学习福利」大神带你进阶工程师,立即查看>>> #include #include using namespace cv; using namespace std; int main() { system("chcp 65001"); //控制台中文支持 Point2f p(6, 2); cout << "[二维点] p = " << p << ";\n" << endl; //[6, 2] Point3f p3f(6, 2, 0); cout << "[三维点] p3f = " << p3f << ";\n" << endl; vectorv{3, 5, 7}; cout << "[给予Mat的vector] shortv = " << Mat(v) << ";\n" << endl; // vectorpoints(20); //failing // for(size_t i = 0; i < points.size(); ++i) // points[i] = Point2f((float)(i*5), (float)(i % 7)); // cout << "二维点向量: points = " <
多媒体
2018-08-30 11:28:00
「深度学习福利」大神带你进阶工程师,立即查看>>> #include #include using namespace cv; using namespace std; int main() { system("chcp 65001"); //控制台中文支持 Mat r = Mat(10, 3, CV_8UC3); randu(r, Scalar::all(0), Scalar::all(255)); //randu随机矩阵[0, 255]之间 cout << "r(opencv默认风格) = " << r << ";" << endl << endl; // cout << "r(Python风格_openCV2) = " << format(r, "python") << ";" << endl << endl; cout << "r(Python风格_openCV3) = " << format(r, Formatter::FMT_PYTHON) << ";" << endl << endl; //cout << "r(,分隔风格_openCV2) = " << format(r, "csv") << ";" << endl << endl; cout << "r(,分隔风格_openCV3) = " << format(r, Formatter::FMT_CSV) << ";" << endl << endl; //cout << "r(numpy风格_openCV2) = " << format(r, "numpy") << ";" << endl << endl; cout << "r(numpy风格_openCV3) = " << format(r, Formatter::FMT_NUMPY) << ";" << endl << endl; // cout << "r(C语言风格_openCV2) = " << format(r, "c") << ";" << endl << endl; cout << "r(C语言风格_openCV3) = " << format(r, Formatter::FMT_C) << ";" << endl << endl; return 0; }

0、准备 system("chcp 65001"); //控制台中文支持 Mat r = Mat(10, 3, CV_8UC3); randu(r, Scalar::all(0), Scalar::all(255)); //randu随机矩阵[0, 255]之间
1、默认风格 cout << "r(opencv默认风格) = " << r << ";" << endl << endl;
2、 cout << "r(Python风格_openCV2) = " << format(r, "python") << ";" << endl << endl; cout << "r(Python风格_openCV3) = " << format(r, Formatter::FMT_PYTHON) << ";" << endl << endl;
3、 cout << "r(,分隔风格_openCV2) = " << format(r, "csv") << ";" << endl << endl; cout << "r(,分隔风格_openCV3) = " << format(r, Formatter::FMT_CSV) << ";" << endl << endl;
4、 cout << "r(numpy风格_openCV2) = " << format(r, "numpy") << ";" << endl << endl; cout << "r(numpy风格_openCV3) = " << format(r, Formatter::FMT_NUMPY) << ";" << endl << endl;
5、 // cout << "r(C语言风格_openCV2) = " << format(r, "c") << ";" << endl << endl; cout << "r(C语言风格_openCV3) = " << format(r, Formatter::FMT_C) << ";" << endl << endl;

-----
多媒体
2018-08-30 11:14:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
1、使用Mat()构造函数
定义一个二维多通道图像 Mat M(2, 2, CV_8UC3, Scalar(0, 0, 255)); cout << "M = " << endl << " " << M << endl; /* *2行2列 * CV_8UC3 :使用8为unsigned char型,每个像素由3个元素组成 * Scalar:short类型向量: 使用指定的定制化来初始化矩阵 */
2、IpIImage* 转换为Mat对象 IplImage *img = cvLoadImage("F:\\MyCode\\dcq.jpg", 1); Mat mtx = cvarrToMat(img, true); //转换IplImage * --->Mat
3、用 Mat类的Create成员函数 Mat M; M.create(4, 4, CV_8UC(3)); cout << "M = " << endl << " " << M << endl;
注意:此函数不能为矩阵设置初值,只是在改变尺寸时重新为矩阵数据开辟内存而已
4、用Matlab式的初始化方式 Mat M = Mat::eye(4, 4, CV_64F); cout << "M = " << endl << " " << M << endl; Mat Z = Mat::zeros(3, 3, CV_64F); cout << "Z = " << endl << " " << Z << endl; Mat F = Mat::ones(2, 2, CV_64F); cout << "F = " << endl << " " << F << endl;
5、 Mat M = (Mat_(3, 3) << 0, 1, 2, 1, 2, 3, 2, 3, 4) ; cout << "M = " << endl << " " << M << endl;
6、为一个已经存在的Mat对象创建一个新的信息头 Mat M = (Mat_(3, 3) << 0, 1, 2, 1, 2, 3, 2, 3, 4) ; cout << "M = " << endl << " " << M << endl << endl << endl; Mat RowClone = M.row(1).clone(); cout << "RowClone=" << endl << " " <--
多媒体
2018-08-30 10:55:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
编者注
由于没有美术,对资产进行减少面数,导致在Unity中运行非常卡顿。于2017年听闻微软推出自动化减面工具,能够适应各个平台。则对改该内容进行调研。
Simplygon Cloud
Simplygon 官网
从官网并没有什么有用信息,SDK倒是可以从官网下载。
根据 微软宣布Simplygon Cloud:自动优化3D内容渲染 的信息,了解到该服务以Azure Marketplace的方式上线。则找到如下内容:
Simplygon Cloud - Microsoft
Overview
Simplygon技术通过REST API的方式访问,用于3D资产优化流程。
Simplygon 在游戏行业的实时渲染的3D内容优化已经使用十年之久。Simplygon现在可以自动优化您的资产,以覆盖多种混合现实的设备与平台。优化后的资产能够适用于HoloLens与WindowsMR,IOS的ARKit,Android的ARCore和浏览器的WebGL。借助Simplygon Cloud能够简单的优化资产构建云端3D流程。
可以按需启动和关闭虚拟机。在几个小时内,能够按照3.6$/小时的费用优化上千资产。
Simplygon通过Cloud SDK和REST API支持自定义流程。
Document
本文档描述
多媒体
2018-08-28 15:13:00
「深度学习福利」大神带你进阶工程师,立即查看>>> 6月24日,又拍云OpenTalk |2018音视频技术沙龙·上海站顺利落幕,这是又拍云OpenTalk | 2018音视频技术沙龙系列活动的第二站。作为又拍云技术分享的看家活动,本次OpenTalk邀请了网易云、谷人云、又拍云、战旗等四家公司的讲师。四位讲师在活动中拿出了看家本领,为到场、观看直播的观众贡献了精彩的分享!
战旗直播高级流媒体研发工程师石硕,在现场分享了《视频直播的用户体验体系与质量监控方案》,重点介绍了直播质量监控方案的搭建,以及直播卡顿、延时监控、首屏秒开三个方面的优化。 直播质量评价体系 直播质量监控方案的结构和逻辑 卡顿优化的十条法则 延时监控:自定义扩展、数字水印 首屏秒开的三个优化方向
以下是石硕分享内容的整理:
大家好,我是来自战旗直播平台的高级流媒体工程师石硕。今天我主要讲两个内容:第一方面,直播、点播的整体用户体验体系。现有公开场合讲整体用户体验体系的内容偏少一点。另一方面,“质量监控方案”,这也很少有提及的。
从本质上来讲,用户体验和质量监控,做的是一件事情,即:在一定程度上保证用户观看直播的体验是最好的。
我的分享内容主要分为四个部分: 直播质量评价。用户体系覆盖了哪些方面,用户整体的体验由几部分组成的。 卡顿监控与优化。卡顿优化依赖于监控体系如何发现卡顿现象。 延时监控的难题。视频延时监控存在难题,因为现在业界对于“延时”的监控还是比较欠缺,商用方案里面还没有看到比较切实有效的办法。 首屏优化要点。首屏优化在行业里讲得比较多,我简单罗列一下要点。
直播质量评价体系
直播质量评价这一块,先讲一下音视频的质量评价体系。
音视频评价起源比较早。早在1996年,ITU国际组织就已经有了主观评价流媒体音视频的传输质量,当时主要评测电话的通话质量。然后在2003年根据个人主观评价提出了一套MOS体系,2012年、2013年对MOS体系进行了不同方面的补充,推出了vMos体系。
今天我主要讲华为的U-vMOS主观质量评价体系。一方面,对整套体系,国内中文的资料比较充实;另一方面,U-vMOS在vMOS的基础上面做扩展的,U-vMOS的整个质量体系,也是在vMOS内容里面的。
MOS质量评价的主要目的,是根据用户的主观体验来对音频或者是视频质量进行评分。它的分值,常规意义上是分为五分,分值越高它的质量就越好。
MOS质量评价体系针对音频质量。视频质量的评价可以在这个基础上做一个延伸,具体看一下U-vMOS质量评价体系。
U-vMOS将视频质量评价分为三个部分: 视频质量,指视频的分辨率、帧率、码率、编码级别; 互动体验,主要指被叫视频载入时间的长短; 观看体验,主要指画面的花屏和卡顿。
针对视频质量的各种相关因素,其中我们能取到的“典型分值”的“分”,主要是播放设备的屏幕尺寸及视频的分辨率,这两个相关度是比较大。
视频质量的评分
4分以上可以算是比较好的观看体验,看这张表格,4.5寸、5.5寸的手机屏幕,都至少需要有720P的视频流才能达到4分以上。那我们做手机的视频服务时,如果用户对于视频的要求不是特别苛刻,通常情况来讲720P足够了;个别提供1080P,其实对观看体验并没有特别大的提升,仅从4.3分提升到4.6分,这个过程不光对码率有要求,视频帧率、解码难度都会高出很多。
一般电视端想要达到4.0分以上的观看体验的话,需要1080P的视频。这个表格对直播企业的对于视频流分辨率的选择是具有一定参考意义的。
视频体验,首屏秒开的标准
互动体验,主要是涉及到常规意义上所讲的“首屏秒开”。首屏秒开通常被认为在100毫秒以内才算完美。
这个要求是局域网环境下,公网环境下首屏秒开达到100毫秒的几乎没有,或者说特别少。常规意义上,我们都会努力让首屏秒开做到1秒也就是1000毫秒左右的时间。
我们能了解到的,现有像快手、斗鱼、虎牙这一类的App,通常首屏时间都会做到3秒以内。3秒是一个界限,大家一般是2秒左右。
观看体验:花屏不再,重在优化卡顿
观看体验包括两部分:花屏和卡顿。
现在直播平台在又拍云等CDN服务商的努力下,“花屏”已经出现得很少了,主要影响观看体验得因素是“卡顿“,主要指的是在一分钟内卡顿出现了多少次,每一次卡顿的时长有多少,最后得出来一个卡顿的时长占比。观看体验的质量评价体系是实验室环境下得出的。
直播质量监控方案的结构和逻辑
国外针对于这一套体系的执行方面可能会更多一些,国内企业目前来说主要关注的可能是卡顿和首屏秒开。延时的话,关注相对会少一些。
我们重点讲一下卡顿的优化体系,包括“卡顿监控”,以及监控的结果收集完之后进行的一些优化工作。
卡顿主要分为四个部分:数据收集、数据分析、数据展示、预警系统。
数据收集,收集主播端和观看端的设备信息、网络环境。设备信息主要是指设备机型、用户IP,以及视频流的分辨率、码率,包括播放过程中的CPU使用率、GDP使用率、内存使用率。网络环境,主要指连接方式。还有一些需要探测才能得知的数据,比如:优先收集手机到本地路由器的网络情况,然后收集手机到公网出口的环境,以及手机到CDN节点的网络情况。第三部分数据是正常监控需要的,包括卡顿数据、首屏数据、延时数据。
数据分析,收集完之后,放到大数据中心做一些数据过滤、综合分析;把用户ID分门别类的处理成我们实际上运营需要的监控数据。
第三是数据展示,这一块主要是地图展示。在一张地图上面,把卡顿率及其它的一些数据展现出来,就是观看会更方便一些。这是战旗整体卡顿监控的监控图表,左下角标注了卡顿率从低到高,最低是“0”,最高是“15”。可以看到,战旗的卡顿率应该在“4”个点以内,一般是3点多。
第四部分是预警系统。这一块主要是运维人员及CDN厂商。常规意义上,这个预警通常会直接给自己公司的运营人员。但是做直播的,基本上都会用到CDN厂商的云加速服务。如果我们发现用户卡顿的话,其实最终会分析得出来用户卡顿原因是CDN某个节点不好,把这个分析反馈给CDN,让CDN进行相应的调整。
整个监控体系,我们这边可以简单的分为五大部分:客户端、监控系统、运维支持、智能调度、CDN厂商。
监控系统首先是给运维支持,然后是给CDN厂商,告诉发生了某个事。然后是给智能调度系统,这一部分的报警级别相对低一点,是针对个别用户的报警。我们可以针对用户报警,根据他的硬件设备情况及网络情况做一次智能调度。比如:探测到带宽不够,发生了卡顿;调度系统只需要给客户端发一条命令,告诉它带宽不够,让它把码率降一级,可能卡顿情况就会缓解。
卡顿优化十条法则
针对于卡顿优化,我们这边可以分为十个部分: HTTP-DNS调度 播放端本地调度 服务端智能调度 服务端手动调度 CDN手动调度 使用UDP推流 推流端流畅度监控 提供多种清晰度选择 播放器优化 用户反馈系统
HTTP-DNS调度 :在国内DNS污染会比较严重,可能会把DNS解析到错误的节点上去。这个解析服务,可能会把你的域名,比如把海徐汇区的域名解析到浦东新区去。我们尽可能去避免这种错误,这个时候就需要HTTP-DNS调度。我们每次拉一个地址之前,使用自己的服务器先做一次解析,保证每次反馈给你是最近的服务节点,这就会比较流畅。
播放端本地调度 :如果发现用户播放比较卡顿,我们本地会有一些检测机制。比如检测是硬件CPU不高,CPU使用率超高了,我们可能就会把它的分辨率、解码率降一下,让CPU有所缓解,这个时候卡顿就会缓解。另外,也可能是网络导致的本地卡顿,我们就可以给他换一个服务节点,或者是做一些其它的处理。
服务器端智能调度、服务器端手动调度 :服务器端的智能调度、手动调度,这个主要是在后端通过远程方式去做一些调整。在智能调度系统里面,我们会根据用户的情况做统一调度。比如用户硬件不够了,我们帮他加一点。如果用户卡顿的话,我们先要判断是CDN节点的问题,还是用户自己的问题。如果是CDN节点的问题,我们帮他自动调到下一个节点去。
CDN手动调度 :指手动干预的方式。比如:现在发生了用户卡顿,智能调度系统比如发现上海徐汇区的一个服务节点特别不好,我们可以手动把这个节点拉黑掉,用户不会访问到这个质量比较差的节点。
第四点、第五点是会有一定的重合度,因为CDN的手动调度也是根据智能调度系统收集到的数据、下发的数据来进行相应的一些处理。
UDP推流 :TCP的协议容灾和慢恢复机制导致它抵抗网络抖动的能力会比较差。为了解决因为网络抖动导致的卡顿问题,我们会使用“自定义”或者是自有UDP协议处理这个问题。谷歌之前有推出类似于“类UDP”的各种SDK包,使用UDP代替TCP对抗网络抖动,可以把TCP的容灾机制、恢复机制规避掉,可以快速地恢复网络。UDP还有一个作用,在丢包的情况下“抗丢包能力”比较好,它可以自己自主决定“重发多少数据包”。
推流端流畅度监控 :这一块主要是监控主播推流是否有音视频不同步或者帧率不够的情况。如果主播端卡顿的话,所有的用户调度到任何节点都会卡顿,所以推流端监控相对会重要一些。针对于主播端的监控,我们实时反馈给主播,让主播切换网络或者是做一些调整。
提供多种清晰度选择 :提供多种清晰度选择,这个主要针对于用户手动操作的。通常情况下会提供标情、高清、超清等多种分辨率。不同的分辨率对应的码率、编码复杂度都是不一样的。清晰度越高,相应解码的难度就会越高。让用户可以手动去选择,在整体情况比较好的时间,他可以提高清晰度。当发生卡顿的情况下,可以手动降清晰度或者是降分辨率,可以解决一部分卡顿的问题。
播放器优化 :播放器的优化,针对于卡顿主要是处理两个事情:一个是音视频不同步导致的卡顿。第二部分,主要是针对于缓冲区的处理,缓冲区相对于对抗“网络抖动”是比较有用处的。
用户反馈系统:这一块是用户主动提供一些卡顿的建议或者问题,用户的反馈系统可以作为我们整体监控系统的一个补充,可以帮我们改善整个监控系统。
延时监控:自定义扩展、数字水印
下面讲一下“延时监控”。“延时监控”我主要讲两个部分的内容:
第一部分,开发阶段延时的计算及延时的优化;
第二部分,发布阶段的延时计算。
通常情况下,直播研发的开发阶段的延时都会以“北京时间”的方式做对比。
开发阶段的延时,推流工具、播放工具在同一台机器上。左边的时间减去右边的时间,实际上就是直播延时。我们可以看到上图的直播延时是2秒2毫秒。
开发阶段的延时计算到了发布阶段已经不管用了,因为正常情况下不可能实时盯着用户手机看“延时多高”,也不可能在视频流里面嵌入一个“北京时间”。
发布阶段延时计算需要借助一些另外的手段,一种方式是自定义扩展,一种方式是数字水印。
自定义扩展实现延时监控
自定义扩展利用直播协议里面的一些自定义字段做延时监控。
一个选择是FLV协议的metadata字段。FLV协议本身有字段,可以嵌入,然后在推流时发“北京时间”放到这个metadata字段里面去,接到之后把它和本地的“北京时间”做差值。
第二个可以扩展的地方,是H.264、H.265编码的SEI字段,这个字段也可以自定义做扩展,计算延时的方法也是相同的,就是在这个字段里面嵌入“北京时间”就可以了。
自定义扩展的两种方法有好处——配置比较简单。
当然也有比较难的地方。因为CDN本身会有转码系统和分发系统,如果不和CDN厂商强调的话,所有的自定义字段从CDN系统过一遍之后全部都会删除掉。
还有一个有问题的地方,在于CDN分发视频流,默认会把所有的视频流,无论是从什么时间开始拉流的,都会“从零开始”。这个时候我们在字段里面嵌入的“北京时间”,实际上就没有参照对象,因为我们是根据视频流里面每一帧视频的时间,以及嵌入到自定义字段的时间,还有本地时间三者做“差”得到的延时,这一部分会有影响。
数字水印实现延时监控
基于前面两种方法的缺点,然后我们又扩展出了根据“数字水印”来嵌入数据计算延时的方法。“数字水印”出现得比较早,原来是用于音视频版权确认,在音视频嵌入不可见、听不到的数据,这对于整体音视频质量影响比较小,但是通过某种算法可以嵌入进去、可以被提取出来,主要是应用在这个方面。
我简单讲一下“数字水印”的原理,数字水印可以嵌入的地方比较多。
先了解下通过 修改YUV原始数据或PCM原始数据植入数字水印 。以720P分辨率的视频流为参照,每一张画面是1280×720像素点,每个像素点是由一个Y以及1/4U、1/4V组成的。通常情况下在Y里面,每一个Y像素是有8比特数据组成的。也就是说,数据范围可以从-127到127。Y数据总共有8比特,我们可以把它末三位抹掉,然后再嵌入我们想要的数据。比如:我们想要的第一个Y像素点里面,就嵌入一个数字“0”或者是“1”,我们可以把8比特后3位抹掉,在里面嵌入后3位是“0-7”的,我们嵌入一个“3”代表“0”,嵌入一个“6”代表“1”。嵌入之后,然后根据同样的方式把这个方式再提取出来,然后把这个数据再还原,就可以得到我们嵌入的YUV里面的数据。这种方式嵌入的话,会有一定的影响。因为正常情况下,我们知道Y数据是“-127”到“127”。末三位改变以后会对色彩有影响。数据准确率越高,就需要抹掉的比特位数越多,对于视频的损伤就越大。我们想要准确度越高,又需要比较多的比特位数,这个时候就需要做一些取舍。
PCM也是同样的方式,在末位不重要、比较小的数据上面做一些修改。
再看下通过 AAC量化子带或H.264块的DCT参数 实现数字水印。
AAC量化子带由一系列的参数组成的,我们可以改写参数第一位。
H.264块的DCT参数,相应这个参数的修改方法也是一样的,改第一位的不重要的这些数据。
客观地讲,刚才几种方式里面,在数据的嵌入、提取的过程中会受到CDN转码系统的一定影响。因为正常情况下,我们知道从YUV数据到H.264重新再解码出YUV的时候,这个YUV数据其实是发生了一些变化,只是这个变化被控制在一定的范围内,绝大多数的情况下是看不出差别。
这个时候,在刚才讲到的这些算法之上,延伸出了一些算法,针对于这些数据的这些参数;我们刚才讲的是直接在这些数据里面做修改,做延伸处理的方法是把这些数据放进,像压缩通常会用到的“离散预选”参数。这个算法相对来说,对于原始数据的修改会更小,然后提取的成功率也会更高。
首屏秒开的三个优化方向
简单过一下首屏秒开优化的要点。首屏优化的话题可能会特别多,我这里就不一个一个去解释了;这里提供首屏秒开的三个优化方向,有优化需求的话,按照推流、转发、播放三个方向去做优化,肯定能收到效果。
推流优化——主播端: 合理的GOP值(建议2秒) 减少帧间依赖,不使用P帧 X264无延时编码 合理的分辨率、码率、帧率 使用UDP对抗网络抖动
转发优化——CDN 缓存GOP数据 提前预热资源 TCP单边加速 提供多路转码流
播放优化——播放端 优先加载视频流数据 可变缓存,先小后大 使用HTTPDNS分配节点 优化FFmpeg视频流探测 网络请求并行加载
我今天讲的内容大体上就是这些,谢谢大家的聆听。
问答部分
Q1 :赛事直播、游戏直播的时候,主播推流一定会延迟。昨天我遇见一个主播,延迟就已经有20秒,也就是说在编码完了之后,会有20秒才会推出来,这样的话,数字水印是不准确的。我看战旗也有很多游戏主播,这个问题怎么解决的?
石硕 :这分为两方面。我们如果要做延时计算,相应来说直播用的工具我们是可控的。像刚才您讲的情况,应该是这个主播用的是开源的OBS推流工具。针对于OBS的推流工具,我们需要做一些特殊的处理。战旗为OBS开发一个插件,这个插件是嵌入视频水印的。第二这个插件要获取OBS缓冲延时,把这个数据获取之后,针对嵌入的视频水印会做一些特殊处理。如果是用自己的直播软件,相应的来说时延就是本地的缓冲区都是自己设置的,相应做一下处理就好了。
Q2 :我前两天刚上线了网页端的H.265,码率比较低;发现卡顿主要原因来自于硬件情况,比如CPU不够或者GPU不行。我考虑在网页端加一些什么样的东西,获取到用户的硬件情况,然后发现能获取到的硬件情况非常有限,跟移动端在上H.265完全不一样的状态。国内主流的浏览器可以获取到用户的内存情况,但获取不到GPU等信息,即便有问题很难定位。战旗在这一块,目前是怎么解决的?
石硕 :这是一个很好的问题。现在主流的浏览器、比较常见的浏览器,通常情况下如果用HTML5做直播,会默认更多得去选硬件。受限于浏览器,我们是拿不到GPU信息的。这个时候,我们会获取一些再外围的数据信息,比如去拿CPU型号,进而获取GPU信息。某几个GPU幸好解1080P确实不行,这个时候我们可以尝试把它的分辨率从1080降到720P,这个时候它的GPU是能吃得消的。
分享PPT+视频实录传送门:
视频直播的用户体验体系与质量监控方案​
多媒体
2018-08-22 14:58:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
D:\ffmpeg\ffmpeg\bin
ffplay.exe 播放rtsp地址:
ffplay.exe -rtsp_transport tcp "rtsp://ip:port/pag://172.16.0.11:7302:33000000001310001748:1:SUB:TCP?cnid=2&pnid=2&auth=50&streamform=rtp"



多媒体
2018-08-20 21:26:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
音频直播服务是叫做 LANmic 无线话筒 的安卓程序。
访问http://192.168.1.8:8080 就能播放了。可以网页播放,vlc,ffmpeg,
那么我sdl能不能播放呢?LANmic 提供了wav编码,可以直接pcm播放。
经过搜索,发现搜到的文章都是一大抄,各种转,都是一样的,sdl本地文件播放,让人头大,无奈只好自己分析方法了。

经过抓包,我发现访问http://192.168.1.8:8080 返回的是http头,后续就是riff wavefmt 搞过wav格式的都知道,这个wav头了
那么我们把http头跳过,读取后面的wav头以及数据,就可以播放了。


经过调试,代码如下 #include #include #include "SDL.h" #include "sockutil.h" #pragma comment(lib, "sdl2.lib") #pragma comment(lib, "SDL2main.lib") Uint32 audio_len;//音频数据大小 Uint8 *audio_pos;//指向音频数据的指针 /**回调函数(由系统调用) * 函数声明:typedef void (SDLCALL * SDL_AudioCallback) * (void *userdata, Uint8 * stream, int len); * This function is called when the audio device needs more data. * * userdata: An application-specific parameter saved in the SDL_AudioSpec structure(SDL_AudioSpec结构中的用户自定义数据,一般情况下可以不用) * stream: A pointer to the audio data buffer.(该指针指向需要填充的音频缓冲区) * len: The length of that buffer in bytes.(音频缓冲区的大小,以字节为单位) * * Once the callback returns, the buffer will no longer be valid. * Stereo samples are stored in a LRLRLR ordering. * * You can choose to avoid callbacks and use SDL_QueueAudio() instead, if * you like. Just open your audio device with a NULL callback. */ void fill_audio(void *userdata, Uint8 *stream, int len) { //SDL2中必须首先使用SDL_memset()将stream中的数据设置为0 SDL_memset(stream, 0, len); if (audio_len == 0) /* Only play if we have data left */ { return; } len = (len > audio_len ? audio_len : len); /* Mix as much data as possible */ /** * 函数声明:extern DECLSPEC void SDLCALL * SDL_MixAudio(Uint8 * dst, const Uint8 * src, Uint32 len, int volume); * This takes two audio buffers of the playing audio format and mixes * them, performing addition, volume adjustment, and overflow clipping. * The volume ranges from 0 - 128, and should be set to ::SDL_MIX_MAXVOLUME * for full audio volume. Note this does not change hardware volume. * This is provided for convenience -- you can mix your own audio data. * * #define SDL_MIX_MAXVOLUME 128 */ SDL_MixAudio(stream, audio_pos, len, SDL_MIX_MAXVOLUME); audio_pos += len; audio_len -= len; } int play() { //初始化SDL if (SDL_Init(SDL_INIT_AUDIO)) { printf("Could not initialize SDL - %s\n", SDL_GetError()); return -1; } //SDL_AudioSpec初始化 SDL_AudioSpec wanted_spec; wanted_spec.freq = 44100; //音频数据的采样率(常用的有48000,44100等) wanted_spec.format = AUDIO_S16SYS;//音频数据的格式 wanted_spec.channels = 1; //声道数(例如单声道取值为1,立体声取值为2) wanted_spec.silence = 0; //设置静音的值 wanted_spec.samples = 1024; //音频缓冲区中的采样个数(要求必须是2的n次方) wanted_spec.callback = fill_audio;//填充音频缓冲区的回调函数 //打开音频 if (SDL_OpenAudio(&wanted_spec, NULL) < 0) { printf("can't open audio.\n"); return -1; } //注释掉的是文件播放. //FILE *fp_pcm = fopen("aa.wav", "rb"); //if (fp_pcm == NULL) //{ // printf("cannot open this file\n"); // return -1; //} SOCK_INIT char target[255]= "192.168.1.8"; // Connect to server. SOCKET c = SockLib::ConnectTarget(target,8080); if ( c == INVALID_SOCKET ) { printf("Connected to server %s err \n", target ); return 0; } printf("Connected to server %s \n", target ); std::string httpheader="" ; int ct=0; while(1) { char buf[1025]=""; int ret=recv(c,buf,1,0); if(ret<0) return 1; httpheader +=buf[0]; ct+=ret; int i=IsHTTPRequestComplete(httpheader.c_str(),ct); //有http结束标志,说明接收完. if (i>0) { break; } } //跳过http头,后面就是RIFF的wav数据了。 Sleep(1000); int pcm_buffer_size = 4096;//每次读取4096字节的数据,同时也是音频帧大小 char *pcm_buffer = (char *)malloc(pcm_buffer_size); int data_count = 0; //播放音频数据 SDL_PauseAudio(0); while (true) { //int ret = fread(pcm_buffer, 1, pcm_buffer_size, fp_pcm); bool ret = SockLib::RecvBuff( c, (BYTE*)pcm_buffer, pcm_buffer_size ); if (ret ==false) { //这里有可能是会有剩余音频数据的,不知道这样改对不对? audio_pos = (Uint8 *)pcm_buffer; audio_len = ret; while (audio_len > 0) { SDL_Delay(1); } //退出 break; //循环播放 // fseek(fp_pcm, 0, SEEK_SET); // fread(pcm_buffer, 1, pcm_buffer_size, fp_pcm); data_count = 0; } printf("Now Playing %10d Bytes data.\n", data_count); data_count += pcm_buffer_size; audio_pos = (Uint8 *)pcm_buffer; //Audio buffer length audio_len = pcm_buffer_size; while (audio_len > 0)//Wait until finish { SDL_Delay(1); } } free(pcm_buffer); // fclose(fp_pcm); SDL_Quit(); return -1; } int _tmain(int argc, _TCHAR* argv[]) { while(1) { play(); } return 0; }
多媒体
2018-08-15 13:45:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
例子
该例子的功能是将mp4文件转换成yuv数据以及h264裸流。 #include #include #include #include "config.h" extern "C" { #include #include #include #include #include #include } ; using namespace std; int main(void) { FILE *f_yuv = fopen("output.yuv", "wb+"); FILE *f_h264 = fopen("output.h264", "wb+"); //初始化avcodec avcodec_register_all(); //初始化 demuxer av_register_all(); //创建一个用于demuxer的结构体 AVFormatContext *av_format_context = avformat_alloc_context(); char source[] = "/Users/yxwang/Downloads/test.mp4"; if (avformat_open_input(&av_format_context, source, NULL, NULL) != 0) { cout << "打开文件失败" << endl; return -1; } //需要关闭尝试是否需要手动获取视频文件信息 if (avformat_find_stream_info(av_format_context, NULL) < 0) { //获取视频文件信息 cout << "Couldn't find stream information." << endl; return -1; } int videoindex = -1; for (int i = 0; i < av_format_context->nb_streams; i++) { if (av_format_context->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) { videoindex = i; break; } } if (videoindex == -1) { cout << "Didn't find a video stream." << endl; return -1; } AVCodecContext *pCodecCtx = av_format_context->streams[videoindex]->codec; AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id); //查找decoder if (pCodec == NULL) { printf("Codec not found.\n"); return -1; } if (avcodec_open2(pCodecCtx, pCodec, NULL) != 0) { printf("Can not open codec.\n"); return -1; } uint8_t *out_buffer = (uint8_t *) av_malloc( av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1)); AVFrame *decodeFrame = av_frame_alloc(); AVFrame *pFrameYUV = av_frame_alloc(); av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1); AVPacket* packet = (AVPacket *) av_malloc(sizeof(AVPacket)); //Output Info----------------------------- printf("--------------- File Information ----------------\n"); av_dump_format(av_format_context, 0, source, 0); printf("-------------------------------------------------\n"); SwsContext *img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL); while (av_read_frame(av_format_context, packet) >= 0) { //读取一帧压缩数据 if (packet->stream_index == videoindex) { fwrite(packet->data, 1, packet->size, f_h264); //把H264数据写入fp_h264文件 if (avcodec_send_packet(pCodecCtx, packet) < 0) { //解码一帧压缩数据 printf("Decode Error.\n"); return -1; } while (avcodec_receive_frame(pCodecCtx, decodeFrame) == 0) { sws_scale(img_convert_ctx, (const unsigned char* const *) decodeFrame->data, decodeFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize); int y_size = pCodecCtx->width * pCodecCtx->height; fwrite(pFrameYUV->data[0], 1, y_size, f_yuv); //Y fwrite(pFrameYUV->data[1], 1, y_size / 4, f_yuv); //U fwrite(pFrameYUV->data[2], 1, y_size / 4, f_yuv); //V } } av_free_packet(packet); } fclose(f_yuv); fclose(f_h264); sws_freeContext(img_convert_ctx); av_frame_free(&pFrameYUV); av_frame_free(&decodeFrame); avcodec_close(pCodecCtx); //内部会调用avformat_free_context avformat_close_input(&av_format_context); return EXIT_SUCCESS; }
深入分析
那么正题来了,我们已经在前面的章节分析过了avcodec_register_all 以及 av_register_all两个函数。并且也已经知道使用avformat_alloc_context来创建一个AVFormatContext 是所有和解封装封装相关的基础操作( ffmpeg源码分析 (二) )。
avformat_open_input
第一个需要研究的函数就是avformat_open_input了,该方法定义在了avformat.h中 /** * Open an input stream and read the header. The codecs are not opened. * The stream must be closed with avformat_close_input(). * 打开一个输入流,并读取它的头 * @param ps 可以传入空指针,这个时候方法会自动创建一个AVFormatContext并且放入ps中 * @param url URL of the stream to open. 流地址 * @param fmt 如果不为空,那么强制使用指定的输入格式,否则ffmpeg会去自动发现格式 * @param options A dictionary filled with AVFormatContext and demuxer-private options. * On return this parameter will be destroyed and replaced with a dict containing * options that were not found. May be NULL. * * @return 0 on success, a negative AVERROR on failure. * * @note If you want to use custom IO, preallocate the format context and set its pb field. */ int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);
该方法算是核心方法了,实现来说还是比较复杂的,这里有一张古时候的调用流程图,在当前版本的ffmpeg基本也是这个流程。

这里还有一张当前的调用流程图


实现写在了 avformat/utils.c中. int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options) { AVFormatContext *s = *ps; int i, ret = 0; AVDictionary *tmp = NULL; ID3v2ExtraMeta *id3v2_extra_meta = NULL; if (!s && !(s = avformat_alloc_context())) //如果AVFormatContext未空,那么新创建一个 return AVERROR(ENOMEM); if (!s->av_class) { av_log(NULL, AV_LOG_ERROR, "Input context has not been properly allocated by avformat_alloc_context() and is not NULL either\n"); return AVERROR(EINVAL); } if (fmt) //如果fmt不为空,那么直接指定AVInputFormat s->iformat = fmt; if (options)//将option拷贝到 tmp中 av_dict_copy(&tmp, *options, 0); if (s->pb) // must be before any goto fail 设置flag 用户自己设置了AVIOContext s->flags |= AVFMT_FLAG_CUSTOM_IO; if ((ret = av_opt_set_dict(s, &tmp)) < 0) goto fail; if (!(s->url = av_strdup(filename ? filename : ""))) { ret = AVERROR(ENOMEM); goto fail; } #if FF_API_FORMAT_FILENAME FF_DISABLE_DEPRECATION_WARNINGS av_strlcpy(s->filename, filename ? filename : "", sizeof(s->filename)); FF_ENABLE_DEPRECATION_WARNINGS ...............
avformat_open_input方法的实现很长,不过其中包含了非常多的保护性代码,比如上面的代码,都是在做一些安全性保护,以及变量初始化。
init_input
这是avformat_open_input中核心方法,主要作用是打开输入的视频数据并且探测视频的格式. /* Open input file and probe the format if necessary. */ static int init_input(AVFormatContext *s, const char *filename, AVDictionary **options) { int ret; AVProbeData pd = { filename, NULL, 0 }; int score = AVPROBE_SCORE_RETRY; //得分 if (s->pb) { //自定义AVIOContext的情况,一般发生在从内存中读取数据,这个时候需要自定义AVIOContext直接输入 s->flags |= AVFMT_FLAG_CUSTOM_IO; if (!s->iformat) //如果没有自己设置iformat,那么使用av_probe_input_buffer2推测AVInputFormat return av_probe_input_buffer2(s->pb, &s->iformat, filename, s, 0, s->format_probesize); else if (s->iformat->flags & AVFMT_NOFILE) av_log(s, AV_LOG_WARNING, "Custom AVIOContext makes no sense and " "will be ignored with AVFMT_NOFILE format.\n"); return 0; //指定了iformat直接返回 } //如果没有设置AVInputFormat,那么使用av_probe_input_format2来判断文件格式。 //如果找到了大于预设值score的分数,那么直接返回分数 if ((s->iformat && s->iformat->flags & AVFMT_NOFILE) || (!s->iformat && (s->iformat = av_probe_input_format2(&pd, 0, &score)))) return score; //如果没有判断出来,那么就需要通过io_open真正打开文件,再去判断AVInputFormat,这个方法的实现我们后些说 if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0) return ret; if (s->iformat) return 0; return av_probe_input_buffer2(s->pb, &s->iformat, filename, s, 0, s->format_probesize); }
在函数的开头的score变量是一个判决AVInputFormat的分数的门限值,如果最后得到的AVInputFormat的分数低于该门限值,就认为没有找到合适的AVInputFormat。FFmpeg内部判断封装格式的原理实际上是对每种AVInputFormat给出一个分数,满分是100分,越有可能正确的AVInputFormat给出的分数就越高。最后选择分数最高的AVInputFormat作为推测结果。
av_probe_input_format2 /** * Guess the file format. * * @param pd data to be probed 存储输入数据信息的AVProbeData结构体。 * @param is_opened Whether the file is already opened; determines whether * demuxers with or without AVFMT_NOFILE are probed. 文件是否打开。 * @param score_max A probe score larger that this is required to accept a * detection, the variable is set to the actual detection * score afterwards. * If the score is <= AVPROBE_SCORE_MAX / 4 it is recommended * to retry with a larger probe buffer.判决AVInputFormat的门限值。只有某格式判决分数大于该 * 门限值的时候,函数才会返回该封装格式,否则返回NULL。 */ AVInputFormat *av_probe_input_format2(AVProbeData *pd, int is_opened, int *score_max);
该函数用于根据输入数据查找合适的AVInputFormat.
其中涉及到一个AVProbeData的结构体,从Init_input上我们可以找打它的构造
AVProbeData pd = { filename, NULL, 0 };
实际上就是用来存储视频数据信息的一个结构体,具体定义如下 /** * This structure contains the data a format has to probe a file. */ typedef struct AVProbeData { const char *filename; //文件路径 unsigned char *buf; /**< Buffer must have AVPROBE_PADDING_SIZE of extra allocated bytes filled with zero. 用于存放推测的媒体数据,但是最后还需要填充AVPROBE_PADDING_SIZE个0(实际就是32个) */ int buf_size; /**< Size of buf except extra allocated bytes buffer长度,不包括填充的0的长度 */ const char *mime_type; /**< mime_type, when known. 存放的推测媒体数据的mime_type*/ } AVProbeData;
回到 av_probe_input_format2 这个函数的定义如下 AVInputFormat *av_probe_input_format2(AVProbeData *pd, int is_opened, int *score_max) { int score_ret; AVInputFormat *fmt = av_probe_input_format3(pd, is_opened, &score_ret); if (score_ret > *score_max) { *score_max = score_ret; return fmt; } else return NULL; }
方法比较简单,实际上就是进一步去调用av_probe_input_format3去查找AVInputFormat,同时还要返回最大得分。通过和阈值得分比价,如果大于阈值得分,那么返回查找到的AVInputFormat会被返回,否则返回null。

av_probe_input_format3
层层递进,不愧是ffmpeg的核心方法,复杂程度也是杠杠得!在分析代码之前可以先了解一些知识:
ID3,一般是位于一个mp3文件的开头或末尾的若干字节内,附加了关于该mp3的歌手,标题,专辑名称,年代,风格等信息,该信息就被称为ID3信息,ID3信息分为两个版本,v1和v2版。
定义没啥好说的,直接来看实现,代码还是比较长的。 AVInputFormat *av_probe_input_format3(AVProbeData *pd, int is_opened, int *score_ret/*最匹配格式的分数,需要改方法填入值*/) { AVProbeData lpd = *pd; AVInputFormat *fmt1 = NULL, *fmt; int score, score_max = 0; void *i = 0; const static uint8_t zerobuffer[AVPROBE_PADDING_SIZE]; enum nodat { NO_ID3, ID3_ALMOST_GREATER_PROBE, ID3_GREATER_PROBE, ID3_GREATER_MAX_PROBE, } nodat = NO_ID3; if (!lpd.buf) lpd.buf = (unsigned char *) zerobuffer; //这一段是用来检查是否有ID3信息的,并且移动指针跳,使lpd.buf移动到数据地址 if (lpd.buf_size > 10 && ff_id3v2_match(lpd.buf, ID3v2_DEFAULT_MAGIC)) { int id3len = ff_id3v2_tag_len(lpd.buf); if (lpd.buf_size > id3len + 16) { if (lpd.buf_size < 2LL*id3len + 16) nodat = ID3_ALMOST_GREATER_PROBE; lpd.buf += id3len; lpd.buf_size -= id3len; } else if (id3len >= PROBE_BUF_MAX) { nodat = ID3_GREATER_MAX_PROBE; } else nodat = ID3_GREATER_PROBE; } fmt = NULL; //遍历所有的 AVIputFormat while ((fmt1 = av_demuxer_iterate(&i))) { if (!is_opened == !(fmt1->flags & AVFMT_NOFILE) && strcmp(fmt1->name, "image2")) continue; score = 0; if (fmt1->read_probe) { score = fmt1->read_probe(&lpd); if (score) av_log(NULL, AV_LOG_TRACE, "Probing %s score:%d size:%d\n", fmt1->name, score, lpd.buf_size); if (fmt1->extensions && av_match_ext(lpd.filename, fmt1->extensions)) { switch (nodat) { case NO_ID3: score = FFMAX(score, 1); break; case ID3_GREATER_PROBE: case ID3_ALMOST_GREATER_PROBE: score = FFMAX(score, AVPROBE_SCORE_EXTENSION / 2 - 1); break; case ID3_GREATER_MAX_PROBE: score = FFMAX(score, AVPROBE_SCORE_EXTENSION); break; } } } else if (fmt1->extensions) { if (av_match_ext(lpd.filename, fmt1->extensions)) score = AVPROBE_SCORE_EXTENSION; } if (av_match_name(lpd.mime_type, fmt1->mime_type)) { if (AVPROBE_SCORE_MIME > score) { av_log(NULL, AV_LOG_DEBUG, "Probing %s score:%d increased to %d due to MIME type\n", fmt1->name, score, AVPROBE_SCORE_MIME); score = AVPROBE_SCORE_MIME; } } if (score > score_max) { score_max = score; fmt = fmt1; } else if (score == score_max) fmt = NULL; } if (nodat == ID3_GREATER_PROBE) score_max = FFMIN(AVPROBE_SCORE_EXTENSION / 2 - 1, score_max); *score_ret = score_max; return fmt; }
该方法最重要的就是循环中的内容,使用av_demuxer_iterate来遍历所有的demuxer(也就是AVInputFormat)。
如果当前AVInputFormat定义了read_probe方法,就是用该方法来匹配数据,并且返回一个所得分。这里我们不去分析每个AVInputFormat到底是如何去计算分数的(需要的时候可以自己去看相关的类型)。
av_match_ext用来比对文件的后缀名和AVInputFormat的后缀名是否相同。
另外还会使用av_match_name()比较输入媒体的mime_type,如果匹配,那么得分就是75分。
基本逻辑就是这样,其中av_match_ext 和 av_match_name其实是很基础的代码,实际上不涉及到多媒体逻辑,只是字符串比较而已,看一下代码一下就能明白,所以这里也不多介绍。

av_probe_input_buffer2
av_probe_input_buffer2(),它根据输入的媒体数据推测该媒体数据的AVInputFormat,声明位于libavformat\avformat.h /** * Probe a bytestream to determine the input format. Each time a probe returns * with a score that is too low, the probe buffer size is increased and another * attempt is made. When the maximum probe size is reached, the input format * with the highest score is returned. * * @param pb the bytestream to probe 用于读取数据的AVIOContext * @param fmt the input format is put here 推测出来的AVInputFormat * @param url the url of the stream 输入媒体的路径 * @param logctx the log context 日志 * @param offset the offset within the bytestream to probe from 开始推测AVInputFormat的偏移量。 * @param max_probe_size the maximum probe buffer size (zero for default) 用于推测格式的媒体数据的最大值。0表示数据大长度 * @return the score in case of success, a negative value corresponding to an * the maximal score is AVPROBE_SCORE_MAX 推测后返回匹配分数 * AVERROR code otherwise */ int av_probe_input_buffer2(AVIOContext *pb, AVInputFormat **fmt, const char *url, void *logctx, unsigned int offset, unsigned int max_probe_size);
实现在avformat.c中 int av_probe_input_buffer2(AVIOContext *pb, AVInputFormat **fmt, const char *filename, void *logctx, unsigned int offset, unsigned int max_probe_size) { AVProbeData pd = { filename ? filename : "" }; uint8_t *buf = NULL; int ret = 0, probe_size, buf_offset = 0; int score = 0; int ret2; //如果没有设置最大读取数据长度,那么设置成默认值,约1M if (!max_probe_size) max_probe_size = PROBE_BUF_MAX; else if (max_probe_size < PROBE_BUF_MIN) { av_log(logctx, AV_LOG_ERROR, "Specified probe size value %u cannot be < %u\n", max_probe_size, PROBE_BUF_MIN); return AVERROR(EINVAL); } if (offset >= max_probe_size) return AVERROR(EINVAL); if (pb->av_class) { uint8_t *mime_type_opt = NULL; char *semi; av_opt_get(pb, "mime_type", AV_OPT_SEARCH_CHILDREN, &mime_type_opt); pd.mime_type = (const char *)mime_type_opt; semi = pd.mime_type ? strchr(pd.mime_type, ';') : NULL; if (semi) { *semi = '\0'; } } #if 0 if (!*fmt && pb->av_class && av_opt_get(pb, "mime_type", AV_OPT_SEARCH_CHILDREN, &mime_type) >= 0 && mime_type) { if (!av_strcasecmp(mime_type, "audio/aacp")) { *fmt = av_find_input_format("aac"); } av_freep(&mime_type); } #endif //这个for循环是精髓,它增量式读取媒体数据进行判断,如果判断出来了,那就直接返回,否则读取更多数据送入判断 for (probe_size = PROBE_BUF_MIN; probe_size <= max_probe_size && !*fmt; probe_size = FFMIN(probe_size << 1, FFMAX(max_probe_size, probe_size + 1))) { score = probe_size < max_probe_size ? AVPROBE_SCORE_RETRY : 0; /* Read probe data. */ //分配空间,用于读取数据 if ((ret = av_reallocp(&buf, probe_size + AVPROBE_PADDING_SIZE)) < 0) goto fail; //读取指定大小的数据 if ((ret = avio_read(pb, buf + buf_offset, probe_size - buf_offset)) < 0) { /* Fail if error was not end of file, otherwise, lower score. */ if (ret != AVERROR_EOF) goto fail; score = 0; ret = 0; /* error was end of file, nothing read */ } buf_offset += ret; if (buf_offset < offset) continue; pd.buf_size = buf_offset - offset; pd.buf = &buf[offset]; memset(pd.buf + pd.buf_size, 0, AVPROBE_PADDING_SIZE); /* Guess file format. */ //最终的数据判断实际上还是调用我们上面介绍的av_probe_input_format2 *fmt = av_probe_input_format2(&pd, 1, &score); if (*fmt) { /* This can only be true in the last iteration. */ if (score <= AVPROBE_SCORE_RETRY) { av_log(logctx, AV_LOG_WARNING, "Format %s detected only with low score of %d, " "misdetection possible!\n", (*fmt)->name, score); } else av_log(logctx, AV_LOG_DEBUG, "Format %s probed with size=%d and score=%d\n", (*fmt)->name, probe_size, score); #if 0 FILE *f = fopen("probestat.tmp", "ab"); fprintf(f, "probe_size:%d format:%s score:%d filename:%s\n", probe_size, (*fmt)->name, score, filename); fclose(f); #endif } } if (!*fmt) ret = AVERROR_INVALIDDATA; fail: /* Rewind. Reuse probe buffer to avoid seeking. */ ret2 = ffio_rewind_with_probe_data(pb, &buf, buf_offset); if (ret >= 0) ret = ret2; av_freep(&pd.mime_type); return ret < 0 ? ret : score; }

再回到前面一些,是不是已经忘了我们到底再分析什么了?我们正在分析 avformat_open_input 这个方法,并且还是刚说完第一步init_input而已。 int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options) { ...... //打开文件并判断数据类型 if ((ret = init_input(s, filename, &tmp)) < 0) goto fail; s->probe_score = ret; //如果 AVIOContext设置了协议白名单,并且AVFormatContext自己没设置,那么拷贝过去 if (!s->protocol_whitelist && s->pb && s->pb->protocol_whitelist) { s->protocol_whitelist = av_strdup(s->pb->protocol_whitelist); if (!s->protocol_whitelist) { ret = AVERROR(ENOMEM); goto fail; } } //黑名单,处理逻辑和白名单相同 if (!s->protocol_blacklist && s->pb && s->pb->protocol_blacklist) { s->protocol_blacklist = av_strdup(s->pb->protocol_blacklist); if (!s->protocol_blacklist) { ret = AVERROR(ENOMEM); goto fail; } } //如果AVFormatContext设置了格式白名单,那么就用当前匹配出来的格式和白名单对比,如果不在白名单中,那么就报错 if (s->format_whitelist && av_match_list(s->iformat->name, s->format_whitelist, ',') <= 0) { av_log(s, AV_LOG_ERROR, "Format not on whitelist \'%s\'\n", s->format_whitelist); ret = AVERROR(EINVAL); goto fail; } //跳过打开文件时的初始字节,在encoding中没有效果,在decoding中用户自己设置 avio_skip(s->pb, s->skip_initial_bytes); ..... //读取id3v2 if (s->pb) ff_id3v2_read_dict(s->pb, &s->internal->id3v2_meta, ID3v2_DEFAULT_MAGIC,&id3v2_extra_meta); ..... }
关于协议的白名单和协议黑名单,我们在之后讲解.
read_header()
该方法用于读取多媒体数据文件头,根据视音频流创建相应的AVStream,不同的AVInputFormat使用会用不同的读取方法,所以该方法会在每个自己的demuxer中自己定义。并且理论上需要调用avformat_new_stream来创建 AVStream(但是我看了flv格式的read_header方法,发现并没有调用avformat_new_stream来创建AVStream,而是被推迟了)。avformat_new_stream方法的作用就是初始化AVFormatContext中的AVSteam,分配空间,但是不会填入值。关于AVStream的创建会补充到 ffmpeg源码分析 (二) 中
参考文档
https://blog.csdn.net/leixiaohua1020/article/details/44064715
多媒体
2018-07-05 19:13:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
mGlProgram = createProgram(mVertexScript, mFragmentScript); glUseProgram(mGlProgram); //reject attribution GLuint a_pos = glGetAttribLocation(mGlProgram, "a_pos"); GLuint a_tex_coord = glGetAttribLocation(mGlProgram, "a_tex_coord"); //active attribution glEnableVertexAttribArray(a_pos); glEnableVertexAttribArray(a_tex_coord); //assigne attribution glVertexAttribPointer(a_pos, 3, GL_FLOAT, GL_TRUE, sizeof(vertex), 0); glVertexAttribPointer(a_tex_coord, 2, GL_FLOAT, GL_TRUE, sizeof(vertex), (GLvoid*) (sizeof(float) * 3)); //glEnable(GL_TEXTURE_2D); glPixelStorei(GL_PACK_ALIGNMENT, 1); // set 4 bytes for the buffer. glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glActiveTexture(GL_TEXTURE0); glGenTextures(1, &mY); glBindTexture(GL_TEXTURE_2D, mY); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);//if widht and height not 2^n , should set this glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, NULL); glGenTextures(1, &mU); glBindTexture(GL_TEXTURE_2D, mU); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width >> 1, height >> 1, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, NULL); glGenTextures(1, &mV); glBindTexture(GL_TEXTURE_2D, mV); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width >> 1, height >> 1, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, NULL); glDrawElements(GL_TRIANGLES, num, GL_UNSIGNED_BYTE, 0); glFlush();




Function: void glVertexAttribPointer( GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride,const GLvoid * pointer); 参数: index 指定要修改的顶点属性的索引值 size 指定每个顶点属性的组件数量。必须为1、2、3或者4。初始值为4。(如position是由3个(x,y,z)组成,而颜色是4个(r,g,b,a)) type 指定数组中每个组件的数据类型。可用的符号常量有GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT,GL_UNSIGNED_SHORT, GL_FIXED, 和 GL_FLOAT,初始值为GL_FLOAT。 normalized 指定当被访问时,固定点数据值是否应该被归一化(GL_TRUE)或者直接转换为固定点值(GL_FALSE)。 stride 指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0。 pointer 指定第一个组件在数组的第一个顶点属性中的偏移量。该数组与GL_ARRAY_BUFFER绑定,储存于缓冲区中。初始值为0;
通过glPixelStore可以修改像素保存时对齐的方式。 像这样: int alignment = 4; glPixelStorei(GL_UNPACK_ALIGNMENT, alignment); 第一个参数表示“设置像素的对齐值”,第二个参数表示实际设置为多少。这里像素可以单字节对齐(实际上就是不使用对齐)、双字节对齐(如果长度为奇数,则再补一个字节)、四字节对齐(如果长度不是四的倍数,则补为四的倍数)、八字节对齐。分别对应alignment的值为1, 2, 4, 8。实际上,默认的值是4,正好与BMP文件的对齐方式相吻合。 glPixelStorei也可以用于设置其它各种参数。
void glGenTextures (GLsizei n, GLuint *textures); //在数组textures中返回n个当期未使用的值,表示纹理对象的名称 //零作为一个保留的纹理对象名,它不会被此函数当做纹理对象名称而返回 GLboolean glIsTexture (GLuint texture); //如果texture是一个已绑定的纹理对象名称,并且没有删除,就返回GL_TRUE;


glActiveTexture && glBindTexture
可以这样简单的理解为:显卡中有N个纹理单元(具体数目依赖你的显卡能力),每个纹理单元(GL_TEXTURE0、GL_TEXTURE1等)都有GL_TEXTURE_1D、GL_TEXTURE_2D等,如下代码: struct TextureUnit { GLuint targetTexture1D; GLuint targetTexture2D; GLuint targetTexture3D; GLuint targetTextureCube; ... }; TextureUnit textureUnits[GL_MAX_TEXTURE_IMAGE_UNITS] GLuint currentTextureUnit = 0;
默认情况下当前活跃的纹理单元为0. void glActiveTexture(GLenum textureUnit) { currentTextureUnit = textureUnit - GL_TEXTURE0 ; }
glActiveTextue 并不是激活纹理单元,而是选择当前活跃的纹理单元。 void glBindTexture(GLenum textureTarget, GLuint textureObject) { TextureUnit *texUnit = &textureUnits[currentTextureUnit]; switch(textureTarget) { case GL_TEXTURE_1D: texUnit->targetTexture1D = textureObject; break; case GL_TEXTURE_2D: texUnit->targetTexture2D = textureObject; break; case GL_TEXTURE_3D: texUnit->targetTexture3D = textureObject; break; case GL_TEXTURE_CUBEMAP: texUnit->targetTextureCube = textureObject; break; } }
从示例代码中可以看到:当绑定纹理目标时,所作用的是当前活跃的纹理单元。
void glDrawElements( GLenum mode, GLsizei count, GLenum type, const GLvoid *indices); 其中: mode指定绘制图元的类型,它应该是下列值之一,GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES, GL_QUAD_STRIP, GL_QUADS, and GL_POLYGON. count为绘制图元的数量乘上一个图元的顶点数。 type为索引值的类型,只能是下列值之一:GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT, or GL_UNSIGNED_INT。 indices:指向索引存贮位置的指针。 glDrawElements函数能够通过较少的函数调用绘制多个几何图元,而不是通过OPENGL函数调用来传递每一个顶点,法线,颜色信息。你可以事先准备一系列分离的顶点、法线、颜色数组,并且调用一次glDrawElements把这些数组定义成一个图元序列。当调用glDrawElements函数的时候,它将通过索引使用count个成序列的元素来创建一系列的几何图元。mode指定待创建的图元类型和数组元素如何用来创建这些图元。但是如果GL_VERTEX_ARRAY 没有被激活的话,不能生成任何图元。被glDrawElements修改的顶点属性在glDrawElements调用返回后的值具有不确定性,例如,GL_COLOR_ARRAY被激活后,当glDrawElements执行完成时,当前的颜色值是没有指定的。没有被修改的属性值保持不变。

多媒体
2018-07-05 11:14:03
「深度学习福利」大神带你进阶工程师,立即查看>>>
在计算机图形学中,图像缩放指的是通过增加或去掉像素来改变图片的尺寸。由于要在效率和图像质量比如平滑度和清晰度之间做折衷,图像缩放并不是个平庸的过程。当图像尺寸增大的时候,组成图像的像素也越来越大,图像看上去就变“柔和”了。而缩小图像的时候, 图像就变得平滑和清晰了。对图像进行缩放,实际上根据原图像的像素信息推导出缩放后图像的像素信息,实际上是通过插值实现了这一问题。
“Inverse Distance to a Power(反距离加权插值法)”、
“Kriging(克里金插值法)”、
“Minimum Curvature(最小曲率)”、
“Modified Shepard's Method(改进谢别德法)”、
“Natural Neighbor(自然邻点插值法)”、
“Nearest Neighbor(最近邻插值法)”、
“Polynomial Regression(多元回归法)”、
“Radial Basis Function(径向基函数法)”、
“Triangulation with Linear Interpolation(线性插值三角网法)”、
“Moving Average(移动平均法)”、
“Local Polynomial(局部多项式法)” 。
1、距离倒数乘方法
距离倒数乘方格网化方法是一个加权平均插值法,可以进行确切的或者圆滑的方式插值。方次参数控制着权系数如何随着离开一个格网结点距离的增加而下降。对于一个较大的方次,较近的数据点被给定一个较高的权重份额,对于一个较小的方次,权重比较均匀地分配给各数据点。
计算一个格网结点时给予一个特定数据点的权值与指定方次的从结点到观测点的该结点被赋予距离倒数成比例。当计算一个格网结点时,配给的权重是一个分数,所有权重的总和等于1.0。当一个观测点与一个格网结点重合时,该观测点被给予一个实际为 1.0 的权重,所有其它观测点被给予一个几乎为 0.0 的权重。换言之,该结点被赋给与观测点一致的值。这就是一个准确插值。
距离倒数法的特征之一是要在格网区域内产生围绕观测点位置的"牛眼"。用距离倒数格网化时可以指定一个圆滑参数。大于零的圆滑参数保证,对于一个特定的结点,没有哪个观测点被赋予全部的权值,即使观测点与该结点重合也是如此。圆滑参数通过修匀已被插值的格网来降低"牛眼"影响。
2、克里金法
克里金法是一种在许多领域都很有用的地质统计格网化方法。克里金法试图那样表示隐含在你的数据中的趋势,例如,高点会是沿一个脊连接,而不是被牛眼形等值线所孤立。 克里金法中包含了几个因子:变化图模型,漂移类型和矿块效应。
3、最小曲率法
最小曲率法广泛用于地球科学。用最小曲率法生成的插值面类似于一个通过各个数据值的,具有最小弯曲量的长条形薄弹性片。最小曲率法,试图在尽可能严格地尊重数据的同时,生成尽可能圆滑的曲面。
使用最小曲率法时要涉及到两个参数:最大残差参数和最大循环次数参数来控制最小曲率的收敛标准。
4、多元回归法
多元回归被用来确定你的数据的大规模的趋势和图案。你可以用几个选项来确定你需要的趋势面类型。多元回归实际上不是插值器,因为它并不试图预测未知的 Z 值。它实际上是一个趋势面分析作图程序。
使用多元回归法时要涉及到曲面定义和指定XY的最高方次设置,曲面定义是选择采用的数据的多项式类型,这些类型分别是简单平面、双线性鞍、二次曲面、三次曲面和用户定义的多项式。参数设置是指定多项式方程中X和Y组元的最高方次。
5、径向基本函数法
径向基本函数法是多个数据插值方法的组合。根据适应你的数据和生成一个圆滑曲面的能力,其中的复二次函数被许多人认为是最好的方法。所有径向基本函数法都是准确的插值器,它们都要为尊重你的数据而努力。为了试图生成一个更圆滑的曲面,对所有这些方法你都可以引入一个圆滑系数。你可以指定的函数类似于克里金中的变化图。当对一个格网结点插值时,这些个函数给数据点规定了一套最佳权重。
6、谢别德法
谢别德法使用距离倒数加权的最小二乘方的方法。因此,它与距离倒数乘方插值器相似,但它利用了局部最小二乘方来消除或减少所生成等值线的"牛眼"外观。谢别德法可以是一个准确或圆滑插值器。 在用谢别德法作为格网化方法时要涉及到圆滑参数的设置。圆滑参数是使谢别德法能够象一个圆滑插值器那样工作。当你增加圆滑参数的值时,圆滑的效果越好。
7、三角网/线形插值法
三角网插值器是一种严密的插值器,它的工作路线与手工绘制等值线相近。这种方法是通过在数据点之间连线以建立起若干个三角形来工作的。原始数据点的连结方法是这样:所有三角形的边都不能与另外的三角形相交。其结果构成了一张覆盖格网范围的,由三角形拼接起来的网。 每一个三角形定义了一个覆盖该三角形内格网结点的面。三角形的倾斜和标高由定义这个三角形的三个原始数据点确定。给定三角形内的全部结点都要受到该三角形的表面的限制。因为原始数据点被用来定义各个三角形,所以你的数据是很受到尊重的。
8、自然邻点插值法
自然邻点插值法(NaturalNeighbor)是Surfer7.0才有的网格化新方法。自然邻点插值法广泛应用于一些研究领域中。其基本原理是对于一组泰森(Thiessen)多边形,当在数据集中加入一个新的数据点(目标)时,就会修改这些泰森多边形,而使用邻点的权重平均值将决定待插点的权重,待插点的权重和目标泰森多边形成比例[9]。实际上,在这些多边形中,有一些多边形的尺寸将缩小,并且没有一个多边形的大小会增加。同时,自然邻点插值法在数据点凸起的位置并不外推等值线(如泰森多边形的轮廓线)。
9、最近邻插值算法
最近邻点插值(NearestNeighbor)算法又称泰森多边形方法,泰森多边形(Thiesen,又叫Dirichlet或Voronoi多边形)分析法是荷兰气象学家A.H.Thiessen提出的一种分析方法。最初用于从离散分布气象站的降雨量数据中计算平均降雨量,现在GIS和地理分析中经常采用泰森多边形进行快速的赋值[2]。实际上,最近邻点插值的一个隐含的假设条件是任一网格点p(x,y)的属性值都使用距它最近的位置点的属性值,用每一个网格节点的最邻点值作为待的节点值[3]。当数据已经是均匀间隔分布,要先将数据转换为SURFER的网格文件,可以应用最近邻点插值法;或者在一个文件中,数据紧密完整,只有少数点没有取值,可用最近邻点插值法来填充无值的数据点。有时需要排除网格文件中的无值数据的区域,在搜索椭圆(SearchEllipse)设置一个值,对无数据区域赋予该网格文件里的空白值。设置的搜索半径的大小要小于该网格文件数据值之间的距离,所有的无数据网格节点都被赋予空白值。在使用最近邻点插值网格化法,将一个规则间隔的XYZ数据转换为一个网格文件时,可设置网格间隔和XYZ数据的数据点之间的间距相等。最近邻点插值网格化法没有选项,它是均质且无变化的,对均匀间隔的数据进行插值很有用,同时,它对填充无值数据的区域很有效。
多媒体
2018-07-02 08:39:08
「深度学习福利」大神带你进阶工程师,立即查看>>>
编者注
由于需要使用Maya编辑角色骨骼,在编辑文档的笔记本安装Maya,发现显示模型区域Viewport 2.0无法正确显示内容。则查询网络。
修正方法
windows -> setting preferences ->Preferences, under Display -> Viewport 2.0 I have changed Rendering engine to DirectX 11
附录
Maya 2017 (Update 3) Viewport 2.0 Issues on 4k display
多媒体
2018-06-27 21:50:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
话不多说,上代码
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import javax.swing.JFrame;
@SuppressWarnings("serial")
class Cardioid extends JFrame {
// 定义窗口大小
private static final int WIDTH = 480;
private static final int HEIGHT = 600;
// 获取屏幕大小
private static int WINDOW_WIDTH = Toolkit.getDefaultToolkit()
.getScreenSize().width;
private static int WINDOW_HEIGHT = Toolkit.getDefaultToolkit()
.getScreenSize().height;
// 构造函数
public Cardioid() {
// 设置窗口标题
super("❤形线");
// 设置背景色
this.setBackground(Color.BLACK);
// 设置窗口位置
this.setLocation((WINDOW_WIDTH - WIDTH) / 2,
(WINDOW_HEIGHT - HEIGHT) / 2);
// 设置窗口大小
this.setSize(WIDTH, HEIGHT);
// 设置窗口布局
this.setLayout(getLayout());
// 设置窗口可见
this.setVisible(true);
// 设置窗口默认关闭方式
this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
}
public void paint(Graphics g) {
double x, y, r; // 横纵坐标及半径
Image image = this.createImage(WIDTH, HEIGHT);
Graphics pic = image.getGraphics();
// 绘制图形
for (int i = 0; i < 100; i++) {
for (int j = 0; j < 100; j++) {
r = Math.PI / 45 + Math.PI / 45 * i
* (1 - Math.sin(Math.PI / 45 * j)) * 18;
x = r * Math.cos(Math.PI / 45 * j) * Math.sin(Math.PI / 45 * i)
+ WIDTH / 2;
y = -r * Math.sin(Math.PI / 45 * j) + HEIGHT / 2;
pic.setColor(Color.MAGENTA);
pic.fillOval((int) x, (int) y, 2, 2);
}
// 生成图片
g.drawImage(image, 0, 0, this);
}
}
public static void main(String[] args) {
new Cardioid();
}
}


--------------------------------------------------------
还有一个简单的
import java.awt.Graphics;
import javax.swing.JApplet;
/**
*
* @author LuckyPig
*/
public class Love extends JApplet {
public void paint(Graphics page)
{
double x, y, t;
page.drawString("I love you !", 275, 60);

for(y = 2; y >= -2; y -= 0.005)
for(x = -200; x <= 200; x += 0.005)
{
t = x*x + y*y - 1;
if(t*t*t + x*x*y*y*y <= 0)
page.drawOval((int)(x*200+300), (int)(y*200+300), 0, 0);
}

}
}
多媒体
2018-06-27 15:50:00
「深度学习福利」大神带你进阶工程师,立即查看>>>

前言:觉得对自己有帮助别忘了也给其他小伙伴一起分享哦!

问题:如果main方法被声明为private会怎样?

答案:能正常编译,但运行的时候会提示”main方法不是public的”。

2.问题:Java里的传引用和传值的区别是什么?

答案:传引用是指传递的是地址而不是值本身,传值则是传递值的一份拷贝。

3.问题:如果要重写一个对象的equals方法,还要考虑什么?

答案:hashCode。

4.问题:Java的”一次编写,处处运行”是如何实现的?

答案:Java程序会被编译成字节码组成的class文件,这些字节码可以运行在任何平台,因此Java是平台独立的。

5.问题:说明一下public static void main(String args[])这段声明里每个关键字的作用

答案:public: main方法是Java程序运行时调用的第一个方法,因此它必须对Java环境可见。所以可见性设置为pulic.

static: Java平台调用这个方法时不会创建这个类的一个实例,因此这个方法必须声明为static。

void: main方法没有返回值。

String是命令行传进参数的类型,args是指命令行传进的字符串数组。
如果你想学习Java可以来这个Q群,首先是532,中间是259,最后是952,里面可以学习和交流,也有资料可以下载。

6.问题:==与equals的区别

答案:==比较两个对象在内存里是不是同一个对象,就是说在内存里的存储位置一致。两个String对象存储的值是一样的,但有可能在内存里存储在不同的地方 .

==比较的是引用而equals方法比较的是内容。public boolean equals(Object obj) 这个方法是由Object对象提供的,可以由子类进行重写。默认的实现只有当对象和自身进行比较时才会返回true,这个时候和==是等价的。String, BitSet, Date, 和File都对equals方法进行了重写,对两个String对象 而言,值相等意味着它们包含同样的字符序列。对于基本类型的包装类来说,值相等意味着对应的基本类型的值一样。

publicclassEqualsTest{

publicstaticvoidmain(String[]args){

Strings1="abc";

Strings2=s1;

Strings5="abc";

Strings3=newString("abc");

Strings4=newString("abc");

System.out.println(" == comparison:"+(s1==s5));

System.out.println(" == comparison:"+(s1==s2));

System.out.println("Using equals method:"+s1.equals(s2));

System.out.println(" == comparison:"+s3==s4);

System.out.println("Using equals method : "+s3.equals(s4));

}

}

结果:

==comparison:true

==comparison:true

Usingequals method:true

false

Usingequals method:true

7.问题:如果去掉了main方法的static修饰符会怎样?

答案:程序能正常编译。运行时会抛NoSuchMethodError异常。

8.问题:为什么oracle type4驱动被称作瘦驱动?

答案:oracle提供了一个type 4 JDBC驱动,被称为瘦驱动。这个驱动包含了一个oracle自己完全用Java实现的一个TCP/IP的Net8的实现,因此它是平台独立的,可以在运行时由浏览器下载,不依赖任何客户端 的oracle实现。客户端连接字符串用的是TCP/IP的地址端口,而不是数据库名的tnsname。

9.问题:介绍一下finalize方法

答案: final: 常量声明。 finally: 处理异常。 finalize: 帮助进行垃圾回收。

接口里声明的变量默认是final的。final类无法继承,也就是没有子类。这么做是出于基础类型的安全考虑,比如String和Integer。这样也使得编译器进行一些优化,更容易保证线程的安全性。final方法无法重写。final变量的值不能改变。finalize()方法在一个对象被销毁和回收前会被调用。finally,通常用于异常处理,不管有没有异常被抛出都会执行到。比如,关闭连接通常放到finally块中完成。

10.问题:什么是Java API?

答案:Java API是大量软件组件的集合,它们提供了大量有用的功能,比如GUI组件。

11-20

11.问题:GregorianCalendar类是什么东西?

答案:GregorianCalendar提供了西方传统日历的支持。

12.问题:ResourceBundle类是什么?

答案:ResourceBundle用来存储指定语言环境的资源,应用程序可以根据运行时的语言环境来加载这些资源,从而提供不同语言的展示。

13.问题:为什么Java里没有全局变量?

答案:全局变量是全局可见的,Java不支持全局可见的变量,因为:全局变量破坏了引用透明性原则。全局变量导致了命名空间的冲突。

14.问题:如何将String类型转化成Number类型?

答案:Integer类的valueOf方法可以将String转成Number。下面是代码示例:

StringnumString=“1000″;

intid=Integer.valueOf(numString).intValue();

15.问题:SimpleTimeZone类是什么?

答案:SimpleTimeZone提供公历日期支持。

16.问题:while循环和do循环有什么不同?

答案:while结构在循环的开始判断下一个迭代是否应该继续。do/while结构在循环的结尾来判断是否将继续下一轮迭代。do结构至少会执行一次循环体。

17.问题:Locale类是什么?

答案:Locale类用来根据语言环境来动态调整程序的输出。

18.问题:面向对象编程的原则是什么?

答案:主要有三点,多态,继承和封装。

19.问题:介绍下继承的原则

答案:继承使得一个对象可以获取另一个对象的属性。使用继承可以让已经测试完备的功能得以复用,并且可以一次修改,所有继承的地方都同时生效。

20.问题:什么是隐式的类型转化?

答案:隐式的类型转化就是简单的一个类型赋值给另一个类型,没有显式的告诉编译器发生了转化。并不是所有的类型都支持隐式的类型转化。

代码示例:

inti=1000;

longj=i;//Implicit casting

21-30

21.问题:sizeof是Java的关键字吗?

答案:不是。

22.问题:native方法是什么?

答案:native方法是非Java代码实现的方法。

23.问题:在System.out.println()里面,System, out, println分别是什么?

答案:System是系统提供的预定义的final类,out是一个PrintStream对象,println是out对象里面一个重载的方法。

24.问题:封装,继承和多态是什么?

答案:简单来说,多态是指一个名字多种实现。多态使得一个实体通过一个通用的方式来实现不同的操作。具体的操作是由实际的实现来决定的。

多态在Java里有三种表现方式:方法重载通过继承实现方法重写通过Java接口进行方法重写。

25.问题:显式的类型转化是什么?

答案:显式的类型转化是明确告诉了编译器来进行对象的转化。

代码示例:

longi=700.20;

intj=(int)i;//Explicit casting

26.问题:什么是Java虚拟机?

答案:Java虚拟机是能移植到不同硬件平台上的软件系统。

27.问题:类型向下转换是什么?

答案:向下转换是指由一个通用类型转换成一个具体的类型,在继承结构上向下进行。

28.问题:Java的访问修饰符是什么?

答案:访问权限修饰符是表明类成员的访问权限类型的关键字。使用这些关键字来限定程序的方法或者变量的访问权限。它们包含:

public: 所有类都可以访问 protected: 同一个包内以及所有子类都可以访问 private: 只有归属的类才能访问默认: 归属类及相同包下的子类可以访问

29.问题:所有类的父类是什么?

答案:Object.

30.问题:Java的基本类型有哪些?

答案:byte,char, short, int, long, float, double, boolean。

31-40

31.问题:静态类型有什么特点?

答案:静态变量是和类绑定到一起的,而不是类的实例对象。每一个实例对象都共享同样一份静态变量。也就是说,一个类的静态变量只有一份,不管它有多少个对象。类变量或者说静态变量是通过static这个关键字来声明的。类变量通常被用作常量。静态变量通常通过类名字来进行访问。当程序运行的时候这个变量就会创建直到程序结束后才会被销毁。类变量的作用域和实例变量是一样的。它的初始值和成员变量也是一样的,当变量没被初始化的时候根据它的数据类型,会有一个默认值。类似的,静态方法是属于类的方法,而不是类对象,它的调用并不作用于类对象,也不需要创建任何的类实例。静态方法本身就是final的,因为重写只会发生在类实例上,静态方法是和类绑定在一起的,不是对象。父类的静态方法会被子类的静态方法屏蔽,只要原来方法没有声明为final。非静态方法不能重写静态方法,也就是说,你不能在子类中把一个静态方法改成实例方法。

非静态变量在每一个对象实例上都有单独的一份值。

32.问题:&操作符和&&操作符有什么区别?

答案:当一个&表达式在求值的时候,两个操作数都会被求值,&&更像是一个操作符的快捷方式。当一个&&表达式求值的时候,先计算第一个操作数,如果它返回true才会计算第二个操作数。如果第一个操作数取值为fale,第二个操作数就不会被求值。

33.问题:Java是如何处理整型的溢出和下溢的?

答案:Java根据类型的大小,将计算结果中的对应低阶字节存储到对应的值里面。

34.问题:public static void写成static public void会怎样?

答案:程序正常编译及运行。

问题,声明变量和定义变量有什么不同?

答案:声明变量我们只提供变量的类型和名字,并没有进行初始化。定义包括声明和初始化两个阶段String s;只是变量声明,String s = new String(“bob”); 或者String s = “bob”;是变量定义。

35.问题:Java支持哪种参数传递类型?

答案:Java参数都是进行传值。对于对象而言,传递的值是对象的引用,也就是说原始引用和参数引用的那个拷贝,都是指向同一个对象。

36.问题:对象封装的原则是什么?

答案:封装是将数据及操作数据的代码绑定到一个独立的单元。这样保障了数据的安全,防止外部代码的错误使用。对象允许程序和数据进行封装,以减少潜在的干涉。对封装的另一个理解是作为数据及代码的保护层,防止保护层外代码的随意访问。

37.问题:你怎么理解变量?

答案:变量是一块命名的内存区域,以便程序进行访问。变量用来存储数据,随着程序的执行,存储的数据也可能跟着改变。

38.问题:数值提升是什么?

答案:数值提升是指数据从一个较小的数据类型转换成为一个更大的数据类型,以便进行整型或者浮点型运算。在数值提升的过程中,byte,char,short值会被转化成int类型。需要的时候int类型也可能被提升成long。long和float则有可能会被转换成double类型。

39.问题:Java的类型转化是什么?

答案:从一个数据类型转换成另一个数据类型叫做类型转换。Java有两种类型转换的方式,一个是显式的类型转换,一个是隐式的。

40.问题:main方法的参数里面,字符串数组的第一个参数是什么?

答案:数组是空的,没有任何元素。不像C或者C++,第一个元素默认是程序名。如果命令行没有提供任何参数的话,main方法中的String数组为空,但不是null。

41-50

41.问题:怎么判断数组是null还是为空?

答案:输出array.length的值,如果是0,说明数组为空。如果是null的话,会抛出空指针异常。

42.问题:程序中可以允许多个类同时拥有都有main方法吗?

答案:可以。当程序运行的时候,我们会指定运行的类名。JVM只会在你指定的类中查找main方法。因此多个类拥有main方法并不存在命名冲突的问题。

43.问题:静态变量在什么时候加载?编译期还是运行期?静态代码块加载的时机呢?

答案:当类加载器将类加载到JVM中的时候就会创建静态变量,这跟对象是否创建无关。静态变量加载的时候就会分配内存空间。静态代码块的代码只会在类第一次初始化的时候执行一次。一个类可以有多个静态代码块,它并不是类的成员,也没有返回值,并且不能直接调用。静态代码块不能包含this或者super,它们通常被用初始化静态变量。

44.问题:一个类能拥有多个main方法吗?

答案:可以,但只能有一个main方法拥有以下签名:

public static void main(String[] args) {}

否则程序将无法通过编译。编译器会警告你main方法已经存在。

45.问题:简单的介绍下JVM是如何工作的?

答案:JVM是一台抽象的计算机,就像真实的计算机那样,它们会先将.java文件编译成.class文件(.class文件就是字节码文件),然后用它的解释器来加载字节码。

46.问题:如果原地交换两个变量的值?

答案:先把两个值相加赋值给第一个变量,然后用得到的结果减去第二个变量,赋值给第二个变量。再用第一个变量减去第二个变量,同时赋值给第一个变量。代码如下:

inta=5,b=10;a=a+b;b=a-b;a=a-b;

使用异或操作也可以交换。第一个方法还可能会引起溢出。异或的方法如下: int a=5,b=10;

a=a+b;b=a-b;a=a-b;

inta=5;intb=10;

a=a^b;

b=a^b;

a=a^b;

47.问题:什么是数据的封装?

答案:数据封装的一种方式是在类中创建set和get方法来访问对象的数据变量。一般来说变量是private的,而get和set方法是public的。封装还可以用来在存储数据时进行数据验证,或者对数据进行计算,或者用作自省(比如在struts中使用javabean)。把数据和功能封装到一个独立的结构中称为数据封装。封装其实就是把数据和关联的操作方法封装到一个独立的单元中,这样使用关联的这些方法才能对数据进行访问操作。封装提供的是数据安全性,它其实就是一种隐藏数据的方式。

48.问题:什么是反射API?它是如何实现的?

答案:反射是指在运行时能查看一个类的状态及特征,并能进行动态管理的功能。这些功能是通过一些内建类的反射API提供的,比如Class,Method,Field, Constructors等。使用的例子:使用Java反射API的getName方法可以获取到类名。

49.问题:JVM自身会维护缓存吗,是不是在堆中进行对象分配,操作系统的堆还是JVM自己管理的堆?为什么?
>>>>阅读全文
多媒体
2018-06-22 14:48:00
「深度学习福利」大神带你进阶工程师,立即查看>>> ffmpeg -i input.mp3 -c:a libfdk_aac -vbr 5 output.m4a 参考: FFmpeg command to convert MP3 to AAC
多媒体
2018-06-14 10:06:00
「深度学习福利」大神带你进阶工程师,立即查看>>> ffmpeg -f concat -safe 0 -i <(printf "file '$PWD/my02.m4a'\nfile '$PWD/my05.m4a'\n") -c copy output.m4a
这里的 -i 参数,使用的是shell的管道方式输入。即这里的 <(printf "file '$PWD/my02.m4a'\nfile '$PWD/my05.m4a'\n") ,其实是本来是一个文件里面的内容,如下: file 'path/my02.m4a' file 'path/my05.m4a'
这样是把my02.m4a和my05.m4a合并为output.m4a。 ffmpeg有两种方式,可以把相同编码文件类型的文件连接起来: the concat ''demuxer'' the concat ''protocol'' 这里使用的是第一种方式。 参考: Concatenate /dev/fd/63: Operation not permitted 使用ffmpeg合并(连接)文件
多媒体
2018-06-14 10:06:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
背景
最近想用airplay投影mkv视频文件到电视机上面播放,结果投影过去的视频,没有自动加载字幕。只要选择把ass字幕文件合并到mkv视频文件里面去。中途,还是使用 mkvtoolnix 进行mkv和ass文件的合并,但是合并出来仍会出现上面的情况。最后,就考虑自己手动使用ffmpeg进行合并。
步骤
首先,准备你的mkv视频文件和ass字幕文件,然后,确认自己的ffmpeg已经是最新版本,如下: $ ffmpeg -version ffmpeg version 3.4.1 Copyright (c) 2000-2017 the FFmpeg developers built with Apple LLVM version 9.0.0 (clang-900.0.39.2) configuration: --prefix=/usr/local/Cellar/ffmpeg/3.4.1 --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-gpl --enable-ffplay --enable-frei0r --enable-libass --enable-libfdk-aac --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopus --enable-librtmp --enable-libspeex --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libxvid --enable-opencl --enable-videotoolbox --disable-lzma --enable-libopenjpeg --disable-decoder=jpeg2000 --extra-cflags=-I/usr/local/Cellar/openjpeg/2.3.0/include/openjpeg-2.3 --enable-nonfree libavutil 55. 78.100 / 55. 78.100 libavcodec 57.107.100 / 57.107.100 libavformat 57. 83.100 / 57. 83.100 libavdevice 57. 10.100 / 57. 10.100 libavfilter 6.107.100 / 6.107.100 libavresample 3. 7. 0 / 3. 7. 0 libswscale 4. 8.100 / 4. 8.100 libswresample 2. 9.100 / 2. 9.100 libpostproc 54. 7.100 / 54. 7.100
然后,进行mkv和ass文件的合并: # 废弃用法: # ffmpeg -i my.mkv -vf "ass=my.ass" my2.mkv # 修改后的用法: ffmpeg -i my.mkv -vf subtitles=my.ass my2.mkv
这里的my.mkv就是自己视频文件,my.ass就是自己的字幕文件。合成出来的文件就是my2.mkv。等待一段时间,这里可能需要花费一段时间,这主要和自身的硬件环境,和视频文件大小有关。
感受
如果ass字幕文件对于ffmpeg不支持的话,可能需要在安装ffmpeg的时候,需要明确指出对ass的支持。 ffmpeg真是一个处理各种音视频利器,各种好用。之前就有谈到过关于ffmpeg的文章: ffmpeg中合并音频文件 ffmpeg将mp3转m4a文件 参考: HowToBurnSubtitlesIntoVideo 使用ffmpeg进行简单的视频编辑之 6. 字幕(subtitle)
多媒体
2018-06-14 10:06:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
gif 动态图应该说是网络上一项重大的发明,gif 能截取某些精彩的动画部分,并且节省体积,自动播放,很受网友的喜爱,不管是斗图还是发表内容,都是不可或缺的。当你看到网上一些有趣的 gif 动态图,你是不是好奇都是怎么制作的,你想不想自己制作喜欢的动态图呢,这次草娘就给大家分享介绍一款很好用的 gif 录制小工具,名字叫做 gifcam。gifcam 小工具本身就很轻巧,只有大约 700K,但是却涵盖了一系列的功能,可以编辑,可添加文字,可暂停继续录制,窗口可任意转换大小,可选择帧率。。。。有了 gifcam 你就可以轻松制作动态图片啦。
rec 按钮可开始录制、暂停录制、继续录制,无缝对接。
Frame 按钮是边框。
Edit 按钮是编辑你所录制的 gif 图片,可删除帧数,可添加文字水印,选择颜色等等。
Save 按钮是保存。并且可以保存成不同的类型。
使用这三个按钮就可以轻松制作,按钮下来列表还有很多功能,可以摸索一下哟。
在录制的过程中,你可以随意的改变窗口大小、位置。不过在改变过程中软件是会暂停录制的,待你释放窗口后才会继续。
如果录制范围内内容无变化,则不增加新帧而只是增加延时,这样可以有效减小文件尺寸
录制停止后,可再次按下 “录制” 继续录制
按下 “单帧” 可以录制单帧:如内容有变化,则形成新的帧;如果无变化,则该帧增加 0.1 秒延时
编辑状态下可查看每一帧,可以删除帧、增加减少当前帧延时
保存 GIF 图片时,你可以根据需要设置不同的色深和帧率(fps)
可以全屏录制,可增加文字,可显示/隐藏鼠标指针
软件官网下载
百度云 -dti0
多媒体
2018-06-13 22:41:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
/data/local/GraphicsMagick/bin/gm convert /data/local/fastdfs_data/storage/images/data/data/00/81/Cns0v1q9oceAa-fBAAB_DXhVYig080.pdf /data/local/fastdfs_data/storage/images/data/data/00/81/Cns0v1q9oceAa-fBAAB_DXhVYig080.jpg
依赖:
ghostscript-9.18.tar.gz
多媒体
2018-06-13 21:27:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
Adobe 的品牌设计团队负责为公司旗下桌面端、移动端和 web 端的产品进行品牌设计。品牌元素的形式很多,可以是两个字母的产品 logo,应用启动界面,产品里的图标等等。
一个很常见却常被忽视的品牌元素就是文件类型图标。文件类型是指用某种特定应用可以创建的特定格式的文件,比如用 Word 制作的 .DOC 文件。文件类型图标与文件类型相关,就是你在存储或打开文件时显示在屏幕上的那个图标。
在今秋发布的最新版本 Creative Cloud 中,用户将发现我们所有的文件类型图标焕然一新!在这篇文章,我将详细阐述这次对文件类型图标系统重新设计背后的思考过程,并且和你分享在升级一个大型产品线的品牌形象系统时所遇到的挑战,以及随之而生的一些见解。
阅读信息:
阅读时间:约9分钟 作者信息: Anny Chen 原文地址: Medium
发现问题
许多消费者并没意识到 Adobe 的三个平台:Creative Cloud,Document Cloud,和 Experience Cloud 拥有超过100个产品和服务。 这意味着设计品牌形象系统时的一个小疏忽可能给整个品牌带来成百上千的问题。
当我们谈及文件类型图标时,人们往往只会想到最主要的那些,比如: Photoshop 的 .PSD Illustrato 的 .AI InDesign 的 .INDD
但是,大多数我们的产品可以导入导出大量的辅助文件类型,比如 Photoshop 就涉及超过120种不同的文件类型。
为了给不同的操作系统优化,我们需要制作十种不同尺寸的 .PGN 文件类型图标,并打包成 .ICNS 格式(Mac)和 .ICO 格式(Windows)文件。 当我们计算制作每个文件类型图标的尺寸和格式时,发现要在每个新版本发布周期修改和管理超过7000份素材。
由于 Adobe CC 产品线在过去四年扩张得很快,以当前的工作流程去创建和维护这些文件类型图标已经不太可能,工作量太大了。
第一步:盘点和调查
在我们开始重新设计整个系统之前,我们必须查一下当前产品里在用什么文件类型图标。我们访问了各个产品团队,借此盘点现存所有的文件类型图标。
处处都能发现失调的地方,大都是下列两个原因导致: 不同的团队主导各自的产品线,并没有规范作为执行准则。 新的文件类型随着新产品不断上线,很多被当作一次性设计。
根据这次盘点中得到的信息,我们做了一张关于现存文件类型结构的鸟瞰图。
首先,我们以产品线组织了文件类型图标,并把不同应用之间重复出现的图标连接起来。借此发现并并删去重复的图标。结束这项工作时,辅助文件类型图标的数量减少到65%。
以产品线组织的旧的文件类型结构片段。
接下来,我们以功能划分文件类型,比如“图像”,“音频”,“代码”或“3D”。通常来说,一个文件类型图标会是一个暗示主要功能的比喻(比如,一个 .HTML 文件将会用来暗示他的功能和代码有关)。
以功能组织的旧的文件类型结构片段。
我们注意到某些文件类型使用了不同版本的相同比喻,还有一些文件类型使用了自定义的比喻,其实可以用更统一的比喻进行替换。我们创建了伞式结构的文件类型,借此为整个产品线安排统一的比喻。做了这个后,我们减少超过一半的比喻数量。
旧的辅助文件类型比喻图标片段。
第二步,草稿和设计
一旦我们对老的文件类型图标系统有广泛的了解,就开始建立新系统的基础规则: 只有主要文件类型才可以和产品 logo 的颜色有关联。比如说 .PSD 文件是蓝色的而 .AI 文件是橘色的。 为那些会被很多应用用到的辅助文件类型创建一个中性色盘。比如说,为 Photoshop 和 Illustrator 创建同样的 .PNG 文件类型图标,而不是各自有一个和品牌色相关的单独的图标。 创造一个主要的知识库来存放文件类型比喻,借此确保图标之间的关联性,并避免为边缘案例定制图标。
旧的文件类型图标模块拆解。
我们遵循上述规则,开始给新的图标打草稿。
项目早期草图的快照。
进行这次重构的主要目的之一是简化图标上的元素,并不丢失重要的信息。我们丢掉了标签,并把文件类型放到了图标的底部。我们也去掉了页面的折角来让设计变得扁平化,让视觉语言更现代化。
Adobe 文件类型图标进化图。
另一个重要的目的是和 Adobe 的新 UI 设计语言 —— Spectrum 一致 —— 正在我们的产品中逐步推出。经过这次努力,我们把文件类型图标的四角做圆,并开始建立一个素材库,使用 Spectrum 规范中现有的比喻,设计新的与其图标风格相一致的素材。
Adobe Spectrum 图标库快照。
最后,我们给图标的描边使用了亮色,以跟现在的产品 logo 一致。这种改变不仅让视觉系统变更紧密,而且新图标在深色界面下看着更清晰。相比之下,旧的图标则会和背景融成一体,难以分辨。
在深色界面下颜色对比度研究。
第三步,迭代和定稿
既然我们定下了设计方向,我们开始在一些场景下测试新文件类型图标。在最初的测试中,我们调查了不同操作系统中、我们自己的产品中所有会出现文件类型图标的地方。我们也查看了在不同尺寸和分辨率下图标出现的情况。
在 Mac 和 Windows 操作系统的桌面上,我们必须统计不同缩放因子的下的图标情况(最小16像素,最大512像素)。还有亮色、深色界面下的情况,比如说 Mac 电脑上的“Recent Items”和“Spotlight Search” ,然后我们检查了自己产品中文件类型图标的出现情况,比如说素材板,媒体文件浏览框,和当你第一次启动应用时的欢迎界面。
这一举动马上让我们陷入了一个深渊,里面遍布藏在各个人迹罕至的角落里的文件类型图标。但是这么做很有价值。我们需要更全面的投身这个任务。
文件类型图标出现的各种场景。
最后一步是检查移动端和 web 端服务里用户界面中文件类型图标的使用情况,比如 Adobe Acrobat 和 Creative Cloud Libraries。由于这些服务由不同设计团队负责,如果我们计划翻新整个文件类型设计系统,就需要和很多人合作。 我们对最终的输出很骄傲,因为新的设计语言更简洁,更紧凑,并且代表了 Adobe 形象识别系统的更新。
Adobe 的新文件类型图标系统。
第四步,设计一个新流程
我们利用 AI 里的脚本功能创建了一个工作流,可以一键生成和导出 .PNG 文件。这个工作流给我们节省了很多时间。
我们也需要一个更好的方法来把这些栅格 .PNG 图片放到 .ICNS (Mac) 和 .ICO (Windows)中。我们以前使用 IconFactory 的 IconBuilder 插件。但是我们想要一个更灵活的解决方式满足需求:拖入任何一组 .PNG 文件,自动输出正确尺寸的 .ICO 和 .ICNS 文件。
在找了一圈三方编译器以后,我们决定最好还是为这个需求定制,和开发人员做一个内部 app。他们开发了一个超赞的工具,我们称之为 Icon 队长,我们用它来生成和打包所有的新文件类型图标。(还在内测,我们的工程师希望在 GitHub 上分享给 Adobe 开发社区里的其他开发者使用!)
Adobe 内部的 .ICO 和 .ICNS 编译器。
第五步,落地
我们仍在这一阶段,并可能持续很长时间。每次我们发布一个新版本的 Creative Cloud,我们都会和许多团队的产品经理和工程师碰需求,以保证我们设计的输出质量。
落地其实是一个反复的过程,需要经历和各种团队反反复复的沟通,安装很多版本来测试素材,找到和解决不可避免的 bug,还有管理很多的产品发布截止日期。
我们的产品构建于不同的代码基础,意味着同样的东西放在不同的平台会产生不同的问题。质量保障和加强品牌设计规范可能是我们团队中最无聊的任务之一,但这对于保持和提升设计系统很重要。
操作系统中 Adobe 新文件类型图标。
合适的支点可以让杠杆撬动地球。
我们的团队中常用修建盆栽来比喻做的工作。 提升一个包含数百产品线的设计系统依赖不断的小改变。我们四处修剪,让树在慢慢时光中长成我们希望的样子。
虽然有时会在细节中迷失,但我们在过程中学习的所有东西将帮助我们进行之后的次次迭代。
多媒体
2018-06-13 10:21:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
本文原文由声网WebRTC技术专家毛玉杰分享。
1、前言
有人说 2017 年是 WebRTC 的转折之年,2018 年将是 WebRTC 的爆发之年,这并非没有根据。就在去年(2017年), WebRTC 1.0 标准草案 出炉(实际上 WebRTC标准草案的早期版本 早在2011年就已经发布,WebRTC并非一夜之间就出现的技术),并将于今年正式发布。与此同时,越来越多的浏览器和厂商都开始对它进行广泛的支持,WebRTC 即将成为互联网的基础设施了,或许门槛如此之高的实时音视频技术终有白菜化的那一天。
补充: WebRTC标准草案的版本演进历史,请 点击进入 。
学习交流: - 即时通讯开发交流3群: 185926912 [推荐]
- 移动端IM开发入门文章:《 新手入门一篇就够:从零开发移动端IM 》
(本文同步发布于: http://www.52im.net/thread-1631-1-1.html )
2、相关文章 《 开源实时音视频技术WebRTC的现状 》
《 简述开源实时音视频技术WebRTC的优缺点 》
《 访谈WebRTC标准之父:WebRTC的过去、现在和未来 》
《 良心分享:WebRTC 零基础开发者教程(中文)[附件下载] 》
《 WebRTC实时音视频技术的整体架构介绍 》
《 新手入门:到底什么是WebRTC服务器,以及它是如何联接通话的? 》
《 WebRTC实时音视频技术基础:基本架构和协议栈 》
《 [观点] WebRTC应该选择H.264视频编码的四大理由 》
《 基于开源WebRTC开发实时音视频靠谱吗?第3方SDK有哪些? 》
《 开源实时音视频技术WebRTC中RTP/RTCP数据传输协议的应用 》
《 实时通信RTC技术栈之:视频编解码 》
《 开源实时音视频技术WebRTC在Windows下的简明编译教程 》
《 网页端实时音视频技术WebRTC:看起来很美,但离生产应用还有多少坑要填? 》
3、实时通信技术的广阔前景
根据腾讯全球合作伙伴大会上发布的《 2017 年微信数据报告 》显示,截止到 2017 年 9 月,微信日成功通话次数 2.05 次,月人均通话时长 139 分钟,月人均通话次数 19 次。通过这些数据我们可以看到,微信视频通话的出现,已潜移默化地改变了人与人通信的方式。
而回望三大运营商的数据,语音通话量在 2015 年首次出现了负增长,可以看到互联网 OTT 应用对传统语音通话业务的冲击有多强烈。正是由于这些日益完善的基础设施,更快的智能手机,更快的网络,更丰富的使用场景,实时通信的需求越来越强烈。
从 2015 开始不断涌现出的互动直播、狼人杀、抓娃娃、直播答题、线上 KTV 等创新,将常见的线下场景转至线上,也足以作为实时音视频通信风头正劲的有力佐证。
越来越多的创业者都在思考如何将线下互动的场景搬到线上,从而打造下一个风靡全民爆款的应用。
说到实时通信,不得不提到 WebRTC,WebRTC 全名为 Web Real Time Communication,从 Web 这个词就可以看出,最初这项技术是为浏览器量身打造用以实时音视频能力而准备的。
但其实 WebRTC 在不同场景下包含不同的含义,它既可以代表 Google 开源的 WebRTC 项目,又可以代表 W3C 工作组制定的 WebRTC 标准,也可以代表浏览器中的 WebRTC 接口,我们将他们统称为 WebRTC 技术。当前具有实时音视频能力的应用或者服务,或多或少都使用了 WebRTC 技术,当然所有的这些背后都离不开 Google 开源的 WebRTC 项目,下面我们扒一扒 WebRTC 背后的故事。
4、WebRTC是什么来头?
说到 WebRTC,我们不得不提到 Gobal IP Solutions,简称 GIPS。这是一家 1990 年成立于瑞典斯德哥尔摩的 VoIP 软件开发商,提供了可以说是世界上最好的语音引擎。相关介绍详见《 访谈WebRTC标准之父:WebRTC的过去、现在和未来 》。
Skype、腾讯 QQ、WebEx、Vidyo 等都使用了它的音频处理引擎,包含了受专利保护的回声消除算法,适应网络抖动和丢包的低延迟算法,以及先进的音频编解码器。
Google 在 Gtalk 中也使用了 GIPS 的授权。Google 在 2011 年收购了 GIPS,并将其源代码开源,加上在 2010 年收购的 On2 获取到的 VPx 系列视频编解码器(详见《 即时通讯音视频开发(十七):视频编码H.264、VP8的前世今生 》),WebRTC 开源项目应运而生,即 GIPS 音视频引擎 + 替换掉 H.264 的 VPx 视频编解码器。
在此之后,Google 又将在 Gtalk 中用于 P2P 打洞的开源项目 libjingle 融合进了 WebRTC。所以目前 WebRTC 提供了在 Web、iOS、Android、Mac、Windows、Linux 在内的所有平台的 API,保证了 API 在所有平台的一致性。
基于这些先进技术,使用 WebRTC 的为我们带来的好处主要有以下几个方面:
免费的使用 GIPS 先进的音视频引擎,在此之前都需要付费授权;
由于音视频传输是基于点对点传输的,所以实现简单的 1 对 1 通话场景,需要较少的服务器资源,借助免费的 STUN/TURN 服务器可以大大节约成本开销;
开发 Web 版本的应用非常方便,使用简单的 JS 接口,无需安装任何插件,即可实现音视频互通。
5、WebRTC 标准掀起的影响
2017 年 11 月 2 日,在经历了 6 年的时间之后, W3C WebRTC 1.0 草案 正式定稿。同样也是在 2017 年,Microsoft Edge 与 Apple Safari 也纷纷宣称了在其最新的版本里支持 WebRTC 1.0 标准 API。
虽然不同浏览器厂商在某些实现细节方面有所差别,比如 Safari 只支持 H.264,不同的 SDP 描述格式等等,但除了 IE 之外,所有主流浏览器 Google Chrome、Mozilla Firefox、Apple Safari、Microsoft Edge 都已经支持 WebRTC 1.0,所有浏览器之间无插件化的音视频互通已经成为一种可能。
即时通讯网注: 为什么苹果等商业公司坚持H.264标准?这其实是以苹果为代表的商业公司或联盟和谷歌之间的音视频编码标准之争,详见《 即时通讯音视频开发(十七):视频编码H.264、VP8的前世今生 》,但业界普遍认为至少目前看来H.264标准(下一代被称为H.265)各方面要优于VP8(下一代是VP9),具体请见《 即时通讯音视频开发(十三):实时视频编码H.264的特点与优势 》。
越来越多的终端设备上,无需借助任何插件或者 native 应用,通过打开网页链接,即可进行高质量的音视频通话,应用开发者也无需关注音视频引擎实现细节,大大节约了开发成本。
6、WebRTC广泛的适用场景
WebRTC 适用的场景可以说是非常广泛,很多行业结合实时通信都可以创造出非常有意思的场景,传统的实时通信应用场景主要是在视频会议、视频面试、VoIP 通话、呼叫中心,产品如 WebEx、Skype 等。
当下比较火的场景主要集中在社交、游戏、体育、电视、相亲类的直播,以及互动连麦、在线教育、在线医疗、金融证券在线开户、智能硬件(如无人机)、智能家居设备如摄像头监控以及智能语音设备。
当然 WebRTC 除了提供音视频传输功能,还有一个容易被忽略的功能就是数据传输。利用点对点的传输机制,一些开发者创造出了诸如 Webtorrent 以及 PeerCDN 这样的不经过服务器的数据传输网络服务。所以 WebRTC 非常适合用来打造实时通信的应用。
而直播作为当下的热点应用,肯定少不了对于 WebRTC 的使用,而这又要提到 rtmp。
7、从 RTMP 到 WebRTC
从应用角度来讲,受到用户使用习惯的改变,越来越多的直播产品都开始加入视频互通的功能。同时,像视频会议、视频核保一类的应用方式也在不断增加。这影响着技术选型的变迁。
RTMP(Real Time Messaging Protocol) 实时消息传送协议是 Adobe Systems 公司为 Flash 播放器和服务器之间音频、视频和数据传输开发的开放协议。随着直播兴起,很多人都将它用在直播上。
在协议方面,rtmp 完全可以满足直播产品的需求,但由于其相对延时较高,不能满足视频互通的产品需求。于是大家很自然地将目光投向 UDP、QUIC(基于 UDP)一类延时更低的网络协议。
在技术框架方面,由于自研一套符合视频互通要求的通信系统相对复杂,不仅涉及网络传输、前端开发、移动端开发,还要解决音视频编解码中复杂的算法优化,对开发者的技术栈要求很高,所以越来越多的人选择 WebRTC。
目前来看,WebRTC 已经获得了越来越多浏览器厂商及相关技术厂商的支持,应用的前景将会更加广阔。
但是受限于 WebRTC 自身的一些缺憾,一般开发者都不是直接完全使用 WebRTC,而是根据实际场景基于 WebRTC 进行二次开发。WebRTC 本身并不是万能钥匙,不可能一套代码以及接口可以解决所有问题。
更多关于RTMP的知识,请见《 基于RTMP数据传输协议的实时流媒体技术研究(论文全文) 》、《 基于RTMP协议的流媒体技术的原理与应用(技术论文)[附件下载] 》。
8、WebRTC很优秀,但当前并非完美
WebRTC 是一个非常优秀的项目,直接拿来使用也存在以下问题,我们简单总结一下:
第一: WebRTC 使用的是对点对传输,虽然节约了服务器资源的开销,但实际使用时也带来了传输质量的问题,比如跨国以及跨运营商网络之间的传输质量往往很难保证,虽然 webRTC 有优秀的端对端质量控制算法,但在错综复杂的网络条件下,表现也很难让人满意;
第二: WebRTC 在移动端的表现也很难让人满意。早期由于缺少对于 H.264 编解码器的支持,使得移动端很长一段时间只能使用 VP8 软件编解码,导致在中低端手机上的表现较差,加上安卓自身碎片化的属性,如果不针对不同机型做适配,很难有统一的用户体验;
第三: WebRTC 是为 1 对 1 通信场景设计的,如果要实现多人的场景,还是需要借助服务端方案。即使当前有很多开源的 webRTC 服务器实现,一个流媒体中转服务器或者混流服务器的部署以及维护也是非常复杂的;
第四: 在 Web 端需要面临不同浏览器之间的兼容性问题。虽然使用 AdapterJS 可以解决不同浏览器之间的接口适配问题,但除此之外依然要面临不同浏览器行为不一致的问题。可以说如果 WebRTC 如果直接拿过来商用的话,几乎是不太可能的,当下普遍的解决方案是自研,根据自身的业务场景进行二次定制开发,或者更简单一点使用第三方 SDK。
不可否认,WebRTC确实很优秀,但目前来说也并非完美,更多这方面的分析和总结请参见《 网页端实时音视频技术WebRTC:看起来很美,但离生产应用还有多少坑要填? 》、《 简述开源实时音视频技术WebRTC的优缺点 》。
9、展望WebRTC
未来在实时通信领域,WebRTC 依然是非常重要的一块拼图。
无论是 Web 还是 Native,都非常依赖 WebRTC 提供的音视频引擎,尤其是在 Web 端,几乎所有浏览器厂商的实现都是基于 Google WebRTC 项目。随着 WebRTC 1.0 标准的定稿,各大浏览器的 WebRTC 接口已经基本得到统一。
一直以来,WebRTC 都缺少测试工具。在去年年底,Google 推出了 KITE 开源项目,用于帮助开发者检测 WebRTC 应用在不同浏览器的互通性。对于标准化社区来讲,下一步工作主要会围绕提供一组更完备的测试套件,不仅可以帮助开发者测试 WebRTC 应用在 Web 端、Native 端的互通性与体验,还有助于保证各厂商浏览器 WebRTC 接口功能的一致性,并逐步完善 WebRTC 缺失的功能。
在相关技术方面,QUIC 也进入更多人的视野。对于 WebRTC 来讲,QUIC 可以加速数据通道的连接(至少原理上可行),还可以完全替代 SCTP。但问题是,目前支持 QUIC 的浏览器只有 Chrome 和 Opera。(有关QUIC协议的基本介绍和应用案例,请见《 技术扫盲:新一代基于UDP的低延时网络传输层协议——QUIC详解 》、《 让互联网更快:新一代QUIC协议在腾讯的技术实践分享 》)
另一方面,各浏览器也在持续不断地修复问题,对不同硬件设备以及系统平台进行适配,保证 WebRTC 能稳定运行于除主流机型、系统版本以外,更多的设备上。
附录:更多实时音视频技术资料 《 即时通讯音视频开发(一):视频编解码之理论概述 》
《 即时通讯音视频开发(二):视频编解码之数字视频介绍 》
《 即时通讯音视频开发(三):视频编解码之编码基础 》
《 即时通讯音视频开发(四):视频编解码之预测技术介绍 》
《 即时通讯音视频开发(五):认识主流视频编码技术H.264 》
《 即时通讯音视频开发(六):如何开始音频编解码技术的学习 》
《 即时通讯音视频开发(七):音频基础及编码原理入门 》
《 即时通讯音视频开发(八):常见的实时语音通讯编码标准 》
《 即时通讯音视频开发(九):实时语音通讯的回音及回音消除概述 》
《 即时通讯音视频开发(十):实时语音通讯的回音消除技术详解 》
《 即时通讯音视频开发(十一):实时语音通讯丢包补偿技术详解 》
《 即时通讯音视频开发(十二):多人实时音视频聊天架构探讨 》
《 即时通讯音视频开发(十三):实时视频编码H.264的特点与优势 》
《 即时通讯音视频开发(十四):实时音视频数据传输协议介绍 》
《 即时通讯音视频开发(十五):聊聊P2P与实时音视频的应用情况 》
《 即时通讯音视频开发(十六):移动端实时音视频开发的几个建议 》
《 即时通讯音视频开发(十七):视频编码H.264、VP8的前世今生 》
《 实时语音聊天中的音频处理与编码压缩技术简述 》
《 网易视频云技术分享:音频处理与压缩技术快速入门 》
《 学习RFC3550:RTP/RTCP实时传输协议基础知识 》
《 基于RTMP数据传输协议的实时流媒体技术研究(论文全文) 》
《 声网架构师谈实时音视频云的实现难点(视频采访) 》
《 浅谈开发实时视频直播平台的技术要点 》
《 还在靠“喂喂喂”测试实时语音通话质量?本文教你科学的评测方法! 》
《 实现延迟低于500毫秒的1080P实时音视频直播的实践分享 》
《 移动端实时视频直播技术实践:如何做到实时秒开、流畅不卡 》
《 如何用最简单的方法测试你的实时音视频方案 》
《 技术揭秘:支持百万级粉丝互动的Facebook实时视频直播 》
《 简述实时音视频聊天中端到端加密(E2EE)的工作原理 》
《 移动端实时音视频直播技术详解(一):开篇 》
《 移动端实时音视频直播技术详解(二):采集 》
《 移动端实时音视频直播技术详解(三):处理 》
《 移动端实时音视频直播技术详解(四):编码和封装 》
《 移动端实时音视频直播技术详解(五):推流和传输 》
《 移动端实时音视频直播技术详解(六):延迟优化 》
《 理论联系实际:实现一个简单地基于HTML5的实时视频直播 》
《 IM实时音视频聊天时的回声消除技术详解 》
《 浅谈实时音视频直播中直接影响用户体验的几项关键技术指标 》
《 如何优化传输机制来实现实时音视频的超低延迟? 》
《 首次披露:快手是如何做到百万观众同场看直播仍能秒开且不卡顿的? 》
《 Android直播入门实践:动手搭建一套简单的直播系统 》
《 网易云信实时视频直播在TCP数据传输层的一些优化思路 》
《 实时音视频聊天技术分享:面向不可靠网络的抗丢包编解码器 》
《 P2P技术如何将实时视频直播带宽降低75%? 》
《 专访微信视频技术负责人:微信实时视频聊天技术的演进 》
《 腾讯音视频实验室:使用AI黑科技实现超低码率的高清实时视频聊天 》
《 微信团队分享:微信每日亿次实时音视频聊天背后的技术解密 》
《 近期大热的实时直播答题系统的实现思路与技术难点分享 》
《 福利贴:最全实时音视频开发要用到的开源工程汇总 》
《 七牛云技术分享:使用QUIC协议实现实时视频直播0卡顿! 》
《 实时音视频聊天中超低延迟架构的思考与技术实践 》
《 理解实时音视频聊天中的延时问题一篇就够 》
《 实时视频直播客户端技术盘点:Native、HTML5、WebRTC、微信小程序 》
《 写给小白的实时音视频技术入门提纲 》
>> 更多同类文章 ……
(本文同步发布于: http://www.52im.net/thread-1631-1-1.html )
多媒体
2018-06-04 12:26:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
首先需要对pjsip有一个简单的认识,PJSIP是一个开放源代码的SIP协议栈。它支持多种SIP的扩展功能,目前可说算是最流行的sip协议栈之一了。
需要下载的软件:
pjsip: http://www.pjsip.org/ (在这里有window和linux下面的两个版本,两个都下载)
pjsip android: https://trac.pjsip.org/repos/wiki/Getting-Started/Android (配置说明)
msys2: http://www.msys2.org/ (这个是用来在window下面执行gcc和Linux下面的命令)
swig: http://swig.org/ (这个是用来把c的代码包装成java的接口,在Android里面调用)
ndk: https://developer.android.google.cn/ndk/downloads/ (ndk的版本需要下载r13b这个版本,如果你下载的其他高版本的ndk,使用的是clang的编译器会导致各种问题无法编译,r18之后官网说会移除gcc编译器)
android sdk: https://developer.android.google.cn/studio/preview/ (这个是不用翻墙的网站可以下载android studio 里面自带的sdk)
android studio: https://developer.android.google.cn/studio/preview/
java sdk: http://www.oracle.com/technetwork/java/javase/downloads/index.html (直接下载)
配置:
JAVA_HOME配置java的安装路径,android sdk感觉在android studio里面配置一下就行了,pjsip直接解压就好了,ndk也是直接解压。
msys2的配置根据官网上的信息就是升级一下本地的数据库然后就是安装gcc,
msys2有三个不同的执行环境:mingw64,mingw32,msys2,根据你的电脑选择安装gcc,我是全都装了一遍,之后安装make,swig。使用命令pacman -S安装这些软件。
编译pjsip:
接下来就是需要编译pjsip,打开mingw64.exe,在pjsip工程里面G:\pjproject-2.7.2\pjlib\include\pj新建一个config_site.h如果你只是编译这个平台下,就可以里面全是空的,如果是android下面的就添加以下内容:
define PJ_CONFIG_ANDROID 1
#include
添加完后就可以开始编译程序了,截图如下:
APP_PLATFORM=android-19 ./configure-android
我这边设置了android平台的等级,一般在sdk里面需要有这个目标平台的sdk。
平台不要选错了,如果没有指定平台默认就是最大的那个。
像这样就是配置完了,开始编译
make dep && make clean && make
差不多这样就可以了,编译成功了,可以到目录里面看看是不是有so文件。
接下来就是编译android的工程。
在这个目录里面执行make
之后so文件就会放在那个Android的工程里面了。
打开android sudio打开里面java的目录里面有个android的工程,build apk
然后就可以在手机上安装:
然后就可以连接试试看效果了。
openh264项目的编译
地址: https://github.com/cisco/openh264
NASM: http://www.nasm.us/ (需要安装)
编译openh264需要在linux环境下,电脑里面需要安装ant软件,android sdk,android ndk(ndk版本是r13的版本跟pjsip使用的一样)
下载完成后在工程的文件夹里面新建一个android的文件夹,这个文件夹用来把编译成功后的那些库和头文件放到这里。
修改make文件指定make install命令安装的路径
接下来直接编译程序
在这里需要指定ndk的路径,ndk里面的android api的版本要在ndk里面要存在,最后一个命令install就是把编译后的结果保存到刚才的android文件夹里面。
把这里面的文件需要都复制,直接复制android文件夹的内容,在window里面使用msys2来编译的时候,需要在config_site.h添加#define PJMEDIA_HAS_VIDEO 1
编译的时候需要指定openh264的位置(就是那个android文件夹的路径)
./configure-android --with-openh264=**/android
出现提醒就说明配置没有问题
Using OpenH264 prefix… /Users/me/openh264/android
checking OpenH264 availability… ok
我之前只复制了so文件然后就各种不行。
在那个android工程里面添加so文件就可以了。
多媒体
2018-06-01 10:48:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
PHP $url = '视频分享链接'; $data = httpGet($url); $data = strstr($data, 'playAddr'); $data = strstr($data, '&line', 1); $data = strstr($data, 'https'); $url = str_replace('playwm', 'play', $data); $arr = get_headers($url, 1); echo $arr['Location']; function httpGet($url) { $user_agent = 'Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0'; $ch = curl_init(); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); curl_setopt($ch, CURLOPT_USERAGENT, $user_agent); curl_setopt($ch, CURLOPT_URL, $url); $output = curl_exec($ch); curl_close($ch); return $output; }
Python import requests url = 'https://www.iesdouyin.com/share/video/6561348471406726407' headers = {'user-agent': 'Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0'} r = requests.get(url, headers=headers) strs = r.text start = strs.find('playAddr') end = strs.find('&line') res = strs[start:end] res = res.replace('playAddr: "','') url = res.replace('playwm','play') r = requests.get(url, headers=headers) print(r.url)
多媒体
2018-05-31 17:26:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
概念
面部识别:从图片或者帧里面判断出是不是人脸(Face)。 面部比对:两个人脸(Face)数据(不是图片或帧)是不是同一个人。
官方教程对面部识别,面部跟踪,特征,表情作出了说明。 https://developers.google.com/vision/face-detection-concepts
下载官方的例子: https://github.com/googlesamples/android-vision.git 这里我主要关注: https://github.com/googlesamples/android-vision/tree/master/visionSamples/FaceTracker这个人脸跟踪项目。
目的
通过修改FaceTracker项目,达到拍摄照片的过程,把人脸截图保存到sd卡里面。
关键问题
手机需要提前安装google play服务
可以看我之前的文章: 您的设备不支持Google Play服务,因此无法运行XXX
获得识别出的面部帧(整张图片,不是面部)
包装FaceDetector: class MyFaceDetector extends Detector { private Detector mDelegate; MyFaceDetector(Detector delegate) { mDelegate = delegate; } public SparseArray detect(Frame frame) { // *** add your custom frame processing code here return mDelegate.detect(frame); } public boolean isOperational() { return mDelegate.isOperational(); } public boolean setFocus(int id) { return mDelegate.setFocus(id); } }
使用包装好的MyFaceDetector: MyFaceDetector myFaceDetector = new MyFaceDetector(detector); myFaceDetector.setProcessor( new MultiProcessor.Builder<>(new GraphicFaceTrackerFactory()) .build());
使用MultiDetector来添加多个识别器: MultiDetector multiDetector = new MultiDetector.Builder() .add(myFaceDetector) .build();
添加到CameraSource对象里面: DisplayMetrics metrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(metrics); Log.e(TAG, "widthPixels: " + metrics.widthPixels + " -- metrics: " + metrics.heightPixels); // mCameraSource = new CameraSource.Builder(context, detector) mCameraSource = new CameraSource.Builder(context, multiDetector) .setRequestedPreviewSize(metrics.widthPixels, metrics.heightPixels) .setFacing(CameraSource.CAMERA_FACING_BACK) .setRequestedFps(60.0f) .setAutoFocusEnabled(true) .build();
这样就可以在MyFaceDetector的detect(Frame frame) 里面获得识别到的面部帧了。
处理识别到的人脸帧
最开始以为通过frame.getBitmap()就可以轻松获得到面部帧,然而没有这里获得是null。首先通过FaceDetector的代理识别出人脸帧: SparseArray detectedFaces = mDelegate.detect(frame); for(int i=0;i然后通过frame.getGrayscaleImageData()方法获得帧数据,通过YuvImage获得jpg图片流,获得人脸帧的bitmap,注意 这里需要对bitmap进行一下图片的旋转处理 : Frame.Metadata metadata = frame.getMetadata(); ByteBuffer byteBuffer = frame.getGrayscaleImageData(); int width = metadata.getWidth(); int height = metadata.getHeight(); int rotation = metadata.getRotation(); byte[] bytes = byteBuffer.array(); YuvImage yuvImage = new YuvImage(bytes, ImageFormat.NV21, width,height, null); ByteArrayOutputStream baos = new ByteArrayOutputStream(); yuvImage.compressToJpeg(new Rect(0, 0, width, height), 100, baos); // Where 100 is the quality of the generated jpeg byte[] jpegArray = baos.toByteArray(); Bitmap bitmap = BitmapFactory.decodeByteArray(jpegArray, 0, jpegArray.length); Matrix matrix = new Matrix(); switch (rotation) { case Frame.ROTATION_0: matrix.postRotate(0); break; case Frame.ROTATION_90: matrix.postRotate(90); break; case Frame.ROTATION_180: matrix.postRotate(90); break; case Frame.ROTATION_270: matrix.postRotate(270); break; } Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
裁剪人脸图片并保存到sd卡 if (resizedBitmap != null){ Bitmap faceBitmap = Bitmap.createBitmap(resizedBitmap, (int) face.getPosition().x, (int) face.getPosition().y, (int) face.getWidth(), (int) face.getHeight()); FileOutputStream out = null; try { //you can create a new file name "test.jpg" in sdcard folder. File file = new File(Environment.getExternalStoragePublicDirectory("face") + File.separator + "myface"+ System.currentTimeMillis() + face.getId() +".png"); File parenFile = file.getParentFile(); if (!parenFile.exists()){ parenFile.mkdirs(); } out = new FileOutputStream(file); faceBitmap.compress(Bitmap.CompressFormat.PNG, 100, out); // bmp is your Bitmap instance // PNG is a lossless format, the compression factor (100) is ignored } catch (Exception e) { e.printStackTrace(); } finally { try { if (out != null) { out.close(); } } catch (IOException e) { e.printStackTrace(); } } }
完整的MyFaceDetector类 package com.google.android.gms.samples.vision.face.facetracker; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.ImageFormat; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.YuvImage; import android.os.Environment; import android.util.SparseArray; import com.google.android.gms.vision.Detector; import com.google.android.gms.vision.Frame; import com.google.android.gms.vision.face.Face; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; /** * 包装FaceDetecor */ public class MyFaceDetector extends Detector { private Detector mDelegate; MyFaceDetector(Detector delegate) { mDelegate = delegate; } public SparseArray detect(Frame frame) { // *** add your custom frame processing code here SparseArray detectedFaces = mDelegate.detect(frame); for(int i=0;i我的修改的项目在这里:
https://github.com/fxtxz2/face-mobile-vision-android.git 参考: Mobile Vision API - concatenate new detector object to continue frame processing
Get The Face bitmap from face detector
Save bitmap to location
Android Saving created bitmap to directory on sd card
How to create Bitmap from grayscaled byte buffer image?
多媒体
2018-05-30 17:20:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
1.由于opencv3.4.1编译成pkgconfig文件opencv.pc后,opencv.pc默认安装在 /usr/local/lib64/pkgconfig/opencv.pc下 ,可以在shell编译脚本中加入:
if [ ! `grep -l "/usr/local/lib/pkgconfig:/usr/local/lib64/pkgconfig" /etc/profile` ]; then
`echo export "PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/usr/local/lib64/pkgconfig" >> /etc/profile`
fi

opencv 3.4.1默认安装目录在/usr/local/lib64下,所以将shell编译脚本加入LD_LIBRARY_PATH变量中
if [ ! `grep -l "export LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64" /etc/profile` ]; then
`echo export "LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64" >> /etc/profile`
fi
. /etc/profile

2. 由于ffmpeg 4.0的configure文件中有 --enable-libopencv编译选项,其支持--enable-libopencv,但是 默认是不编译的:
--enable-libopencv enable video filtering via libopencv [no]
opencv 3.4.1先编译成库和头文件后,ffmpeg 4.0 configure --enable-libopencv编译的话,会报:
ERROR: libopencv not found
查看ffbuild/config.log,是由于其 /usr/local/include/opencv2/core/cvdef.h 最后的地方定义c++的命名空间namespace cv, 由于其在命名空间中重新定义基础类型和c中的stdint.h一致,顾将其改为:
#ifdef __cplusplus
// Integer types portatibility
#ifdef OPENCV_STDINT_HEADER
#include OPENCV_STDINT_HEADER
#else
#if defined(_MSC_VER) && _MSC_VER < 1600 /* MSVS 2010 */
namespace cv {
......
#include
namespace cv {
typedef ::int8_t int8_t;
typedef ::uint8_t uint8_t;
typedef ::int16_t int16_t;
typedef ::uint16_t uint16_t;
typedef ::int32_t int32_t;
typedef ::uint32_t uint32_t;
typedef ::int64_t int64_t;
typedef ::uint64_t uint64_t;
}
#endif
#endif
#else
#include
#endif
这样再编译ffmpeg用configure --enable-libopencv就可以编译通过。
编译完成安装后,可以用ffmpeg/ffplay查看其是否包含opencv:

红箭头标注的地方就是包含opencv了。
多媒体
2018-05-26 21:05:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
一、在 Anaconda2 中配置 OpenCV 解压 opencv,添加系统环境变量, 计算机-->右键属性-->高级系统设置-->环境变量-->系统变量-->编辑path-->添加 F:\Program Files (x86)\opencv-3.2.0-vc14\buildd\vc14\bin 拷贝 opencv/build/python/2.7/x64/cv2.pyd 到 Anaconda2/Lib/Site-packages/ 注意: 从上面 python/2.7 可以看出,opencv 官方的 python 接口只支持 Anaconda2的版本 ,如果你装的是 Anaconda3 的话,可以打开 cmd ,然后执行 conda install -c https://conda.anaconda.org/menpo opencv3 ; 也可以参考此博客进行 Anaconda3 的配置 打开 ipython 测试一下 import cv2 print(cv2.__version__)
二、OpenCV 基础知识
1. 读取、显示和写入图像 import cv2 import matplotlib.pyplot as plt # 读取图像,第二个参数可以为1(默认读入彩图, 可省略), 0(以灰度图读入) im = cv2.imread('empire.jpg', 1) # 函数imread()返回图像为一个标准的 NumPy 数组 height, weight. channel = im.shape print height, weight. channel # 注意:height 和 width 分别对用图像坐标系的 y 轴 和 x 轴 # cropped_im = im[ny: ny + size, nx: nx + size, :],height(对应y轴)在前 # 显示图像,第一个参数是窗口的名字,其次才是我们的图像,窗口会自动调整为图像大小。 cv2.imshow('image', img) cv2.waitKey(0) # 为防止图像一闪而过,无限期的等待键盘输入 cv2.destroyAllWindows() # 关闭所有图像 # 保存图像(必须设置保存图像的路径和扩展名) cv2.imwrite('result.png', im) # 使用 plt 显示图像(可显示像素坐标及像素值)、保存图像 # 使用 plt 显示图像时,必须先把图像转换为 RGB 格式 im = cv2.cvtColor(im, cv2.COLOR_BGR2RGB) plt.imshow(im, cmap='gray', interpolation='bicubic') plt.savefig('figpath.png', bbox_inches='tight') plt.show()
2. 颜色空间转换 在OpenCV 中,图像不是按传统的RGB 颜色通道,而是 按BGR 顺序 (即RGB 的倒序)存储的。读取图像时默认的是BGR,但是还有一些可用的转换函数。颜色空间的转换可以用函数 cvtColor() 来实现。 # 1.使用opencv读取并创建灰度图像,按 BGR 顺序 im = cv2.imread('empire.jpg') gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY) # 2.使用matplotlib.image 读入并创建灰度图像,按 RGB 顺序 import matplotlib.image as mpl_img im = mpl_img.imread('empire.jpg') gray = cv2.cvtColor(im, cv2.COLOR_RGB2GRAY) # Note: 注意1和2的区别在颜色转换代码 # 常用:cv2.COLOR_BGR2RGB、cv2.COLOR_GRAY2BGR、cv2.COLOR_BGR2HSV
3. 在图像上画直线、矩形、圆、多边形(曲线) 注意: 传入的坐标必须为 int 型 画直线: cv2.line() import cv2 # 读取图像,按 BGR 顺序 img = cv2.imread('empire.jpg') # 传入图像、起点坐标、终点坐标、线的颜色(color)、线的厚度(thickness) # color : Color of the shape. for BGR, pass it as a tuple, eg: (255,0,0) for blue. For grayscale, just pass the scalar value. # thickness : if -1 is passed for closed figures like circles, it will fill the shape, default thickness = 1. img = cv2.line(img, (0, 0), (511, 511), (255, 0, 0), 5) 画矩形: cv2.rectangle() # 需要传入图像、左上角顶点坐标、右下角顶点坐标、颜色、线宽 img = cv2.rectangle(img, (384, 0), (510, 128), (0, 255, 0), 3) 画圆: cv2.circle() # 需要传入图像、圆的中心点坐标、半径、颜色、线宽 img = cv2.circle(img, (447, 63), 63, (0, 0, 255), -1) # If -1 is passed for closed figures like circles, it will fill the shape. default thickness = 1 画多边形(包括曲线): cv2.polylines() # 数组的数据类型必须为int32,若知道曲线方程,可以生成一堆点,就可以画出曲线来啦 pts = np.array([[10,5],[20,30],[70,20],[50,10]], np.int32) # 第一个参数为-1, 表明这一维的长度(点的数量)是根据后面的维度的计算出来的 pts = pts.reshape((-1,1,2)) # 如果第三个参数是False,我们得到的多边形是不闭合的(首尾不相连) img = cv2.polylines(img, [pts], True, (0, 255, 255)) 在图片上添加文字: cv2.putText() font = cv2.FONT_HERSHEY_SIMPLEX # 第 3~6 个参数为:bottom-left corner where data starts、font size、color、thickness cv2.putText(img,'OpenCV',(10,500), font, 4, (255, 255, 255), 2, cv2.LINE_AA)
4. 图像的基础操作 获取并修改像素值 import cv2 import numpy as np img = cv2.imread('messi5.jpg') px = img[100, 100] print px [57 63 68] # accessing only blue pixel blue = img[100, 100, 0] print blue 57 # modify the pixel img[100, 100] = [255, 255, 255] print img[100, 100] [255 255 255] # channel 2 所有值置为0 img[:, :, 2] = 0 获取图像属性 img = cv2.imread('messi5.jpg') print img.shape (960L, 1280L, 3L) print img.size 3686400 print img.dtype uint8 选取图像块 img = cv2.imread('messi5.jpg') # select the ball and copy it to another region ball = img[280:340, 330:390] # 注意:340和390取不到 img[273:333, 100:160] = ball
参考文档:
1.通过 Python 接口使用 OpenCV : https://blog.csdn.net/mzpmzk/article/details/68952160
多媒体
2018-05-17 11:27:00
「深度学习福利」大神带你进阶工程师,立即查看>>> #coding:utf-8 ''' Schmid滤波 参考:https://blog.csdn.net/xiaowei_cqu/article/details/25131473 ''' import cv2 import numpy as np import math def schmid_kernel(tao,sigma): r = float(sigma / (4.0 * tao)) sigma2 = float(sigma * sigma) half_filter_size = 10 filter_size = half_filter_size * 2 + 1 schmid = np.zeros((filter_size,filter_size),np.float32) filter_sum = 0.0 for i in range(filter_size): #s = schmid[i,:] for j in range(filter_size): x = i - half_filter_size y = j - half_filter_size r = math.sqrt(x * x + y * y) tmp = 2.0 * math.pi * tao * r / sigma tmp2 = float(r * r / (2.0 * sigma2)) schmid[i,j] = math.cos(tmp) * math.exp(-tmp2) filter_sum += schmid[i,j] if np.abs(filter_sum - 0.0) < 1e-6: return schmid for i in range(filter_size): #s = schmid[i,:] for j in range(filter_size): schmid[i,j] /= filter_sum return schmid src = cv2.imread('datas/face.jpg') kernel = schmid_kernel(4,2) # print('kernel = \n',kernel) dist = np.zeros_like(src) cv2.filter2D(src, -1, kernel, dist) cv2.imshow('src',src) cv2.imshow('dst',dist) cv2.waitKey() cv2.destroyAllWindows()
多媒体
2018-04-16 23:03:00
「深度学习福利」大神带你进阶工程师,立即查看>>> #coding:utf-8 ''' 图像重映射 ''' import cv2 import numpy as np src = cv2.imread('datas/l1.jpg') rows,cols,channels = src.shape img_x = np.zeros((rows,cols),np.float32) img_y = np.zeros((rows,cols),np.float32) # 坐标映射 for y in range(rows): for x in range(cols): img_y[y,x] = rows - y img_x[y,x] = cols - x dst = cv2.remap(src,img_x,img_y,cv2.INTER_LINEAR) cv2.imshow('src',src) cv2.imshow('dst',dst) cv2.waitKey() cv2.destroyAllWindows()
多媒体
2018-04-16 21:54:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
MagicLeap SDK 安装指南
MagicLeap( https://www.magicleap.com/ )是一个先进的混合现实(MR,Mixed Reality)头戴式设备,预计在2018年会发布开发者版。不过,在硬件设备推出之前,2018年3月已经发布了其开发SDK,下面我们来体验一下。
1、VR/AR/MR概念解析 VR (Virtual Reality) ,创建一个完全虚拟化的数字环境。通过VR头盔,可以浏览360度的环绕视频,进入立体渲染的环境和三维的游戏场景中,获得极强的沉浸感。 AR (Argumented Reality) ,在现实影像的基础上叠加入数字对象,从而看到复合的影像。AR头盔可以获得增强的现实图像。AR头盔一般通过半透明体的眼镜叠加投影图像来实现,这种方式一般会降低图像的明度,虚拟物品会漂浮在空中。 MR (Mixed Reality) ,将现实影像与虚拟对象实现深度混合,包括光照、色彩和几何形状的融合,从而产生完全现实感的混合图像。
2、Magic与其它AR/MR的区别
MagicLeap声称拥有如下的先进技术: 光场融合 ,将虚拟场景的数字光线与环境光线实现融合,从而产生逼真的现实感,避免数字对象漂浮在现实场景之上的虚幻感。 环境建模 ,对环境进行实时测量、建模,并与数据对象进行混合,从而将数字对象融入现实空间,产生数据对象与现实环境完全融合的体验。而且提供对象的持久化,数字对象会在其现实空间一直存在,离开以后再回来还可以看见。 声场音频 ,采用特殊的技术,让数字对象发出的声音具有空间位置响应,使用者可以判断出不同对象的声音的位置,从而声音与环境也融为一体,让数字对象“听”起来也是真实的。 高效率芯 片,环境计算和实时混合都需要消耗大量的计算能力,需要高效的芯片来保障。 下一代界面 ,由于采用了独特的技术,为了保障真实的场景感,人机交互界面也会完全不同。
3、安装MagicLeap SDK
安装分为三步: 下载SDK。 下载运行引擎。 安装开发工具。
3.1 下载SDK
首先到MagicLeap( https://www.magicleap.com/ )现在SDK,需要注册和输入验证码,然后进行安装。
3.2 下载运行引擎
启动MagicLeap SDK,里面的软件包还需要再次下载。
选中想要的软件包,按照右侧的提示进行操作即可。
3.3 下载开发工具
目前MagicLeap SDK支持Unity和Unreal两种开发环境。
下载安装器,然后安装、注册后,就能使用啦。

多媒体
2018-04-13 15:53:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
A Handy Introduction to Cloud Optimized GeoTIFFs
Since I started working at Planet , I kept hearing about Cloud Optimized GeoTIFF s (COGs). My colleague Chris Holmes even wrote a post explaining what they are and how they’re useful . I decided to implement a Go library to be able to work with COGs. Much of Planet’s satellite imagery is in the GeoTIFF format, including this nice image of Los Angeles from our February 2018 Global Basemap:
An image of Los Angeles from Planet’s February 2018 Basemap. ©2018 Planet Labs Inc, CC BY-SA 4.0.
As a first step to writing a library, I needed to learn about 1) Cloud Optimized GeoTIFFs, 2) GeoTIFFs in general, and 3) even more general than that: TIFFs. I’ve always found binary file formats intimidating — they just look like gibberish when you dump them into the console! But there’s logic in that gibberish. Here’s what you can gain from my exploration of this 30-year-old file format.
What Even Is a TIFF?
My first experience with TIFF was from the scanner I had as a kid. I scanned my bad manga-style drawings and it produced TIFFs, which the scanner software in Windows ‘95 told me meant “Tagged Image File Format.” What exactly does that mean? Let’s take a look at the Tag Image File Format FAQ to find out.
The TIFF header — the first few bytes of the file — gives us a lot of useful information: +-------+-------------+-----------------+ Byte Offset: | 0 | 2 | 4 | +---------------------------------------+ Size: | Word | Word | Long | +---------------------------------------+ Content: | Byte | Version (42)| Offset to first | | Order | | IFD | +-------+-------------+-----------------+
Let’s ignore IFD for now and start with something that’s easier to understand. We can see that Word is two bytes and Long is four bytes. Byte-order tells us if the file is big-endian (MM) or little-endian (II). The TIFF Version is 42, which the TIFF specification is an “arbitrary but carefully chosen number.”
How can we view a TIFF to confirm our understanding of the file header so far? I recommend the very nice xxd tool, which prints out binary files in a readable way. I like xxd because the terse name makes me feel like a hacker. Let’s download the TIFF of Los Angeles that I showed above, and dump the first few bytes of the TIFF: $ curl https://oldpatricka.com/planet/la.tif $ xxd la.tif | head -n 1 00000000: 4949 2a00 0800 0000 1200 0001 0300 0100 II*.............
The first two bytes of this file are 4949, and xxd helpfully tells us this is II in ASCII. Remember that II is little-endian, which means the bytes are stored in least-to-most-significant order. (This is also the way that sensible Canadians like me prefer to write dates.) Next, we can confirm that it’s actually a TIFF. The next two bytes are 2a00. We can read this as the hex number 0x002A, which is 42 in base ten! Cheers — we just read the file type and byte order of a TIFF without any special tools!
Next let’s learn about IFDs.
IFD: the Image File Directory
TIFFs are divided up into “pages,” which are individual images within a TIFF. A TIFF isn’t actually a single image, but rather a container for a number of images! The Image File Directory (IFD) describes one of these pages, and includes a set of tags to accomplish this. The tags include metadata about the file as well as where to find the image bits themselves in the file. There is a good reference for tags on the Aware Systems website, and the TIFF Specification is a very good reference and easy to read too!
So what does an IFD look like? +---------+-------------------+-----------------+ Byte Offset: | 0 | 2 | 2 + n Tags * 12 | +-----------------------------------------------+ Size: | Word | 12 Bytes * n Tags | Unsigned Long | +-----------------------------------------------+ Content: | Number | Tag Data | Offset to Next | | of Tags | | IFD (or 0) | +---------+-------------------+-----------------+
The overall structure includes the number of tags, the tag data itself, then a pointer (byte offset) to the next IFD or 0 if there are no more IFDs.
So what does an individual tag look like? Here’s a picture: +--------+--------------+----------------+-----------------+ Byte | 0 | 2 | 4 | 8 | Offset: | | | | | +----------------------------------------------------------+ Size: | Word | Word | Unsigned Long | Variable | +----------------------------------------------------------+ Content:| Tag ID | Tag Datatype | Number of | Tag Data or | | | | Values | Pointer to Data | +--------+--------------+----------------+-----------------+
So we can see an individual tag has the ID of the tag , the data type of the tag data, the number of values, and the tag data itself. Now let’s try to read a tag.
Back to the TIFF header: $ xxd la.tif | head -n1 00000000: 4949 2a00 0800 0000 1200 0001 0300 0100 II*.............
We have the byte order, the TIFF version, and the byte offset to the IFD. Let’s seek straight to the IFD offset in the TIFF header and only show that offset so it’s not overwhelming: $ xxd -s 4 -l 4 la.tif 00000004: 0800 0000
These bytes (0800 0000) are 0x8 in hex, which is 8 in decimal too. This means that the first IFD is at offset 8, which is right after the header. So, let’s seek to that offset and read the number of tags in the IFD by showing the first two bytes: $ xxd -s 8 -l 2 la.tif 00000008: 1200
These bytes (1200) mean 0x12 in hex or 16 tags in the IFD. Great, now let’s read one of these tags! To do that, we can add 2 to the offset and try to read the first tag. Remember that a tag is 12 bytes, so let’s read 12 bytes: $ xxd -s 10 -l 12 la.tif 0000000a: 0001 0300 0100 0000 0010 0000
So the tag ID is the first two bytes (0001) or 0x100 in hex, which is 256 in decimal. The tag reference says ID 256 is the ImageWidth . Now we need to know the data type to be able to read the value. This is stored in the next two bytes (0300) or 0x0003 in hex, which is 3 in decimal. The TIFF spec says 3 is a short, which is a two byte integer. Now we can check how many of these values there are with the next 4 bytes (0100 0000), which is 0x1 in hex or 1 in decimal. So, one ImageWidth value. That makes sense. Now, let’s read that value itself. It is a short, so we read the next two bytes (0010), which is 0x1000 in hex or 4096 in decimal. Now we’ve learned that the first image in our TIFF has a width of 4096 pixels. Cool! Again, we aren’t even using any software that knows about TIFFs.
Now, I’m not going to read the rest of the tags this way or the other IFDs (Ian Hansen and I wrote a script that dumps the IFDs in Go), but now we now have a good understanding of how a TIFF is structured and how to read them, all just using the command line and TIFF references.
What About the Image Data?
So all this metadata is interesting, but aren’t TIFFs images? Where is the image data itself? Well, a few special tags describe where to find the data in the file. In the case of COGs, you can use TileWidth, TileLength, TileOffsets, and TileByteCounts to read the image data since it happens to be tiled. You might have something like:
TileWidth: 256, TileLength: 256, TileOffsets: [100, 10000, 20000, 30000], TileByteCounts: [9900, 10000, 10000, 9000]. This tells you all you need to know to read out the bytes of a tiled portion of an image.
What is a GeoTIFF, Then?
A GeoTIFF is just a TIFF that has special tags to allow you to georeference the image. This tell you where on Earth is represented by the image. There are a lot of tags, but they let you know about the bounds of the image with respect to Earth, the projection it uses, and many other things.
This is easy to understand now that we have a good understanding of TIFFs. GeoTIFFs are just like what we were looking at above, but with a specific tag set.
So What Makes a Cloud Optimized GeoTIFF (COG) Interesting?
Now we know what a TIFFs and GeoTIFFs are, what is a COG? If we check out the specification , it’s pretty easy to understand, now that we understand TIFFs.
COGs are just the following stored inside the file in exactly this order:
A TIFF with one or more IFDs (describing the original image and zoomed out versions). The data for the images described isn’t that exactly the same as a regular TIFF? No actually! We didn’t cover this above, but aside from the header, the different parts of a TIFF can be in any order in the file, and TIFF readers just need to follow the offsets to read through a file. A TIFF could be structured like either of these: Nice COG: Annoying TIFF: +-------------+ +-------------+ | TIFF Header | | TIFF Header | +-------------+ +-------------+ | IFD 0 | | Image Data | +-------------+ | for IFD 1 | | IFD 1 | | | +-------------+ | | | IFD 2 | +-------------+ +-------------+ | IFD 2 | | Image Data | +-------------+ | for IFD 2 | | IFD 0 | | | +-------------+ | | | Image Data | +-------------+ | for IFD 0 | | Image Data | | | | for IFD 1 | | | | | +-------------+ | | | IFD 1 | +-------------+ +-------------+ | Image Data | | Image Data | | for IFD 0 | | for IFD 2 | | | | | | | | | +-------------+ +-------------+
If it’s structured like the one on the left, the benefit is that we can download smaller chunks of a TIFF at a time, and don’t always need to download the whole file if we only want a little chunk. We could do that by using an HTTP Range Request to ask for the specific bits of the TIFF we’re interested in. For example, we could download just the header and the IFDs with something like: $ curl http://example.com/example.tiff -i -H "Range: bytes=0-1023"
We’re just guessing that the IFDs fit into 1K, but now that we’ve retrieved this, we can use the IFDs to fetch individual tiles without fetching the whole TIFF. Say we find that the TileOffsets are: [2048, 52048, 102048, 152048] and the TileByteCounts: [50000, 50000, 50000, 50000], we can fetch just the first tile with: $ curl http://example.com/example.tiff -i -H "Range: bytes=2048-52047"
Neat, huh?
So why would you want to do this? Say you are running a tile server and want to render tiles for display on the web on-the-fly, you can split up your workload across multiple hosts and only pull chunks of the TIFF to render. Or say you want to run some analysis on your imagery that can be split up into small chunks, you can now grab small chunks of the file.
COGs Aren’t so Complicated!
We now understand how TIFFs, GeoTIFFs, and COGs work! There’s a lot here, but if you go slowly and build on your knowledge as you go, it’s not too hard to understand. Of course, I didn’t go over some details, like how the image data can be stored in strips or tiled, compressed into different formats, or the pixels themselves interleaved in different ways!
If you’d like to learn more about COGs, there is a great reference at cogeo.org , and you can view COGs in your browser with the cog map project . If you want more information about Planet APIs to get images like the one above, check out the Developer Center .
To close, I’d like to thank some of my colleagues at Planet: Ian Hansen for working with me to understand COGs; Joe Kington for filling in a lot of blanks about how the mosaics team uses COGs; Chris Holmes for encouraging me to share this outside of Planet; Dana Bauer for a ton of help reviewing this post; and Frank Warmerdam for being available as a resource to learn about GeoTIFFs and COGs specifically.
If I made any mistakes here, please let me know on Twitter or here on Medium! I’d love to learn more about TIFFs!
多媒体
2018-04-13 15:00:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
First impressions of Benewake CE30 solid-state LIDAR
April 8, 2018 zlite
The era of small, cheap (sub-$1,000) Lidar is upon us, but it’s still a bit in its teething stage. In this post, I’ll give some initial hands-on impressions of one of the first solid-state 3D (actually closer to 2.5D) Lidars to hit the market, the Benewake CE30 series , which has just been released.
Unlike the other Lidars I’ve been using, such as the RP-Lidar A2/A3 series and (now discontinued) Scanse Sweep , which are rotating 2D Lidars (just viewing a thin horizontal disc around themselves), the CE30 has no moving parts and has both horizontal and (limited) vertical scanning (132° horizontal and 9° vertical), as well as an impressive 4m-30m range (depending on which version you get) as well as an excellent 20Hz refresh rate. That’s perfect for small autonomous cars like ours — solid state means nothing to break in a crash, and having a vertical as well a horizontal sweep means that we can see obstacles from ground level to above the car.
Official pricing is around $1,500, depending on the version, but at volume they’ll be available for $400-$500. If you want to buy one now, you can find them on eBay: the CE30-C (ethernet version) can be found for $999, the CE30-A (USB version) for $800 and the CE30-D (long range) for $1,499 .
Here’s a table that shows how they compare:
CE30-A CE30-C CE30-D Max range 4m 4m 30m
Interface Notes Typical single-unit price
USB/CAN Has built-in obstacle detection function $800
Ethernet $1,000
Ethernet 4mm larger in width and height $1,500

Size-wise, it’s about the same size as the 2D Lidars, which is to say just right for our cars. (It’s shown above mounted on a Donkeycar, next to a Scanse Sweep for comparison).
Here’s a screen-capture of the sort of data it provides (me in my workshop waving my arms like a dork). The top window is the uncorrected depth map and the bottom window is a top-down view.

The software support is still pretty minimalistic: a Windows demo program (shown above) and C++ libraries for Windows and Linux (including ROS). The Linux library is designed for x86 computers and won’t compile on a RaspberryPi (which is ARM-based) yet, but Benewake says that compatibility is coming soon. Stay tuned while I wait for that — I’m particularly interested in the built-in obstacle detection mode of the CE30-A, but want to run it on the RPi.
One of the tricky things if you’re using the CE30-C (ethernet interface) with a RaspberryPi is figuring out how to talk to the Lidar on Ethernet at the same time you’re connected to the Internet (or another network) over Wifi. After a lot of research and asking around, I finally figured it out. Raspian (the RaspberryPi Linux distro) has been changing its networking configuration process with each version, which makes it hard to find reliable tutorials online, but for the latest version (Raspian Stretch), edit your etc/dhcpcd.conf file to include these two lines:
This will assign your Ethernet port to the CE30, while leaving your Wifi adapter free to connect to your regular Wifi network.
Needless to say, if you opt for the USB version (CE30-A) you don’t need to deal with this — it just shows up as a serial port.
Finally, here’s Benewake’s promotional video that shows what these Lidar’s are really designed for.

Next post will be after I get the software up and running in an autonomous car stack. But so far so good!
多媒体
2018-04-13 14:40:00