diff --git a/index.html b/index.html index 4c08c27..0763247 100644 --- a/index.html +++ b/index.html @@ -187,6 +187,11 @@ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } +#lut-preview { + max-width: 100%; + height: auto; +} + canvas { border: 1px solid #ddd; margin: 10px 0; @@ -687,8 +692,15 @@

Captured Colors

1 +
+ + + Enables cross-channel influence and better rolloff curves +
+ +
@@ -817,7 +829,21 @@

LUT Preview

blue: null }, lutImage: null, - cubeData: null + cubeData: null, + useAdvancedProcessing: true, + transformationModels: { + basic: { + red: null, + green: null, + blue: null + }, + multivariate: { + red: null, + green: null, + blue: null + } + }, + gammaCurves: null }; document.addEventListener('DOMContentLoaded', function() { @@ -1100,18 +1126,24 @@

LUT Preview

colorPreview.style.backgroundColor = rgbToHex(state.referenceColors[index]); } - // Initialize color card upload - function initializeColorCardUpload() { - const fileInput = document.getElementById('colorcard-upload'); - fileInput.addEventListener('change', function(event) { - const file = event.target.files[0]; - if (file) { - loadImage(file, type); - logMessage(`Demo ${type} image loaded successfully`, 'success'); - } - }) - } - + function initializeColorCardUpload() { + const fileInput = document.getElementById('colorcard-upload'); + fileInput.addEventListener('change', function(event) { + const file = event.target.files[0]; + if (file) { + loadImage(file, 'colorcard'); // Fixed: using 'colorcard' instead of undefined 'type' + logMessage(`Color card image loaded successfully`, 'success'); + } + }); + + // Add demo image functionality properly + const demoButton = document.getElementById('use-demo-image'); + if (demoButton) { + demoButton.addEventListener('click', function() { + loadDemoImage('colorcard'); + }); + } + }; // Extract colors from the color card image function extractColorsFromImage(img) { logMessage('Extracting colors from image...', 'info'); @@ -1627,206 +1659,317 @@

LUT Preview

}); } - // Generate LUT function generateLUT() { if (!state.transformationCoefficients.red) { logMessage('No color transformation available. Please process colors first.', 'error'); return; } - + logMessage('Generating LUT...', 'info'); updateProgress('export', 0); - - // Create canvas for the LUT + const lutCanvas = document.getElementById('lut-preview'); const lutCtx = lutCanvas.getContext('2d'); - + + const useAdvancedProcessing = state.useAdvancedProcessing && + state.transformationModels && + state.transformationModels.multivariate && + state.transformationModels.multivariate.red; + + const gammaCurves = useAdvancedProcessing ? state.gammaCurves : null; + if (state.lutFormat === 'png') { - // For PNG LUT (OBS compatible) - lutCanvas.width = 64; - lutCanvas.height = 64; - - // Clear canvas - lutCtx.clearRect(0, 0, lutCanvas.width, lutCanvas.height); - - // Generate neutral LUT data - const lutData = lutCtx.createImageData(64, 64); - const data = lutData.data; - - // Fill the entire 64x64 grid - let progress = 0; - for (let y = 0; y < 64; y++) { - for (let x = 0; x < 64; x++) { - // Calculate RGB values from position - // In 64x64 format, each component (r,g,b) gets 8 values (0-7) - // Format is essentially RRRRRRRR GGGGGGGG BBBBBBBB ... etc. - const r = Math.floor(x % 8) * 255 / 7; // R cycles every 8 pixels - const g = Math.floor(x / 8) * 255 / 7; // G cycles every 8 columns of R - const b = Math.floor(y / 8) * 255 / 7; // B changes every 8 rows - - // Calculate pixel index - const index = (y * 64 + x) * 4; - - // Set RGBA values - data[index] = r; - data[index + 1] = g; - data[index + 2] = b; - data[index + 3] = 255; // Alpha - - // Update progress (less frequently to improve performance) - if (++progress % 256 === 0) { - updateProgress('export', (progress / (64 * 64)) * 30); - } - } - } - - // Apply transformation to LUT - for (let i = 0; i < data.length; i += 4) { - // Get original values - const r = data[i]; - const g = data[i + 1]; - const b = data[i + 2]; - - // Apply color transformation (with safeguards against NaN) - let newR = safeApplyPolynomial(r, state.transformationCoefficients.red); - let newG = safeApplyPolynomial(g, state.transformationCoefficients.green); - let newB = safeApplyPolynomial(b, state.transformationCoefficients.blue); - - // Apply brightness adjustment - newR += state.brightnessAdjustment; - newG += state.brightnessAdjustment; - newB += state.brightnessAdjustment; - - // Apply gamma (using lookup table) - const gammaValue = state.gammaValue || 1.0; - if (gammaValue !== 1.0) { - // Create simple gamma lookup with checks to prevent NaN - newR = safeGamma(newR, gammaValue); - newG = safeGamma(newG, gammaValue); - newB = safeGamma(newB, gammaValue); - } - - // Clamp values - data[i] = Math.min(255, Math.max(0, Math.round(newR))); - data[i + 1] = Math.min(255, Math.max(0, Math.round(newG))); - data[i + 2] = Math.min(255, Math.max(0, Math.round(newB))); - - // Update progress (less frequently to improve performance) - if (i % 1024 === 0) { - updateProgress('export', 30 + (i / data.length) * 60); - } - } - - // Put the data back to the canvas - lutCtx.putImageData(lutData, 0, 0); - + generate2dPngLUT(lutCanvas, lutCtx, useAdvancedProcessing, gammaCurves); } else if (state.lutFormat === 'cube') { - // For CUBE LUT - const lutSize = state.lutSize; - - // Generate header - let cubeText = `# LUT generated by Browser-based LUT Maker\n`; - cubeText += `# Created: ${new Date().toISOString()}\n`; - cubeText += `LUT_3D_SIZE ${lutSize}\n\n`; + generate3dCubeLUT(lutCanvas, lutCtx, useAdvancedProcessing, gammaCurves); + } + + updateProgress('export', 100); + logMessage('LUT generation complete!', 'success'); + + document.getElementById('lut-download-container').style.display = 'block'; + } + + function applyTransformations(r, g, b, useAdvancedProcessing, gammaCurves) { + let newR, newG, newB; + + if (useAdvancedProcessing) { + [newR, newG, newB] = processPixelAdvanced(r, g, b, state.transformationModels, state.brightnessAdjustment, gammaCurves); + } else { + newR = safeApplyPolynomial(r, state.transformationCoefficients.red); + newG = safeApplyPolynomial(g, state.transformationCoefficients.green); + newB = safeApplyPolynomial(b, state.transformationCoefficients.blue); + + newR += state.brightnessAdjustment; + newG += state.brightnessAdjustment; + newB += state.brightnessAdjustment; + + const gammaValue = state.gammaValue || 1.0; + if (gammaValue !== 1.0) { + newR = safeGamma(newR, gammaValue); + newG = safeGamma(newG, gammaValue); + newB = safeGamma(newB, gammaValue); + } + } + + return [Math.min(255, Math.max(0, Math.round(newR))), + Math.min(255, Math.max(0, Math.round(newG))), + Math.min(255, Math.max(0, Math.round(newB)))]; + } + + function generate2dPngLUT(lutCanvas, lutCtx, useAdvancedProcessing, gammaCurves) { + // Set canvas dimensions for 8x8 grid (64 cells) + lutCanvas.width = 512; + lutCanvas.height = 512; + lutCtx.clearRect(0, 0, lutCanvas.width, lutCanvas.height); + const lutData = lutCtx.createImageData(512, 512); + const data = lutData.data; + + // Cell size (each cell is 64x64 pixels) + const cellSize = 64; + + // Generate the 8x8 grid of color gradients + for (let y = 0; y < 512; y++) { + for (let x = 0; x < 512; x++) { + // Calculate grid position (0-7 for both x and y) + const gridX = Math.floor(x / cellSize); + const gridY = Math.floor(y / cellSize); + + // Calculate position within cell (0-63 for both x and y) + const cellX = x % cellSize; + const cellY = y % cellSize; + + // Calculate normalized coordinates within cell (0-1) + const normalizedX = cellX / (cellSize - 1); + const normalizedY = cellY / (cellSize - 1); + + // Map grid position to base colors + // Red varies horizontally across entire width + const r = normalizedX * 255; + + // Green varies vertically + const g = normalizedY * 255; + + // Blue varies by grid position (creating the different hues in each cell) + const b = (gridX / 7 + gridY / 7) * 255; + + // Calculate array index for the current pixel + const index = (y * 512 + x) * 4; + + // Set RGBA values + data[index] = r; + data[index + 1] = g; + data[index + 2] = b; + data[index + 3] = 255; // Alpha is always 255 (fully opaque) + } + } + + // Apply additional transformations if required + if (useAdvancedProcessing && typeof applyTransformations === 'function') { + for (let i = 0; i < data.length; i += 4) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + const [newR, newG, newB] = applyTransformations(r, g, b, useAdvancedProcessing, gammaCurves); + data[i] = newR; + data[i + 1] = newG; + data[i + 2] = newB; + } + } + + // Apply the image data to the canvas + lutCtx.putImageData(lutData, 0, 0); + } + + function generate3dCubeLUT(lutCanvas, lutCtx, useAdvancedProcessing, gammaCurves) { + // Default to 33×33×33 grid (standard size for 3D LUTs) or use state.lutSize + const size = state.lutSize || 33; + + // Generate CUBE data + let cubeData = []; + let header = `# CUBE LUT generated by WebLUT\n`; + header += `LUT_3D_SIZE ${size}\n\n`; + + // Calculate total operations for progress updates + const totalOps = size * size * size; + let completedOps = 0; + + logMessage(`Generating ${size}x${size}x${size} CUBE LUT data...`, 'info'); + + // Generate the 3D grid of color values + for (let b = 0; b < size; b++) { + for (let g = 0; g < size; g++) { + for (let r = 0; r < size; r++) { + // Calculate normalized RGB values (0-1) + const normalizedR = r / (size - 1); + const normalizedG = g / (size - 1); + const normalizedB = b / (size - 1); - // Generate data points - let count = 0; - const totalCount = lutSize * lutSize * lutSize; + // Scale to 0-255 for processing + let rOut = normalizedR * 255; + let gOut = normalizedG * 255; + let bOut = normalizedB * 255; - for (let b = 0; b < lutSize; b++) { - for (let g = 0; g < lutSize; g++) { - for (let r = 0; r < lutSize; r++) { - // Original normalized values - const origR = r / (lutSize - 1); - const origG = g / (lutSize - 1); - const origB = b / (lutSize - 1); - - // Apply transformation - let newR = safeApplyPolynomial(origR * 255, state.transformationCoefficients.red) / 255; - let newG = safeApplyPolynomial(origG * 255, state.transformationCoefficients.green) / 255; - let newB = safeApplyPolynomial(origB * 255, state.transformationCoefficients.blue) / 255; - - // Apply brightness adjustment - newR += state.brightnessAdjustment / 255; - newG += state.brightnessAdjustment / 255; - newB += state.brightnessAdjustment / 255; - - // Apply gamma - const gammaValue = state.gammaValue || 1.0; - if (gammaValue !== 1.0) { - newR = safeGammaValue(newR, gammaValue); - newG = safeGammaValue(newG, gammaValue); - newB = safeGammaValue(newB, gammaValue); - } - - // Clamp values between 0 and 1 - newR = Math.min(1, Math.max(0, newR)); - newG = Math.min(1, Math.max(0, newG)); - newB = Math.min(1, Math.max(0, newB)); - - // Add to CUBE data - cubeText += `${newR.toFixed(6)} ${newG.toFixed(6)} ${newB.toFixed(6)}\n`; - - count++; - if (count % 128 === 0) { - updateProgress('export', (count / totalCount) * 90); - } - } - } + // Apply transformations + if (useAdvancedProcessing) { + [rOut, gOut, bOut] = applyTransformations(rOut, gOut, bOut, useAdvancedProcessing, gammaCurves); + } else { + rOut = safeApplyPolynomial(rOut, state.transformationCoefficients.red); + gOut = safeApplyPolynomial(gOut, state.transformationCoefficients.green); + bOut = safeApplyPolynomial(bOut, state.transformationCoefficients.blue); + + rOut += state.brightnessAdjustment; + gOut += state.brightnessAdjustment; + bOut += state.brightnessAdjustment; + + const gammaValue = state.gammaValue || 1.0; + if (gammaValue !== 1.0) { + rOut = safeGamma(rOut, gammaValue); + gOut = safeGamma(gOut, gammaValue); + bOut = safeGamma(bOut, gammaValue); + } } - // Store the CUBE data - state.cubeData = cubeText; + // Clamp values 0-255 + rOut = Math.min(255, Math.max(0, rOut)); + gOut = Math.min(255, Math.max(0, gOut)); + bOut = Math.min(255, Math.max(0, bOut)); - // Create a preview of the LUT - lutCanvas.width = 256; - lutCanvas.height = 16; - lutCtx.clearRect(0, 0, lutCanvas.width, lutCanvas.height); + // Convert back to 0-1 range for CUBE format + rOut = rOut / 255; + gOut = gOut / 255; + bOut = bOut / 255; - // Draw a gradient to visualize the LUT - const gradient = lutCtx.createLinearGradient(0, 0, lutCanvas.width, 0); - for (let i = 0; i <= 1; i += 0.1) { - // Original color - const r = i * 255; - const g = i * 255; - const b = i * 255; - - // Transformed color - let newR = safeApplyPolynomial(r, state.transformationCoefficients.red); - let newG = safeApplyPolynomial(g, state.transformationCoefficients.green); - let newB = safeApplyPolynomial(b, state.transformationCoefficients.blue); - - // Apply brightness and gamma - newR += state.brightnessAdjustment; - newG += state.brightnessAdjustment; - newB += state.brightnessAdjustment; - - if (state.gammaValue !== 1.0) { - newR = safeGamma(newR, state.gammaValue); - newG = safeGamma(newG, state.gammaValue); - newB = safeGamma(newB, state.gammaValue); - } - - // Clamp values - newR = Math.min(255, Math.max(0, Math.round(newR))); - newG = Math.min(255, Math.max(0, Math.round(newG))); - newB = Math.min(255, Math.max(0, Math.round(newB))); - - const color = `rgb(${newR}, ${newG}, ${newB})`; - gradient.addColorStop(i, color); - } + // Format with proper precision and add to data array + cubeData.push(`${rOut.toFixed(6)} ${gOut.toFixed(6)} ${bOut.toFixed(6)}`); - lutCtx.fillStyle = gradient; - lutCtx.fillRect(0, 0, lutCanvas.width, lutCanvas.height); + // Update progress every so often + completedOps++; + if (completedOps % Math.floor(totalOps / 10) === 0) { + const progress = Math.floor((completedOps / totalOps) * 100); + updateProgress('export', progress); + } + } } + } + + // Combine header and data + const fullCubeData = header + cubeData.join('\n'); + + // Store CUBE data in state for download + state.cubeData = fullCubeData; + + // Generate a visual preview for the CUBE LUT + generateCubePreview(lutCanvas, lutCtx); + + return fullCubeData; + } + + // Generate a visual representation for the CUBE LUT preview + function generateCubePreview(lutCanvas, lutCtx) { + // Set canvas dimensions for preview + lutCanvas.width = 512; + lutCanvas.height = 512; + lutCtx.clearRect(0, 0, lutCanvas.width, lutCanvas.height); + + // Display size info and create a visual representation + lutCtx.fillStyle = '#f8f8f8'; + lutCtx.fillRect(0, 0, lutCanvas.width, lutCanvas.height); + + // Draw header info + lutCtx.fillStyle = '#333333'; + lutCtx.font = 'bold 24px sans-serif'; + lutCtx.fillText(`CUBE LUT (${state.lutSize}×${state.lutSize}×${state.lutSize})`, 40, 40); + + // Draw file info + lutCtx.font = '16px monospace'; + lutCtx.fillText(`# CUBE LUT generated by WebLUT`, 40, 70); + lutCtx.fillText(`LUT_3D_SIZE ${state.lutSize}`, 40, 95); + + // Draw a grid representation - slightly smaller and positioned higher + const gridSize = Math.min(16, state.lutSize); // Limit grid lines for cleaner visual + const cellSize = 280 / gridSize; + const startX = 116; + const startY = 120; + + // Draw grid borders + lutCtx.strokeStyle = '#666666'; + lutCtx.lineWidth = 1; + + // Draw X-Y plane + lutCtx.strokeRect(startX, startY, 280, 280); + + // Draw some sample lines to represent the 3D grid + lutCtx.lineWidth = 0.5; + for (let i = 1; i < gridSize; i++) { + // X-Y plane horizontal lines + lutCtx.beginPath(); + lutCtx.moveTo(startX, startY + i * cellSize); + lutCtx.lineTo(startX + 280, startY + i * cellSize); + lutCtx.stroke(); - updateProgress('export', 100); - logMessage('LUT generation complete!', 'success'); + // X-Y plane vertical lines + lutCtx.beginPath(); + lutCtx.moveTo(startX + i * cellSize, startY); + lutCtx.lineTo(startX + i * cellSize, startY + 280); + lutCtx.stroke(); + } + + // Indicate file is ready for download + lutCtx.font = 'bold 18px sans-serif'; + lutCtx.fillStyle = '#009900'; + lutCtx.fillText('CUBE file ready for download!', 156, 420); + + // Draw legend with sample colors - moved higher up + lutCtx.fillStyle = '#333333'; + lutCtx.font = 'bold 16px sans-serif'; + lutCtx.fillText('Sample Color Transformations:', 40, 440); + + // Draw sample color squares with transformations + const sampleColors = [ + [0, 0, 0], // Black + [255, 0, 0], // Red + [0, 255, 0], // Green + [0, 0, 255], // Blue + [255, 255, 0], // Yellow + [255, 0, 255], // Magenta + [0, 255, 255], // Cyan + [255, 255, 255] // White + ]; + + const colorNames = ['Black', 'Red', 'Green', 'Blue', 'Yellow', 'Magenta', 'Cyan', 'White']; + + // Draw original and transformed color pairs - reorganized in two rows + for (let i = 0; i < sampleColors.length; i++) { + // Position in two rows of 4 items + const x = 40 + (i % 4) * 120; + const y = 460 + Math.floor(i / 4) * 40; - // Show download button - document.getElementById('lut-download-container').style.display = 'block'; + const [r, g, b] = sampleColors[i]; + + // Draw original color + lutCtx.fillStyle = `rgb(${r},${g},${b})`; + lutCtx.fillRect(x, y, 15, 15); + + // Apply transformation + const transformedColor = applyTransformations(r, g, b, + state.useAdvancedProcessing, + state.gammaCurves); + + // Ensure valid RGB values + const [tR, tG, tB] = transformedColor.map(v => Math.min(255, Math.max(0, Math.round(v)))); + + // Draw transformed color + lutCtx.fillStyle = `rgb(${tR},${tG},${tB})`; + lutCtx.fillRect(x + 25, y, 15, 15); + + // Draw label + lutCtx.fillStyle = '#333333'; + lutCtx.font = '12px sans-serif'; + lutCtx.fillText(colorNames[i], x + 50, y + 12); + } } - + // Safe version of applyPolynomial that prevents NaN values function safeApplyPolynomial(value, coefficients) { if (!coefficients || !Array.isArray(coefficients)) { @@ -1846,15 +1989,55 @@

