update skpdiff visualization (image magnification with alpha mask)
authordjsollen@google.com <djsollen@google.com@2bbb7eff-a529-9590-31e7-b0007b416f81>
Thu, 7 Nov 2013 19:24:06 +0000 (19:24 +0000)
committerdjsollen@google.com <djsollen@google.com@2bbb7eff-a529-9590-31e7-b0007b416f81>
Thu, 7 Nov 2013 19:24:06 +0000 (19:24 +0000)
R=epoger@google.com

Review URL: https://codereview.chromium.org/29103005

git-svn-id: http://skia.googlecode.com/svn/trunk@12174 2bbb7eff-a529-9590-31e7-b0007b416f81

tools/skpdiff/SkDiffContext.cpp
tools/skpdiff/SkDiffContext.h
tools/skpdiff/SkDifferentPixelsMetric.h
tools/skpdiff/SkDifferentPixelsMetric_cpu.cpp
tools/skpdiff/SkDifferentPixelsMetric_opencl.cpp
tools/skpdiff/SkImageDiffer.h
tools/skpdiff/diff_viewer.js
tools/skpdiff/skpdiff_main.cpp
tools/skpdiff/viewer.html

index 07d304b64cb2e0fac003848c8cfa1e7d2bb80fed..ce1fad9d58ccabe1f46a3702d32793d22e65f1b0 100644 (file)
@@ -41,6 +41,12 @@ SkDiffContext::~SkDiffContext() {
     }
 }
 
