天天看點

OpenNI2 開發者指南 總覽 OpenNI 類 裝置類(Device Class) 重放控制類(PlaybackControl Class) 視訊流類(VideoStream Class) 視訊幀引用類(VideoFrameRef Class) 記錄器類(Recorder Class)

本文由官方文檔翻譯而來

總覽

目的

OpenNI 2.0 API(應用程式程式設計接口)提供了通路PrimerSense的相容深度傳感器。這就使得一個應用程式能夠初始化傳感器和從裝置接收深度(depth),彩圖(RGB)和紅外(IR)視訊流,還提供了一個統一的接口給傳感器和通過深度傳感器建立.oni記錄檔案。

OpenNI也提供了第三方中間件開發者可以互相使用深度傳感器的統一接口。應用程式還能用第三方中間件(如NITE2),也可以使用直接由OpenNI提供的基礎的深度和視訊資料。

進階API一覽

擷取深度視訊流需要使用4個主要的類(class)。

1.     openni::OpenNI提供了一個靜态的API進入點。它提供通路裝置,裝置相關事件,版本和錯誤資訊。當然,首先得確定你連接配接了裝置。

2.     openni::Device提供了一個傳感器裝置連接配接系統的接口(個人了解就是通過Device類來通路控制傳感器)。在它建立之前需要對OpenNI類進行初始化。Device可以通路流(Streams)。

3.     openni::VideoStream從一個裝置(Device)裡提取一個視訊流,需要擷取視訊幀引用(VideoFrameRefs)。

4.     openni::VideoFramRef從相關的源資料裡提取一個視訊幀。這是從一個特定的流裡面擷取的。

除了這些主要的類以外,還有許多類和結構體用來保持一些特殊類型的資料。Rocorder類就是用來存儲OpenNI視訊流到檔案的。還有Listener類用來監聽OpenNI和Stream類産生的事件。

視訊流可以通過兩種方式來擷取資料:輪詢和事件。下面會有具體介紹。

OpenNI 類

簡介

首先是最主要組成OpenNI2的是openni::OpenNI,這個類提供了一個API的靜态入口。可用來通路系統中所有的裝置(傳感器裝置)。也可以生成許多裝置連接配接和斷開事件,和提供以輪詢方式通路資料流的功能一樣。

裝置的基礎通路

OpenNI類提供了API的靜态入口,是在OpenNI::initialize()方法。這個方法初始化所有的傳感器驅動并且掃描系統中所有可用的傳感器裝置。所有使用OpenNI的應用程式在使用其他API之前都應該調用此方法。

一旦初始化方法完成,将可能會裝置(Device)對象,并使用這些對象來和真實的傳感器硬體進行互動。OpenNI::enumerateDevices()方法會傳回一個已經連接配接上系統的可用的傳感器裝置清單。

當應用程式準備退出時,必須調用OpenNI::shutdown()方法來關閉所有驅動并且正确地清除所有。

視訊流(Video Streams)的基礎通路

流的輪詢通路系統是OpenNI::waitForStream()方法實作的。此方法的參數之一就是流的清單。當方法調用時,就會鎖定直到清單中的流有新的可用資料。然後傳回一個狀态碼(status code)并指向是哪個流有可用資料了。

裝置的事件驅動通路

OpenNI類提供了一個在事件驅動方式(event driven manner)中通路裝置的架構。OpenNI定義了3個事件:裝置連接配接事件(onDeviceConnected),裝置斷開事件(onDeviceDisconnected),裝置狀态改變事件(onDeviceStateChanged)。裝置連接配接事件是在一個新的裝置連接配接并通過OpenNI可用時産生的,裝置斷開事件是在一個裝置從系統中移除時産生的。裝置狀态改變事件是在裝置的設定被改變時産生的。

可以用下列方法從事件處理清單中增添或者移除監聽器類(Listener classes):

OpenNI::addDeviceConnectedListener()//添加裝置連接配接事件監聽器

OpenNI::addDeviceDisconnectedListener()//添加裝置斷開事件監聽器

OpenNI::addDeviceStateChangedListener()//添加裝置狀态改變事件監聽器

OpenNI::removeDeviceConnectedListener()//移除裝置連接配接事件監聽器

OpenNI::removeDeviceDisconnectedListener()//移除裝置斷開事件監聽器

