Коэффициент корреляции по большому набору данных двоичного изображения — низкая производительность

Я пытаюсь создать OCR, вычислив коэффициент корреляции между символами, извлеченными из изображения, с каждым символом, который я предварительно сохранил в базе данных. Моя реализация основана на Java, и предварительно сохраненные символы загружаются в ArrayList в начале приложения, т.е.

ArrayList<byte []> storedCharacters, extractedCharacters;
storedCharacters = load_all_characters_from_database();
extractedCharacters = extract_characters_from_image();

// Calculate the coefficent between every extracted character
// and every character in database.
double maxCorr = -1;
for(byte [] extractedCharacter : extractedCharacters)
  for(byte [] storedCharacter : storedCharactes)
  {
     corr = findCorrelation(extractedCharacter, storedCharacter)
     if (corr > maxCorr)
       maxCorr = corr;
  }
...
...
public double findCorrelation(byte [] extractedCharacter, byte [] storedCharacter)
{
  double mag1, mag2, corr = 0;
  for(int i=0; i < extractedCharacter.length; i++)
  {
     mag1 += extractedCharacter[i] * extractedCharacter[i];
     mag2 += storedCharacter[i] * storedCharacter[i];
     corr += extractedCharacter[i] * storedCharacter[i];
  } // for
  corr /= Math.sqrt(mag1*mag2);
  return corr;
}

Количество извлеченных символов составляет около 100-150 на изображение, но в базе данных хранится 15600 двоичных символов. Проверка коэффициента корреляции между каждым извлеченным символом и каждым сохраненным символом влияет на производительность, поскольку для завершения каждого изображения требуется около 15-20 секунд с процессором Intel i5. Есть ли способ улучшить скорость этой программы или предложить другой путь построения, дающий аналогичные результаты. (Результаты, полученные при сравнении каждого символа с таким большим набором данных, довольно хороши).

заранее спасибо

ОБНОВЛЕНИЕ 1

public static void run() {
    ArrayList<byte []> storedCharacters, extractedCharacters;
    storedCharacters = load_all_characters_from_database();
    extractedCharacters = extract_characters_from_image();
    
    // Calculate the coefficent between every extracted character
    // and every character in database.
    computeNorms(charComps, extractedCharacters);       
    double maxCorr = -1;
    for(byte [] extractedCharacter : extractedCharacters)
      for(byte [] storedCharacter : storedCharactes)
      {
         corr = findCorrelation(extractedCharacter, storedCharacter)
         if (corr > maxCorr)
           maxCorr = corr;
      }
    }
}
private static double[] storedNorms;
private static double[] extractedNorms;
       
// Correlation  between to binary images
public static double findCorrelation(byte[] arr1, byte[] arr2, int strCharIndex, int extCharNo){
         final int dotProduct = dotProduct(arr1, arr2);
         final double corr = dotProduct * storedNorms[strCharIndex] * extractedNorms[extCharNo];
         return corr;
}
    
public static void computeNorms(ArrayList<byte[]> storedCharacters, ArrayList<byte[]> extractedCharacters) {
          storedNorms = computeInvNorms(storedCharacters);
          extractedNorms = computeInvNorms(extractedCharacters);
}
    
private static double[] computeInvNorms(List<byte []> a) {
         final double[] result = new double[a.size()];
         
         for (int i=0; i < result.length; ++i) 
            result[i] = 1 / Math.sqrt(dotProduct(a.get(i), a.get(i)));
         return result;
}
      
private static int dotProduct(byte[] arr1, byte[] arr2) {
         int dotProduct = 0; 
         for(int i = 0; i< arr1.length; i++)
            dotProduct += arr1[i] * arr2[i];
          
         return dotProduct;
}

