/* * Find motion vectors * * Copyright (c) 2016 Project Nayuki * All rights reserved. Contact Nayuki for licensing. * https://www.nayuki.io/page/go-train-acceleration-analyzed-by-video */ import java.awt.Point; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import javax.imageio.ImageIO; public final class FindMotionVectors { /*---- Configuration ----*/ private static final int NUM_FRAMES = 967; private static final int IMAGE_SCALE = 4; private static final int IMAGE_WIDTH = 1280 * IMAGE_SCALE; private static final int IMAGE_HEIGHT = 192 * IMAGE_SCALE; private static final Point SEARCH_RANGE = new Point(4, 2); private static final int SEARCH_WINDOW_WIDTH = 640 * IMAGE_SCALE; private static final int SEARCH_WINDOW_HEIGHT = 150 * IMAGE_SCALE; private static final int SEARCH_WINDOW_TOP = 16 * IMAGE_SCALE; private static File framesDir; /*---- Main application: High-level search logic ----*/ public static void main(String[] args) throws IOException { // Handle command line arguments if (args.length != 1) { System.err.println("Usage: java FindMotionVectors FramesDir"); System.exit(1); return; } framesDir = new File(args[0]); if (!framesDir.isDirectory()) throw new RuntimeException("Invalid frames directory"); // First search displacements for all adjacent frames System.out.println("From frame number\tTo frame number\tDisplacement x\tDisplacement y"); Point[][] knownDisplacements = new Point[NUM_FRAMES - 1][NUM_FRAMES]; { Point prevDisplacement = new Point(0, 0); for (int i = 1; i < NUM_FRAMES; i++) { Point displacement = searchForDisplacement(i - 1, i, prevDisplacement, SEARCH_RANGE); knownDisplacements[i - 1][i] = displacement; System.out.printf("%d\t%d\t%d\t%d%n", i - 1, i, displacement.x, displacement.y); prevDisplacement = displacement; } } // Perform deep search for each starting frame AtomicInteger startFrame = new AtomicInteger(0); for (int i = 0, numThreads = Runtime.getRuntime().availableProcessors(); i < numThreads; i++) { new Thread() { public void run() { try { while (true) { int sf = startFrame.getAndIncrement(); if (sf + 1 >= NUM_FRAMES) break; doDeepSearch(sf, knownDisplacements); } } catch (IOException e) { e.printStackTrace(); System.exit(1); } } }.start(); } } // Updates knownDisplacements[startFrame][various indices] and prints lines to standard output. private static void doDeepSearch(int startFrame, Point[][] knownDisplacements) throws IOException { Point velocity = knownDisplacements[startFrame][startFrame + 1]; Point prevDisplacement = new Point(velocity); for (int endFrame = startFrame + 2; endFrame < NUM_FRAMES; endFrame++) { Point displacement = new Point(prevDisplacement.x + velocity.x, prevDisplacement.y + velocity.y); // Initial estimate try { displacement = searchForDisplacement(startFrame, endFrame, displacement, SEARCH_RANGE); } catch (IllegalArgumentException e) { break; } knownDisplacements[startFrame][endFrame] = displacement; synchronized(FindMotionVectors.class) { System.out.printf("%d\t%d\t%d\t%d%n", startFrame, endFrame, displacement.x, displacement.y); } velocity = new Point(displacement.x - prevDisplacement.x, displacement.y - prevDisplacement.y); prevDisplacement = displacement; } } /*---- Image search/matching logic ----*/ // Compares the images at beforeFrame and afterFrame, searches for a motion displacement within // searchCenter+/-searchRange, and returns the best match as a new displacement. Pure function. // Throws IllegalArgumentException if the motion search range exceeds image bounds. private static Point searchForDisplacement(int beforeFrame, int afterFrame, Point searchCenter, Point searchRange) throws IOException { int[] beforePixels = getFramePixels(beforeFrame); int[] afterPixels = getFramePixels(afterFrame ); int beforeValidWidth = getValidImageWidth(beforeFrame); int afterValidWidth = getValidImageWidth(afterFrame ); int imageCenterX = (IMAGE_WIDTH - SEARCH_WINDOW_WIDTH) / 2; boolean symmetricWindow; { int minBeforeX = imageCenterX - (searchCenter.x + searchRange.x) / 2; int maxBeforeX = imageCenterX - (searchCenter.x - searchRange.x) / 2 + SEARCH_WINDOW_WIDTH; int minAfterX = imageCenterX - (searchCenter.x - searchRange.x) / 2 + (searchCenter.x - searchRange.x); int maxAfterX = imageCenterX - (searchCenter.x + searchRange.x) / 2 + (searchCenter.x + searchRange.x) + SEARCH_WINDOW_WIDTH; symmetricWindow = 0 <= minBeforeX && maxBeforeX <= beforeValidWidth && 0 <= minAfterX && maxAfterX <= afterValidWidth; } long bestError = Long.MAX_VALUE; int resultX = 0; int resultY = 0; for (int dy = -searchRange.y; dy <= searchRange.y; dy++) { for (int dx = -searchRange.x; dx <= searchRange.x; dx++) { int dispX = searchCenter.x + dx; int dispY = searchCenter.y + dy; int beforeY = SEARCH_WINDOW_TOP - dispY / 2;; int afterY = beforeY + dispY; int beforeX, afterX, windowWidth; if (symmetricWindow) { beforeX = imageCenterX - dispX / 2; afterX = beforeX + dispX; windowWidth = SEARCH_WINDOW_WIDTH; } else { // Incomplete algorithm that is well-adapted to the particular situation windowWidth = Math.min(afterValidWidth, SEARCH_WINDOW_WIDTH); afterX = afterValidWidth - windowWidth; beforeX = afterX - dispX; } if (beforeX < 0 || beforeX + SEARCH_WINDOW_WIDTH > IMAGE_WIDTH || beforeY < 0 || beforeY + SEARCH_WINDOW_HEIGHT > IMAGE_HEIGHT || afterX < 0 || afterX + SEARCH_WINDOW_WIDTH > IMAGE_WIDTH || afterY < 0 || afterY + SEARCH_WINDOW_HEIGHT > IMAGE_HEIGHT) throw new IllegalArgumentException("Motion vector too large for search window"); long error = getSubimageDifference(beforePixels, afterPixels, beforeX, beforeY, afterX, afterY, windowWidth, SEARCH_WINDOW_HEIGHT); if (error < bestError) { bestError = error; resultX = dispX; resultY = dispY; } } } return new Point(resultX, resultY); } // Returns the total difference for the subimages pix0[x0 : x0 + h, y0 : y0 + h] versus pix1[x1 : x1 + w, y1 : y1 + h]. // The result is always non-negative. Pure function. Throws an exception if any range is out of bounds. private static long getSubimageDifference(int[] pix0, int[] pix1, int x0, int y0, int x1, int y1, int w, int h) { if (pix0.length != pix1.length || pix0.length != IMAGE_WIDTH * IMAGE_HEIGHT) throw new IllegalArgumentException(); if (x0 < 0 || y0 < 0 || x1 < 0 || y1 < 0 || w < 0 || h < 0 || x0 + w > IMAGE_WIDTH || x1 + w > IMAGE_WIDTH || y0 + h > IMAGE_HEIGHT || y1 + h > IMAGE_HEIGHT) throw new IndexOutOfBoundsException(); long result = 0; for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { int p = pix0[(y0 + y) * IMAGE_WIDTH + (x0 + x)]; int q = pix1[(y1 + y) * IMAGE_WIDTH + (x1 + x)]; result += Math.abs(((p >>> 16) & 0xFF) - ((q >>> 16) & 0xFF)); // Red result += 2 * Math.abs(((p >>> 8) & 0xFF) - ((q >>> 8) & 0xFF)); // Green result += Math.abs((p & 0xFF) - (q & 0xFF)); // Blue } } return result; } // Returns how many leftmost pixels of the given frame number is valid. The result is in the range [0, IMAGE_WIDTH]. // Basically, the answer is IMAGE_WIDTH for most of the video, excep the last hundred frames where the result // linearly decreases to zero because the train moves leftward and uncovers the non-moving background. Pure function. private static int getValidImageWidth(int frameNum) { if (frameNum < 0 || frameNum >= NUM_FRAMES) throw new IndexOutOfBoundsException(); else if (frameNum < 909) return IMAGE_WIDTH; else { int sf = 909; int ef = 966; int sx = 5075; int ex = 106; return (int)Math.round((double)(frameNum - sf) / (ef - sf) * (ex - sx) + sx); } } /*---- Image reading with caching ----*/ private static List framePixelCache = new ArrayList<>(); private static final int MAX_CACHE_SIZE = 10; // Returns a read-only array of RGB pixels for the given frame number. Pure function and thread-safe. private static int[] getFramePixels(int frameNum) throws IOException { if (frameNum < 0 || frameNum >= NUM_FRAMES) throw new IndexOutOfBoundsException(); // Search the cache synchronized(FindMotionVectors.class) { for (int i = 0; i < framePixelCache.size(); i++) { if (((Integer)framePixelCache.get(i)[0]).intValue() == frameNum) { // Move to front Object[] entry = framePixelCache.remove(i); framePixelCache.add(0, entry); return (int[])entry[1]; } } } File file = new File(framesDir, String.format("%04d.bmp", frameNum)); BufferedImage image = ImageIO.read(file); int[] pixels = new int[image.getWidth() * image.getHeight()]; image.getRGB(0, 0, image.getWidth(), image.getHeight(), pixels, 0, image.getWidth()); synchronized(FindMotionVectors.class) { if (!framePixelCache.isEmpty() && framePixelCache.size() >= MAX_CACHE_SIZE) framePixelCache.remove(framePixelCache.size() - 1); framePixelCache.add(0, new Object[]{frameNum, pixels}); } return pixels; } }