OpenNI::removeDeviceStateChangedListener()//移除裝置狀态改變事件監聽器

3個事件都提供了一個指針指向OpenNI::DeviceInfo對象。這個對象用來擷取被事件送出的裝置的細節和辨別。此外,裝置狀态改變事件還提供了一個指針指向DeviceState對象,這個對象被用來檢視裝置新的狀态資訊。

事件驅動通過視訊流類(VideoStream class)來通路真實的視訊流

錯誤資訊

在SDK中有許多方法都會傳回一個類型為“Status”的值。當錯誤發生,Status就會包含有一個記錄或者顯示給使用者的代碼。OpenNI::getExtendedError()方法會傳回更多的關于錯誤的可讀資訊。

版本資訊

API的版本資訊由OpenNI::getVersion()方法來擷取。這個方法傳回應用程式目前使用的API的版本資訊。

裝置類(Device Class)

簡介

openni::Device類提供了一個實體硬體裝置的接口(通過驅動)。也通過一個從實體裝置得來的ONI記錄檔案提供了一個模拟硬體裝置的接口。

裝置的基本目的是提供流。裝置對象被用來連接配接和配置底層檔案或者硬體裝置,并從裝置中建立流。

注意:這裡裝置對象是和一整個裝置(比如一台kinect或者一台xtion)聯系的,而不是具體到某個傳感器。

裝置聯系的前置條件(這裡的裝置聯系指的是代碼裡連接配接裝置擷取流,實體上的連接配接必須在此之前)

在裝置類能連接配接到實體硬體裝置前,裝置必須在實體上正确地連接配接到主機,并且驅動必須安裝完畢。OpenNI2自帶PrimeSense傳感器驅動。

如果連接配接的是ONI檔案,那要求在系統運作應用程式時ONI記錄檔案必須可用,而且應用程式有足夠的權限去通路。

當然,也需要在聯系裝置前openni::OpenNI::initialize()方法被調用。這将會初始化驅動,使API知道裝置連接配接了。

基本操作

構造函數

裝置類(Device class)的構造函數沒有參數,也不會聯系到實體硬體裝置。隻是簡單地建立對象。

Device::open()

此方法用來聯系到實體硬體裝置。open()方法有一個參數,裝置的URI(統一資源辨別符),方法傳回一個狀态碼訓示是否成功。

最簡單的用法是用常量openni::ANY_DEVICE作為裝置的URI,用這個常量會使系統連接配接所有的硬體裝置。當恰好隻有一個裝置已連接配接,這招就會非常有用。

如果多個傳感器連接配接了,那就得先調用OpenNI::enumerateDevices()來擷取可用的裝置清單。然後找到你要找的裝置,通過調用DeviceInfo::getUri()來擷取URI,用此方法的輸出作為Device::open()的參數,然後就能打開對應的裝置了。

如果打開檔案,那參數就是ONI檔案的路徑。

Device::close()

close()方法用來關閉硬體裝置。按照慣例,所有打開的裝置必須要關閉。這會分離硬體裝置和驅動,這樣後面的應用程式連接配接它們就不會有什麼麻煩了。

Device::isValid()

isValid()方法用來确定裝置是否正确地和裝置對象聯系到了

從一個裝置中擷取資訊

可以擷取關于裝置的基礎資訊。資訊包括名稱,供應商,uri,USB VID/PID(usb的id有兩部分,供應商id即VID,産品id即PID)。openni::DeviceInfo類就提供了相關資訊。每個可用資訊都有getter方法,從給定的裝置對象裡擷取DeviceInfo就調用Device::getDeviceInfo()方法

一個裝置可能由許多傳感器組成。比如一個PrimeSense裝置就由IR(紅外)傳感器,一個color(顔色)傳感器和一個depth(深度)傳感器組成。流的打開必須基于已經有的傳感器。也就是說有什麼傳感器你才能打開什麼流

可以從一個裝置中得到傳感器清單。Device::hasSensor()方法用來查詢裝置是否有特定的傳感器。傳感器類型如下:

SENSOR_IR – The IR video sensor 紅外視訊傳感器

SENSOR_COLOR – The RGB-Color video sensor 彩色視訊傳感器

SENSOR_DEPTH – The depth video sensor 深度視訊傳感器

