Bashing Aspect Ratio Mess

I got a large pile of photographs to work with which have several aspect ratios, mostly 4:3, 3:2 and 16:9. A large part of the work was automatable with a shell-script or two and the help of ImageMagick. As some of these scripts depend on the specific aspect ratio I wrote a little script to check for it.

The program identify does what the name suggests…well, not really, it just gives every statistic available from the picture (try the “-verbose” option) including the width and the height and the ability to do some math.

identify -format "%[fx:w/h]	%M" *jpg

The empty space in the format string is a tab. If you do it in the bash try ^V (ctrl+v and TAB) to insert one.

$ identify -format "%[fx:w/h]	%M" *jpg
$ 1.3335	001.jpg
$ 1.33351	002.jpg
$ 0.749729	003.jpg
$ 0.75013	004.jpg
$ 0.75	005.jpg

“As expected,” you might say, “The number of pixels are not necessarily multiples of 3 and 4!” Which is right, of course. Hence this script.

We need the program identify, some program to do floating point math, I chose awk, and an editor for convenience.
No bash specialities are needed, the first line can be #!/bin/sh.

#!/bin/sh

A sign of good style is to give the script full paths to the programs

# Adjust if necessary
IDENTIFY=/usr/local/bin/identify;
AWK=/usr/bin/awk;

To avoid clobber this script works on a single file only but it should be simple to wrap a loop around.

# Input file is just the first argument given
INFILE=$1;

Some sanity checks are obligatory. Short example:

# Some sanity checks
if [ $# -eq 0 ]; then
  echo "Usage: $0 picture";
  exit 0;
elif [ $# -gt 11 ]; then
  echo "Usage: $0 picture";
  echo "That means: only one picture! Sheesh!";
  exit 0;
fi;

We do the computation with awk, identify is for getting the dimensions only.

# Get the width and the height at once, send errors from <code>stderr</code>
# to /dev/null.
INFILE_FORMAT=$($IDENTIFY -format '%[width]x%[height]' $INFILE  2>/dev/null);

The variable $INFILE_FORMAT holds either the dimension of the picture in the format WxH or nothing.

# "identify" checks it, so we don't need to.
if [ -z $INFILE_FORMAT ]; then
  echo "\"$INFILE\" is not a picture?";
  exit 0;
fi;

The format WxH is easy to splice with cut from the GNU textutils. The argument -dx sets the delimiter at which to splice to the letter “x” and the arguments -f1 and -f2 get the first and second field respectively.

INFILE_WIDTH=$(echo "$INFILE_FORMAT"| cut -dx -f1);
INFILE_HEIGHT=$(echo "$INFILE_FORMAT"| cut -dx -f2);

It can be done without an external program by setting the IFS (Input Field Seperator) variable. It is not deemed good style to mess around with global variables and the GNU textutils are available almost anywhere. But here is an example with IFS manipulation:

# example input
INPUT="123x345";
# make a backup of the global IFS variable
IFSOLD=$IFS;
# set IFS to the value we want 
IFS='x';
# loop over all ingredients
for i in $INPUT;do
   # do something with it here
   echo $i;
done;
# reset IFS to original
IFS=$IFSOLD;

Of course we could just change the format given to identity and use space instead of the letter “x”, but, no, that would be way to simple 😉

After all of the preparing we are finally ready to do some real work: branch according to where the bigger value is.

# Do the actual work
if [ $INFILE_WIDTH -ge $INFILE_HEIGHT ];then
  # landscape/square
  ASPECT_RATIO=$($AWK "BEGIN {printf \"%.2f\n\",$INFILE_WIDTH/$INFILE_HEIGHT}");
else
  # portrait
  ASPECT_RATIO=$($AWK "BEGIN {printf \"%.2f\n\",$INFILE_HEIGHT/$INFILE_WIDTH}");
fi

The printf function of awk rounds according to IEEE-745 and uses the format known from printf(3). We use two decimal digits of precision here. The final output is made into a string. It is probably a good idea to do as I did and put your scripts/commands there instead of just echo "some string", e.g.: mv -v $1 "directory where it belongs".

# Excerpt. See http://en.wikipedia.org/wiki/Aspect_ratio_%28image%29 and
# http://en.wikipedia.org/wiki/List_of_common_resolutions for more.
case $ASPECT_RATIO in
  1.00)
       echo "1to1"
       ;;
  1.17)
       echo "7to6"
       ;;
  1.22)
       echo "11to9" # video conference CIF
       ;;
  1.25)
       echo "5to4"
       ;;
  1.33)
       echo "4to3"
       ;;
  1.40)
       echo "7to5"
       ;;
  1.50)
       echo "3to2"
       ;;
  1.60)
       echo "16to10" # older computer displays, non-HD, e.g.: 1280×800
       ;;
  1.78)
       echo "16to9"
       ;;
  1.90)
       echo "256to135" # 4K format (4096×2160) 
       ;;
  3.00)
       echo "3to1" # quite common panorama format
       ;;
  4.00)
       echo "4to1" # quite uncommon panorama format
       ;;
  *)
       echo "unknown value: $ASPECT_RATIO"
       ;;
esac
# aaaand let's get outta here, now!
exit 1;

A bit too much hassle for a couple of pictures? Not if that couple has a six digit number 😉

But identity is quite slow and large; any alternatives?
Well, there is jhead which is quite fast and gives with the -c option a short overview with

"name of file in double quotes" widthxheight (exposure_time) f/fstop_float

The delimiter between width and height is the letter “x”, how apt 😉
It is quite fast but you need some code to get at the fruit hidden inside. And it can spit out more information in this format if available (e.g.: flash) as I found out when I looked into the code (file exif.c at the very end):

//--------------------------------------------------------------------------
// Summarize highlights of image info on one line (suitable for grep-ing)
//--------------------------------------------------------------------------
void ShowConciseImageInfo(void)
{
    printf("\"%s\"",ImageInfo.FileName);

    printf(" %dx%d",ImageInfo.Width, ImageInfo.Height);

    if (ImageInfo.ExposureTime){
        if (ImageInfo.ExposureTime <= 0.5){
            printf(" (1/%d)",(int)(0.5 + 1/ImageInfo.ExposureTime));
        }else{
            printf(" (%1.1f)",ImageInfo.ExposureTime);
        }
    }

    if (ImageInfo.ApertureFNumber){
        printf(" f/%3.1f",(double)ImageInfo.ApertureFNumber);
    }

    if (ImageInfo.FocalLength35mmEquiv){
        printf(" f(35)=%dmm",ImageInfo.FocalLength35mmEquiv);
    }

    if (ImageInfo.FlashUsed >= 0 && ImageInfo.FlashUsed & 1){
        printf(" (flash)");
    }

    if (ImageInfo.IsColor == 0){
        printf(" (bw)");
    }

    printf("\n");
}

Or write it yourself. It is a bit more complicated than looking for the right field in
the JIFF file but not much. You can either take jhead and change it
diff -u jhead-2.97 jhead-2.97-cz

diff -u jhead-2.97/exif.c jhead-2.97-cz/exif.c
--- jhead-2.97/exif.c	2013-01-30 18:02:56.000000000 +0100
+++ jhead-2.97-cz/exif.c	2014-08-24 02:15:05.000000000 +0200
@@ -1591,3 +1591,10 @@
 
     printf("\n");
 }
+//--------------------------------------------------------------------------
+// Show image dimensions widthxheight
+//-------------------------------------------------------------------------
+void ShowConciseImageDimensions(void)
+{
+    printf("%dx%d\n",ImageInfo.Width, ImageInfo.Height);
+}
diff -u jhead-2.97/jhead.c jhead-2.97-cz/jhead.c
--- jhead-2.97/jhead.c	2013-01-30 18:02:56.000000000 +0100
+++ jhead-2.97-cz/jhead.c	2014-08-24 02:05:34.000000000 +0200
@@ -55,6 +55,7 @@
 static int Quiet        = FALSE;    // Be quiet on success (like unix programs)
        int DumpExifMap  = FALSE;
 static int ShowConcise  = FALSE;
+static int ShowConciseDimensions  = FALSE;
 static int CreateExifSection = FALSE;
 static char * ApplyCommand = NULL;  // Apply this command to all images.
 static char * FilterModel = NULL;
@@ -886,8 +887,10 @@
     }
 
     FileSequence += 1; // Count files processed.
