Fabrication industrielle
Internet des objets industriel | Matériaux industriels | Entretien et réparation d'équipement | Programmation industrielle |
home  MfgRobots >> Fabrication industrielle >  >> Industrial programming >> VHDL

Image bitmap du fichier BMP lue avec TEXTIO

La conversion du fichier image au format bitmap constitue le moyen le plus simple de lire une image à l'aide de VHDL. La prise en charge du format de fichier d'image graphique raster BMP est intégrée au système d'exploitation Microsoft Windows. Cela fait de BMP un format d'image approprié pour stocker des photos à utiliser dans des bancs de test VHDL.

Dans cet article, vous apprendrez à lire un fichier image binaire comme BMP et à stocker les données en mémoire dynamique dans le simulateur. Nous allons utiliser un exemple de module de traitement d'image pour convertir l'image en niveaux de gris, ce sera notre appareil sous test (DUT). Enfin, nous écrivons la sortie du DUT dans une nouvelle image que nous pouvons comparer visuellement à l'original.

Ce billet de blog fait partie d'une série sur l'utilisation de la bibliothèque TEXTIO en VHDL. Lisez les autres articles ici :

Comment initialiser la RAM à partir d'un fichier à l'aide de TEXTIO

Fichier Stimulus lu dans testbench avec TEXTIO

Pourquoi le bitmap est le meilleur format pour le VHDL

Les formats de fichiers image les plus courants sur Internet sont JPEG et PNG. Les deux utilisent la compression, JPEG est avec perte tandis que PNG est sans perte. La plupart des formats offrent une certaine forme de compression, car cela peut réduire considérablement la taille de stockage de l'image. Bien que cela convienne à une utilisation normale, ce n'est pas idéal pour lire dans un banc de test VHDL.

Pour pouvoir traiter une image dans un logiciel ou un matériel, vous devez avoir accès aux données de pixel brutes dans votre application. Vous voulez que les données de couleur et de luminance soient stockées dans une matrice d'octets, c'est ce qu'on appelle des graphiques bitmap ou raster.

La plupart des éditeurs d'images bien connus tels que Photoshop ou GIMP sont basés sur raster. Ils peuvent ouvrir un large éventail de formats d'image, mais ils sont tous convertis en graphiques raster en interne dans l'éditeur.

Vous pouvez également le faire en VHDL, mais cela nécessiterait un effort de codage considérable car il n'existe pas de solutions toutes faites pour décoder les images compressées. Une meilleure solution consiste à convertir manuellement les images d'entrée de test dans un format bitmap tel que BMP ou en les incorporant dans le script qui lance votre testbench.

Le format de fichier image BMP

Le format de fichier BMP est bien documenté sur Wikipedia. Ce format a de nombreuses variantes différentes, mais nous allons nous mettre d'accord sur certains paramètres spécifiques qui nous faciliteront grandement la tâche. Pour créer nos images d'entrée, nous les ouvrons dans Microsoft Paint qui est préinstallé avec Windows. Ensuite, nous cliquons sur Fichier→Enregistrer sous , sélectionnez Type de fichier :Bitmap 24 bits (*bmp ; *.dib) . Donnez au fichier un nom se terminant par le suffixe .bmp et cliquez sur Enregistrer.

En s'assurant que le fichier est créé comme ceci, nous pouvons supposer que l'en-tête est toujours la variante BITMAPINFOHEADER de 54 octets avec le format de pixel RGB24 mentionné sur la page Wikipedia. De plus, nous ne nous soucierons que de quelques champs sélectionnés dans l'en-tête. Le tableau ci-dessous montre les champs d'en-tête que nous allons lire.

Décalage (déc.) Taille (B) Attendu (Hex) Description
0 2 « BM » (42 4D) Champ ID
10 4 54 (36 00 00 00) Décalage du tableau de pixels
14 4 40 (28 00 00 00) Taille de l'en-tête
18 4 Lire la valeur Largeur de l'image en pixels
22 4 Lire la valeur Hauteur de l'image en pixels
26 1 1 (01) Nombre de plans de couleur
28 1 24 (18) Nombre de bits par pixel

