Размытая ошибка

в консоли было пусто

Однажды я написал очень простой код для размытия изображения in-place:

  for (var y = 0; y < fullh; y++) {
  for (var x = 0; x < fullw; x++) {
    var r = 5
    var v = 0
    for (var dy = -r; dy <= +r; dy++) {
      var yy = y + dy
      if (yy < 0) yy = 0; else if (yy >= fullh) yy = fullh-1
      for (var dx = -r; dx <= r; dx++) {
        var xx = x + dx
        if (xx < 0) xx = 0; else if (xx >= fullw) xx = fullw-1
        v += fullmask[yy][xx]
      }
    }
    v /= (2*r+1)*(2*r+1)
    if (v < 0) v = 0; else if (v > 1) v = 1
    fullmask[y][x] = v
  }
}

Тут мы обходим все пиксели изображения и просто заменяем каждый пиксель усреднением его квадратной области с радиусом r = 5. Результат размытия:

Изображение действительно размылось, но большая часть изображения стала чёрной!

Первая мысль — кривое обращение к массиву. В этих случаях Chrome спасает сообщениями вроде "Uncaught TypeError: Cannot read property '0' of undefined, sucker!". Но в консоли было пусто…

Как не надо отлаживать код

Что делать, если F12 не помогает? Я не умею пользоваться отладчиками, поэтому в таких случаях начинаю прокручивать код в голове, пытаясь логически найти ошибку. Но это тоже ничего не дало. Ещё я имею некоторый опыт неравной борьбы с бажно оптимизирующими компиляторами для микроконтроллеров, поэтому уже начал задумывать недоброе насчёт оптимизатора js-машины Chrome. Возможно его оптимизатор отключается? Мы бы точно это узнали, но сначала я решил протестировать цикл на пограничные условия, сузив его покрытие:

for (var y = 16; y < fullh-16; y++) {
  for (var x = 16; x < fullw-16; x++) {

Удивительно, но это убрало чёрный занавес! Только края изображения, конечно, теперь перестали обрабатываться. Честно говоря, в моём конкретном случае я мог бы спокойно пожертвовать пограничной областью изображения. Но не понимая причин ошибки, мы не можем гарантировать область её действия, поэтому необходимо разобираться до конца.

Эта благая мысль вновь вернула меня к прокручиванию кода в голове. Но я снова не обнаружил в нём ничего криминального. Стыдно признаться, что я с ним делал, так что лучше не буду. Но в результате всех этих различных манипуляций я заметил, что если мы будем читать не элемент fullmask[yy][xx], а fullmask[y][x], то чёрной области не возникает и изображение обрабатывается полностью. Это открытие навело меня на множество различных догадок, все из которых, впрочем, оказались ошибочными. Поэтому, не смотря на открытие, я отчаялся что-то найти в этом блоке. ## Расширение области локализации ошибки

Я вернул код к первоначальному виду и решил посмотреть вокруг. Что предшествует данному коду? От чего он может зависеть? Коду размытия предшествовал код интерполяции, строящий матрицу fullmask на основе матрицы меньшего размера mask путём билинейной интерполяции:

var fullmask = new Array(fullh)
for (var y = 0; y < fullh; y++) {
  var yy = Math.floor(y / (fullh / h))
  var dy = y % (fullh / h)
  var ky = 1.0 - dy / (fullh / h)
  fullmask[y] = new Array(fullw)
  for (var x = 0; x < fullw; x++) {
    var xx = Math.floor(x / (fullw / w))
    var dx = x % (fullw / w)
    var kx = 1.0 - dx / (fullw / w)
    var v1 = kx * mask[yy][xx]   + (1.0 - kx) * mask[yy][xx+1]
    var v2 = kx * mask[yy+1][xx] + (1.0 - kx) * mask[yy+1][xx+1]
    var v = ky * v1 + (1.0 - ky) * v2
    if (v < 0) v = 0; else if (v > 1) v = 1
    fullmask[y][x] = v
  }
}

Также вполне тривиальный код. Однако, к моему удивлению, при его закоменчивании до

var fullmask = new Array(fullh) for (var y = 0; y < fullh; y++) { fullmask[y] = new Array(fullw) for (var x = 0; x < fullw; x++) { fullmask[y][x] = 1 } }

мы снова получаем полное размытие, пусть и чисто белого изображения (fullmask[y][x] = 1). Следовательно, данный код как-то влияет на код размытия. Хм!

Проверка на нечисла

Тут мне пришла наконц-то хорошая идея: проверить конечное изображение на наличие значений undefined и NaN:

if ((typeof v != 'undefined') && (!isNaN(v)))

Что выявило следующее:

Здесь красным выведены значения undefined и NaN. Т.е. та область, которая выглядела чёрной, была на самом деле никакой (NaN). И лишь интерпретировалась канвой как чёрная.

Анализируя, далее, код интерполяии, мы выдим, что он использует хак лишнего элемента, т.е. матрица mask имеет вместо необходимых w x h размеры (w+1) x (h+1), что позволяет не производить дополнительные проверки на выход за пределы матрицы при интерполяции.

var v1 = kx * mask[yy][xx]   + (1.0 - kx) * mask[yy][xx+1]
var v2 = kx * mask[yy+1][xx] + (1.0 - kx) * mask[yy+1][xx+1]

Однако эти дополнительные элементы оказались нигде не инициированными — undefined. А арифметические операции с undefined в js дают NaN.

Но почему тогда после размытия в ничто превращается целая непрямоугольная область изображения? А не лишь его край?

Ответ можно проиллюстрировать, изменив радиус размытия с 5 до 1:

При таком изменении мы наблюдаем, что уничтожение изображения замедлилось. Следовательно сам код размытия, как лесной пожар, размножает NaN-значения, и чем больше радиус, тем быстрее размножение. И это, действительно, следует из логики его алгоритма.

До же размытия NaN-область занимает лишь самые границы изображения (справа):

 

Заключение

В конце хочу обозначить, что я на мой взгляд при этом дебаге делал правильно, а что — нет. Неправильным было недоверие к тривиальному, но новому коду, а также попытка проецировать вину за некорректность на внешние факторы, в данном случае на оптимизирующий JIT-компилятор браузера. Что, впрочем, всегда всё-таки нужно также держать в голове как маловероятный, но возможный источник багов. Много времени было потрачено на всестороннее тестирование корректного блока кода и очень долго не хотелось верить, что ошибка может таиться в уже казалось бы отлаженном коде, предшествующему данному. Правильным было решение обязательно выяснить причины и механику найденной ошибки. А вот истинной причиной, той неправильной строкой инициализации матрицы, которая и приводила в конечном итоге к значительной деструкции изображения, я обязан тому, что с недостаточным вниманием отнёсся к внесению хака с лишним элементом: размер массива был увеличен, но инициализация не была корректно обновлена, и появились коварные значения undefined. Можете трактовать это как кару за C-like кодинг в js]

Как-то кто-то сказал: if it isn't tested, it's broken. Что ж, стоит признать, что отладка прошла бы намного быстрее, если бы в коде растеризации матрицы на канве изначально была бы встроена проверка на ненормальные значения.

 

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

 

Желаю всем писать код любой сложности с первого раза.

 

shitpoet@gmail.com

 



 

free hit counters