Създаване на приложение, което ви позволява да маркирате и запазвате пояснения към дадено изображение

Анотацията на изображението е процес на етикетиране на различни обекти в изображение. Основното приложение на анотацията на изображения е генерирането на данни, които могат да се използват за обучение на алгоритми за машинно обучение. Анотацията на изображението е задача, която се извършва главно ръчно. Работата по анотациите на изображения вече е неизбежна част от машинното обучение и AI и се възлага основно на външни изпълнители в страни като Индия и Филипините. В тази част ще изградим приложение за анотации на изображения, което ви позволява да маркирате и запазвате анотации към дадено изображение.

Има много библиотеки и инструменти, които ни позволяват да правим анотации на изображения, включително много опции на цена като Labelbox, но за това приложение ще използваме безплатна библиотека, наречена Annotorious.

Преди да започнем разработването на частта за анотации на изображения, нека набързо да разгледаме задната част на нашето приложение. Annotorious е javascript библиотека, която може да се използва с всякакви бек-енд рамки. Тъй като се чувствам по-удобно в релсите, ще изградим приложение за релси. Нека бързо да настроим нашето приложение и back-end.

Нека създадем ново приложение. Въведете в терминала:

rails new image_annotater

Нашето приложение трябва да съдържа две таблици - таблица с елементи за съхраняване на различни изображения и таблица с етикети за съхраняване на анотациите. Целта е да създадете множество етикети върху изображение на артикул.

В нашите артикули трябва да качим изображения. За това можем да използваме скъпоценния камък на носещата вълна. Добавете следните редове към GemFile:

gem ‘carrierwave’, ‘~> 0.11.2’
gem ‘mini_magick’, ‘~> 4.8’

Няма да навлизам в подробности за внедряването на носеща вълна.

Нека генерираме модела Items.

rails g model Item

Освен това добавете следния ред към routes.rb

resources :items

Миграцията на модела трябва да има следните полета:

image_annotater/db/migrate/20190123073338_create_items.rb

class CreateItems < ActiveRecord::Migration[5.2]
 def change
   create_table :items do |t|
     t.string :name
     t.text :description
     t.string :image
     t.timestamps
   end
 end
end

Сега нека създадем модела на етикетите:

rails g model Label

Също така добавете това към routes.rb:

resources :labels

Това е най-важната таблица в това приложение. Координатите на анотациите или правоъгълните полета, създадени от Annotorious, трябва да бъдат запазени в този модел. И така, миграцията трябва да изглежда така:

class CreateLabels < ActiveRecord::Migration[5.2]
  def change
    create_table :labels do |t|
      t.string :text
      t.string :context
      t.decimal :x_value
      t.decimal :y_value
      t.decimal :width
      t.decimal :height
      t.references :item, index: true
      t.timestamps
    end
  end
end

Можем да видим, че координатите x и y, заедно с височината и ширината, се очакват в анотация, заедно с текст. Контекстът е пътят към образа. Можем също да видим, че има препратка към артикул. Това означава, че един елемент или изображение може да има няколко етикета.

Моделът на артикула трябва да бъде като този:

/image_annotater/app/models/item.rb

class Item < ApplicationRecord
 mount_uploader :image, ImageUploader
 has_many :labels
end

Моделът на етикета трябва да бъде като този:

/image_annotater/app/models/label.rb

class Label < ApplicationRecord
 belongs_to :item
end

ItemsControllerще бъде нормален CRUD контролер.

LabelsController ще бъде така:

class LabelsController < ApplicationController
  before_action :set_label, only: [:show, :edit, :update, :destroy]
  def index
    @labels = label.all
  end

  # GET /labels/1.json
  def show
  end
  def new
    @label = label.new
  end
  def edit
  end

  def create
    @label = Label.new(label_params)
    @label.save
    render(:json => {}, :status => :created)
  end
  def update
    respond_to do |format|
      if @label.update(label_params)
        render(:json => {}, :status => :updated)
      else
        render(:json => {}, :status => :not_created)
      end
    end
  end

  def destroy
    @label.destroy
    render(:json => {}, :status => :removed)
  end
private

    def set_label
      @label = Label.find(params[:id])
    end
    def label_params
      params.require(:label).permit(:text, :context, :x_value, :y_value, :width, :height, :item_id)
    end
end

Можете да видите, че нашият LabelsController има опция за създаване, актуализиране и изтриване на нови етикети.

Нашият бекенд е почти готов. Нека изградим два потребителски интерфейса.

Първият ще бъде формата за създаване на елементи. Потребителите ще могат да качват изображение и да създават елемент от тази страница.

