Aegisub/OverLua/raster_ops.cpp
Niels Martin Hansen 17f235515e Add raster.separable_filter function.
Originally committed to SVN as r1483.
2007-08-13 14:56:02 +00:00

457 lines
14 KiB
C++

/*
* Raster image operations for OverLua
*
Copyright 2007 Niels Martin Hansen
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Contact:
E-mail: <jiifurusu@gmail.com>
IRC: jfs in #aegisub on irc.rizon.net
*/
#include "cairo_wrap.h"
#include <math.h>
//#include <windows.h>
#include <omp.h>
#include <stdint.h>
#include "raster_ops.h"
#include "../lua51/src/lauxlib.h"
/*#define cimg_display_type 0
#include "CImg.h"
using namespace cimg_library;
// Type of images processed
typedef CImg<unsigned char> Img;
// Make an Img representing the image of a Cairo image surface
// from the Lua wrapper of Cairo.
static inline Img ImgFromSurf(lua_State *L, int idx)
{
LuaCairoSurface *surfobj = LuaCairoSurface::GetObjPointer(L, idx);
cairo_surface_t *surf = surfobj->GetSurface();
if (cairo_surface_get_type(surf) != CAIRO_SURFACE_TYPE_IMAGE) {
lua_pushliteral(L, "Object for raster operation is not an image surface. Video frames are not accepted.");
lua_error(L);
}
cairo_surface_flush(surf);
int width = cairo_image_surface_get_width(surf);
int height = cairo_image_surface_get_height(surf);
int stride = cairo_image_surface_get_stride(surf);
int dim = 0;
switch (cairo_image_surface_get_format(surf)) {
case CAIRO_FORMAT_ARGB32:
case CAIRO_FORMAT_RGB24:
dim = 4;
break;
case CAIRO_FORMAT_A8:
dim = 1;
break;
case CAIRO_FORMAT_A1:
lua_pushliteral(L, "1 bpp image surfaces are not supported for raster operations");
lua_error(L);
break;
default:
lua_pushliteral(L, "Unknown pixel format for image surface");
lua_error(L);
}
unsigned char *data = cairo_image_surface_get_data(surf);
Img res;
// Copy over data
if (dim == 4) {
res = Img(width, height, 1, 4);
unsigned char *ptrA, *ptrR, *ptrG, *ptrB;
ptrA = res.ptr(0, 0, 0, 0);
ptrR = res.ptr(0, 0, 0, 1);
ptrG = res.ptr(0, 0, 0, 2);
ptrB = res.ptr(0, 0, 0, 3);
// Can't use cimg_mapXY since we need to take stride into account
for (int row = 0; row < height; row++) {
unsigned char *ptrI = data + row*stride;
for (int x = width-1; x > 0; x--) {
*(ptrA)++ = *(ptrI++);
*(ptrR)++ = *(ptrI++);
*(ptrG)++ = *(ptrI++);
*(ptrB)++ = *(ptrI++);
}
}
}
else if (dim == 1) {
// Also need to take stride into account here
res = Img(width, height, 1, 1);
unsigned char *ptrO = res.ptr(0, 0, 0, 0);
for (int row = 0; row < height; row++) {
unsigned char *ptrI = data + row*stride;
for (int x = width-1; x > 0; x--) {
*(ptrO++) = *(ptrI++);
}
}
}
return res;
}
static inline void ImgToSurf(lua_State *L, int idx, const Img &img)
{
// Assume it has already been checked that a suitable surface is in the stack index
LuaCairoSurface *surfobj = LuaCairoSurface::GetObjPointer(L, idx);
cairo_surface_t *surf = surfobj->GetSurface();
int width = cairo_image_surface_get_width(surf);
int height = cairo_image_surface_get_height(surf);
int stride = cairo_image_surface_get_stride(surf);
int dim = 0;
switch (cairo_image_surface_get_format(surf)) {
case CAIRO_FORMAT_ARGB32:
case CAIRO_FORMAT_RGB24:
dim = 4;
break;
case CAIRO_FORMAT_A8:
dim = 1;
break;
case CAIRO_FORMAT_A1:
lua_pushliteral(L, "1 bpp image surfaces are not supported for raster operations");
lua_error(L);
break;
default:
lua_pushliteral(L, "Unknown pixel format for image surface");
lua_error(L);
}
if (width != img.dimx() || height != img.dimy() || dim != img.dimv()) {
lua_pushliteral(L, "Internal error, attempting to write back CImg to image surface with mismatching dimensions");
lua_error(L);
}
unsigned char *data = cairo_image_surface_get_data(surf);
// Copy over data
if (dim == 4) {
const unsigned char *ptrA, *ptrR, *ptrG, *ptrB;
ptrA = img.ptr(0, 0, 0, 0);
ptrR = img.ptr(0, 0, 0, 1);
ptrG = img.ptr(0, 0, 0, 2);
ptrB = img.ptr(0, 0, 0, 3);
// Can't use cimg_mapXY since we need to take stride into account
for (int row = 0; row < height; row++) {
unsigned char *ptrO = data + row*stride;
for (int x = width-1; x > 0; x--) {
*(ptrO)++ = *(ptrA++);
*(ptrO)++ = *(ptrR++);
*(ptrO)++ = *(ptrG++);
*(ptrO)++ = *(ptrB++);
}
}
}
else if (dim == 1) {
// Also need to take stride into account here
const unsigned char *ptrI = img.ptr(0, 0, 0, 0);
for (int row = 0; row < height; row++) {
unsigned char *ptrO = data + row*stride;
for (int x = width-1; x > 0; x--) {
*(ptrO++) = *(ptrI++);
}
}
}
cairo_surface_mark_dirty(surf);
}*/
static inline cairo_surface_t *CheckSurface(lua_State *L, int idx)
{
LuaCairoSurface *surfobj = LuaCairoSurface::GetObjPointer(L, idx);
cairo_surface_t *surf = surfobj->GetSurface();
if (cairo_surface_get_type(surf) != CAIRO_SURFACE_TYPE_IMAGE) {
lua_pushliteral(L, "Object for raster operation is not an image surface. Video frames are not accepted.");
lua_error(L);
}
return surf;
}
static inline double NormalDist(double sigma, double x)
{
if (sigma <= 0 && x == 0) return 1;
else if (sigma <= 0) return 0;
else return exp(-(x*x)/(2*sigma*sigma)) / (sigma * sqrt(2*3.1415926535));
}
// Filter an image in horizontal direction with a one-dimensional filter
// PixelWidth is the distance in bytes between pixels
template<ptrdiff_t PixelWidth>
void SeparableFilterX(unsigned char *src, unsigned char *dst, int width, int height, ptrdiff_t stride, int *kernel, int kernel_size, int divisor)
{
#pragma omp parallel for
for (int y = 0; y < height; y++) {
unsigned char *in = src + y*stride;
unsigned char *out = dst + y*stride;
for (int x = 0; x < width; x++) {
int accum = 0;
for (int k = 0; k < kernel_size; k++) {
int xofs = k - kernel_size/2;
//if (x+xofs < 0 || x+xofs >= width) continue;
if (x+xofs < 0) xofs += width;
if (x+xofs >= width) xofs -= width;
accum += (int)(in[xofs*PixelWidth] * kernel[k]);
}
accum /= divisor;
if (accum > 255) accum = 255;
if (accum < 0) accum = 0;
*out = (unsigned char)accum;
in+=PixelWidth;
out+=PixelWidth;
}
}
}
// Filter an image in vertical direction with a one-dimensional filter
// This one templated with PixelWidth since the channel interlacing is horizontal only,
// filtering once vertically will automatically catch all channels.
// (Width must be multiplied by pixel width for that to happen though.)
void SeparableFilterY(unsigned char *src, unsigned char *dst, int width, int height, ptrdiff_t stride, int *kernel, int kernel_size, int divisor)
{
#pragma omp parallel for
for (int x = 0; x < width; x++) {
unsigned char *in = src + x;
unsigned char *out = dst + x;
for (int y = 0; y < height; y++) {
int accum = 0;
for (int k = 0; k < kernel_size; k++) {
int yofs = k - kernel_size/2;
//if (y+yofs < 0 || y+yofs >= height) continue;
if (y+yofs < 0) yofs += height;
if (y+yofs >= height) yofs -= height;
accum += (int)(in[yofs*stride] * kernel[k]);
}
accum /= divisor;
if (accum > 255) accum = 255;
if (accum < 0) accum = 0;
*out = (unsigned char)accum;
in += stride;
out += stride;
}
}
}
void ApplySeparableFilter(lua_State *L, cairo_surface_t *surf, int *kernel, int kernel_size, int divisor)
{
// Get surface properties
cairo_surface_flush(surf);
int width = cairo_image_surface_get_width(surf);
int height = cairo_image_surface_get_height(surf);
ptrdiff_t stride = (ptrdiff_t)cairo_image_surface_get_stride(surf);
cairo_format_t format = cairo_image_surface_get_format(surf);
unsigned char *data = cairo_image_surface_get_data(surf);
if (format != CAIRO_FORMAT_ARGB32 && format != CAIRO_FORMAT_RGB24 && format != CAIRO_FORMAT_A8) {
lua_pushliteral(L, "Unsupported image format for raster operation");
lua_error(L);
}
// Work image
unsigned char *wimg = new unsigned char[height*stride];
// Do the filtering
if (format == CAIRO_FORMAT_ARGB32 || format == CAIRO_FORMAT_RGB24) {
// Horizontal
SeparableFilterX<4>(data+0, wimg+0, width, height, stride, kernel, kernel_size, divisor);
SeparableFilterX<4>(data+1, wimg+1, width, height, stride, kernel, kernel_size, divisor);
SeparableFilterX<4>(data+2, wimg+2, width, height, stride, kernel, kernel_size, divisor);
SeparableFilterX<4>(data+3, wimg+3, width, height, stride, kernel, kernel_size, divisor);
// Vertical
//memcpy(data, wimg, height*stride);
SeparableFilterY(wimg, data, width*4, height, stride, kernel, kernel_size, divisor);
} else if (format == CAIRO_FORMAT_A8) {
// Horizontal
SeparableFilterX<1>(data, wimg, width, height, stride, kernel, kernel_size, divisor);
// Vertical
SeparableFilterY(wimg, data, width, height, stride, kernel, kernel_size, divisor);
}
// Clean up
cairo_surface_mark_dirty(surf);
delete[] wimg;
}
// raster.gaussian_blur(imgsurf, sigma)
static int gaussian_blur(lua_State *L)
{
// Get arguments
cairo_surface_t *surf = CheckSurface(L, 1);
double sigma = luaL_checknumber(L, 2);
// Generate gaussian kernel
int kernel_size = (int)(sigma*3 + 0.5) | 1; // binary-or with 1 to make sure the number is odd
if (kernel_size < 3) kernel_size = 3;
int *kernel = new int[kernel_size];
kernel[kernel_size/2] = (int)(NormalDist(sigma, 0) * 255);
int ksum = kernel[kernel_size/2];
for (int x = kernel_size/2-1; x >= 0; x--) {
int val = (int)(NormalDist(sigma, kernel_size/2-x) * 255 + 0.5);
ksum += val*2;
kernel[x] = val;
kernel[kernel_size - x - 1] = val;
}
ApplySeparableFilter(L, surf, kernel, kernel_size, ksum);
delete[] kernel;
return 0;
}
// n tap box blur
static int box_blur(lua_State *L)
{
cairo_surface_t *surf = CheckSurface(L, 1);
int width = luaL_checkint(L, 2);
if (width <= 0) { luaL_error(L, "Width of box kernel for raster.box must be larger than zero, specified to %d.", width); return 0; }
int applications = luaL_optint(L, 3, 1);
int *kernel = new int[width];
for (int i = 0; i < width; i++)
kernel[i] = 1;
while (applications-- > 0)
ApplySeparableFilter(L, surf, kernel, width, width);
delete[] kernel;
return 0;
}
static int invert_image(lua_State *L)
{
cairo_surface_t *surf = CheckSurface(L, 1);
cairo_surface_flush(surf);
int width = cairo_image_surface_get_width(surf);
int height = cairo_image_surface_get_height(surf);
ptrdiff_t stride = (ptrdiff_t)cairo_image_surface_get_stride(surf);
unsigned char *data = cairo_image_surface_get_data(surf);
cairo_format_t format = cairo_image_surface_get_format(surf);
// ARGB32 and RGB24 must be treated differently due to the premultipled alpha in ARGB32
// Also the alpha in ARGB32 is not inverted, only the colour channels
if (format == CAIRO_FORMAT_ARGB32) {
#pragma omp parallel for
for (int y = 0; y < height; y++) {
uint32_t *line = (uint32_t*)(data + y*stride);
for (int x = 0; x < width; x++, line++) {
// The R, G and B channels are in range 0..a, and inverting means calculating
// max-current, so inversion here is a-c for each channel.
unsigned char a = (*line & 0xff000000) >> 24;
unsigned char r = (*line & 0x00ff0000) >> 16;
unsigned char g = (*line & 0x0000ff00) >> 8;
unsigned char b = *line & 0x000000ff;
*line = (a<<24) | ((a-r)<<16) | ((a-g)<<8) | (a-b);
}
}
}
else if (format == CAIRO_FORMAT_RGB24) {
#pragma omp parallel for
for (int y = 0; y < height; y++) {
uint32_t *line = (uint32_t*)(data + y*stride);
for (int x = 0; x < width; x++, line++) {
// Invert R, G and B channels by XOR'ing them with 0xff each.
*line = *line ^ 0x00ffffff;
}
}
}
else if (format == CAIRO_FORMAT_A8) {
#pragma omp parallel for
for (int y = 0; y < height; y++) {
unsigned char *line = data + y*stride;
for (int x = 0; x < width; x++, line++) {
*line = ~ *line;
}
}
}
else if (format == CAIRO_FORMAT_A1) {
int lwidth = (width + 31) / 32; // "long-width", width in 32 bit longints, rounded up
#pragma omp parallel for
for (int y = 0; y < height; y++) {
// Pixels are stored packed into 32 bit quantities
uint32_t *line = (uint32_t*)(data + y*stride);
for (int x = 0; x < lwidth; x++, line++) {
// Yes this means we might end up inverting too many bits.. hopefully won't happen.
// (And even then, who would seriously use A1 surfaces?)
*line = ~ *line;
}
}
}
cairo_surface_mark_dirty(surf);
return 0;
}
static int separable_filter(lua_State *L)
{
cairo_surface_t *surf = CheckSurface(L, 1);
if (!lua_istable(L, 2)) {
luaL_error(L, "Expected table as second argument to raster.separable_filter, got %s", luaL_typename(L, 2));
return 0;
}
int divisor = luaL_checkint(L, 3);
int width = (int)lua_objlen(L, 2);
if (width < 1) {
luaL_error(L, "Cannot apply empty filter");
return 0;
}
int *kernel = new int[width];
int i = 0;
lua_pushnil(L);
while (lua_next(L, 2)) {
if (lua_isnumber(L, -1)) {
kernel[i] = (int)lua_tointeger(L, -1);
}
i++;
lua_pop(L, 1);
}
ApplySeparableFilter(L, surf, kernel, width, divisor);
delete[] kernel;
return 0;
}
// Registration
static luaL_Reg rasterlib[] = {
{"gaussian_blur", gaussian_blur}, {"box_blur", box_blur},
{"separable_filter", separable_filter},
{"invert", invert_image},
{NULL, NULL}
};
int luaopen_raster(lua_State *L)
{
luaL_register(L, "raster", rasterlib);
return 0;
}