Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions cmd/showcase/main.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

///|
Expand Down
12 changes: 12 additions & 0 deletions web/bindings.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
96 changes: 51 additions & 45 deletions web/dist/web.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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) => {
Expand All @@ -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;
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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 }
5 changes: 5 additions & 0 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ <h2>性能</h2>
<p class="bench-result" id="benchResult"></p>
</section>

<section class="group">
<h2>亮度直方图</h2>
<canvas id="histCanvas" width="256" height="80" style="width:100%;height:80px;display:block;border-radius:6px;background:rgba(255,255,255,0.04);"></canvas>
</section>

<p class="footnote">
像素缓冲区以 <code>Uint8ClampedArray</code> 直接进出 MoonBit
(<code>FixedArray[Byte]</code>),无需序列化拷贝。
Expand Down
7 changes: 6 additions & 1 deletion web/moon.pkg
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
},
)
2 changes: 2 additions & 0 deletions web/pkg.generated.mbti
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 20 additions & 1 deletion web/playground.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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() {
Expand Down
Loading