Точный алгоритм рисования субпиксельных линий (алгоритм растеризации)

Мне нужен алгоритм, который может быть (немного) медленнее, чем алгоритм рисования линий Брезенхема, но имеет если быть более точным. Под «точным» я имею в виду: каждый пиксель, которого коснулись, должен быть напечатан. Не больше, но и не меньше! Это означает, что использование более толстой линии или чего-то подобного невозможно, так как будет задействовано слишком много пикселей. Также мне не нужна графическая структура или что-то подобное, как это было спросил раньше, мне нужен алгоритм! Приложение действительно не в «графике», а в географическая область, где пиксели являются" плитками ".

Основная проблема для меня заключается в том, что мне нужна субпиксельная точность, что означает, что линия может начинаться с 0,75 / 0,33, а не только с 0/0, как в случае с целочисленными значениями. Я пытался создать рабочее решение последние несколько часов, но не могу заставить его работать - слишком много крайних случаев.

Сначала я подумал, что версия с антиалиасингом, такая как алгоритм из Wu, должна работать, но она печатает слишком много пикселей (особенно для начальной и конечной точек) и в некоторых случаях все еще пропускает некоторые пиксели, например для очень коротких строк.

Затем я попытался заставить работать Брезенхэма, заменив второе «если» на «иначе, если», как указано здесь, и он ближе, но все же не там. Затем я попытался переместить Bresenham с целочисленной точности на точность с плавающей запятой, что привело к бесконечному циклу (поскольку значения x, y перепрыгивали через условие завершения if (y1 == y2 && x1 == x2)).

Я мог бы использовать решение наивного рисования линий, но какое delta мне следует использовать? Например. если я использую 0.1, я все равно пропущу некоторые пиксели, а использование меньших значений, вероятно, займет слишком много времени (и все равно пропустить пиксели).

Было бы признательно за рабочее решение на C / Java / .... По крайней мере, он должен работать для октанта 1, но полноценное решение было бы еще лучше.

Обновление. Мне пришла в голову следующая идея: используя простую растеризацию линии, вы можете рассчитать 4 пикселя-кандидата для каждой точки. Затем проверьте эти 4 пикселя, действительно ли линия их пересекает. Но я не уверен, может ли пересечение линии / прямоугольника быть достаточно быстрым.


person Karussell    schedule 10.07.2014    source источник
comment
должен быть напечатан каждый затронутый пиксель, даже если 0,01 пикселя или менее пересекается с линией? Какую форму имеют оба конца линии (округлые, вогнутые, выпуклые, плоские)?   -  person Tarik    schedule 10.07.2014
comment
да, если есть математическое пересечение, его следует включить (конечно, мы можем предположить типичную ошибку округления). Концы линии должны быть плоскими (без ворса или сглаживания, только «математический» конец).   -  person Karussell    schedule 10.07.2014
comment
как насчет цвета? является ли цвет линии постоянным или должен быть интерполирован в соответствии с используемой областью пикселя, как это делает сглаживание?   -  person Spektre    schedule 10.07.2014
comment
Цвет не нужен. Это действительно та же проблема, которую указал MBo в своем ответе (практическая реализация): пространственное подразделение, поэтому мне нужно только знать, попадает ли «луч» в пиксель или нет.   -  person Karussell    schedule 11.07.2014
comment
Ха, я вижу, что мой ответ здесь, вероятно, лучше подходит для этого вопроса, чем этот. Я считаю, что мое решение - это именно то, о чем спрашивает этот вопрос.   -  person FA-18_1    schedule 13.03.2021
comment
Похоже на то. Спасибо! Может быть, вы добавите его сюда (я проголосую и позже изучу его подробно) и ссылку из другого ответа здесь?   -  person Karussell    schedule 14.03.2021


Ответы (2)


Если вам нужен только постоянный цвет (не интерполированный по используемой области пикселя), используйте DDA:

void line_DDA_subpixel(int x0,int y0,int x1,int y1,int col) // DDA subpixel -> thick
    {
    int kx,ky,c,i,xx,yy,dx,dy;
    x1-=x0; kx=0; if (x1>0) kx=+1; if (x1<0) { kx=-1; x1=-x1; } x1++;
    y1-=y0; ky=0; if (y1>0) ky=+1; if (y1<0) { ky=-1; y1=-y1; } y1++;
    if (x1>=y1)
     for (c=x1,i=0;i<x1;i++,x0+=kx)
        {
        pnt(x0,y0,col); // this is normal pixel the two below are subpixels 
        c-=y1; if (c<=0) { if (i!=x1-1) pnt(x0+kx,y0,col); c+=x1; y0+=ky; if (i!=x1-1) pnt(x0,y0,col); }
        }
    else
     for (c=y1,i=0;i<y1;i++,y0+=ky)
        {
        pnt(x0,y0,col); // this is normal pixel the two below are subpixels 
        c-=x1; if (c<=0) { if (i!=y1-1) pnt(x0,y0+ky,col); c+=y1; x0+=kx; if (i!=y1-1) pnt(x0,y0,col); }
        }
    }

куда:

void pnt(int x,int y,int col);

это процедура, которая растрирует пиксель (x,y) с помощью color col. Источник находится на C ++

Я думаю, что это прямолинейно, но в любом случае

DDA используйте параметрическое линейное уравнение y=k*x+q или x=ky+q в зависимости от разницы (если разница больше x или y, значит нет отверстий). k равно dy/dx или dx/dy, и все деление сводится к вычитанию + сложению внутри цикла (последняя строка каждого цикла). Его можно легко изменить на любое количество измерений (я обычно использую с ним 7D или больше). На современных машинах скорость иногда лучше, чем у Брезенхэма (зависит от платформы и использования).

Вот как это выглядит по сравнению с простым DDA

Линии DDA и DDA_subpixel

[edit2] двойные координаты // изначально [edit1]

Хорошо, вот новый код:

void line_DDA_subpixel1(double x0,double y0,double x1,double y1,int col)    // DDA subpixel -> thick
    {
    int i,n,x,y,xx,yy;
    // prepare data n-pixels,x1,y1 is line dx,dy step per pixel
    x1-=x0; i=ceil(fabs(x1));
    y1-=y0; n=ceil(fabs(y1));
    if (n<i) n=i; if (!n) n=1;
    x1/=double(n);
    y1/=double(n); n++;
    // rasterize DDA line
    for (xx=x0,yy=y0,i=0;i<=n;i++,x0+=x1,y0+=y1)
        {
        // direct pixel
        pnt(x,y,col);
        // subpixels on change in both axises
        x=x0; y=y0;
        if ((i<n)&&(x!=xx)&&(y!=yy)) { pnt(xx,y,col); pnt(x,yy,col); }
        xx=x; yy=y;
        }
    }

А вот как это выглядит:

Двойные координаты линий DDA и DDA_subpixel

Угол теперь должен иметь точность double, но pnt (x, y, col) по-прежнему находится в целых числах !!!

[edit3] пересечение пиксельной сетки