LUT Preview

} // Safe version of gamma calculation to prevent NaN - function safeGamma(value, gamma) { + function safeGamma(value, gamma, isNormalized = false) { if (!Number.isFinite(value) || value < 0) return 0; if (value === 0) return 0; - const normalizedValue = Math.max(0, Math.min(1, value / 255)); - const result = Math.pow(normalizedValue, 1 / (gamma || 1.0)) * 255; + let normalizedValue, result; + + if (isNormalized) { + normalizedValue = Math.max(0, Math.min(1, value)); + result = Math.pow(normalizedValue, 1 / (gamma || 1.0)); + // Result is already normalized + } else { + normalizedValue = Math.max(0, Math.min(1, value / 255)); + result = Math.pow(normalizedValue, 1 / (gamma || 1.0)) * 255; + } return Number.isFinite(result) ? result : value; } + + function processPixelAdvanced(r, g, b, models, brightnessAdjustment, gammaCurves) { + // Check if we have multivariate models for each channel + const hasRedModel = models.multivariate && models.multivariate.red; + const hasGreenModel = models.multivariate && models.multivariate.green; + const hasBlueModel = models.multivariate && models.multivariate.blue; + + // Apply transformation based on available models + let newR = hasRedModel ? + applyPoly3d(r, g, b, models.multivariate.red) : + safeApplyPolynomial(r, models.basic.red); + + let newG = hasGreenModel ? + applyPoly3d(r, g, b, models.multivariate.green) : + safeApplyPolynomial(g, models.basic.green); + + let newB = hasBlueModel ? + applyPoly3d(r, g, b, models.multivariate.blue) : + safeApplyPolynomial(b, models.basic.blue); + + // Apply brightness adjustment + newR += brightnessAdjustment; + newG += brightnessAdjustment; + newB += brightnessAdjustment; + + // Apply advanced gamma with roll-off curves if available + if (gammaCurves) { + return applyAdvancedGamma(newR, newG, newB, gammaCurves); + } + + return [newR, newG, newB]; + } // Safe version of gamma calculation for normalized values (0-1) function safeGammaValue(value, gamma) { @@ -1867,30 +2050,63 @@