person javasuns    schedule 14.05.2014    source источник
comment
Ваш вопрос довольно расплывчатый. В любом случае, способ исследования состоит в том, чтобы не пытаться сначала сопоставить буквы-прототипы, а затем сравнивать только с буквами, связанными с этим прототипом. Например, если буква очень близка к X и сильно отличается от a, то вы сравниваете ее только с x, X, Y, а не с s. Конечно, найти набор букв и его лучший прототип — задача нетривиальная.   -  person SJuan76    schedule 14.05.2014
comment
Насколько велики ваши массивы байтов? Вы можете подготовить модель с низким разрешением (меньший массив) для предварительного теста, как предлагает SJuan76.   -  person Maxx    schedule 14.05.2014
comment
И вы можете использовать более одного потока (я думаю, что i5 допускает 4 потока).   -  person assylias    schedule 14.05.2014
comment
ЕСЛИ я правильно понял, узкое место - это циклы с вызовом findCorrelation. Можно было бы дать более целенаправленные советы и предложения, если бы вы предоставили код этого метода. В противном случае можно давать только общие утверждения, такие как оптимизация высокого уровня (многопоточность, как было упомянуто), или подсказки, относящиеся к самому подходу (т. сложно, если вы не описываете подход более подробно.   -  person Marco13    schedule 14.05.2014
comment
Фрагмент, который вы показываете, не следует оптимизировать. Возможно улучшение того, что вы скрываете (например, findCorrelation). На этом уровне можно было только распараллелить (что может и стоит, но ускорение ограничено количеством ядер).   -  person maaartinus    schedule 14.05.2014
comment
Что ж, метод findCorrelation() — это просто цикл, который проходит через пиксели ExtractedCharacter и сравнивается с пикселями символов в базе данных. Каждый символ имеет размер 12x16, так что да, я согласен, что создание буквы меньшего размера будет полезно, но это может повлиять на результаты. Использование mutithreading также не является ответом, который я ищу, потому что я хотел бы, чтобы приложение имело очень близкие тайминги независимо от архитектуры ЦП. Возможно, использование нейронных сетей или извлечение характеристик персонажа перед запуском процедуры корреляции было бы эффективным подспорьем.   -  person javasuns    schedule 14.05.2014
comment
Также база данных содержит наборы символов каждого шрифта, поэтому в будущем может быть добавлено больше шрифтов. Это снова повлияет на производительность.   -  person javasuns    schedule 14.05.2014
comment
Существует один цикл (i) для двумерного массива, а j не определен. Так что я не уверен, что происходит. В любом случае, он не может скомпилироваться. Если вы действительно обрабатываете двумерный массив, преобразуйте его в одномерный и наслаждайтесь ускорением.   -  person maaartinus    schedule 15.05.2014
comment
@maaartinus, ну, я не уверен, есть ли большая разница между 2D-массивами и 1D-массивами с Java HotSpot 1.7.   -  person javasuns    schedule 15.05.2014
comment
Могу поспорить, что есть разница, поскольку ваши данные не помещаются в L2, а косвенность означает промах кеша (и предварительная выборка невероятно помогает). Кроме того, проверка границ массива является обязательной (она выводится из цикла, но только на 10 или 15 итерациях все еще измерима).   -  person maaartinus    schedule 15.05.2014
comment
Интересно, в чем отличие, поскольку даже ваш оригинальный алгоритм занимает всего 1,3 секунды на моем процессоре i5-2400 с тактовой частотой 3,10 ГГц.   -  person maaartinus    schedule 15.05.2014
comment
@maaartinus В исходном алгоритме есть два отличия от того, который я представил здесь. а) Символы, извлеченные из изображения, и те, которые сохранены, находятся в 2D-массиве. Я использую 2D-массив, потому что им легче манипулировать с помощью x, y. б) Для каждого извлеченного символа я также запускаю нормализацию, чтобы привести символ к тому же размеру, что и символы, загруженные из базы данных, в чем я ошибся, сказав, что их размер составляет 12x16. они 16х25   -  person javasuns    schedule 16.05.2014
comment
ХОРОШО. Таким образом, ваши символы в два раза больше, но ваша версия на моем компьютере работает в 10 раз быстрее. Коэффициент 5 остается ... и я бы приписал большую его долю вашим 2D-массивам! Я бы сказал, что 2D-массивами легче манипулировать, только если вы не написали вспомогательный класс длиной, может быть, 15 строк.   -  person maaartinus    schedule 16.05.2014
comment
@maaartinus Ну, например, изображение со всеми символами имеет размер 640x480. Поместив его в 2D-массив, можно получить пиксель напрямую с помощью x, y, в то время как в 1D-массиве мне понадобится высота и некоторые вычисления извлечения, чтобы получить пиксель. 2D Array =› image[x][y] 1D Array =› image[x * height + y] Считаете ли вы, что x * height + y будет иметь лучшую производительность, чем [x][y]?   -  person javasuns    schedule 17.05.2014
comment
1. Какая часть вашего приложения занимает больше всего времени? Я думаю, это тот, о котором вы спрашиваете в этом вопросе. 2. Нужны ли ему x и y? Нет, он просто проходит через все пиксели 3. Таким образом, для скорости одномерный массив лучше. 4. Даже для вычислений, требующих 2D-доступа, можно использовать 1D-макет. Умножение составляет всего 3 цикла, промах кэша L1 может быть 10. В цикле умножение оптимизируется.   -  person maaartinus    schedule 17.05.2014


Ответы (1)


В настоящее время трудно найти процессор с одним ядром (даже в мобильных телефонах). Поскольку задачи хорошо разделены, вы можете сделать это всего несколькими строками. Так что я бы пошел на это, хотя выигрыш ограничен.

Если вы действительно имеете в виду взаимную корреляцию, то преобразование типа ТПФ или DCT может помочь. Для больших изображений они точно подходят, а вот с вашим 12х16 я не уверен.

Может быть, вы имеете в виду просто точечный продукт? А может, ты нам расскажешь?

Обратите внимание, что вам на самом деле не нужно вычислять корреляцию, большую часть времени вам нужно только выяснить, превышает ли она пороговое значение:

corr = findCorrelation(extractedCharacter, storedCharacter)
..... more code to check if this is the best match ......

Это может привести к некоторой оптимизации или нет, в зависимости от того, как выглядят изображения.

Также обратите внимание, что простая низкоуровневая оптимизация может дать вам почти 4-кратный коэффициент, как в этом моем вопросе. Может, тебе действительно стоит рассказать нам, что ты делаешь?

