WebRTC视频 03 - 视频采集类 VideoCaptureDS 上篇

WebRTC视频 01 - 视频采集整体架构
WebRTC视频 02 - 视频采集类 VideoCaptureModule
[WebRTC视频 03 - 视频采集类 VideoCaptureDS 上篇](本文)
WebRTC视频 04 - 视频采集类 VideoCaptureDS 中篇
WebRTC视频 05 - 视频采集类 VideoCaptureDS 下篇

一、前言:

前面两篇文章我们介绍了WebRtc的视频采集架构,并且,分析了所有关键类之间如何相互协调,一直分析到操作VideoCaptureDS这个类为止。心中有了框架,接下来我们分析具体的点,就是VideoCaptureDS再往下如何操作硬件的。

二、流程图:

其实主要干了几件事:

在这里插入图片描述

  • 连接CaptureFilter的输出Pin到SinkFilter的输入Pin,这样数据就源源不断从输出Pin到输入Pin了。
  • CaptureFilter是由DirectShow提供的,可以通过CaptureFilter来控制DirectShow完成视频采集,而SinkFilter是webrtc自己构造的。
  • 入口函数还记得吗?是VideoCaptureDS::Init()。

三、COM编程方法介绍:

CreateClassEnumerator:

CreateClassEnumerator是DirectShow API中的一个函数,它用于创建一个枚举器对象,该对象可用于枚举系统中注册的所有DirectShow滤波器的类标识符(CLSID)。

该函数的原型通常是:

HRESULT CreateClassEnumerator(REFCLSID   clsidDeviceClass,IEnumMoniker **ppEnumMoniker,DWORD      dwFlags
);
  • clsidDeviceClass: 指定要枚举的设备类别的 CLSID。传入 NULL 时,将枚举所有的设备类别。
  • ppEnumMoniker: 指向 IEnumMoniker 接口指针的指针。枚举器将通过该指针返回。
  • dwFlags: 可选的标志,用于指定枚举器的行为。

IEnumMoniker:

IEnumMoniker 接口是COM编程中的一个接口,用于枚举 IMoniker 接口的集合。IEnumMoniker 接口中的 Next 方法用于检索指定数量的Moniker对象。

下面是 IEnumMoniker 接口的 Next 方法的一般原型:

HRESULT Next(ULONG        celt,IMoniker     **rgelt,ULONG        *pceltFetched
);
  • celt: 指定要检索的Moniker对象数量。
  • rgelt: 用于输出枚举的Moniker对象的指针数组。
  • pceltFetched: 指向一个 ULONG 变量的指针,用于返回实际成功检索的Moniker对象数量。

Next 方法会尝试从枚举器的当前位置检索指定数量的Moniker对象,并将它们填充到提供的 rgelt 数组中。成功获取的Moniker对象数量将通过 pceltFetched 参数返回。如果成功检索了指定数量的Moniker对象,则返回 S_OK,否则返回 S_FALSE

BindToStorage:

IMoniker::BindToStorage 是一个用于将 Moniker 绑定到存储对象的方法。在 COM 编程中,Moniker 是用于标识和定位对象的抽象机制,而 BindToStorage 允许将 Moniker 解析为存储对象,从而可以访问该对象的数据。

具体来说,IMoniker::BindToStorage 方法的作用是将 Moniker 绑定到存储器,并返回一个指向该存储器对象的接口指针,以便可以访问存储器中所包含的数据。这个方法通常用于从 Moniker 获取实际对象的数据或属性。

下面是 IMoniker::BindToStorage 方法的一般原型:

HRESULT BindToStorage(IBindCtx *pbc,IMoniker *pmkToLeft,REFIID   riid,void     **ppvObj
);
  • pbc: 指向绑定上下文对象的指针,用于控制绑定操作的一些方面。
  • pmkToLeft: 在某些情况下可能用到,表示左侧的 Moniker 对象。
  • riid: 指定所请求接口的 IID(接口标识符)。
  • ppvObj: 用于返回存储器对象的接口指针的指针。

通过调用 IMoniker::BindToStorage 方法,可以通过 Moniker 定位并访问存储器对象中的数据。这在 COM 编程中特别有用,特别是在处理对象链接和嵌入(OLE)等场景中。

IPropertyBag:

IPropertyBag 是 COM 编程中的一个接口,用于提供一种机制,允许通过属性名称来检索和设置属性值。它通常用于在 COM 对象之间传递属性信息,并提供一种灵活的方式来访问和操作属性。

下面是 IPropertyBag 接口的一般原型:

interface IPropertyBag : IUnknown
{virtual HRESULT Read(LPCOLESTR pszPropName, VARIANT *pVar, IErrorLog *pErrorLog) = 0;virtual HRESULT Write(LPCOLESTR pszPropName, VARIANT *pVar) = 0;
};
  • Read: 通过属性名称读取属性值,并将其存储在传入的 VARIANT 结构中。如果属性不存在或读取失败,可以使用 IErrorLog 接口来记录错误信息。
  • Write: 根据属性名称设置属性值,传入要设置的属性值的 VARIANT 结构。

通过 IPropertyBag 接口,可以实现一种通用的属性存储和检索机制,使得 COM 对象之间可以方便地传递和共享属性信息。这种机制在许多场景下非常有用,特别是在配置对象、持久化对象属性、或者在不同组件之间传递配置信息等方面。

BindToObject:

BindToObject 是 COM 编程中常用的一个方法,通常用于将 Moniker 绑定到对象,从而获取对象的接口指针。这个方法通常由 IMoniker 接口提供,可以用于实现对象的定位和访问。

下面是 IMoniker::BindToObject 方法的一般原型:

HRESULT BindToObject(IBindCtx *pbc,IMoniker *pmkToLeft,REFIID   riidResult,void     **ppvResult
);
  • pbc: 指向绑定上下文对象的指针,用于控制绑定操作的一些方面。
  • pmkToLeft: 在某些情况下可能用到,表示左侧的 Moniker 对象。
  • riidResult: 请求的接口的 IID(接口标识符)。
  • ppvResult: 用于返回绑定到的对象的接口指针的指针。

通过调用 IMoniker::BindToObject 方法,可以将 Moniker 解析为一个对象,并获取该对象的接口指针。这个方法在 COM 编程中常用于实现对象的定位和访问,特别是在处理对象链接、远程过程调用(RPC)和其他需要动态定位对象的场景中。

三、CaptureFilter:

1、作用:

CaptureFilter就是控制DirectShow完成视频采集的。

2、获取CaptureFilter:

代码入口:

int32_t VideoCaptureDS::Init(const char* deviceUniqueIdUTF8) {// ...// 构造CaptureFilter_captureFilter = _dsInfo.GetDeviceFilter(deviceUniqueIdUTF8);if (!_captureFilter) {RTC_LOG(LS_INFO) << "Failed to create capture filter.";return -1;}// ...
}

可以看出,是通过DeviceInfoDS的对象 _dsInfo 来获取CaptureFilter的。

看看如何获取CaptureFilter的:

