📜 ⬆️ ⬇️

Guessing celebrity

At the cinema there is a quiz called "Guess the celebrity." It is necessary for 10 seconds to guess the actor (director, screenwriter, just a well-known personality) in the photo. The rules are simple, but it’s not so easy to recognize a person. Especially if you do not know. It was here that the idea of ​​"helping" myself in solving it was born.

First you need to decide on the concept. The first thing that comes to mind is to find out the ID of a person and upload his photo. It’s easy to find out a person’s ID, film search is constantly being updated and one of these innovations was autocomplete in the search box (before searching for redirects for another domain - s.opopoisk.ru, this would further complicate the task). Separately, to search for people in it, queries like the following are used:

www.kinopoisk.ru/handler_search_people.php?q={query}


In response, a handsome JSON comes. We have person identifiers, it remains to upload photos. To speed up the download process, we will use thumbnails of photos. They are located at:
')
st.kinopoisk.ru/images/sm_actor{id}.jpg


As you can see, the static is on a different domain (and this will add to our problems). All the data we have, it remains to add a few styles and arrange in the form of user script:

 (function(){ function doMain(){ $('img[name="guess_image"]').css({"border":"1px solid black","margin":"10px 0 10px 0"}); $("#answer_table").parent().css({"background":"#f60","padding-left":"130px","padding-bottom":"30px"}); for (var i=0; i<4; ++i){ $('<div><img src="http://st.kinopoisk.ru/images/users/user-no-big.gif" \ class="cheet_image" width=52 hight=82 /></div>') .bind("click", function(){ $(".cheet_image").css({'box-shadow':'','border':''}); }) .bind("load", function() { $(this).css({'box-shadow':'0 0 10px rgba(0,0,0,0.9)',"border":"1px solid red"}); }) .appendTo("#a_win\\["+i+"\\]"); } $('img[name="guess_image"]').bind("load", function(){ doLoader(0); }); } function doLoader(i){ $.getJSON( "/handler_search_people.php", { q: $("#win_text\\["+i+"\\]").html() }, function(data){ $(".cheet_image").eq(i) .attr('src','http://st.kinopoisk.ru/images/sm_actor/'+data[0].id+'.jpg'); if (i < 4) doLoader(++i); } ); } window.addEventListener('DOMContentLoaded', doMain, false); })(); 

Now every time we upload a new image, we upload photos from the answer options:



The disadvantages of this method include the fact that we still need to perform certain actions - to visually recognize photos. Ideally, we should be required only one action - pressing the "start" button.

We will improve our script. Now we will compare the images and select the correct option based on the comparison. To begin with we will try to compare image hashes. We need to make sure that the conceived image and the statically available equivalent are the same. Open the images in the HEX-editor and see what is wrong:





As you can see, the images are generated dynamically. The only way out is to compare images pixel by pixel. And this is where HTML5 comes to the rescue, in particular the <canvas> . All we have to do is draw the image and call the getImageData(x, y, width, height) method. However, we remember that the image is stored on another domain and there is no talk about CORS :



The way out of this situation is the use of interwindow communication - the postMessage() method and the message event. In the hidden frame, we will load the main page of the domain on which the photos are located, load the image itself, convert it to the base64 string and send it to the parent frame. Although of course, you can do it differently: load an image, dynamically create a canvas element and get an array of pixel values ​​from it. Since the type of the resulting object will not be just Array, but Uint8ClampedArray (a simple 8-bit array), which does not have a join method, will have to use JSON to serialize / deserialize the data. The very saboy is very costly and loses in performance to the first method, which we will use.

First we need to get a base64 encoded image. In the hidden frame we load the main page, and in the anchor we transmit the image identifier and the number of the answer option. In the frame itself, we load the desired image and find its base64 code:

 xhr = new XMLHttpRequest(); xhr.open('GET', '/images/sm_actor/'+hash[0]+'.jpg', false); xhr.overrideMimeType('text/plain;charset=x-user-defined'); xhr.onload = function() { if (xhr.readyState == 4){ var resp = xhr.responseText; var data = 'data:'; data += xhr.getResponseHeader('Content-Type'); data += ';base64,'; var decodedResp = ''; for(var i = 0, L = resp.length; i < L; ++i) decodedResp += String.fromCharCode(resp.charCodeAt(i) & 255); data += btoa(decodedResp); } }; xhr.send(null); 

When sending an image in the Chrome browser, one unpleasant feature emerged: the image obtained in this way is still protected by the CORS policy and you cannot get its data from the canvas. The way out of this deadlock is to embed the script in the code of the page and send the image in this way (as it turned out, this method does not work the first time):

 if (typeof window.chrome == 'undefined') window.parent.postMessage(hash[1]+"|"+data, "http://www.kinopoisk.ru/"); else { var scr = document.createElement("script"); scr.setAttribute('type','application/javascript'); scr.textContent = "window.parent.postMessage('"+hash[1]+"|"+data+"', 'http://www.kinopoisk.ru/');"; document.body.appendChild(scr); } 

Now the most delicious begins - image comparison. First of all, my choice fell on the IM.js library (from the words Image Match, to the well-known Internet Messager has nothing to do). For unknown reasons, she refused to start up with me. I had to study the literature about the comparison of images. I settled on the simplest method — using the ΔE * metric and its simplest implementation, CIE76. Although it uses the color space LAB, we will use it in ordinary RGB. Because of this, errors will inevitably arise, but even with them the result is quite acceptable. Moreover, it will be necessary to convert RGB -> LAB through the intermediate space XYZ, which will cause even greater errors. The essence of CIE76 comes down to finding the rms color:



In code, it looks like this:

 //     //  ,    function doDiff(context) { var all_pixels = 25*40*4; var changed_pixels = 0; var first_data = context.getImageData(0, 0, 25, 40); var first_pixel_array = first_data.data; //     //      canvas var second_ctx = $("#guess_transformed").get(0).getContext('2d'); var second_data = second_ctx.getImageData(0, 0, 25, 40); var second_pixel_array = second_data.data; for(var i = 0; i < all_pixels; i+=4) { if (first_pixel_array[ i ] != second_pixel_array[ i ] || // R first_pixel_array[i+1] != second_pixel_array[i+1] || // G first_pixel_array[i+2] != second_pixel_array[i+2]) // B { changed_pixels+=Math.sqrt( Math.pow( first_pixel_array[ i ] - second_pixel_array[ i ] , 2) + Math.pow( first_pixel_array[i+1] - second_pixel_array[i+1] , 2) + Math.pow( first_pixel_array[i+2] - second_pixel_array[i+2] , 2) ) / (255*Math.sqrt(3)); } } return 100 - Math.round(changed_pixels / all_pixels * 100); } 

Everything is ready, it remains to design all the parts in the form of user scripts and test them.



As we can observe, everything works. The most expensive part is downloading images. That is why all images are loaded sequentially (after receiving the message event). When simultaneously loading images, it took sometimes more than 10 seconds to process all 4 results. Also pay attention to the percentage of the degree of similarity. It is never higher than 96% and less than 75% even with completely different images.

The final chord of our opera will be the addition of an automatic comparison and click on the desired button:

 //   message function doMessage(e) { var data = e.data.split("|", 2); var index = parseInt(data[0]); // ... if (index == 3) $(document).trigger("cheetcompare"); //... } //  main      function doMain(){ // ... $(document).bind("cheetcompare", function(e){ var max = 0; //  input,      var cheetd = $(".cheet_diff"); for(var i = 0; i < 4; ++i) { max = (cheetd.eq(max).val() > cheetd.eq(i).val()) ? max : i; } $("#a_win\\["+max+"\\]").trigger("click"); }); // ... } 

Alas, it was not possible to completely abandon the visual control, from time to time photos do not appear from the avatar, but from the gallery. Nevertheless, they are a minority. A simple filter of visual control will be the search for a result with a degree of similarity above 93. The result of the script can be viewed in this video:



The script has been tested in Opera 12, Chrome 22 + Tampermonkey (if it doesn't work, refresh the page, it doesn't work the first time). In Firefox 16.0.1, the script refused to start up - the getImageData hidden image does not work.

Download the script from userscripts.org: DOWNLOAD

Literature

  1. Getting cross-domain data in Google Chrome via userscript
  2. canvas same origin security violation work-around
  3. Canvas training
  4. Uint8ClampedArray
  5. IM.js: Quick image comparison pixel by pixel
  6. Comparing images and generating picture differences in ruby
  7. Color_Distinct Formula



UPD # 1 As a habrauser Monder rightly noted, an error crept into the formula. Namely, the divider, which is the maximum color difference (maximum color difference). You can visualize this as follows:



If we represent the RGB family in the form of a cube, then the maximum color difference will be the diagonal, which can be found as follows:



It is worth noting that the spread of values ​​has become more adequate: 60% - 95%. Now the bar of the visual filter can be lowered to 90%. In this case, there is almost no similar photo and you have to guess it yourself.

UPD # 2 Habrayuzer nick4fake prompted the successfully forgotten rationing formula to 0 ... 100:

new = (old - min) / (max - min) * 100

Applied to our problem, it looks like this: Y = (X - 55) / 38 * 100 . The spread of values ​​has become even more noticeable, especially for photos in which different shades prevail (light / dark), now it is about 30% - 90%.

Source: https://habr.com/ru/post/154801/


All Articles