Skip to content

Commit b7a166e

Browse files
committed
optional downsampling + prepare adding histograms charts
1 parent 60b7552 commit b7a166e

1 file changed

Lines changed: 43 additions & 16 deletions

File tree

‎public/histogram-matching-js.html‎

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -112,17 +112,24 @@ <h3>Pure JS, client-side, Histogram Matching</h3>
112112

113113
// helper for uint8 histograms
114114
function getNormalizedCdfUint8(arr) {
115-
return cumsum(bincount(arr, 256, 0, 255, true));
115+
const bincounts = bincount(arr, 256, 0, 255, true)
116+
const cdf = cumsum(bincounts)
117+
return { bincounts, cdf };
116118
}
117119

118120
// compute mapping between source and target
119121
export function matchCumulativeCdf(source, target) {
120122
const srcCdf = getNormalizedCdfUint8(source);
121123
const tgtCdf = getNormalizedCdfUint8(target);
122124
const values = Array.from({ length: 256 }, (_, i) => i);
125+
const mapping = interp(srcCdf.cdf, tgtCdf.cdf, values)
123126

124127
// For each source intensity, find the CDF value, then find which target intensity has the closest CDF value.
125-
return interp(srcCdf, tgtCdf, values);
128+
return {
129+
mapping,
130+
srcHist: srcCdf.bincounts,
131+
tgtHist: tgtCdf.bincounts
132+
};
126133
}
127134

128135
// ------------------- high-level API -------------------
@@ -134,20 +141,29 @@ <h3>Pure JS, client-side, Histogram Matching</h3>
134141
*
135142
* @param {Uint8Array} source - Source image array (flat 1D, 1/3/4 channels interleaved).
136143
* @param {Uint8Array} target - Target image array (same shape/channels as source).
137-
* @param Number channels - count of channels to do correct de-interleaving of RGB channels values.
144+
* @param {Number} channels - count of channels to do correct de-interleaving of RGB channels values.
138145
* @param {'MAPPING'|'MATCHED_DATA'} returnType - Whether to return mappings or matched data.
146+
* @param {Number} maxMpx - Whether to subsample/apply downscaling to source/target image rasters If pixelCount > maxMpx. 1 = 100% res, 2 = 2x downsampling/subsampling
139147
* @returns {{mappings: Float64Array[], matched?: Uint8Array}}
140148
*/
141-
export function matchHistograms(source, target, channels = 3, returnType = 'MATCHED_DATA') {
142-
// const channels = source.length % 4 === 0 ? 4 :
143-
// source.length % 3 === 0 ? 3 : 1;
149+
export function matchHistograms(source, target, channels = 3, returnType = 'MATCHED_DATA', maxMpx=-1) {
150+
// Downsample source and target arrays in case they exceed user-defined maxMpx param
151+
let srcDownsample = 1, tgtDownsample = 1;
152+
if (maxMpx > 0) {
153+
// srcDownsample = Math.max(1, Math.ceil((source.length / channels / 1e6) / maxMpx))
154+
// tgtDownsample = Math.max(1, Math.ceil((source.length / channels / 1e6) / maxMpx))
155+
srcDownsample = Math.ceil((source.length / channels / 1e6) / maxMpx)
156+
tgtDownsample = Math.ceil((source.length / channels / 1e6) / maxMpx)
157+
console.log('Downsampling factors', srcDownsample, tgtDownsample)
158+
}
159+
144160
// RGB / RGBA
145161
const mappings = [];
146162
for (let c = 0; c < Math.min(3, channels); c++) {
147163
const srcChannel = [];
148164
const tgtChannel = [];
149-
for (let i = c; i < source.length; i += channels) srcChannel.push(source[i]);
150-
for (let i = c; i < target.length; i += channels) tgtChannel.push(target[i]);
165+
for (let i = c; i < source.length; i += channels * srcDownsample) srcChannel.push(source[i]);
166+
for (let i = c; i < target.length; i += channels * tgtDownsample) tgtChannel.push(target[i]);
151167
mappings[c] = matchCumulativeCdf(srcChannel, tgtChannel);
152168
}
153169
console.log('mappings', mappings)
@@ -156,26 +172,27 @@ <h3>Pure JS, client-side, Histogram Matching</h3>
156172
const out = new Uint8Array(source.length);
157173
for (let i = 0; i < source.length; i += channels) {
158174
for (let c = 0; c < 3; c++) {
159-
out[i + c] = mappings[0 + c][source[i + c]];
175+
out[i + c] = mappings[0 + c].mapping[source[i + c]];
160176
}
161177
if (channels === 4) out[i + 3] = source[i + 3]; // keep alpha
162178
}
163179
return {mappings, matched: out};
164180
}
165181

166-
167-
168182
// --- image handling ---
169183
const img_objs = {};
170184
// slot is source or target, indexing img_objs, mapped to input and img slot+"-img-div
171-
function loadImage(fileOrUrl, slot) {
185+
function loadImage(fileOrUrl, slot, maxMpx=-1) { // TODO 2 is for testing default downscale = 1
172186
const img = new Image();
173187
img.onload = function() {
174188
const canvas = document.createElement("canvas");
189+
// naturalWidth / downscale to make canvas smaller and downsample naturally, downscale = 2 is nicer, but then would need to use source input and re-apply mapping, so passed source resolution instead
175190
canvas.width = img.naturalWidth; // naturalWidth instead of width
176191
canvas.height = img.naturalHeight; // naturalHeight instead of height
177192
const ctx = canvas.getContext("2d");
178-
ctx.drawImage(img, 0, 0);
193+
ctx.drawImage(img, 0, 0); // rescale image
194+
// ctx.drawImage(img, 0, 0, canvas.width, canvas.height); // rescale image
195+
179196
img_objs[slot] = {ctx, img, canvas};
180197
document.getElementById(slot+"-img-div").replaceChildren(img);
181198

@@ -189,7 +206,7 @@ <h3>Pure JS, client-side, Histogram Matching</h3>
189206
// Match Histograms Computation
190207
// Check getImageData always return 4 channels RGBA
191208
const start = Date.now();
192-
const matched = matchHistograms(sourceArr, targetArr, 4, 'MATCHED_DATA');
209+
const matched = matchHistograms(sourceArr, targetArr, 4, 'MATCHED_DATA', maxMpx);
193210
const elapsed_ms = Date.now() - start
194211

195212
// Write matched image to outCanvas
@@ -204,7 +221,8 @@ <h3>Pure JS, client-side, Histogram Matching</h3>
204221
// Display to user and log stats
205222
const container = document.getElementById('matched-img-div');
206223
console.log('matched', matched, 'mappings slice')
207-
console.log(matched.mappings[0].slice(0, 30))
224+
console.log(matched.mappings[0].mapping.slice(0, 30))
225+
208226
const stats_div = document.createElement("pre");
209227
const statsObj = {
210228
source: {
@@ -228,12 +246,21 @@ <h3>Pure JS, client-side, Histogram Matching</h3>
228246

229247
// container.replaceChildren(outCanvas);
230248
outCanvas.toBlob(function(blob) {
249+
// Add image resulting from histogram matching to output div
231250
const url = URL.createObjectURL(blob);
232251
const img = document.createElement("img");
233252
img.src = url;
234253
container.replaceChildren(img);
235254

236-
container.appendChild(stats_div)
255+
// Also show some stats
256+
const stats_details = document.createElement('details');
257+
stats_details.style = "text-align: left;"
258+
const summary = document.createElement("summary");
259+
summary.textContent = "Show stats";
260+
stats_details.appendChild(summary);
261+
stats_details.appendChild(stats_div);
262+
stats_details.open = true;
263+
container.appendChild(stats_details)
237264
}, "image/png");
238265

239266
}

0 commit comments

Comments
 (0)