// 获取CaptureFilter走这儿,其中productUniqueIdUTF8和productUniqueIdUTF8Length都传入的0
IBaseFilter* DeviceInfoDS::GetDeviceFilter(const char* deviceUniqueIdUTF8,char* productUniqueIdUTF8,uint32_t productUniqueIdUTF8Length) {const int32_t deviceUniqueIdUTF8Length = (int32_t)strlen((char*)deviceUniqueIdUTF8);  // UTF8 is also NULL terminatedif (deviceUniqueIdUTF8Length > kVideoCaptureUniqueNameLength) {RTC_LOG(LS_INFO) << "Device name too long";return NULL;}// enumerate all video capture devicesRELEASE_AND_CLEAR(_dsMonikerDevEnum);// CreateClassEnumerator 是获取视频采集设备的枚举器到_dsMonikerDevEnumHRESULT hr = _dsDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory,&_dsMonikerDevEnum, 0);if (hr != NOERROR) {RTC_LOG(LS_INFO) << "Failed to enumerate CLSID_SystemDeviceEnum, error 0x"<< rtc::ToHex(hr) << ". No webcam exist?";return 0;}// reset之后就可以从0开始遍历了_dsMonikerDevEnum->Reset();ULONG cFetched;IMoniker* pM;IBaseFilter* captureFilter = NULL;bool deviceFound = false;// 使用Moniker遍历所有视频采集设备while (S_OK == _dsMonikerDevEnum->Next(1, &pM, &cFetched) && !deviceFound) {IPropertyBag* pBag;// 获取对象的Bag接口,通过这个Bag接口后续获取属性hr = pM->BindToStorage(0, 0, IID_IPropertyBag, (void**)&pBag);if (S_OK == hr) {// Find the description or friendly name.// 先找设备唯一标识,找不到就去找设备描述,再找不到就去找设备名VARIANT varName;VariantInit(&varName);// 判断我们是否要获取设备唯一Id(UniqueId)if (deviceUniqueIdUTF8Length > 0) {hr = pBag->Read(L"DevicePath", &varName, 0);if (FAILED(hr)) {hr = pBag->Read(L"Description", &varName, 0);if (FAILED(hr)) {hr = pBag->Read(L"FriendlyName", &varName, 0);}}if (SUCCEEDED(hr)) {// 将设备路径进行 UTF-8 编码转换char tempDevicePathUTF8[256];// 临时存储 UTF-8 编码的设备路径tempDevicePathUTF8[0] = 0;// 将获取的devicePath保存到tempDevicePathUTF8当中WideCharToMultiByte(CP_UTF8, 0, varName.bstrVal, -1,tempDevicePathUTF8, sizeof(tempDevicePathUTF8),NULL, NULL);// 比较下是否为我们想要找的deviceif (strncmp(tempDevicePathUTF8, (const char*)deviceUniqueIdUTF8,deviceUniqueIdUTF8Length) == 0) {// We have found the requested device// 找到了请求的设备deviceFound = true;// 获取CaptureFilter接口到captureFilterhr =pM->BindToObject(0, 0, IID_IBaseFilter, (void**)&captureFilter);ifFAILED(hr) {RTC_LOG(LS_ERROR) << "Failed to bind to the selected ""capture device "<< hr;}// 如果产品唯一标识存在且长度大于 0,获取设备名称,我们调用的时候传入的Null和0,这儿不会执行if (productUniqueIdUTF8 &&productUniqueIdUTF8Length > 0)  // Get the device name{GetProductId(deviceUniqueIdUTF8, productUniqueIdUTF8,productUniqueIdUTF8Length);}}}}VariantClear(&varName);pBag->Release();}pM->Release();}return captureFilter;
}
  • 我们找到第一个就直接退出了,不会找出所有设备;
  • 我们调用的时候productUniqueIdUTF8使用的缺省值NULL,productUniqueIdUTF8Length使用的缺省值0,因此不会执行GetProductId;

至此,我们VideoCaptureDS就持有了CaptureFilter了。

3、添加CaptureFilter到FilterGraph:

我们所有的Filter都必须添加到FilterGraph,这样,FilterGraph才能控制我们完成一些业务逻辑。

int32_t VideoCaptureDS::Init(const char* deviceUniqueIdUTF8) {// 省略部分代码...// 构造CaptureFilter_captureFilter = _dsInfo.GetDeviceFilter(deviceUniqueIdUTF8);if (!_captureFilter) {RTC_LOG(LS_INFO) << "Failed to create capture filter.";return -1;}// Get the interface for DirectShow's GraphBuilder// 创建FilterGraph,并返回IGraphBuilder接口HRESULT hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,IID_IGraphBuilder, (void**)&_graphBuilder);if (FAILED(hr)) {RTC_LOG(LS_INFO) << "Failed to create graph builder.";return -1;}// 获取IMediaControl接口,用于控制数据的流转hr = _graphBuilder->QueryInterface(IID_IMediaControl, (void**)&_mediaControl);if (FAILED(hr)) {RTC_LOG(LS_INFO) << "Failed to create media control builder.";return -1;}// 将前面构造好的CaptureFilter添加到FilterGraph当中hr = _graphBuilder->AddFilter(_captureFilter, CAPTURE_FILTER_NAME);if (FAILED(hr)) {RTC_LOG(LS_INFO) << "Failed to add the capture device to the graph.";return -1;}// 省略部分代码...
}

