309
\$\begingroup\$

Given an image of a goat, your program should best try to identify whether the goat is upside down, or not.

Examples

These are examples of what the input may be. Not actual inputs

Input:

Downgoat

Output: Downgoat

Spec

Your program should be at most 30,000 bytes

  • The input will contain the full goat
  • The picture will always contain a goat
  • If the goat is upside down, output Downgoat, otherwise Upgoat

Input will be however you can take an image as an input (file name, base64 of the image, etc.)

Don't rely on the image name or other metadata for containing "Upgoat" or "Downgoat" as the gist file names are just for reference.


Please don't hardcode. It's boring, I can't enforce it completely but I can ask nicely.

Test Cases

Gist with images. images beginning with downgoat have Downgoat output and images beginning with upgoat have Upgoat output.

Second Batch of Test Cases Make sure to test your images on all the test cases. These images are a jpgs. The image sizes do vary but not by that much.


Note: A few test cases may be added before accepting an answer to avoid answers which hardcode and to check the general performance of the program.

Bonus points for getting my avatar correct :P

Scoring

Score is a percent which can be calculated by: (number_correct / total) * 100

\$\endgroup\$
  • 1
    \$\begingroup\$ Does "fitting" count as hard-coding? \$\endgroup\$ – Nick T Feb 13 '16 at 18:48
  • \$\begingroup\$ @NickT what do you mean by "fitting"? \$\endgroup\$ – Downgoat Feb 13 '16 at 18:49
  • \$\begingroup\$ @Downgoat coming up parameters for a model (equation) that outputs if the goat is facing the correct way. By ""fitting"" I mean fitting the model to the entire data set, versus some training set. \$\endgroup\$ – Nick T Feb 13 '16 at 18:53
  • \$\begingroup\$ en.wikipedia.org/wiki/Overfitting \$\endgroup\$ – Nick T Feb 13 '16 at 19:03
  • 29
    \$\begingroup\$ I'm curious to see how these solutions will handle two goats in one picture. \$\endgroup\$ – Daniel Feb 15 '16 at 2:33
293
+50
\$\begingroup\$

Mathematica, 100%, 141 bytes

