Сообщество Engee

Работа с матрицами пикселей

作者
avatar-yurevyurev
Notebook

Работа с матрицами пикселей

In [ ]:
Pkg.add(["Images", "FileIO", "ImageShow"])
using Images, FileIO, ImageShow

Изображения с использованием пакета Images представляют собой многомерные массивы — по сути, матрицы, где каждый элемент содержит значение цвета (например, RGB). Такой подход позволяет легко манипулировать пикселями, используя стандартные операции работы с массивами: индексацию, срезы, поэлементные вычисления и т.д.

В этом примере мы решаем следующую задачу: загружаем произвольное изображение, делим его на равные вертикальные полосы, затем собираем отдельно полосы с нечётными номерами и отдельно с чётными, формируя два новых изображения. Наконец, выводим их рядом для визуального сравнения.

Этот пример наглядно демонстрирует:

  • как загрузить и отобразить изображение;
  • как получить его размеры;
  • как рассчитать количество полос и их ширину с учётом остатка;
  • как создать новые массивы (изображения) и заполнить их пикселями из исходного;
  • как использовать циклы и условные операторы для обработки каждого пикселя.

Таким образом, мы осваиваем базовые приёмы работы с изображениями как с матрицами, что является фундаментом для более сложных алгоритмов компьютерного зрения и обработки графики, код написан с использованием пакетов Images, FileIO и ImageShow.

Шаг 1. Загрузка изображения и получение его характеристик

In [ ]:
original_img = load("IMG.jpg")
display(original_img)
height, width = size(original_img)
println("height: $height, width: $width")
No description has been provided for this image
height: 980, width: 1960

В этом блоке мы:

  • Загружаем изображение из файла IMG.jpg с помощью функции load из пакета FileIO. Результат сохраняется в переменную original_img. Теперь original_img — это многомерный массив (матрица), каждый элемент которого содержит цвет пикселя в формате RGB (или оттенки серого, если изображение чёрно-белое).
  • Выводим изображение на экран с помощью display. Это возможно благодаря пакету ImageShow, который обеспечивает отображение графических объектов.
  • Получаем размеры изображения: height (высота, количество строк) и width (ширина, количество столбцов). Функция size возвращает кортеж, который мы распаковываем в две переменные.
  • Печатаем полученные значения в консоль с помощью println. Это позволяет убедиться, что изображение загружено корректно, и узнать его разрешение.

Важно: для работы с цветными изображениями пакет Images автоматически представляет их в виде матрицы элементов типа RGB (или RGB4). Каждый такой элемент содержит поля r, g, b (красный, зелёный, синий) со значениями от 0 до 1. Таким образом, мы можем обращаться к отдельным пикселям, например original_img[100, 200] даст цвет пикселя в строке 100, столбце 200.

На этом этапе мы подготовили исходные данные для дальнейшей обработки.

Шаг 2. Определяем параметры полос

In [ ]:
n_stripes = min(height, width)
base_width = div(width, n_stripes)
remainder = width % n_stripes
widths = [i <= remainder ? base_width + 1 : base_width for i in 1:n_stripes]
max_width = maximum(widths)
odd_stripes = []
even_stripes = []
start_col = 1;

Здесь мы готовим всё необходимое для разделения изображения на вертикальные полосы:

  • n_stripes = min(height, width)
    Количество полос выбирается равным минимальному из размеров изображения. Это гарантирует, что полос не будет больше, чем строк или столбцов, и каждая полоса будет иметь достаточную ширину (в пикселях) для наглядности.

  • base_width = div(width, n_stripes)
    Вычисляем базовую ширину полосы как целочисленное деление общей ширины на количество полос. Так как ширина может не делиться нацело, часть полос получится на один пиксель шире.

  • remainder = width % n_stripes
    Остаток от деления — сколько полос будут иметь дополнительный пиксель.

  • widths = [i <= remainder ? base_width + 1 : base_width for i in 1:n_stripes]
    Создаём массив widths, где для каждой полосы i указана её ширина. Первые remainder полос получают ширину base_width + 1, остальные — base_width. Таким образом, все полосы имеют почти одинаковую ширину, отличаясь максимум на 1 пиксель, что позволяет равномерно покрыть всю ширину изображения.

  • max_width = maximum(widths)
    Находим максимальную ширину среди всех полос. Это потребуется позже, когда мы будем создавать изображения для хранения полос: каждая полоса будет представлена прямоугольником одинаковой высоты (равной высоте исходного изображения) и одинаковой ширины max_width, чтобы их можно было легко склеивать. Более узкие полосы будут дополнены чёрными пикселями справа.

  • odd_stripes = [] и even_stripes = []
    Инициализируем два пустых массива для хранения самих полос: в odd_stripes будем складывать полосы с нечётными номерами, в even_stripes — с чётными.

  • start_col = 1
    Переменная-счётчик, которая будет указывать на начало очередной полосы в исходном изображении при последующем проходе.