4、获取输出Pin:

前面我们枚举整个终端的视频采集设备,找到了我们请求的设备,并返回了CaptureFilter。我们CaptureFilter也有很多Pin,因此,如法炮制,继续枚举CaptureFilter的Pin,找到我们想要的输出Pin。

入口函数:

int32_t VideoCaptureDS::Init(const char* deviceUniqueIdUTF8) {// 省略部分代码...// 获取CaptureFilter的输出Pin_outputCapturePin = GetOutputPin(_captureFilter, PIN_CATEGORY_CAPTURE);if (!_outputCapturePin) {RTC_LOG(LS_INFO) << "Failed to get output capture pin";return -1;}
}

注入我们要的输出Pin类型是PIN_CATEGORY_CAPTURE

/*** 获取输出引脚* @param filter 表示是哪个Filter的引脚* @param Category 表示引脚的种类*/
IPin* GetOutputPin(IBaseFilter* filter, REFGUID Category) {HRESULT hr;IPin* pin = NULL;IEnumPins* pPinEnum = NULL;// 获得枚举pin的接口到pPinEnum中filter->EnumPins(&pPinEnum);if (pPinEnum == NULL) {return NULL;}// get first unconnected pinhr = pPinEnum->Reset();  // set to first pin 让从0开始枚举// 遍历每个pinwhile (S_OK == pPinEnum->Next(1, &pin, NULL)) {// 获取这个pin的方向PIN_DIRECTION pPinDir;pin->QueryDirection(&pPinDir);if (PINDIR_OUTPUT == pPinDir)  // This is an output pin{// 判断pin的类型,是否为我们想要的// GUID_NULL表示任意类型if (Category == GUID_NULL || PinMatchesCategory(pin, Category)) {pPinEnum->Release();return pin;}}pin->Release();pin = NULL;}pPinEnum->Release();return NULL;
}

其实逻辑也很简单了,就是遍历这个CaptureFilter的所有Pin,判断下是不是输出pin,如果是,再判断下pin类型是否为我们想要的,都符合就找到了。

那么,如何判断pin类型是否为我们想要的呢?

/*** 判断pin类型是否匹配*/
BOOL PinMatchesCategory(IPin* pPin, REFGUID Category) {BOOL bFound = FALSE;// 获取IKsPropertySet接口到pKs当中IKsPropertySet* pKs = NULL;HRESULT hr = pPin->QueryInterface(IID_PPV_ARGS(&pKs));if (SUCCEEDED(hr)) {GUID PinCategory;DWORD cbReturned;// 从AMPROPSETID_Pin这个属性集中,获取AMPROPERTY_PIN_CATEGORY属性,// 将属性数据存放于PinCategory当中,实际返回的数据大小存于cbReturned中hr = pKs->Get(AMPROPSETID_Pin, AMPROPERTY_PIN_CATEGORY, NULL, 0,&PinCategory, sizeof(GUID), &cbReturned);// 判断返回的数据和我们要存储的数据大小是否一致,一致表示找到了目标pinif (SUCCEEDED(hr) && (cbReturned == sizeof(GUID))) {bFound = (PinCategory == Category);}pKs->Release();}return bFound;
}

发现它是去获取我们请求的AMPROPERTY_PIN_CATEGORY这种类型的Pin的属性是否和我们请求的一直,一直就认为类型一致。

四、SinkFilter:

前面已经创建好了输入数据的Filter,也就是CaptureFilter,并将它加入到了FilterGraph当中,同时找到了合适的输出Pin,准备输出数据,我们现在就创建一个输出Filter,也就是SinkFilter,接收CaptureFilter采集的数据。

注意:之前讲的CaptureFilter是由DirectShow提供的,而SinkFilter是webrtc自己构造的;

入口函数:

