Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Adrian Mariano
2025-06-13 17:52:50 -04:00
5 changed files with 85 additions and 39 deletions

View File

@@ -21,7 +21,7 @@ jobs:
run: sudo apt-get install python3-pip python3-dev python3-setuptools python3-pil gifsicle libfuse2 run: sudo apt-get install python3-pip python3-dev python3-setuptools python3-pil gifsicle libfuse2
- name: Install OpenSCAD-DocsGen package. - name: Install OpenSCAD-DocsGen package.
run: sudo pip3 install openscad-docsgen run: sudo pip3 install openscad-docsgen imageio
- name: Install OpenSCAD - name: Install OpenSCAD
run: | run: |

View File

@@ -21,7 +21,7 @@ jobs:
run: sudo apt-get install python3-pip python3-dev python3-setuptools python3-pil gifsicle libfuse2 run: sudo apt-get install python3-pip python3-dev python3-setuptools python3-pil gifsicle libfuse2
- name: Install OpenSCAD-DocsGen package. - name: Install OpenSCAD-DocsGen package.
run: sudo pip3 install openscad-docsgen run: sudo pip3 install openscad-docsgen imageio
- name: Install OpenSCAD - name: Install OpenSCAD
run: | run: |

View File

@@ -43,7 +43,7 @@ jobs:
run: sudo apt-get install python3-pip python3-dev python3-setuptools python3-pil libfuse2 run: sudo apt-get install python3-pip python3-dev python3-setuptools python3-pil libfuse2
- name: Install OpenSCAD-DocsGen package. - name: Install OpenSCAD-DocsGen package.
run: sudo pip3 install openscad-docsgen run: sudo pip3 install openscad-docsgen imageio
- name: Install OpenSCAD - name: Install OpenSCAD
run: | run: |

View File

@@ -3523,7 +3523,7 @@ function _showstats_isosurface(voxsize, bbox, isoval, cubes, triangles, faces) =
// Function&Module: contour() // Function&Module: contour()
// Synopsis: Creates a 2D contour from a function or array of values. // Synopsis: Creates a 2D contour from a function or array of values.
// SynTags: Geom,Path,Region // SynTags: Geom,Path,Region
// Topics: Isosurfaces, Path Generators (2D), Regions // Topics: Contours, Path Generators (2D), Regions
// Usage: As a module // Usage: As a module
// contour(f, isovalue, bounding_box, pixel_size, [pixel_count=], [use_centers=], [smoothing=], [exact_bounds=], [show_stats=], [show_box=], ...) [ATTACHMENTS]; // contour(f, isovalue, bounding_box, pixel_size, [pixel_count=], [use_centers=], [smoothing=], [exact_bounds=], [show_stats=], [show_box=], ...) [ATTACHMENTS];
// Usage: As a function // Usage: As a function

View File