Les valeurs marquées en vert sont les seules que nous devons vraiment examiner car nous savons à quelles valeurs s'attendre dans les autres champs d'en-tête. Si vous avez accepté de n'utiliser que des images de dimensions fixes prédéfinies à chaque fois, vous pouvez ignorer tout l'en-tête et commencer à lire au décalage d'octet numéro 54 dans le fichier BMP, c'est là que les données de pixel seront trouvées.

Néanmoins, nous vérifierons que les autres valeurs listées sont bien celles attendues. Ce n'est pas difficile à faire puisque nous lisons déjà l'en-tête. Il fournit également une protection contre les erreurs de l'utilisateur, si vous ou l'un de vos collègues fournissez une image du mauvais codage au testbench à tout moment dans le futur.

Le cas de test

Ce billet de blog explique comment lire une image à partir d'un fichier dans un banc d'essai VHDL, mais pour être complet, j'ai inclus un exemple DUT. Nous diffuserons les données de pixels via le DUT au fur et à mesure que nous lirons l'image. Enfin, nous écrivons les résultats dans un autre fichier BMP de sortie qui peut être examiné dans votre visionneuse d'images préférée.

entity grayscale is
  port (
    -- RGB input
    r_in : in std_logic_vector(7 downto 0);
    g_in : in std_logic_vector(7 downto 0);
    b_in : in std_logic_vector(7 downto 0);

    -- RGB output
    r_out : out std_logic_vector(7 downto 0);
    g_out : out std_logic_vector(7 downto 0);
    b_out : out std_logic_vector(7 downto 0)
  );
end grayscale; 

Le code ci-dessus montre l'entité de notre DUT. Le module en niveaux de gris prend les données RVB 24 bits pour un pixel en entrée et les convertit en une représentation en niveaux de gris qui est présentée sur la sortie. Notez que le pixel de sortie représente une nuance de gris toujours dans l'espace colorimétrique RVB, nous ne convertissons pas le BMP en un BMP en niveaux de gris qui est un format différent.

Le module est purement combinatoire, il n'y a pas d'entrée d'horloge ou de réinitialisation. Le résultat apparaît immédiatement sur la sortie lorsque quelque chose est assigné à l'entrée. Pour plus de simplicité, la conversion en niveaux de gris utilise une approximation à virgule fixe de la valeur luma (luminosité) selon le système de codage ITU-R BT.2100 RVB vers luma.

Vous pouvez télécharger le code du module en niveaux de gris et l'ensemble du projet en utilisant le formulaire ci-dessous.

L'image du Boeing 747 que vous voyez ci-dessous sera notre exemple d'image d'entrée. Autrement dit, ce n'est pas l'image BMP réelle qui est intégrée dans ce billet de blog, ce ne serait pas possible. C'est une représentation JPEG de l'image BMP que nous allons lire dans notre testbench. Vous pouvez demander l'image BMP originale en laissant votre adresse e-mail dans le formulaire ci-dessus et vous la recevrez immédiatement dans votre boîte de réception.

L'image de test mesure 1000 x 1000 pixels. Cependant, le code présenté dans cet article devrait fonctionner avec toutes les dimensions d'image tant qu'il est au format BMP BITMAPINFOHEADER 24 bits. Cependant, la lecture d'une grande image prendra beaucoup de temps de simulation car l'accès aux fichiers dans la plupart des simulateurs VHDL est lent. Cette image fait 2930 Ko et prend quelques secondes à charger dans ModelSim.

Importer la librairie TEXTIO

Pour lire ou écrire dans des fichiers en VHDL, vous devez importer la bibliothèque TEXTIO. Assurez-vous d'inclure les lignes de la liste ci-dessous en haut de votre fichier VHDL. Nous devons également importer le finish mot-clé du package standard pour arrêter la simulation lorsque tous les tests sont terminés.

use std.textio.all;
use std.env.finish;

Les instructions ci-dessus nécessitent l'utilisation de VHDL-2008 ou d'une version plus récente.

Déclarations de type personnalisé