int32_t VideoCaptureDS::Init(const char* deviceUniqueIdUTF8) {// Create the sink filte used for receiving Captured frames.// 开始构造CaptureSinkFiltersink_filter_ = new ComRefCount<CaptureSinkFilter>(this);// 将CaptureSinkFilter加入到GraphicBuilder当中hr = _graphBuilder->AddFilter(sink_filter_, SINK_FILTER_NAME);if (FAILED(hr)) {RTC_LOG(LS_INFO) << "Failed to add the send filter to the graph.";return -1;}// 获取SinkFilter的输入pin_inputSendPin = GetInputPin(sink_filter_);if (!_inputSendPin) {RTC_LOG(LS_INFO) << "Failed to get input send pin";return -1;}return 0;
}

发现我们是创建了一个CaptureSinkFilter对象,并让GraphicBuilder将自己管理起来,最后获取SinkFilter的输入Pin,既然SinkFilter是自己构建的,我们看看它的类长什么样:

1、CaptureSinkFilter:

class CaptureSinkFilter : public IBaseFilter {public:CaptureSinkFilter(VideoCaptureImpl* capture_observer);HRESULT SetRequestedCapability(const VideoCaptureCapability& capability);// Called on the capture thread.// Filter采集到数据之后,通过这个方法传给上层void ProcessCapturedFrame(unsigned char* buffer,size_t length,const VideoCaptureCapability& frame_info);void NotifyEvent(long code, LONG_PTR param1, LONG_PTR param2);bool IsStopped() const;//  IUnknownSTDMETHOD(QueryInterface)(REFIID riid, void** ppv) override;// IPersistSTDMETHOD(GetClassID)(CLSID* clsid) override;// IMediaFilter.STDMETHOD(GetState)(DWORD msecs, FILTER_STATE* state) override;STDMETHOD(SetSyncSource)(IReferenceClock* clock) override;STDMETHOD(GetSyncSource)(IReferenceClock** clock) override;STDMETHOD(Pause)() override;STDMETHOD(Run)(REFERENCE_TIME start) override;STDMETHOD(Stop)() override;// IBaseFilterSTDMETHOD(EnumPins)(IEnumPins** pins) override; // 遍历所有引脚STDMETHOD(FindPin)(LPCWSTR id, IPin** pin) override;STDMETHOD(QueryFilterInfo)(FILTER_INFO* info) override;STDMETHOD(JoinFilterGraph)(IFilterGraph* graph, LPCWSTR name) override;STDMETHOD(QueryVendorInfo)(LPWSTR* vendor_info) override;protected:virtual ~CaptureSinkFilter();private:SequenceChecker main_checker_;const rtc::scoped_refptr<ComRefCount<CaptureInputPin>> input_pin_;VideoCaptureImpl* const capture_observer_;FILTER_INFO info_ RTC_GUARDED_BY(main_checker_) = {};// Set/cleared in JoinFilterGraph. The filter must be stopped (no capture)// at that time, so no lock is required. While the state is not stopped,// the sink will be used from the capture thread.IMediaEventSink* sink_ = nullptr;FILTER_STATE state_ RTC_GUARDED_BY(main_checker_) = State_Stopped;
};
  • capture_observer_: 是一个观察者,sinkFilter获取到数据之后,通过这个observer传给上层;
  • state_: sinkFilter的状态,初始为stoped,运行之后就是started;
  • input_pin_:sinkFilter的输入pin,真正获取数据的地方;
  • ProcessCapturedFrame // Filter采集到数据之后,通过这个方法传给上层;
  • EnumPins // 遍历所有引脚

2、输入Pin:

