I have been using the CImg library, and have been pleased with how easy it is to integrate and use. However, I now want to draw thick lines (i.e., more than one pixel thick). It is not clear from the API documentation of the draw_line
function (here) how this can be done. A second version of the function (just below the first in the documentation) even takes a texture as input, but again no width. It seems strange that such a comprehensive library would not have this feature. Perhaps it's supposed to be done using some kind of transformation? I know I could do it using a polygon (i.e., a rectangle where I would compute the corners of the polygon using a normal to the line), but I fear that would be significantly slower.
Asked
Active
Viewed 3,830 times
5
3 Answers
4
This function can be used to draw thick lines as polygons.
void draw_line(cimg_library::CImg<uint8_t>& image,
const int x1, const int y1,
const int x2, const int y2,
const uint8_t* const color,
const unsigned int line_width)
{
if (x1 == x2 && y1 == y2) {
return;
}
// Convert line (p1, p2) to polygon (pa, pb, pc, pd)
const double x_diff = std::abs(x1 - x2);
const double y_diff = std::abs(y1 - y2);
const double w_diff = line_width / 2.0;
// Triangle between pa and p1: x_adj^2 + y_adj^2 = w_diff^2
// Triangle between p1 and p2: x_diff^2 + y_diff^2 = length^2
// Similar triangles: y_adj / x_diff = x_adj / y_diff = w_diff / length
// -> y_adj / x_diff = w_diff / sqrt(x_diff^2 + y_diff^2)
const int x_adj = y_diff * w_diff / std::sqrt(std::pow(x_diff, 2) + std::pow(y_diff, 2));
const int y_adj = x_diff * w_diff / std::sqrt(std::pow(x_diff, 2) + std::pow(y_diff, 2));
// Points are listed in clockwise order, starting from top-left
cimg_library::CImg<int> points(4, 2);
points(0, 0) = x1 - x_adj;
points(0, 1) = y1 + y_adj;
points(1, 0) = x1 + x_adj;
points(1, 1) = y1 - y_adj;
points(2, 0) = x2 + x_adj;
points(2, 1) = y2 - y_adj;
points(3, 0) = x2 - x_adj;
points(3, 1) = y2 + y_adj;
image.draw_polygon(points, color);
}
Benchmarks with line_width
20 and 3 colors. First time is using this function, second time is drawing single 1 px wide line using image.draw_line()
.
- 1000,1000 to 2000,2000: 216 µs / 123 µs
- 2000,2000 to 8000,4000: 588 µs / 151 µs
- 3000,1000 to 3020,1000: 21 µs / 5 µs

VLL
- 9,634
- 1
- 29
- 54
-
1@MKatz I added some measurements. – VLL Jun 04 '18 at 08:53
-
Thanks! Also would be interested in seeing some pictures of how the lines look around various types of polygon/polyline angles. – M Katz Jun 05 '18 at 09:14
-
What does x_adj and y_adj do? – Paul Feb 11 '21 at 00:47
-
@Paul We are converting a line (two points) to a rectangle (four points). In case of `p1`, it has to be converted to two points `pa` and `pb`, by adding `w_diff` that is perpendicular to the line. `x_adj` and `y_adj` are the difference from the original point. I added how the values are calculated. – VLL Feb 11 '21 at 07:01
4
Apparently, it is not possible 'out-of-the-box', but creating your own routine that calls multiple times the 'draw_line()' routine of CImg, with one or two pixels shifts should give you the result you want, without much work.

Tournevissette
- 66
- 1
-
1Thanks, but I don't believe this will work smoothly for an arbitrary angled line. I will have to compute the normal to the line and offset "one pixel" in that direction. But that will be a fractional amount in general, so due to rounding and also due to aliasing in the Bresenham algorithm I don't expect that the additional lines will spoon exactly with the first line, and I'll end up with a combination of gap pixels ("holes" in the line) and overlap pixels where the line is only one pixel thick. – M Katz Apr 15 '11 at 17:23
-
Actually, after thinking about this a bit more, I think your suggestion will work for me. Since I only want the ability to draw widths up to about 4 pixels, I can be more careful in how I choose the offsets for my lines. Instead of trying to match the normal angle exactly, I can offset by a limited set of choices ( (1,1), or (1,0) or (0, -1), and so on) based on the rough angle of my line, and if I keep my line lengths the same in theory (?) the Bresenham algorithm will make the offset lines "spoon" correctly with the original line. – M Katz Apr 15 '11 at 19:30
-
Actually, it turned out not to work by drawing several lines shifted by one or two pixels. The line-drawing algorithm is such that it was possible and fairly common to get unfilled pixels and streaks inside the filled line. I ended up using the draw_polygon() function in CImg, which worked pretty well for this purpose. E.g., to draw a two-pixel-wide line from (0, 0) to (10, 10), I fill the polygon outlined by (0, 0), (10,10), (11,10), (1,0). I have not compared the speed of doing things this way vs. drawing multiple lines, but it's working well enough for my purposes. – M Katz Oct 18 '11 at 21:38
-
1Well, my method of drawing thick lines using polygons wasn't so good either. I ended up redoing it by drawing a rectangle at each point. That works well and is fast enough for my purposes. – M Katz May 08 '12 at 09:22
1
Basically this code does the same as @vll's answer, but also handles the case when (x1-x2)/(y1-y2) < 0
(I remove the abs
function).
void draw_line(cimg_library::CImg<uint8_t>& image,
const int x1, const int y1,
const int x2, const int y2,
const uint8_t* const color,
const uint8_t line_width,
const double opacity=1.0)
{
if (x1 == x2 && y1 == y2) {
return;
}
// Convert line (p1, p2) to polygon (pa, pb, pc, pd)
const double x_diff = (x1 - x2);
const double y_diff = (y1 - y2);
const double w_diff = line_width / 2.0;
// Triangle between pa and p1: x_adj^2 + y_adj^2 = w_diff^2
// Triangle between p1 and p2: x_diff^2 + y_diff^2 = length^2
// Similar triangles: y_adj / x_diff = x_adj / y_diff = w_diff / length
// -> y_adj / x_diff = w_diff / sqrt(x_diff^2 + y_diff^2)
const int x_adj = y_diff * w_diff / std::sqrt(std::pow(x_diff, 2) + std::pow(y_diff, 2));
const int y_adj = x_diff * w_diff / std::sqrt(std::pow(x_diff, 2) + std::pow(y_diff, 2));
// Points are listed in clockwise order, starting from top-left
cimg_library::CImg<int> points(4, 2);
points(0, 0) = x1 - x_adj;
points(0, 1) = y1 + y_adj;
points(1, 0) = x1 + x_adj;
points(1, 1) = y1 - y_adj;
points(2, 0) = x2 + x_adj;
points(2, 1) = y2 - y_adj;
points(3, 0) = x2 - x_adj;
points(3, 1) = y2 + y_adj;
image.draw_polygon(points, color, opacity);
}

lamductan
- 126
- 1
- 3