ref: d157450005cca96de25eb1798075ee8b919e56cb
parent: a97542bf723a772cc92534c985040115f32a2404
author: rodri <rgl@antares-labs.eu>
date: Wed Jan 14 15:50:43 EST 2026
add image/histogram
--- a/sys/man/1/image
+++ b/sys/man/1/image
@@ -1,6 +1,6 @@
.TH IMAGE 1
.SH NAME
-affinewarp, correlate - image processing tools
+affinewarp, correlate, histogram - image processing tools
.SH SYNOPSIS
.B image/affinewarp
[
@@ -29,6 +29,11 @@
[
.I denom
]
+.br
+.B image/histogram
+[
+.I file
+]
.SH DESCRIPTION
Image processing tools for
.IR image (6)
@@ -104,6 +109,16 @@
.TP
.B -p
Enable parallelism.
+.PP
+.I Histogram
+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.
+.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.
.SH SOURCE
.B /sys/src/cmd/image
.SH SEE ALSO
--- a/sys/src/cmd/image/fns.h
+++ b/sys/src/cmd/image/fns.h
@@ -4,3 +4,5 @@
Memimage *eallocmemimage(Rectangle, ulong);
Memimage *ereadmemimage(int);
int ewritememimage(int, Memimage*);
+Image *eallocimage(Display*, Rectangle, ulong, int, ulong);
+Image *memimage2image(Display*, Memimage*);
--- /dev/null
+++ b/sys/src/cmd/image/histogram.c
@@ -1,0 +1,621 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <draw.h>
+#include <memdraw.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <geometry.h>
+#include "fns.h"
+
+enum {+ Hmargin = 10,
+ Vmargin = 15,
+};
+
+typedef struct Sampler Sampler;
+typedef struct Histogram Histogram;
+typedef struct Slider Slider;
+
+struct Sampler
+{+ Memimage *i;
+ uchar *a;
+ int bpl;
+ int cmask;
+};
+
+struct Histogram
+{+ char *title;
+ Image *img;
+ Screen *scr;
+ Image *win;
+ uvlong vals[256];
+ ulong col;
+ int pid;
+};
+
+struct Slider
+{+ Point2 p0;
+ Point2 p1;
+ int left;
+ int min;
+ int max;
+ int val;
+};
+
+Histogram histos[4] = {+ { .title = "red", .col = 0xCC0000FF },+ { .title = "green", .col = 0x00BB00FF },+ { .title = "blue", .col = 0x0000CCFF },+ { .title = "alpha", .col = DBlack },+};
+
+Memimage *mimage;
+Image *image;
+char title[64];
+char winname[128];
+Matrix warpmat;
+Warp warp;
+int smoothen;
+
+int
+sgn(double n)
+{+ return n > 0? 1: (n < 0? -1: 0);
+}
+
+void
+mktranslate(Matrix m, double x, double y)
+{+ identity(m);
+ m[0][2] = x;
+ m[1][2] = y;
+}
+
+void
+mkscale(Matrix m, double s)
+{+ identity(m);
+ m[0][0] = m[1][1] = s;
+}
+
+void
+translate(Matrix m, double x, double y)
+{+ Matrix t;
+
+ memmove(t, m, sizeof(Matrix));
+ mktranslate(m, x, y);
+ mulm(m, t);
+}
+
+void
+scale(Matrix m, double s)
+{+ Matrix t;
+
+ memmove(t, m, sizeof(Matrix));
+ mkscale(m, s);
+ mulm(m, t);
+}
+
+void
+initsampler(Sampler *s, Memimage *i)
+{+ s->i = i;
+ s->a = i->data->bdata + i->zero;
+ s->bpl = sizeof(ulong)*i->width;
+ s->cmask = (1ULL << i->depth) - 1;
+}
+
+ulong
+getpixel(Sampler *s, Point pt)
+{+ uchar *p, r, g, b, a;
+ ulong val, chan, ctype, ov, v;
+ int nb, off, bpp, npack;
+
+ val = 0;
+ a = 0xFF;
+ r = g = b = 0xAA; /* garbage */
+ p = s->a + pt.y*s->bpl + (pt.x*s->i->depth >> 3);
+
+ /* pixelbits() */
+ switch(bpp = s->i->depth){+ case 1:
+ case 2:
+ case 4:
+ npack = 8/bpp;
+ off = pt.x%npack;
+ val = p[0] >> bpp*(npack-1-off);
+ val &= s->cmask;
+ break;
+ case 8:
+ val = p[0];
+ break;
+ case 16:
+ val = p[0]|(p[1]<<8);
+ break;
+ case 24:
+ val = p[0]|(p[1]<<8)|(p[2]<<16);
+ break;
+ case 32:
+ val = p[0]|(p[1]<<8)|(p[2]<<16)|(p[3]<<24);
+ break;
+ }
+
+ while(bpp < 32){+ val |= val<<bpp;
+ bpp <<= 1;
+ }
+
+ /* imgtorgba() */
+ for(chan = s->i->chan; chan; chan >>= 8){+ if((ctype = TYPE(chan)) == CIgnore){+ val >>= s->i->nbits[ctype];
+ continue;
+ }
+ nb = s->i->nbits[ctype];
+ ov = v = val & s->i->mask[ctype];
+ val >>= nb;
+
+ while(nb < 8){+ v |= v<<nb;
+ nb <<= 1;
+ }
+ v >>= nb-8;
+
+ switch(ctype){+ case CRed:
+ r = v;
+ break;
+ case CGreen:
+ g = v;
+ break;
+ case CBlue:
+ b = v;
+ break;
+ case CAlpha:
+ a = v;
+ break;
+ case CGrey:
+ r = g = b = v;
+ break;
+ case CMap:
+ p = s->i->cmap->cmap2rgb+3*ov;
+ r = p[0];
+ g = p[1];
+ b = p[2];
+ break;
+ }
+ }
+ return (r<<24)|(g<<16)|(b<<8)|a;
+}
+
+ulong
+clralpha(ulong c)
+{+ int r, g, b, a;
+
+ r = (c >> 3*8) & 0xFF;
+ g = (c >> 2*8) & 0xFF;
+ b = (c >> 1*8) & 0xFF;
+ a = (c >> 0*8) & 0xFF;
+ r = (r * 255)/a;
+ g = (g * 255)/a;
+ b = (b * 255)/a;
+ return (r<<24)|(g<<16)|(b<<8)|a;
+}
+
+/*
+ * to do this correctly you want a putpixel() that writes into b, but
+ * we only use this with RGBA32, so it doesn't matter.
+ */
+int
+fillcolor(Image *i, ulong c)
+{+ uchar b[4];
+
+ b[0] = (c >> 0*8) & 0xFF;
+ b[1] = (c >> 1*8) & 0xFF;
+ b[2] = (c >> 2*8) & 0xFF;
+ b[3] = (c >> 3*8);
+ return loadimage(i, i->r, b, (i->depth+7)/8);
+}
+
+void
+drawgradient(Image *dst, Rectangle r, ulong min, ulong max)
+{+ Image *a, *b;
+ Rectangle dr;
+ ulong dx, v;
+
+ a = eallocimage(display, Rect(0,0,1,1), RGBA32, 1, DNofill);
+ b = eallocimage(display, Rect(0,0,1,1), RGBA32, 1, DNofill);
+ dr = r;
+ dr.max.x = dr.min.x + 1;
+
+ dx = Dx(r);
+ for(; dr.min.x < r.max.x; dr.min.x = dr.max.x++){+ v = (dr.min.x-r.min.x)*0xFF/dx;
+ fillcolor(a, setalpha(min, 0xFF - v));
+ fillcolor(b, setalpha(max, v));
+ draw(dst, dr, a, nil, ZP);
+ draw(dst, dr, b, nil, ZP);
+ }
+ freeimage(a);
+ freeimage(b);
+}
+
+void
+measureimage(Memimage *i)
+{+ Histogram *h;
+ Image *brush;
+ Rectangle hr, dr, gr, r;
+ Sampler s;
+ Point sp;
+ ulong c;
+ int chan, ht, j;
+ uvlong vmax;
+
+ 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++){+ 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]++;
+ }
+
+ 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);
+ gr = Rect(dr.min.x, dr.max.y+3, dr.max.x, dr.max.y+3+8);
+ ht = Dy(dr);
+
+ for(h = histos; h < histos+nelem(histos); h++){+ if(loadimage(brush, brush->r, (uchar*)&h->col, 4) < 0)
+ sysfatal("loadimage2: %r");+
+ 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];
+
+ 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;
+ 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);
+ }
+ freeimage(brush);
+}
+
+int
+winctl(Display *d, char *fmt, ...)
+{+ char buf[128];
+ va_list a;
+ int fd, n;
+
+ n = 0;
+ snprint(buf, sizeof buf, "%s/wctl", d->windir);
+ fd = open(buf, OWRITE|OCEXEC);
+ if(fd >= 0){+ va_start(a, fmt);
+ n = vfprint(fd, fmt, a);
+ va_end(a);
+ close(fd);
+ }
+ return n;
+}
+
+int
+winmove(Display *d, Point p)
+{+ return winctl(d, "move -minx %d -miny %d", p.x, p.y);
+}
+
+int
+winresize(Display *d, Point sz)
+{+ return winctl(d, "resize -dx %d -dy %d", sz.x+2*Borderwidth, sz.y+2*Borderwidth);
+}
+
+int
+winsetlabel(Display *d, char *fmt, ...)
+{+ char buf[128];
+ va_list a;
+ int fd, n;
+
+ n = 0;
+ snprint(buf, sizeof buf, "%s/label", d->windir);
+ fd = open(buf, OWRITE|OCEXEC);
+ if(fd >= 0){+ va_start(a, fmt);
+ n = vfprint(fd, fmt, a);
+ va_end(a);
+ close(fd);
+ }
+ return n;
+}
+
+void
+histredraw(Histogram *h)
+{+ draw(h->win, h->win->r, h->img, nil, ZP);
+ flushimage(display, 1);
+}
+
+void
+histresize(Histogram *h)
+{+ lockdisplay(display);
+ if(gengetwindow(display, winname, &h->win, &h->scr, 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);
+}
+
+void
+histmouse(Histogram *h, Mousectl *mc)
+{+ static Mouse omtab[nelem(histos)];
+ 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);
+ flushimage(display, 1);
+ }
+ *om = mc->Mouse;
+}
+
+void
+histkey(Rune r)
+{+ switch(r){+ case 'q':
+ case Kdel:
+ threadexitsall(nil);
+ }
+}
+
+void
+histproc(void *arg)
+{+ Histogram *histo;
+ Mousectl *mc;
+ Keyboardctl *kc;
+ Rune r;
+
+ histo = arg;
+ histo->pid = getpid();
+
+ 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)));
+
+ if(gengetwindow(display, winname, &histo->win, &histo->scr, Refnone) < 0)
+ sysfatal("gengetwindow: %r");+ unlockdisplay(display);
+ if((mc = initmouse(nil, histo->win)) == nil)
+ sysfatal("initmouse2: %r");+ if((kc = initkeyboard(nil)) == nil)
+ sysfatal("initkeyboard2: %r");+
+ histredraw(histo);
+
+ enum { MOUSE, RESIZE, KEY };+ Alt a[] = {+ {mc->c, &mc->Mouse, CHANRCV},+ {mc->resizec, nil, CHANRCV},+ {kc->c, &r, CHANRCV},+ {nil, nil, CHANEND}+ };
+ for(;;)
+ switch(alt(a)){+ default: sysfatal("alt interrupted");+ case MOUSE:
+ histmouse(histo, mc);
+ break;
+ case RESIZE:
+ histresize(histo);
+ break;
+ case KEY:
+ histkey(r);
+ break;
+ }
+}
+
+void
+redraw(void)
+{+ static Point titlep = {10, 10};+
+ draw(screen, screen->r, display->black, nil, ZP);
+ affinewarp(screen, screen->r, image, image->r.min, warp, smoothen);
+ stringbg(screen, addpt(screen->r.min, titlep), display->white, ZP, font, title, display->black, ZP);
+ flushimage(display, 1);
+}
+
+void
+resize(void)
+{+ lockdisplay(display);
+ if(getwindow(display, Refnone) < 0)
+ fprint(2, "can't reattach to window\n");
+ unlockdisplay(display);
+ redraw();
+}
+
+static char *
+genrmbmenu(int idx)
+{+ if(idx > 0)
+ return nil;
+ return smoothen? "sharpen": "smoothen";
+}
+
+void
+rmb(Mousectl *mc)
+{+ static Menu menu = { .gen = genrmbmenu };+
+ switch(menuhit(3, mc, &menu, _screen)){+ case 0:
+ smoothen ^= 1;
+ break;
+ }
+ redraw();
+}
+
+void
+mouse(Mousectl *mc)
+{+ enum {+ ScrollzoomΔ = 0.05,
+ Scrollzoomin = 1.00+ScrollzoomΔ,
+ Scrollzoomout = 1.00-ScrollzoomΔ,
+ };
+ static Mouse om;
+ static Point p;
+ int tainted;
+
+ tainted = 0;
+ if((om.buttons & 1) && (mc->buttons & 1)){+ translate(warpmat, mc->xy.x - om.xy.x, mc->xy.y - om.xy.y);
+ tainted++;
+ }else if(mc->buttons & 2){+ if((om.buttons & 2) == 0)
+ p = subpt(mc->xy, screen->r.min);
+ switch(sgn(mc->xy.y - om.xy.y)){+ case 1: goto zoomout;
+ case -1: goto zoomin;
+ }
+ }else if((om.buttons & 4) == 0 && (mc->buttons & 4))
+ rmb(mc);
+ if(mc->buttons & 8){+ p = subpt(mc->xy, screen->r.min);
+zoomin:
+ translate(warpmat, -p.x, -p.y);
+ scale(warpmat, Scrollzoomin);
+ translate(warpmat, p.x, p.y);
+ tainted++;
+ }else if(mc->buttons & 16){+ p = subpt(mc->xy, screen->r.min);
+zoomout:
+ translate(warpmat, -p.x, -p.y);
+ scale(warpmat, Scrollzoomout);
+ translate(warpmat, p.x, p.y);
+ tainted++;
+ }
+ if(tainted){+ mkwarp(warp, warpmat);
+ redraw();
+ }
+ om = mc->Mouse;
+}
+
+void
+key(Rune r)
+{+ switch(r){+ case Kdel:
+ case 'q':
+ threadexitsall(nil);
+ }
+}
+
+void
+usage(void)
+{+ fprint(2, "usage: %s [file]\n", argv0);
+ exits("usage");+}
+
+void
+threadmain(int argc, char *argv[])
+{+ Mousectl *mc;
+ Keyboardctl *kc;
+ Rune r;
+ char cs[10];
+ int fd, i;
+
+ fd = 0;
+ ARGBEGIN{+ default: usage();
+ }ARGEND;
+ if(argc == 1){+ fd = open(argv[0], OREAD);
+ if(fd < 0)
+ sysfatal("open: %r");+ }else if(argc > 1)
+ usage();
+
+ if(initdraw(nil, nil, "histogram") < 0)
+ sysfatal("initdraw: %r");+ if((mc = initmouse(nil, screen)) == nil)
+ sysfatal("initmouse: %r");+ if((kc = initkeyboard(nil)) == nil)
+ sysfatal("initkeyboard: %r");+
+ mimage = ereadmemimage(fd);
+ image = memimage2image(display, mimage);
+ 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");
+ identity(warpmat);
+ mkwarp(warp, warpmat);
+ redraw();
+
+ measureimage(mimage);
+ unlockdisplay(display);
+ snprint(winname, sizeof winname, "%s/winname", display->windir);
+ for(i = 0; i < nelem(histos); i++)
+ proccreate(histproc, &histos[i], mainstacksize);
+
+ enum { MOUSE, RESIZE, KEY };+ Alt a[] = {+ {mc->c, &mc->Mouse, CHANRCV},+ {mc->resizec, nil, CHANRCV},+ {kc->c, &r, CHANRCV},+ {nil, nil, CHANEND}+ };
+ for(;;)
+ switch(alt(a)){+ default: sysfatal("alt interrupted");+ case MOUSE:
+ mouse(mc);
+ break;
+ case RESIZE:
+ resize();
+ break;
+ case KEY:
+ key(r);
+ break;
+ }
+}
--- a/sys/src/cmd/image/mkfile
+++ b/sys/src/cmd/image/mkfile
@@ -4,6 +4,7 @@
TARG=\
correlate\
affinewarp\
+ histogram\
OFILES=\
util.$O\
--- a/sys/src/cmd/image/util.c
+++ b/sys/src/cmd/image/util.c
@@ -79,3 +79,30 @@
sysfatal("writememimage: %r");return rc;
}
+
+Image *
+eallocimage(Display *d, Rectangle r, ulong chan, int repl, ulong col)
+{+ Image *i;
+
+ i = allocimage(d, r, chan, repl, col);
+ if(i == nil)
+ sysfatal("allocimage: %r");+ setmalloctag(i, getcallerpc(&d));
+ return i;
+}
+
+Image *
+memimage2image(Display *d, Memimage *m)
+{+ Image *i;
+ Rectangle lr;
+
+ i = eallocimage(d, m->r, m->chan, 0, DNofill);
+ lr = m->r;
+ lr.max.y = lr.min.y + 1;
+ for(; lr.min.y < m->r.max.y; lr.min.y = lr.max.y++)
+ if(loadimage(i, lr, byteaddr(m, lr.min), bytesperline(lr, m->depth)) < 0)
+ sysfatal("loadimage: %r");+ return i;
+}
--
⑨