Nous déclarerons quelques types personnalisés au début de la région déclarative de notre testbench. Le format des structures de données pour stocker les données de pixel dépend du type d'entrée que le DUT attend. Le module de niveaux de gris attend trois octets qui représentent chacun l'une des composantes de couleur rouge, vert et bleu. Parce qu'il fonctionne sur un pixel à la fois, nous sommes libres de stocker l'ensemble de pixels comme nous le souhaitons.

Comme nous pouvons le voir dans le code ci-dessous, nous déclarons d'abord un header_type tableau que nous utiliserons pour stocker toutes les données d'en-tête. Nous examinerons certains champs de l'en-tête, mais nous devons également les stocker car nous allons écrire les données d'image traitées dans un nouveau fichier à la fin du testbench. Ensuite, nous devons inclure l'en-tête d'origine dans l'image de sortie.

type header_type  is array (0 to 53) of character;

type pixel_type is record
  red : std_logic_vector(7 downto 0);
  green : std_logic_vector(7 downto 0);
  blue : std_logic_vector(7 downto 0);
end record;

type row_type is array (integer range <>) of pixel_type;
type row_pointer is access row_type;
type image_type is array (integer range <>) of row_pointer;
type image_pointer is access image_type;

La deuxième déclaration déclare un enregistrement nommé pixel_type . Ce type personnalisé agira comme un conteneur pour les données RVB pour un pixel.

Enfin, les structures de données dynamiques pour stocker tous les pixels sont déclarées. Tant que row_type est un tableau sans contrainte de pixel_type , le row_pointer est un type d'accès à celui-ci, un pointeur VHDL. De même, nous construisons un image_type sans contrainte tableau pour stocker toutes les lignes de pixels.

Ainsi, le image_pointer type fonctionnera comme un handle vers l'image complète dans la mémoire allouée dynamiquement.

Instanciation du DUT

À la fin de la région déclarative, nous déclarons les signaux d'interface pour le DUT, comme indiqué ci-dessous. Les signaux d'entrée sont postfixés avec _in et les signaux de sortie avec _out . Cela nous permet de les identifier facilement dans le code ainsi que dans la forme d'onde. Le DUT est instancié au début de l'architecture avec les signaux assignés via la carte de port.

signal r_in : std_logic_vector(7 downto 0);
signal g_in : std_logic_vector(7 downto 0);
signal b_in : std_logic_vector(7 downto 0);
signal r_out : std_logic_vector(7 downto 0);
signal g_out : std_logic_vector(7 downto 0);
signal b_out : std_logic_vector(7 downto 0);

begin

DUT :entity work.grayscale(rtl)
port map (
  r_in => r_in,
  g_in => g_in,
  b_in => b_in,
  r_out => r_out,
  g_out => g_out,
  b_out => b_out
);

Variables de processus et descripteurs de fichiers

Nous allons créer un seul processus de banc d'essai pour contenir toutes les lectures et écritures de fichiers. La région déclarative du processus est illustrée ci-dessous. On commence par déclarer un nouveau char_file type pour définir le type de données que nous souhaitons lire à partir du fichier image d'entrée. Le fichier BMP est codé en binaire; donc on veut opérer sur des octets, le character tapez en VHDL. Sur les deux lignes suivantes, nous utilisons le type pour ouvrir un fichier d'entrée et un fichier de sortie.

process
  type char_file is file of character;
  file bmp_file : char_file open read_mode is "boeing.bmp";
  file out_file : char_file open write_mode is "out.bmp";
  variable header : header_type;
  variable image_width : integer;
  variable image_height : integer;
  variable row : row_pointer;
  variable image : image_pointer;
  variable padding : integer;
  variable char : character;
begin

Ensuite, nous déclarons une variable pour contenir les données d'en-tête, ainsi que deux variables entières pour contenir la largeur et la hauteur de l'image. Après cela, nous déclarons un row pointeur et un image aiguille. Ce dernier sera notre descripteur de l'image complète une fois qu'elle aura été lue à partir du fichier.

Enfin, nous déclarons deux variables de commodité ; padding de type integer et char de type character . Nous les utiliserons pour stocker temporairement les valeurs que nous lisons dans le fichier.

