Luigi Di Biasi


XP Programming Style
Sondaggio
Argomento Spider e WebCrawler

Interessante
Non interessante

Lettura del flusso (RAW) direttamente dalla periferica - Parte 1 -

Il mio blog si è trasferito all'indirizzo http://www.luigidibiasi.it

Introduzione
Nei post precedenti abbiamo usato DirectShow per eseguire alcune operazioni come la cattura di un frame o il salvataggio del flusso su hard-disk. Adesso ci poniamo un altro obbiettivo: vogliamo recuperare i dati grezzi che effettivamente dalla periferica fisica vengono inviati in memoria. Parte di questa operazione viene effettuata dal Driver in modalità Kernel che rende disponibili i dati, sotto forma di array di byte, alla controparte in user-mode ovvero il filtro di cattura (CLSID_VideoInputDeviceCategory) che recuperiamo tramite l'enumeratore di sistema e che abbiamo già utilizzato negli esempi precedenti.

Un filtro in DirectShow è caratterizzato dalla presenza di PIN (oggetti COM) di Input e Output utilizzati per concatenarlo con eventuali altri filtri (in cascata) potendo così suddividere l'elaborazione del flusso in blocchi. Quando recuperiamo un Moniker, tramite l'enumeratore, e dopo averne fatto il binding (tramite la bindToObject) sull'interfaccia IBaseFilter, possiamo gestire i PIN presenti e messi a disposizione dal Driver della periferica. 

Tipicamente i filtri di cattura mettono a disposizione due pin: uno denominato preview e uno capture (entrambi li abbiamo già usati) ma non mancano esempi di filtri che mettono a disposizione solo il pin di cattura (come ad esempio le webcam da 10 euro che si trovano in giro).

I filtri comunicano attraverso i pin. Ogni pin di output và collegato ad un pin di input, in cascata. I filtri di tipo source non hanno PIN di input (o ne hanno uno che proviene da un eventuale crossbar presente sulla scheda). I Filtri di rendering non hanno filtri di output. (da specifica. Chiaramente se ci scriviamo un nostro filtro di rendering possiamo metterci quanti PIN vogliamo.)

I dati vengono passati da un filtro all'altro mappandoli in oggetti COM che implementano l'interfaccia IMediaSample. Sono i Sample a viaggiare da un filtro all'altro. Un Sample può subire trasformazioni. Quando si trova nel filtro sorgente avrà un determinato formato ma durante le varie elaborazioni potrebbe assumerne altri. In ogni punto del grafo quindi potrebbe esserci un diverso formato di rappresentazione dei dati.

L'interfaccia IMediaSample mette a disposizione i metodi per accedere al buffer che contiene i dati del Sample. Noi focalizzeremo la nostra attenzione sui Sample generati dal filtro Sorgente e precisamente sul tipo di dati RGB32 che la webcam utilizzata per scrivere questo post fornisce sul PIN di cattura. La webcam è di tipo economico, senza marca poiché comprata su un mercatino dell'usato.

Negli esempi precedenti abbiamo utilizzato il CaptureGraphBuilder per far fare tutto il lavoro sporco al framework di DirectShow: quando utilizziamo la funzione RenderStream vengono automaticamente aggiunti i filtri necessari per recuperare l'input dal source, trasformarlo e renderlo disponibile per il rendering su schermo. (Il rendering viene effettuato da qualche filtro di tipo VideoRender) Inoltre, DirectShow si occupa di collegare automaticamente i PIN dei filtri.

L'idea che sta alla base di questo post è quella di costruire un PIN di Input da collegare al PIN di output del filtro sorgente, per ricevere i Sample (in formato RGB32) e salvarli su disco in un formato di test. Per il momento assumiamo che ogni Sample sia un frame video. Chiaramente non svilupperemo un filtro completo per due motivi: principalmente perché ci vorrebbe troppo tempo; poi, perché dovremmo fargli fare qualcosa (se veramente vogliamo investire tempo a scriverlo) ma, siccome DirectShow mette a disposizione tanti filtri belli e pronti, è inutile (ed al momento fuori dalla mia portata) reinventare la ruota. 

Tuttavia, il fatto di scrivere una classe che implementa un PIN, collegarlo e vedere che tutto funziona ci fa capire che possiamo sfruttare DirectShow anche a nostro piacimento senza seguire lo standard Microsoft che "impone" la costruzione di una DLL contenente il filtro, etc... etc... Se a noi serve solo una parte di DirectShow perché non imbrogliare? 

