Skip to content
lispnikPublic

About

Lisp CFFI bindings for IM, a toolkit for image representation, storage, capture and processing.

Topics

Resources

Stars

3 stars

Watchers

1 watching

Forks

Latest commit

 

History

123 Commits

Folders and files

Repository files navigation

im — Common Lisp bindings to the IM imaging toolkit

CFFI bindings to IM, Tecgraf's toolkit for image representation, storage, capture and processing, plus im(1), a command line tool that drives them.

This project is unaffiliated with Tecgraf.

Requires tecgraf-im v2.2.1 or later: v0.8.0 binds the watershed, the edge-preserving denoising filters, Richardson-Lucy deconvolution and the shape and intensity measurements, none of which earlier releases export. v2.2.1 rather than v2.2.0 because two bugs found while writing those bindings are fixed in it, and working around them here cost more than the operations were worth — a progress callback killed the process outright in the OpenMP build, and asking for fewer regions than an image carries corrupted the heap. v0.7.x needs v2.1.1, the first usable release carrying the decorrelation stretch.

Built against lispnik/tecgraf-im, a CMake fork of IM 3.15. The bindings cover 468 C functions — every function exported by libim, libim_process, libim_capture, libim_fftw3 and the format add-ons, apart from a documented list of driver internals.

Installing

Prebuilt binaries for Linux (amd64 and arm64), macOS (Apple Silicon) and Windows are attached to each release. They embed an SBCL core but do not bundle IM, so the shared libraries still have to be present -- see Finding the libraries below.

Building from source

  • SBCL, and ocicl for dependencies
  • IM 3.15 shared libraries
ocicl install          # restore the pinned dependencies
make                   # build bin/im
make test              # run the test suite

Finding the libraries

The bindings look for libim and its add-ons in this order:

  1. im:*library-path*, if set
  2. the IM_LIBRARY_PATH environment variable
  3. a lib/ directory beside the running executable — the release layout
  4. cffi:*foreign-library-directories* and the platform's own search path
IM_LIBRARY_PATH=/path/to/tecgraf-im/build/lib ./bin/im library

im library reports the IM version and exactly which shared objects were opened, which is the fastest way to tell whether an add-on is present.

Add-ons are optional. libim_jp2, libim_heif and libim_capture are all switched off in upstream's default build, and the bindings load without them — their formats simply do not appear in im formats.

The command line tool

im info FILE...        format, dimensions, colour mode, attributes
im formats             the registered formats and their compressions
im convert IN OUT      format, compression, colour space or depth
im process IN OUT      a pipeline of operations
im analyze FILE        label connected regions and measure them
im stats FILE          per-plane statistics and histograms
im compare A B         RMS error and signal-to-noise ratio
im diff A B            SSIM, perceptual hash, and a difference heatmap
im montage FILE...     lay many images out as one contact sheet
im capture             list capture devices, or grab a frame
im library             IM version and the libraries in use

Every subcommand takes --json, which emits one JSON value:

im info photo.jpg --json | jq '.frames[0] | {width, height}'

Operations in im process are given as repeated --op arguments and applied in the order written, which is why they are not one flag each:

im process in.jpg out.png \
    --op resize=50% --op colorspace=gray --op gaussian=1.5 --op sobel

--op dstretch=SPACE[,SCALE] is the decorrelation stretch — the enhancement DStretch is built on, which pulls apart colours that lie along a single axis so faint differences become visible. lds is the general-purpose space and the default scale is 6:

im process faded.jpg enhanced.png --op dstretch=lds

lre, yre and crgb favour reds, yye and lye yellows, ybk and lbk blacks and blues. im:decorrelation-fit and im:decorrelation-apply expose the same thing as a transform that can be fitted to one image and applied to a whole series, which is how you get consistent colour across a set.

Three filters smooth an image without blurring across its edges, which gaussian and median cannot do. The parameter that matters in each is a threshold in the image's own sample units, and the right value for it is roughly the noise level:

im process noisy.png clean.png --op bilateral=3,25      # spatial, range
im process noisy.png clean.png --op diffusion=15,25     # iterations, kappa
im process noisy.png clean.png --op nlmeans=5,2,20      # search, patch, stddev

nlmeans recovers repeated fine structure the other two smooth away, and is one to two orders of magnitude slower for it. diffusion takes an optional time step and conductance function — exponential, quadratic or tukey.

--op deconvolve=STDDEV[,ITERATIONS] is Richardson-Lucy deconvolution of a Gaussian blur. The iteration count is the only regularisation there is: past a few tens of steps it starts fitting the noise, which shows as ringing that grows with every further one.

im process blurred.png sharp.png --op deconvolve=1.5,20

im:deconvolve-richardson-lucy takes any point spread function image, for a measured PSF rather than a Gaussian.

--op watershed[=CONNECTIVITY][,lines] splits touching objects that connected component labelling has to call one region, and writes a label image. It does not binarise for you — which binarisation is used decides what the objects are — so put a threshold in front of it:

