Image sequence diffing

This was another script I wrote for An over-analytical analysis of style. This script runs through a generated sequence of frame images and performs a visual diff between adjacent frames. I used this to measure how much movement/change there was within the two movies I was analyzing.

Instructions

  1. Copy the code below into a file on your machine. Let’s name it image-sequence-diffing.js
  2. Install Node.js
  3. Fire up a Terminal window and cd to the directory where you saved the image-sequence-diffing.js file
  4. This script requires looks-same. To install, cd into your project’s directory and run npm i looks-same --save-dev in your terminal. I try to avoid using third-party libraries, but I ended up taking a shortcut for this project.
  5. Edit all constants to fit your needs
  6. Run node image-sequence-diffing.js in your terminal
  7. Read out the results returned in the terminal
            
import looksSame from 'looks-same';
import fs from 'fs';

// Toggle whether diff data should be generated and exported to JSON. This is an expensive operation, so it's helpful to set this to false if you're only tuning the visuals
const EXPORT_JSON = true
// The directory where the frame images exist 
const SEQUENCE_DIRECTORY_PATH = './frames'
// The naming of each frame (including separator). The script depends on a sequential number for each image with no leading zeros. Example: `frame_1, frame_2, frame_3`
const SEQUENCE_IMAGE_FILE_PREFIX = 'frame_'
// The image type. I've only tested this with jpg images
const SEQUENCE_IMAGE_EXTENSION = '.jpg'
// The frame to start with. This can be helpful if you want to isolate the visualization to specific areas of a sequence
const START_FRAME = 1
// The frame to end with. If set to `-1`, the script will run until there are no more frames to load
const END_FRAME = -1
// The name/location for the generated JSON file containing all frame diff values. This JSON file is used to generate the final visualization
const EXPORT_JSON_PATH = './data.json'

// The anchor HSL hue for each cell. 
const BASE_HUE = - 70
// The amount of hue latitude each cell color can shift based on its value
const HUE_RANGE = 110
// The saturation of each cell. Saturation remains consistent across each cell color
const BASE_SAT = 80
// The anchor HSL lightness for each cell. 
const BASE_LIGHTNESS = 10
// The amount of lightness latitude each cell color can shift based on its value
const LIGHTNESS_RANGE = 80
// The number of cell rows the visualization will generate prior to wrapping
const ROWS = 60
// The width/height of each cell
const CELL_SIZE = 30
// The name/location for the generated SVG file
const EXPORT_SVG_PATH = './dataviz.svg'


async function diffFrames(imagePath, imageName, jsonFilePath, options={}) {
  
  let defaults = {
    imageExtension: SEQUENCE_IMAGE_EXTENSION,
    startFrame: START_FRAME,
    endFrame: END_FRAME
  };
  options = Object.assign({}, defaults, options);

  let frame = options.startFrame + 1;
  let diffs = []

  while(fs.existsSync(imagePath + '/' + imageName + '' + frame + options.imageExtension)) {
    let diff = await looksSame(imagePath + '/' + imageName + '' + (frame - 1) + options.imageExtension, imagePath + '/' + imageName + '' + (frame) + options.imageExtension, {createDiffImage: true});
    if(diff) {

      let diffPercent = (diff.differentPixels / diff.totalPixels);
      diffs.push({frame: frame, difference: diffPercent})      
    }
    frame++;

    if(options.endFrame != -1 && frame > options.endFrame) {
      break;
    }

  }

  fs.writeFileSync(jsonFilePath, JSON.stringify(diffs))
  
  return diffs;
}


function visualizeDifferences(diffArray, svgFilePath, options = {}) {
  let svg = ''
  let column = 0;
  let row = 0;
  let diffSum = 0;
  let differences = []
  let defaults = {
    baseHue: BASE_HUE,
    hueRange: HUE_RANGE,
    baseSat: BASE_SAT,
    baseLightness: BASE_LIGHTNESS,
    lightnessRange: LIGHTNESS_RANGE,
    rows: ROWS,
    cellSize: CELL_SIZE

  };
  options = Object.assign({}, defaults, options);

  for(var i = 0; i < diffArray.length; i++) {
 
    let diffPercent = (diffArray[i].difference == null) ? 0 : diffArray[i].difference;
    diffSum+=diffPercent;
    let hue = (options.hueRange * diffPercent) + options.baseHue;

    differences.push(Number(diffPercent.toFixed(3)))

    if (hue > 360) {
      hue -= 360;
    }
    else if (hue < 0) {
        hue += 360;
    }

    let sat = options.baseSat;
    let lightness = (options.lightnessRange * diffPercent) + options.baseLightness;

    if(row === options.rows) {
      row = 0;
      column++
    }

    svg += '<rect x="' + column * options.cellSize + '" y="' + row * options.cellSize + '" width="' + options.cellSize +'" height="' + options.cellSize + '" style="fill: hsl(' + hue + ', ' + sat + '%, ' + lightness + '%);" />' 
    row++;
  }
  
  svg = '<svg width="' + (column + 1) * options.cellSize + '" height="' + (options.rows) * options.cellSize + '" preserveAspectRatio="none" viewBox="0 0 ' +  (column + 1) * options.cellSize  + ' ' + (options.rows) * options.cellSize + '" fill="#000" xmlns="http://www.w3.org/2000/svg">' + svg + '</svg>'
  fs.writeFileSync(svgFilePath, svg)
}

async function generateDiff(exportJSON, sequenceDirectory, sequenceFileName, jsonFilePath, svgFilePath, options = {}) {

  let defaults = {
    imageExtension: SEQUENCE_IMAGE_EXTENSION,
    startFrame: START_FRAME,
    endFrame: END_FRAME,
    baseHue: BASE_HUE,
    hueRange: HUE_RANGE,
    baseSat: BASE_SAT,
    baseLightness: BASE_LIGHTNESS,
    lightnessRange: LIGHTNESS_RANGE,
    rows: ROWS,
    cellSize: CELL_SIZE
  };
  options = Object.assign({}, defaults, options);


  if(exportJSON) {
    await diffFrames(sequenceDirectory, sequenceFileName, jsonFilePath, options)
  }

  let diffData = JSON.parse(fs.readFileSync(jsonFilePath))
  visualizeDifferences(diffData, svgFilePath, options)
}

generateDiff(
  EXPORT_JSON,
  SEQUENCE_DIRECTORY_PATH, 
  SEQUENCE_IMAGE_FILE_PREFIX, 
  EXPORT_JSON_PATH, 
  EXPORT_SVG_PATH
)