Oil Paint Filter

node v8.17.0
version: 17.0.0
endpointsharetweet
This is a requirable node module, rich documentation, and a *LIVE* API. To require it, use require("notebook")("tolmasky/oil-paint-filter/16.0.0"). To access it as an API, go to https://tonicdev.io/tolmasky/oil-paint-filter/16.0.0?url=IMG_URL (you can also post data to it) What follows is Literate Documentation: This an implementation of an oil paint filter. Takes in a buffer and returns a buffer. This is a port of the C# tutorial http://softwarebydefault.com/2013/06/29/oil-painting-cartoon-filter/, using the filter tutorial found at http://www.html5rocks.com/en/tutorials/canvas/imagefilters/ We'll set up our exports, and also just run it right in this document so we can see the result.
var R = require("ramda"); // For requires module.exports = oilPaintFilter; // For endpoint, we we can accept POST data. module.exports.tonicEndpoint = oilPaintFilterEndpoint; // If we're not endpoint, let's go ahead and show a pretty horse. if (!process.env.TONIC_MOUNT_PATH) { var notebook = require("notebook"); var googleSearch = notebook("tolmasky/google-image-search/3.0.0"); var horseBuffer = await googleSearch("horse (domestic)") await (oilPaintFilter(horseBuffer)); }
This is our main export. We take a buffer, run it through our filter, and return the buffer. This is a simplication of our more sophisticated filter function below. Perhaps later we should provide the same customizability instead of, or as well as, this one with all the presets already selected.
function oilPaintFilter(aBuffer) { var size = require('image-size')(aBuffer); var canvas = new (require("canvas"))(size.width, size.height); var context = canvas.getContext("2d"); var image = new (require("canvas").Image)(); image.src = aBuffer; context.drawImage(image, 0, 0, size.width, size.height); context.putImageData(oilPaint(context, size, 13, 5), 0, 0); return canvas.toBuffer(); };
This is our endpoint function. We grab the POST data, and then run it through our main function.
async function oilPaintFilterEndpoint(req, res) { var url = require('url').parse(req.url, true).query.url; var body = null; if (url) { var result = await require("got")(url, { encoding: null }); body = result.body; } else { var getRawBody = require("raw-body"); var body = await getRawBody(req); } var buffer = oilPaintFilter(body); res.writeHead(200, {'Content-Type': require("image-type")(buffer)}); res.end(buffer); }
The real work. We actually support more customization here which we hide to the outside world (levels and filter size). Perhaps we could split this out into a separate function for users that want more control.
function oilPaint(aContext, size, levels, filterSize) { levels = levels - 1; var filterOffset = Math.floor((filterSize - 1) / 2); var byteOffset = 0; var calcOffset = 0; var currentIntensity = 0; var maxIntensity = 0; var maxIndex = 0; var imageData = aContext.getImageData(0, 0, size.width, size.height); var pixelBuffer = imageData.data; var resultImageData = aContext.createImageData(imageData); var resultBuffer = resultImageData.data; for (var i = 0; i < pixelBuffer.length; ++i) resultBuffer[i] = pixelBuffer[i]; var toPixel = (x, y) => x * 4 + y * size.width * 4; for (var offsetY = filterOffset; offsetY < imageData.height - filterOffset; offsetY++) { for (var offsetX = filterOffset; offsetX < size.width - filterOffset; offsetX++) { var blue = 0; var green = 0; var red = 0; var currentIntensity = 0; var maxIntensity = 0; var maxIndex = 0; var intensityBin = R.times(() => 0, levels + 1); var blueBin = R.times(() => 0, levels + 1); var greenBin = R.times(() => 0, levels + 1); var redBin = R.times(() => 0, levels + 1); var byteOffset = toPixel(offsetX, offsetY); for (var filterY = -filterOffset; filterY <= filterOffset; filterY++) { for (var filterX = -filterOffset; filterX <= filterOffset; filterX++) { var calcOffset = byteOffset + toPixel(filterX, filterY); var currentIntensity = Math.round(( (pixelBuffer[calcOffset] + pixelBuffer[calcOffset + 1] + pixelBuffer[calcOffset + 2]) / 3.0 * (levels)) / 255.0); intensityBin[currentIntensity] += 1; blueBin[currentIntensity] += pixelBuffer[calcOffset]; greenBin[currentIntensity] += pixelBuffer[calcOffset + 1]; redBin[currentIntensity] += pixelBuffer[calcOffset + 2]; if (intensityBin[currentIntensity] > maxIntensity) { maxIntensity = intensityBin[currentIntensity]; maxIndex = currentIntensity; } } } var blue = blueBin[maxIndex] / maxIntensity; var green = greenBin[maxIndex] / maxIntensity; var red = redBin[maxIndex] / maxIntensity; resultBuffer[byteOffset] = ClipByte(blue); resultBuffer[byteOffset + 1] = ClipByte(green); resultBuffer[byteOffset + 2] = ClipByte(red); resultBuffer[byteOffset + 3] = 255; } } return resultImageData; } function ClipByte(byte) { return byte > 255 ? 255 : byte < 0 ? 0 : byte; }
Loading…

3 comments

  • posted a year ago by olddynasty
    https://www.facebook.com/fabian.silvacardenas.3?mibextid=ZbWKwL
  • posted a year ago by olddynasty
    https://www.facebook.com/olddynastysilba?mibextid=ZbWKwL
  • posted a year ago by olddynasty
    https://youtube.com/@OLDDYNASTYSILVA

sign in to comment