How can I rank images by only comparing them to each other?

Viewed 1348

I have a large collection of images which I'm trying to sort according to quality by crowd-sourcing. Images can be assigned 1, 2, 3, 4, or 5 stars according to how much the user likes them. A 5-star image would be very visually appealing, a 1-star image might be blurry and out of focus.

At first I created a page showing an image with the option to rate it directly by choosing 1-5 stars. But it was too time-consuming to to do this. I'd like to try to create an interface where the user is presented with 2 images side by side and asked to click the image s/he likes more. Using this comparison data of one image compared to another, is there then some way to convert it over to a score of 1-5?

What kind of algorithm would allow me to globally rank images by comparing them only to each other, and how could I implement it in python?

3 Answers

Sounds like you need a ranking algorithm similar to what is used in sport to rank players. Think of the comparison of two images as a match and the one the user selects as the better one is the winner of the match. After some time, many players have played many matches and sometimes against the same person. They win some they lose some eh? How do you rank which is the best overall?

You can look at the Elo Rating System. which is used in chess to rank chess players. There is an algorithm specified so it should be a matter of implementing in your language of choice.

Let each image start with a ranking of 3 (the mean of 1 … 5), then for each comparison (which wasn't equal) lower the rank of the loser image and increase the rank of the winner image. I propose to simply count the +1s and the -1s, so that you have a number of wins and a number of losses for each image.

Then the value 1 … 5 could be calculated as:

import math

def rank(wins, losses):
  return 3 + 4 * math.atan(wins - losses) / math.pi

This will rank images higher and higher with each win, but it will lead to the silly situation that (+1010 / -1000) will be ranked alike with (+10 / -0) which is not desirable.

One can remedy this flaw by using a mean of the values:

def rank(wins, losses):
  return (3 + 4 * math.atan((wins - losses) / (wins + losses) * 10) / math.pi
          if wins + losses > 0 else 3)

Both curves will never quite reach 1 or 5, but they will come ever closer if an image always wins or always loses.

If you don't want to deal with a complex statistical model like the Elo rating system suggested by @VincentRamdhanie (which will yield optimal results), you can always model this as a simple optimization problem.

You have datapoints of the type a>b. If you assign values to a and b, a>b is simply a condition that evaluates to true or false. Then, one possible "solution" to this problem is to maximize the number of conditions (datapoints) that evaluate to true.

Since you already know your metric, all that is left is to choose a search algorithm. Hill climbing is a very simple one. It works like this:

  • assign to each rating a random value - this is your current solution
  • change the value of one of the ratings
  • if by performing the change, our metric (number of valid conditions) is improved, the change is incorporated in our current solution
  • repeat until you're happy (typically, until you can't improve the solution for some time)

It's important to mention that this method will yield meaningful order between the images, but the ratings themselves will not have any meaning. There's no difference between a rating of 3 or 4 if there are no images with ratings between 3 or 4. After the search algorithm runs, you may want to simply take the order information, and distribute the images evenly in the rating space (if you have 3 images, they'll have final ratings of 1, 3 and 5, for example).

Here's some code to illustrate this:

import random

N_ELEMS= 5 #number of images
N_RATINGS= 5 #number of (faked) user-given ratings
MIN_SCORE, MAX_SCORE= 0.0, 5.0
N_ITERATIONS= 1000 #for search stopping condition

def random_score():
    #generate random score between MIN_SCORE and MAX_SCORE
    return MIN_SCORE+random.random()*(MAX_SCORE-MIN_SCORE)

elements=range(N_ELEMS) #this would be strings or objects in the real world
ratings= [] #tuples of (elem_a, elem_b), representing rating a<b
for i in range(N_RATINGS):
    #generate fake ratings
    while True:
        elem_a, elem_b=(random.choice(elements),random.choice(elements))
        if elem_a!=elem_b:
            break
    ratings.append((elem_a, elem_b))
scores= [random_score() for i in range(N_ELEMS)] #assign random scores

def evaluate_condition( rating ):
    #is a user-provided rating true, given the current scores
    return scores[rating[0]]<scores[rating[1]]
def metric():
    #number of true conditions
    return sum( map(evaluate_condition, ratings))

no_improvement_iterations=0 #number of successive iterations where there has been no improvement
current_score= metric()
while no_improvement_iterations<N_ITERATIONS:
    change_element= random.randint(0,N_ELEMS-1)
    new_value= random_score()
    old_value= scores[change_element]
    scores[change_element]= new_value
    new_score= metric()
    if new_score<=current_score:
        scores[change_element]= old_value
        no_improvement_iterations+=1
    else:
        no_improvement_iterations=0
        current_score= new_score

def distribute_scores(scores):
    '''distribute scores evently in the interval (MIN_SCORE, MAX_SCORE)'''
    sorted_scores= sorted(scores)
    order= [sorted_scores.index(x) for x in scores] #inefficient but easy to understand
    step= (MAX_SCORE-MIN_SCORE)/(len(order))
    return [x*step for x in order]

print "ratings:", ", ".join(["{0}<{1}".format(a,b) for a,b in ratings])
print "scores:", scores
print "distributed scores:", distribute_scores(scores)

And output:

ratings: 1<2, 3<4, 0<3, 4<3, 4<0
scores: [2.3647080073611955, 0.7188260611863462, 4.295792794993049, 4.286501742802684, 0.3471914376983337]
distributed scores: [2.0, 1.0, 4.0, 3.0, 0.0]

This is not strictly hill climbing, since we generate new_value randomly. Hill climbing would choose new_value to maximize the score, but we can't calculate that directly, so we use random optimization

Also, obviously, this is not the best way to solve the search problem - a genetic algorithm that operated on the order would probably be faster, for example. I aimed for simplicity, not efficiency.

Related