Следващата ще бъде страница за показване на елементите. Тази страница е важна, тъй като анотацията (създаването на етикетите) ще бъде извършена на тази страница.

<p>
  <strong>Name:</strong>
  <%= @item.name %>
</p>
<p>
  <strong>Description:</strong>
  <%= @item.description %>
</p>
<p>
  <strong>Image:</strong>
  <div>
  <%= image_tag(@item.image.url, size: "800x500")%>
  </div>
<%= hidden_field_tag 'item_id', @item.id %>
</p>

Annotorious изпълнение

За да настроите Annotorious в нашето приложение, първо изтеглете най-новата версия на Annotorious от официалния сайт. Изтегленият zip съдържа файл annotorious.min.js, CSS папка с файла annotorious.css. Папката CSS също ще съдържа някои необходими файлове с изображения.

Ако добавяме annotorious към обикновен HTML файл, трябва само да добавим следните редове към главата на страницата:

<link type=”text/css” rel=”stylesheet” href=”css/annotorious.css” /><script type=”text/javascript” src=”annotorious.min.js”></script>

Но тъй като използваме това в приложение за rails, отделете файловете и поставете js файла в папката app/assets/javascripts, CSS файла в app/assets/stylesheets иimages вприложение/активи/изображения.

Сега можем да направим нашето изображение годно за анотиране. Има два начина да направите това. API на Annotorious Javascript вече е наличен в нашите страници, който може да бъде извикан от променливата anno

Вариант 1. CSS клас с възможност за анотиране

Добавете CSS клас с анотация към етикета на изображението. При зареждане на страницата Annotorious автоматично ще сканира страницата ви за изображения с този клас и ще ги направи анотиращи.

Пример:

<img src="example.jpg" class="annotatable" />

Вариант 2: Използване на JavaScript

Javascript API на Annotorious може да се използва, за да направите изображенията „ръчно“ анотирани.

Пример:

<script>
  function init() {
    anno.makeAnnotatable(document.getElementById('myImage'));
  }
</script>
...
<body onload="init();">
  <img src="example.jpg" id="myImage" />
</body>

Ще използваме Вариант 2.

Нашето изображение е в app/views/items/show.html.erb. Добавете идентификатор към него, за да можем уникално да го идентифицираме.

<%= image_tag(@item.image.url, size: "800x500", id: "annotatable")%>

Сега в частта JS. Добавете функция, за да направите изображението с идентификационен номер за анотиране годно за анотиране.

function init() {
   anno.makeAnnotatable(document.getElementById(‘annotatable’));
 }

Извикайте тази функция, когато документът е готов.

$( document ).ready(function() {
  init();
  function init() {
    anno.makeAnnotatable(document.getElementById('annotatable'));
    }
}

Сега, ако отново проверим страницата за показване на елемента и плъзнем мишката върху изображението, можем да видим, че можем да създаваме анотации върху изображението.

Страхотно!!

Библиотеката Annotorious върши цялата работа вместо нас и опцията за анотиране е готова. Но остават още две неща.

  1. Опции за създаване, актуализиране и изтриване на етикети/анотации от потребителския интерфейс
  2. Показване на всички етикети върху изображението на елемента, когато страницата се зареди.

Етикетите за създаване, изтриване и актуализиране могат да се обработват от манипулаторите на събития, предоставени от Annotorious.

Идеята е да се получат данните за анотацията и да се предадат като AJAX функция към LabelsController, когато възникнат събития като създаване, редактиране и изтриване на анотация.

Целият следващ код трябва да бъде написан в нашата init()функция.

Създайте етикетите

Всеки път, когато се начертае анотация и се щракне върху бутона за запазване, можем да задействаме манипулатора на събитие onAnnotationCreated(annotation)върху променливатаanno. Така че в нашия случай можем да напишем манипулатора на събитие като:

anno.addHandler('onAnnotationCreated', function(annotation) {
     var text = annotation.text;
     var context = annotation.src;
     var x = annotation.shapes[0].geometry.x;
     var y = annotation.shapes[0].geometry.y;
     var width = annotation.shapes[0].geometry.width;
     var height = annotation.shapes[0].geometry.height;
     var id = $("#item_id").val();
     $.ajax({
         type: 'POST',
         url: "/labels/",
         data: {
           label :{
                 text:text,context:context,
                 x_value:x,y_value:y,width:width,
                 height:height,item_id:id
            } 
          },
       success: function(data) {}
     });
    });

Можем да видим как координатите и текстът от обекта за анотация се извличат и след това се предават като AJAX на функцията Create в LabelController.

Показване на всички етикети

Преди да преминем към актуализиране и изтриване, можем да използваме тази опция, за да покажем всички анотации или етикети при зареждането на страницата. За целта трябва да добавим нова функция към ItemsControllerза да получим всички етикети за даден елемент.

#app/controllers/items_controller.rb
def get_labels
  labels = Item.find(params[:id]).labels
  render json: labels
end

В JS частта трябва да извикаме метода CreateAnnotation на annotorious, за да преначертаем всички запазени етикети. Това може да стане по следния начин:

$.ajax({
        type: "POST",
        dataType: "json",
        url: "/items/get_labels",
        data: {
         id: 6
        },
        success: function(data){
         $.each(data, function() {
          var myAnnotation = {}
          $.each(this, function(k, v) {
          if(k == 'text'){
            myAnnotation["text"] = v;
          }
          if(k == 'id'){
           myAnnotation["id"] = v;
          }
          if(k == 'context'){
           myAnnotation["src"] = v;
          }
          if(k == 'x_value'){
           myAnnotation['x_value'] = v;
          }
          if(k == 'y_value'){
           myAnnotation['y_value'] = v;
          }
          if(k == 'height'){
           myAnnotation['height'] = v;
          }
          if(k == 'width'){
           myAnnotation['width'] = v;
          }
         });
         var annotation = create_annotation(myAnnotation);
         anno.addAnnotation(annotation)
        });
        }
    });
create_annotation = function(myAnnotation_hash){
     var myAnnotation = {
      src : myAnnotation_hash["src"],
      text : myAnnotation_hash["text"],
      shapes : [{
          type : 'rect',
          geometry : {
            x : parseFloat(myAnnotation_hash["x_value"]),
            y: parseFloat(myAnnotation_hash["y_value"]),
            width : parseFloat(myAnnotation_hash["width"]), 
            height: parseFloat(myAnnotation_hash["height"]),
            label_id: myAnnotation_hash["id"] }
      }]
  }
     return myAnnotation;
    } 
  }
  });

