Skip to content

Instantly share code, notes, and snippets.

@twobob
Last active October 17, 2025 23:44
Show Gist options
  • Save twobob/93f504e3120c8369da1aeb6298b0ac77 to your computer and use it in GitHub Desktop.
Save twobob/93f504e3120c8369da1aeb6298b0ac77 to your computer and use it in GitHub Desktop.
export interface DeSepAIOptions {
strength?: number
notchWhiten?: boolean
shiftBase?: [number, number, number]
preserveDark?: boolean
}
export interface ImageData {
data: Uint8ClampedArray
width: number
height: number
}
export function detectShiftBase(imageData: ImageData): [number, number, number] {
const { data } = imageData
const lum: number[] = []
for (let i = 0; i < data.length; i += 4)
lum.push(0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2])
const sorted = lum.slice().sort((a, b) => a - b)
const threshold = sorted[Math.floor(sorted.length * 0.9)]
let sum = [0, 0, 0]
let count = 0
for (let i = 0; i < lum.length; i++) {
if (lum[i] >= threshold) {
const idx = i * 4
sum[0] += data[idx]
sum[1] += data[idx + 1]
sum[2] += data[idx + 2]
count++
}
}
const avg: [number, number, number] = count
? [sum[0] / count, sum[1] / count, sum[2] / count]
: [255, 255, 255]
return [255 - avg[0], 255 - avg[1], 255 - avg[2]]
}
export function processPixelData(
imageData: ImageData,
options: DeSepAIOptions = {}
): ImageData {
const {
strength = 100,
notchWhiten = false,
shiftBase = [9, 15, 27],
preserveDark = strength > 50
} = options
// Create new pixel data array (don't modify original)
const d = new Uint8ClampedArray(imageData.data)
const scale = strength / 100
if (notchWhiten) {
const refR = 255 - shiftBase[0]
const refG = 255 - shiftBase[1]
const refB = 255 - shiftBase[2]
const notch = 8
for (let i = 0; i < d.length; i += 4) {
const r = d[i], g = d[i + 1], b = d[i + 2]
const bri = 0.2126 * r + 0.7152 * g + 0.0722 * b
const att = preserveDark ? Math.min(1, bri / 128) : 1
if (
r >= refR - notch && r <= refR + notch &&
g >= refG - notch && g <= refG + notch &&
b >= refB - notch && b <= refB + notch
) {
d[i] = Math.min(255, r + (255 - r) * scale * att)
d[i + 1] = Math.min(255, g + (255 - g) * scale * att)
d[i + 2] = Math.min(255, b + (255 - b) * scale * att)
} else {
d[i] = Math.min(255, r + shiftBase[0] * scale * att)
d[i + 1] = Math.min(255, g + shiftBase[1] * scale * att)
d[i + 2] = Math.min(255, b + shiftBase[2] * scale * att)
}
}
} else {
for (let i = 0; i < d.length; i += 4) {
const r = d[i], g = d[i + 1], b = d[i + 2]
const bri = 0.2126 * r + 0.7152 * g + 0.0722 * b
const att = preserveDark ? Math.min(1, bri / 128) : 1
d[i] = Math.min(255, r + shiftBase[0] * scale * att)
d[i + 1] = Math.min(255, g + shiftBase[1] * scale * att)
d[i + 2] = Math.min(255, b + shiftBase[2] * scale * att)
}
}
return {
data: d,
width: imageData.width,
height: imageData.height
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DeSepAI Image Processor Test</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
}
.controls {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
}
.control-group {
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
label {
font-weight: bold;
min-width: 120px;
}
input[type="file"] {
padding: 8px;
border: 2px dashed #ddd;
border-radius: 5px;
background: white;
}
input[type="range"] {
flex: 1;
margin: 0 10px;
}
input[type="number"] {
width: 60px;
padding: 5px;
border: 1px solid #ddd;
border-radius: 3px;
}
input[type="checkbox"] {
transform: scale(1.2);
}
.range-value {
font-weight: bold;
color: #007bff;
min-width: 40px;
}
.shift-base {
display: flex;
gap: 10px;
align-items: center;
}
.shift-base input {
width: 50px;
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background: #0056b3;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.results {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-top: 30px;
}
.image-container {
text-align: center;
}
.image-container h3 {
margin-bottom: 10px;
color: #333;
}
.image-container img {
max-width: 100%;
max-height: 400px;
border: 1px solid #ddd;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.error {
background: #f8d7da;
color: #721c24;
padding: 10px;
border-radius: 5px;
margin: 10px 0;
}
.info {
background: #d1ecf1;
color: #0c5460;
padding: 10px;
border-radius: 5px;
margin: 10px 0;
}
.stats {
background: #e2e3e5;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
}
.preset-buttons {
display: flex;
gap: 10px;
margin-top: 10px;
}
.preset-buttons button {
background: #6c757d;
font-size: 14px;
padding: 5px 10px;
}
.preset-buttons button:hover {
background: #5a6268;
}
</style>
</head>
<body>
<div class="container">
<h1>DeSepAI Image Processor Test</h1>
<div class="controls">
<div class="control-group">
<label for="fileInput">Select Image:</label>
<input type="file" id="fileInput" accept="image/*">
</div>
<div class="control-group">
<label for="strength">Strength:</label>
<input type="range" id="strength" min="0" max="200" value="120">
<span class="range-value" id="strengthValue">120</span>
</div>
<div class="control-group">
<label for="notchWhiten">Notch Whiten:</label>
<input type="checkbox" id="notchWhiten" checked>
</div>
<div class="control-group">
<label>Shift Base (RGB):</label>
<div class="shift-base">
<span>R:</span><input type="number" id="shiftR" value="9" min="0" max="255">
<span>G:</span><input type="number" id="shiftG" value="15" min="0" max="255">
<span>B:</span><input type="number" id="shiftB" value="27" min="0" max="255">
</div>
</div>
<div class="preset-buttons">
<button onclick="setPreset('default')">Default</button>
<button onclick="setPreset('subtle')">Subtle</button>
<button onclick="setPreset('strong')">Strong</button>
<button onclick="setPreset('auto')">Auto Detect</button>
</div>
<div class="control-group" style="margin-top: 20px;">
<button id="processBtn" onclick="processImage()" disabled>Process Image</button>
<button onclick="downloadResult()" id="downloadBtn" disabled>Download Result</button>
</div>
</div>
<div id="errorContainer"></div>
<div id="infoContainer"></div>
<div class="results" id="results" style="display: none;">
<div class="image-container">
<h3>Original Image</h3>
<img id="originalImage" alt="Original">
</div>
<div class="image-container">
<h3>Processed Image</h3>
<img id="processedImage" alt="Processed">
</div>
</div>
<div id="stats" class="stats" style="display: none;">
<h4>Processing Stats</h4>
<div id="statsContent"></div>
</div>
</div>
<script>
console.log('Starting DeSepAI Test Page Load...');
// Global variables
let currentFile = null;
let processedBase64 = null;
// DeSepAI Core Functions (UI-agnostic, converted from TypeScript)
function detectShiftBase(imageData) {
console.log('Detecting shift base from image data...');
const { data } = imageData;
const lum = [];
for (let i = 0; i < data.length; i += 4) {
lum.push(0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2]);
}
const sorted = lum.slice().sort((a, b) => a - b);
const threshold = sorted[Math.floor(sorted.length * 0.9)];
let sum = [0, 0, 0];
let count = 0;
for (let i = 0; i < lum.length; i++) {
if (lum[i] >= threshold) {
const idx = i * 4;
sum[0] += data[idx];
sum[1] += data[idx + 1];
sum[2] += data[idx + 2];
count++;
}
}
const avg = count
? [sum[0] / count, sum[1] / count, sum[2] / count]
: [255, 255, 255];
const result = [255 - avg[0], 255 - avg[1], 255 - avg[2]];
console.log('Detected shift base:', result);
return result;
}
function processPixelData(imageData, options = {}) {
console.log('Processing pixel data with options:', options);
const {
strength = 100,
notchWhiten = false,
shiftBase = [9, 15, 27],
preserveDark = strength > 50
} = options;
// Create new pixel data array (don't modify original)
const d = new Uint8ClampedArray(imageData.data);
const scale = strength / 100;
if (notchWhiten) {
const refR = 255 - shiftBase[0];
const refG = 255 - shiftBase[1];
const refB = 255 - shiftBase[2];
const notch = 8;
for (let i = 0; i < d.length; i += 4) {
const r = d[i], g = d[i + 1], b = d[i + 2];
const bri = 0.2126 * r + 0.7152 * g + 0.0722 * b;
const att = preserveDark ? Math.min(1, bri / 128) : 1;
if (
r >= refR - notch && r <= refR + notch &&
g >= refG - notch && g <= refG + notch &&
b >= refB - notch && b <= refB + notch
) {
d[i] = Math.min(255, r + (255 - r) * scale * att);
d[i + 1] = Math.min(255, g + (255 - g) * scale * att);
d[i + 2] = Math.min(255, b + (255 - b) * scale * att);
} else {
d[i] = Math.min(255, r + shiftBase[0] * scale * att);
d[i + 1] = Math.min(255, g + shiftBase[1] * scale * att);
d[i + 2] = Math.min(255, b + shiftBase[2] * scale * att);
}
}
} else {
for (let i = 0; i < d.length; i += 4) {
const r = d[i], g = d[i + 1], b = d[i + 2];
const bri = 0.2126 * r + 0.7152 * g + 0.0722 * b;
const att = preserveDark ? Math.min(1, bri / 128) : 1;
d[i] = Math.min(255, r + shiftBase[0] * scale * att);
d[i + 1] = Math.min(255, g + shiftBase[1] * scale * att);
d[i + 2] = Math.min(255, b + shiftBase[2] * scale * att);
}
}
console.log('Pixel processing completed');
return {
data: d,
width: imageData.width,
height: imageData.height
};
}
// UI-specific image handling functions
async function getImageDataFromBase64(base64) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
resolve({
data: imageData.data,
width: canvas.width,
height: canvas.height
});
};
img.onerror = () => reject(new Error("Failed to load image"));
img.src = base64;
});
}
function imageDataToBase64(imageData) {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = imageData.width;
canvas.height = imageData.height;
const canvasImageData = ctx.createImageData(imageData.width, imageData.height);
canvasImageData.data.set(imageData.data);
ctx.putImageData(canvasImageData, 0, 0);
return canvas.toDataURL("image/png");
}
async function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(new Error("Failed to read file"));
reader.readAsDataURL(file);
});
}
// High-level processing functions combining UI and core logic
async function processBase64ToBase64(inputBase64, options) {
const imageData = await getImageDataFromBase64(inputBase64);
const processedImageData = processPixelData(imageData, options);
return imageDataToBase64(processedImageData);
}
async function processFileBase64(file, options) {
console.log('Processing file to base64:', file.name);
const inputBase64 = await fileToBase64(file);
return processBase64ToBase64(inputBase64, options);
}
// UI Event Handlers
function setupEventListeners() {
console.log('Setting up event listeners...');
const fileInput = document.getElementById('fileInput');
const strengthSlider = document.getElementById('strength');
const strengthValue = document.getElementById('strengthValue');
if (!fileInput || !strengthSlider || !strengthValue) {
console.error('Critical UI elements not found!');
return;
}
fileInput.addEventListener('change', function(e) {
console.log('File input changed:', e.target.files);
if (e.target.files && e.target.files[0]) {
currentFile = e.target.files[0];
console.log('File selected:', currentFile.name, 'Size:', currentFile.size, 'Type:', currentFile.type);
document.getElementById('processBtn').disabled = false;
// Show original image
const reader = new FileReader();
reader.onload = function(e) {
console.log('Image loaded for preview');
document.getElementById('originalImage').src = e.target.result;
document.getElementById('results').style.display = 'grid';
};
reader.readAsDataURL(currentFile);
clearMessages();
}
});
strengthSlider.addEventListener('input', function(e) {
strengthValue.textContent = e.target.value;
});
console.log('Event listeners set up successfully');
}
function setPreset(preset) {
console.log('Setting preset:', preset);
const strengthSlider = document.getElementById('strength');
const strengthValue = document.getElementById('strengthValue');
const notchWhiten = document.getElementById('notchWhiten');
const shiftR = document.getElementById('shiftR');
const shiftG = document.getElementById('shiftG');
const shiftB = document.getElementById('shiftB');
switch(preset) {
case 'default':
strengthSlider.value = 120;
notchWhiten.checked = true;
shiftR.value = 9;
shiftG.value = 15;
shiftB.value = 27;
break;
case 'subtle':
strengthSlider.value = 60;
notchWhiten.checked = false;
shiftR.value = 5;
shiftG.value = 8;
shiftB.value = 12;
break;
case 'strong':
strengthSlider.value = 180;
notchWhiten.checked = true;
shiftR.value = 15;
shiftG.value = 25;
shiftB.value = 40;
break;
case 'auto':
if (currentFile) {
autoDetectShiftBase();
return;
} else {
showError('Please select an image first for auto-detection');
return;
}
}
strengthValue.textContent = strengthSlider.value;
console.log('Preset applied:', preset);
}
async function autoDetectShiftBase() {
if (!currentFile) {
showError('Please select an image first');
return;
}
try {
showInfo('Auto-detecting optimal shift base values...');
console.log('Starting auto-detection with image data...');
const inputBase64 = await fileToBase64(currentFile);
const imageData = await getImageDataFromBase64(inputBase64);
const detectedShift = detectShiftBase(imageData);
document.getElementById('shiftR').value = Math.round(detectedShift[0]);
document.getElementById('shiftG').value = Math.round(detectedShift[1]);
document.getElementById('shiftB').value = Math.round(detectedShift[2]);
showInfo(`Auto-detected shift base: R=${Math.round(detectedShift[0])}, G=${Math.round(detectedShift[1])}, B=${Math.round(detectedShift[2])}`);
} catch (error) {
showError('Auto-detection failed: ' + error.message);
console.error('Auto-detection error:', error);
}
}
async function processImage() {
console.log('Process image button clicked');
if (!currentFile) {
console.error('No file selected');
showError('Please select an image first');
return;
}
try {
clearMessages();
showInfo('Processing image...');
console.log('Starting image processing...');
const startTime = performance.now();
const options = {
strength: parseInt(document.getElementById('strength').value),
notchWhiten: document.getElementById('notchWhiten').checked,
shiftBase: [
parseInt(document.getElementById('shiftR').value),
parseInt(document.getElementById('shiftG').value),
parseInt(document.getElementById('shiftB').value)
]
};
console.log('Processing options:', options);
processedBase64 = await processFileBase64(currentFile, options);
const endTime = performance.now();
const processingTime = Math.round(endTime - startTime);
console.log('Processing completed in', processingTime, 'ms');
console.log('Result base64 length:', processedBase64.length);
document.getElementById('processedImage').src = processedBase64;
document.getElementById('downloadBtn').disabled = false;
// Show stats
showStats(options, processingTime, currentFile);
showInfo(`Image processed successfully in ${processingTime}ms`);
} catch (error) {
console.error('Processing failed:', error);
showError('Failed to process image: ' + error.message);
}
}
function downloadResult() {
if (!processedBase64) {
showError('No processed image to download');
return;
}
const link = document.createElement('a');
link.download = 'processed_' + (currentFile.name || 'image.png');
link.href = processedBase64;
link.click();
}
function showStats(options, processingTime, file) {
const statsContent = document.getElementById('statsContent');
const fileSizeKB = Math.round(file.size / 1024);
statsContent.innerHTML = `
<strong>File:</strong> ${file.name} (${fileSizeKB} KB)<br>
<strong>Processing Time:</strong> ${processingTime}ms<br>
<strong>Strength:</strong> ${options.strength}<br>
<strong>Notch Whiten:</strong> ${options.notchWhiten ? 'Yes' : 'No'}<br>
<strong>Shift Base:</strong> R=${options.shiftBase[0]}, G=${options.shiftBase[1]}, B=${options.shiftBase[2]}<br>
<strong>File Type:</strong> ${file.type}
`;
document.getElementById('stats').style.display = 'block';
}
function showError(message) {
const container = document.getElementById('errorContainer');
container.innerHTML = `<div class="error">${message}</div>`;
}
function showInfo(message) {
const container = document.getElementById('infoContainer');
container.innerHTML = `<div class="info">${message}</div>`;
}
function clearMessages() {
document.getElementById('errorContainer').innerHTML = '';
document.getElementById('infoContainer').innerHTML = '';
}
// Initialize page
document.addEventListener('DOMContentLoaded', function() {
console.log('DOMContentLoaded - Initializing page...');
console.log('Function availability check:', {
detectShiftBase: typeof detectShiftBase,
processBase64ToBase64: typeof processBase64ToBase64,
processFileBase64: typeof processFileBase64,
getImageDataFromBase64: typeof getImageDataFromBase64,
fileToBase64: typeof fileToBase64
});
console.log('UI Elements check:', {
fileInput: !!document.getElementById('fileInput'),
strengthSlider: !!document.getElementById('strength'),
processBtn: !!document.getElementById('processBtn'),
originalImage: !!document.getElementById('originalImage'),
processedImage: !!document.getElementById('processedImage')
});
setupEventListeners();
loadDefaultImage();
showInfo('Page loaded successfully! Default image (fish.png) loaded. You can select a different image or process the default one.');
console.log('DeSepAI Test Page fully initialized and ready!');
});
// Load default fish.png image
async function loadDefaultImage() {
try {
console.log('Loading default fish.png image...');
console.log('Current working directory:', window.location.href);
// Fetch fish.png and convert to File object
const response = await fetch('./fish.png');
console.log('Fetch response status:', response.status, response.statusText);
if (!response.ok) {
throw new Error(`Failed to fetch fish.png: ${response.status} ${response.statusText}`);
}
const blob = await response.blob();
console.log('Blob created, size:', blob.size, 'type:', blob.type);
const file = new File([blob], 'fish.png', { type: 'image/png' });
console.log('File object created:', file.name, file.size, file.type);
// Set as current file
currentFile = file;
console.log('currentFile set to:', currentFile.name);
// Show original image
const base64 = await fileToBase64(file);
console.log('Base64 conversion successful, length:', base64.length);
const originalImg = document.getElementById('originalImage');
const resultsDiv = document.getElementById('results');
const processBtn = document.getElementById('processBtn');
if (!originalImg || !resultsDiv || !processBtn) {
throw new Error('Required DOM elements not found');
}
originalImg.src = base64;
resultsDiv.style.display = 'grid';
processBtn.disabled = false;
console.log('Default fish.png image loaded successfully');
showInfo('Fish.png loaded as default image. Ready to process!');
} catch (error) {
console.error('Failed to load default image:', error);
showError('Failed to load default fish.png image: ' + error.message);
showInfo('Select an image file to begin processing');
}
}
</script>
</body>
</html>
@twobob
Copy link
Author

twobob commented Oct 17, 2025

fish

@twobob
Copy link
Author

twobob commented Oct 17, 2025

fish.png

@twobob
Copy link
Author

twobob commented Oct 17, 2025

the html is to show the kind of the things the .ts can do

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment