add-license-1
halfmonty 2019-02-28 20:35:52 -06:00 zatwierdzone przez GitHub
commit 9bebcaf803
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
7 zmienionych plików z 1980 dodań i 0 usunięć

392
index.html 100644
Wyświetl plik

@ -0,0 +1,392 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/gh/nicolaspanel/numjs@0.15.1/dist/numjs.min.js"></script>
<script src="ndarray.js"></script>
<script src="loading-bar.js"></script>
<title>Hello OpenCV.js</title>
<link rel="stylesheet" type="text/css" href="loading-bar.css" />
</head>
<body>
<h2>Hello OpenCV.js</h2>
<p id="status">OpenCV.js is loading...</p>
<div id="loadingbar"></div><br/><br/>
<div>
<div class="inputoutput">
<div class="caption">imageSrc <input type="file" id="fileInput" name="file" /></div><br/>
<img id="imageSrc" alt="No Image" />
</div>
<div class="inputoutput">
<div class="caption">Temp Formatted output</div>
<canvas id="canvasOutput" ></canvas>
</div>
<div class="inputoutput">
<div class="caption">Line Output</div>
<canvas id="canvasOutput2" ></canvas>
</div>
</div>
<script type="text/javascript">
let MAX_LINES = 4000;
let N_PINS = 36*8;
let MIN_LOOP = 20;
let MIN_DISTANCE = 20;
let LINE_WEIGHT = 15;
let FILENAME = "";
let SCALE = 20;
let HOOP_DIAMETER = 0.625;
let img;
// set up element variables
let imgElement = document.getElementById("imageSrc")
let inputElement = document.getElementById("fileInput");
inputElement.addEventListener("change", (e) => {
imgElement.src = URL.createObjectURL(e.target.files[0]);
}, false);
let canvas = document.getElementById("canvasOutput");
var ctx=canvas.getContext("2d");
let canvas2 = document.getElementById("canvasOutput2");
var ctx2=canvas.getContext("2d");
let status = document.getElementById("status");
var bar1 = new ldBar("#loadingbar");
var bar2 = document.getElementById('loadingbar').ldBar;
bar1.set(0);
let length;
var R = {};
//pre initilization
let pin_coords;
let center;
let radius;
let line_cache_y;
let line_cache_x;
let line_cache_length;
let line_cache_weight;
//line variables
let error;
let img_result;
let result;
let line_mask;
let line_sequence;
let pin;
let thread_length;
let last_pins;
imgElement.onload = function() {
//initially load image to canvas
let mat = cv.imread(imgElement, cv.IMREAD_GRAYSCALE);
cv.imshow('canvasOutput', mat);
mat.delete();
//read from canvas, make grayscale
let src = cv.imread("canvasOutput", cv.IMREAD_GRAYSCALE);
let img = new cv.Mat();
cv.cvtColor(src, img, cv.COLOR_RGBA2GRAY, 0);
cv.imshow('canvasOutput', img);
length = img.rows;
src.delete(); img.delete();
//circle crop canvas
ctx.globalCompositeOperation='destination-in';
ctx.beginPath();
ctx.arc(length/2,length/2, length/2, 0, Math.PI*2);
ctx.closePath();
ctx.fill();
// fetch clean grayscale cropped image back from canvas
src = cv.imread("canvasOutput");
let rgbaPlanes = new cv.MatVector();
cv.split(src, rgbaPlanes);
// Get R channel because the other channels aren't necessary in grayscale
R = rgbaPlanes.get(0);
length = R.rows;
src.delete(); rgbaPlanes.delete();
NonBlockingCalculatePins();
}
function NonBlockingCalculatePins(){
// set up necessary variables
console.log("Calculating pins...");
status.textContent = "Calculating pins...";
pin_coords = [];
center = length / 2;
radius = length / 2 - 1/2
let i = 0;
(function codeBlock(){
if(i < N_PINS){
angle = 2 * Math.PI * i / N_PINS;
pin_coords.push([Math.floor(center + radius * Math.cos(angle)),
Math.floor(center + radius * Math.sin(angle))]);
i++;
setTimeout(codeBlock, 0);
} else {
console.log('Done Calculating pins');
status.textContent = "Done Calculating pins";
NonBlockingPrecalculateLines();
}
})();
}
function NonBlockingPrecalculateLines(){
// set up necessary variables
console.log("Precalculating all lines...");
status.textContent = "Precalculating all lines...";
line_cache_y = Array.apply(null, {length: (N_PINS * N_PINS)});
line_cache_x = Array.apply(null, {length: (N_PINS * N_PINS)});
line_cache_length = Array.apply(null, {length: (N_PINS * N_PINS)}).map(Function.call, function(){return 0;});
line_cache_weight = Array.apply(null, {length: (N_PINS * N_PINS)}).map(Function.call, function(){return 1;});
let a = 0;
(function codeBlock(){
if(a < N_PINS){
for (b = a + MIN_DISTANCE; b < N_PINS; b++) {
x0 = pin_coords[a][0];
y0 = pin_coords[a][1];
x1 = pin_coords[b][0];
y1 = pin_coords[b][1];
d = Math.floor(Number(Math.sqrt((x1 - x0) * (x1 - x0) + (y1 - y0)*(y1 - y0))));
xs = linspace(x0, x1, d);
ys = linspace(y0, y1, d);
line_cache_y[b*N_PINS + a] = ys;
line_cache_y[a*N_PINS + b] = ys;
line_cache_x[b*N_PINS + a] = xs;
line_cache_x[a*N_PINS + b] = xs;
line_cache_length[b*N_PINS + a] = d;
line_cache_length[a*N_PINS + b] = d;
}
a++;
setTimeout(codeBlock, 0);
} else {
console.log('Done Precalculating Lines');
status.textContent = "Done Precalculating Lines";
NonBlockingLineCalculator();
}
})();
}
function NonBlockingLineCalculator(){
// set up necessary variables
console.log("Drawing Lines...");
status.textContent = "Drawing Lines...";
error = nj.ones([R.rows, R.cols]).multiply(0xff).subtract(nj.uint8(R.data).reshape(R.rows,R.cols));
img_result = nj.ones([R.cols, R.rows ]).multiply(0xff);
result = nj.ones([R.cols * SCALE, R.rows * SCALE]).multiply(0xff);
result = new cv.matFromArray(R.cols * SCALE, R.rows * SCALE, cv.CV_8UC1, result.selection.data);
line_mask = nj.zeros([R.cols, R.rows], 'float64');
line_sequence = [];
pin = 0;
line_sequence.push(pin);
thread_length = 0;
last_pins = [];
let l = 0;
(function codeBlock(){
if(l < MAX_LINES){
if(l%10 == 0){
draw();
}
max_err = -1;
best_pin = -1;
for(offset=MIN_DISTANCE; offset < N_PINS - MIN_DISTANCE; offset++){
test_pin = (pin + offset) % N_PINS;
if(last_pins.includes(test_pin)){
continue;
}else {
xs = line_cache_x[test_pin * N_PINS + pin];
ys = line_cache_y[test_pin * N_PINS + pin];
line_err = getLineErr(error, ys, xs) * line_cache_weight[test_pin * N_PINS + pin];
if( line_err > max_err){
max_err = line_err;
best_pin = test_pin;
}
}
}
line_sequence.push(best_pin);
xs = line_cache_x[best_pin * N_PINS + pin];
ys = line_cache_y[best_pin * N_PINS + pin];
weight = LINE_WEIGHT * line_cache_weight[best_pin * N_PINS + pin];
line_mask = nj.zeros([R.cols, R.rows], 'float64');
line_mask = setLine(line_mask, ys, xs, weight);
error = subtractArrays(error, line_mask);
p = new cv.Point(pin_coords[pin][0] * SCALE, pin_coords[pin][1] * SCALE);
p2 = new cv.Point(pin_coords[best_pin][0] * SCALE, pin_coords[best_pin][1] * SCALE);
cv.line(result, p, p2, new cv.Scalar(0, 0, 0), 4, 8, 0);
x0 = pin_coords[pin][0];
y0 = pin_coords[pin][1];
x1 = pin_coords[best_pin][0];
y1 = pin_coords[best_pin][1];
dist = Math.sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0));
thread_length += HOOP_DIAMETER / length * dist;
last_pins.push(best_pin);
if(last_pins.length > 20){
last_pins.shift();
}
pin = best_pin;
//update loading bar
bar1.set(Math.round((l / MAX_LINES) * 100))
status.textContent = l + " Lines drawn";
l++;
setTimeout(codeBlock, 0);
} else {
console.log('Done Drawing Lines');
Finalize();
}
})();
}
function draw(){
let dsize = new cv.Size(R.cols, R.rows);
let dst = new cv.Mat();
cv.resize(result, dst, dsize, 0, 0, cv.INTER_AREA);
cv.imshow('canvasOutput2', dst);
dst.delete();
}
function Finalize() {
let dsize = new cv.Size(R.cols, R.rows);
let dst = new cv.Mat();
cv.resize(result, dst, dsize, 0, 0, cv.INTER_AREA);
img_result = nj.uint8(dst.data).reshape(R.rows,R.cols);
let diff = img_result.subtract(nj.uint8(R.data).reshape(R.rows,R.cols));
mul = nj.uint8(compareMul(img_result.selection.data, R.data)).reshape(R.rows,R.cols);
absdiff = compareAbsdiff(diff.selection.data, mul.selection.data);
console.log(getSum(absdiff) / (length * length));
console.log("complete");
cv.imshow('canvasOutput2', dst);
console.log(line_sequence);
status.textContent = "Complete";
dst.delete();
}
function getLineErr(arr, coords1, coords2){
let result = new Uint8Array(coords1.length);
for(i=0;i<coords1.length;i++){
result[i] = arr.get(coords1[i], coords2[i]);
}
return getSum(result);
}
function setLine(arr, coords1, coords2, line){
for(i=0;i<coords1.length;i++){
arr.set(coords1[i], coords2[i], line);
}
return arr;
}
function compareMul(arr1, arr2){
let result = new Uint8Array(arr1.length);
for(i=0;i<arr1.length;i++){
result[i] = (arr1[i] < arr2[i]) * 254 + 1 ;
}
return result;
}
function compareAbsdiff(arr1, arr2){
let rsult = new Uint8Array(arr1.length);
for(i=0;i<arr1.length;i++){
rsult[i] = (arr1[i] * arr2[i]);
}
return rsult;
}
function subtractArrays(arr1, arr2) {
for(i=0; i<arr1.selection.data.length;i++){
arr1.selection.data[i] = arr1.selection.data[i] - arr2.selection.data[i]
if(arr1.selection.data[i] < 0){
arr1.selection.data[i] = 0;
}else if (arr1.selection.data[i] > 255){
arr1.selection.data[i] = 255;
}
}
return arr1;
}
function subtractArraysSimple(arr1, arr2) {
for(i=0; i<arr1.length;i++){
arr1[i] = arr1[i] - arr2[i];
}
return arr1;
}
function getSum(arr) {
let v = 0;
for(i=0;i<arr.length;i++){
v = v + arr[i];
}
return v;
}
function makeArr(startValue, stopValue, cardinality) {
var arr = [];
var currValue = startValue;
var step = (stopValue - startValue) / (cardinality - 1);
for (var i = 0; i < cardinality; i++) {
arr.push(Math.round(currValue + (step * i)));
}
return arr;
}
function AddRGB(arr1, arr2, arr3){
for(i=0;i<arr1.data.length;i++){
var avg = (arr1.data[i] + arr2.data[i] + arr3.data[i]);
arr1.data[i] = avg;
}
return arr1;
}
function linspace(a,b,n) {
if(typeof n === "undefined") n = Math.max(Math.round(b-a)+1,1);
if(n<2) { return n===1?[a]:[]; }
var i,ret = Array(n);
n--;
for(i=n;i>=0;i--) { ret[i] = Math.floor((i*b+(n-i)*a)/n); }
return ret;
}
function onOpenCvReady() {
// even when this is called, sometimes it's still not ready, adding slight time buffer
setTimeout(function(){
document.getElementById('status').innerHTML = 'OpenCV.js is ready.';
}, 1000);
}
</script>
<script async src="opencv.js" onload="onOpenCvReady();" type="text/javascript"></script>
</body>
</html>