Lire l'en-tête BMP

Au début du corps du processus, nous lisons l'intégralité de l'en-tête du fichier BMP dans le header variable, comme indiqué dans le code ci-dessous. L'en-tête fait 54 octets de long, mais au lieu d'utiliser la valeur codée en dur, nous obtenons la plage à parcourir en référençant le header_type'range attribut. Vous devez toujours utiliser des attributs lorsque vous le pouvez pour conserver les valeurs constantes définies aussi peu d'endroits que possible.

  for i in header_type'range loop
    read(bmp_file, header(i));
  end loop;

Vient ensuite quelques instructions assert où nous vérifions que certains des champs d'en-tête sont comme prévu. Ceci est une protection contre les erreurs de l'utilisateur car nous n'utilisons pas les valeurs lues pour quoi que ce soit, nous vérifions simplement qu'elles sont comme prévu. Les valeurs attendues sont celles répertoriées dans ce tableau, présentées plus haut dans l'article.

Le code ci-dessous montre les déclarations assert, chacune avec un report instruction décrivant l'erreur et un severity failure instruction pour arrêter la simulation si l'expression affirmée est false . Nous devons utiliser un niveau de gravité élevé car au moins avec les paramètres par défaut dans ModelSim, il imprimera simplement un message d'erreur et continuera la simulation.

  -- Check ID field
  assert header(0) = 'B' and header(1) = 'M'
    report "First two bytes are not ""BM"". This is not a BMP file"
    severity failure;

  -- Check that the pixel array offset is as expected
  assert character'pos(header(10)) = 54 and
    character'pos(header(11)) = 0 and
    character'pos(header(12)) = 0 and
    character'pos(header(13)) = 0
    report "Pixel array offset in header is not 54 bytes"
    severity failure;

  -- Check that DIB header size is 40 bytes,
  -- meaning that the BMP is of type BITMAPINFOHEADER
  assert character'pos(header(14)) = 40 and
    character'pos(header(15)) = 0 and
    character'pos(header(16)) = 0 and
    character'pos(header(17)) = 0
    report "DIB headers size is not 40 bytes, is this a Windows BMP?"
    severity failure;

  -- Check that the number of color planes is 1
  assert character'pos(header(26)) = 1 and
    character'pos(header(27)) = 0
    report "Color planes is not 1" severity failure;

  -- Check that the number of bits per pixel is 24
  assert character'pos(header(28)) = 24 and
    character'pos(header(29)) = 0
    report "Bits per pixel is not 24" severity failure;

Ensuite, nous lisons les champs de largeur et de hauteur de l'image à partir de l'en-tête. Ce sont les deux seules valeurs que nous allons réellement utiliser. Par conséquent, nous les affectons au image_width et image_height variables. Comme nous pouvons le voir dans le code ci-dessous, nous devons multiplier les octets suivants par la puissance pondérée de deux valeurs pour convertir les champs d'en-tête de quatre octets en valeurs entières appropriées.

  -- Read image width
  image_width := character'pos(header(18)) +
    character'pos(header(19)) * 2**8 +
    character'pos(header(20)) * 2**16 +
    character'pos(header(21)) * 2**24;

  -- Read image height
  image_height := character'pos(header(22)) +
    character'pos(header(23)) * 2**8 +
    character'pos(header(24)) * 2**16 +
    character'pos(header(25)) * 2**24;

  report "image_width: " & integer'image(image_width) &
    ", image_height: " & integer'image(image_height);

Enfin, nous imprimons la hauteur et la largeur de lecture sur la console du simulateur en utilisant le report déclaration.

Lecture des données de pixels

Nous devons savoir combien d'octets de remplissage il y aura sur chaque ligne avant de pouvoir commencer à lire les données de pixel. Le format BMP exige que chaque rangée de pixels soit complétée par un multiple de quatre octets. Dans le code ci-dessous, nous nous occupons de cela avec une formule à une ligne en utilisant l'opérateur modulo sur la largeur de l'image.

  -- Number of bytes needed to pad each row to 32 bits
  padding := (4 - image_width*3 mod 4) mod 4;