f@x_:=Count[1>0]@Table[ImageInstanceQ[x,"caprine animal",RecognitionThreshold->i/100],{i,0,50}];If[f@#>f@ImageReflect@#,"Up","Down"]<>"goat"&

Well, this feels more than a little like cheating. It's also incredibly slow as well as being very silly. Function f sees roughly how high you can set the Recognition threshold in one of Mathematica's computer vision builtins, and still recognise the image as a Caprine animal.

We then see whether the image or the flipped image is more goaty. Works on your profile image only because tie is broken in favour of downgoat. There are probably loads of ways this could be improved including asking it if the image represents Bovids or other generalisations of the Caprine animal entity type.

Answer as written scores 100% for the first testing set and 94% for the second testing set, as the algorithm yields an inconclusive result for goat 1. This can be raised back up to 100% at the expense of an even longer computational time by testing more values of RecognitionThreshold. Raising from 100 to 1000 sufficies; for some reason Mathematica thinks that's a very ungoaty image! Changing the recognition entity from Caprine animal to Hoofed Mammal also seems to work.

Ungolfed:

goatness[image_] := Count[
                      Table[
                        ImageInstanceQ[
                          image, Entity["Concept", "CaprineAnimal::4p79r"],
                          RecognitionThreshold -> threshold
                        ],
                        {threshold, 0, 0.5, 0.01}
                      ],
                      True
                    ]

Function[{image},
  StringJoin[      
    If[goatness[image] > goatness[ImageReflect[image]],
      "Up",
      "Down"
    ],
    "goat"
  ]
]

Alternative solution, 100% + bonus

g[t_][i_] := ImageInstanceQ[i, "caprine animal", RecognitionThreshold -> t]
f[i_, l_: 0, u_: 1] := Module[{m = (2 l + u)/3, r},
  r = g[m] /@ {i, ImageReflect@i};
  If[Equal @@ r,
   If[First@r, f[i, m, u], f[i, l, m]],
   If[First@r, "Up", "Down"] <> "goat"
   ]
  ]

This one uses the same strategy as before, but with a binary search over the threshold. There are two functions involved here:

  • g[t] returns whether or not its argument is a goaty image with threshold t.
  • f takes three parameters: an image, and an upper and lower bound on the threshold. It is recursive; it works by testing a threshold m between the upper and lower thresholds (biased towards the lower). If the image and the reflected image are both goaty or non-goaty, it eliminates the lower or upper part of the range as appropriate and calls itself again. Otherwise, if one image is goaty and the other is non-goaty, it returns Upgoat if the first image is goaty and Downgoat otherwise (if the second, reflected image is goaty).

The function definitions deserves a little explanation. First, function application is left-associative. This means that something like g[x][y] is interpreted as (g[x])[y]; "the result of g[x] applied to y."

Second, assignment in Mathematica is roughly equivalent to defining a replacement rule. That is, f[x_] := x^2 does not mean "declare a function named f with parameter x that returns x^2;" its meaning is closer to, "whenever you see something like f[ ... ], call the thing inside x and replace the whole thing with x^2."

Putting these two together, we can see that the definition of g is telling Mathematica to replace any expression of the form (g[ ... ])[ ... ] with the right-hand side of the assignment.

When Mathematica encounters the expression g[m] (in the second line of f), it sees that the expression does not match any rules that it knows and leaves it unchanged. Then it matches the Map operator /@, whose arguments are g[m] and the list {i, ImageReflect@i}. (/@ is infix notation; this expression is exactly equivalent to Map[g[m], { ... }].) The Map is replaced by applying its first argument to each element of its second argument, so we get {(g[m])[i], (g[m])[ ... ]}. Now Mathematica sees that each element matches the definition of g and does the replacement.

In this way we got g to act like a function that returns another function; that is, it acts roughly like we wrote:

g[t_] := Function[{i}, ImageInstanceQ[i, "caprine animal", RecognitionThreshold -> t]]

(Except in this case g[t] on its own evaluates to a Function, whereas before g[t] on its own was not transformed at all.)

The final trick I use is an optional pattern. The pattern l_ : 0 means "match any expression and make it available as l, or match nothing and make 0 available as l." So, if you call f[i] with one argument (the image to test) it is as if you had called f[i, 0, 1].

Here is the test harness I used:

gist = Import["https://api.github.com/gists/3fb94bfaa7364ccdd8e2", "JSON"];
{names, urls} = Transpose[{"filename", "raw_url"} /. Last /@ ("files" /. gist)];
images = Import /@ urls;
result = f /@ images
Tally@MapThread[StringContainsQ[##, IgnoreCase -> True] &, {names, result}]
(* {{True, 18}} *)

user = "items" /.
           Import["https://api.stackexchange.com/2.2/users/40695?site=codegolf", "JSON"];
pic = Import[First["profile_image" /. user]];
name = First["display_name" /. user];
name == f@pic
(* True *)
\$\endgroup\$
  • 344
    \$\begingroup\$ Mathematica has a builtin for determining goats. I don't know how to feel about that. \$\endgroup\$ – Robert Fraser Feb 10 '16 at 15:21
  • 119
    \$\begingroup\$ Whaaat O.o there's a builtin for this.... Wow... \$\endgroup\$ – Downgoat Feb 10 '16 at 15:23
  • 171
    \$\begingroup\$ You've goat to be kidding me... \$\endgroup\$ – corsiKa Feb 10 '16 at 15:57
  • 27
    \$\begingroup\$ +1 for Mathematica being able to see which image is "more goaty". \$\endgroup\$ – QBrute Feb 12 '16 at 12:23
  • 9
    \$\begingroup\$ This is positively ridiculous. +1. \$\endgroup\$ – ApproachingDarknessFish Feb 13 '16 at 7:27
71
\$\begingroup\$

JavaScript, 93.9%

var solution = function(imageUrl, settings) {

  // Settings
  settings = settings || {};
  var colourDifferenceCutoff = settings.colourDifferenceCutoff || 0.1,
      startX = settings.startX || 55,
      startY = settings.startY || 53;

  // Draw the image to the canvas
  var canvas = document.createElement("canvas"),
      context = canvas.getContext("2d"),
      image = new Image();
  image.src = imageUrl;
  image.onload = function(e) {
    canvas.width = image.width;
    canvas.height = image.height;
    context.drawImage(image, 0, 0);

    // Gets the average colour of an area
    function getColour(x, y) {

      // Get the image data from the canvas
      var sizeX = image.width / 100,
          sizeY = image.height / 100,
          data = context.getImageData(
            x * sizeX | 0,
            y * sizeY | 0,
            sizeX | 0,
            sizeY | 0
          ).data;

      // Get the average of the pixel colours
      var average = [ 0, 0, 0 ],
          length = data.length / 4;
      for(var i = 0; i < length; i++) {
        average[0] += data[i * 4] / length;
        average[1] += data[i * 4 + 1] / length;
        average[2] += data[i * 4 + 2] / length;
      }
      return average;
    }

    // Gets the lightness of similar colours above or below the centre
    function getLightness(direction) {
      var centre = getColour(startX, startY),
          colours = [],
          increment = direction == "above" ? -1 : 1;
      for(var y = startY; y > 0 && y < 100; y += increment) {
        var colour = getColour(startX, y);

        // If the colour is sufficiently different
        if(
          (
            Math.abs(colour[0] - centre[0]) +
            Math.abs(colour[1] - centre[1]) +
            Math.abs(colour[2] - centre[2])
          ) / 256 / 3
          > colourDifferenceCutoff
        ) break;
        else colours.push(colour);
      }

      // Calculate the average lightness
      var lightness = 0;
      for(var i = 0; i < colours.length; i++) {
        lightness +=
          (colours[i][0] + colours[i][1] + colours[i][2])
          / 256 / 3 / colours.length;
      }

      /*
      console.log(
        "Direction:", direction,
        "Checked y = 50 to:", y,
        "Average lightness:", lightness
      );
      */
      return lightness;
    }

    // Compare the lightness above and below the starting point
    //console.log("Results for:", imageUrl);
    var above = getLightness("above"),
        below = getLightness("below"),
        result = above > below ? "Upgoat" : "Downgoat";
    console.log(result);
    return result;
  };
};
<div ondrop="event.preventDefault();r=new FileReader;r.onload=e=>{document.getElementById`G`.src=imageUrl=e.target.result;console.log=v=>document.getElementById`R`.textContent=v;solution(imageUrl);};r.readAsDataURL(event.dataTransfer.files[0]);" ondragover="event.preventDefault()" style="height:160px;border-radius:12px;border:2px dashed #999;font-family:Arial,sans-serif;padding:8px"><p style="font-style:italic;padding:0;margin:0">Drag & drop image <strong>file</strong> (not just link) to test here... (requires HTML5 browser)</p><image style="height:100px" id="G" /><pre id="R"></pre></div>

Explanation

Simple implementation of @BlackCap's idea of checking where the light is coming from.

Most of the goats are in the centre of their images, and their bellies are always darker than their backs because of the sunlight. The program starts at the middle of the image and makes a note of the colour. It then gets the average lightness of the pixels above and below the centre up to where the colour is different to the colour at the centre (when the body of the goat ends and the background starts). Whichever side is lighter determines whether it is an upgoat or a downgoat.

Fails for downgoat 9 and upgoats 7 and 9 in the second test case.

\$\endgroup\$
  • 4
    \$\begingroup\$ Nice! I didn't expect a 100% to be so easy. I've added a second batch of test cases, can you update your answer based on that? \$\endgroup\$ – Downgoat Feb 10 '16 at 16:01
  • \$\begingroup\$ Here's an alternative link does that work? \$\endgroup\$ – Downgoat Feb 10 '16 at 23:30
  • \$\begingroup\$ @Downgoat Yep. Score updated. \$\endgroup\$ – user81655 Feb 10 '16 at 23:37
  • \$\begingroup\$ Unfortunately, it fails after I rotated the image 180° and flipped it vertically. screenshot \$\endgroup\$ – mr5 Feb 12 '16 at 7:57
  • \$\begingroup\$ @mr5 Interesting... So is the image in your screenshot slightly different to downgoat 4? Also there are slight differences between browsers (and maybe operating systems?). With the parameters in this answer I got these same results for both Chrome and Firefox (using Windows). \$\endgroup\$ – user81655 Feb 12 '16 at 8:33
63
\$\begingroup\$

Python, 100%, 225 bytes

import requests

SEARCH = "http://www.bing.com/images/searchbyimage?FORM=IRSBIQ&cbir=sbi&imgurl="
THRESHOLD = 30
url = raw_input()
print "Upgoat" if requests.get(SEARCH + url).content.count('img') > THRESHOLD else "Downgoat"

Use reverse image search on the goat. If the page returns a satisfiable amount of results, it is probably an upwards goat. This solution will probably not work on hand-drawn goats or if Bing ever gets corrupted.

\$\endgroup\$
  • 32
    \$\begingroup\$ I'm not sure on how I feel about this answer. It's borderline valid, and is nearly violating this loophole. Currently it is breaking the explicit rule that the input is either a file, or local path, not a url. It's in interesting answer but considering how borderline valid it is, I'd say it's competitiveness is questionable. \$\endgroup\$ – Downgoat Feb 11 '16 at 5:05
  • 50
    \$\begingroup\$ @Downgoat so you downgoated his answer? \$\endgroup\$ – Ave Feb 11 '16 at 6:15
  • 2
    \$\begingroup\$ fix it by uploading the file to imgur or something^^ Also why in the world would you use bing??? \$\endgroup\$ – Eumel Feb 11 '16 at 11:49
  • 17
    \$\begingroup\$ @Eumel Because Google checks if the User-Agent in the HTTP request belongs to an actual web browser (or something they allow) and not to some other application or script. Bing doesn't check that, they're kinda desperate about getting incoming requests. I guess User-Agent can be faked with extra code and wouldn't matter since this is not code-golf. \$\endgroup\$ – JordiVilaplana Feb 11 '16 at 14:14
  • 14
    \$\begingroup\$ That standard loophole is there for code golf answers to make them smaller. This is not a code golf challenge so I don't see why the loophole would apply \$\endgroup\$ – SztupY Jul 20 '16 at 15:48
58
\$\begingroup\$

Java, 93.9% 100%

This works by determining the row contrast in the upper and lower part of the image. I assume that the contrast in the bottom half of the image is bigger for 2 reasons:

  • the 4 legs are in the bottom part
  • the background in the upper part will be blurred because it is usually the out-of-focus-area

I determine the contrast for each row by calculating the difference of neighboring pixel values, squaring the difference, and summing all squares.

Update

Some images from the second batch caused problems with the original algorithm.

upgoat3.jpg

This image was using transparency which was ignored previously. There are several possibilities to solve this problem, but I simply chose to render all images on a 400x400 black background. This has the following advantages:

  • handles images with alpha channel
  • handles indexed and grayscale images
  • improves performance (no need to process those 13MP images)

downgoat8.jpg/upgoat8.jpg

These images have exaggerated detail in the body of the goat. The solution here was to blur the image in vertical direction only. However, this generated problems with images from the first batch, which have vertical structures in the background. The solution here was to simply count differences which exceed a certain threshold, and ignore the actual value of the difference.

Shortly said, the updated algorithm looks for areas with many differences in images that after the preprocessing look like this:

enter image description here

import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

public class UpDownGoat {
    private static final int IMAGE_SIZE = 400;
    private static final int BLUR_SIZE = 50;

    private static BufferedImage blur(BufferedImage image) {
        BufferedImage result = new BufferedImage(image.getWidth(), image.getHeight() - BLUR_SIZE + 1,
                BufferedImage.TYPE_INT_RGB);
        for (int b = 0; b < image.getRaster().getNumBands(); ++b) {
            for (int x = 0; x < result.getWidth(); ++x) {
                for (int y = 0; y < result.getHeight(); ++y) {
                    int sum = 0;
                    for (int y1 = 0; y1 < BLUR_SIZE; ++y1) {
                        sum += image.getRaster().getSample(x, y + y1, b);
                    }
                    result.getRaster().setSample(x, y, b, sum / BLUR_SIZE);
                }
            }
        }
        return result;
    }

    private static long calcContrast(Raster raster, int y0, int y1) {
        long result = 0;
        for (int b = 0; b < raster.getNumBands(); ++b) {
            for (int y = y0; y < y1; ++y) {
                long prev = raster.getSample(0, y, b);
                for (int x = 1; x < raster.getWidth(); ++x) {
                    long current = raster.getSample(x, y, b);
                    result += Math.abs(current - prev) > 5 ? 1 : 0;
                    prev = current;
                }
            }
        }
        return result;
    }

    private static boolean isUp(File file) throws IOException {
        BufferedImage image = new BufferedImage(IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_INT_RGB);
        Graphics2D graphics = image.createGraphics();
        graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
        graphics.drawImage(ImageIO.read(file), 0, 0, image.getWidth(), image.getHeight(), null);
        graphics.dispose();
        image = blur(image);
        int halfHeight = image.getHeight() / 2;
        return calcContrast(image.getRaster(), 0, halfHeight) < calcContrast(image.getRaster(),
                image.getHeight() - halfHeight, image.getHeight());
    }

    public static void main(String[] args) throws IOException {
        System.out.println(isUp(new File(args[0])) ? "Upgoat" : "Downgoat");
    }
}
\$\endgroup\$
  • \$\begingroup\$ Here's an alternative link does that work? \$\endgroup\$ – Downgoat Feb 10 '16 at 23:30
  • \$\begingroup\$ @Downgoat Yes, that worked. I updated the score (not including the bonus points for your avatar which is recognized correctly :). \$\endgroup\$ – Sleafar Feb 11 '16 at 5:33
38
\$\begingroup\$

Python 3, 91.6%

-edited with the new test cases

set filename to the goat picture you wish to test. It uses a kernel to make an image top/bottom asymmetric.I tried the sobel operator, but this was better.

from PIL import Image, ImageFilter
import statistics
k=(2,2,2,0,0,0,-2,-2,-2)
filename='0.png'
im=Image.open(filename)
im=im.filter(ImageFilter.Kernel((3,3),k,1,128))
A=list(im.resize((10,10),1).getdata())
im.close()
a0=[]
aa=0
for y in range(0,len(A)):
    y=A[y]
    a0.append(y[0]+y[1]+y[2])
aa=statistics.mean(a0)
if aa<383.6974:
    print('Upgoat')
else:
    print('Downgoat')
\$\endgroup\$
  • 3
    \$\begingroup\$ +1 Nice job! I should really figure out how to install PIL on a Mac... \$\endgroup\$ – Downgoat Feb 10 '16 at 5:12
  • \$\begingroup\$ I've added a second batch of test cases, can you update your answer based on that? \$\endgroup\$ – Downgoat Feb 10 '16 at 16:01
  • \$\begingroup\$ @Downgoat just did \$\endgroup\$ – Magenta Feb 10 '16 at 17:26
  • \$\begingroup\$ @Downgoat pip install Pillow \$\endgroup\$ – Assaf Lavie Feb 12 '16 at 7:59
16
\$\begingroup\$

OpenCV with Hough Transform, 100%

My original idea was to detect the vertical lines of the goat's legs and determine its vertical position relative to the body and horizon.

As it turns out, in all the images, the ground is extremely noisy, making lots of Canny edge detection output and corresponding detected lines from the Hough transform. My strategy was then to determine whether the horizontal lines lie in the upper or lower half of the image, which was enough to solve the problem.

# Most of this code is from OpenCV examples
import cv2
import numpy as np

def is_upgoat(path):
    img = cv2.imread(path)
    height, width, channels = img.shape
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    edges = cv2.Canny(gray, 100, 200, apertureSize=3)

    lines = cv2.HoughLines(edges, 1, np.pi/180, 200, None, 0, 0, np.pi/2-0.5, np.pi/2+0.5)
    rho_small = 0

    for line in lines:
        rho, theta = line[0]
        a = np.cos(theta)
        b = np.sin(theta)
        x0 = a*rho
        y0 = b*rho
        x1 = int(x0 + 5000*(-b))
        y1 = int(y0 + 5000*(a))
        x2 = int(x0 - 5000*(-b))
        y2 = int(y0 - 5000*(a))

        if rho/height < 1/2: rho_small += 1
        cv2.line(img,(x1,y1),(x2,y2),(0,0,255),1, cv2.LINE_AA)

    output_dir = "output/"
    img_name = path[:-4]
    cv2.imwrite(output_dir + img_name + "img.jpg", img)
    cv2.imwrite(output_dir + img_name + "edges.jpg", edges)

    return rho_small / len(lines) < 1/2


for i in range(1, 10):
    downgoat_path = "downgoat" + str(i) + ".jpg"
    print(downgoat_path, is_upgoat(downgoat_path))

for i in range(1, 10):
    upgoat_path = "upgoat" + str(i) + ".jpg"
    print(upgoat_path, is_upgoat(upgoat_path))

Here's the entire function without outputting images:

def is_upgoat(path):
    img = cv2.imread(path)
    height, width, channels = img.shape
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    edges = cv2.Canny(gray, 100, 200, apertureSize=3)

    lines = cv2.HoughLines(edges, 1, np.pi/180, 200, None, 0, 0, np.pi/2-0.5, np.pi/2+0.5)
    rho_small = 0

    for line in lines:
        rho, theta = line[0]
        if rho/height < 1/2: rho_small += 1

    return rho_small / len(lines) < 1/2

Downgoat1 edges:

Downgoat1 edges

Downgoat1 lines:

Downgoat1 lines

Upgoat2 edges and lines:

Upgoat2 edges Upgoat2 lines

The method even worked well on particularly noisy images. Here's downgoat3 edges and lines:

downgoat3 edges downgoat3 lines


Addendum

It turns out median blur and adaptive Gaussian thresholding before the Hough Transform works much better than Canny edge detection, mostly since median blur is good in noisy areas. However the problems of my original approach immediately are clear: prominent background lines are detected, as well as the goat's face in some pictures.

def is_upgoat2(path):
    img = cv2.imread(path)
    #height, width, channels = img.shape
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    gray = cv2.medianBlur(gray, 19)
    thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                                   cv2.THRESH_BINARY_INV, 11, 2)

    lines = cv2.HoughLinesP(thresh, 1, np.pi / 180, threshold=100,
                            minLineLength=50, maxLineGap=10)

    vert_y = []
    horiz_y = []
    for line in lines:
        x1, y1, x2, y2 = line[0]
        # Vertical lines
        if x1 == x2 or abs((y2-y1)/(x2-x1)) > 3:
            vert_y.append((y1+y2)/2)
            cv2.line(img, (x1, y1), (x2, y2), (0, 255, 0), 2)

        # Horizontal lines
        if x1 != x2 and abs((y2-y1)/(x2-x1)) < 1/3:
            horiz_y.append((y1+y2)/2)
            cv2.line(img, (x1, y1), (x2, y2), (0, 0, 255), 2)


    print(np.median(vert_y), np.median(horiz_y))

Here's downgoat8:

downgoat8 thresh downgoat8 edges

Contours (code not shown) detect the top edge of the goat (spine) pretty well but fail to get the entire shape.

contours

Further research: OpenCV has Haar-feature based object detection which is usually used for things like cars and faces, but it could probably work for goats too, given their distinctive shape.

2D Feature recognition looks promising (template matching won't work because of scaling and rotation) but I'm too lazy to figure out OpenCV for C++.

\$\endgroup\$
10
\$\begingroup\$

Python 3, numpy, scikit, 100%

This code runs a goat-trained image classifier against a single filename, printing out 'Upgoat' or 'Downgoat'. The code itself is one line of python3, preceded by a single gigantic string, and an import line. The giant string is actually the goat-trained classifier, which is unpickled at runtime and given the input image for classification.

The classifier was created by using the TPOT system, from Randal Olson and team at the University of Pennsylvania. TPOT helps to evolve machine-learning image classifier pipelines using genetic programming. Basically it uses artificial selection to choose various parameters and types of classification to work best with the input data you give it, so you don't have to know much about machine learning to get a pretty good pipeline setup. https://github.com/EpistasisLab/tpot . TPOT runs on top of scikit-learn, of INRIA et al, http://scikit-learn.org/stable/

I gave TPOT about a hundred goat images that I found on the internet. I chose ones that looked relatively similar to the goats in Test, i.e. "in a field", from the side, without much else going on in the image. The output of this TPOT process was basically a scikit-learn ExtraTreesClassifier object. This image classifier, after being trained (or 'fit') on my goats, was pickled into the huge string. The string, then, contains not just classifier code, but the "imprint" of the training of all the goat images it was trained on.

I cheated slightly during training, by including the 'goat standing on a log' test image in the training images, but it still works pretty well on generic goat-in-a-field images. There seems to be a tradeoff - the longer I let TPOT run, the better classifier it created. However, better classifiers also seem to be 'bigger' and eventually run up against the 30,000 byte limit given by @Downgoat in the golf game. This program as it stands is currently at about 27kbytes. Please note that the 'second group' of test images is broken, as is the 'backup link', so I'm not sure how it would do on them. If they were to be repaired, I would probably start over, rerun TPOT and feed it a bunch of new images, and see if I could create a new classifier under 30k bytes.

Thanks

import pickle,bz2,base64,numpy,sys,skimage.transform,skimage.io
s='''
QlpoOTFBWSZTWbH8iTYAp4Z/////////////////////////////////////////////4E6fAAPR
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
BPBFEE//F3JFOFCQsfyJNg==
'''
print(['','Up','Down'][int(pickle.loads(bz2.decompress(base64.b64decode(s))).predict(numpy.array([skimage.transform.resize(skimage.io.imread(sys.argv[1],as_grey=True),(24,12),mode='constant').flatten()]))[0])]+'goat')

update: per request here is the training data, resized to 24x12 and combined into a single image for ease of upload/presentation. its over a hundred images. http://deeplearning.net/datasets/ , http://www.vision.caltech.edu/Image_Datasets/Caltech256/, duckduckgo image search, google image search, etc

training data at 24x12 pixels

\$\endgroup\$
  • \$\begingroup\$ Can you post your training data? \$\endgroup\$ – qwr Jun 13 '18 at 4:13
  • \$\begingroup\$ some of the original exact images i used are copyright so i cant post them all, however i have shrunk a bunch of them to the size used in the system, 24x12, and posted them in a single montage image above, which should qualify as 'fair use'. \$\endgroup\$ – don bright Jun 14 '18 at 7:37
6
\$\begingroup\$

Scikit-learn with Random Forests, 100%

The tried-and-true approach is convnets, but random forests can perform very well out-of-the-box (few parameters to tune). Here I show some general techniques in image classification tasks.

I started off with 100 images of goats for training I found through Google Images (AFAIK none in the training data match the test data). Each image is rescaled to 20x16 in grayscale, then the array is flattened to produce one row in a 2D array. A flipped version of the image is also added as a row for the training data. I did not need to use any data augmentation techniques.

grid of goats

Then I feed the 2D array into the random forest classifier and call predict to produce 50 decision trees. Here is the (messy) code:

RESIZE_WIDTH = 20
RESIZE_HEIGHT = 16

def preprocess_img(path):
    img = cv2.imread(path, 0)  # Grayscale
    resized_img = cv2.resize(img, (RESIZE_WIDTH, RESIZE_HEIGHT))
    return resized_img


def train_random_forest(downgoat_paths, upgoat_paths, data_paths):
    assert len(data_paths) == 100
    # Create blank image grid
    img_grid = np.zeros((10*RESIZE_HEIGHT, 10*RESIZE_WIDTH), np.uint8)

    # Training data
    TRAINING_EXAMPLES = 2*len(data_paths)
    train_X = np.zeros((TRAINING_EXAMPLES, RESIZE_WIDTH*RESIZE_HEIGHT), np.uint8)
    train_y = np.zeros(TRAINING_EXAMPLES, np.uint8)

    TEST_EXAMPLES = len(downgoat_paths) + len(upgoat_paths)
    test_X = np.zeros((TEST_EXAMPLES, RESIZE_WIDTH*RESIZE_HEIGHT), np.uint8)
    test_y = np.zeros(TEST_EXAMPLES, np.uint8)


    for i, data_path in enumerate(data_paths):
        img = preprocess_img(data_path)

        # Paste to grid
        ph = (i//10) * RESIZE_HEIGHT
        pw = (i%10) * RESIZE_WIDTH
        img_grid[ph:ph+RESIZE_HEIGHT, pw:pw+RESIZE_WIDTH] = img
        flipped_img = np.flip(img, 0)

        # Add to train array
        train_X[2*i,], train_y[2*i] = img.flatten(), 1
        train_X[2*i+1,], train_y[2*i+1] = flipped_img.flatten(), 0

    cv2.imwrite("grid.jpg", img_grid)

    clf = RandomForestClassifier(n_estimators=50, verbose=1)
    clf.fit(train_X, train_y)
    joblib.dump(clf, 'clf.pkl')

    for i, img_path in enumerate(downgoat_paths + upgoat_paths):
        test_X[i,] = preprocess_img(img_path).flatten()
        test_y[i] = (i >= len(downgoat_paths))


    predict_y = clf.predict(test_X)
    print(predict_y)
    print(test_y)
    print(accuracy_score(predict_y, test_y))

    # Draw tree 0
    tree.export_graphviz(clf.estimators_[0], out_file="tree.dot", filled=True)
    os.system('dot -Tpng tree.dot -o tree.png')


def main():
    downgoat_paths = ["downgoat" + str(i) + ".jpg" for i in range(1, 10)]
    upgoat_paths = ["upgoat" + str(i) + ".jpg" for i in range(1, 10)]
    data_paths = ["data/" + file for file in os.listdir("data")]

    train_random_forest(downgoat_paths, upgoat_paths, data_paths)

Here is the first decision tree (though since the model is in an ensemble, it is not particularly useful)

decision tree #0

\$\endgroup\$
  • \$\begingroup\$ that is very interesting.... your training data seems a lot more diverse types of pictures than mine. \$\endgroup\$ – don bright Jan 20 at 21:44
  • \$\begingroup\$ @donbright I would post my training data but the folder with all my pictures was on a hard drive that died. If anyone is ambitious enough, they can use reverse google image search and find the pictures I used. \$\endgroup\$ – qwr Jan 28 at 21:25
  • \$\begingroup\$ thats cool. i downloaded a bunch of images but i spent a huge amount of time sorting through them for "clean" images. it is interesting to see how its possible to train based on more 'dirty' images without having to spend as much time sorting through maybe. \$\endgroup\$ – don bright Jan 29 at 0:47
  • \$\begingroup\$ @donbright I believe more training data and variety is better. For "clean" and "dirty" we may use data augmentation to create "more data". \$\endgroup\$ – qwr Jan 29 at 3:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.