LUT Preview

return Number.isFinite(result) ? result : value; } - // Download the generated LUT function downloadLUT() { - if (state.lutFormat === 'png') { - const lutCanvas = document.getElementById('lut-preview'); - const dataURL = lutCanvas.toDataURL('image/png'); - const a = document.createElement('a'); - a.href = dataURL; - a.download = 'custom-lut.png'; - a.click(); - } else if (state.lutFormat === 'cube' && state.cubeData) { - const blob = new Blob([state.cubeData], { type: 'text/plain' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'custom-lut.cube'; - a.click(); - URL.revokeObjectURL(url); - } - } + if (state.lutFormat === 'png') { + const lutCanvas = document.getElementById('lut-preview'); + + // Ensure the canvas is properly sized for download + if (lutCanvas.width !== 512 || lutCanvas.height !== 512) { + logMessage('Resizing LUT to full 512x512 for download...', 'info'); + + // Create a temporary canvas for the full-sized LUT + const downloadCanvas = document.createElement('canvas'); + downloadCanvas.width = 512; + downloadCanvas.height = 512; + + // Copy the preview LUT to the download canvas, stretching if needed + const downloadCtx = downloadCanvas.getContext('2d'); + downloadCtx.drawImage(lutCanvas, 0, 0, 512, 512); + + // Get data URL from the download canvas + const dataURL = downloadCanvas.toDataURL('image/png'); + const a = document.createElement('a'); + a.href = dataURL; + a.download = 'custom-lut.png'; + a.click(); + } else { + // The canvas is already the right size + const dataURL = lutCanvas.toDataURL('image/png'); + const a = document.createElement('a'); + a.href = dataURL; + a.download = 'custom-lut.png'; + a.click(); + } + } else if (state.lutFormat === 'cube' && state.cubeData) { + // Create blob from cube data + const blob = new Blob([state.cubeData], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + + // Create temporary link and trigger download + const a = document.createElement('a'); + a.href = url; + a.download = 'custom-lut.cube'; + document.body.appendChild(a); // Need to append to body for Firefox + a.click(); + document.body.removeChild(a); // Clean up + URL.revokeObjectURL(url); + + logMessage('CUBE file downloaded successfully!', 'success'); + } else { + logMessage('No LUT data available for download', 'error'); + } + } // Initialize additional event listeners function initializeEventListeners() { - // Nothing additional to initialize currently - // This function can be expanded as needed + document.getElementById('advanced-processing-toggle').addEventListener('change', function() { + state.useAdvancedProcessing = this.checked; + logMessage(`Advanced processing ${this.checked ? 'enabled' : 'disabled'}`, 'info'); + }); } // Display the test image @@ -1900,7 +2116,7 @@