-
-    if (ShowConcise){
+    if (ShowConciseDimensions){
+        ShowConciseImageDimensions();
+    }
+    else if (ShowConcise){
         ShowConciseImageInfo();
     }else{
         if (!(DoModify) || ShowTags){
@@ -1311,6 +1314,7 @@
            "  -exifmap   Dump header bytes, annotate.  Pipe thru sort for better viewing\n"
            "  -se        Supress error messages relating to corrupt exif header structure\n"
            "  -c         concise output\n"
+           "  -d         image size: widthxheight\n"
            "  -nofinfo   Don't show file info (name/size/date)\n"
 
            "\nFILE MATCHING AND SELECTION:\n"
@@ -1462,6 +1466,8 @@
             SupressNonFatalErrors = TRUE;
         }else if (!strcmp(arg,"-c")){
             ShowConcise = TRUE;
+        }else if (!strcmp(arg,"-d")){
+            ShowConciseDimensions = TRUE;
         }else if (!strcmp(arg,"-nofinfo")){
             ShowFileInfo = 0;
 
diff -u jhead-2.97/jhead.h jhead-2.97-cz/jhead.h
--- jhead-2.97/jhead.h	2013-01-30 18:02:56.000000000 +0100
+++ jhead-2.97-cz/jhead.h	2014-08-24 02:12:14.000000000 +0200
@@ -160,6 +160,7 @@
 void process_EXIF (unsigned char * CharBuf, unsigned int length);
 void ShowImageInfo(int ShowFileInfo);
 void ShowConciseImageInfo(void);
+void ShowConciseImageDimensions(void);
 const char * ClearOrientation(void);
 void PrintFormatNumber(void * ValuePtr, int Format, int ByteCount);
 double ConvertAnyFormat(void * ValuePtr, int Format);

or write it completely yourself. I ripped the necessary parts from jhead
and put it in one file:

/*
  Shamelessly stolen from jhead-2.97 http://www.sentex.net/~mwandel/jhead/
  (http://www.sentex.net/~mwandel/jhead/jhead-2.97.tar.gz)
  written by Matthias Wandel and Public Domain.
  The whole program linked above is very fast, faster than exiftools, so
  if not the whole whistles and belöls of exiftool is needed, jhead is a
  good and fast alternative and I heartly recommend it.

  This extract prints only the dimensions of the JPEG in a widthxheight
  format but look at the function process_SOFn() for the information
  if the file is BW or colored and if it is colored, how.
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <ctype.h>

#include <utime.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <limits.h>

#ifndef TRUE
#   define TRUE 1
#   define FALSE 0
#endif

#ifndef PATH_MAX
#   define PATH_MAX 1024
#endif

typedef struct {
    unsigned char *Data;
    int Type;
    unsigned Size;
} Section_t;

typedef struct {
    char FileName[PATH_MAX + 1];
    time_t FileDateTime;
    unsigned FileSize;
    int Height, Width;
    int IsColor;
} ImageInfo_t;


// prototypes 
void ErrFatal(const char *msg);
void ErrNonfatal(const char *msg, int a1, int a2);
void ShowConciseImageInfo(void);
int ReadJpegSections(FILE * infile);
void DiscardData(void);
int ReadJpegFile(const char *FileName);
int Get16m(const void *Short);
void process_SOFn(const unsigned char * Data);
//--------------------------------------------------------------------------
// JPEG markers consist of one or more 0xFF bytes, followed by a marker
// code byte (which is not an FF).  Here are the marker codes of interest
// in this program.  (See jdmarker.c for a more complete list.)
//--------------------------------------------------------------------------

#define M_SOF0  0xC0		// Start Of Frame N
#define M_SOF1  0xC1		// N indicates which compression process
#define M_SOF2  0xC2		// Only SOF0-SOF2 are now in common use
#define M_SOF3  0xC3
#define M_SOF5  0xC5		// NB: codes C4 and CC are NOT SOF markers
#define M_SOF6  0xC6
#define M_SOF7  0xC7
#define M_SOF9  0xC9
#define M_SOF10 0xCA
#define M_SOF11 0xCB
#define M_SOF13 0xCD
#define M_SOF14 0xCE
#define M_SOF15 0xCF
#define M_SOI   0xD8		// Start Of Image (beginning of datastream)
#define M_EOI   0xD9		// End Of Image (end of datastream)
#define M_SOS   0xDA		// Start Of Scan (begins compressed data)
#define M_JFIF  0xE0		// Jfif marker
#define M_EXIF  0xE1		// Exif marker.  Also used for XMP data!
#define M_XMP   0x10E1		// Not a real tag (same value in file as Exif!)
#define M_COM   0xFE		// COMment
#define M_DQT   0xDB		// Define Quantization Table
#define M_DHT   0xC4		// Define Huffmann Table
#define M_DRI   0xDD
#define M_IPTC  0xED		// IPTC marker
// Storage for simplified info extracted from file.
ImageInfo_t ImageInfo;
int ShowTags;

Section_t *Sections = NULL;
int SectionsAllocated;
int SectionsRead;
int HaveAll;
int SupressNonFatalErrors = FALSE;

int FilesMatched;
int FileSequence;

const char *CurrentFile;

const char *progname;		// program name for error messages
#define PSEUDO_IMAGE_MARKER 0x123;	// Extra value.
//--------------------------------------------------------------------------
// Get 16 bits motorola order (always) for jpeg header stuff.
//--------------------------------------------------------------------------
int Get16m(const void *Short)
{
    return (((unsigned char *) Short)[0] << 8) | ((unsigned char *) Short)[1];
}



//--------------------------------------------------------------------------
// Process a SOFn marker.  This is useful for the image dimensions
//--------------------------------------------------------------------------
void process_SOFn(const unsigned char * Data)
{
    int data_precision, num_components;

    data_precision = Data[2];
    ImageInfo.Height = Get16m(Data + 3);
    ImageInfo.Width = Get16m(Data + 5);
    num_components = Data[7];

    if (num_components == 3) {
	ImageInfo.IsColor = 1;
    } else {
	ImageInfo.IsColor = 0;
    }
    if (ShowTags) {
	printf
	    ("JPEG image is %uw * %uh, %d color components, %d bits per sample\n",
	     ImageInfo.Width, ImageInfo.Height, num_components, data_precision);
    }
}

//--------------------------------------------------------------------------
// Check sections array to see if it needs to be increased in size.
//--------------------------------------------------------------------------
void CheckSectionsAllocated(void)
{
    if (SectionsRead > SectionsAllocated) {
	ErrFatal("allocation screwup");
    }
    if (SectionsRead >= SectionsAllocated) {
	SectionsAllocated += SectionsAllocated / 2;
	Sections =
	    (Section_t *) realloc(Sections,
				  sizeof(Section_t) * SectionsAllocated);
	if (Sections == NULL) {
	    ErrFatal("could not allocate data for entire image");
	}
    }
}

//--------------------------------------------------------------------------
// Error exit handler
//--------------------------------------------------------------------------
void ErrFatal(const char *msg)
{
    fprintf(stderr, "\nError : %s\n", msg);
    if (CurrentFile)
	fprintf(stderr, "in file '%s'\n", CurrentFile);
    exit(EXIT_FAILURE);
}

//--------------------------------------------------------------------------
// Report non fatal errors.  Now that microsoft.net modifies exif headers,
// there's corrupted ones, and there could be more in the future.
//--------------------------------------------------------------------------
void ErrNonfatal(const char *msg, int a1, int a2)
{
    if (SupressNonFatalErrors)
	return;

    fprintf(stderr, "\nNonfatal Error : ");
    if (CurrentFile)
	fprintf(stderr, "'%s' ", CurrentFile);
    fprintf(stderr, msg, a1, a2);
    fprintf(stderr, "\n");
}

int ReadJpegSections(FILE * infile)
{
    int a;

    a = fgetc(infile);

    if (a != 0xff || fgetc(infile) != M_SOI) {
	return FALSE;
    }

    for (;;) {
	int itemlen;
	int prev;
	int marker = 0;
	int ll, lh, got;
	unsigned char *Data;

	CheckSectionsAllocated();

	prev = 0;
	for (a = 0;; a++) {
	    marker = fgetc(infile);
	    if (marker != 0xff && prev == 0xff)
		break;
	    if (marker == EOF) {
		ErrFatal("Unexpected end of file");
	    }
	    prev = marker;
	}

	if (a > 10) {
	    ErrNonfatal("Extraneous %d padding bytes before section %02X",
			a - 1, marker);
	}

	Sections[SectionsRead].Type = marker;

	// Read the length of the section.
	lh = fgetc(infile);
	ll = fgetc(infile);
	if (lh == EOF || ll == EOF) {
	    ErrFatal("Unexpected end of file");
	}

	itemlen = (lh << 8) | ll;

	if (itemlen < 2) {
	    ErrFatal("invalid marker");
	}

	Sections[SectionsRead].Size = itemlen;

	Data = (unsigned char *) malloc(itemlen);
	if (Data == NULL) {
	    ErrFatal("Could not allocate memory");
	}
	Sections[SectionsRead].Data = Data;

	// Store first two pre-read bytes.
	Data[0] = (unsigned char) lh;
	Data[1] = (unsigned char) ll;

	got = fread(Data + 2, 1, itemlen - 2, infile);	// Read the whole section.
	if (got != itemlen - 2) {
	    ErrFatal("Premature end of file?");
	}
	SectionsRead += 1;

	switch (marker) {

	case M_SOS:		// stop before hitting compressed data 
	    return TRUE;

	case M_DQT:
	case M_DHT:
	    break;


	case M_EOI:		// in case it's a tables-only JPEG stream
	    fprintf(stderr, "No image in jpeg!\n");
	    return FALSE;

	case M_COM:		// Comment section
	    free(Sections[--SectionsRead].Data);
	    break;

	case M_JFIF:
	    free(Sections[--SectionsRead].Data);
	    break;

	case M_EXIF:
	    free(Sections[--SectionsRead].Data);
	    break;

	case M_IPTC:
	    free(Sections[--SectionsRead].Data);
	    break;

	case M_SOF0:
	case M_SOF1:
	case M_SOF2:
	case M_SOF3:
	case M_SOF5:
	case M_SOF6:
	case M_SOF7:
	case M_SOF9:
	case M_SOF10:
	case M_SOF11:
	case M_SOF13:
	case M_SOF14:
	case M_SOF15:
	    process_SOFn(Data);
	    break;
	default:
	    break;
	}
    }
    return TRUE;
}

//--------------------------------------------------------------------------
// Discard read data.
//--------------------------------------------------------------------------
void DiscardData(void)
{
    int a;

    for (a = 0; a < SectionsRead; a++) {
	free(Sections[a].Data);
    }

    memset(&ImageInfo, 0, sizeof(ImageInfo));
    SectionsRead = 0;
    HaveAll = 0;
}

//--------------------------------------------------------------------------
// Read image data.
//--------------------------------------------------------------------------
int ReadJpegFile(const char *FileName)
{
    FILE *infile;
    int ret;

    infile = fopen(FileName, "rb");	// Unix ignores 'b', windows needs it.

    if (infile == NULL) {
	fprintf(stderr, "can't open '%s'\n", FileName);
	return FALSE;
    }

    // Scan the JPEG headers.
    ret = ReadJpegSections(infile);
    if (!ret) {
	fprintf(stderr, "Not JPEG: %s\n", FileName);
    }

    fclose(infile);

    if (ret == FALSE) {
	DiscardData();
    }
    return ret;
}

//--------------------------------------------------------------------------
// Do selected operations to one file at a time.
//--------------------------------------------------------------------------
void ProcessFile(const char *FileName)
{
    if (strlen(FileName) >= PATH_MAX - 1) {
	// Protect against buffer overruns in strcpy / strcat's on filename
	ErrFatal("filename too long");
    }
    CurrentFile = FileName;
    FilesMatched = 1;

    if (Sections == NULL) {
	Sections = (Section_t *) malloc(sizeof(Section_t) * 5);
	SectionsAllocated = 5;
    }

    SectionsRead = 0;

    memset(&ImageInfo, 0, sizeof(ImageInfo));
    // Store file date/time.
    {
	struct stat st;
	if (stat(FileName, &st) >= 0) {
	    ImageInfo.FileDateTime = st.st_mtime;
	    ImageInfo.FileSize = st.st_size;
	} else {
	    ErrFatal("No such file");
	}
    }

    strncpy(ImageInfo.FileName, FileName, PATH_MAX);

    if (!ReadJpegFile(FileName))
	return;

    FileSequence += 1;		// Count files processed.
    printf("%ix%i\n", ImageInfo.Width, ImageInfo.Height);
    DiscardData();
    return;
}

//--------------------------------------------------------------------------
// The main program.
//--------------------------------------------------------------------------
int main(int argc, char **argv)
{
    int argn = 1;
    progname = argv[0];


    FileSequence = 0;
    for (; argn < argc; argn++) {
	FilesMatched = FALSE;
	ProcessFile(argv[argn]);
	if (!FilesMatched) {
	    fprintf(stderr, "Error: No files matched '%s'\n", argv[argn]);
	}

    }

    if (FileSequence == 0) {
	return EXIT_FAILURE;
    } else {
	return EXIT_SUCCESS;
    }
}

Compile with

$ ls
image_dimensions.c
$ gcc -O3 -W -Wall  -std=c99  image_dimensions.c -o image_dimensions
$ strip image_dimensions

The single difference is that the single file solution is ten times smaller than the full jhead program. But that does not say anything, the full jhead program is just 78kb “large”. Runtime is about the same.

Benchmark with exiftool -s -ImageSize, identify -format '%[width]x%[height]', and jhead -d run in a for-loop in a shell. Runtime of for-loop measured by simply using echo. Sytem had medium load (streaming radio).

Program Real User Sys
exiftool -s -ImageSize 0m14.437s 0m8.917s 0m1.000s
identify -format ‘%[width]x%[height]’ 2m22.363s 0m55.419s 0m16.489s
jhead -d 0m0.299s 0m0.064s 0m0.068s
echo 0m0.031s 0m0.004s 0m0.000s

I expected the ranking, identify has to load the whole Image Magick stuff, exiftool parses the whole EXIF data and jhead parses only a subset and is so small that it can be kept in the processor cache most of the time if not the whole time.
But I am still quite astonished by the very large gaps!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s