ОБНОВЛЕНИЕ 1

Я предполагаю, что из-за вычисления трех продуктов в цикле достаточно параллелизма на уровне инструкций, поэтому цикл разворачивается вручную, как в мом выше вопросе не нужно.

Однако я вижу, что эти три продукта вычисляются несколько раз 100 * 15600, в то время как только один из них зависит как от extractedCharacter, так и от storedCharacter. Таким образом, вы можете вычислить

100 + 15600 + 100 * 15600

точечные произведения вместо

 3 * 100 * 15600

Таким образом, вы можете легко получить коэффициент три.

Или не. После этого шага на соответствующем шаге вычисляется единственная сумма, и применяется проблема, указанная выше. Как и его решение (развертывание вручную).

Фактор 5.2

результаты

Хотя byte[] довольно компактен, вычисления включают в себя расширение их до целых чисел, что требует некоторого времени, поскольку мой контрольный показатель показывает. Преобразование byte[]s в int[]s до того, как будут вычислены все корреляции, экономит время. Еще лучше использовать тот факт, что это преобразование для storedCharacters можно сделать заранее.

Двойное развертывание цикла вручную помогает, а повторное развертывание — нет.

person maaartinus    schedule 14.05.2014
comment
Я включил в исходный пост код для findCorrelation(). Процедура возвращает коэффициент корреляции между двумя изображениями A и B, где A и B — векторы одинакового размера, содержащие значения 0 и 1. Возвращаемое значение corr представляет собой значение от 0,0 до 1,0, показывающее, насколько близки два изображения, где 1 означает, что они идентичны. - person javasuns; 14.05.2014
comment
@javasuns: понятно. Это то, что я назвал dot product выше (я не говорю, что корреляция неверна, просто я понял это по-другому). Забудьте о преобразованиях, упомянутых выше. Я скоро отредактирую свой ответ. - person maaartinus; 14.05.2014
comment
Ну, я понимаю, что вы говорите, и это, вероятно, может улучшить производительность. Я опубликую свои обновленные результаты - person javasuns; 15.05.2014
comment
Что ж, я попытался изменить byte на int, но не увидел реальной разницы в производительности. Какую версию Java вы используете? - person javasuns; 16.05.2014
comment
Замена bytes на int сама по себе, вероятно, мало поможет; Я думаю, вам сначала нужна другая (и более простая) оптимизация. java -version openjdk version "1.9.0-internal", но это не имеет значения (я провел много тестов, и они почти не изменились, когда я временно перешел с 1.7 на этот экспериментальный сетап). - person maaartinus; 16.05.2014
comment
Какая другая и более простая оптимизация? :) - person javasuns; 17.05.2014
comment
@javasuns См. timeMine1 в моем связанном тесте. Кстати, с переключением на 16x25 время просто удвоилось, как и ожидалось и соотношение осталось прежним. Возвращение к версии Java 1.7.0_55 ничего не дало. - person maaartinus; 17.05.2014
comment
Спасибо за все что ты сделал для меня. При изменении кода в соответствии с вашими указаниями обработка символов в 2D-массивах завершается за 2200 мс, что на 1/3 быстрее, чем раньше. Я не изменил их на массив целых чисел, просто оставил их как массивы байтов. Это результаты, которые вы получаете? Также я заметил, что если я изменю код, в котором скалярное произведение вычисляется между сохраненным символом и извлеченным символом (во вложенных циклах) с final int dotProduct = (int) Math.random() * 1000; процедура завершается менее чем за 200 мс. Таким образом, эта линия является той, которая берет на себя весь процесс. - person javasuns; 17.05.2014
comment
Что вы подразумеваете под 1/3 быстрее, чем раньше? Что-то вроде 66%? Слишком хромой. Мой первый шаг привел к ускорению почти в 3 раза (см. timeMine1). Обратите внимание, что здесь есть только тривиальное изменение (сохранение двух из трех вычислений скалярного произведения). Посмотрите код или получите калипер и запустите его. Изменение на int[] происходит позже для большего ускорения, поэтому можно игнорировать его, пока вы не добьетесь успеха в первой части. И, конечно же, это точечный продукт, который ест все время. - person maaartinus; 17.05.2014
comment
Как вы думаете, есть ли другой способ повысить производительность, избегая выполнения всех этих вычислений? Мне нужно еще больше сократить время, потому что добавление большего количества персонажей наверняка повлияет на производительность позже. Еще раз спасибо за вашу помощь - person javasuns; 18.05.2014
comment
Преобразование 2D-изображений массива в 1D-массив перед процедурой снизило время на два 1100 мс, что на 50% больше. Однако изменение массивов с byte на int не дает никакого увеличения производительности в моей ситуации, но почти всегда дает худшее время, будучи примерно на 100 мс медленнее. - person javasuns; 18.05.2014
comment
@javasuns Я не вижу способа ускорить что-то вроде timeMine2 в Java (в сборке вы можете использовать инструкции SIMD). Может есть какая-то оптимизация высокого уровня, я не знаю. Вы должны опубликовать код, как это сделал я, потому что теперь никто не знает, как он выглядит. - person maaartinus; 18.05.2014