class CaptureInputPin : public IMemInputPin, public IPin {public:CaptureInputPin(CaptureSinkFilter* filter);HRESULT SetRequestedCapability(const VideoCaptureCapability& capability);// Notifications from the filter.void OnFilterActivated();void OnFilterDeactivated();protected:virtual ~CaptureInputPin();private:CaptureSinkFilter* Filter() const;HRESULT AttemptConnection(IPin* receive_pin, const AM_MEDIA_TYPE* media_type);std::vector<AM_MEDIA_TYPE*> DetermineCandidateFormats(IPin* receive_pin,const AM_MEDIA_TYPE* media_type);void ClearAllocator(bool decommit);HRESULT CheckDirection(IPin* pin) const;// IUnknownSTDMETHOD(QueryInterface)(REFIID riid, void** ppv) override;// clang-format off// clang isn't sure what to do with the longer STDMETHOD() function// declarations.// IPin// 用于连接某个pinSTDMETHOD(Connect)(IPin* receive_pin,const AM_MEDIA_TYPE* media_type) override;// 当与某个pin连接成功之后,回调这个方法,查看能否与某个pin进行连接STDMETHOD(ReceiveConnection)(IPin* connector,const AM_MEDIA_TYPE* media_type) override;STDMETHOD(Disconnect)() override;STDMETHOD(ConnectedTo)(IPin** pin) override;STDMETHOD(ConnectionMediaType)(AM_MEDIA_TYPE* media_type) override;STDMETHOD(QueryPinInfo)(PIN_INFO* info) override;STDMETHOD(QueryDirection)(PIN_DIRECTION* pin_dir) override;STDMETHOD(QueryId)(LPWSTR* id) override;STDMETHOD(QueryAccept)(const AM_MEDIA_TYPE* media_type) override;STDMETHOD(EnumMediaTypes)(IEnumMediaTypes** types) override;STDMETHOD(QueryInternalConnections)(IPin** pins, ULONG* count) override;STDMETHOD(EndOfStream)() override;STDMETHOD(BeginFlush)() override;STDMETHOD(EndFlush)() override;STDMETHOD(NewSegment)(REFERENCE_TIME start, REFERENCE_TIME stop,double rate) override;// IMemInputPin// 分配一个内存分配器(因为有些Filter是虚拟的,必须靠这个来 IMemInputPin 这些方法管理内存STDMETHOD(GetAllocator)(IMemAllocator** allocator) override;STDMETHOD(NotifyAllocator)(IMemAllocator* allocator, BOOL read_only) override;STDMETHOD(GetAllocatorRequirements)(ALLOCATOR_PROPERTIES* props) override;// 获取当前引脚的数据(比如CaptureSinkFilter调用这个接口获取)STDMETHOD(Receive)(IMediaSample* sample) override;STDMETHOD(ReceiveMultiple)(IMediaSample** samples, long count,long* processed) override;STDMETHOD(ReceiveCanBlock)() override;// clang-format onSequenceChecker main_checker_;SequenceChecker capture_checker_;// 用户请求的能力VideoCaptureCapability requested_capability_ RTC_GUARDED_BY(main_checker_);// Accessed on the main thread when Filter()->IsStopped() (capture thread not// running), otherwise accessed on the capture thread.// 最终最接近用户请求能力的真实能力VideoCaptureCapability resulting_capability_;DWORD capture_thread_id_ = 0;// 内存分配器rtc::scoped_refptr<IMemAllocator> allocator_ RTC_GUARDED_BY(main_checker_);// 与当前pin连接的外部pinrtc::scoped_refptr<IPin> receive_pin_ RTC_GUARDED_BY(main_checker_);std::atomic_bool flushing_{false};std::atomic_bool runtime_error_{false};// Holds a referenceless pointer to the owning filter, the name and// direction of the pin. The filter pointer can be considered const.// pin信息PIN_INFO info_ = {};// 每个pin都有自己支持的媒体类型,不支持的会拒绝掉AM_MEDIA_TYPE media_type_ RTC_GUARDED_BY(main_checker_) = {};
};

我基本都写注释了,但是,还有几点需要注意:

  • IMemInputPin: 是与内存相关的,因为有些引脚是物理引脚,有些引脚是虚拟的,比如CaptureSinkFilter就是虚拟的Filter,虚拟的就会涉及到内存的申请释放,IMemInputPin就是定义这些方法的;

  • IPin就是实际的引脚;

  • 当调用CaptureInputPin的Receive获取CaptureInputPin的数据之后,就可以交给CaptureSinkFilter,再通过其ProcessCapturedFrame 传给capture_observer_;

至于InputPin的枚举获取和之前CaptureFilter的OutputPin逻辑一样,不再赘述。

五、连接Filter:

Filter连接是一个比较复杂的流程,打算单独写一篇介绍,读者可以先思考几个问题:

  1. 每个Filter都有自己支持的能力,那么这俩Filter要连起来,分别应该选择哪个能力?
  2. 两个Filter之间要传递数据怎么传递?存储数据的Buffer由哪个Filter管理?
  3. 需要创建多大的Buffer,依据是什么?大了浪费空间,小了不够存。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

高光谱深度学习调研

综述 高光谱深度学习只有小综述&#xff0c;没有大综述。小综述里面场景分类、目标检测的综述比较多。 Wang C, Liu B, Liu L, et al. A review of deep learning used in the hyperspectral image analysis for agriculture[J]. Artificial Intelligence Review, 2021, 54(7)…

计算机视觉 1-8章 (硕士)

文章目录 零、前言1.先行课程&#xff1a;python、深度学习、数字图像处理2.查文献3.环境安装 第一章&#xff1a;概论1.计算机视觉的概念2.机器学习 第二章&#xff1a;图像处理相关基础1.图像的概念2.图像处理3.滤波器4.卷积神经网络CNN5.图像的多层表示&#xff1a;图像金字…

24-Ingest Pipeline Painless Script

将文档中的tags字段按照逗号&#xff08;,&#xff09;分隔符进行分割。 同时为文档&#xff0c;增加一个字段。blog查看量 DELETE tech_blogs#Blog数据&#xff0c;包含3个字段&#xff0c;tags用逗号间隔 PUT tech_blogs/_doc/1 {"title":"Introducing big …

【ubuntu18.04】vm虚拟机复制粘贴键不能用-最后无奈换版本

我是ubuntu16版本的 之前费老大劲安装的vmware tools结果不能用 我又卸载掉&#xff0c;安装了open-vm-tools 首先删除VMware tools sudo vmware-uninstall-tools.pl sudo rm -rf /usr/lib/vmware-tools sudo apt-get autoremove open-vm-tools --purge再下载open-vm-tools s…

Android Mobile Network Settings | APN 菜单加载异常

问题 从log看是有创建APN对应的Controller&#xff08;功能逻辑是ok的&#xff09;&#xff0c;但是Mobile Network Settings无法显示&#xff08;UI异常&#xff09;。 相关术语&#xff1a; GSM&#xff08;Global System for Mobile Communications&#xff09; 全球移动…

AndroidStudio-Activity的生命周期

一、Avtivity的启动和结束 从当前页面跳到新页面&#xff0c;跳转代码如下&#xff1a; startActivity(new Intent(源页面.this&#xff0c;目标页面.class))&#xff1b; 从当前页面回到上一个页面&#xff0c;相当于关闭当前页面&#xff0c;返回代码如下&#xff1a; finis…

python机器人Agent编程——多Agent框架的底层逻辑(上)

目录 一、前言二、两个核心概念2.1 Routines&#xff08;1&#xff09;清晰的Prompt&#xff08;2&#xff09;工具调用json schema自动生成&#xff08;3&#xff09;解析模型的toolcall指令&#xff08;4&#xff09;单Agent的循环决策与输出 PS.扩展阅读ps1.六自由度机器人相…

SOP搭建:企业标准化操作程序构建与实施指南

一、引言 在当今充满竞争的商业领域&#xff0c;实现企业运营的标准化、高效化和高质量化是提升企业市场竞争力的关键所在。标准操作程序&#xff08;SOP&#xff09;作为一种至关重要的管理工具&#xff0c;能够清晰地阐述业务流程&#xff0c;规范操作行为&#xff0c;并促进…

用 Python 从零开始创建神经网络(五):损失函数(Loss Functions)计算网络误差

用损失函数&#xff08;Loss Functions&#xff09;计算网络误差 引言1. 分类交叉熵损失&#xff08;Categorical Cross-Entropy Loss&#xff09;2. 分类交叉熵损失类&#xff08;The Categorical Cross-Entropy Loss Class&#xff09;展示到目前为止的所有代码3. 准确率计算…

ubuntu 安装kafka-eagle