@@ -8,8 +8,11 @@ Version 6: 23 April 2025 - added cropping UI
Version 7: 25 April 2025 - added contrast and threshold sliders Version 7: 25 April 2025 - added contrast and threshold sliders
Version 8: 26 April 2025 - added file size estimate to output section Version 8: 26 April 2025 - added file size estimate to output section
Version 9: 20 May 2025 - improved appearance UI, added Sobel edge detection Version 9: 20 May 2025 - improved appearance UI, added Sobel edge detection
Version 10: 21 May 2025 - Added array_name_size value at top of output file
Version 11: 22 May 2025 - Fixed filter artifacts at image edges, added sharpening filter
Version 12: 30 May 2025 - Made filters mutually exclusive
--> -->
<title>Image to OpenSCAD array, v9</title><!-- REMEMBER TO CHANGE VERSION --> <title>Image to OpenSCAD array, v12</title><!-- REMEMBER TO CHANGE VERSION -->
<meta charset="UTF-8"> <meta charset="UTF-8">
<style> <style>
body { font-family: sans-serif; padding-left:1em; padding-right:1em;} body { font-family: sans-serif; padding-left:1em; padding-right:1em;}
@@ -222,13 +225,18 @@ Alpha channel is ignored. After processing the image as desired, you may save it
<div style="margin-top:8px;"> <div style="margin-top:8px;">
<label><input type="checkbox" id="invertBrightness"> Invert brightness</label> <label><input type="checkbox" id="invertBrightness"> Invert brightness</label>
</div> </div>
<div style="margin:8px 0;"> <fieldset style="margin:8px 0;">
<legend style="font-size:medium;">Filter</legend>
<input type="radio" name="filterSelect" value="blur" checked>
<label for="blurRadius">Gaussian blur radius (pixels):</label> <label for="blurRadius">Gaussian blur radius (pixels):</label>
<input type="number" id="blurRadius" size="5" min="0" max="20" value="0"><br> <input type="number" id="blurRadius" size="5" min="0" max="20" value="0"><br>
<label for="sobelRadius" class="tooltip">Edge detect radius (pixels): <input type="radio" name="filterSelect" value="sharp">
<span class="tooltiptext">Sobel filter uses own radius if Gaussian blur=0</span></label> <label for="sharpenRadius">Sharpen radius (pixels):
<input type="number" id="sharpenRadius" size="5" min="0" max="20" value="0"><br>
<input type="radio" name="filterSelect" value="edge">
<label for="sobelRadius">Edge detect radius (pixels):
<input type="number" id="sobelRadius" size="5" min="0" max="20" value="0"> <input type="number" id="sobelRadius" size="5" min="0" max="20" value="0">
</div> </fieldset>
<div class="slider-row"> <div class="slider-row">
<label for="contrast" class="slider-label tooltip">Contrast <label for="contrast" class="slider-label tooltip">Contrast
@@ -290,8 +298,10 @@ Alpha channel is ignored. After processing the image as desired, you may save it
const cropLeft = document.getElementById('cropLeft'); const cropLeft = document.getElementById('cropLeft');
const cropRight = document.getElementById('cropRight'); const cropRight = document.getElementById('cropRight');
const cropBottom = document.getElementById('cropBottom'); const cropBottom = document.getElementById('cropBottom');
const filterSelect = document.getElementById('filterSelect');
const blurRadiusInput = document.getElementById('blurRadius'); const blurRadiusInput = document.getElementById('blurRadius');
const sobelRadiusInput = document.getElementById('sobelRadius'); const sobelRadiusInput = document.getElementById('sobelRadius');
const sharpenRadiusInput = document.getElementById('sharpenRadius');
const contrastInput = document.getElementById('contrast'); const contrastInput = document.getElementById('contrast');
const contrastValue = document.getElementById('contrastValue'); const contrastValue = document.getElementById('contrastValue');
const thresholdInput = document.getElementById('threshold'); const thresholdInput = document.getElementById('threshold');
@@ -354,47 +364,47 @@ Alpha channel is ignored. After processing the image as desired, you may save it
return kernel.map(v => v / norm); return kernel.map(v => v / norm);
} }
function convolve1DHorizontal(matrix, kernel, normalize=true) { function convolve1DHorizontal(matrix, kernel) {
const width = matrix[0].length; const width = matrix[0].length;
const height = matrix.length; const height = matrix.length;
const r = Math.floor(kernel.length / 2); const r = Math.floor(kernel.length / 2);
const result = []; const result = [];
let indx, nx;
for (let y = 0; y < height; y++) { for (let y = 0; y < height; y++) {
result[y] = []; result[y] = [];
for (let x = 0; x < width; x++) { for (let x = 0; x < width; x++) {
let sum = 0; let sum = 0;
let weightSum = 0;
for (let k = -r; k <= r; k++) { for (let k = -r; k <= r; k++) {
const nx = x + k; indx = x+k;
nx = indx<0 ? -indx : (indx>=width ? 2*(width-1)-indx : indx); // reflect edges
if (nx >= 0 && nx < width) { if (nx >= 0 && nx < width) {
sum += matrix[y][nx] * kernel[k+r]; sum += matrix[y][nx] * kernel[k+r];
weightSum += kernel[k+r];
} }
} }
result[y][x] = normalize ? (weightSum !== 0 ? sum / weightSum : 0) : sum; result[y][x] = sum;
} }
} }
return result; return result;
} }
function convolve1DVertical(matrix, kernel, normalize=true) { function convolve1DVertical(matrix, kernel) {
const width = matrix[0].length; const width = matrix[0].length;
const height = matrix.length; const height = matrix.length;
const r = Math.floor(kernel.length / 2); const r = Math.floor(kernel.length / 2);
const result = []; const result = [];
let indx, ny;
for (let y = 0; y < height; y++) { for (let y = 0; y < height; y++) {
result[y] = []; result[y] = [];
for (let x = 0; x < width; x++) { for (let x = 0; x < width; x++) {
let sum = 0; let sum = 0;
let weightSum = 0;
for (let k = -r; k <= r; k++) { for (let k = -r; k <= r; k++) {
const ny = y + k; indx = y+k;
ny = indx<0 ? -indx : (indx >= height ? 2*(height-1)-indx : indx); // reflect edges
if (ny >= 0 && ny < height) { if (ny >= 0 && ny < height) {
sum += matrix[ny][x] * kernel[k+r]; sum += matrix[ny][x] * kernel[k+r];
weightSum += kernel[k+r];
} }
} }
result[y][x] = normalize ? (weightSum !== 0 ? sum / weightSum : 0) : sum; result[y][x] = sum;
} }
} }
return result; return result;
@@ -415,18 +425,34 @@ Alpha channel is ignored. After processing the image as desired, you may save it
} }
function applyGaussianBlur(matrix, blurRadius) { function applyGaussianBlur(matrix, blurRadius) {
if (blurRadius <= 0) return matrix;
const gKernel = gaussianKernel1D(blurRadius) const gKernel = gaussianKernel1D(blurRadius)
g1 = convolve1DVertical(matrix, gKernel); g1 = convolve1DVertical(matrix, gKernel);
return convolve1DHorizontal(g1, gKernel); return convolve1DHorizontal(g1, gKernel);
} }
function applySobel(matrix, sobelRadius, blurRadius) { function applySharpen(original, radius, k=1.0) {
if (sobelRadius <= 0) return matrix; // No edge detection if (radius <= 0) return original;
const sobelSize = 2 * sobelRadius + 1; const height = original.length;
const width = original[0].length;
blurred = applyGaussianBlur(original, radius);
const result = [];
for (let y = 0; y < height; y++) {
result[y] = [];
for (let x = 0; x < width; x++) {
result[y][x] = original[y][x] + k * (original[y][x] - blurred[y][x]);
}
}
return result;
}
function applySobel(matrix, radius) {
if (radius <= 0) return matrix; // No edge detection
const sobelSize = 2 * radius + 1;
const dKernel = sobelDerivativeKernel(sobelSize); const dKernel = sobelDerivativeKernel(sobelSize);
let gblur = blurRadius === 0 ? applyGaussianBlur(matrix, sobelRadius) : matrix; let gblur = applyGaussianBlur(matrix, radius);
gx = convolve1DHorizontal(gblur, dKernel, false); gx = convolve1DHorizontal(gblur, dKernel);
gy = convolve1DVertical(gblur, dKernel, false); gy = convolve1DVertical(gblur, dKernel);
return computeEdgeMagnitude(gx, gy); return computeEdgeMagnitude(gx, gy);
} }
@@ -491,13 +517,29 @@ Alpha channel is ignored. After processing the image as desired, you may save it
brightnessMatrix.push(row); brightnessMatrix.push(row);
} }
// apply blurring to the grayscale image // apply filter
const blurRadius = parseInt(blurRadiusInput.value) || 0; const blurRadius = parseInt(blurRadiusInput.value) || 0;
const blurredMatrix = applyGaussianBlur(brightnessMatrix, blurRadius); const sharpenRadius = parseInt(sharpenRadiusInput.value) || 0;
// apply Sobel edge detection
const sobelRadius = parseInt(sobelRadiusInput.value) || 0; const sobelRadius = parseInt(sobelRadiusInput.value) || 0;
const sobelMatrix = applySobel(blurredMatrix, sobelRadius, blurRadius); let filteredMatrix = [];
switch(document.querySelector('input[name="filterSelect"]:checked').value) {
// any of the filters return the original if the radius=0
case "blur":
console.log("blur");
filteredMatrix = applyGaussianBlur(brightnessMatrix, blurRadius);
break;
case "sharp":
console.log("sharp");
filteredMatrix = applySharpen(brightnessMatrix, sharpenRadius);
break;
case "edge":
console.log("edge");
filteredMatrix = applySobel(brightnessMatrix, sobelRadius);
break;
default:
console.log("none");
filteredMatrix = brightnessMatrix;
}
// crop the matrix, gather min and max values in crop area // crop the matrix, gather min and max values in crop area
const cropMatrix = []; const cropMatrix = [];
@@ -505,14 +547,14 @@ Alpha channel is ignored. After processing the image as desired, you may save it
let cropx2 = parseInt(cropID[edgeID[0]].value) || 0; let cropx2 = parseInt(cropID[edgeID[0]].value) || 0;
let cropy1 = parseInt(cropID[edgeID[1]].value) || 0; let cropy1 = parseInt(cropID[edgeID[1]].value) || 0;
let cropy2 = parseInt(cropID[edgeID[3]].value) || 0; let cropy2 = parseInt(cropID[edgeID[3]].value) || 0;
let min = 255; let min = 32000;
let max = 0; let max = -32000;
for (let y=cropy1; y<uncropDim.height-cropy2; y++) { for (let y=cropy1; y<uncropDim.height-cropy2; y++) {
const row = []; const row = [];
for(let x=cropx1; x<uncropDim.width-cropx2; x++) { for(let x=cropx1; x<uncropDim.width-cropx2; x++) {
row.push(sobelMatrix[y][x]); row.push(filteredMatrix[y][x]);
min = Math.min(min, sobelMatrix[y][x]); min = Math.min(min, filteredMatrix[y][x]);
max = Math.max(max, sobelMatrix[y][x]); max = Math.max(max, filteredMatrix[y][x]);
} }
cropMatrix.push(row); cropMatrix.push(row);
} }
@@ -625,8 +667,11 @@ Alpha channel is ignored. After processing the image as desired, you may save it
// set up event listeners for all the input gadgets // set up event listeners for all the input gadgets
[blurRadiusInput, sobelRadiusInput, contrastInput, thresholdInput, [blurRadiusInput, sobelRadiusInput, sharpenRadiusInput, contrastInput, thresholdInput,
...document.querySelectorAll('input[name="grayModel"]')].forEach(el => el.addEventListener('input', processImage)); ...document.querySelectorAll('input[name="grayModel"]'),
...document.querySelectorAll('input[name="filterSelect"]')
].forEach(el => el.addEventListener('input', processImage)
);
resizeWidthInput.addEventListener('input', function () { resizeWidthInput.addEventListener('input', function () {
let min = parseInt(this.min); let min = parseInt(this.min);
@@ -772,9 +817,10 @@ Alpha channel is ignored. After processing the image as desired, you may save it
return " [" + row.map(val => useUnit ? parseFloat((val/255.0).toFixed(3)) : val).join(",") + "]"; return " [" + row.map(val => useUnit ? parseFloat((val/255.0).toFixed(3)) : val).join(",") + "]";
}).join(",\n"); }).join(",\n");
const introcomment = " = [ // " + cropDim.width + "×" + cropDim.height + "\n"; const introcomment = " = [ // " + cropDim.width + "×" + cropDim.height + "\n";
const dimSuffix = "_"+cropDim.width + "x" + cropDim.height const sizevar = (arrayName.value.length>0 ? arrayName.value : 'image_array')+"_size = [" + cropDim.width + "," + cropDim.height + "];\n";
const openscadArray = (arrayName.value.length>0 ? arrayName.value : 'image_array') + introcomment + arrayContent + "\n];"; const openscadArray = sizevar + (arrayName.value.length>0 ? arrayName.value : 'image_array') + introcomment + arrayContent + "\n];";
const blob = new Blob([openscadArray], { type: "text/plain" }); const blob = new Blob([openscadArray], { type: "text/plain" });
const dimSuffix = "_"+cropDim.width + "x" + cropDim.height;
let filename = (arrayName.value.length>0 ? arrayName.value : "image_array") + dimSuffix + '.scad'; let filename = (arrayName.value.length>0 ? arrayName.value : "image_array") + dimSuffix + '.scad';
if (window.showSaveFilePicker) { if (window.showSaveFilePicker) {
saveWithFilePicker(blob, filename); saveWithFilePicker(blob, filename);