如果要找的傳感器可用,Device::getSensorInfo()方法就可以用來擷取其資訊。SensorInfo提供了傳感器類型,包含了視訊模式的數組的getter方法。個别視訊模式被封裝進了VideoMode類。

特殊裝置功能

對齊(Registration,我個人更喜歡OpenNI1的叫法,視圖替換)

一些裝置會同時産生深度流和彩圖流。通常這些流由不同的實體攝像頭來生成。然而它們在實際上的位置是不同,就造成了它們生成的同一畫面是從不同角度得來的。這就使得從同一裝置對象不同的流得到的圖像有所不同。

兩個攝像頭之間的幾何關系和距離對于裝置對象來說都是已知的。這就可以通過數學上的變換來使得兩幅圖像能夠一緻。讓一個疊加在另一個上。比如彩圖的每個像素疊加到深度圖上。這個過程就是對齊(每一個像素疊加到另一張圖上)

一些裝置能夠在硬體進行運算,那麼可以校準資料。如果這個功能可用,那硬體上有個标示flag來進行開或關。

裝置對象提供了isImageRegistrationSupported()方法來測試已連接配接的裝置是否支援對齊功能。如果支援,那getImageRegistrationMode()能用來查詢這個功能的狀态,setImageRegistrationMode()就可以設定它。openni::ImageRegistrationMode枚舉提供了以下值用來set或get:

IMAGE_REGISTRATION_OFF – Hardware registration features aredisabled 硬體對齊功能被禁用

IMAGE_REGISTRATION_DEPTH_TO_IMAGE – The depth image is transformed tohave the same apparent vantage point as the RGB image 深度圖像被變換疊加至彩圖上

需要注意的是兩個傳感器的可視範圍有部分不重疊。這就導緻部分深度圖不會在顯示在結果中。在深度圖有毛邊的地方的可以看到“影子”或者“孔洞”,由于距離攝像頭距離的不同而看起來不同數量的物體被“轉移變形(shifted)”了。導緻遠的物體移動大過近的物體。而它們之間留下了一個沒有可用的深度資訊的空間。(這一段我也有點迷糊了...)

幀同步(FramSync)

當深度和彩圖流都可用,那可能兩個流會出現不同步,會導緻輕微的幀率不同,或者是幀到達時間的輕微不同,即使是幀率相同時。

一些裝置提供了使兩個幀同步的功能,為了在确定的最大時間範圍内分别從擷取到兩個幀,通常這個最大值都小于兩幀間隔。這個功能就是幀同步。

啟用或禁用此功能用setDepthColorSyncEnable()。

通用功能(General Capabilities)

一些裝置有功能設定不同于幀同步和登記。OpenNI 2.0提供了setProperty()和getProperty()方法來激活這些功能。setProperty方法用一個屬性Id和值來設定它。getProperty方法則傳回對應Id的屬性的值。

查閱傳感器供應商對于特殊附加屬性的支援,及對應數字id和屬性的值。

檔案裝置(File Devices)

總覽

OpenNI 2.0 提供了記錄裝置輸出到檔案的功能(記錄檔案是ONI檔案,通常擴充名為.oni)。可以選擇記錄裝置裡的所有的流,在錄像時保證設定都為使能。一旦錄像開始,那麼錄像就可以作為“檔案裝置”打開。

打開檔案裝置和打開裝置差不多,都是調用Device::open(),隻不過檔案裝置是用檔案路徑作為URI。

這個功能在運作調試時非常有用。實時場景很難甚至不能再現,而通過錄像功能,就使得同一輸入能夠用于多個算法,調試,性能比較。此功能能用于應用的自動化測試,或者是在一個項目中攝像頭不足,測試代碼就可以用記錄檔案來替代。最後,錄像還可以使得技術支援通過檢視使用者攝像頭的輸出檔案找出問題實作遠端支援。

重放控制類(PlaybackControl class)用于通路檔案裝置的特殊功能。檢視關于此類的章節擷取更多資訊。為了促進寫出通用目的的處理檔案和裝置的代碼,OpenNI提供了Device::isFile()方法,允許應用在嘗試使用重放控制之前确定是檔案還是裝置。

重放控制類(PlaybackControl Class)

簡介

在處理記錄檔案(recorded file)時可能會有一系列操作。這些操作包括在流裡查找,确定記錄有多長,循環播放,改變重放速度。這個功能封裝在PlaybackControl類中。

