Размытая ошибка
в консоли было пусто
Однажды я написал очень простой код для размытия изображения 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