LUT Preview

const originalCtx = originalCanvas.getContext('2d'); // Set canvas size - const maxWidth = 400; + const maxWidth = 1280; const scale = Math.min(1, maxWidth / img.width); originalCanvas.width = img.width * scale; originalCanvas.height = img.height * scale; @@ -1985,17 +2201,477 @@

LUT Preview

// Initialize test image upload function initializeTestImageUpload() { - const fileInput = document.getElementById('test-upload'); - fileInput.addEventListener('change', function(event) { - const file = event.target.files[0]; - if (file) { - loadImage(file, 'test'); - } - }); - - + const testFileInput = document.getElementById('test-upload'); + if (testFileInput) { + testFileInput.addEventListener('change', function(event) { + const file = event.target.files[0]; + if (file) { + loadImage(file, 'test'); + logMessage(`Test image loaded successfully`, 'success'); + } + }); + } } - + + // Advanced multivariate polynomial regression + function polyfit3d(r, g, b, targetValues, degree) { + // Create degrees for multivariate polynomial + const degrees = []; + for (let i = 0; i < degree; i++) { + for (let j = 0; j < degree; j++) { + for (let k = 0; k < degree; k++) { + degrees.push([i, j, k]); + } + } + } + + // Create design matrix + const matrix = []; + const n = r.length; + + for (let i = 0; i < n; i++) { + const row = []; + for (const [di, dj, dk] of degrees) { + // Calculate r^i * g^j * b^k + row.push(Math.pow(r[i], di) * Math.pow(g[i], dj) * Math.pow(b[i], dk)); + } + matrix.push(row); + } + + // Solve using normal equations (X^T * X)^-1 * X^T * y + // First calculate X^T * X + const XtX = []; + const m = degrees.length; + + for (let i = 0; i < m; i++) { + XtX.push(new Array(m).fill(0)); + } + + for (let i = 0; i < m; i++) { + for (let j = 0; j < m; j++) { + for (let k = 0; k < n; k++) { + XtX[i][j] += matrix[k][i] * matrix[k][j]; + } + } + } + + // Calculate X^T * y + const Xty = new Array(m).fill(0); + for (let i = 0; i < m; i++) { + for (let k = 0; k < n; k++) { + Xty[i] += matrix[k][i] * targetValues[k]; + } + } + + // Create augmented matrix [XtX|Xty] + const augmentedMatrix = []; + for (let i = 0; i < m; i++) { + augmentedMatrix.push([...XtX[i], Xty[i]]); + } + + // Gaussian elimination + for (let i = 0; i < m; i++) { + // Find pivot + let maxRow = i; + for (let j = i + 1; j < m; j++) { + if (Math.abs(augmentedMatrix[j][i]) > Math.abs(augmentedMatrix[maxRow][i])) { + maxRow = j; + } + } + + // Swap rows + if (maxRow !== i) { + [augmentedMatrix[i], augmentedMatrix[maxRow]] = [augmentedMatrix[maxRow], augmentedMatrix[i]]; + } + + // Eliminate below + for (let j = i + 1; j < m; j++) { + if (augmentedMatrix[i][i] === 0) continue; // Skip if pivot is zero + + const factor = augmentedMatrix[j][i] / augmentedMatrix[i][i]; + for (let k = i; k <= m; k++) { + augmentedMatrix[j][k] -= factor * augmentedMatrix[i][k]; + } + } + } + + // Back substitution + const coefficients = new Array(m).fill(0); + for (let i = m - 1; i >= 0; i--) { + if (augmentedMatrix[i][i] === 0) continue; // Skip if diagonal element is zero + + let sum = 0; + for (let j = i + 1; j < m; j++) { + sum += augmentedMatrix[i][j] * coefficients[j]; + } + coefficients[i] = (augmentedMatrix[i][m] - sum) / augmentedMatrix[i][i]; + } + + return { coefficients, degrees }; + } + + function applyPoly3d(r, g, b, model) { + if (!model || !model.coefficients || !model.degrees) { + console.error('Invalid model provided to applyPoly3d'); + return r; // Return input value as fallback + } + + let result = 0; + const { coefficients, degrees } = model; + + try { + for (let i = 0; i < coefficients.length; i++) { + if (i >= degrees.length) { + console.warn('Coefficients and degrees length mismatch'); + break; + } + + const [di, dj, dk] = degrees[i]; + const coef = coefficients[i] || 0; // Handle undefined coefficients + + // Protect against large powers which can cause overflow + const term = coef * + Math.pow(Math.min(255, Math.max(0, r)), di) * + Math.pow(Math.min(255, Math.max(0, g)), dj) * + Math.pow(Math.min(255, Math.max(0, b)), dk); + + // Check if term is valid number + if (isFinite(term)) { + result += term; + } + } + + return isFinite(result) ? result : r; // Fallback to input value if result is invalid + } catch (error) { + console.error('Error in applyPoly3d:', error); + return r; // Fallback to input value on error + } + } + + function generateGammaCurves() { + // Create highlight gamma curve (similar to Python LUT2) + const highlightCurve = new Array(256); + for (let i = 0; i < 256; i++) { + // For 225-255 range + if (i > 225) { + // Similar to original Python calculation + // Smoother curve - x is input (225-255), y is output + const x = i - 225; + const y = x + Math.pow(x / 30, 0.8) * 30; + highlightCurve[i] = Math.min(255, Math.max(0, Math.round(225 + y))); + } else { + highlightCurve[i] = i; + } + } + + // Create shadow gamma curve (similar to Python LUT1) + const shadowCurve = new Array(256); + for (let i = 0; i < 256; i++) { + // For 0-30 range + if (i < 30) { + // Smoother curve for shadows + const x = 30 - i; + const y = x - Math.pow(x / 30, 0.8) * 10; + shadowCurve[i] = Math.min(255, Math.max(0, Math.round(30 - y))); + } else { + shadowCurve[i] = i; + } + } + + return { highlightCurve, shadowCurve }; + } + + function applyAdvancedGamma(r, g, b, curves) { + const { highlightCurve, shadowCurve } = curves; + + // Apply highlight roll-off (when RGB > 225) + r = r > 225 ? highlightCurve[Math.min(255, Math.round(r))] : r; + g = g > 225 ? highlightCurve[Math.min(255, Math.round(g))] : g; + b = b > 225 ? highlightCurve[Math.min(255, Math.round(b))] : b; + + // Apply shadow roll-off (when RGB < 30) + r = r < 30 ? shadowCurve[Math.max(0, Math.round(r))] : r; + g = g < 30 ? shadowCurve[Math.max(0, Math.round(g))] : g; + b = b < 30 ? shadowCurve[Math.max(0, Math.round(b))] : b; + + return [r, g, b]; + } + + function processColorTransformation() { + logMessage('Starting color transformation processing...', 'info'); + + if (state.capturedColors.length === 0) { + logMessage('No captured colors found. Please upload a color card image first.', 'error'); + return; + } + + if (state.referenceColors.length === 0) { + logMessage('No reference colors defined. Please set up your reference colors first.', 'error'); + return; + } + + try { + // Update progress + updateProgress('process', 10); + + // Calculate basic polynomial regression models + state.transformationCoefficients.red = polynomialRegression( + state.capturedColors.map(color => color[0]), + state.referenceColors.map(color => color[0]), + state.polynomialDegree + ); + logMessage('Red channel transformation calculated', 'info'); + + updateProgress('process', 30); + state.transformationCoefficients.green = polynomialRegression( + state.capturedColors.map(color => color[1]), + state.referenceColors.map(color => color[1]), + state.polynomialDegree + ); + logMessage('Green channel transformation calculated', 'info'); + + updateProgress('process', 50); + state.transformationCoefficients.blue = polynomialRegression( + state.capturedColors.map(color => color[2]), + state.referenceColors.map(color => color[2]), + state.polynomialDegree + ); + logMessage('Blue channel transformation calculated', 'info'); + + // Store in models + state.transformationModels.basic = { + red: state.transformationCoefficients.red, + green: state.transformationCoefficients.green, + blue: state.transformationCoefficients.blue + }; + + // If advanced processing is enabled, compute multivariate model + // Add this error handling to the multivariate model calculation in processColorTransformation function + if (state.useAdvancedProcessing) { + updateProgress('process', 60); + + try { + // Create more data points using the basic coefficients + const gridSize = 3; + const grid = []; + for (let r = 0; r < 256; r += 256 / gridSize) { + for (let g = 0; g < 256; g += 256 / gridSize) { + for (let b = 0; b < 256; b += 256 / gridSize) { + grid.push([r, g, b]); + } + } + } + + // Apply basic transformation to grid points + const transformedGrid = grid.map(([r, g, b]) => [ + applyPolynomial(r, state.transformationCoefficients.red), + applyPolynomial(g, state.transformationCoefficients.green), + applyPolynomial(b, state.transformationCoefficients.blue) + ]); + + // Combine original captured colors and grid for training data + const trainColors = [...state.capturedColors, ...grid]; + const trainTargets = [...state.referenceColors, ...transformedGrid]; + + updateProgress('process', 70); + + // Perform multivariate regression for each output channel + // Use degree 2 for multivariate to avoid overfitting + const multivariateDegree = 2; + + // Prepare input arrays + const rInputs = trainColors.map(color => color[0]); + const gInputs = trainColors.map(color => color[1]); + const bInputs = trainColors.map(color => color[2]); + + try { + state.transformationModels.multivariate.red = polyfit3d( + rInputs, gInputs, bInputs, + trainTargets.map(color => color[0]), + multivariateDegree + ); + logMessage('Advanced red channel transformation calculated', 'info'); + } catch (error) { + logMessage(`Error calculating red channel advanced model: ${error.message}. Falling back to basic model.`, 'warning'); + state.transformationModels.multivariate.red = null; + } + + updateProgress('process', 80); + + try { + state.transformationModels.multivariate.green = polyfit3d( + rInputs, gInputs, bInputs, + trainTargets.map(color => color[1]), + multivariateDegree + ); + logMessage('Advanced green channel transformation calculated', 'info'); + } catch (error) { + logMessage(`Error calculating green channel advanced model: ${error.message}. Falling back to basic model.`, 'warning'); + state.transformationModels.multivariate.green = null; + } + + updateProgress('process', 90); + + try { + state.transformationModels.multivariate.blue = polyfit3d( + rInputs, gInputs, bInputs, + trainTargets.map(color => color[2]), + multivariateDegree + ); + logMessage('Advanced blue channel transformation calculated', 'info'); + } catch (error) { + logMessage(`Error calculating blue channel advanced model: ${error.message}. Falling back to basic model.`, 'warning'); + state.transformationModels.multivariate.blue = null; + } + + // Check if any of the multivariate models failed + if (!state.transformationModels.multivariate.red || + !state.transformationModels.multivariate.green || + !state.transformationModels.multivariate.blue) { + + logMessage('Some advanced models failed. Partial advanced processing will be used where available.', 'warning'); + } + + // Generate gamma curves + state.gammaCurves = generateGammaCurves(); + logMessage('Gamma curves generated', 'info'); + } catch (error) { + logMessage(`Advanced processing failed: ${error.message}. Using basic transformation only.`, 'error'); + state.transformationModels.multivariate.red = null; + state.transformationModels.multivariate.green = null; + state.transformationModels.multivariate.blue = null; + } + } + + updateProgress('process', 100); + logMessage(`Color transformation processing complete!${state.useAdvancedProcessing ? ' (Advanced mode)' : ''}`, 'success'); + + // Enable next step + document.getElementById('process-next').disabled = false; + + // If a test image is already loaded, process it with the new coefficients + if (state.testImage) { + processTestImage(); + } + } catch (error) { + logMessage(`Error processing color transformation: ${error}`, 'error'); + } + } + + function processPixelMultivariate(r, g, b, models, brightnessAdjustment, gammaCurves) { + // Apply multivariate transformation + const newR = applyPoly3d(r, g, b, models.multivariate.red) + brightnessAdjustment; + const newG = applyPoly3d(r, g, b, models.multivariate.green) + brightnessAdjustment; + const newB = applyPoly3d(r, g, b, models.multivariate.blue) + brightnessAdjustment; + + // Apply advanced gamma with roll-off curves + return applyAdvancedGamma(newR, newG, newB, gammaCurves); + } + + // Generate LUT image with enhanced processing + function generateEnhancedLUT(transformationModels, brightnessAdjustment, gammaValue) { + if (!transformationModels) { + logMessage('No color transformation available. Please process colors first.', 'error'); + return null; + } + + logMessage('Generating enhanced LUT...', 'info'); + + // Create gamma curves for specialized roll-off + const gammaCurves = generateGammaCurves(); + + // For PNG LUT (OBS compatible) + const lutCanvas = document.createElement('canvas'); + const lutCtx = lutCanvas.getContext('2d'); + lutCanvas.width = 64; + lutCanvas.height = 64; + + // Clear canvas + lutCtx.clearRect(0, 0, lutCanvas.width, lutCanvas.height); + + // Generate neutral LUT data + const lutData = lutCtx.createImageData(64, 64); + const data = lutData.data; + + // Fill the entire 64x64 grid + for (let y = 0; y < 64; y++) { + for (let x = 0; x < 64; x++) { + // Calculate RGB values from position + // In 64x64 format, each component (r,g,b) gets 8 values (0-7) + const r = Math.floor(x % 8) * 255 / 7; // R cycles every 8 pixels + const g = Math.floor(x / 8) * 255 / 7; // G cycles every 8 columns of R + const b = Math.floor(y / 8) * 255 / 7; // B changes every 8 rows + + // Calculate pixel index + const index = (y * 64 + x) * 4; + + // Set initial RGBA values + data[index] = r; + data[index + 1] = g; + data[index + 2] = b; + data[index + 3] = 255; // Alpha + } + } + + // Apply transformation to LUT + for (let i = 0; i < data.length; i += 4) { + // Get original values + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + + // Process pixel with multivariate model + const [newR, newG, newB] = processPixelMultivariate( + r, g, b, + transformationModels, + brightnessAdjustment, + gammaCurves + ); + + // Clamp values + data[i] = Math.min(255, Math.max(0, Math.round(newR))); + data[i + 1] = Math.min(255, Math.max(0, Math.round(newG))); + data[i + 2] = Math.min(255, Math.max(0, Math.round(newB))); + } + + // Put the data back to the canvas + lutCtx.putImageData(lutData, 0, 0); + + logMessage('Enhanced LUT generation complete!', 'success'); + return lutCanvas; + } + + // Create gamma curves for roll-off like in the Python code + function generateGammaCurves() { + // Create highlight roll-off curve (LUT2 in Python) + const highlightCurve = new Array(256); + for (let i = 0; i < 256; i++) { + if (i > 225) { + // Similar to your Python code: for y in range(22500,25500) + const y = i * 100; + const x = Math.round((y/100-225) + 1/(1.2**(225-y/100)) + 225); + highlightCurve[i] = x; + } else { + highlightCurve[i] = i; + } + } + + // Create shadow roll-off curve (LUT1 in Python, which is 255-flip(LUT2)) + const shadowCurve = new Array(256); + for (let i = 0; i < 256; i++) { + if (i < 30) { + // Inverse of the highlight curve logic + const y = (255 - i) * 100; + const x = Math.round((y/100-225) + 1/(1.2**(225-y/100)) + 225); + shadowCurve[i] = 255 - x; + } else { + shadowCurve[i] = i; + } + } + + return { highlightCurve, shadowCurve }; + } +