初始化(Initializing)

在使用PlaybackControl類之前,必須執行個體化和從檔案初始化一個Device類。一旦一個可用的檔案裝置被建立,你就可以通過調用Device::getPlaybackControl()獲得其中的PlaybackControl對象。Device::IsFile()方法被用來确定一個Device是否從一個檔案建立的。

查找定位(seek)

提供了兩個方法從一個記錄中查找定位

PlaybackControl::seek()方法用一個視訊流指針(VideoStream pointer)和幀ID(frameID)作為輸入,然後重放到指定的幀。如果一個記錄中有多個流,那所有的流都會被設定到同樣的位置上(定位的位置是指定流指定幀ID的位置)。

PlaybackControl::getNumberOfFrames()方法用來确定這個記錄有多長。從根本上确定可用目标來查找很有用。此方法以一個流的指針作為輸入,傳回指定流所包含的幀的數目。需要注意的是同一記錄的不同流可能不同的幀總數。因為真不會一直都同步。

重放速度(Playback Speed)

此功能在測試一個有很大輸入資料集合的算法時很有效。因為可以更快地得到結果。

PlaybackControl::setSpeed()方法使用一個浮點數作為輸入。這個輸入值作用于記錄制作的多種速度。比如記錄是一個30fps的流,然後輸入值為2.0,那麼重放速度為60fps,如果輸入值為0.5,那重放速度為15fps。

設定速度為0.0會導緻流播放速度為極限速度(系統能運作的最大速度)。設定速度為-1會導緻流變成手動讀取,即播放會暫停,卡在這一幀,直到應用程式去去讀取下一幀。将記錄置為手動模式(manual mode),讀取将會很緊密地循環,這就和設定速度為0.0很像。設定速度0.0是因為在用事件驅動模式進行資料讀取時很有用。

PlaybackControl::getSpeed()方法會傳回最近設定的速度值。

循環播放(Playback Looping)

一個實體傳感器會繼續産生資料無法确定,而記錄又隻有一定數量的幀。這時用一個記錄來模拟一個實體傳感器就會有問題,應用程式的代碼設計是用來處理實體傳感器,是以不能拿來處理記錄的結束。

為了解決這個問題,API提供了一個循環播放的方法。PlaybackControl::setRepeatEnabled()方法用來開關循環。設定值為true則重複讀取,讀完最後一幀又讀第一幀。如果設定值為false,那麼在記錄讀取完後導緻沒有資料幀了。

PlaybackControl::getRepeatEnable()可用來擷取目前的重複(repeat)值。

視訊流類(VideoStream Class)

簡介

由裝置類建立的視訊流類封裝了所有的資料流。這就使得你可以對資料流進行開始,停止,和配置。也被用來進行流一級(和裝置一級相對)的參數配置。

視訊流的基礎功能

建立和初始化視訊流

調用視訊流預設的構造函數會建立一個空的未初始化的視訊流對象。在使用前,這個對象必須調用VideoStream::create()進行初始化。而create()方法要求一個已經初始化的裝置對象。一旦建立,你就可以調用VideoStream::start()方法來産生資料流。VideoStream::stop()方法則會停止産生資料流。

基于輪詢的資料讀取

一旦視訊流建立完畢,就可以直接通過VideoStream::readFrame()方法進行讀取資料。如果有新的可用資料了,這個方法就會傳回一個可以通路由視訊流生成的最新的視訊幀引用(VideoFrameRef)。如果沒有新的幀可用,那就會鎖定直到有新的幀可用。

需要注意的是,如果非循環地從記錄中讀取,那麼在追後一幀讀取完畢後程式将永遠卡死在此方法。

基于事件的資料讀取

在事件驅動方式下(event driven manner)從視訊流中讀取資料是可以的。首先,需要建立一個類繼承自VideoStream::Listener類,此類應該實作方法onNewFrame()。一旦你建立了這個類,執行個體化了它,就可以通過VideoStream::addListener()方法來添加監聽器。當有新的幀到達,自定義的監聽器類的onNewFrame()方法就被調用。然後你就可以調用readFrame()讀取了。

擷取關于視訊流的資訊

傳感器資訊(SensorInfo)和視訊模式(VideoMode)