im process cells.png labels.png --op threshold=otsu --op watershed=8

im process --list-ops lists all twenty-six. Sizes accept WxH, 800x or x600 to preserve the aspect ratio, and 50%.

im analyze labels regions and measures them. --measure picks what to report — area, centroid, bbox, hull, feret, intensity, or all; the default is area and centroid. --watershed separates touching objects first, the same split --op watershed does:

im analyze cells.png --watershed --measure all --json | jq '.regions[0]'

feret is the caliper diameters — the longest distance across a region and its narrowest width, with angles. intensity is the only measurement that reads the image rather than the labels, and so the only one that says how bright a region is rather than what shape it is.

Exit codes are 0 for success, 1 for an IM error, 2 for a usage error and 130 for an interrupt. Diagnostics go to stderr, so piping stdout to jq is safe.

im compare measures how far apart two images' pixels are; im diff answers the structural question — are they the same picture — with SSIM, a perceptual hash (which compares even across sizes and re-encodings), and, with --output, a heatmap of where they differ:

im diff before.png after.png --output changed.png
im diff original.jpg thumbnail.jpg        # different sizes: hashes still answer

im montage composes many images into one contact sheet, normalising mixed sizes, colour spaces and depths onto a plain background:

im montage shots/*.png --output sheet.png --columns 4 --tile 200x200

The MCP server

bin/im-mcp (built by make) is a Model Context Protocol server, so an agent can inspect, measure and transform images through the same binding. It speaks JSON-RPC over stdio and exposes eight tools:

Tool What it answers
im_info format, dimensions, colour space, data type, frames
im_stats per-plane min, max, mean, stddev
im_formats what this build can read and write
im_diff are these the same picture — RMSE, PSNR, SSIM, perceptual hashes
im_thumbnail the picture itself, inline
im_montage many pictures as one contact sheet, inline
im_analyze how many objects, where, and how big
im_process a pipeline of operations, result inline

im_thumbnail, im_montage and im_process return the image inline, so the agent gets the picture rather than a path it cannot open. Point an MCP client at the executable:

{ "command": "/path/to/bin/im-mcp" }

im_analyze is the one that answers a question no thumbnail can. Counting objects, measuring them, and separating the ones that touch is not something a model can do by looking:

{ "name": "im_analyze",
  "arguments": { "path": "cells.png", "watershed": true, "measure": "all" } }

im_process takes the same --op vocabulary as im process, applied in order, and returns a scaled preview inline — with output to write the full-size result somewhere:

{ "name": "im_process",
  "arguments": { "path": "noisy.png", "ops": ["bilateral=3,25", "unsharp=2,1,0"] } }

Every tool reuses the image algebra behind im(1) rather than reimplementing it — im_analyze and im_process call the very functions im analyze and im process do — so the command line and the agent interface cannot drift.

The library

(asdf:load-system :im)

(im:with-image (photo (im:load #p"photo.jpg"))
  (im:with-image (edges (im:create-based photo :color-space :color-space-gray))
    (im:convolve-sobel photo edges)
    (im:save edges #p"edges.png")))

Images

im:image is a CLOS object wrapping IM's imImage. Its storage is released by im:destroy, by im:with-image on unwind, or — for images that escape both — by a finalizer. im:destroy is idempotent and disarms the finalizer, so the two cannot race. Operating on a destroyed image signals im:invalid-image rather than reading freed memory.

Pixel data is reached through im:plane-pointer, a raw foreign pointer. That is deliberate, and it is IM's own reasoning: the library supports so many data organisations that general-purpose per-pixel accessors would be both complicated and slow. Planes are always unpacked and stored bottom-up.

Attributes

Images carry named metadata, and formats store what they recognise. Attributes are read from a file with im:attributes, and set on an image — which is what im:save writes out — with im:set-image-attribute or setf:

(im:with-image (photo (im:load #p"photo.jpg"))
  (setf (im:image-attribute photo "Author") "Ada Lovelace")
  (im:set-image-attribute photo "Levels" #(1 2 3))          ; stored as int
  (im:set-image-attribute photo "Gamma" 2.2d0)              ; stored as double
  (im:set-image-attribute photo "Small" 200 :data-type :data-type-byte)
  (im:save photo #p"tagged.png"))

(im:attribute #p"tagged.png" "Author")
;; => ("Ada Lovelace" :DATA-TYPE-BYTE 13)

The data type is inferred as the narrowest one that holds the value exactly — byte for a string, int for integers, double for floats, cdouble for complex numbers — and :data-type overrides it. A value that will not fit the type is an error rather than a truncation, and so is a ratio, which no IM type holds exactly; (setf (im:image-attribute ...) nil) removes the attribute.

Byte attributes do not round-trip as vectors: IM stores text in them, so #(65 66) reads back as "AB".

What survives the write is the format's decision. PNG keeps "Author"; TIFF has no tag for it and drops it silently. Read the file back rather than assuming.

Displaying an image in the REPL

im:display writes a PNG and asks the attached editor to show it:

(im:with-image (photo (im:load #p"photo.jpg"))
  (im:display photo))

One thing has to be set up on the Emacs side, and neither can be checked from Lisp:

Front end Prerequisite Where the image appears
SLY (setq sly-enable-evaluate-in-emacs t) an *im-image* buffer
SLIME (slime-setup '(slime-media)) inline, as the REPL result

Both are one-way messages, so a missing prerequisite is reported by Emacs, not here. With nothing attached at all — a bare REPL, a script — im:display signals im:display-unavailable rather than writing a file no one will look at. Bind im:*display-function* to render somewhere else.

im:enable-repl-images goes one step further under SLIME: a bare im:image result then renders itself instead of printing #<IM:IMAGE …>. (SLY's mrepl has no result hook, so there it signals and you call im:display yourself.)

Both paths under SLIME need the slime-media contrib, and this is where its absence shows: an image result errors in Emacs with

slime-dispatch-event: slime-dcase failed: (:write-image ((:type png :file "…")) "#<IM:IMAGE …>")

That is SLIME's core dispatcher rejecting the :write-image event because slime-media — which handles it — was never loaded. Add it and reconnect:

(slime-setup '(slime-repl slime-media))

or, to fix a running session without restarting, evaluate in Emacs:

(require 'slime-media)
(add-hook 'slime-event-hooks 'slime-dispatch-media-event)

A REPL workbench

The processing operations take (source destination …) — you allocate the destination, they fill it. That is the right primitive and an awkward shape for the REPL, so im:pipe threads an image through a series of functional stages, reclaiming each intermediate as the next consumes it:

(im:pipe (im:load #p"photo.jpg")
         (im:resized :scale 0.5)          ; functional wrappers for the two
         #'im:grayscale                   ; shape-changing operations
         (im:derive #'im:convolve-sobel)  ; any (src dst) op, made functional
         #'im:show)                        ; print stats + display, pass through

A stage is a function of one image returning a fresh one. im:derive turns any same-shape operation (convolve-sobel, negative, morph-erode, …) into that shape; im:grayscale and im:resized wrap the common shape-changers; anything else is a one-line lambda. The image passed in and the one returned are the caller's to own — only the intermediates are freed, and only after the next stage has used them (im:destroy is idempotent and finalizer-backed, so this is safe even for a stage that returns its own argument).

im:show prints an image's geometry and per-plane statistics and displays it, returning the image so it drops into a pipe as a tap.

Conditions

Every failure is a subtype of im:im-error, and each of IM's error codes has its own class, so causes are distinguished by handler rather than by testing a slot:

(handler-case (im:load path)
  (im:open-error   (c) (format t "cannot open: ~A" (im:error-detail c)))
  (im:format-error (c) (format t "unrecognised format")))

Progress and cancellation

A callback installed with im:with-progress is called as an operation runs and can stop it. Cancelling signals im:operation-aborted — a real error, not a silent NIL — and the operation offers retry and continue restarts:

(im:with-progress ((lambda (id text percent)
                     (declare (ignore id text))
                     (< percent 500)))          ; stop halfway
  (im:convolve-gaussian source destination 8.0))

Layout

Path Contents
src/ffi/ The raw bindings. Generated, then hand-corrected. Do not add hand-written files here — the generator clears it.
src/ffi-structs.lisp Hand-written C structs, kept outside the generated directory.
src/*.lisp The Lisp API: conditions, library loading, images, files, processing, capture.
src/cli/ im(1), one file per subcommand group.
tools/gen-bindings.lisp The binding generator. Not part of any shipped system.

Regenerating the bindings

make bindings IM_SOURCE=/path/to/tecgraf-im

The generator takes its symbol list from nm on the built libraries, not from the headers, so it cannot bind a function that does not exist. Headers declare several that no library implements. It also emits src/ffi/manifest.lisp, which the test suite uses to check at runtime that every bound function resolves.

Regenerating overwrites everything under src/ffi/. Run it into a clean tree and read the diff.

Deviations from the C API

  • Names are spelled out and hyphenated: imFileOpen → im:load, imProcessReduceBy4 → im:resize.
  • Enums and bitfields are keywords: :color-space-rgb, :data-type-byte.
  • IM packs a colour space and three configuration bits into one int; the Lisp API keeps im:color-space and im:color-mode-config separate.
  • C setters become setf functions, and out-parameters become multiple values.

Notes

libim_jp2 prints a JasPer deprecation banner on startup. It comes from jas_init inside IM's JP2 driver, goes to stderr, and is harmless.

imProcessBitwiseOp's :xor is a true exclusive-or in this fork of IM; upstream's computed NOR, which is available here as :nor. Code ported from stock IM 3.15 changes behaviour silently.

On macOS, im capture can enumerate devices from a terminal but connecting to one requires NSCameraUsageDescription in an application bundle. Without it the process is killed by TCC rather than being allowed to fail.

License

MIT. See LICENSE.

About

Lisp CFFI bindings for IM, a toolkit for image representation, storage, capture and processing.

Topics

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages