A layered image editor for Linux and Omarchy, ported from Compositor (github.com/robbietilton/Compositor), whose Swift source is its specification. It reads and writes Compositor's .comp projects. Packages for Ubuntu, Fedora and Arch come from the release workflow; scripts/install/ holds one install script per distribution family, and flake.nix serves NixOS. Co-Authored-By: Claude <noreply@anthropic.com>
260 lines
12 KiB
C
260 lines
12 KiB
C
#include "HealPixels.h"
|
|
#include <math.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
enum { OUTSIDE = 0, RING = 1, HOLE = 2 };
|
|
|
|
void heal_coverage_bounds(const uint8_t *gray, size_t width, size_t height, size_t stride, long bounds[4]) {
|
|
long x0 = (long)width, y0 = (long)height, x1 = 0, y1 = 0;
|
|
for (size_t y = 0; y < height; ++y) {
|
|
const uint8_t *row = gray + y * stride;
|
|
for (size_t x = 0; x < width; ++x) {
|
|
if (!row[x]) continue;
|
|
if ((long)x < x0) x0 = (long)x;
|
|
if ((long)x + 1 > x1) x1 = (long)x + 1;
|
|
if ((long)y < y0) y0 = (long)y;
|
|
if ((long)y + 1 > y1) y1 = (long)y + 1;
|
|
}
|
|
}
|
|
if (x1 <= x0 || y1 <= y0) x0 = y0 = x1 = y1 = 0;
|
|
bounds[0] = x0; bounds[1] = y0; bounds[2] = x1; bounds[3] = y1;
|
|
}
|
|
|
|
static inline uint32_t heal_hash(uint32_t x) {
|
|
x ^= x >> 16; x *= 0x7feb352dU;
|
|
x ^= x >> 15; x *= 0x846ca68bU;
|
|
x ^= x >> 16;
|
|
return x;
|
|
}
|
|
static inline double heal_unit(uint32_t key) { return (double)(heal_hash(key) >> 8) / 16777216.0; }
|
|
|
|
// Mean squared difference between the ring around the spot and the ring around the patch
|
|
// offset by (dx, dy). Infinite when the patch would overlap the spot or leave the image.
|
|
static double heal_score(const uint8_t *rgba, size_t stride, const uint8_t *role, long wx0, long wy0,
|
|
long ww, long wh, long dx, long dy, long W, long H) {
|
|
if (labs(dx) < ww && labs(dy) < wh) return INFINITY;
|
|
if (wx0 + dx < 0 || wy0 + dy < 0 || wx0 + ww + dx > W || wy0 + wh + dy > H) return INFINITY;
|
|
double sum = 0;
|
|
long n = 0;
|
|
for (long y = 0; y < wh; ++y) {
|
|
for (long x = 0; x < ww; ++x) {
|
|
if (role[y * ww + x] != RING) continue;
|
|
const uint8_t *t = rgba + (size_t)(wy0 + y) * stride + (size_t)(wx0 + x) * 4;
|
|
const uint8_t *s = rgba + (size_t)(wy0 + y + dy) * stride + (size_t)(wx0 + x + dx) * 4;
|
|
for (int c = 0; c < 4; ++c) { double d = (double)t[c] - s[c]; sum += d * d; }
|
|
++n;
|
|
}
|
|
}
|
|
return n ? sum / n : INFINITY;
|
|
}
|
|
|
|
// Solves for smooth values over HOLE pixels, fixed to the RING values around them. A coarser
|
|
// copy is solved first and used as the starting point, so large spots settle in few passes.
|
|
static void heal_solve(float *value, const uint8_t *role, long w, long h, int depth) {
|
|
int iterations = 300;
|
|
if (w > 32 && h > 32 && depth < 16) {
|
|
long cw = (w + 1) / 2, ch = (h + 1) / 2;
|
|
float *coarse = calloc((size_t)(cw * ch * 4), sizeof(float));
|
|
uint8_t *coarseRole = calloc((size_t)(cw * ch), 1);
|
|
if (coarse && coarseRole) {
|
|
for (long y = 0; y < ch; ++y) {
|
|
for (long x = 0; x < cw; ++x) {
|
|
int known = 0, hole = 0;
|
|
float knownSum[4] = {0, 0, 0, 0}, holeSum[4] = {0, 0, 0, 0};
|
|
for (long j = 0; j < 2; ++j) {
|
|
for (long i = 0; i < 2; ++i) {
|
|
long fx = x * 2 + i, fy = y * 2 + j;
|
|
if (fx >= w || fy >= h) continue;
|
|
long p = fy * w + fx;
|
|
if (role[p] == RING) { ++known; for (int c = 0; c < 4; ++c) knownSum[c] += value[p * 4 + c]; }
|
|
else if (role[p] == HOLE) { ++hole; for (int c = 0; c < 4; ++c) holeSum[c] += value[p * 4 + c]; }
|
|
}
|
|
}
|
|
long q = y * cw + x;
|
|
if (known) { coarseRole[q] = RING; for (int c = 0; c < 4; ++c) coarse[q * 4 + c] = knownSum[c] / known; }
|
|
else if (hole) { coarseRole[q] = HOLE; for (int c = 0; c < 4; ++c) coarse[q * 4 + c] = holeSum[c] / hole; }
|
|
}
|
|
}
|
|
heal_solve(coarse, coarseRole, cw, ch, depth + 1);
|
|
for (long y = 0; y < h; ++y) {
|
|
for (long x = 0; x < w; ++x) {
|
|
long p = y * w + x, q = (y / 2) * cw + x / 2;
|
|
if (role[p] == HOLE && coarseRole[q] == HOLE) memcpy(value + p * 4, coarse + q * 4, 4 * sizeof(float));
|
|
}
|
|
}
|
|
iterations = 40;
|
|
}
|
|
free(coarse);
|
|
free(coarseRole);
|
|
}
|
|
const float omega = 1.8f;
|
|
for (int it = 0; it < iterations; ++it) {
|
|
for (long y = 0; y < h; ++y) {
|
|
for (long x = 0; x < w; ++x) {
|
|
long p = y * w + x;
|
|
if (role[p] != HOLE) continue;
|
|
float sum[4] = {0, 0, 0, 0};
|
|
int n = 0;
|
|
long neighbors[4][2] = {{x - 1, y}, {x + 1, y}, {x, y - 1}, {x, y + 1}};
|
|
for (int k = 0; k < 4; ++k) {
|
|
long nx = neighbors[k][0], ny = neighbors[k][1];
|
|
if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
|
|
long q = ny * w + nx;
|
|
if (role[q] == OUTSIDE) continue;
|
|
for (int c = 0; c < 4; ++c) sum[c] += value[q * 4 + c];
|
|
++n;
|
|
}
|
|
if (!n) continue;
|
|
for (int c = 0; c < 4; ++c) value[p * 4 + c] += omega * (sum[c] / n - value[p * 4 + c]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int spot_heal(uint8_t *rgba, const uint8_t *coverage, size_t width, size_t height, size_t stride,
|
|
float opacity, int mode, uint32_t seed) {
|
|
long W = (long)width, H = (long)height;
|
|
long bounds[4];
|
|
heal_coverage_bounds(coverage, width, height, width, bounds);
|
|
if (bounds[2] <= bounds[0]) return 0;
|
|
long bw = bounds[2] - bounds[0], bh = bounds[3] - bounds[1], size = bw > bh ? bw : bh;
|
|
long ring = size / 8;
|
|
if (ring < 2) ring = 2;
|
|
if (ring > 16) ring = 16;
|
|
// Work box: the spot plus its ring, clipped to the image.
|
|
long wx0 = bounds[0] - ring < 0 ? 0 : bounds[0] - ring, wy0 = bounds[1] - ring < 0 ? 0 : bounds[1] - ring;
|
|
long wx1 = bounds[2] + ring > W ? W : bounds[2] + ring, wy1 = bounds[3] + ring > H ? H : bounds[3] + ring;
|
|
long ww = wx1 - wx0, wh = wy1 - wy0, wn = ww * wh;
|
|
|
|
uint8_t *role = calloc((size_t)wn, 1), *near = calloc((size_t)wn, 1);
|
|
long *prefix = malloc((size_t)((ww > wh ? ww : wh) + 1) * sizeof(long));
|
|
float *value = malloc((size_t)wn * 4 * sizeof(float));
|
|
int status = -1;
|
|
if (!role || !near || !prefix || !value) goto done;
|
|
for (long y = 0; y < wh; ++y)
|
|
for (long x = 0; x < ww; ++x)
|
|
role[y * ww + x] = coverage[(size_t)(wy0 + y) * width + (size_t)(wx0 + x)] ? HOLE : OUTSIDE;
|
|
// The ring: pixels within `ring` of the spot (a square dilation, row pass then column pass).
|
|
for (long y = 0; y < wh; ++y) {
|
|
prefix[0] = 0;
|
|
for (long x = 0; x < ww; ++x) prefix[x + 1] = prefix[x] + (role[y * ww + x] == HOLE);
|
|
for (long x = 0; x < ww; ++x) {
|
|
long lo = x - ring < 0 ? 0 : x - ring, hi = x + ring + 1 > ww ? ww : x + ring + 1;
|
|
near[y * ww + x] = prefix[hi] - prefix[lo] > 0;
|
|
}
|
|
}
|
|
for (long x = 0; x < ww; ++x) {
|
|
prefix[0] = 0;
|
|
for (long y = 0; y < wh; ++y) prefix[y + 1] = prefix[y] + near[y * ww + x];
|
|
for (long y = 0; y < wh; ++y) {
|
|
long lo = y - ring < 0 ? 0 : y - ring, hi = y + ring + 1 > wh ? wh : y + ring + 1;
|
|
if (role[y * ww + x] == OUTSIDE && prefix[hi] - prefix[lo] > 0) role[y * ww + x] = RING;
|
|
}
|
|
}
|
|
long ringCount = 0;
|
|
for (long p = 0; p < wn; ++p) ringCount += role[p] == RING;
|
|
if (!ringCount) { status = 0; goto done; }
|
|
|
|
// Source patch for Content-Aware and Proximity Match.
|
|
long ox = 0, oy = 0;
|
|
int haveSource = 0;
|
|
if (mode != 1) {
|
|
static const double factors[5] = {1.05, 1.35, 1.75, 2.25, 2.8};
|
|
int count = mode == 2 ? 2 : 5;
|
|
double best = INFINITY;
|
|
for (int f = 0; f < count; ++f) {
|
|
for (int a = 0; a < 24; ++a) {
|
|
double angle = a * M_PI / 12.0;
|
|
long dx = lround(cos(angle) * factors[f] * ww), dy = lround(sin(angle) * factors[f] * wh);
|
|
double score = heal_score(rgba, stride, role, wx0, wy0, ww, wh, dx, dy, W, H);
|
|
if (!isfinite(score)) continue;
|
|
score *= mode == 2 ? 1.0 + 0.6 * f : 1.0 + 0.1 * f; // nearer patches win ties
|
|
if (score < best) { best = score; ox = dx; oy = dy; }
|
|
}
|
|
}
|
|
if (isfinite(best)) {
|
|
// Fine-tune the alignment so repeating texture lines up.
|
|
long cx = ox, cy = oy;
|
|
double refined = heal_score(rgba, stride, role, wx0, wy0, ww, wh, cx, cy, W, H);
|
|
for (long j = -3; j <= 3; ++j) {
|
|
for (long i = -3; i <= 3; ++i) {
|
|
double score = heal_score(rgba, stride, role, wx0, wy0, ww, wh, cx + i, cy + j, W, H);
|
|
if (score < refined) { refined = score; ox = cx + i; oy = cy + j; }
|
|
}
|
|
}
|
|
haveSource = 1;
|
|
}
|
|
}
|
|
|
|
// Membrane: the edge difference between the original and the patch (or the original itself
|
|
// for a smooth fill), spread across the spot.
|
|
double mean[4] = {0, 0, 0, 0}, detail[3] = {0, 0, 0};
|
|
for (long y = 0; y < wh; ++y) {
|
|
for (long x = 0; x < ww; ++x) {
|
|
long p = y * ww + x;
|
|
if (role[p] != RING) { memset(value + p * 4, 0, 4 * sizeof(float)); continue; }
|
|
long ix = wx0 + x, iy = wy0 + y;
|
|
const uint8_t *t = rgba + (size_t)iy * stride + (size_t)ix * 4;
|
|
const uint8_t *s = haveSource ? rgba + (size_t)(iy + oy) * stride + (size_t)(ix + ox) * 4 : NULL;
|
|
for (int c = 0; c < 4; ++c) {
|
|
value[p * 4 + c] = (float)t[c] - (s ? s[c] : 0);
|
|
mean[c] += value[p * 4 + c];
|
|
}
|
|
if (!haveSource) {
|
|
// Fine detail around the spot: each pixel against the average of its neighbours.
|
|
for (int c = 0; c < 3; ++c) {
|
|
double around = 0; int n = 0;
|
|
long offsets[4][2] = {{ix - 1, iy}, {ix + 1, iy}, {ix, iy - 1}, {ix, iy + 1}};
|
|
for (int k = 0; k < 4; ++k) {
|
|
if (offsets[k][0] < 0 || offsets[k][1] < 0 || offsets[k][0] >= W || offsets[k][1] >= H) continue;
|
|
around += rgba[(size_t)offsets[k][1] * stride + (size_t)offsets[k][0] * 4 + c];
|
|
++n;
|
|
}
|
|
if (n) { double d = t[c] - around / n; detail[c] += d * d; }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (int c = 0; c < 4; ++c) mean[c] /= ringCount;
|
|
for (long p = 0; p < wn; ++p)
|
|
if (role[p] == HOLE) for (int c = 0; c < 4; ++c) value[p * 4 + c] = (float)mean[c];
|
|
heal_solve(value, role, ww, wh, 0);
|
|
for (int c = 0; c < 3; ++c) detail[c] = sqrt(detail[c] / ringCount) * 0.9;
|
|
|
|
for (long y = 0; y < wh; ++y) {
|
|
for (long x = 0; x < ww; ++x) {
|
|
long p = y * ww + x;
|
|
if (role[p] != HOLE) continue;
|
|
long ix = wx0 + x, iy = wy0 + y;
|
|
uint8_t *t = rgba + (size_t)iy * stride + (size_t)ix * 4;
|
|
const uint8_t *s = haveSource ? rgba + (size_t)(iy + oy) * stride + (size_t)(ix + ox) * 4 : NULL;
|
|
double amount = coverage[(size_t)iy * width + (size_t)ix] / 255.0 * opacity;
|
|
double grain = 0;
|
|
if (!haveSource) {
|
|
uint32_t key = heal_hash(seed ^ heal_hash((uint32_t)(iy * W + ix)));
|
|
double u1 = heal_unit(key), u2 = heal_unit(key ^ 0x68e31da4U);
|
|
grain = sqrt(-2.0 * log(1.0 - u1)) * cos(2.0 * M_PI * u2);
|
|
}
|
|
double out[4];
|
|
for (int c = 0; c < 4; ++c) {
|
|
double healed = (s ? s[c] : 0) + value[p * 4 + c] + (c < 3 ? grain * detail[c] : 0);
|
|
out[c] = t[c] + (healed - t[c]) * amount;
|
|
}
|
|
double alpha = out[3] < 0 ? 0 : out[3] > 255 ? 255 : out[3];
|
|
t[3] = (uint8_t)lround(alpha);
|
|
for (int c = 0; c < 3; ++c) {
|
|
double v = out[c] < 0 ? 0 : out[c] > t[3] ? t[3] : out[c];
|
|
t[c] = (uint8_t)lround(v);
|
|
}
|
|
}
|
|
}
|
|
status = 0;
|
|
done:
|
|
free(role);
|
|
free(near);
|
|
free(prefix);
|
|
free(value);
|
|
return status;
|
|
}
|