傳感器資訊和視訊模式類可以一直追蹤視訊流的資訊。視訊模式封裝了視訊流的幀率(frame rate),分辨率(resolution)和像素格式(pixel format)。傳感器資訊包含了産生視訊流的傳感器的類型和每個流的視訊模式對象清單。通過周遊這個清單,那就能确定傳感器生成的流的所有可能的模式。

使用VideoStream::getSensorInfo能夠得到目前流的傳感器資訊對象

視野(Field of View)

此功能為确定建立了視訊流的傳感器的視野範圍。使用getHorizonFieldOfView()和getVerticalFieldOfView()方法來确定視野。其傳回的值是弧度。

像素最大最小值(Min and Max PixelValues)

在深度流中,通常知道一個像素可能出現的最大值和最小值是很有用的。用getMinPixelValue()和getMaxPixleValue()方法就能擷取這些資訊。

配置視訊流

視訊模式(Video Mode)

可以設定給定流的幀率(frame rate),分辨率(resolution)和像素格式(pixel type)。設定這些就要用到setVideoMode()方法。在此之前,你首先需要擷取已配置視訊流的傳感器資訊(SensorInfo),然後你才能選擇一個可用的視訊模式。

裁剪(Cropping)

如果傳感器支援裁剪,視訊流會提供方法來控制它。使用VideoStream::isCroppingSupported()方法來确定是否支援。

如果支援,使用setCropping()來使能裁剪和設定裁剪的具體配置。ResetCropping()方法被用來再次關閉裁剪。getCropping()方法用來擷取目前的裁剪設定。

鏡像(Mirroring) 

鏡像,顧名思義,就是使視訊流所展現的看起來就像在鏡子裡一樣。啟用或禁用鏡像,使用VideoStream::setMirroringEnable()方法。設定true為啟用,設定false為禁用。可用getMirroringEnable()來擷取目前設定。

通用屬性(General Properties)

在固件層,大多數的傳感器設定都存儲為位址/值的隊(address/value pairs,就是一種鍵值對)。是以可以通過setProperty和getProperty方法直接操作。這些方法被sdk内部用來實作裁剪,鏡像,等等。而它們通常不會被應用程式頻繁地使用,因為大多數有用的屬性都被更加友好的方法封裝了。

視訊幀引用類(VideoFrameRef Class)

簡介

視訊幀引用類封裝了從視訊流讀取的單個幀的所有的相關資料。是視訊流用來傳回每一個新的幀。它提供了通路包含了幀資料(中繼資料,工作所需的幀)基礎數組。

視訊幀引用對象是從VideoStream::readFrame()方法擷取的。

視訊幀引用資料可以從紅外攝像頭,RGB攝像頭或者深度攝像頭擷取。getSensorType()方法用來确定産生此幀的傳感器類型。它會傳回傳感器類型,一個枚舉值。

通路幀資料

VideoFrameRef::getDate()方法傳回一個直接指向幀資料的指針。類型為void,這樣每個像素的資料類型才能正确地索引。

中繼資料(metadata)

每個幀都會提供一系列的中繼資料來促進資料本身的工作。

資料裁剪(Cropping data)

資料幀引用知道視訊流的裁剪設定,是以可以用來确定裁剪框的原點,裁剪框的大小和幀是否啟用裁剪功能。實作方法如下:getCropOriginX(),getCropOriginY(),getCroppingEnable().若啟用裁剪功能,則裁剪框大小等于幀大小。是以确定這些的方法和确定幀分辨率的方法是一樣一樣兒的(東北話)。

時間戳(TimeStamp)

每幀資料都有個時間戳。這個值是基于任意0值開始的微妙數。是不同于兩幀之間時間差。同一裝置的所有流用的都是同一0值,是以時間戳的內插補點可以用來比較不同流的幀。

OpenNI 2.0中,時間戳的0值是第一幀資料的到達時間。然而這無法保證每次都一樣,是以程式代碼應該使用時間戳增量。時間戳的值本身不應該用作一種絕對的時間指向。

幀索引(FrameIndex)

除了時間戳,幀還提供了連續的幀索引号。這在确定已知的兩幀之間有多少幀很有用。如果流使用了同步方法Device::setColorDepthSync(),那相應的幀的幀号應該就是一緻的。

如果沒有同步,那幀号将不一定比對。這種情況下,用時間戳來确定相關幀的位置更有效。

