Skip to content

Commit bd9ad52

Browse files
committed
fix: preserve alpha in perfect pixel
1 parent 270bc77 commit bd9ad52

1 file changed

Lines changed: 76 additions & 38 deletions

File tree

frontend/src/lib/perfectPixel/perfectPixel.ts

Lines changed: 76 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,24 @@ export interface PerfectPixelOptions {
1717
}
1818

1919
const FFT_THUMB_MAX = 512
20+
const TRANSPARENT_ALPHA = 8
2021

21-
function rgbaToRgbFloat(imageData: ImageData): { r: Float32Array; g: Float32Array; b: Float32Array; w: number; h: number } {
22+
function rgbaToRgbaFloat(imageData: ImageData): { r: Float32Array; g: Float32Array; b: Float32Array; a: Float32Array; w: number; h: number } {
2223
const w = imageData.width
2324
const h = imageData.height
2425
const d = imageData.data
2526
const n = w * h
2627
const r = new Float32Array(n)
2728
const g = new Float32Array(n)
2829
const b = new Float32Array(n)
30+
const a = new Float32Array(n)
2931
for (let i = 0, p = 0; i < n; i++, p += 4) {
3032
r[i] = d[p]!
3133
g[i] = d[p + 1]!
3234
b[i] = d[p + 2]!
35+
a[i] = d[p + 3]!
3336
}
34-
return { r, g, b, w, h }
37+
return { r, g, b, a, w, h }
3538
}
3639

3740
function rgbToGrayFloat32(r: Float32Array, g: Float32Array, b: Float32Array, n: number): Float32Array {
@@ -419,6 +422,7 @@ function sampleCenter(
419422
r: Float32Array,
420423
g: Float32Array,
421424
b: Float32Array,
425+
a: Float32Array,
422426
W: number,
423427
H: number,
424428
xCoords: number[],
@@ -439,10 +443,18 @@ function sampleCenter(
439443
Math.max(0, Math.round((xCoords[i]! + xCoords[i + 1]!) * 0.5)),
440444
)
441445
const si = cy * W + cx
442-
data[o++] = r[si]!
443-
data[o++] = g[si]!
444-
data[o++] = b[si]!
445-
data[o++] = 255
446+
const aa = a[si]!
447+
if (aa <= TRANSPARENT_ALPHA) {
448+
data[o++] = 0
449+
data[o++] = 0
450+
data[o++] = 0
451+
data[o++] = 0
452+
} else {
453+
data[o++] = r[si]!
454+
data[o++] = g[si]!
455+
data[o++] = b[si]!
456+
data[o++] = aa
457+
}
446458
}
447459
}
448460
return new ImageData(data, nx, ny)
@@ -452,6 +464,7 @@ function sampleMedian(
452464
r: Float32Array,
453465
g: Float32Array,
454466
b: Float32Array,
467+
a: Float32Array,
455468
W: number,
456469
H: number,
457470
xCoords: number[],
@@ -460,7 +473,10 @@ function sampleMedian(
460473
const nx = xCoords.length - 1
461474
const ny = yCoords.length - 1
462475
const data = new Uint8ClampedArray(nx * ny * 4)
463-
const buf: number[] = []
476+
const rs: number[] = []
477+
const gs: number[] = []
478+
const bs: number[] = []
479+
const alphas: number[] = []
464480
let o = 0
465481
for (let j = 0; j < ny; j++) {
466482
const y0 = Math.max(0, Math.min(H, Math.floor(yCoords[j]!)))
@@ -470,36 +486,47 @@ function sampleMedian(
470486
const x0 = Math.max(0, Math.min(W, Math.floor(xCoords[i]!)))
471487
let x1 = Math.max(0, Math.min(W, Math.floor(xCoords[i + 1]!)))
472488
if (x1 <= x0) x1 = Math.min(x0 + 1, W)
473-
buf.length = 0
489+
rs.length = 0
490+
gs.length = 0
491+
bs.length = 0
492+
alphas.length = 0
474493
for (let y = y0; y < y1; y++) {
475494
for (let x = x0; x < x1; x++) {
476495
const si = y * W + x
477-
buf.push(r[si]!, g[si]!, b[si]!)
496+
const aa = a[si]!
497+
alphas.push(aa)
498+
if (aa > TRANSPARENT_ALPHA) {
499+
rs.push(r[si]!)
500+
gs.push(g[si]!)
501+
bs.push(b[si]!)
502+
}
478503
}
479504
}
480-
if (buf.length === 0) {
505+
if (alphas.length === 0 || rs.length === 0) {
481506
data[o] = 0
482507
data[o + 1] = 0
483508
data[o + 2] = 0
484-
data[o + 3] = 255
509+
data[o + 3] = 0
485510
o += 4
486511
continue
487512
}
488-
const rs: number[] = []
489-
const gs: number[] = []
490-
const bs: number[] = []
491-
for (let k = 0; k < buf.length; k += 3) {
492-
rs.push(buf[k]!)
493-
gs.push(buf[k + 1]!)
494-
bs.push(buf[k + 2]!)
513+
alphas.sort((x, y) => x - y)
514+
const alpha = Math.round(medianSorted(alphas))
515+
if (alpha <= TRANSPARENT_ALPHA) {
516+
data[o] = 0
517+
data[o + 1] = 0
518+
data[o + 2] = 0
519+
data[o + 3] = 0
520+
o += 4
521+
continue
495522
}
496-
rs.sort((a, b) => a - b)
497-
gs.sort((a, b) => a - b)
498-
bs.sort((a, b) => a - b)
523+
rs.sort((x, y) => x - y)
524+
gs.sort((x, y) => x - y)
525+
bs.sort((x, y) => x - y)
499526
data[o++] = Math.round(medianSorted(rs))
500527
data[o++] = Math.round(medianSorted(gs))
501528
data[o++] = Math.round(medianSorted(bs))
502-
data[o++] = 255
529+
data[o++] = alpha
503530
}
504531
}
505532
return new ImageData(data, nx, ny)
@@ -587,6 +614,7 @@ function sampleMajority(
587614
r: Float32Array,
588615
g: Float32Array,
589616
b: Float32Array,
617+
a: Float32Array,
590618
W: number,
591619
H: number,
592620
xCoords: number[],
@@ -597,7 +625,8 @@ function sampleMajority(
597625
const ny = yCoords.length - 1
598626
const data = new Uint8ClampedArray(nx * ny * 4)
599627
const cellBuf = new Float32Array(maxSamples * 3)
600-
const pixelList: number[] = []
628+
const visiblePixels: number[] = []
629+
const alphas: number[] = []
601630
let o = 0
602631

603632
for (let j = 0; j < ny; j++) {
@@ -608,43 +637,52 @@ function sampleMajority(
608637
const x0 = Math.max(0, Math.min(W, Math.floor(xCoords[i]!)))
609638
let x1 = Math.max(0, Math.min(W, Math.floor(xCoords[i + 1]!)))
610639
if (x1 <= x0) x1 = Math.min(x0 + 1, W)
611-
pixelList.length = 0
640+
visiblePixels.length = 0
641+
alphas.length = 0
642+
let totalPixels = 0
612643
for (let y = y0; y < y1; y++) {
613644
for (let x = x0; x < x1; x++) {
614-
pixelList.push(y * W + x)
645+
totalPixels++
646+
const si = y * W + x
647+
const aa = a[si]!
648+
if (aa > TRANSPARENT_ALPHA) {
649+
visiblePixels.push(si)
650+
alphas.push(aa)
651+
}
615652
}
616653
}
617-
const np = pixelList.length
618-
if (np === 0) {
654+
const visibleN = visiblePixels.length
655+
if (totalPixels === 0 || visibleN === 0 || visibleN * 2 < totalPixels) {
619656
data[o] = 0
620657
data[o + 1] = 0
621658
data[o + 2] = 0
622-
data[o + 3] = 255
659+
data[o + 3] = 0
623660
o += 4
624661
continue
625662
}
626-
let useN = np
627-
if (np > maxSamples) {
663+
let useN = visibleN
664+
if (visibleN > maxSamples) {
628665
useN = maxSamples
629666
for (let s = 0; s < useN; s++) {
630-
const pick = pixelList[Math.floor(Math.random() * np)]!
667+
const pick = visiblePixels[Math.floor(Math.random() * visibleN)]!
631668
cellBuf[s * 3] = r[pick]!
632669
cellBuf[s * 3 + 1] = g[pick]!
633670
cellBuf[s * 3 + 2] = b[pick]!
634671
}
635672
} else {
636-
for (let s = 0; s < np; s++) {
637-
const pick = pixelList[s]!
673+
for (let s = 0; s < visibleN; s++) {
674+
const pick = visiblePixels[s]!
638675
cellBuf[s * 3] = r[pick]!
639676
cellBuf[s * 3 + 1] = g[pick]!
640677
cellBuf[s * 3 + 2] = b[pick]!
641678
}
642679
}
643680
const [rr, gg, bb] = kmeans2RgbCell(cellBuf.subarray(0, useN * 3), useN)
681+
alphas.sort((x, y) => x - y)
644682
data[o++] = rr
645683
data[o++] = gg
646684
data[o++] = bb
647-
data[o++] = 255
685+
data[o++] = Math.round(medianSorted(alphas))
648686
}
649687
}
650688
return new ImageData(data, nx, ny)
@@ -749,7 +787,7 @@ export function getPerfectPixel(imageData: ImageData, opts: PerfectPixelOptions
749787
const refineIntensity = opts.refineIntensity ?? 0.25
750788
const fixSquare = opts.fixSquare ?? true
751789

752-
const { r, g, b, w, h } = rgbaToRgbFloat(imageData)
790+
const { r, g, b, a, w, h } = rgbaToRgbaFloat(imageData)
753791
const gray = rgbToGrayFloat32(r, g, b, w * h)
754792

755793
let sizeX: number
@@ -770,11 +808,11 @@ export function getPerfectPixel(imageData: ImageData, opts: PerfectPixelOptions
770808

771809
let out: ImageData
772810
if (sampleMethod === 'majority') {
773-
out = sampleMajority(r, g, b, W, H, xCoords, yCoords)
811+
out = sampleMajority(r, g, b, a, W, H, xCoords, yCoords)
774812
} else if (sampleMethod === 'median') {
775-
out = sampleMedian(r, g, b, W, H, xCoords, yCoords)
813+
out = sampleMedian(r, g, b, a, W, H, xCoords, yCoords)
776814
} else {
777-
out = sampleCenter(r, g, b, W, H, xCoords, yCoords)
815+
out = sampleCenter(r, g, b, a, W, H, xCoords, yCoords)
778816
}
779817

780818
return fixSquareOutput(out, fixSquare)

0 commit comments

Comments
 (0)