ref: ecbe4e5758f634cceae3763d57f60968d7928476
parent: 7b17aa1d5adb783d52bd999697538442f8b7a4c3
author: rodri <rgl@antares-labs.eu>
date: Sat Jan 17 13:51:43 EST 2026
image/histogram: display histograms in a single window and let the user select a region i also fixed the warp transformations. i wrote the first version in an outdated drawterm running an old version of devdraw where memaffinewarp(2) used the dst->clipr as the origin for the blitting. now it uses dst->r, and the screen is a backed-store "view" of the actual screenimage so internally, in devdraw, it refers to a Memimage with its r equal to that of the entire display, and its r.min always equal to ZP.
--- a/sys/man/1/image
+++ b/sys/man/1/image
@@ -114,8 +114,8 @@
reads an image from a given
.I file
or stdin, opens a minimal visualizer in the current window, and
-generates a histogram for each of its channels in multiple,
-independent windows.
+generates a histogram for each of its channels in an independent
+window.
.br
When in the visualizer, use LMB to move the image around, scroll up/down or
MMB to control zoom and RMB to open a menu with some extra options.
--- a/sys/src/cmd/image/histogram.c
+++ b/sys/src/cmd/image/histogram.c
@@ -11,6 +11,8 @@
enum {Hmargin = 10,
Vmargin = 15,
+
+ DOrange = 0xFFA500FF,
};
typedef struct Sampler Sampler;
@@ -29,11 +31,10 @@
{char *title;
Image *img;
- Screen *scr;
- Image *win;
uvlong vals[256];
+ uvlong avg;
+ uvlong max;
ulong col;
- int pid;
};
struct Slider
@@ -40,19 +41,24 @@
{Point2 p0;
Point2 p1;
- int left;
- int min;
- int max;
- int val;
+ Point2 kp; /* knob position */
+ int left; /* or right */
+ int min; /* value at p0 */
+ int max; /* value at p1 */
+ int val; /* current value */
};
Histogram histos[4] = {- { .title = "red", .col = 0xCC0000FF },- { .title = "green", .col = 0x00BB00FF },- { .title = "blue", .col = 0x0000CCFF }, { .title = "alpha", .col = DBlack },+ { .title = "blue", .col = 0x0000CCFF },+ { .title = "green", .col = 0x00BB00FF },+ { .title = "red", .col = 0xCC0000FF },};
+Screen *histscr;
+Image *histwin;
+Point histspan;
+Channel *histrefc;
Memimage *mimage;
Image *image;
char title[64];
@@ -60,6 +66,8 @@
Matrix warpmat;
Warp warp;
int smoothen;
+Rectangle region;
+Image *regioncol;
int
sgn(double n)
@@ -251,8 +259,9 @@
}
void
-measureimage(Memimage *i)
+measureimage(Memimage *i, Rectangle sr)
{+ static int inited;
Histogram *h;
Image *brush;
Rectangle hr, dr, gr, r;
@@ -259,19 +268,34 @@
Sampler s;
Point sp;
ulong c;
- int chan, ht, j;
- uvlong vmax;
+ int ht, j;
+ if(inited)
+ for(h = histos; h < histos+nelem(histos); h++){+ memset(h->vals, 0, sizeof(h->vals));
+ h->max = h->avg = 0;
+ }
+
initsampler(&s, i);
- for(sp.y = i->r.min.y; sp.y < i->r.max.y; sp.y++)
- for(sp.x = i->r.min.x; sp.x < i->r.max.x; sp.x++){+ for(sp.y = sr.min.y; sp.y < sr.max.y; sp.y++)
+ for(sp.x = sr.min.x; sp.x < sr.max.x; sp.x++){c = getpixel(&s, sp);
if(i->flags & Falpha)
c = clralpha(c);
- for(chan = 4-1; chan >= 0; chan--)
- histos[4-1-chan].vals[(c >> chan*8)&0xFF]++;
+ for(h = histos; h < histos+nelem(histos); h++){+ if(++h->vals[c&0xFF] > h->max)
+ h->max = h->vals[c&0xFF];
+ h->avg += c&0xFF;
+ c >>= 8;
+ }
}
+ for(h = histos; h < histos+nelem(histos); h++){+ if(h->max == 0)
+ h->max = 1;
+ h->avg /= Dx(sr)*Dy(sr);
+ }
+
brush = eallocimage(display, Rect(0,0,1,1), RGBA32, 1, DNofill);
hr = Rect(0, 0, Hmargin+256+Hmargin, 4+font->height+Vmargin+150+3+8+1+Vmargin);
dr = Rect(Hmargin, 4+font->height+Vmargin, hr.max.x-Hmargin, hr.max.y-Vmargin-1-8-3);
@@ -279,30 +303,42 @@
ht = Dy(dr);
for(h = histos; h < histos+nelem(histos); h++){- if(loadimage(brush, brush->r, (uchar*)&h->col, 4) < 0)
- sysfatal("loadimage2: %r");+ if(inited)
+ draw(h->img, dr, display->white, nil, ZP);
+ else{+ h->img = eallocimage(display, hr, screen->chan, 0, DWhite);
+ string(h->img, addpt(hr.min, Pt(4, 4)), display->black, ZP, font, h->title);
+ border(h->img, dr, -1, display->black, ZP);
+ }
- h->img = eallocimage(display, hr, screen->chan, 0, DWhite);
- string(h->img, addpt(hr.min, Pt(4, 4)), display->black, ZP, font, h->title);
- border(h->img, dr, -1, display->black, ZP);
-
- for(vmax = j = 0; j < 256; j++)
- if(h->vals[j] > vmax)
- vmax = h->vals[j];
-
+ /* draw the columns */
+ if(fillcolor(brush, h->col) < 0)
+ sysfatal("fillcolor: %r");r = dr;
r.max.x = r.min.x + 1;
for(j = 0; j < 256; j++){r.min.y = r.max.y;
- r.min.y -= h->vals[j]*ht / vmax;
+ r.min.y -= h->vals[j]*ht / h->max;
draw(h->img, r, brush, nil, ZP);
r.min.x = r.max.x++;
}
- border(h->img, gr, -1, display->black, ZP);
- drawgradient(h->img, gr, DBlack, DWhite);
+ /* draw the avg line */
+ r = dr;
+ r.min.x += h->avg;
+ r.max.x = r.min.x + 1;
+ if(fillcolor(brush, setalpha(DOrange, 0xBF)) < 0)
+ sysfatal("fillcolor: %r");+ draw(h->img, r, brush, nil, ZP);
+
+ if(!inited){+ border(h->img, gr, -1, display->black, ZP);
+ drawgradient(h->img, gr, DBlack, DWhite);
+ }
}
freeimage(brush);
+ if(!inited)
+ inited++;
}
int
@@ -356,37 +392,43 @@
}
void
-histredraw(Histogram *h)
+histredraw(void)
{- draw(h->win, h->win->r, h->img, nil, ZP);
+ Histogram *h;
+ Point off;
+
+ off = histwin->r.min;
+ off.y += histspan.y;
+ for(h = histos; h < histos+nelem(histos); h++){+ off.y -= Dy(h->img->r);
+ draw(histwin, rectaddpt(h->img->r, off), h->img, nil, ZP);
+ }
flushimage(display, 1);
}
void
-histresize(Histogram *h)
+histresize(void)
{lockdisplay(display);
- if(gengetwindow(display, winname, &h->win, &h->scr, Refnone) < 0)
+ if(gengetwindow(display, winname, &histwin, &histscr, Refnone) < 0)
sysfatal("gengetwindow2: %r");unlockdisplay(display);
- if((Dx(h->win->r) != Dx(h->img->r)
- || Dy(h->win->r) != Dy(h->img->r)))
- winresize(display, subpt(h->img->r.max, h->img->r.min));
- histredraw(h);
+ if(Dx(histwin->r) != histspan.x
+ || Dy(histwin->r) != histspan.y)
+ winresize(display, histspan);
+ histredraw();
}
void
-histmouse(Histogram *h, Mousectl *mc)
+histmouse(Mousectl *mc)
{- static Mouse omtab[nelem(histos)];
- Mouse *om;
+ static Mouse om;
- om = &omtab[h-histos];
- if((om->buttons & 4) && (mc->buttons & 4)){- line(h->win, om->xy, mc->xy, Enddisc, Enddisc, 1, display->black, ZP);
+ if((om.buttons & 4) && (mc->buttons & 4)){+ line(histwin, om.xy, mc->xy, Enddisc, Enddisc, 1, display->black, ZP);
flushimage(display, 1);
}
- *om = mc->Mouse;
+ om = mc->Mouse;
}
void
@@ -400,39 +442,37 @@
}
void
-histproc(void *arg)
+histproc(void *)
{- Histogram *histo;
Mousectl *mc;
Keyboardctl *kc;
Rune r;
- histo = arg;
- histo->pid = getpid();
+ threadsetname("histograms");- threadsetname("%s", histo->title);-
lockdisplay(display); /* avoid races while attaching to new window */
newwindow(nil);
- winsetlabel(display, "%s", histo->title);
- winresize(display, subpt(histo->img->r.max, histo->img->r.min));
- winmove(display, Pt(0, (Dy(histo->img->r)+2*Borderwidth)*(histo-histos)));
+ winsetlabel(display, "histograms");
+ histspan = Pt(Dx(histos[0].img->r), Dy(histos[0].img->r)*nelem(histos));
+ winresize(display, histspan);
+ winmove(display, ZP);
- if(gengetwindow(display, winname, &histo->win, &histo->scr, Refnone) < 0)
+ if(gengetwindow(display, winname, &histwin, &histscr, Refnone) < 0)
sysfatal("gengetwindow: %r");unlockdisplay(display);
- if((mc = initmouse(nil, histo->win)) == nil)
+ if((mc = initmouse(nil, histwin)) == nil)
sysfatal("initmouse2: %r");if((kc = initkeyboard(nil)) == nil)
sysfatal("initkeyboard2: %r");- histredraw(histo);
+ histredraw();
- enum { MOUSE, RESIZE, KEY };+ enum { MOUSE, RESIZE, KEY, REFRESH }; Alt a[] = { {mc->c, &mc->Mouse, CHANRCV}, {mc->resizec, nil, CHANRCV}, {kc->c, &r, CHANRCV},+ {histrefc, nil, CHANRCV}, {nil, nil, CHANEND}};
for(;;)
@@ -439,17 +479,51 @@
switch(alt(a)){ default: sysfatal("alt interrupted");case MOUSE:
- histmouse(histo, mc);
+ histmouse(mc);
break;
case RESIZE:
- histresize(histo);
+ histresize();
break;
case KEY:
histkey(r);
break;
+ case REFRESH:
+ histredraw();
+ break;
}
}
+Rectangle
+xformrect(Rectangle r, Matrix m)
+{+ Point2 p0, p1;
+
+ p0 = (Point2){r.min.x, r.min.y, 1};+ p1 = (Point2){r.max.x, r.max.y, 1};+ p0 = xform(p0, m);
+ p1 = xform(p1, m);
+ return Rect(p0.x+0.5, p0.y+0.5, p1.x+0.5, p1.y+0.5);
+}
+
+int
+getregion(Mousectl *mc)
+{+ Matrix invwarpmat;
+
+ region = getrect(3, mc);
+ if(Dx(region)*Dy(region) <= 4){+badregion:
+ region = ZR;
+ return 0;
+ }
+ memmove(invwarpmat, warpmat, sizeof(Matrix));
+ invm(invwarpmat);
+ region = xformrect(region, invwarpmat);
+ if(!rectclip(®ion, rectsubpt(image->r, image->r.min)))
+ goto badregion;
+ return 1;
+}
+
void
redraw(void)
{@@ -457,6 +531,8 @@
draw(screen, screen->r, display->black, nil, ZP);
affinewarp(screen, screen->r, image, image->r.min, warp, smoothen);
+ if(!eqrect(region, ZR))
+ border(screen, xformrect(region, warpmat), -1, regioncol, ZP);
stringbg(screen, addpt(screen->r.min, titlep), display->white, ZP, font, title, display->black, ZP);
flushimage(display, 1);
}
@@ -464,10 +540,16 @@
void
resize(void)
{+ Point dp;
+
+ dp = screen->r.min;
lockdisplay(display);
if(getwindow(display, Refnone) < 0)
fprint(2, "can't reattach to window\n");
unlockdisplay(display);
+ dp = subpt(screen->r.min, dp);
+ translate(warpmat, dp.x, dp.y);
+ mkwarp(warp, warpmat);
redraw();
}
@@ -474,9 +556,11 @@
static char *
genrmbmenu(int idx)
{- if(idx > 0)
- return nil;
- return smoothen? "sharpen": "smoothen";
+ switch(idx){+ case 0: return smoothen? "sharpen": "smoothen";
+ case 1: return "select region";
+ default: return nil;
+ }
}
void
@@ -483,11 +567,23 @@
rmb(Mousectl *mc)
{ static Menu menu = { .gen = genrmbmenu };+ static int therewas; /* a region selected */
switch(menuhit(3, mc, &menu, _screen)){case 0:
smoothen ^= 1;
break;
+ case 1:
+ if(getregion(mc)){+ measureimage(mimage, rectaddpt(region, mimage->r.min));
+ therewas = 1;
+ nbsend(histrefc, nil);
+ }else if(therewas){+ measureimage(mimage, mimage->r);
+ therewas = 0;
+ nbsend(histrefc, nil);
+ }
+ break;
}
redraw();
}
@@ -510,7 +606,7 @@
tainted++;
}else if(mc->buttons & 2){if((om.buttons & 2) == 0)
- p = subpt(mc->xy, screen->r.min);
+ p = mc->xy;
switch(sgn(mc->xy.y - om.xy.y)){case 1: goto zoomout;
case -1: goto zoomin;
@@ -518,7 +614,7 @@
}else if((om.buttons & 4) == 0 && (mc->buttons & 4))
rmb(mc);
if(mc->buttons & 8){- p = subpt(mc->xy, screen->r.min);
+ p = mc->xy;
zoomin:
translate(warpmat, -p.x, -p.y);
scale(warpmat, Scrollzoomin);
@@ -525,7 +621,7 @@
translate(warpmat, p.x, p.y);
tainted++;
}else if(mc->buttons & 16){- p = subpt(mc->xy, screen->r.min);
+ p = mc->xy;
zoomout:
translate(warpmat, -p.x, -p.y);
scale(warpmat, Scrollzoomout);
@@ -563,7 +659,7 @@
Keyboardctl *kc;
Rune r;
char cs[10];
- int fd, i;
+ int fd;
fd = 0;
ARGBEGIN{@@ -588,15 +684,17 @@
snprint(title, sizeof title, "%s %dx%d %s",
chantostr(cs, image->chan)? cs: "unknown", Dx(image->r), Dy(image->r),
argc > 0? argv[0]: "main");
+ regioncol = eallocimage(display, Rect(0,0,1,1), XRGB32, 1, DOrange);
identity(warpmat);
+ translate(warpmat, screen->r.min.x, screen->r.min.y);
mkwarp(warp, warpmat);
redraw();
- measureimage(mimage);
- unlockdisplay(display);
+ measureimage(mimage, mimage->r);
snprint(winname, sizeof winname, "%s/winname", display->windir);
- for(i = 0; i < nelem(histos); i++)
- proccreate(histproc, &histos[i], mainstacksize);
+ histrefc = chancreate(sizeof(void*), 1);
+ unlockdisplay(display);
+ proccreate(histproc, nil, mainstacksize);
enum { MOUSE, RESIZE, KEY }; Alt a[] = {--
⑨