1
loading-bar.css 100644
Wyświetl plik

@ -0,0 +1 @@
.ldBar{position:relative;}.ldBar.label-center > .ldBar-label{position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);text-shadow:0 0 3px #fff}.ldBar-label:after{content:"%";display:inline}.ldBar.no-percent .ldBar-label:after{content:""}

803
loading-bar.js 100644

File diff suppressed because one or more lines are too long

353
ndarray.js 100644
Wyświetl plik

@ -0,0 +1,353 @@
function iota(n) {
var result = new Array(n)
for(var i=0; i<n; ++i) {
result[i] = i
}
return result
}
function isBuffer (obj) {
return obj != null && obj.constructor != null &&
typeof obj.constructor.isBuffer === 'function' && obj.constructor.isBuffer(obj)
}
var hasTypedArrays = ((typeof Float64Array) !== "undefined")
function compare1st(a, b) {
return a[0] - b[0]
}
function order() {
var stride = this.stride
var terms = new Array(stride.length)
var i
for(i=0; i<terms.length; ++i) {
terms[i] = [Math.abs(stride[i]), i]
}
terms.sort(compare1st)
var result = new Array(terms.length)
for(i=0; i<result.length; ++i) {
result[i] = terms[i][1]
}
return result
}
function compileConstructor(dtype, dimension) {
var className = ["View", dimension, "d", dtype].join("")
if(dimension < 0) {
className = "View_Nil" + dtype
}
var useGetters = (dtype === "generic")
if(dimension === -1) {
//Special case for trivial arrays
var code =
"function "+className+"(a){this.data=a;};\
var proto="+className+".prototype;\
proto.dtype='"+dtype+"';\
proto.index=function(){return -1};\
proto.size=0;\
proto.dimension=-1;\
proto.shape=proto.stride=proto.order=[];\
proto.lo=proto.hi=proto.transpose=proto.step=\
function(){return new "+className+"(this.data);};\
proto.get=proto.set=function(){};\
proto.pick=function(){return null};\
return function construct_"+className+"(a){return new "+className+"(a);}"
var procedure = new Function(code)
return procedure()
} else if(dimension === 0) {
//Special case for 0d arrays
var code =
"function "+className+"(a,d) {\
this.data = a;\
this.offset = d\
};\
var proto="+className+".prototype;\
proto.dtype='"+dtype+"';\
proto.index=function(){return this.offset};\
proto.dimension=0;\
proto.size=1;\
proto.shape=\
proto.stride=\
proto.order=[];\
proto.lo=\
proto.hi=\
proto.transpose=\
proto.step=function "+className+"_copy() {\
return new "+className+"(this.data,this.offset)\
};\
proto.pick=function "+className+"_pick(){\
return TrivialArray(this.data);\
};\
proto.valueOf=proto.get=function "+className+"_get(){\
return "+(useGetters ? "this.data.get(this.offset)" : "this.data[this.offset]")+
"};\
proto.set=function "+className+"_set(v){\
return "+(useGetters ? "this.data.set(this.offset,v)" : "this.data[this.offset]=v")+"\
};\
return function construct_"+className+"(a,b,c,d){return new "+className+"(a,d)}"
var procedure = new Function("TrivialArray", code)
return procedure(CACHED_CONSTRUCTORS[dtype][0])
}
var code = ["'use strict'"]
//Create constructor for view
var indices = iota(dimension)
var args = indices.map(function(i) { return "i"+i })
var index_str = "this.offset+" + indices.map(function(i) {
return "this.stride[" + i + "]*i" + i
}).join("+")
var shapeArg = indices.map(function(i) {
return "b"+i
}).join(",")
var strideArg = indices.map(function(i) {
return "c"+i
}).join(",")
code.push(
"function "+className+"(a," + shapeArg + "," + strideArg + ",d){this.data=a",
"this.shape=[" + shapeArg + "]",
"this.stride=[" + strideArg + "]",
"this.offset=d|0}",
"var proto="+className+".prototype",
"proto.dtype='"+dtype+"'",
"proto.dimension="+dimension)
//view.size:
code.push("Object.defineProperty(proto,'size',{get:function "+className+"_size(){\
return "+indices.map(function(i) { return "this.shape["+i+"]" }).join("*"),
"}})")
//view.order:
if(dimension === 1) {
code.push("proto.order=[0]")
} else {
code.push("Object.defineProperty(proto,'order',{get:")
if(dimension < 4) {
code.push("function "+className+"_order(){")
if(dimension === 2) {
code.push("return (Math.abs(this.stride[0])>Math.abs(this.stride[1]))?[1,0]:[0,1]}})")
} else if(dimension === 3) {
code.push(
"var s0=Math.abs(this.stride[0]),s1=Math.abs(this.stride[1]),s2=Math.abs(this.stride[2]);\
if(s0>s1){\
if(s1>s2){\
return [2,1,0];\
}else if(s0>s2){\
return [1,2,0];\
}else{\
return [1,0,2];\
}\
}else if(s0>s2){\
return [2,0,1];\
}else if(s2>s1){\
return [0,1,2];\
}else{\
return [0,2,1];\
}}})")
}
} else {
code.push("ORDER})")
}
}
//view.set(i0, ..., v):
code.push(
"proto.set=function "+className+"_set("+args.join(",")+",v){")
if(useGetters) {
code.push("return this.data.set("+index_str+",v)}")
} else {
code.push("return this.data["+index_str+"]=v}")
}
//view.get(i0, ...):
code.push("proto.get=function "+className+"_get("+args.join(",")+"){")
if(useGetters) {
code.push("return this.data.get("+index_str+")}")
} else {
code.push("return this.data["+index_str+"]}")
}
//view.index:
code.push(
"proto.index=function "+className+"_index(", args.join(), "){return "+index_str+"}")
//view.hi():
code.push("proto.hi=function "+className+"_hi("+args.join(",")+"){return new "+className+"(this.data,"+
indices.map(function(i) {
return ["(typeof i",i,"!=='number'||i",i,"<0)?this.shape[", i, "]:i", i,"|0"].join("")
}).join(",")+","+
indices.map(function(i) {
return "this.stride["+i + "]"
}).join(",")+",this.offset)}")
//view.lo():
var a_vars = indices.map(function(i) { return "a"+i+"=this.shape["+i+"]" })
var c_vars = indices.map(function(i) { return "c"+i+"=this.stride["+i+"]" })
code.push("proto.lo=function "+className+"_lo("+args.join(",")+"){var b=this.offset,d=0,"+a_vars.join(",")+","+c_vars.join(","))
for(var i=0; i<dimension; ++i) {
code.push(
"if(typeof i"+i+"==='number'&&i"+i+">=0){\
d=i"+i+"|0;\
b+=c"+i+"*d;\
a"+i+"-=d}")
}
code.push("return new "+className+"(this.data,"+
indices.map(function(i) {
return "a"+i
}).join(",")+","+
indices.map(function(i) {
return "c"+i
}).join(",")+",b)}")
//view.step():
code.push("proto.step=function "+className+"_step("+args.join(",")+"){var "+
indices.map(function(i) {
return "a"+i+"=this.shape["+i+"]"
}).join(",")+","+
indices.map(function(i) {
return "b"+i+"=this.stride["+i+"]"
}).join(",")+",c=this.offset,d=0,ceil=Math.ceil")
for(var i=0; i<dimension; ++i) {
code.push(
"if(typeof i"+i+"==='number'){\
d=i"+i+"|0;\
if(d<0){\
c+=b"+i+"*(a"+i+"-1);\
a"+i+"=ceil(-a"+i+"/d)\
}else{\
a"+i+"=ceil(a"+i+"/d)\
}\
b"+i+"*=d\
}")
}
code.push("return new "+className+"(this.data,"+
indices.map(function(i) {
return "a" + i
}).join(",")+","+
indices.map(function(i) {
return "b" + i
}).join(",")+",c)}")
//view.transpose():
var tShape = new Array(dimension)
var tStride = new Array(dimension)
for(var i=0; i<dimension; ++i) {
tShape[i] = "a[i"+i+"]"
tStride[i] = "b[i"+i+"]"
}
code.push("proto.transpose=function "+className+"_transpose("+args+"){"+
args.map(function(n,idx) { return n + "=(" + n + "===undefined?" + idx + ":" + n + "|0)"}).join(";"),
"var a=this.shape,b=this.stride;return new "+className+"(this.data,"+tShape.join(",")+","+tStride.join(",")+",this.offset)}")
//view.pick():
code.push("proto.pick=function "+className+"_pick("+args+"){var a=[],b=[],c=this.offset")
for(var i=0; i<dimension; ++i) {
code.push("if(typeof i"+i+"==='number'&&i"+i+">=0){c=(c+this.stride["+i+"]*i"+i+")|0}else{a.push(this.shape["+i+"]);b.push(this.stride["+i+"])}")
}
code.push("var ctor=CTOR_LIST[a.length+1];return ctor(this.data,a,b,c)}")
//Add return statement
code.push("return function construct_"+className+"(data,shape,stride,offset){return new "+className+"(data,"+
indices.map(function(i) {
return "shape["+i+"]"
}).join(",")+","+
indices.map(function(i) {
return "stride["+i+"]"
}).join(",")+",offset)}")
//Compile procedure
var procedure = new Function("CTOR_LIST", "ORDER", code.join("\n"))
return procedure(CACHED_CONSTRUCTORS[dtype], order)
}
function arrayDType(data) {
if(isBuffer(data)) {
return "buffer"
}
if(hasTypedArrays) {
switch(Object.prototype.toString.call(data)) {
case "[object Float64Array]":
return "float64"
case "[object Float32Array]":
return "float32"
case "[object Int8Array]":
return "int8"
case "[object Int16Array]":
return "int16"
case "[object Int32Array]":
return "int32"
case "[object Uint8Array]":
return "uint8"
case "[object Uint16Array]":
return "uint16"
case "[object Uint32Array]":
return "uint32"
case "[object Uint8ClampedArray]":
return "uint8_clamped"
}
}
if(Array.isArray(data)) {
return "array"
}
return "generic"
}
var CACHED_CONSTRUCTORS = {
"float32":[],
"float64":[],
"int8":[],
"int16":[],
"int32":[],
"uint8":[],
"uint16":[],
"uint32":[],
"array":[],
"uint8_clamped":[],
"buffer":[],
"generic":[]
}
;(function() {
for(var id in CACHED_CONSTRUCTORS) {
CACHED_CONSTRUCTORS[id].push(compileConstructor(id, -1))
}
});
function wrappedNDArrayCtor(data, shape, stride, offset) {
if(data === undefined) {
var ctor = CACHED_CONSTRUCTORS.array[0]
return ctor([])
} else if(typeof data === "number") {
data = [data]
}
if(shape === undefined) {
shape = [ data.length ]
}
var d = shape.length
if(stride === undefined) {
stride = new Array(d)
for(var i=d-1, sz=1; i>=0; --i) {
stride[i] = sz
sz *= shape[i]
}
}
if(offset === undefined) {
offset = 0
for(var i=0; i<d; ++i) {
if(stride[i] < 0) {
offset -= (shape[i]-1)*stride[i]
}
}
}
var dtype = arrayDType(data)
var ctor_list = CACHED_CONSTRUCTORS[dtype]
while(ctor_list.length <= d+1) {
ctor_list.push(compileConstructor(dtype, ctor_list.length-1))
}
var ctor = ctor_list[d+1]
return ctor(data, shape, stride, offset)
}
//module.exports = wrappedNDArrayCtor

45
opencv.js 100644

File diff suppressed because one or more lines are too long

193
stringart.js 100644
Wyświetl plik

@ -0,0 +1,193 @@
var collections = require('collections');
var math = require('math');
var os = require('os');
var cv2 = require('cv2');
var np = require('numpy');
var time = require('time');
MAX_LINES = 4000;
N_PINS = 36*8;
MIN_LOOP = 20 // To avoid getting stuck in a loop
MIN_DISTANCE = 20 // To avoid very short lines
LINE_WEIGHT = 15 // Tweakable parameter
FILENAME = 'leki-lig.jpg';
SCALE = 25 // For making a very high resolution render, to attempt to accurately gauge how thick the thread must be
HOOP_DIAMETER = 0.625 // To calculate total thread length
tic = time.perf_counter();
img = cv2.imread(FILENAME, cv2.IMREAD_GRAYSCALE);
// Didn't bother to make it work for non-square images
assert img.shape[0] == img.shape[1];
length = img.shape[0];
function disp(image) {
cv2.imshow('image', image);
cv2.waitKey(0);
cv2.destroyAllWindows();
}
// Cut away everything around a central circle
X,Y = np.ogrid[0:length, 0:length];
circlemask = (X - length/2) ** 2 + (Y - length/2) ** 2 > length/2 * length/2;
img[circlemask] = 0xFF;
pin_coords = [];
center = length / 2;
radius = length / 2 - 1/2;
// Precalculate the coordinates of every pin
for (i in range(N_PINS)) {
angle = 2 * math.pi * i / N_PINS;
pin_coords.push((math.floor(center + radius * math.cos(angle)),
math.floor(center + radius * math.sin(angle))));
}
line_cache_y = [null] * N_PINS * N_PINS;
line_cache_x = [null] * N_PINS * N_PINS;
line_cache_weight = [1] * N_PINS * N_PINS // Turned out to be unnecessary, unused
line_cache_length = [0] * N_PINS * N_PINS;
console.log('Precalculating all lines... ', end='', flush=true);
for (a in range(N_PINS)) {
for (b in range(a + MIN_DISTANCE, N_PINS)) {
x0 = pin_coords[a][0];
y0 = pin_coords[a][1];
x1 = pin_coords[b][0];
y1 = pin_coords[b][1];
d = Number(math.sqrt((x1 - x0) * (x1 - x0) + (y1 - y0)*(y1 - y0)));
// A proper (slower) Bresenham does not give any better result *shrug*
xs = np.linspace(x0, x1, d, dtype=int);
ys = np.linspace(y0, y1, d, dtype=int);
line_cache_y[b*N_PINS + a] = ys;
line_cache_y[a*N_PINS + b] = ys;
line_cache_x[b*N_PINS + a] = xs;
line_cache_x[a*N_PINS + b] = xs;
line_cache_length[b*N_PINS + a] = d;
line_cache_length[a*N_PINS + b] = d;
}
}
console.log('done');
error = np.ones(img.shape) * 0xFF - img.copy();
img_result = np.ones(img.shape) * 0xFF;
lse_buffer = np.ones(img.shape) * 0xFF // Used in the unused LSE algorithm
result = np.ones((img.shape[0] * SCALE, img.shape[1] * SCALE), np.uint8) * 0xFF;
line_mask = np.zeros(img.shape, np.float64) // XXX
line_sequence = [];
pin = 0;
line_sequence.push(pin);
thread_length = 0;
last_pins = collections.deque(maxlen = MIN_LOOP);
for (l in range(MAX_LINES)) {
if (l % 100 == 0) {
console.log('%d ' % l, end='', flush=true);
}
img_result = cv2.resize(result, img.shape, Numbererpolation=cv2.INTER_AREA);
}
// Some trickery to fast calculate the absolute difference, to estimate the error per pixel
diff = img_result - img;
mul = np.uint8(img_result < img) * 254 + 1;
absdiff = diff * mul;
console.log(absdiff.sum() / (length * length));
max_err = -math.inf;
best_pin = -1;
// Find the line which will lower the error the most
for (offset in range(MIN_DISTANCE, N_PINS - MIN_DISTANCE)) {
test_pin = (pin + offset) % N_PINS;
if (test_pin in last_pins) {
continue;
}
xs = line_cache_x[test_pin * N_PINS + pin];
ys = line_cache_y[test_pin * N_PINS + pin];
}
// Simple
// Error defined as the sum of the brightness of each pixel in the original
// The idea being that a wire can only darken pixels in the result
line_err = np.sum(error[ys,xs]) * line_cache_weight[test_pin*N_PINS + pin];
'\n' +
' # LSE Unused\n' +
' goal_pixels = img[ys, xs]\n' +
' old_pixels = lse_buffer[ys, xs]\n' +
' new_pixels = np.clip(old_pixels - LINE_WEIGHT * line_cache_weight[test_pin*N_PINS + pin], 0, 255)\n' +
' line_err = np.sum((old_pixels - goal_pixels) ** 2) - np.sum((new_pixels - goal_pixels) ** 2)\n' +
' #LSE\n' +
' ';
if (line_err > max_err) {
max_err = line_err;
best_pin = test_pin;
}
line_sequence.push(best_pin);
xs = line_cache_x[best_pin * N_PINS + pin];
ys = line_cache_y[best_pin * N_PINS + pin];
weight = LINE_WEIGHT * line_cache_weight[best_pin*N_PINS + pin];
'\n' +
' #LSE\n' +
' old_pixels = lse_buffer[ys, xs]\n' +
' new_pixels = np.clip(old_pixels - weight, 0, 255)\n' +
' lse_buffer[ys, xs] = new_pixels\n' +
' #LSE\n' +
' ';
// Subtract the line from the error
line_mask.fill(0);
line_mask[ys, xs] = weight;
error = error - line_mask;
error.clip(0, 255);
// Draw the line in the result
cv2.line(result,
(pin_coords[pin][0] * SCALE, pin_coords[pin][1] * SCALE),
(pin_coords[best_pin][0] * SCALE, pin_coords[best_pin][1] * SCALE),
color=0, thickness=4, lineType=8);
x0 = pin_coords[pin][0];
y0 = pin_coords[pin][1];
x1 = pin_coords[best_pin][0];
y1 = pin_coords[best_pin][1];
// Calculate physical distance
dist = math.sqrt((x1 - x0) * (x1 - x0) + (y1 - y0)*(y1 - y0));
thread_length += HOOP_DIAMETER / length * dist;
last_pins.push(best_pin);
pin = best_pin;
img_result = cv2.resize(result, img.shape, Numbererpolation=cv2.INTER_AREA);
diff = img_result - img;
mul = np.uint8(img_result < img) * 254 + 1;
absdiff = diff * mul;
console.log(absdiff.sum() / (length * length));
console.log('\x07');
toc = time.perf_counter();
console.log('%.1f seconds' % (toc - tic));
cv2.imwrite(os.path.splitext(FILENAME)[0] + '-out.png', result);
with open(os.path.splitext(FILENAME)[0] + '.json', 'w') as f) {
f.write(str(line_sequence)) ;
}

