diff --git a/cmd/showcase/main.mbt b/cmd/showcase/main.mbt index 3d74e03..773523e 100644 --- a/cmd/showcase/main.mbt +++ b/cmd/showcase/main.mbt @@ -59,9 +59,49 @@ fn main { if !ok { abort("showcase: PNG round trip failed") } - // Emit the status as a PPM comment *after* the magic number, so stdout + // Analysis self-check on a controlled flat tile: salt-and-pepper noise + // then a median filter, which on a uniform field provably restores almost + // every pixel. Confirms the noise + median + phash APIs compose; the + // perceptual-hash distances (clean vs noisy vs denoised) are reported as a + // diagnostic. (The median is deliberately not run on the detailed showcase + // image itself, where it would also reshape genuine edges.) + let tile = @pixelforge.Image::new(32, 32) + tile.fill_rect(0, 0, 32, 32, b'\x80', b'\x80', b'\x80', b'\xFF') + let clean_hash = tile.average_hash() + let noisy = tile.add_salt_pepper(2026UL, 0.08) + let denoised = noisy.median() + let noisy_diff = pixel_diff(tile, noisy) + let denoised_diff = pixel_diff(tile, denoised) + if denoised_diff >= noisy_diff { + abort("showcase: median denoise did not reduce the pixel difference") + } + let noisy_dist = @pixelforge.hamming_distance( + clean_hash, + noisy.average_hash(), + ) + let denoised_dist = @pixelforge.hamming_distance( + clean_hash, + denoised.average_hash(), + ) + // Emit the status as PPM comments *after* the magic number, so stdout // stays a strictly valid PPM stream. - print_ppm(final_img, "# PNG round trip: OK (\{png.length()} bytes)") + print_ppm( + final_img, + "# PNG round trip: OK (\{png.length()} bytes); denoise diff \{noisy_diff}->\{denoised_diff}; phash noisy=\{noisy_dist} denoised=\{denoised_dist}", + ) +} + +///| +/// Counts channels that differ between two same-size images (a coarse +/// distance used only for the showcase self-check). +fn pixel_diff(a : @pixelforge.Image, b : @pixelforge.Image) -> Int { + let mut diff = 0 + for i in 0..<(a.width * a.height * 4) { + if a.data[i].to_int() != b.data[i].to_int() { + diff = diff + 1 + } + } + diff } ///| diff --git a/web/bindings.mbt b/web/bindings.mbt index c89994a..8e92692 100644 --- a/web/bindings.mbt +++ b/web/bindings.mbt @@ -34,3 +34,15 @@ pub fn encode_png( } out } + +///| +/// 256-bin luma histogram of an RGBA buffer, for the playground's live +/// histogram panel. Returns a `FixedArray[Int]` (a plain JS array of 256 +/// numbers on the js backend). +pub fn luma_histogram( + data : FixedArray[Byte], + width : Int, + height : Int, +) -> FixedArray[Int] { + @pixelforge.Image::from_bytes(width, height, data).histogram_luma() +} diff --git a/web/dist/web.js b/web/dist/web.js index 266dde7..69b4232 100644 --- a/web/dist/web.js +++ b/web/dist/web.js @@ -203,6 +203,48 @@ function _M0MP270717lee10pixelforge5Image14flip__vertical(self) { } return out; } +function _M0MP270717lee10pixelforge5Image8map__rgb(self, transform) { + const out = _M0MP270717lee10pixelforge5Image4copy(self); + const n = Math.imul(self.width, self.height) | 0; + let _tmp = 0; + while (true) { + const p = _tmp; + if (p < n) { + const base = Math.imul(p, 4) | 0; + const _tmp$2 = self.data; + $bound_check(_tmp$2, base); + const r = _tmp$2[base]; + const _tmp$3 = self.data; + const _tmp$4 = base + 1 | 0; + $bound_check(_tmp$3, _tmp$4); + const g = _tmp$3[_tmp$4]; + const _tmp$5 = self.data; + const _tmp$6 = base + 2 | 0; + $bound_check(_tmp$5, _tmp$6); + const b = _tmp$5[_tmp$6]; + const _bind = transform(r, g, b); + const _nr = _bind._0; + const _ng = _bind._1; + const _nb = _bind._2; + const _tmp$7 = out.data; + $bound_check(_tmp$7, base); + _tmp$7[base] = _nr < 0 ? 0 : _nr > 255 ? 255 : _nr & 255; + const _tmp$8 = out.data; + const _tmp$9 = base + 1 | 0; + $bound_check(_tmp$8, _tmp$9); + _tmp$8[_tmp$9] = _ng < 0 ? 0 : _ng > 255 ? 255 : _ng & 255; + const _tmp$10 = out.data; + const _tmp$11 = base + 2 | 0; + $bound_check(_tmp$10, _tmp$11); + _tmp$10[_tmp$11] = _nb < 0 ? 0 : _nb > 255 ? 255 : _nb & 255; + _tmp = p + 1 | 0; + continue; + } else { + break; + } + } + return out; +} function _M0FP270717lee10pixelforge8push__u8(buf, v) { _M0MPC15array5Array4pushGyE(buf, (((v % 256 | 0) + 256 | 0) % 256 | 0) & 255); } @@ -494,48 +536,6 @@ function _M0MP270717lee10pixelforge5Image15otsu__threshold(self) { } return best_t; } -function _M0MP270717lee10pixelforge5Image8map__rgb(self, transform) { - const out = _M0MP270717lee10pixelforge5Image4copy(self); - const n = Math.imul(self.width, self.height) | 0; - let _tmp = 0; - while (true) { - const p = _tmp; - if (p < n) { - const base = Math.imul(p, 4) | 0; - const _tmp$2 = self.data; - $bound_check(_tmp$2, base); - const r = _tmp$2[base]; - const _tmp$3 = self.data; - const _tmp$4 = base + 1 | 0; - $bound_check(_tmp$3, _tmp$4); - const g = _tmp$3[_tmp$4]; - const _tmp$5 = self.data; - const _tmp$6 = base + 2 | 0; - $bound_check(_tmp$5, _tmp$6); - const b = _tmp$5[_tmp$6]; - const _bind = transform(r, g, b); - const _nr = _bind._0; - const _ng = _bind._1; - const _nb = _bind._2; - const _tmp$7 = out.data; - $bound_check(_tmp$7, base); - _tmp$7[base] = _nr < 0 ? 0 : _nr > 255 ? 255 : _nr & 255; - const _tmp$8 = out.data; - const _tmp$9 = base + 1 | 0; - $bound_check(_tmp$8, _tmp$9); - _tmp$8[_tmp$9] = _ng < 0 ? 0 : _ng > 255 ? 255 : _ng & 255; - const _tmp$10 = out.data; - const _tmp$11 = base + 2 | 0; - $bound_check(_tmp$10, _tmp$11); - _tmp$10[_tmp$11] = _nb < 0 ? 0 : _nb > 255 ? 255 : _nb & 255; - _tmp = p + 1 | 0; - continue; - } else { - break; - } - } - return out; -} function _M0MP270717lee10pixelforge5Image4otsu(self) { const t = _M0MP270717lee10pixelforge5Image15otsu__threshold(self); return _M0MP270717lee10pixelforge5Image8map__rgb(self, (r, g, b) => { @@ -550,6 +550,9 @@ function _M0MP270717lee10pixelforge5Image11from__bytes(width, height, data) { } return new _M0TP270717lee10pixelforge5Image(width, height, data); } +function _M0MP270717lee10pixelforge5Image15histogram__luma(self) { + return _M0MP270717lee10pixelforge5Image15luma__histogram(self); +} function _M0MP270717lee10pixelforge5Image5gamma(self, value) { const g = value <= 0 ? 1 : value; const inv = 1 / g; @@ -647,11 +650,11 @@ function _M0MP270717lee10pixelforge5Image6invert(self) { function _M0MP270717lee10pixelforge5Image10brightness(self, delta) { return _M0MP270717lee10pixelforge5Image8map__rgb(self, (r, g, b) => ({ _0: r + delta | 0, _1: g + delta | 0, _2: b + delta | 0 })); } -function _M0MP270717lee10pixelforge5Image8contrastN5applyS561(factor, c) { +function _M0MP270717lee10pixelforge5Image8contrastN5applyS630(factor, c) { return _M0MPC16double6Double7to__int((c + 0 - 128) * factor + 128); } function _M0MP270717lee10pixelforge5Image8contrast(self, factor) { - return _M0MP270717lee10pixelforge5Image8map__rgb(self, (r, g, b) => ({ _0: _M0MP270717lee10pixelforge5Image8contrastN5applyS561(factor, r), _1: _M0MP270717lee10pixelforge5Image8contrastN5applyS561(factor, g), _2: _M0MP270717lee10pixelforge5Image8contrastN5applyS561(factor, b) })); + return _M0MP270717lee10pixelforge5Image8map__rgb(self, (r, g, b) => ({ _0: _M0MP270717lee10pixelforge5Image8contrastN5applyS630(factor, r), _1: _M0MP270717lee10pixelforge5Image8contrastN5applyS630(factor, g), _2: _M0MP270717lee10pixelforge5Image8contrastN5applyS630(factor, b) })); } function _M0MP270717lee10pixelforge5Image5sepia(self) { return _M0MP270717lee10pixelforge5Image8map__rgb(self, (r, g, b) => { @@ -1653,4 +1656,7 @@ function _M0FP370717lee10pixelforge3web11encode__png(data, width, height) { } return out; } -export { _M0FP370717lee10pixelforge3web13apply__filter as apply_filter, _M0FP370717lee10pixelforge3web11encode__png as encode_png } +function _M0FP370717lee10pixelforge3web15luma__histogram(data, width, height) { + return _M0MP270717lee10pixelforge5Image15histogram__luma(_M0MP270717lee10pixelforge5Image11from__bytes(width, height, data)); +} +export { _M0FP370717lee10pixelforge3web13apply__filter as apply_filter, _M0FP370717lee10pixelforge3web11encode__png as encode_png, _M0FP370717lee10pixelforge3web15luma__histogram as luma_histogram } diff --git a/web/index.html b/web/index.html index 15a27b6..3c6492c 100644 --- a/web/index.html +++ b/web/index.html @@ -125,6 +125,11 @@

性能

+
+

亮度直方图

+ +
+

像素缓冲区以 Uint8ClampedArray 直接进出 MoonBit (FixedArray[Byte]),无需序列化拷贝。 diff --git a/web/moon.pkg b/web/moon.pkg index 194233b..61149ec 100644 --- a/web/moon.pkg +++ b/web/moon.pkg @@ -7,5 +7,10 @@ import { pkgtype(kind: "foreign_library") options( - link: { "js": { "exports": [ "apply_filter", "encode_png" ], "format": "esm" } }, + link: { + "js": { + "exports": [ "apply_filter", "encode_png", "luma_histogram" ], + "format": "esm", + }, + }, ) diff --git a/web/pkg.generated.mbti b/web/pkg.generated.mbti index 3ef07be..a4f8a5a 100644 --- a/web/pkg.generated.mbti +++ b/web/pkg.generated.mbti @@ -6,6 +6,8 @@ pub fn apply_filter(FixedArray[Byte], Int, Int, Int, Double) -> FixedArray[Byte] pub fn encode_png(FixedArray[Byte], Int, Int) -> FixedArray[Byte] +pub fn luma_histogram(FixedArray[Byte], Int, Int) -> FixedArray[Int] + // Errors // Types and methods diff --git a/web/playground.js b/web/playground.js index 6eee419..d8a00fd 100644 --- a/web/playground.js +++ b/web/playground.js @@ -4,7 +4,7 @@ // canvas buffer crosses in with zero copies. // * "wasm" — MoonBit compiled to a linear-memory WebAssembly module; pixels // are bulk-copied into the exported memory via a Uint8Array view. -import { apply_filter, encode_png } from "./dist/web.js"; +import { apply_filter, encode_png, luma_histogram } from "./dist/web.js"; // Load the genuine-WASM module (no imports required). Fall back to JS if it // cannot be loaded for any reason. @@ -105,6 +105,25 @@ function updateStats(ms) { $("statTime").textContent = `${ms.toFixed(2)} ms`; const mpPerSec = imgW * imgH / 1e6 / (ms / 1000); $("statThroughput").textContent = ms > 0 ? `${mpPerSec.toFixed(1)} MP/s` : "—"; + drawHistogram(); +} + +// Live luma histogram of whatever is currently on the canvas, computed by +// the library's own histogram_luma through the js binding. +function drawHistogram() { + const hc = $("histCanvas"); + if (!hc || !originalData) return; + const hctx = hc.getContext("2d"); + const pixels = ctx.getImageData(0, 0, imgW, imgH).data; + const bins = luma_histogram(new Uint8Array(pixels.buffer.slice(0)), imgW, imgH); + let max = 1; + for (let i = 0; i < 256; i++) if (bins[i] > max) max = bins[i]; + hctx.clearRect(0, 0, hc.width, hc.height); + hctx.fillStyle = "rgba(96, 165, 250, 0.9)"; + for (let i = 0; i < 256; i++) { + const h = Math.round(bins[i] / max * (hc.height - 4)); + if (h > 0) hctx.fillRect(i, hc.height - h, 1, h); + } } function render() {