void DDAf_line_subpixel(float x0,float y0,float x1,float y1,int col)    // DDA subpixel -> thick
    {
    int i,n; float a,a0,a1,aa,b,d;
    // end-points
    pnt(x0,y0,col);
    pnt(x1,y1,col);
    // x-axis pixel cross
    a0=1; a1=0; n=0;
    if (x0<x1) { a0=ceil(x0); a1=floor(x1); d=(y1-y0)/(x1-x0); a=a0; b=y0+(a0-x0)*d; n=fabs(a1-a0); } else
    if (x0>x1) { a0=ceil(x1); a1=floor(x0); d=(y1-y0)/(x1-x0); a=a0; b=y1+(a0-x1)*d; n=fabs(a1-a0); }
    if (a0<=a1) for (aa=a,i=0;i<=n;i++,aa=a,a++,b+=d) { pnt(aa,b,col); pnt( a,b,col); }
    // y-axis pixel cross
    a0=1; a1=0; n=0;
    if (y0<y1) { a0=ceil(y0); a1=floor(y1); d=(x1-x0)/(y1-y0); a=a0; b=x0+(a0-y0)*d; n=fabs(a1-a0); } else
    if (y0>y1) { a0=ceil(y1); a1=floor(y0); d=(x1-x0)/(y1-y0); a=a0; b=x1+(a0-y1)*d; n=fabs(a1-a0); }
    if (a0<=a1) for (aa=a,i=0;i<=n;i++,aa=a,a++,b+=d) { pnt(b,aa,col); pnt(b, a,col); }
    }

Наконец, у меня было время для этого, поэтому я немного подправил DDA, но id привело к множеству if, поэтому я немного изменил растеризацию. Теперь вычисляются все пересечения (пересечения) пиксельной сетки, а затем для каждого правого субпикселя добавляется. Вот как это выглядит (без неправильных субпикселей):

субпиксель пересечения пикселя линии

Для каждой x или y линий сетки вычисляется первая точка пересечения (a,b), а step находится на одной оси 1 пикселей, а на второй - остальные в соответствии с dy/dx или dx/dy. После этого цикл for заполняет субпиксели ...

person Spektre    schedule 10.07.2014
comment
если требуется процент площади, его можно вывести из состояния переменной c, но я использовал его новее, потому что он мне не нужен. - person Spektre; 10.07.2014
comment
Это хороший алгоритм для конкретного случая того, что ищет OP, но AFAICT не касается чрезвычайно вероятных случаев, когда координаты начала и конца строки имеют нецелочисленные значения. - person Kaganar; 10.07.2014
comment
Будет ли это работать, если я заменю аргументы int, например, двойной? - person Karussell; 10.07.2014
comment
@Karussell да, но тогда будет быстрее сделать dy / dx или dx / dy классическим способом. а также затем вы должны добавить точку субпикселя через if ((x-floor (x)) ›0.0) ... и то же самое касается y - person Spektre; 11.07.2014
comment
@Karussell Я бы оставил целые числа как есть и при необходимости добавил бы эти 2 подпункта для каждой конечной точки. - person Spektre; 11.07.2014
comment
Я не могу использовать int, так как тогда линия будет иметь другой угол, чем мой исходный - person Karussell; 11.07.2014
comment
@Karussell, какая вам нужна точность угла в градусах или радианах? - person Spektre; 11.07.2014
comment
@Karussell добавил [edit1], чтобы ответить с двойными координатами и плавающей точностью. - person Spektre; 11.07.2014
comment
@Kaganar добавил [edit1], теперь он также должен охватывать это - person Spektre; 11.07.2014
comment
@Karussell добавил [edit2] с проверенными плавающими координатами (переписал мое тестовое приложение для поддержки субпиксельных координат мыши), код немного изменился, чтобы увеличить точность деления строки - person Spektre; 11.07.2014
comment
Хммм, но это также проблема, что он, например, печатает пиксель без линии. Например. некоторые из нижних / левых пикселей. - person Karussell; 11.07.2014
comment
@Karussell да, потому что он не заботится о линейном квадранте, в этом случае будет 2/4 (| dx | ‹| dy | и знак dx, dy) ifs, и вместо этого будет добавлена ​​только одна точка субпикселя, но это необходимо немного поиграться с кодом (сейчас у меня нет на это времени хотя бы до вторника) - person Spektre; 11.07.2014
comment
@Karussel (вернулся раньше, чем ожидалось :)) добавил [edit3] с новым подходом (точным) попробуйте и дайте мне знать - person Spektre; 14.07.2014