193
stringart.py 100644
Wyświetl plik

@ -0,0 +1,193 @@
import collections
import math
import os
import cv2
import numpy as np
import time
MAX_LINES = 4000
N_PINS = 36*8
MIN_LOOP = 20 # To avoid getting stuck in a loop
MIN_DISTANCE = 20 # To avoid very short lines
LINE_WEIGHT = 15 # Tweakable parameter
FILENAME = "h2.jpg"
SCALE = 25 # For making a very high resolution render, to attempt to accurately gauge how thick the thread must be
HOOP_DIAMETER = 0.625 # To calculate total thread length
tic = time.perf_counter()
img = cv2.imread(FILENAME, cv2.IMREAD_GRAYSCALE)
# Didn't bother to make it work for non-square images
assert img.shape[0] == img.shape[1]
length = img.shape[0]
def disp(image):
cv2.imshow('image', image)
cv2.waitKey(0)
cv2.destroyAllWindows()
# Cut away everything around a central circle
X,Y = np.ogrid[0:length, 0:length]
circlemask = (X - length/2) ** 2 + (Y - length/2) ** 2 > length/2 * length/2
img[circlemask] = 0xFF
breakpoint()
pin_coords = []
center = length / 2
radius = length / 2 - 1/2
# Precalculate the coordinates of every pin
for i in range(N_PINS):
angle = 2 * math.pi * i / N_PINS
pin_coords.append((math.floor(center + radius * math.cos(angle)),
math.floor(center + radius * math.sin(angle))))
line_cache_y = [None] * N_PINS * N_PINS
line_cache_x = [None] * N_PINS * N_PINS
line_cache_weight = [1] * N_PINS * N_PINS # Turned out to be unnecessary, unused
line_cache_length = [0] * N_PINS * N_PINS
print("Precalculating all lines... ", end='', flush=True)
for a in range(N_PINS):
for b in range(a + MIN_DISTANCE, N_PINS):
x0 = pin_coords[a][0]
y0 = pin_coords[a][1]
x1 = pin_coords[b][0]
y1 = pin_coords[b][1]
d = int(math.sqrt((x1 - x0) * (x1 - x0) + (y1 - y0)*(y1 - y0)))
#d = max(abs(y1-y0), abs(x1-x0)) inf-norm
# A proper (slower) Bresenham does not give any better result *shrug*
xs = np.linspace(x0, x1, d, dtype=int)
ys = np.linspace(y0, y1, d, dtype=int)
line_cache_y[b*N_PINS + a] = ys
line_cache_y[a*N_PINS + b] = ys
line_cache_x[b*N_PINS + a] = xs
line_cache_x[a*N_PINS + b] = xs
line_cache_length[b*N_PINS + a] = d
line_cache_length[a*N_PINS + b] = d
print("done")
error = np.ones(img.shape) * 0xFF - img.copy()
img_result = np.ones(img.shape) * 0xFF
lse_buffer = np.ones(img.shape) * 0xFF # Used in the unused LSE algorithm
result = np.ones((img.shape[0] * SCALE, img.shape[1] * SCALE), np.uint8) * 0xFF
line_mask = np.zeros(img.shape, np.float64) # XXX
line_sequence = []
pin = 0
line_sequence.append(pin)
thread_length = 0
last_pins = collections.deque(maxlen = MIN_LOOP)
for l in range(MAX_LINES):
if l % 100 == 0:
print("%d " % l, end='', flush=True)
img_result = cv2.resize(result, img.shape, interpolation=cv2.INTER_AREA)
# Some trickery to fast calculate the absolute difference, to estimate the error per pixel
diff = img_result - img
mul = np.uint8(img_result < img) * 254 + 1
absdiff = diff * mul
print(absdiff.sum() / (length * length))
max_err = -math.inf
best_pin = -1
# Find the line which will lower the error the most
for offset in range(MIN_DISTANCE, N_PINS - MIN_DISTANCE):
test_pin = (pin + offset) % N_PINS
if test_pin in last_pins:
continue
xs = line_cache_x[test_pin * N_PINS + pin]
ys = line_cache_y[test_pin * N_PINS + pin]
# Simple
# Error defined as the sum of the brightness of each pixel in the original
# The idea being that a wire can only darken pixels in the result
line_err = np.sum(error[ys,xs]) * line_cache_weight[test_pin*N_PINS + pin]
'''
# LSE Unused
goal_pixels = img[ys, xs]
old_pixels = lse_buffer[ys, xs]
new_pixels = np.clip(old_pixels - LINE_WEIGHT * line_cache_weight[test_pin*N_PINS + pin], 0, 255)
line_err = np.sum((old_pixels - goal_pixels) ** 2) - np.sum((new_pixels - goal_pixels) ** 2)
#LSE
'''
if line_err > max_err:
max_err = line_err
best_pin = test_pin
line_sequence.append(best_pin)
xs = line_cache_x[best_pin * N_PINS + pin]
ys = line_cache_y[best_pin * N_PINS + pin]
weight = LINE_WEIGHT * line_cache_weight[best_pin*N_PINS + pin]
'''
#LSE
old_pixels = lse_buffer[ys, xs]
new_pixels = np.clip(old_pixels - weight, 0, 255)
lse_buffer[ys, xs] = new_pixels
#LSE
'''
# Subtract the line from the error
line_mask.fill(0)
line_mask[ys, xs] = weight
error = error - line_mask
error.clip(0, 255)
# Draw the line in the result
cv2.line(result,
(pin_coords[pin][0] * SCALE, pin_coords[pin][1] * SCALE),
(pin_coords[best_pin][0] * SCALE, pin_coords[best_pin][1] * SCALE),
color=0, thickness=4, lineType=8)
x0 = pin_coords[pin][0]
y0 = pin_coords[pin][1]
x1 = pin_coords[best_pin][0]
y1 = pin_coords[best_pin][1]
# Calculate physical distance
dist = math.sqrt((x1 - x0) * (x1 - x0) + (y1 - y0)*(y1 - y0))
thread_length += HOOP_DIAMETER / length * dist
last_pins.append(best_pin)
pin = best_pin
img_result = cv2.resize(result, img.shape, interpolation=cv2.INTER_AREA)
diff = img_result - img
mul = np.uint8(img_result < img) * 254 + 1
absdiff = diff * mul
print(absdiff.sum() / (length * length))
print('\x07')
toc = time.perf_counter()
print("%.1f seconds" % (toc - tic))
cv2.imwrite(os.path.splitext(FILENAME)[0] + "-out.png", result)
with open(os.path.splitext(FILENAME)[0] + ".json", "w") as f:
f.write(str(line_sequence))