Точное отображение пикселей, масштабированных с помощью Graphics.DrawImage - PullRequest
3 голосов
/ 01 марта 2011

У меня есть приложение Winforms, которое использует Graphics.DrawImage для растягивания и рисования растрового изображения, и мне нужна помощь, чтобы точно понять, как исходные пиксели отображаются на место назначения.

В идеале я хочу написать такую ​​функцию, как:

 Point MapPixel(Point p, Size src, Size dst)

, который берет координату пикселя на исходном изображении и возвращает координату «левого верхнего» пикселя, соответствующего ему в масштабированном пункте назначения.

Для ясности, вот тривиальный пример, где растровое изображение 2x2 масштабируется до 4x4:

Zoom example

Стрелка показывает, как вводится точка (1,0) в MapPixel:

MapPixel(new Point(1, 0), new Size(2, 2), new Size(4, 4))

должен дать результат (2,0).

Сделать так, чтобы MapPixel работал для приведенного выше примера, просто с помощью логики:

double scaleX = (double)dst.Width / (double)src.Width;
x_dst = (int)Math.Round((double)x_src * scaleX);

Однако я заметил, что эта наивная реализация выдает ошибки из-за округления, когда dst.Width не является кратным src.Width. В этом случае DrawImage нужно выбрать некоторые пиксели, чтобы рисовать больше, чем другие, чтобы привести изображение в соответствие, и у меня возникают проблемы с дублированием его логики.

Следующий код демонстрирует проблему, масштабируя растровое изображение 2x1 до нескольких значений ширины:

Bitmap src = new Bitmap(2, 1);
src.SetPixel(0, 0, Color.Red);
src.SetPixel(1, 0, Color.Blue);

Bitmap[] dst = {
  new Bitmap(3, 1),
  new Bitmap(5, 1),
  new Bitmap(7, 1),
  new Bitmap(9, 1)};

// Draw stretched images
foreach (Bitmap b in dst) {
  using (Graphics g = Graphics.FromImage(b)) {
    g.InterpolationMode = InterpolationMode.NearestNeighbor;
    g.PixelOffsetMode = PixelOffsetMode.Half;
    g.DrawImage(src, 0, 0, b.Width, b.Height);
  }
}

Вот как выглядят исходное src изображение и выходные dst изображения, а также некоторые цифры, показывающие, как MapPixel необходимо отобразить синий пиксель:

Scaling examples

Не могу понять, как DrawImage решает, какой пиксель увеличить. Кажется, иногда округляется, а иногда - вниз. Мне все равно, какой он выберет, но мне нужно, чтобы он был предсказуемым, чтобы моя функция работала правильно.

Я попытался изменить мой пример MapPixel выше, чтобы использовать MidpointRounding.AwayFromZero, и даже заменил Math.Round на функцию, которая округляет до ближайшего нечетного числа (немного улучшает результаты, но все еще не идеально). Я также попытался позволить классу Graphics обрабатывать масштабирование - то есть я установил ScaleTransform, вызвал DrawImageUnscaled, а затем попытался использовать TransformPoints для преобразования координат. Интересно, что результаты метода TransformPoints не всегда согласуются с тем, что делают DrawImage и DrawImageUnscaled.

Я также пытался найти подсказки в GDI +, но пока не нашел ничего полезного.

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

Если вам интересно, причина, по которой я использую InterpolationMode.NearestNeighbor, состоит в том, чтобы избежать сглаживания (мне нужно поддерживать точность отдельных пикселей), а PixelOffsetMode.Half включено, потому что если его там нет, то DrawImage панорамирует мое растровое изображение на полпикселя.

Еще пара примеров проблемных точек: x = 7 при масштабировании от 4 до 13 пикселей и x = 8 при масштабировании от 4 до 17 пикселей.

При желании я могу опубликовать полный код модульного теста, который позволит вам зайти и проверить функцию MapPixel. Пока что единственный способ достичь 100% точности - это уродливый хак, который генерирует исходное изображение «подсказка», в котором каждому пикселю присваивается уникальный цвет. Он отображает координаты, проверяя, какого цвета это изображение подсказки, а затем ищет этот цвет в уменьшенной версии изображения подсказки. Оптимизация возможна (например, изображения подсказок имеют ширину или высоту в один пиксель, а наивная логика выше используется, чтобы угадать приблизительный ответ и оттуда работать наружу), но все равно это уродливо.

Буду признателен, если кто-нибудь сможет пролить свет на сантехнику позади DrawImage и помочь мне придумать более простую (но все же точную) реализацию для MapPixel.

1 Ответ

0 голосов
/ 30 марта 2011

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

Я сделаю это по одной оси (обозначено N ниже)

Исходная позиция может рассматриваться как диапазон [0, origN] целевая (масштабированная) позиция может рассматриваться как диапазон [0, destN]

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

 origPos                    destPos
---------  for orig, and,  --------- for dest
  origN                      destN

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

for current_dest_position in range(destLength):
    required_source_position=floor( (current_dest_position*sourceN)/destN )

srcN и destN всегда на единицу меньше общей ширины (это имеет отношение к позиции 0, являющейся действительным пикселем), поэтому длина источника равна, скажем, 16, а длина dest равна 64, тогда srcN равна 15, а destN равно 63 (диапазон (k) оператор в вышеупомянутом приводит к итерации по [0, k-1]). Требуется слово, если ваш язык не обеспечивает простой способ принудительного целочисленного деления, как в большинстве языков, использующих значения типа «утка» (javascript, php, lua, python и т. Д.) В C / C ++, вы можете просто привести приведение к int с:

required_source_position=(int)((current_dest_position*sourceN)/destN);

Это объясняет процесс в одной оси. Его легко использовать для других осей с вложенными циклами для других осей и замены N в приведенных выше примерах осью (X, Y, Z и т. Д.).

...