На этом этапе мы полностью подготовили данные для цикла извлечения полос: знаем, сколько их, какой они ширины, и готовы их разделять по чётности.

Шаг 3. Извлекаем полосы из исходного изображения

In [ ]:
for i in 1:n_stripes
    w = widths[i]
    end_col = start_col + w - 1
    stripe_rect = original_img[:, start_col:end_col]
    stripe_img = similar(original_img, height, max_width)
    fill!(stripe_img, RGB(0, 0, 0))
    stripe_img[:, 1:w] = stripe_rect
    if isodd(i)
        push!(odd_stripes, stripe_img)
    else
        push!(even_stripes, stripe_img)
    end
    start_col += w
end

В этом цикле мы последовательно проходим по всем полосам слева направо и выполняем следующие действия:

  • Определяем границы полосы:
    Используя текущее значение start_col (левый край) и ширину w, вычисляем end_col — правый край полосы. Так мы знаем, какой диапазон столбцов исходного изображения соответствует i-й полосе.

  • Вырезаем прямоугольник:
    stripe_rect = original_img[:, start_col:end_col] — берём все строки (:) и нужные столбцы. Получается матрица размером height × w, содержащая пиксели данной полосы.

  • Создаём стандартизированный прямоугольник:
    stripe_img = similar(original_img, height, max_width) создаёт новую матрицу (изображение) такого же типа, как исходное, но размером height × max_width. Это нужно, чтобы все полосы имели одинаковую ширину — тогда их будет легко склеивать в итоговые изображения img1 и img2.
    Затем fill!(stripe_img, RGB(0,0,0)) заполняет этот прямоугольник чёрным цветом (все компоненты равны 0).

  • Копируем пиксели полосы:
    stripe_img[:, 1:w] = stripe_rect — вставляем вырезанный фрагмент в левую часть созданного прямоугольника. Если полоса была уже, чем max_width, справа останутся чёрные пиксели (фон).

  • Сортируем по чётности:
    С помощью isodd(i) проверяем номер полосы. Нечётные полосы добавляем в массив odd_stripes, чётные — в even_stripes. Так мы разделяем полосы на две группы для последующего создания двух отдельных изображений.

  • Сдвигаем начало:
    start_col += w — увеличиваем счётчик на ширину только что обработанной полосы, чтобы в следующей итерации начать с того места, где закончилась текущая.

В результате после цикла мы имеем два массива, каждый из которых содержит набор матриц (полос) одинакового размера height × max_width, причём все пиксели из исходного изображения распределены по этим полосам в соответствии с их порядком.

Шаг 4. Формируем два изображения из полос разной чётности

In [ ]:
img1 = similar(original_img, height, max_width * length(odd_stripes))
img2 = similar(original_img, height, max_width * length(even_stripes))
fill!(img1, RGB(0, 0, 0))
fill!(img2, RGB(0, 0, 0))
col_offset = 1
for stripe in odd_stripes
    w = size(stripe, 2)
    img1[:, col_offset:col_offset+w-1] = stripe
    col_offset += w
end
col_offset = 1
for stripe in even_stripes
    w = size(stripe, 2)
    img2[:, col_offset:col_offset+w-1] = stripe
    col_offset += w
end

