git: 9front

Download patch

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;
+}
--