Il PIN
Prima di continuare è necessario dire che implementando unicamente un PIN di INPUT e collegandolo ad un PIN di OUTPUT andiamo contro lo standard e contro tutte le direttive che Microsoft riporta per gli sviluppatori che vogliono implementare filtri per DirectShow. Infatti, la specifica impone la costruzione di un filtro utilizzando le classi messe a disposizione nel Windows SDK ma, per il fine che vogliamo raggiungere, by-passiamo tutto ciò. 

Per chi fosse interessato ad approfondire lo sviluppo dei filtri basta collegarsi a msdn.com cambiare la lingua da italiano ad inglese (united states) e cercare DirectShow Filter, accedere alla sezione Using DirectShow e alla sottosezione Writing DirectShow Filter. 

Secondo MSDN i PIN di OUTPUT vanno collegati ai PIN di INPUT. Quindi il PIN che implementeremo riceverà la richiesta di connessione da parte del PIN di OUTPUT della sorgente. Il nostro PIN implementerà tre interfacce IPin, IMemInputPin e IUnknown, poiché i PIN sono oggetti COM. 

MSDN, inoltre, riporta la descrizione accurata dell'handshake che avviene durante la connessione di due filtri. Per quanto riguarda diverse webcam (economiche) che ho provato l'implementazione dell'handshake completo non è stata necessaria. (nessun driver ha chiesto di gestire anche il MemoryAllocator, un oggetto che si occupa di coordinare i buffer) Tuttavia, provando il PIN su una webcam integrata su un portatile Acer l'esempio non funziona =( poiché il driver di quella periferica richiede esplicitamente l'handshake. (Ci accorgiamo di ciò se durante il debug della classe vengono richiamate le funzioni GetAllocator, NotifyAllocator etc... nel prossimo eventuale post cercheremo di capire come imbrogliare anche li.

I metodi che implementeremo in questo esempio sono:
 - ReceiveConnection (richiamato dal PIN di OUTPUT sul PIN di INPUT quando tenterà la connessione)
 - ConnectedTo    ( che ritonerà la PIN di OUTPUT al quale il pin di INPUT è collegato)
 - QueryDirection (per informare il PIN di OUTPUT che il nostro PIN è di INPUT ) (altrimenti l'altro non si collega)
 - EnumMediaTypes (qui imbrogliamo un pò, spiegato dopo)
 - QueryInterface (poiché il PIN deve essere un oggetto COM e il pin di output richiamerà questo metodo)
 - Receive (il metodo più importante, richiamato dal PIN di OUTPUT sul nostro PIN ogni qual volta un Sample verrà passato al nostro PIN. E qui che andremo a lavorare per grabbare il frame)


La classe che implementa il PIN è la seguente: (Ho inserito i commenti per descrivere cosa fà il PIN)

#include "dshow.h"                      // (linkare anche strmiids.lib)
class MyB : IPin,IMemInputPin
{

// Pin controparte e Media type supportato
IPin *connectedPin;
AM_MEDIA_TYPE *media;

        public: // Il nostro PIN è di input quindi assumiamo che NON ci collegheremo mai ad un altro input
        HRESULT STDMETHODCALLTYPE IPin::Connect(IPin *pReceivePin,__in_opt  const AM_MEDIA_TYPE *pmt)
{
return S_OK;
}

// Richiamato dal PIN di OUTPUT della sorgente. Ci comunica un riferimento al pin di output che si sta collegando con il nostro pin ed il media type
// usato dalla sorgente.         
        HRESULT STDMETHODCALLTYPE IPin::ReceiveConnection(IPin *pConnector,const AM_MEDIA_TYPE *pmt)
{
connectedPin=pConnector;
media = (AM_MEDIA_TYPE*) malloc(sizeof(AM_MEDIA_TYPE));
media->bFixedSizeSamples=pmt->bFixedSizeSamples;
media->bTemporalCompression=pmt->bTemporalCompression;
media->cbFormat=pmt->cbFormat;
media->formattype=pmt->formattype;
media->lSampleSize=pmt->lSampleSize;
media->majortype=pmt->majortype;
media->pbFormat=pmt->pbFormat;
media->pUnk=media->pUnk;
media->subtype=pmt->subtype;
return S_OK;
}

        
        HRESULT STDMETHODCALLTYPE IPin::Disconnect(void)
{
return S_OK;
}


        // Quando viene richiamato ritorniamo il pin della sorgente ovvero il pin al quale siamo connessi. (msdn docet)
        HRESULT STDMETHODCALLTYPE IPin::ConnectedTo(__out  IPin **pPin)
{
pPin=&connectedPin;
return S_OK;
}
        
        HRESULT STDMETHODCALLTYPE IPin::ConnectionMediaType(__out  AM_MEDIA_TYPE *pmt)
{
return S_OK;
}
        
        HRESULT STDMETHODCALLTYPE IPin::QueryPinInfo( __out  PIN_INFO *pInfo)
{
return S_OK;
}
        
// Indichiamo di essere PIN di INPUT (la controparte ci chiede di che tipo siamo)
        HRESULT STDMETHODCALLTYPE IPin::QueryDirection(__out  PIN_DIRECTION *pPinDir)
{
*pPinDir=PINDIR_INPUT;
return S_OK;
}
        
        HRESULT STDMETHODCALLTYPE IPin::QueryId(__out  LPWSTR *Id)
{
return S_OK;
}
        
        HRESULT STDMETHODCALLTYPE IPin::QueryAccept(const AM_MEDIA_TYPE *pmt)
{
return S_OK;
}


// Qui imbrogliamo. La sorgente prima di instaurare la connessione ci chiede quali MediaType supportiamo. 
  // Tuttavia, noi gli rispondiamo che non siamo connessi a niente e ritorniamo un FAIL. DirectShow impone ai PIN che ricevono
// un fail di questo genere di ELENCARE LORO ai Pin di INPUT i MediaType disponibili... Così facendo, noi accettiamo qualsiasi mediatype
// che la sorgente ci comunicherà (Nella ReceiveConnection!) e permettiamo che la connessione avvenga normalmente.
// Qui andrebbe data un'occhiata ad MSDN per capire la fase di collegamento tra i PIN poiché abbiamo bypassato l'intero handshake :D

        HRESULT STDMETHODCALLTYPE IPin::EnumMediaTypes(__out  IEnumMediaTypes **ppEnum)
{
return VFW_E_NOT_CONNECTED;
}
        
        HRESULT STDMETHODCALLTYPE IPin::QueryInternalConnections(__out_ecount_part_opt(*nPin, *nPin)  IPin **apPin, ULONG *nPin)
{
return S_OK;
}
        
        HRESULT STDMETHODCALLTYPE IPin::EndOfStream( void)
{
return S_OK;
}
        
        HRESULT STDMETHODCALLTYPE IPin::BeginFlush( void)
{
return S_OK;
}
        
        HRESULT STDMETHODCALLTYPE IPin::EndFlush( void)
{
return S_OK;
}
        
        HRESULT STDMETHODCALLTYPE IPin::NewSegment(REFERENCE_TIME tStart, REFERENCE_TIME tStop, double dRate)
{
return S_OK;
}
        
// Implementiamo per rendere l'oggetto compatibile con COM. supportiamo solo le due interfacce e quindi le ritorniamo quando richieste
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, __RPC__deref_out void __RPC_FAR *__RPC_FAR *ppvObject)
{

if (riid==IID_IPin) { *ppvObject = (IPin*) this;
return S_OK;}
if (riid==IID_IMemInputPin) { *ppvObject = (IMemInputPin*) this;
return S_OK;}
 
else
return E_NOINTERFACE;
}


        ULONG STDMETHODCALLTYPE AddRef( void)
{
return S_OK;
}

ULONG STDMETHODCALLTYPE Release( void)
{
return S_OK;
}



HRESULT STDMETHODCALLTYPE GetAllocator(__out  IMemAllocator **ppAllocator)
{
return S_OK;
}
        
        HRESULT STDMETHODCALLTYPE NotifyAllocator(IMemAllocator *pAllocator,BOOL bReadOnly) 
{
return S_OK;
}
        
        HRESULT STDMETHODCALLTYPE GetAllocatorRequirements(__out  ALLOCATOR_PROPERTIES *pProps)
{
return S_OK;
}

// Ogni volta che la sorgente ci passa un Sample viene richiamata questa funzione
        HRESULT STDMETHODCALLTYPE Receive(IMediaSample *pSample)
{
    

int bSize=pSample->GetSize();
int vSize=pSample->GetActualDataLength();
BYTE *buf;
pSample->GetPointer(&buf); // Il buffer contenente il frame di tipo RGB32


// dove lo salvo?
HANDLE hf = CreateFileA("C:\\temp\\test.txt", GENERIC_WRITE, 
        FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, 0, NULL);


// Il sample può essere visto come una sequenza di RGB (se il formato è RGB32) in cui ogni byte rappresenta l'intensità
// del pixel. Non usiamo il SampleGrabber questa volta bensì ci leggiamo a mano i valori dei colori per i pixel e li
// andiamo a salvare in un file testuale di cui effettueremo il parse in seguito.
        
int toWrite[3];
         DWORD dwWritten = 0;
int r;
int g;
int b;
int j;
for (j=0;j<vSize;j+=3)
{
toWrite[0] = buf[j]; // recupero valore red
toWrite[1] =  buf[j+1]; // del verde
toWrite[2] = buf[j+2]; // del blu
WriteFile(hf, &toWrite,sizeof(toWrite), &dwWritten, NULL); // lo scrivo in un file
}
CloseHandle(hf);


 

return S_OK;
exit(-1); // termina brutalmente :D
}
        
        HRESULT STDMETHODCALLTYPE ReceiveMultiple(__in_ecount(nSamples)  IMediaSample **pSamples,long nSamples,__out  long *nSamplesProcessed)
{
return S_OK;
}
HRESULT STDMETHODCALLTYPE ReceiveCanBlock( void)
{
return S_OK;
}

};


I restanti metodi ritornano tutti S_OK in modo da indicare al Pin di OUTPUT del source che l'operazione richiesta è stata eseguita e quindi facendono andare avanti nell'invio dei Sample. Per testare il tutto possiamo istanziare la nostra webcam e collegarne il pin di output al nostro pin di input così come avviene nel codice seguente:


int _tmain(int argc, _TCHAR* argv[])
{

HRESULT hr;
CoInitialize(NULL);
IPin *ptest = (IPin*) new MyB();
IPin *pCap;
IMoniker *pMoniker;
IBaseFilter *myDev;
ICreateDevEnum *devEnum;
IEnumMoniker *monEnum;
IEnumPins *pinEnum;

hr = CoCreateInstance(CLSID_SystemDeviceEnum,0,CLSCTX_INPROC_SERVER,IID_ICreateDevEnum,(void**) &devEnum);
devEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory,&monEnum,NULL);

while(monEnum->Next(1,&pMoniker,0)==S_OK)
{
hr = pMoniker->BindToObject(NULL,NULL,IID_IBaseFilter,(void**) &myDev);
myDev->EnumPins(&pinEnum);
pinEnum->Next(1,&pCap,0);
hr = pCap->Connect(ptest,NULL);
hr = myDev->Run(0);
}

MSG msg;
 
 
while(true);

CoUninitialize();

return 0;
}

Se il driver della periferica non richiede la gestione del MemoryAllocator, durante l'esecuzione della Connect, nel file c:\temp\test.txt (o la path inserita) troveremo un file nel formato RGB, con un byte per ogni colore. 

NOTA IMPORTANTISSIMA
Questo esempio funziona SOLO con i driver che non richiedono la gestione del MemoryAllocator...
Se riusciamo a grabbare il frame per ricostruirlo c'è bisogno di capire altezza e larghezza dell'immagine. Attualmente ho provato a ricostruire l'immagine tramite VB leggendo il file 3 byte alla volta e utilizzando il metodo setPixel  come nel codice seguente:

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

        Dim z As Drawing.Bitmap = New Drawing.Bitmap(1000, 1000, Imaging.PixelFormat.Format32bppArgb)
        Dim i As Integer = 0
        Dim j As Integer = 0

        Dim r As Integer
        Dim g As Integer
        Dim b As Integer

        FileOpen(1, "C:\temp\test.txt", OpenMode.Binary)
        While Not EOF(1)
            FileGet(1, r)
            FileGet(1, g)
            FileGet(1, b)
            z.SetPixel(i, j, Color.FromArgb(r, g, b))
            i += 1
            If i = 320 Then
                i = 0
                j += 1
                If j = 240 Then
                    j = 0
                    Stop
                End If
            End If
        End While

        PictureBox1.Image = z

End Sub

Tuttavia, quel 320x240 l'ho trovato provando le varie risoluzioni finché nella picturebox non usciva l'immagine corretta ... nel prossimo post cercheremo di leggere la risoluzione del frame direttamente tramite il PIN in modo da salvarla nel file per effettuarne un parse più semplice.



mercoledì, 09 giu 2010 Ore. 11.42
Statistiche
  • Views Home Page: 27.380
  • Views Posts: 51.329
  • Views Gallerie: 0
  • n° Posts: 41
  • n° Commenti: 33
Archivio Posts
Anno 2012

Anno 2011

Anno 2010

Anno 2009

Anno 2008
Copyright © 2002-2007 - Blogs 2.0
dotNetHell.it | Home Page Blogs
ASP.NET 2.0 Windows 2003