視訊模式(Video Modes)

VideoFrameRef::getVideoMode()用來确定生成目前幀的傳感器的視訊模式。資訊包括像素格式,分辨率,幀率。

資料大小(Data Size)

getDataSize()用來确定圖像數組中所有資料的大小。在配置設定存儲幀的緩沖區時或者确定幀數時很有用。需要注意的是這是整個數組的資料大小。用VideoMode::getPixelFormat()來确定每個數組元素的大小。

圖像分辨率(Image Resolution)

getHeight()和getWidth()方法來确定幀的分辨率很容易。這個資料資料也可以通過VideoFrameRef::getVideoMode().getResolutionX()和VideoFrameRef::getVideoMode().getResolutionY()來擷取,但不适合頻繁調用,因為太低效了。

資料有效性(Data Validity)

VideoFrameRef::isValid()方法确定目前視訊幀引用是否是有效資料。

在視訊幀引用初始化結構體和第一次資料加載之間調用會傳回false。

傳感器類型(Sensor Type)

确定産生資料幀的傳感器類型用getSensorType()。方法傳回傳感器類型,為以下的枚舉值:

SENSOR_IR– for an image taken with an IR camera 紅外傳感器

SENSOR_COLOR – for an image taken with an RGB camera 彩圖傳感器

SENSOR_DEPTH – for an image taken with a depth sensor 深度傳感器

數組跨度(Array Stride)

包含幀的數組跨度可以用getStrideBytes()來擷取。它将傳回數組每行資料的大小,機關位元組byte。主要用于索引二維圖像資料。

記錄器類(Recorder Class)

簡介

記錄器類用來記錄視訊資料到ONI檔案中。ONI檔案是OpenNI記錄深度傳感器輸出的标準記錄檔案。包含了一到多個流的資訊(如從一個PrimeSense傳感器中同時記錄一個深度和彩圖流)。還包含了裝置的設定資訊。是以可以用來通過檔案執行個體化裝置對象。

設定記錄器

設定一個記錄器有三大步。

一,調用預設構造函數構造一個記錄器對象。這不同于其他類的執行個體化。

二,調用Recorder::creat()方法,參數為記錄檔案的檔案名。建立和寫入檔案出錯時傳回一個錯誤碼。

三,提供一個資料流進行記錄。使用Recorder::attach()方法來聯系上給定的視訊流。如果你記錄多個流,那就多次調用來聯系每個視訊流,也就是逐個添加(很明顯是寫API的人偷懶了)。

記錄

視訊流聯系上後,調用Recorder::start()方法開始記錄。方法一調用,每幀資料都會被寫入ONI檔案。通過調用Recorder::stop()方法來結束記錄。調用Recorder::destroy()方法來讓檔案存盤,釋放所有記憶體。

重放

ONI可以被許多OpenNI程式和公用程式進行重放。程式打開ONI檔案都是作為檔案裝置打開的。重放控制封裝在重放控制類裡(Playback Class)。

支撐類(SupportClasses)

簡介

除了OpenNI的主要類外,還有一系列的支撐類。這些類主要服務于封裝資料,在其他主要類的章節都有所提及。

傳感器配置類

裝置資訊(DeviceInfo)

此類記錄了裝置的所有配置,包括裝置名,URI,USB VID/PID描述符和供應商。

傳感器資訊(SensorInfo)

此類存儲了傳感器的所有配置,這裡的傳感器僅是三大傳感器之一的一個。一個裝置有多個傳感器。

視訊模式(VideoMode)

此類存儲了分辨率,幀率和像素格式。用于視訊流的設定和檢視設定,由視訊幀引用檢視這些設定,由傳感器資訊提供一個視訊模式的清單。

攝像頭設定(CameraSetting)

存儲了RGB攝像頭的設定,可以啟用或禁用自動白平衡和自動曝光。

資料存儲類/結構體

版本

軟體版本。由OpenNI釋出。一些應用程式需要适配相應版本的時候使用。

RGB888Pixel

結構體,存儲彩色像素值。

數組(Array)

OpenNI提供的簡單數組類。包含圖像資料。

坐标轉換(Coordinate Conversion)

坐标轉換類用來進行真實坐标和深度坐标的轉換。詳情請參考API。

本文轉自:http://blog.csdn.net/linkageworld/article/details/52621156