Nous devons également réserver de l'espace pour toutes les lignes de données de pixels que nous allons lire. Le image variable est un type d'accès, un pointeur VHDL. Pour le faire pointer vers un espace mémoire inscriptible on utilise le new mot-clé pour réserver de l'espace pour image_height nombre de lignes en mémoire dynamique, comme indiqué ci-dessous.

  -- Create a new image type in dynamic memory
  image := new image_type(0 to image_height - 1);

Il est maintenant temps de lire les données d'image. La liste ci-dessous montre la boucle for qui lit le tableau de pixels, ligne par ligne. Pour chaque ligne, nous réservons de l'espace pour un nouveau row_type objet, pointé par le row variable. Ensuite, nous lisons le nombre de pixels attendus, d'abord le bleu, puis le vert et enfin la couleur rouge. Il s'agit de l'ordre selon la norme BMP 24 bits.

  for row_i in 0 to image_height - 1 loop

    -- Create a new row type in dynamic memory
    row := new row_type(0 to image_width - 1);

    for col_i in 0 to image_width - 1 loop

      -- Read blue pixel
      read(bmp_file, char);
      row(col_i).blue :=
        std_logic_vector(to_unsigned(character'pos(char), 8));

      -- Read green pixel
      read(bmp_file, char);
      row(col_i).green :=
        std_logic_vector(to_unsigned(character'pos(char), 8));

      -- Read red pixel
      read(bmp_file, char);
      row(col_i).red :=
        std_logic_vector(to_unsigned(character'pos(char), 8));

    end loop;

    -- Read and discard padding
    for i in 1 to padding loop
      read(bmp_file, char);
    end loop;

    -- Assign the row pointer to the image vector of rows
    image(row_i) := row;

  end loop;

Après avoir lu la charge utile pour chaque ligne, nous lisons et supprimons les octets de remplissage supplémentaires (le cas échéant). Enfin, à la fin de la boucle, nous affectons la nouvelle ligne dynamique de pixels au bon emplacement du image déployer. Lorsque la boucle for termine le image La variable doit contenir des données de pixel pour l'ensemble de l'image BMP.

Tester le DUT

Le module en niveaux de gris utilise uniquement la logique combinatoire, nous n'avons donc pas à nous soucier des signaux d'horloge ou de réinitialisation. Le code ci-dessous parcourt chaque pixel de chaque ligne tout en écrivant les valeurs RVB aux entrées DUT. Après avoir attribué les valeurs d'entrée, nous attendons 10 nanosecondes pour laisser tous les retards de cycle delta dans le DUT se dérouler. Toutes les valeurs de temps supérieures à 0 fonctionneront, ou même wait for 0 ns; répété suffisamment de fois.

  for row_i in 0 to image_height - 1 loop
    row := image(row_i);

    for col_i in 0 to image_width - 1 loop

      r_in <= row(col_i).red;
      g_in <= row(col_i).green;
      b_in <= row(col_i).blue;
      wait for 10 ns;

      row(col_i).red := r_out;
      row(col_i).green := g_out;
      row(col_i).blue := b_out;

    end loop;
  end loop;

Lorsque le programme sort de l'instruction d'attente, les sorties du DUT doivent contenir les valeurs RVB pour la couleur en niveaux de gris de ce pixel. À la fin de la boucle, nous laissons la sortie DUT remplacer les valeurs de pixel que nous lisons à partir du fichier BMP d'entrée.

Écriture du fichier BMP de sortie

À ce stade, tous les pixels du image variable aurait dû être manipulée par le DUT. Il est temps d'écrire les données d'image dans le out_file objet, qui pointe vers un fichier local nommé "out.bmp". Dans le code ci-dessous, nous parcourons chaque pixel des octets d'en-tête que nous avons stockés à partir du fichier BMP d'entrée et les écrivons dans le fichier de sortie.

  for i in header_type'range loop
    write(out_file, header(i));
  end loop;

Après l'en-tête, nous devons écrire les pixels dans l'ordre dans lequel nous les avons lus à partir du fichier d'entrée. Les deux boucles for imbriquées dans la liste ci-dessous s'en chargent. Notez qu'après chaque ligne, nous utilisons le deallocate mot-clé pour libérer la mémoire allouée dynamiquement pour chaque ligne. La récupération de place n'est incluse que dans VHDL-2019, dans les versions précédentes de VHDL, vous pouvez vous attendre à des fuites de mémoire si vous omettez cette ligne. À la fin de la boucle for, nous écrivons des octets de remplissage si nécessaire pour amener la longueur de la ligne à un multiple de 4 octets.

  for row_i in 0 to image_height - 1 loop
    row := image(row_i);

    for col_i in 0 to image_width - 1 loop

      -- Write blue pixel
      write(out_file,
        character'val(to_integer(unsigned(row(col_i).blue))));

      -- Write green pixel
      write(out_file,
        character'val(to_integer(unsigned(row(col_i).green))));

      -- Write red pixel
      write(out_file,
        character'val(to_integer(unsigned(row(col_i).red))));

    end loop;

    deallocate(row);

    -- Write padding
    for i in 1 to padding loop
      write(out_file, character'val(0));
    end loop;

  end loop;

Une fois la boucle terminée, nous libérons l'espace mémoire pour le image variables, comme indiqué ci-dessous. Ensuite, nous fermons les fichiers en appelant file_close sur les descripteurs de fichiers. Ce n'est pas strictement nécessaire dans la plupart des simulateurs car le fichier est implicitement fermé lorsque le sous-programme ou le processus se termine. Néanmoins, il n'est jamais mauvais de fermer les fichiers lorsque vous en avez terminé.

  deallocate(image);

  file_close(bmp_file);
  file_close(out_file);

  report "Simulation done. Check ""out.bmp"" image.";
  finish;
end process;

À la fin du processus de banc d'essai, nous imprimons un message à la console ModelSim indiquant que la simulation est terminée, avec un indice de l'endroit où l'image de sortie peut être trouvée. Le finish nécessite VHDL-2008, c'est une façon élégante d'arrêter le simulateur une fois tous les tests terminés.

L'image BMP de sortie

L'image ci-dessous montre à quoi ressemble le fichier "out.bmp" une fois le testbench terminé. Le fichier réel affiché dans ce billet de blog est un JPEG car les BMP ne conviennent pas à l'intégration sur des pages Web, mais vous pouvez laisser votre adresse e-mail dans le formulaire ci-dessus pour obtenir un fichier zip avec le projet complet, y compris le fichier "boeing.bmp".

Dernières remarques

Pour le traitement d'image dans FPGA, le schéma de codage de couleur YUV est souvent utilisé à la place de RVB. En YUV, la composante luma (luminance), Y, est séparée des informations de couleur. Le format YUV est plus étroitement lié à la perception visuelle humaine. Heureusement, il est facile de convertir entre RVB et YUV.

La conversion RVB en CMJN est un peu plus compliquée car il n'y a pas de formule de pixel un à un.

Une autre alternative lors de l'utilisation de tels schémas de codage exotiques consiste à inventer votre propre format de fichier image. Stockez simplement les tableaux de pixels dans un format de fichier personnalisé suffixé par ".yuv" ou ".cmyk". Il n'y a pas besoin d'en-tête lorsque vous savez quel type de format d'image les pixels auront, allez-y et lisez-le simplement dans votre banc d'essai.

Vous pouvez toujours intégrer une conversion logicielle dans votre flux de conception. Par exemple, convertissez automatiquement une image PNG au format BMP à l'aide d'un logiciel standard de conversion d'image en ligne de commande avant le démarrage de la simulation. Ensuite, lisez-le dans votre testbench en utilisant VHDL comme vous l'avez appris dans cet article.


VHDL

  1. C# en utilisant
  2. Gestion des fichiers C
  3. Classe de fichier Java
  4. Comment initialiser la RAM à partir d'un fichier à l'aide de TEXTIO
  5. Java BufferedReader :comment lire un fichier en Java avec un exemple
  6. Python JSON :encoder (dumps), décoder (chargers) et lire le fichier JSON
  7. Opérations Verilog File IO
  8. C - Fichiers d'en-tête
  9. Caméra plénoptique