Забележете, че създадохме хеш от данни за анотации от базата данни и използвахме този хеш за създаване на анотации. Освен това можете да видите, че добавих първичния ключ на етикета към геометрията на анотацията като label_id. Създадената анотация се добавя към обекта anno като anno.addAnnotation(annotation).

Актуализиране на етикетите

За да актуализирате текста в съществуващ етикет, щракнете върху иконата на молив върху анотацията. Ще бъде предложена опцията за добавяне на нов текст:

Когато щракнем върху опцията Редактиране, манипулаторът на събитие onAnnotationUpdated(annotation)се задейства. Можем да използваме това, за да актуализираме етикета.

anno.addHandler('onAnnotationUpdated', function(annotation) {
 var label_id = annotation.shapes[0].geometry["label_id"];
if(label_id == "" || label_id != null){
   var text = annotation.text;
   var context = annotation.src;
   var x = annotation.shapes[0].geometry.x;
   var y = annotation.shapes[0].geometry.y;
   var width = annotation.shapes[0].geometry.width;
   var height = annotation.shapes[0].geometry.height
   var item_id = $("#item_id").val();
   $.ajax({
       type: 'PUT',
       url: "/labels/"+label_id,
       data: {
        label :{
           text:text,
           context:context,
           x_value:x,
           y_value:y,
           width:width,
           height:height,
           item_id: item_id
        } 
       },
       success: function(data) {}
     });
     }
  });

Премахване на етикетите

За да премахнете запазена анотация, щракнете върху знака X върху анотацията. Това ще задейства anno.onAnnotationRemovedобратно извикване.

anno.addHandler('onAnnotationRemoved', function(annotation) {
 var label_id = annotation.shapes[0].geometry["label_id"];
 if(label_id == "" || label_id != null){
   $.ajax({
       type: 'DELETE',
       url: "/labels/"+label_id,
       data: {
       },
       success: function(data) {}
     });
     }
  });

Това е.

Вече можем да създаваме, актуализираме и изтриваме етикети и да ги извличаме при презареждане на страницата. Един недостатък на текущата реализация е, че за да премахнем или редактираме създаден етикет, трябва да презаредим страницата сега, тъй като създаването става чрез AJAX.

Има много другиманипулатори на събитияи функции, налични в Annotorious, които не са обсъдени в тази част. Вижте ги в тяхната официална документация.

Освен това в Annotorious са налични много плъгини, проверете и тях. Създаването на плъгини е толкова лесно — дори създадох плъгин за добавяне на падащо меню към текстовото поле за анотации.

Надявам се това да е било полезно!

Код: https://github.com/amkurian/image_annotater