Binär-Daten auslesen



  • Hallo Leute,

    nachdem ich vom DWD leider keine Antwort auf meine Frage bekomme, wende ich mich hoffnungsvoll an euch.

    Ich habe vor, ein Radarkomposit auszulesen. Kurz gesagt ist das ein 900 x 900 km Quadrat über Deutschland, welches pro km einen Pixel mit einem Wert ausgibt, der die Niederschlagsmenge wiederspiegelt.

    Eine Beispieldatei hierzu findet ihr hier: http://www.eknim.de/dwd/data.bin
    Es ist eine Binär-Datei mit einem Header. Ende des Headers ist ist mit dem Steuerzeichen ETX (0x03) gekennzeichnet. Danach beginnen die Binärdaten.

    Ein Pixel wird durch 2 Byte dargestellt, wobei die 2 Byte in little endian kodiert sind.

    Näheres zum genauen Format der Datei könnt ihr in der pdf nachlesen: http://www.eknim.de/dwd/RADOLAN_Komposit_format.pdf

    Mit folgendem C++-Code suche ich das Ende des Headers, damit ich die Binärdaten auslesen kann:

    // open binary data file
    	ifstream readFile;
    	readFile.open("data.bin", ios::in | ios::binary);
    
    	//skip header
    	string line;
    	getline(readFile, line); 
    		int pos = line.find(0x03);
    	readFile.seekg(pos+1);
    

    Ferner habe ich eine Typendefinition vorgenommen, damit ich 2 Byte auslesen kann:

    typedef struct bindata {
    	unsigned short buf; //entspricht 2 Byte
    } BINDATA;
    

    Dann lese ich die Datei folgendermaßen in 2 Byte-Blöcken aus:

    BINDATA buffer;
    	while (readFile)
    	{
    		readFile.read((char *)&buffer, sizeof(BINDATA));
    
    		//Bit 13 => sek Datensatz -4096
    		if(buffer.buf & (1<<12))
    		{
    			//cout << "Sekundärer Datensatz\r\n";
    			buffer.buf -= 4096;
    		}
    
    		//Bit 14 => ERROR -8192
    		if(buffer.buf & (1<<13))
    		{
    			//cout << "Errorbit gesetzt ";
    			buffer.buf -= 8192;
    		}
    
    		//Bit 15 => negatives Vorzeichen -16384
    		if(buffer.buf & (1<<14))
    		{
    			//cout << "negatives Vorzeichen\r\n";
    			buffer.buf -= 16384;
    		}
    
    		//Bit 16 => Cluttermarkierung -32768
    		if(buffer.buf & (1<<15))
    		{
    			//cout << "Cluttermarkierung ";
    			buffer.buf -= 32768;
    		}
    
    		//cout << buffer.buf << "\r\n";
    		map.push_back (buffer.buf);		
    	}
    

    Nachdem die Steuerbits 13 - 16 ggf. herausgerechnet wurden (vgl. mit pdf-Datei), erhält buffer.buf schließlich einen Wert zwischen 0 und 4095 (Abstufung der Farbwerte jedes einzelnen Pixel).

    Vgl. mit Auszug aus der PDF-Datei:
    http://www.eknim.de/dwd/binaer.jpg

    Danach gebe ich map zum Überpprüfen in eine Datei aus, welche nach 900 Zeichen einen Zeilenumbruch durchführt. Hierfür werden die 4095 Werte in 0-9 heruntergebrochen, damit ich pro Pixel ein Zeichen habe:

    ofstream output;
    	output.open("output.txt", ios::out);
    	int str;
    	for (int i = 1; i < map.size(); i++)
    	{
    		//900 nebeneinander, danach Zeilenumbruch
    		//output.txt datei mit Zahlenwerten von 0-9 (4095/10)
    		//900 Zeichen in einer Zeile
    		str = map[i]/410;
    
    		output << str;
    		if (i % 900 == 0)
    		{
    			//Umbruch
    			output << "\n";
    		}
    	}
    

    Soweit so gut.
    Nun folgen die Probleme. Leider habe ich keine Referenz, um zu schauen, ob die Daten richtig ausgelesen werden. Das macht es recht schwer, zu überprüfen, ob die o. g. Herangehensweise auch funktioniert.

    Nach dem Überprüfen der Datei stellte ich fest, dass ich nicht 900 x 900 Zeichen habe (810000), sondern 812955 (900 x 903,...) Zeichen bekomme. Das sind 2955 zu viel.
    Dann musste ich feststellen, dass nicht bei jedem gesetzten Error-Bit 14 auch der Wert 2500 zurückgegeben wird, wie es aber laut pdf-Beschreibung sein sollte.
    Ferner wird auch nicht bei jedem gesetzten Clutter-Bit 16 der Wert 2490 zurückgegeben, sondern vielmehr zumneist 1065.
    Das sind 3 Dinge, die mich zweifeln lassen, dass mein o. g. Code die Daten ordnungsgemäß ausliest.

    Kann jemand von euch das irgendwie nachvollziehen und mir ein paar Tips geben? Leider habe ich bisher zu wenig Erfahrung an das Herangehen an Little- und Big-Endians. Vlt. liegt auch hier der Fehler. Und warum bekomme ich mehr als 810000 Pixel aus der Datei heraus, wenn ich doch strickt 2 Byte auslese?

    Für reichlich Input bin ich euch sehr dankbar!

    Viele Grüße vom deutschen Eck

    Stefan



  • Hi,
    sorry ich habe mir jetzt nich alles durchgelesen, aber bist du sicher, dass

    typedef struct bindata { 
        unsigned short buf; //entspricht 2 Byte 
    } BINDATA;
    

    wirklich 2 Byte groß ist? Der Compiler darf hier nämlich sogenannte padding bytes einfügen, was dazu führen kann, dass sizeof(BINDATA) != 2 ist.



  • Hey!

    Vielen Dank für die Antwort.

    Gleiches hatte ich auch schon vermutet und getestet.
    Ich erhalte in der while-Schleife mit

    cout << sizeof(buffer) << endl;
    cout << sizeof(BINDATA) << endl;
    

    stets 2 als Ausgabe. Somit nehme ich dan, dass es passen sollte und nicht daran liegt.



  • Welchen Wert hat denn pos?

    Der richtige Datentyp sollte uint16_t sein.
    Nackt, nicht in einer struct verpackt.

    Und bei

    if(buffer.buf & (1<<12))
            {
                buffer.buf -= 4096;
    ....
            }
    

    solltest du dir mal ansehen, wie man ein Bit löscht. Nebenbei sind 1<<12 die 4096.



  • Dirk, Danke für die Anregungen.

    pos hat in dem o. g. Fall 244 (int) und wird sich stets etwa in dieser Region befinden. Macht es in dem Fall einen Unterschied, ob ich int oder uint16_t verwende?

    Oki, das Bit könnte ich auch so löschen:

    buffer.buf &= ~(1<<12);
    

    Effektiv dürfte aber bei beidem das gleiche rauskommen, oder? Zumindest ist die output.txt indentisch. Aber schöner ist deine Variante. Danke!



  • MediStef schrieb:

    Macht es in dem Fall einen Unterschied, ob ich int oder uint16_t verwende?

    Ja.
    Vergleich mal sizeof von den Beiden.
    Und du hast ja auch nicht ohne Grund unsigned short gewählt.
    Bei uint16_t kannst du dir aber sicher sein, das es 16-Bit sind.



  • aso Dirk, du meinst der Datentyp vom buffer... hab ich falsch verstanden und ist geändert. Dann hab ich das struct weg.

    uint16_t buffer;
    .
    .
    .
    readFile.read((char *)&buffer, 2);
    .
    .
    .
    if(buffer & (1<<12))
    {
    	//cout << "Sekundärer Datensatz\r\n";
    	buffer &= ~(1<<12);
    }
    .
    .
    .
    map.push_back (buffer);
    

    Aber am Ergebnis ändert es leider nix.



  • Hallo Stefan,

    Du beginnst mit ' getline ', um den Header zu überlesen:

    MediStef schrieb:

    // open binary data file
    	ifstream readFile;
    	readFile.open("data.bin", ios::in | ios::binary);
    
    	//skip header
    	string line;
    	getline(readFile, line); 
    		int pos = line.find(0x03);
    	readFile.seekg(pos+1);
    

    getline liest bis zum ersten LF (Linefeed; Code = 10; bzw. 0xA0). In der Datei auf die Du velinkst hast, ist der Header aber nicht mit einem LF abgeschlossen, d.h. getline liest viel weiter, bis in die Pixel-Daten hinein.

    Das Format ist ein Fortran-Format, d.h. die Anzahl der Zeichen pro Datum ist (meistens) konstant. Die geeignete Methode den Header zu überlesen, wäre also ignore - genauer

    readFile.ignore( numeric_limits< streamsize >::max(), char(3) );
    

    Gruß
    Werner



  • Kann er nicht das 0x03 bei getline als delimiter angeben? 😕



  • Hey Werner,

    jopp, hast Recht. Danke! Formal wäre es schöner, die Zeichen zu zählen, aber der Header muss soll mit variabler Länge ausgelesen werden, weil der sich ändern kann.

    Ich hab das mal abgeändert um nen paar Zeichen zu sparen:

    string header;
    char delim = 0x03;
    getline(readFile, header, delim);
    

    Der Header schließt mit ETX (0x03) ab. So steht der Zeiger nach dem Steuerzeichen.

    Das Ergebnis ist aber das leider gleiche.



  • PS.: die Datei enthält tatsächlich 900x903 Datensätze a 2 Byte

    .. ja - readfile.ignore( .. ,char(3)) ist die erste Wahl (s.o.), wenn der Header nicht benötigt wird. Ich würde aber immer auch die ersten beiden Zeichen lesen und auf 'SF' prüfen.



  • Danke Werner für die Rückmeldung. Wie hast du die Datei ausgelesen? Hast du mein Prog übernommen oder es mit nem anderen Reader gemacht?

    Ich bleib bei meiner Variante, da ich den Header eventuell noch benötige.

    Wenn das tatsächlich keine 900x900 sind, sondern mehr, muss ich wohl mal ein Wörtchen mit dem DWD sprechen. 🕶 Oder hab ich was Wichtiges in der PDF überlesen?



  • MediStef schrieb:

    Danke Werner für die Rückmeldung. Wie hast du die Datei ausgelesen? Hast du mein Prog übernommen oder es mit nem anderen Reader gemacht?

    Hallo Stefan,

    ich habe einfach ignore-bis-char(3) gemacht und dann die verbleibenden Bytes gezählt. Dazu waren nur eine Handvoll Programmzeilen notwendig.

    MediStef schrieb:

    Wenn das tatsächlich keine 900x900 sind, sondern mehr, muss ich wohl mal ein Wörtchen mit dem DWD sprechen. 🕶 Oder hab ich was Wichtiges in der PDF überlesen

    Vielleicht - es gibt noch zwei Informationen über die Anzahl die folgenden Datensätze.

    Zum einen steht hinter der Kennung 'GP' die Anzahl der Pixel - hier '900x900' und hinter der Kennung 'BY' steht noch mal die Größe des gesamten Datensatzes inklusive des Headers - hier '1620245' bei einer Header-Länge von 245 sind das genau 1620000 Byte macht 1620000/2=810000=900x900 Datensätze.
    Die beiden Angaben sind redundant, da die Datensatzlänge von 2Byte für das 'SF'-Format festgelegt ist; im Grunde ist das schlechter Stil - na ja - wahrscheinlich ein 'gewachsenes' Format.

    Ich würde Dir zunächst mal empfehlen eine struct für so einen Datensatz zu bauen:

    #include <cstdint> // std::uint16_t
    #include <cassert>
    #include <istream>
    
    namespace RADOLAN
    {
        struct Datensatz
        {
            Datensatz()
                : data_(0x29C4) // invalid
            {}
            bool is_cluttermarkierung() const
            {
                return (data_ & 0x8000) != 0;
            }
            bool valid() const
            {
                return (data_ & 0x2000) == 0;
            }
            int get() const
            {
                assert( valid() );
                int ret = data_ & 0x0fff;
                if( data_ & 0x4000 ) // Vorzeichen-Bit
                    ret = -ret;
                return ret;
            }
            friend std::istream& operator>>( std::istream& in, Datensatz& d );
    
        private:
            std::uint16_t data_; // ist sicher 2 Byte groß!
        };
    }
    

    Der Streaming-Operator in Zeile 28 liest so einen Datensatz binär ein - Implementierung:

    namespace RADOLAN
    {
        // --   Einlesen im Binärformat aus 2 byte (little endian)
        std::istream& operator>>( std::istream& in, Datensatz& d )
        {
            std::istream::sentry ok( in, true ); // true := noskip von 'White Space Charaters'
            if( ok )
            {
                std::ios_base::iostate state = std::ios_base::goodbit;
                try
                {
                    char buf[2];
                    if( in.rdbuf()->sgetn( buf, 2 ) == 2 )
                    {   // little endian -> LOW Byte; HIGH Byte
                        d.data_ = static_cast< std::uint16_t >( static_cast< unsigned char >( buf[0] ) )
                            | (static_cast< std::uint16_t >( static_cast< unsigned char >( buf[1] ) ) << 8);
                    }
                    else
                        state |= std::ios_base::eofbit | std::ios_base::failbit;
                }
                catch( ... )
                {
                    state |= std::ios_base::badbit;
                    if( in.exceptions() & std::ios_base::badbit )
                        throw;
                }
                in.setstate( state );
            }
            return in;
        }
    }
    

    Ich habe diesen Extractor wie auch die folgenden gleich auf 'streambuf-Ebene' realisiert - näheres dazu findest Du hier: http://www.c-plusplus.net/forum/p2256837#2256837

    Mit Hilfe des obigen Codes kannst Du so eine Datei sehr leicht einlesen:

    #include <iostream>
    #include <fstream>
    #include <limits>
    #include <vector>
    
    int main()
    {
        using namespace std;
        using namespace RADOLAN;
        const char ETX(3);
    
        ifstream readfile( "data.bin", ios_base::binary );
        if( !readfile.is_open() )
        {
            cerr << "Fehler beim Oeffnen" << endl;
            return -2;
        }
        readfile.ignore( numeric_limits< streamsize >::max(), ETX ); // Header überlesen
    
        vector< Datensatz > pixel;
        int count = 0;
        for( Datensatz d; count < 900*900 && readfile >> d; ++count )
            pixel.push_back( d );
        if( readfile )
            cout << "ohne Fehler gelesen" << endl;
    }
    

    MediStef schrieb:

    Ich bleib bei meiner Variante, da ich den Header eventuell noch benötige.

    .. dann solltest Du diesen auch lesen und nicht nur die Zeichen in einen String kopieren 😉

    Der Header besteht aus mehreren Einträgen, die immer mit einer Kennung beginnen. Für die Kennung habe ich auch ein Helferlein gebastelt, so kann man z.B. prüfen, ob das überhaupt ein 'SF'-Format ist

    // -- einfügen vor Zeile 18
        Kennung kennung;
        if( (readfile >> kennung).fail() || kennung != "SF" )
        {
            cerr << "kein RADOLAN-SF-Dateiformat" << endl;
            return -3;
        }
    

    Die Implementierung dazu:

    #include <string>
    #include <istream>
    
    namespace RADOLAN
    {
        struct Kennung
        {
            Kennung() : str_()  {}
            bool operator==( const std::string& b ) const
            {
                return str_ == b;
            }
            bool operator!=( const std::string& b ) const
            {
                return str_ != b;
            }
            friend std::istream& operator>>( std::istream& in, Kennung& tok );
        private:
            std::string str_;
        };
    
        std::istream& operator>>( std::istream& in, Kennung& tok )
        {
            std::istream::sentry ok( in, true );
            if( ok )
            {
                std::ios_base::iostate state = std::ios_base::goodbit;
                try
                {
                    typedef std::istream::traits_type Traits;
                    const char ETX(3);
                    std::string str;
                    for( int i=0; i<2; ++i )
                    {
                        Traits::int_type m = in.rdbuf()->sbumpc();
                        if( Traits::eq_int_type( m, Traits::eof() ) )
                        {
                            state |= std::ios_base::eofbit | std::ios_base::failbit;
                            break;
                        }
                        const char c = Traits::to_char_type(m);
                        str.append( 1, c );
                        if( c == ETX )
                            break;
                    }
                    if( state == std::ios_base::goodbit )
                        swap( str, tok.str_ );
                }
                catch( ... )
                {
                    state |= std::ios_base::badbit;
                    if( in.exceptions() & std::ios_base::badbit )
                        throw;
                }
                in.setstate( state );
            }
            return in;
        }
    }
    

    das allein macht noch keinen Sinn, ohne auch die Parameter hinter den Kennungen zu lesen. Der std::istream ist per se nicht in der Lage Zahlen mit einer festen Anzahl von Ziffern einzulesen und dann das Lesen zu beenden. Im Allgemeinen muss ein Datum (Zahl oder String) immer mit einen White-Space-Character beendet werden, damit es gelesen werden kann. Das ist bei diesem FORTRAN-Format nicht der Fall.

    Deshalb ist ein weiteres Helferlein fällig, welches Integer mit fester Stellen liest. Damit kann dann z.B. hinter "SF" das Datum gelesen werden:

    int tag, monat, jahr;
        if( (readfile >> number<2>( tag )).ignore( 2*2+5 ) >> number<2>( monat ) >> number<2>( jahr ) )
        {   // Format: 3I2 I5 2I2
            cout << "Datum: " << tag << "." << monat << ".20" << jahr << endl;
        }
    

    Die Implementierung von number<> :

    namespace RADOLAN
    {
        // --   der 'number_reader' liest eine Folge von genau 'N' Ziffern in eine Variable
        //      vom Typ 'T' ein. (Dezimalsystem)
        template< int N, typename T >
        struct number_reader
        {
            number_reader( T& number )
                : number_( number ) {}
    
            friend std::istream& operator>>( std::istream& in, number_reader nr )
            {
                std::istream::sentry ok( in, true ); // true := noskip
                if( ok )
                {
                    T number = 0;
                    std::ios_base::iostate state = std::ios_base::goodbit;
                    try
                    {
                        typedef std::istream::traits_type Traits;
                        bool digit_read = false;
                        const std::ctype< char >& ct = std::use_facet< std::ctype< char > >( in.getloc() );
                        for( int i=0; i<N; ++i )
                        {
                            const Traits::int_type m = in.rdbuf()->sbumpc();
                            if( Traits::eq_int_type( m, Traits::eof() ) )
                            {
                                state |= std::ios_base::eofbit | std::ios_base::failbit;
                                break;
                            }
                            const char c = Traits::to_char_type( m );
                            if( ct.is( std::ctype_base::digit, c ) || (!digit_read && ct.is( std::ctype_base::space, c )) )
                            {
                                T digit = 0;
                                if( ct.is( std::ctype_base::digit, c ) )
                                {
                                    digit_read = true;
                                    digit = T(c - '0');
                                }
                                if( number > (std::numeric_limits< T >::max() - digit)/10  )
                                {   // overflow
                                    state |= std::ios_base::failbit;
                                    break;
                                }
                                (number *= 10) += digit;
                            }
                            else
                            {
                                state |= std::ios_base::failbit;
                                break;
                            }
                        }
                        if( (state & std::ios_base::failbit) == 0 )
                            nr.number_ = number;
                    }
                    catch( ... )
                    {
                        state |= std::ios_base::badbit;
                        if( in.exceptions() & std::ios_base::badbit )
                            throw;
                    }
                    in.setstate( state );
                }
                return in;
            }
        private:
            T& number_;
        };
    
        template< int N, typename T >
        number_reader< N, T > number( T& n )
        {
            return number_reader< N, T >( n );
        }
    }
    

    Damit ist dann auch der nächste Datensatz 'BY' kein Problem mehr:

    size_t size;
        readfile >> kennung;
        if( kennung != "BY" )
            readfile.setstate( ios_base::failbit );
        readfile >> number<7>( size ); // Format I7
    

    .. die weiteren überlasse ich Dir.

    Gruß
    Werner


Anmelden zum Antworten