Если ваша линия тонкая, а пиксели прямоугольные (квадратные):

введите описание изображения здесь

затем рассмотрите возможность использования алгоритмов обхода сетки вокселей, например, см. статью "Fast Voxel Алгоритм обхода ... "Ву и Аманатидес.

Практическая реализация (в разделе обхода сетки)

Ответ на комментарий:
Правильная инициализация переменных X-координаты (то же самое для Y)

  DX = X2 - X1
  tDeltaX = GridCellWidth / DX
  tMaxX = tDeltaX * (1.0 - Frac(X1 / GridCellWidth)) 
  //Frac if fractional part of float, for example, Frac(1.3) = 0.3

пример в моем ответе здесь

person MBo    schedule 10.07.2014
comment
Спасибо! Выглядит неплохо - я займусь расследованием! Мои пиксели прямоугольные, но не квадратные, поэтому я думаю, что это сработает. Ваше изображение предполагает, что начальная или конечная точка может быть только в центре - возможно ли также произвольное начало или конец? - person Karussell; 10.07.2014
comment
Вы начинаете с плитки, содержащей вашу произвольную начальную позицию, и заканчиваете плиткой, содержащей вашу произвольную конечную позицию. Обход происходит по линии, которая пересекает начальную и конечную точки. Так что да, возможно произвольное начало и конец. - person Kaganar; 10.07.2014
comment
Да, возможны произвольные позиции. - person MBo; 10.07.2014
comment
Спасибо, все хорошо. Я реализовал это здесь: gist.github.com/karussell/df699108fd8fbe0d44e1 оставит ответ открытым для других идей в следующие дни - person Karussell; 11.07.2014
comment
Не могли бы вы добавить пример того, как правильно инициализировать tMaxX? У меня все еще есть эти разовые проблемы. Например. если вы перейдете от (x1 / y1) = 1/3 к 3/0, он в настоящее время дает мне 1/3, 1/2, 2/2, 2/1, 3/1, 3/0, где он должен быть 1/3, 1/2, 1/1, 2/1, 2/0, 3/0 - person Karussell; 11.07.2014
comment
Спасибо, но уже видел этот ответ, и мой код очень похож. Проблема в том, что 1/3 находится на границе 4 ячеек, и предполагается, что она уже пересекла значение x 1 и значение y 3 или что-то в этом роде. - person Karussell; 11.07.2014
comment
Алго должен выдавать последовательность 1/3, 1/2, 1/1, 2/1, 2/0, 3/0 - это правильно (обработка затронутых углов ячеек - особый случай - я пометил их синим). Кажется, что ячейка 2/2 возникла из-за проблем с реализацией. У вас начальная и конечная точки лежат в одном углу прямоугольника ячейки? - person MBo; 11.07.2014
comment
О, кажется, я понимаю - последовательность 1/3, 1/2, 2/2, 2/1, 3/1, 3/0 действительна для начальной и конечной точек в центре ячеек (как на моем рисунке), а вторая последовательность действительна для точек в углу ячейки. Выберите необходимое представление и координаты подъячейки начальной и конечной точек - person MBo; 11.07.2014
comment
Что вы имеете в виду под необходимым представлением? - person Karussell; 11.07.2014
comment
Как ваша начальная и конечная точки расположены относительно ячейки? - person MBo; 11.07.2014
comment
Я реализовал ваш java-код (ссылка на github) на python для прототипирования. И это не сработало для всех случаев (все комбинации начальной и конечной точек). Мои ячейки сосредоточены на целых числах, мои начальная и конечная точки - произвольные значения с плавающей запятой. Я изменил следующее, и это сработало: if stepX < 0: maxX = deltaX * (0.5 + (tmp - np.round(tmp))); else: maxX = deltaX * (0.5 - (tmp - np.round(tmp))); То же самое для maxY с использованием stepY. Добавлен ; как знак окончания строки в коде - person Andreas; 17.03.2016