2021-09-11

A YUV420 image viewer on Raspberry Pi: my first GTK program

Recently, I revived some camera programming on my old Raspberry Pi 3 (see link).  Since it generates many YUV (actually YUV420) image formats and I found there is no viewer on Raspberry Pi (maybe I am ignorant), I start to code one myself.  (By the way, I have been using a Windows version found in sourceforge.net)

While YUV format is good for image data processing, it lacks many meta data.  The dimensions cannot be derived from the file.  Worst still, if there is any programming error, I simply got a corrupted image.

Frankly speaking, I have no graphical programming experience on Linux.  After some information gathering, I try to use GTK.  Luckily the learning curve is not deep.

The following are my codes

/*
File: yuv420viewer.c
Date: 2021-09-11
By: waihungmm
Description: A YUV420 image viewer based on GTK
Environment: originally compiler on Raspberry Pi
*/

#include <gtk/gtk.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>

void on_destroy (GtkWidget *widget G_GNUC_UNUSED, gpointer user_data G_GNUC_UNUSED)
{
gtk_main_quit ();
}

int main (int argc, char *argv[])
{
char *filename;
FILE *f;
struct stat st;
int width = -1;
int height = -1;
int stride_width;
int stride_height;
int yuv_file_size;
unsigned char *rgb_buffer;
unsigned char *yuv_buffer;
unsigned char *u_buffer;
unsigned char *v_buffer;

int my_opt_err = 0;
int opt;
while ((opt = getopt (argc, argv, ":h:w:")) != -1) {
  // the first character of optstring is : to use silent error reporting mode
  switch (opt) {
    case 'w':
      width = atoi (optarg);
      if (width <= 0) {
        printf ("Error: Invalid width parameter (%s)\n", optarg);
        my_opt_err = 1;
      }
      break;
    case 'h':
      height = atoi (optarg);
      if (height <= 0) {
        printf ("Error: Invalid height parameter (%s)\n", optarg);
        my_opt_err = 1; 
        }
      break;
    case '?':
      if (optopt == 'h') {
        printf ("option -h needs a value\n");
        my_opt_err = 1;
        }
      else if (optopt == 'w') {
        printf ("option -w needs a value\n");
        my_opt_err = 1;
        }
      else {
        printf ("unknown option %c\n", optopt);
        my_opt_err = 1;
        }
      break;
    } // switch
  } // while

if (width == -1) {
  printf ("Error: no width paramter is given\n");
  my_opt_err = 1;
  }

if (height == -1) {
  printf ("Error: no height paramter is given\n");
  my_opt_err = 1;
  }

if (argc == optind) {
  printf ("Error: no filename is given\n");
  my_opt_err = 1;
  }
else if (optind == (argc-1))
  filename = argv[optind];
else {
  printf ("Error: too many filenames are given\n");
  my_opt_err = 1;
  }

if (my_opt_err) {
  printf ("Usage: %s -w yuv_width -h yuv_height yuv_filename\n", argv[0]);
  return 0;
  }

if (stat(filename, &st) == -1) {
  printf ("Error: Cannot access file \"%s\"\n", filename);
  return 0;
  }

if ((width % 32) == 0)
  stride_width = width;
else
  stride_width = ((width / 32) + 1) * 32;

if ((height % 16) == 0)
  stride_height = height;
else
  stride_height = ((height / 16) + 1) * 16;

yuv_file_size = st.st_size;
if (yuv_file_size != (stride_width * stride_height * 3 / 2)) {
  printf ("Error: filesize (%d) is not 1.5 multiple of stride width (%d) x stride height (%d)\n", yuv_file_size, stride_width, stride_height);
  return 0;
  }

rgb_buffer = (unsigned char*)malloc(width * height * 3);
yuv_buffer = (unsigned char*)malloc(stride_width * stride_height * 3 / 2);
// set offset to access the u data and v data
u_buffer = yuv_buffer + stride_width * stride_height;
v_buffer = u_buffer + (stride_width * stride_height) / 4;

f = fopen (filename, "r");
if (f == NULL) {
  printf ("Error: cannot open file \"%s\"\n", filename);
  return 0;
  }

int length = fread (yuv_buffer, 1, yuv_file_size, f);
fclose (f);

if (length != yuv_file_size) {
  printf ("Error: cannot read the whole file \"%s\" successfully\n", filename);
  }

int i,j;
int y, u, v;
int r, g, b;
for (i=0; i< height; i++) {
 for (j=0; j< width; j++) {
  y = yuv_buffer[i*stride_width+j];
  u = u_buffer[(i/2)*(stride_width/2)+(j/2)];
  v = v_buffer[(i/2)*(stride_width/2)+(j/2)];
  r = (1164 * (y-16) + 1596 * (v-128)) / 1000;
  g = (1164 * (y-16) - 391 * (u-128) - 813 * (v-128)) / 1000;
  b = (1164 * (y-16) + 2018 * (u-128) ) / 1000;;
  if (r < 0) r = 0;
  if (g < 0) g = 0;
  if (b < 0) b = 0;
  if (r > 255) r = 255;
  if (g > 255) g = 255;
  if (b > 255) b = 255;
  rgb_buffer [3*(i*width+j) ] = r;
  rgb_buffer [3*(i*width+j)+1] = g;
  rgb_buffer [3*(i*width+j)+2] = b;
  }
 }

// the following are the gtk stuff

GdkPixbuf *pixbuf;
GtkWidget *image;
gtk_init (&argc, &argv);
GtkWidget *win = gtk_window_new (GTK_WINDOW_TOPLEVEL);
pixbuf = gdk_pixbuf_new_from_data (rgb_buffer, GDK_COLORSPACE_RGB, FALSE, 8, width, height, (width)*3, NULL, NULL);
gtk_window_set_title (GTK_WINDOW (win), filename);

int screen_width = gdk_screen_width();
int screen_height = gdk_screen_height();

if ((width > screen_width) || (height > screen_height))
gtk_widget_set_size_request (win, screen_width-10, screen_height-75);
else
gtk_widget_set_size_request (win, width+10, height+10);

GtkWidget *scrolled_window;
scrolled_window = gtk_scrolled_window_new (NULL, NULL);
gtk_container_set_border_width (GTK_CONTAINER (scrolled_window), 1);

gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);

image = gtk_image_new_from_pixbuf (pixbuf);

gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (scrolled_window), image);

gtk_container_add(GTK_CONTAINER (win), scrolled_window);

gtk_widget_show_all (win);
g_signal_connect (win, "destroy", G_CALLBACK(on_destroy), NULL);
gtk_main ();
free (yuv_buffer);
free (rgb_buffer);
return 0;
}

The compilation step is simply as follows:
gcc `pkg-config --cflags --libs gtk+-2.0` -o yuv420viewer yuv420viewer.c

The following is a sample screen dump