+void SkDiffContext::setDifferenceDir(const SkString& path) {
+    if (!path.isEmpty() && sk_mkdir(path.c_str())) {
+        fDifferenceDir = path;
+    }
+}
+
 void SkDiffContext::setDiffers(const SkTDArray<SkImageDiffer*>& differs) {
     // Delete whatever the last array of differs was
     if (NULL != fDiffers) {
@@ -55,6 +61,23 @@ void SkDiffContext::setDiffers(const SkTDArray<SkImageDiffer*>& differs) {
     differs.copy(fDiffers);
 }
 
+static SkString get_common_prefix(const SkString& a, const SkString& b) {
+    const size_t maxPrefixLength = SkTMin(a.size(), b.size());
+    SkASSERT(maxPrefixLength > 0);
+    for (size_t x = 0; x < maxPrefixLength; ++x) {
+        if (a[x] != b[x]) {
+            SkString result;
+            result.set(a.c_str(), x);
+            return result;
+        }
+    }
+    if (a.size() > b.size()) {
+        return b;
+    } else {
+        return a;
+    }
+}
+
 void SkDiffContext::addDiff(const char* baselinePath, const char* testPath) {
     // Load the images at the paths
     SkBitmap baselineBitmap;
@@ -75,9 +98,21 @@ void SkDiffContext::addDiff(const char* baselinePath, const char* testPath) {
     newRecord->fNext = fRecords;
     fRecords = newRecord;
 
+    // compute the common name
+    SkString baseName = SkOSPath::SkBasename(baselinePath);
+    SkString testName = SkOSPath::SkBasename(testPath);
+    newRecord->fCommonName = get_common_prefix(baseName, testName);
+
+    bool alphaMaskPending = false;
+    bool alphaMaskCreated = false;
+
     // Perform each diff
     for (int differIndex = 0; differIndex < fDifferCount; differIndex++) {
         SkImageDiffer* differ = fDiffers[differIndex];
+        // TODO only enable for one differ
+        if (!alphaMaskCreated && !fDifferenceDir.isEmpty()) {
+            alphaMaskPending = differ->enablePOIAlphaMask();
+        }
         int diffID = differ->queueDiff(&baselineBitmap, &testBitmap);
         if (diffID >= 0) {
 
@@ -91,6 +126,25 @@ void SkDiffContext::addDiff(const char* baselinePath, const char* testPath) {
             SkIPoint* poi = differ->getPointsOfInterest(diffID);
             diffData.fPointsOfInterest.append(poiCount, poi);
 
+            if (alphaMaskPending
+                    && SkImageDiffer::RESULT_CORRECT != diffData.fResult
+                    && newRecord->fDifferencePath.isEmpty()) {
+                newRecord->fDifferencePath = SkOSPath::SkPathJoin(fDifferenceDir.c_str(),
+                                                                  newRecord->fCommonName.c_str());
+
+                // compute the image diff and output it
+                SkBitmap* alphaMask = differ->getPointsOfInterestAlphaMask(diffID);
+                SkBitmap copy;
+                alphaMask->copyTo(&copy, SkBitmap::kARGB_8888_Config);
+                SkImageEncoder::EncodeFile(newRecord->fDifferencePath.c_str(), copy,
+                                           SkImageEncoder::kPNG_Type, 100);
+            }
+
+            if (alphaMaskPending) {
+                alphaMaskPending = false;
+                alphaMaskCreated = true;
+            }
+
             // Because we are doing everything synchronously for now, we are done with the diff
             // after reading it.
             differ->deleteDiff(diffID);
@@ -201,18 +255,8 @@ void SkDiffContext::outputRecords(SkWStream& stream, bool useJSONP) {
             SkString baselineAbsPath = get_absolute_path(currentRecord->fBaselinePath);
             SkString testAbsPath = get_absolute_path(currentRecord->fTestPath);
 
-            // strip off directory structure and find the common part of the filename
-            SkString baseName = SkOSPath::SkBasename(baselineAbsPath.c_str());
-            SkString testName = SkOSPath::SkBasename(testAbsPath.c_str());
-            for (size_t x = 0; x < baseName.size(); ++x) {
-                if (baseName[x] != testName[x]) {
-                    baseName.insertUnichar(x, '\n');
-                    break;
-                }
-            }
-
             stream.writeText("            \"commonName\": \"");
-            stream.writeText(baseName.c_str());
+            stream.writeText(currentRecord->fCommonName.c_str());
             stream.writeText("\",\n");
 
             stream.writeText("            \"differencePath\": \"");
index 20193b38514248e77ba6e7a34752cf299a413d14..9669ae0ad39e79250f6007c30a3bf47707e10f37 100644 (file)
@@ -25,6 +25,12 @@ public:
 
     void setThreadCount(int threadCount) { fThreadCount = threadCount; }
 
+    /**
+     * Creates the directory if it does not exist and uses it to store differences
+     * between images.
+     */
+    void setDifferenceDir(const SkString& directory);
+
     /**
      * Sets the differs to be used in each diff. Already started diffs will not retroactively use
      * these.
@@ -106,6 +112,7 @@ private:
     };
 
     struct DiffRecord {
+        SkString           fCommonName;
         SkString           fDifferencePath;
         SkString           fBaselinePath;
         SkString               fTestPath;
@@ -121,6 +128,8 @@ private:
     SkImageDiffer** fDiffers;
     int fDifferCount;
     int fThreadCount;
+
+    SkString fDifferenceDir;
 };
 
 #endif
index 38fa5ac55c826e59a02b67a82d2d199791b914e1..614f92035672b214ac7b886449fca6742acf4eb3 100644 (file)
@@ -27,13 +27,17 @@ class SkDifferentPixelsMetric :
     public SkImageDiffer {
 #endif
 public:
+    SkDifferentPixelsMetric() : fPOIAlphaMask(false) {}
+
     virtual const char* getName() SK_OVERRIDE;
+    virtual bool enablePOIAlphaMask() SK_OVERRIDE;
     virtual int queueDiff(SkBitmap* baseline, SkBitmap* test) SK_OVERRIDE;
     virtual void deleteDiff(int id) SK_OVERRIDE;
     virtual bool isFinished(int id) SK_OVERRIDE;
     virtual double getResult(int id) SK_OVERRIDE;
     virtual int getPointsOfInterestCount(int id) SK_OVERRIDE;
     virtual SkIPoint* getPointsOfInterest(int id) SK_OVERRIDE;
+    virtual SkBitmap* getPointsOfInterestAlphaMask(int id) SK_OVERRIDE;
 
 protected:
 #if SK_SUPPORT_OPENCL
@@ -41,8 +45,9 @@ protected:
 #endif
 
 private:
-    struct QueuedDiff;
+    bool fPOIAlphaMask;
 
+    struct QueuedDiff;
     SkTDArray<QueuedDiff> fQueuedDiffs;
 
 #if SK_SUPPORT_OPENCL
index 4e5e93969c3f743ecc6024bbba9576a06a3791ef..a3e4a383f2e8ef0edc32882a35958b3af28c2018 100644 (file)
@@ -16,12 +16,18 @@ struct SkDifferentPixelsMetric::QueuedDiff {
     bool finished;
     double result;
     SkTDArray<SkIPoint>* poi;
+    SkBitmap poiAlphaMask;
 };
 
 const char* SkDifferentPixelsMetric::getName() {
     return "different_pixels";
 }
 
+bool SkDifferentPixelsMetric::enablePOIAlphaMask() {
+    fPOIAlphaMask = true;
+    return true;
+}
+
 int SkDifferentPixelsMetric::queueDiff(SkBitmap* baseline, SkBitmap* test) {
     double startTime = get_seconds();
     int diffID = fQueuedDiffs.count();
@@ -44,6 +50,14 @@ int SkDifferentPixelsMetric::queueDiff(SkBitmap* baseline, SkBitmap* test) {
     int height = baseline->height();
     int differentPixelsCount = 0;
 
+    // Prepare the POI alpha mask if needed
+    if (fPOIAlphaMask) {
+        diff->poiAlphaMask.setConfig(SkBitmap::kA8_Config, width, height);
+        diff->poiAlphaMask.allocPixels();
+        diff->poiAlphaMask.lockPixels();
+        diff->poiAlphaMask.eraseARGB(SK_AlphaOPAQUE, 0, 0, 0);
+    }
+
     // Prepare the pixels for comparison
     baseline->lockPixels();
     test->lockPixels();
@@ -56,6 +70,9 @@ int SkDifferentPixelsMetric::queueDiff(SkBitmap* baseline, SkBitmap* test) {
             if (std::memcmp(&baselineRow[x * 4], &testRow[x * 4], 4) != 0) {
                 poi->push()->set(x, y);
                 differentPixelsCount++;
+                if (fPOIAlphaMask) {
+                    *diff->poiAlphaMask.getAddr8(x,y) = SK_AlphaTRANSPARENT;
+                }
             }
         }
     }
@@ -93,3 +110,10 @@ int SkDifferentPixelsMetric::getPointsOfInterestCount(int id) {
 SkIPoint* SkDifferentPixelsMetric::getPointsOfInterest(int id) {
     return fQueuedDiffs[id].poi->begin();
 }
+
+SkBitmap* SkDifferentPixelsMetric::getPointsOfInterestAlphaMask(int id) {
+    if (fQueuedDiffs[id].poiAlphaMask.empty()) {
+        return NULL;
+    }
+    return &fQueuedDiffs[id].poiAlphaMask;
+}
index f2a02c1c1823972793a9af7685910e6d46ec4d06..b3f5d2d7e0b00c70fb813e5b73ca4df3b5b9430d 100644 (file)
@@ -47,6 +47,10 @@ const char* SkDifferentPixelsMetric::getName() {
     return "different_pixels";
 }
 
+bool SkDifferentPixelsMetric::enablePOIAlphaMask() {
+    return false;
+}
+
 int SkDifferentPixelsMetric::queueDiff(SkBitmap* baseline, SkBitmap* test) {
     int diffID = fQueuedDiffs.count();
     double startTime = get_seconds();
index 6c570cbb8e41bc3d8437937047f55396cd262258..3b50d5775d2e8f23bbcfd0acab0fd9ddf73a9c3a 100644 (file)
@@ -19,6 +19,9 @@ public:
     SkImageDiffer();
     virtual ~SkImageDiffer();
 
+    static const double RESULT_CORRECT = 1.0f;
+    static const double RESULT_INCORRECT = 0.0f;
+
     /**
      * Gets a unique and descriptive name of this differ
      * @return A statically allocated null terminated string that is the name of this differ
@@ -36,6 +39,12 @@ public:
      */
     virtual bool requiresOpenCL() { return false; }
 
+    /**
+     * Enables the generation of an alpha mask for all points of interest.
+     * @return True if the differ supports generating an alpha mask and false otherwise.
+     */
+    virtual bool enablePOIAlphaMask() { return false; }
+
     /**
      * Wraps a call to queueDiff by loading the given filenames into SkBitmaps
      * @param  baseline The file path of the baseline image
@@ -87,6 +96,13 @@ public:
      */
     virtual SkIPoint* getPointsOfInterest(int id) = 0;
 
+    /*
+     * Gets a bitmap containing an alpha mask containing transparent pixels at the points of
+     * interest for the diff of the given id. The results are only meaningful after the
+     * queued diff has finished.
+     * @param  id The id of the queued diff to query
+     */
+    virtual SkBitmap* getPointsOfInterestAlphaMask(int id) { return NULL; }
 
 
 protected:
index 9c33f84fa13e897a83356c5d0a0586d88a7af30d..06a864edf4e219ade95c0c322cc14d935e3141b5 100644 (file)
 var MAX_SWAP_IMG_SIZE = 400;
+var MAGNIFIER_WIDTH = 200;
+var MAGNIFIER_HEIGHT = 200;
+var MAGNIFIER_HALF_WIDTH = MAGNIFIER_WIDTH * 0.5;
+var MAGNIFIER_HALF_HEIGHT = MAGNIFIER_HEIGHT * 0.5;
+// TODO add support for a magnified scale factor
+var MAGNIFIER_SCALE_FACTOR = 2.0;
 
 angular.module('diff_viewer', []).
-config(['$routeProvider', function($routeProvider) {
-    // Show the list of differences by default
-    $routeProvider.
-    otherwise({ templateUrl: '/diff_list.html', controller: DiffListController});
-}]).
-directive('swapImg', function() {
-    // Custom directive for showing an image that gets swapped my mouseover.
-    return {
-        restrict: 'E', // The directive can be used as an element name
-        replace: true, // The directive replaces itself with the template
-        template: '<canvas ng-mouseenter="swap()" ng-mouseleave="swap()"></canvas>',
-        scope: { // The attributes below are bound to the scope
-            leftSrc: '@',
-            rightSrc: '@',
-            side: '@'
-        },
-        link: function(scope, elm, attrs, ctrl) {
-            var leftImage = new Image();
-            var rightImage = new Image();
-            var ctx = elm[0].getContext('2d');
-
-            scope.render = function() {
-                var image;
-                if (scope.side == "left") {
-                    image = leftImage;
-                } else {
-                    image = rightImage;
-                }
-
-                // Make it so the maximum size of an image is MAX_SWAP_IMG_SIZE, and the images are
-                // scaled down in halves.
-                var divisor = 1;
-                while ((image.width / divisor) > MAX_SWAP_IMG_SIZE) {
-                    divisor *= 2;
-                }
-
-                // Set canvas to correct size and draw the image into it
-                elm[0].width = image.width / divisor;
-                elm[0].height = image.height / divisor;
-                ctx.drawImage(image, 0, 0, elm[0].width, elm[0].height);
-            };
+directive('imgCompare', function() {
+  // Custom directive for comparing (3-way) images
+  return {
+      restrict: 'E', // The directive can be used as an element name
+      replace: true, // The directive replaces itself with the template
+      template: '<canvas/>',
+      scope: true,
+      link: function(scope, elm, attrs, ctrl) {
+          var image = new Image();
+          var canvas = elm[0];
+          var ctx = canvas.getContext('2d');
+
+          var magnifyContent = false;
+
+          // When the type attribute changes, load the image and then render
+          attrs.$observe('type', function(value) {
+              switch(value) {
+                case "alphaMask":
+                    image.src = scope.record.differencePath;
+                    break;
+                case "baseline":
+                    image.src = scope.record.baselinePath;
+                    magnifyContent = true;
+                    break;
+                case "test":
+                    image.src = scope.record.testPath;
+                    magnifyContent = true;
+                    break;
+                default:
+                    console.log("Unknown type attribute on <img-compare>: " + value);
+                    return;
+              }
+
+              image.onload = function() {
+                  // compute the scaled image width/height for image and canvas
+                  var divisor = 1;
+                  // Make it so the maximum size of an image is MAX_SWAP_IMG_SIZE,
+                  // and the images are scaled down in halves.
+                  while ((image.width / divisor) > MAX_SWAP_IMG_SIZE) {
+                      divisor *= 2;
+                  }
+
+                  scope.setImgScaleFactor(1 / divisor);
+
+                  // Set canvas to correct size
+                  canvas.width = image.width * scope.imgScaleFactor;
+                  canvas.height = image.height * scope.imgScaleFactor;
+
+                  // render the image onto the canvas
+                  scope.renderImage();
+              }
+          });
+
+          // When the magnify attribute changes, render the magnified rect at
+          // the default zoom level.
+          scope.$watch('magnifyCenter', function(magCenter) {
+              if (!magnifyContent) {
+                  return;
+              }
+
+              scope.renderImage();
+
+              if (!magCenter) {
+                  return;
+              }
+
+              var magX = magCenter.x - MAGNIFIER_HALF_WIDTH;
+              var magY = magCenter.y - MAGNIFIER_HALF_HEIGHT;
+
+              var magMaxX = canvas.width - MAGNIFIER_WIDTH;
+              var magMaxY = canvas.height - MAGNIFIER_HEIGHT;
+
+              var magRect = { x: Math.max(0, Math.min(magX, magMaxX)),
+                              y: Math.max(0, Math.min(magY, magMaxY)),
+                              width: MAGNIFIER_WIDTH,
+                              height: MAGNIFIER_HEIGHT
+                            };
+
+              var imgRect = { x: (magCenter.x / scope.imgScaleFactor) - MAGNIFIER_HALF_WIDTH,
+                              y: (magCenter.y  / scope.imgScaleFactor) - MAGNIFIER_HALF_HEIGHT,
+                              width: MAGNIFIER_WIDTH,
+                              height: MAGNIFIER_HEIGHT
+                            };
+
+              // draw the magnified image
+              ctx.clearRect(magRect.x, magRect.y, magRect.width, magRect.height);
+              ctx.drawImage(image, imgRect.x, imgRect.y, imgRect.width, imgRect.height,
+                            magRect.x, magRect.y, magRect.width, magRect.height);
 
-            // When the leftSrc attribute changes, load the image and then rerender
-            attrs.$observe('leftSrc', function(value) {
-                leftImage.src = value;
-                leftImage.onload = function() {
-                    if (scope.side == "left") {
-                        scope.render();
-                    }
-                };
-            });
-
-            // When the rightSrc attribute changes, load the image and then rerender
-            attrs.$observe('rightSrc', function(value) {
-                rightImage.src = value;
-                rightImage.onload = function() {
-                    if (scope.side == "right") {
-                        scope.render();
-                    }
-                };
-            });
-
-            // Swap which side to draw onto the canvas and then rerender
-            scope.swap = function() {
-                if (scope.side == "left") {
-                    scope.side = "right";
-                } else {
-                    scope.side = "left";
-                }
-                scope.render();
+              // draw the outline rect
+              ctx.beginPath();
+              ctx.rect(magRect.x, magRect.y, magRect.width, magRect.height);
+              ctx.lineWidth = 2;
+              ctx.strokeStyle = 'red';
+              ctx.stroke();
+
+          });
+
+          // render the image to the canvas. This is often done every frame prior
+          // to any special effects (i.e. magnification).
+          scope.renderImage = function() {
+            ctx.clearRect(0, 0, canvas.width, canvas.height);
+            ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
+          };
+
+          // compute a rect (x,y,width,height) that represents the bounding box for
+          // the magnification effect
+          scope.computeMagnifierOutline = function(event) {
+            var scaledWidth = MAGNIFIER_WIDTH * scope.imgScaleFactor;
+            var scaledHeight = MAGNIFIER_HEIGHT * scope.imgScaleFactor;
+            return {
+              x: event.offsetX - (scaledWidth * 0.5),
+              y: event.offsetY  - (scaledHeight * 0.5),
+              width: scaledWidth,
+              height: scaledHeight
             };
-        }
-    };
+          };
+
+          // event handler for mouse events that triggers the magnification
+          // effect across the 3 images being compared.
+          scope.MagnifyDraw = function(event, startMagnify) {
+              if (startMagnify) {
+                  scope.setMagnifierState(true);
+              }  else if (!scope.magnifierOn) {
+                  return;
+              }
+
+              scope.renderImage();
+
+              // render the magnifier outline rect
+              var rect = scope.computeMagnifierOutline(event);
+              ctx.save();
+              ctx.beginPath();
+              ctx.rect(rect.x, rect.y, rect.width, rect.height);
+              ctx.lineWidth = 2;
+              ctx.strokeStyle = 'red';
+              ctx.stroke();
+              ctx.restore();
+
+              // update scope on baseline / test that will cause them to render
+              scope.setMagnifyCenter({x: event.offsetX, y: event.offsetY});
+          };
+
+          // event handler that triggers the end of the magnification effect and
+          // resets all the canvases to their original state.
+          scope.MagnifyEnd = function(event) {
+              scope.renderImage();
+              // update scope on baseline / test that will cause them to render
+              scope.setMagnifierState(false);
+              scope.setMagnifyCenter(undefined);
+        };
+      }
+  };
 });
 
+function ImageController($scope, $http, $location, $timeout, $parse) {
+  $scope.imgScaleFactor = 1.0;
+  $scope.magnifierOn = false;
+  $scope.magnifyCenter = undefined;
+
+  $scope.setImgScaleFactor = function(scaleFactor) {
+    $scope.imgScaleFactor = scaleFactor;
+  }
+
+  $scope.setMagnifierState = function(magnifierOn) {
+    $scope.magnifierOn = magnifierOn;
+  }
+
+  $scope.setMagnifyCenter = function(magnifyCenter) {
+      $scope.magnifyCenter = magnifyCenter;
+  }
+}
+
 function DiffListController($scope, $http, $location, $timeout, $parse) {
     // Detect if we are running the web server version of the viewer. If so, we set a flag and
     // enable some extra functionality of the website for rebaselining.
index 55640f7f0ade5f7c58930fa73845b68af52e8496..b1bf9173c7c5fad03e6d30aa23f12f17d3a34afe 100644 (file)
@@ -38,6 +38,7 @@ DEFINE_string2(differs, d, "", "The names of the differs to use or all of them b
 DEFINE_string2(folders, f, "", "Compare two folders with identical subfile names: <baseline folder> <test folder>");
 DEFINE_string2(patterns, p, "", "Use two patterns to compare images: <baseline> <test>");
 DEFINE_string2(output, o, "", "Writes the output of these diffs to output: <output>");
+DEFINE_string(alphaDir, "", "Writes the alpha mask of these diffs to output: <output>");
 DEFINE_bool(jsonp, true, "Output JSON with padding");
 DEFINE_string(csv, "", "Writes the output of these diffs to a csv file");
 DEFINE_int32(threads, -1, "run N threads in parallel [default is derived from CPUs available]");
@@ -186,9 +187,20 @@ int tool_main(int argc, char * argv[]) {
         }
     }
 
+    if (!FLAGS_alphaDir.isEmpty()) {
+        if (1 != FLAGS_alphaDir.count()) {
+            SkDebugf("alphaDir flag expects one argument: <directory>\n");
+            return 1;
+        }
+    }
+
     SkDiffContext ctx;
     ctx.setDiffers(chosenDiffers);
 
+    if (!FLAGS_alphaDir.isEmpty()) {
+        ctx.setDifferenceDir(SkString(FLAGS_alphaDir[0]));
+    }
+
     if (FLAGS_threads >= 0) {
         ctx.setThreadCount(FLAGS_threads);
     }
index 1d3793bbf5f6cfd6b347d5af674f3322ace36d72..6ae65f756c54bbaff4a153d868b64f07d1abfb6b 100644 (file)
@@ -9,73 +9,73 @@
     <link rel="stylesheet" type="text/css" href="viewer_style.css">
     <title>SkPDiff</title>
   </head>
-  <body>
-    <!--
-      All templates are being included with the main page to avoid using AJAX, which would force
-      us to use a webserver.
-    -->
-    <script type="text/ng-template" id="/diff_list.html">
-      <div ng-class="statusClass">
-        <!-- The commit button -->
-        <div ng-show="isDynamic" class="commit">
-          <button ng-click="commitRebaselines()">Commit</button>
-        </div>
-        <!-- Give a choice of how to order the differs -->
-        <div style="margin:8px">
-          Show me the worst by metric:
-          <button ng-repeat="differ in differs" ng-click="setSortIndex($index)">
-            <span class="result-{{ $index }}" style="padding-left:12px;">&nbsp;</span>
-            {{ differ.title }}
-          </button>
-        </div>
-        <!-- Begin list of differences -->
-        <table>
-          <thead>
-            <tr>
-              <td ng-show="isDynamic">Rebaseline?</td>
-              <td>Name</td>
-              <td>Expected Image</td>
-              <td>Actual Image</td>
-              <td>Results</td>
-            </tr>
-          </thead>
-          <tbody>
-            <!--
-              Loops through every record and crates a row for it. This sorts based on the whichever
-              metric the user chose, and places a limit on the max number of records to show.
-            -->
-            <tr ng-repeat="record in records | orderBy:sortingDiffer | limitTo:500"
-                ng-class="{selected: record.isRebaselined}">
-              <td ng-show="isDynamic">
-                <input type="checkbox"
-                       ng-model="record.isRebaselined"
-                       ng-click="selectedRebaseline($index, $event)"
-                       ng-class="{lastselected: lastSelectedIndex == $index}" />
-              </td>
-              <td class="common-name">{{ record.commonName }}</td>
-              <td>
-                <swap-img left-src="{{ record.baselinePath }}"
-                          right-src="{{ record.testPath }}"
-                          side="left"
-                          class="gm-image left-image" /></td>
-              <td>
-                <swap-img left-src="{{ record.baselinePath }}"
-                          right-src="{{ record.testPath }}"
-                          side="right"
-                          class="gm-image right-image" /></td>
-              <td>
-                <div ng-repeat="diff in record.diffs"
-                     ng-mouseover="highlight(diff.differName)"
-                     class="result result-{{$index}}">
-                  <span class="result-button">{{ diff.result }}</span>
-                </div>
-              </td>
-            </tr>
-          </tbody>
-        </table>
-      </div>
-    </script>
-    <!-- Whatever template is used is rendered in the following div -->
-    <div ng-view></div>
+  <body ng-controller="DiffListController" ng-class="statusClass">
+    <!-- The commit button -->
+    <div ng-show="isDynamic" class="commit">
+      <button ng-click="commitRebaselines()">Commit</button>
+    </div>
+    <!-- Give a choice of how to order the differs -->
+    <div style="margin:8px">
+      Show me the worst by metric:
+      <button ng-repeat="differ in differs" ng-click="setSortIndex($index)">
+        <span class="result-{{ $index }}" style="padding-left:12px;">&nbsp;</span>
+        {{ differ.title }}
+      </button>
+    </div>
+    <!-- Begin list of differences -->
+    <table>
+      <thead>
+        <tr>
+          <td ng-show="isDynamic">Rebaseline?</td>
+          <td>Name</td>
+          <td>Difference Mask</td>
+          <td>Expected Image</td>
+          <td>Actual Image</td>
+          <td>Results</td>
+        </tr>
+      </thead>
+      <tbody>
+        <!--
+          Loops through every record and crates a row for it. This sorts based on the whichever
+          metric the user chose, and places a limit on the max number of records to show.
+        -->
+        <tr ng-repeat="record in records | orderBy:sortingDiffer | limitTo:500"
+            ng-class="{selected: record.isRebaselined}" 
+            ng-controller="ImageController">
+          <td ng-show="isDynamic">
+            <input type="checkbox"
+                   ng-model="record.isRebaselined"
+                   ng-click="selectedRebaseline($index, $event)"
+                   ng-class="{lastselected: lastSelectedIndex == $index}" />
+          </td>
+          <td class="common-name">{{ record.commonName }}</td>
+          <td>
+            <img-compare type="alphaMask"
+                         class="left-image"
+                         ng-mousedown="MagnifyDraw($event, true)"
+                         ng-mousemove="MagnifyDraw($event, false)"
+                         ng-mouseup="MagnifyEnd($event)"
+                         ng-mouseleave="MagnifyEnd($event)"/>
+          </td>
+          <td>
+            <img-compare type="baseline"
+                         name="{{record.commonName}}"
+                         class="gm-image left-image" />
+          </td>
+          <td>
+            <img-compare type="test"
+                         name="{{record.commonName}}"
+                         class="gm-image right-image" />
+          </td>
+          <td>
+            <div ng-repeat="diff in record.diffs"
+                 ng-mouseover="highlight(diff.differName)"
+                 class="result result-{{$index}}">
+              <span class="result-button">{{ diff.result }}</span>
+            </div>
+          </td>
+        </tr>
+      </tbody>
+    </table>
   </body>
 </html>