ref: 0dd9f39cf5c2ffc4ea83abcc703b1944e7d19093
parent: f67e66ebd0e98f0ff7487b56375d048af133cf09
author: rodri <rgl@antares-labs.eu>
date: Sun Sep 14 07:35:18 EDT 2025
add image(1): a new set of tools for image processing
--- /dev/null
+++ b/lib/image/filter/box4
@@ -1,0 +1,2 @@
+1 1
+1 1
--- /dev/null
+++ b/lib/image/filter/box9
@@ -1,0 +1,3 @@
+1 1 1
+1 1 1
+1 1 1
--- /dev/null
+++ b/lib/image/filter/edge1
@@ -1,0 +1,3 @@
+0 -1 0
+-1 4 -1
+0 -1 0
--- /dev/null
+++ b/lib/image/filter/edge2
@@ -1,0 +1,3 @@
+-1 -1 -1
+-1 8 -1
+-1 -1 -1
--- /dev/null
+++ b/lib/image/filter/gauss9
@@ -1,0 +1,3 @@
+1 2 1
+2 4 2
+1 2 1
--- /dev/null
+++ b/lib/image/filter/sharpen1
@@ -1,0 +1,3 @@
+0 -1 0
+-1 5 -1
+0 -1 0
--- /dev/null
+++ b/lib/image/filter/sharpen2
@@ -1,0 +1,5 @@
+0 0 1 0 0
+0 1 1 1 0
+1 1 -9 1 1
+0 1 1 1 0
+0 0 1 0 0
--- a/sys/lib/rootstub
+++ b/sys/lib/rootstub
@@ -7,6 +7,7 @@
mkdir -p 386/bin/disk
mkdir -p 386/bin/fs
mkdir -p 386/bin/games
+mkdir -p 386/bin/image
mkdir -p 386/bin/ip/httpd
mkdir -p 386/bin/ndb
mkdir -p 386/bin/nusb
@@ -23,6 +24,7 @@
mkdir -p 68000/bin/disk
mkdir -p 68000/bin/fs
mkdir -p 68000/bin/games
+mkdir -p 68000/bin/image
mkdir -p 68000/bin/ip/httpd
mkdir -p 68000/bin/ndb
mkdir -p 68000/bin/nusb
@@ -39,6 +41,7 @@
mkdir -p 68020/bin/disk
mkdir -p 68020/bin/fs
mkdir -p 68020/bin/games
+mkdir -p 68020/bin/image
mkdir -p 68020/bin/ip/httpd
mkdir -p 68020/bin/ndb
mkdir -p 68020/bin/nusb
@@ -66,6 +69,7 @@
mkdir -p amd64/bin/disk
mkdir -p amd64/bin/fs
mkdir -p amd64/bin/games
+mkdir -p amd64/bin/image
mkdir -p amd64/bin/ip/httpd
mkdir -p amd64/bin/ndb
mkdir -p amd64/bin/nusb
@@ -82,6 +86,7 @@
mkdir -p arm/bin/disk
mkdir -p arm/bin/fs
mkdir -p arm/bin/games
+mkdir -p arm/bin/image
mkdir -p arm/bin/ip/httpd
mkdir -p arm/bin/ndb
mkdir -p arm/bin/nusb
@@ -98,6 +103,7 @@
mkdir -p arm64/bin/disk
mkdir -p arm64/bin/fs
mkdir -p arm64/bin/games
+mkdir -p arm64/bin/image
mkdir -p arm64/bin/ip/httpd
mkdir -p arm64/bin/ndb
mkdir -p arm64/bin/nusb
@@ -129,6 +135,7 @@
mkdir -p mips/bin/disk
mkdir -p mips/bin/fs
mkdir -p mips/bin/games
+mkdir -p mips/bin/image
mkdir -p mips/bin/ip/httpd
mkdir -p mips/bin/ndb
mkdir -p mips/bin/nusb
@@ -145,6 +152,7 @@
mkdir -p spim/bin/disk
mkdir -p spim/bin/fs
mkdir -p spim/bin/games
+mkdir -p spim/bin/image
mkdir -p spim/bin/ip/httpd
mkdir -p spim/bin/ndb
mkdir -p spim/bin/nusb
@@ -161,6 +169,7 @@
mkdir -p power/bin/disk
mkdir -p power/bin/fs
mkdir -p power/bin/games
+mkdir -p power/bin/image
mkdir -p power/bin/ip/httpd
mkdir -p power/bin/ndb
mkdir -p power/bin/nusb
@@ -177,6 +186,7 @@
mkdir -p power64/bin/disk
mkdir -p power64/bin/fs
mkdir -p power64/bin/games
+mkdir -p power64/bin/image
mkdir -p power64/bin/ip/httpd
mkdir -p power64/bin/ndb
mkdir -p power64/bin/nusb
@@ -196,6 +206,7 @@
mkdir -p sparc/bin/disk
mkdir -p sparc/bin/fs
mkdir -p sparc/bin/games
+mkdir -p sparc/bin/image
mkdir -p sparc/bin/ip/httpd
mkdir -p sparc/bin/ndb
mkdir -p sparc/bin/nusb
@@ -212,6 +223,7 @@
mkdir -p sparc64/bin/disk
mkdir -p sparc64/bin/fs
mkdir -p sparc64/bin/games
+mkdir -p sparc64/bin/image
mkdir -p sparc64/bin/ip/httpd
mkdir -p sparc64/bin/ndb
mkdir -p sparc64/bin/nusb
--- /dev/null
+++ b/sys/man/1/image
@@ -1,0 +1,112 @@
+.TH IMAGE 1
+.SH NAME
+affinewarp, correlate - image processing tools
+.SH SYNOPSIS
+.B image/affinewarp
+[
+.B -Rqp
+] [[
+.B -s
+.I x y
+] [
+.B -r
+.I θ
+] [
+.B -t
+.I x y
+] [
+.B -S
+.I x y
+]
+.I ...
+]
+.br
+.B image/correlate
+[
+.B -cRp
+]
+.I kernel
+[
+.I denom
+]
+.SH DESCRIPTION
+Image processing tools for
+.IR image (6)
+files.
+.PP
+.I Affinewarp
+applies a sequence of affine image warping transformations to an image
+read from stdin and writes the result to stdout. The transformations
+happen in a left-handed coordinate system where the origin is the
+upper-left corner, and are applied in the order of the arguments. Its
+options:
+.TP
+.B -R
+Replicate the source image over the entire destination span. Without
+this option pixels that map outside the bounds of the image will be
+set to black; or transparent for images with an alpha channel.
+.TP
+.B -q
+Apply a bilinear filter when sampling the source for a higher-quality
+(albeit blurrier) result.
+.TP
+.B -p
+Enable parallelism.
+.TP
+.B -s
+Scale the picture
+.I x
+times along the x-axis and
+.I y
+times along the y-axis.
+.TP
+.B -r
+Rotate the image
+.I θ
+degrees.
+.TP
+.B -t
+Translate the picture
+.I x
+pixels along the x-axis and
+.I y
+pixels along the y-axis.
+.TP
+.B -S
+Shear the image
+.I x
+times along the x-axis and
+.I y
+times along the y-axis.
+.PP
+.I Correlate
+applies a correlation operation between an image read from stdin and a
+filtering
+.I kernel
+specified in a file of the same name, writing the result to stdout.
+It will look for the file in the current directory first, and if it
+doesn't find it, it will try in
+.BR /lib/image/filter .
+A
+.I denom
+parameter controls the normalizing coefficient applied to the kernel
+before doing the operation, and it can be any real number; by default
+or when set to zero, it will use the sum of coefficients in the
+kernel. Its options:
+.TP
+.B -c
+Apply a convolution instead of a correlation.
+.TP
+.B -R
+Replicate the source image. This changes the behavior of the sampler
+when accessing pixels outside of the image boundaries, warping the
+probe point so that it falls back into the image (a toroidal map).
+.TP
+.B -p
+Enable parallelism.
+.SH SOURCE
+.B /sys/src/cmd/image
+.SH SEE ALSO
+.IR crop (1),
+.IR resample (1),
+.IR image (6)
--- /dev/null
+++ b/sys/src/cmd/image/affinewarp.c
@@ -1,0 +1,236 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <memdraw.h>
+#include <geometry.h>
+#include "fns.h"
+
+typedef struct Mstk Mstk;
+struct Mstk
+{+ Matrix *items;
+ ulong size;
+};
+
+void
+pushmat(Mstk *stk, Matrix m)
+{+ if(stk->size % 4 == 0)
+ stk->items = erealloc(stk->items, (stk->size + 4)*sizeof(Matrix));
+ memmove(stk->items[stk->size++], m, sizeof(Matrix));
+}
+
+void
+popmat(Mstk *stk, Matrix m)
+{+ memmove(m, stk->items[--stk->size], sizeof(Matrix));
+ if(stk->size == 0){+ free(stk->items);
+ stk->items = nil;
+ }
+}
+
+void
+mkrotation(Matrix m, double θ)
+{+ double c, s;
+
+ c = cos(θ);
+ s = sin(θ);
+ Matrix R = {+ c, -s, 0,
+ s, c, 0,
+ 0, 0, 1,
+ };
+ memmove(m, R, sizeof(Matrix));
+}
+
+void
+mkscale(Matrix m, double sx, double sy)
+{+ Matrix S = {+ sx, 0, 0,
+ 0, sy, 0,
+ 0, 0, 1,
+ };
+ memmove(m, S, sizeof(Matrix));
+}
+
+void
+mktranslation(Matrix m, double tx, double ty)
+{+ Matrix T = {+ 1, 0, tx,
+ 0, 1, ty,
+ 0, 0, 1,
+ };
+ memmove(m, T, sizeof(Matrix));
+}
+
+void
+mkshear(Matrix m, double shx, double shy)
+{+ Matrix Sxy = {+ 1, shx, 0,
+ shy, 1, 0,
+ 0, 0, 1,
+ };
+ memmove(m, Sxy, sizeof(Matrix));
+}
+
+void
+mkxform(Matrix m, Mstk *stk)
+{+ Matrix t;
+
+ identity(m);
+ while(stk->size > 0){+ popmat(stk, t);
+ mulm(m, t);
+ }
+}
+
+Point2
+Ptpt2(Point p)
+{+ return (Point2){p.x, p.y, 1};+}
+
+#define min(a,b) ((a)<(b)?(a):(b))
+#define max(a,b) ((a)>(b)?(a):(b))
+Rectangle
+getbbox(Rectangle *sr, Matrix m, Point2 *dp0)
+{+ Point2 p0, p1, p2, p3;
+
+ p0 = Pt2(sr->min.x + 0.5, sr->min.y + 0.5, 1);
+ p0 = *dp0 = xform(p0, m);
+ p1 = Pt2(sr->max.x + 0.5, sr->min.y + 0.5, 1);
+ p1 = xform(p1, m);
+ p2 = Pt2(sr->min.x + 0.5, sr->max.y + 0.5, 1);
+ p2 = xform(p2, m);
+ p3 = Pt2(sr->max.x + 0.5, sr->max.y + 0.5, 1);
+ p3 = xform(p3, m);
+
+ p0.x = min(min(min(p0.x, p1.x), p2.x), p3.x);
+ p0.y = min(min(min(p0.y, p1.y), p2.y), p3.y);
+ p1.x = max(max(max(p1.x, p1.x), p2.x), p3.x);
+ p1.y = max(max(max(p1.y, p1.y), p2.y), p3.y);
+ return Rect(p0.x, p0.y, p1.x, p1.y);
+}
+
+void
+usage(void)
+{+ fprint(2, "usage: %s [-Rqp] [[-s x y] [-r θ] [-t x y] [-S x y]]...\n", argv0);
+ exits("usage");+}
+
+void
+main(int argc, char *argv[])
+{+ Memimage *dst, *src;
+ Matrix m;
+ Mstk stk;
+ Point2 Δp;
+ Warp w;
+ Rectangle dr, *wr;
+ double x, y, θ;
+ int smooth, dorepl, parallel, nproc, i;
+ char *nprocs;
+
+ dorepl = 0;
+ smooth = 0;
+ parallel = 0;
+ ARGBEGIN{+ case 's':
+ x = strtod(EARGF(usage()), nil);
+ y = strtod(EARGF(usage()), nil);
+ mkscale(m, x, y);
+ pushmat(&stk, m);
+ break;
+ case 'r':
+ θ = strtod(EARGF(usage()), nil)*DEG;
+ mkrotation(m, θ);
+ pushmat(&stk, m);
+ break;
+ case 't':
+ x = strtod(EARGF(usage()), nil);
+ y = strtod(EARGF(usage()), nil);
+ mktranslation(m, x, y);
+ pushmat(&stk, m);
+ break;
+ case 'S':
+ x = strtod(EARGF(usage()), nil);
+ y = strtod(EARGF(usage()), nil);
+ mkshear(m, x, y);
+ pushmat(&stk, m);
+ break;
+ case 'R':
+ dorepl++;
+ break;
+ case 'q':
+ smooth++;
+ break;
+ case 'p':
+ parallel++;
+ break;
+ default:
+ usage();
+ }ARGEND;
+ if(argc != 0)
+ usage();
+
+ if(memimageinit() != 0)
+ sysfatal("memimageinit: %r");+
+ src = ereadmemimage(0);
+ if(dorepl)
+ src->flags |= Frepl;
+
+ mkxform(m, &stk);
+ dr = getbbox(&src->r, m, &Δp);
+ Δp = subpt2(Δp, Ptpt2(dr.min));
+ Matrix T = {+ 1, 0, Δp.x,
+ 0, 1, Δp.y,
+ 0, 0, 1,
+ };
+ mulm(T, m);
+ mkwarp(w, T);
+
+ dr = rectaddpt(dr, subpt(src->r.min, dr.min));
+ dst = eallocmemimage(dr, src->chan);
+ memfillcolor(dst, DTransparent);
+
+ if(parallel){+ nprocs = getenv("NPROC");+ if(nprocs == nil || (nproc = strtoul(nprocs, nil, 10)) < 2)
+ nproc = 1;
+ free(nprocs);
+
+ wr = emalloc(nproc*sizeof(Rectangle));
+ initworkrects(wr, nproc, &dr);
+
+ for(i = 0; i < nproc; i++){+ switch(rfork(RFPROC|RFMEM)){+ case -1:
+ sysfatal("rfork: %r");+ case 0:
+ if(memaffinewarp(dst, wr[i], src, src->r.min, w, smooth) < 0)
+ fprint(2, "[%d] memaffinewarp: %r\n", getpid());
+ exits(nil);
+ }
+ }
+ while(waitpid() != -1)
+ ;
+
+ free(wr);
+ }else
+ if(memaffinewarp(dst, dr, src, src->r.min, w, smooth) < 0)
+ sysfatal("memaffinewarp: %r");+
+ ewritememimage(1, dst);
+
+ exits(nil);
+}
--- /dev/null
+++ b/sys/src/cmd/image/correlate.c
@@ -1,0 +1,177 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <draw.h>
+#include <memdraw.h>
+#include "fns.h"
+
+char kerndir[] = "/lib/image/filter";
+
+void
+reversek(double *k, int len)
+{+ double *e, t;
+
+ e = k + len;
+ while(k < e){+ t = *k;
+ *k++ = *--e;
+ *e = t;
+ }
+}
+
+#define isspace(c) ((c) == ' ' || (c) == '\t')
+Memimage *
+readimagekernel(int fd, double denom, int reverse)
+{+ Memimage *ik;
+ Biobuf *bin;
+ double *k, d;
+ int nk, dx0, dx, dy;
+ char *line;
+
+ k = nil;
+ nk = 0;
+ dx0 = dx = dy = 0;
+
+ bin = Bfdopen(fd, OREAD);
+ if(bin == nil)
+ sysfatal("Bfdopen: %r");+
+ while((line = Brdstr(bin, '\n', 1)) != nil){+ while(*line){+ d = strtod(line, &line);
+ if(*line && !isspace(*line)){+ free(k);
+ Bterm(bin);
+ werrstr("bad image kernel file");+ return nil;
+ }
+ k = erealloc(k, (nk + 1)*sizeof(double));
+ k[nk++] = d;
+ dx++;
+ }
+ if(dy > 0 && dx != dx0){+ free(k);
+ Bterm(bin);
+ werrstr("image kernel file has inconsistent dx");+ return nil;
+ }
+ if(dy++ == 0)
+ dx0 = dx;
+ dx = 0;
+ }
+ Bterm(bin);
+
+ if(reverse)
+ reversek(k, dx0*dy);
+
+ ik = allocmemimagekernel(k, dx0, dy, denom);
+ free(k);
+ if(ik == nil){+ werrstr("allocmemimagekernel: %r");+ return nil;
+ }
+ return ik;
+}
+
+void
+usage(void)
+{+ fprint(2, "usage: %s [-cRp] kernel [denom]\n", argv0);
+ exits("usage");+}
+
+void
+main(int argc, char *argv[])
+{+ Memimage *dst, *src;
+ Memimage *ikern;
+ Rectangle dr, *wr;
+ int convolve, dorepl, parallel, fd, nproc, i;
+ char *nprocs, *p, kernf[128];
+ double denom;
+
+ denom = 0;
+ convolve = 0;
+ dorepl = 0;
+ parallel = 0;
+ ARGBEGIN{+ case 'c':
+ convolve++;
+ break;
+ case 'R':
+ dorepl++;
+ break;
+ case 'p':
+ parallel++;
+ break;
+ default:
+ usage();
+ }ARGEND;
+ switch(argc){+ case 2:
+ denom = strtod(argv[1], nil);
+ /* fallthrough */
+ case 1:
+ break;
+ default:
+ usage();
+ }
+
+ if(memimageinit() != 0)
+ sysfatal("memimageinit: %r");+
+ fd = open(argv[0], OREAD);
+ if(fd < 0){+ p = strrchr(argv[0], '/');
+ p = p? p+1: argv[0];
+ snprint(kernf, sizeof kernf, "%s/%s", kerndir, p);
+ fd = open(kernf, OREAD);
+ if(fd < 0)
+ sysfatal("could not find filter: %r");+ }
+ ikern = readimagekernel(fd, denom, convolve);
+ if(ikern == nil)
+ sysfatal("readimagekernel: %r");+ close(fd);
+
+ src = ereadmemimage(0);
+ if(dorepl)
+ src->flags |= Frepl;
+
+ dr = src->r;
+ dst = eallocmemimage(dr, src->chan);
+ memfillcolor(dst, DTransparent);
+
+ if(parallel){+ nprocs = getenv("NPROC");+ if(nprocs == nil || (nproc = strtoul(nprocs, nil, 10)) < 2)
+ nproc = 1;
+ free(nprocs);
+
+ wr = emalloc(nproc*sizeof(Rectangle));
+ initworkrects(wr, nproc, &dr);
+
+ for(i = 0; i < nproc; i++){+ switch(rfork(RFPROC|RFMEM)){+ case -1:
+ sysfatal("rfork: %r");+ case 0:
+ if(memimagecorrelate(dst, wr[i], src, src->r.min, ikern) < 0)
+ fprint(2, "[%d] memimagecorrelate: %r\n", getpid());
+ exits(nil);
+ }
+ }
+ while(waitpid() != -1)
+ ;
+
+ free(wr);
+ }else
+ if(memimagecorrelate(dst, dr, src, src->r.min, ikern) < 0)
+ sysfatal("memimagecorrelate: %r");+
+ ewritememimage(1, dst);
+
+ exits(nil);
+}
--- /dev/null
+++ b/sys/src/cmd/image/fns.h
@@ -1,0 +1,6 @@
+void initworkrects(Rectangle*, int, Rectangle*);
+void *emalloc(ulong);
+void *erealloc(void*, ulong);
+Memimage *eallocmemimage(Rectangle, ulong);
+Memimage *ereadmemimage(int);
+int ewritememimage(int, Memimage*);
--- /dev/null
+++ b/sys/src/cmd/image/mkfile
@@ -1,0 +1,14 @@
+</$objtype/mkfile
+
+BIN=/$objtype/bin/image
+TARG=\
+ correlate\
+ affinewarp\
+
+OFILES=\
+ util.$O\
+
+HFILES=\
+ fns.h\
+
+</sys/src/cmd/mkmany
--- /dev/null
+++ b/sys/src/cmd/image/util.c
@@ -1,0 +1,81 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <memdraw.h>
+#include "fns.h"
+
+void
+initworkrects(Rectangle *wr, int nwr, Rectangle *fbr)
+{+ int i, Δy;
+
+ wr[0] = *fbr;
+ Δy = Dy(wr[0])/nwr;
+ wr[0].max.y = wr[0].min.y + Δy;
+ for(i = 1; i < nwr; i++)
+ wr[i] = rectaddpt(wr[i-1], Pt(0,Δy));
+ if(wr[nwr-1].max.y < fbr->max.y)
+ wr[nwr-1].max.y = fbr->max.y;
+}
+
+void *
+emalloc(ulong n)
+{+ void *v;
+
+ v = malloc(n);
+ if(v == nil)
+ sysfatal("realloc: %r");+ setmalloctag(v, getcallerpc(&n));
+ return v;
+}
+
+void *
+erealloc(void *v, ulong n)
+{+ void *nv;
+
+ nv = realloc(v, n);
+ if(nv == nil)
+ sysfatal("realloc: %r");+ if(v == nil)
+ setmalloctag(nv, getcallerpc(&v));
+ else
+ setrealloctag(nv, getcallerpc(&v));
+ return nv;
+}
+
+Memimage *
+eallocmemimage(Rectangle r, ulong chan)
+{+ Memimage *i;
+
+ i = allocmemimage(r, chan);
+ if(i == nil)
+ sysfatal("allocmemimage: %r");+ memfillcolor(i, DTransparent);
+ setmalloctag(i, getcallerpc(&r));
+ return i;
+}
+
+Memimage *
+ereadmemimage(int fd)
+{+ Memimage *i;
+
+ i = readmemimage(fd);
+ if(i == nil)
+ sysfatal("readmemimage: %r");+ return i;
+}
+
+int
+ewritememimage(int fd, Memimage *i)
+{+ int rc;
+
+ rc = writememimage(fd, i);
+ if(rc < 0)
+ sysfatal("writememimage: %r");+ return rc;
+}
--
⑨