На этом этапе мы объединяем отдельные полосы в два цельных изображения:

  • Создание холстов:
    img1 и img2 создаются с помощью similar, что гарантирует тот же тип пикселей, что и у исходного изображения. Их высота равна высоте оригинала (height), а ширина вычисляется как max_width (максимальная ширина полосы), умноженная на количество полос соответствующей группы.
    Например, если у нас 10 полос, то в odd_stripes будет 5 полос (нечётные), значит ширина img1 = max_width * 5.
    Затем оба изображения заполняются чёрным цветом (RGB(0,0,0)) — это будет фон, на который мы будем накладывать пиксели полос.

  • Сборка изображения из нечётных полос:
    Переменная col_offset отслеживает текущую позицию (столбец) в итоговом изображении, начиная с 1. В цикле по всем полосам из odd_stripes мы:

    • определяем фактическую ширину текущей полосы w (она равна max_width, так как все полосы были дополнены до этого размера);
    • вставляем матрицу полосы stripe в соответствующий диапазон столбцов img1[:, col_offset:col_offset+w-1];
    • увеличиваем col_offset на w, переходя к месту для следующей полосы.
  • Сборка изображения из чётных полос:
    Аналогичный цикл выполняется для even_stripes, формируя img2.

В результате мы получаем два изображения, каждое из которых представляет собой склеенные по горизонтали полосы исходной картинки:

  • img1 содержит полосы 1, 3, 5, … (нечётные);
  • img2 содержит полосы 2, 4, 6, … (чётные).

Обратите внимание: поскольку все полосы были приведены к единой ширине max_width, в img1 и img2 между фрагментами нет промежутков — они расположены вплотную. Чёрные области справа от более узких полос (если исходная ширина полосы была меньше max_width) сохраняются, поэтому на стыках могут быть вертикальные чёрные полоски — это артефакт выравнивания, но он не мешает дальнейшему сравнению.

Шаг 5. Объединяем два изображения для сравнения и выводим результат

In [ ]:
h1, w1 = size(img1)
h2, w2 = size(img2)
max_h = max(h1, h2)
side_by_side = similar(img1, max_h, w1 + w2 + 10)
fill!(side_by_side, RGB(1, 1, 1))
side_by_side[1:h1, 1:w1] = img1
side_by_side[1:h2, w1+11:w1+w2+10] = img2
display(side_by_side)
println("height_img1: $h1, height_img2: $h2")
println("width_img1: $w1, width_img2: $w2")
No description has been provided for this image
height_img1: 980, height_img2: 980
width_img1: 980, width_img2: 980

На этом завершающем этапе мы:

  • Получаем размеры сформированных изображений:
    h1, w1 = size(img1) и h2, w2 = size(img2). Оба изображения имеют одинаковую высоту (height исходной картинки), но ширина может различаться, если количество нечётных и чётных полос отличается (например, при нечётном общем количестве полос). В нашем примере оба оказались по 980 пикселей, потому что исходная ширина 1960, количество полос 980, и нечётных/чётных поровну.

  • Создаём общий холст side_by_side:
    max_h = max(h1, h2) — выбираем максимальную высоту (они равны, но код универсален).
    side_by_side = similar(img1, max_h, w1 + w2 + 10) — создаём изображение-холст такого же типа, как img1, высотой max_h и шириной, равной сумме ширин двух изображений плюс 10 пикселей промежутка.
    fill!(side_by_side, RGB(1,1,1)) — заливаем холст белым цветом (все компоненты равны 1). Белый фон создаёт визуальный разделитель между двумя картинками.

  • Размещаем изображения на холсте:
    side_by_side[1:h1, 1:w1] = img1 — копируем img1 в левую часть холста (строки 1:h1, столбцы 1:w1).
    side_by_side[1:h2, w1+11:w1+w2+10] = img2 — вставляем img2 справа, начиная с позиции w1+11 (оставляем 10 пикселей белого промежутка после img1, плюс один пиксель индексации — поэтому +11). Диапазон столбцов захватывает ровно w2 столбцов.

  • Отображаем результат:
    display(side_by_side) показывает итоговое составное изображение, где слева — полосы с нечётными номерами, справа — с чётными. Белая полоса между ними позволяет легко визуально разделить картинки.

  • Выводим размеры в консоль:
    println сообщает нам точные размеры полученных изображений. Это полезно для проверки и понимания того, как исходное изображение было преобразовано.

Вывод

Мы прошли полный путь от загрузки изображения до визуализации результата его необычной «разрезки». Этот пример наглядно показывает, как работать с изображениями воспренимая их как обычные матрицы: извлекать фрагменты, создавать новые массивы, заполнять их пикселями и склеивать. Такой подход лежит в основе многих задач компьютерного зрения и обработки графики — от простого кропа до сложных фильтров и преобразований.

Интересный факт: Несмотря на то, что мы разделили исходное изображение на две группы полос (нечётные и чётные), визуально новые изображения выглядят идентично, это связанно с исходной симметрией изображения.