上传压缩包 kafka-eagle-bin-2.0.8.tar.gz 到集群 /root/efak 目录 cd /root/efak tar -zxvf kafka-eagle-bin-2.0.8.tar.gz cd /root/efak/kafka-eagle-bin-2.0.8 mkdir /root/efakmodule tar -zxvf efak-web-2.0.8-bin.tar.gz -C /root/efakmodule/ mv /root/efakmodule/efak…

Zotero 7本地pdf文件名自适应中英文格式

问题 Zotero7默认语言是中文&#xff0c;发现本地pdf文献中均会出现“等”字&#xff0c;出现中英文不统一的不便。 &#xff08;注&#xff1a;存在et al.的pdf&#xff0c;是从外部直接拖进去的&#xff0c;不是自动产生的。&#xff09; 解决 zotero 7提供了丰富的文件后…

Redis性能优化——针对实习面试

目录 Redis性能优化什么是bigkey&#xff1f;bigkey的危害&#xff1f;如何处理bigkey?什么是hotkey&#xff1f;hotkey的危害&#xff1f;如何处理hotkey&#xff1f;如何处理大量key集中过期问题&#xff1f;什么是内存碎片&#xff1f;为什么会有Redis内存碎片&#xff1f;…

牛客挑战赛77

#include <iostream>// 函数 kXOR&#xff1a;计算两个数在 k 进制下的异或和 // 参数&#xff1a; // a: 第一个正整数 // b: 第二个正整数 // k: 进制基数 // 返回值&#xff1a; // 两数在 k 进制下的异或和&#xff08;十进制表示&#xff09; long long kXO…

开源共建 | 长安链开发常见问题及规避

长安链开源社区鼓励社区成员参与社区共建&#xff0c;参与形式包括不限于代码贡献、文章撰写、社区答疑等。腾讯云区块链王燕飞在参与长安链测试工作过程中&#xff0c;深入细致地总结了长安链实际开发应用中的常见问题及其有效的规避方法&#xff0c;相关内容多次解答社区成员…

EWM 打印

目录 1 简介 2 后台配置 3 主数据 4 业务操作 1 简介 打印即输出管理&#xff08;output management&#xff09;利用“条件表”那一套理论实现。而当打印跟 EWM 集成到一起时&#xff0c;也需要利用 PPF&#xff08;Post Processing Framework&#xff09;那一套理论。而…

LLaMA-Factory全流程训练模型

&#x1f917;本文主要讲述在docker下使用LLaMA-Factory训练推理模型。 &#x1fae1;拉取镜像 首先需要启动docker&#xff0c;然后在终端中输入&#xff1a; docker run -tid --gpus all -p 8000:8000 --name LLM -e NVIDIA_DRIVER_CAPABILITIEScompute,utility -e NVIDIA…

WebSocket简易聊天室实现(有详细解释)

完整代码 Arata08/online-chat-demo 服务端: 1.编写配置类&#xff0c;扫描有 ServerEndpoint 注解的 Bean import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.s…

Excel超级处理器:高效实现2种批量生成二维码方式

在Excel数据处理中&#xff0c;二维码的批量生成是一个常见且重要的需求。借助Excel超级处理器这一强大的插件&#xff0c;用户可以轻松实现二维码的两种主要批量生成方式&#xff1a;直接在单元格中显示二维码图片&#xff0c;以及直接生成二维码图片并保存在文件夹中。超级处…

Linux Android 正点原子RK3568替换开机Logo完整教程

0.这CSDN是有BUG吗?大家注意:表示路径的2个点号全都变成3个点号啦! 接下来的后文中,应该是2个点都被CSDN变成了3个点: 1.将这两个 bmp 图片文件720x1280_8bit拷贝到内核源码目录下,替换内核源码中默认的 logo 图片。注意:此时还缺少电量显示图片 2.编译内核 make d…

性能高于Transformer模型1.7-2倍,彩云科技发布基于DCFormer架构通用大模型云锦天章

2017年&#xff0c;谷歌发布《Attention Is All You Need》论文&#xff0c;首次提出Transformer架构&#xff0c;掀开了人工智能自然语言处理&#xff08;NLP&#xff09;领域发展的全新篇章。Transformer架构作为神经网络学习中最重要的架构&#xff0c;成为后来席卷全球的一…