Merge pull request #25 from GeorgLegato/sendto_outpaint_as_cubemap

Convert to Cubemap, Toolbox, panorama viewer tab back as video viewer only
pull/27/head
GeorgLegato 2023-04-01 17:30:05 +02:00 committed by GitHub
commit c087c85e75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 394 additions and 27 deletions

202
javascript/e2c.js Normal file
View File

@ -0,0 +1,202 @@
/* code based jaxry, github, LGPL 3.0 */
function clamp(x, min, max) {
return Math.min(max, Math.max(x, min));
}
function mod(x, n) {
return ((x % n) + n) % n;
}
function copyPixelNearest(read, write) {
const {width, height, data} = read;
const readIndex = (x, y) => 4 * (y * width + x);
return (xFrom, yFrom, to) => {
const nearest = readIndex(
clamp(Math.round(xFrom), 0, width - 1),
clamp(Math.round(yFrom), 0, height - 1)
);
for (let channel = 0; channel < 3; channel++) {
write.data[to + channel] = data[nearest + channel];
}
};
}
function copyPixelBilinear(read, write) {
const {width, height, data} = read;
const readIndex = (x, y) => 4 * (y * width + x);
return (xFrom, yFrom, to) => {
const xl = clamp(Math.floor(xFrom), 0, width - 1);
const xr = clamp(Math.ceil(xFrom), 0, width - 1);
const xf = xFrom - xl;
const yl = clamp(Math.floor(yFrom), 0, height - 1);
const yr = clamp(Math.ceil(yFrom), 0, height - 1);
const yf = yFrom - yl;
const p00 = readIndex(xl, yl);
const p10 = readIndex(xr ,yl);
const p01 = readIndex(xl, yr);
const p11 = readIndex(xr, yr);
for (let channel = 0; channel < 3; channel++) {
const p0 = data[p00 + channel] * (1 - xf) + data[p10 + channel] * xf;
const p1 = data[p01 + channel] * (1 - xf) + data[p11 + channel] * xf;
write.data[to + channel] = Math.ceil(p0 * (1 - yf) + p1 * yf);
}
};
}
// performs a discrete convolution with a provided kernel
function kernelResample(read, write, filterSize, kernel) {
const {width, height, data} = read;
const readIndex = (x, y) => 4 * (y * width + x);
const twoFilterSize = 2*filterSize;
const xMax = width - 1;
const yMax = height - 1;
const xKernel = new Array(4);
const yKernel = new Array(4);
return (xFrom, yFrom, to) => {
const xl = Math.floor(xFrom);
const yl = Math.floor(yFrom);
const xStart = xl - filterSize + 1;
const yStart = yl - filterSize + 1;
for (let i = 0; i < twoFilterSize; i++) {
xKernel[i] = kernel(xFrom - (xStart + i));
yKernel[i] = kernel(yFrom - (yStart + i));
}
for (let channel = 0; channel < 3; channel++) {
let q = 0;
for (let i = 0; i < twoFilterSize; i++) {
const y = yStart + i;
const yClamped = clamp(y, 0, yMax);
let p = 0;
for (let j = 0; j < twoFilterSize; j++) {
const x = xStart + j;
const index = readIndex(clamp(x, 0, xMax), yClamped);
p += data[index + channel] * xKernel[j];
}
q += p * yKernel[i];
}
write.data[to + channel] = Math.round(q);
}
};
}
function copyPixelBicubic(read, write) {
const b = -0.5;
const kernel = x => {
x = Math.abs(x);
const x2 = x*x;
const x3 = x*x*x;
return x <= 1 ?
(b + 2)*x3 - (b + 3)*x2 + 1 :
b*x3 - 5*b*x2 + 8*b*x - 4*b;
};
return kernelResample(read, write, 2, kernel);
}
function copyPixelLanczos(read, write) {
const filterSize = 5;
const kernel = x => {
if (x === 0) {
return 1;
}
else {
const xp = Math.PI * x;
return filterSize * Math.sin(xp) * Math.sin(xp / filterSize) / (xp * xp);
}
};
return kernelResample(read, write, filterSize, kernel);
}
const orientations = {
pz: (out, x, y) => {
out.x = -1;
out.y = -x;
out.z = -y;
},
nz: (out, x, y) => {
out.x = 1;
out.y = x;
out.z = -y;
},
px: (out, x, y) => {
out.x = x;
out.y = -1;
out.z = -y;
},
nx: (out, x, y) => {
out.x = -x;
out.y = 1;
out.z = -y;
},
py: (out, x, y) => {
out.x = -y;
out.y = -x;
out.z = 1;
},
ny: (out, x, y) => {
out.x = y;
out.y = -x;
out.z = -1;
}
};
function renderFace({data: readData, face, rotation, interpolation, maxWidth = Infinity}) {
const faceWidth = Math.min(maxWidth, readData.width / 4);
const faceHeight = faceWidth;
const cube = {};
const orientation = orientations[face];
const writeData = new ImageData(faceWidth, faceHeight);
const copyPixel =
interpolation === 'linear' ? copyPixelBilinear(readData, writeData) :
interpolation === 'cubic' ? copyPixelBicubic(readData, writeData) :
interpolation === 'lanczos' ? copyPixelLanczos(readData, writeData) :
copyPixelNearest(readData, writeData);
for (let x = 0; x < faceWidth; x++) {
for (let y = 0; y < faceHeight; y++) {
const to = 4 * (y * faceWidth + x);
// fill alpha channel
writeData.data[to + 3] = 255;
// get position on cube face
// cube is centered at the origin with a side length of 2
orientation(cube, (2 * (x + 0.5) / faceWidth - 1), (2 * (y + 0.5) / faceHeight - 1));
// project cube face onto unit sphere by converting cartesian to spherical coordinates
const r = Math.sqrt(cube.x*cube.x + cube.y*cube.y + cube.z*cube.z);
const lon = mod(Math.atan2(cube.y, cube.x) + rotation, 2 * Math.PI);
const lat = Math.acos(cube.z / r);
copyPixel(readData.width * lon / Math.PI / 2 - 0.5, readData.height * lat / Math.PI - 0.5, to);
}
}
postMessage({data: writeData, faceName: face});
}
onmessage = function({data}) {
if ( data.type && data.type.startsWith("panorama/")) {
renderFace(data);
}
};

View File

@ -2,8 +2,10 @@
pano_titles = {
"Pano 👀":"Send to Panorama Viewer Tab",
"Pano 🌐": "Switch between selected image and Equirectangular view",
"Pano 🧊": "Switch between selected image and CubeMap view"
"🌐": "Switch between selected image and Equirectangular view",
"🧊": "Switch between selected image and CubeMap view",
"✜": "Convert current spherical map into cubemap (for better outpainting)",
"❌": "Close current panorama viewer"
}

View File

@ -62,6 +62,9 @@ function panorama_here(phtml, mode, buttonId) {
return
}
/* close mimics to open a none-iframe */
if (!phtml) return
/* TODO, disabled; no suitable layout found to insert Panoviewet, yet.
if (!galImage) {
// if no item currently selected, check if there is only one gallery-item,
@ -110,7 +113,7 @@ function panorama_send_image(dataURL, name = "Embed Resource") {
function panorama_change_mode(mode) {
return () => {
openpanorama.frame.contentWindow.postMessage({
openpanorama.frame.contentWindow.postMessage({
type: "panoramaviewer/change-mode",
mode: mode
})
@ -196,12 +199,19 @@ function setPanoFromDroppedFile(file) {
console.log(file)
reader.onload = function (event) {
if (panoviewer.adapter.hasOwnProperty("video")) {
panoviewer.setPanorama({source: event.target.result})
panoviewer.setPanorama({ source: event.target.result })
} else {
panoviewer.setPanorama(event.target.result)
}
}
reader.readAsDataURL(file);
/* comes from upload button */
if (file.hasOwnProperty("data")) {
panoviewer.setPanorama({ source: file.data })
}
else {
reader.readAsDataURL(file);
}
}
function dropHandler(ev) {
@ -236,7 +246,6 @@ function dragOverHandler(ev) {
}
function onPanoModeChange(x) {
console.log("Panorama Viewer: PanMode change to: " + x.target.value)
}
@ -246,10 +255,18 @@ function onGalleryDrop(ev) {
const triggerGradio = (g, file) => {
reader = new FileReader();
if (!file instanceof Blob) {
const blob = new Blob([file], { type: file.type });
file = blob
}
reader.onload = function (event) {
g.value = event.target.result
g.dispatchEvent(new Event('input'));
}
reader.readAsDataURL(file);
}
@ -272,7 +289,7 @@ function onGalleryDrop(ev) {
} else {
// Use DataTransfer interface to access the file(s)
[...ev.dataTransfer.files].forEach((file, i) => {
if (i === 0) { triggerGradio(file) }
if (i === 0) { triggerGradio(g, file) }
console.log(`… file[${i}].name = ${file.name}`);
});
}
@ -282,11 +299,12 @@ function onGalleryDrop(ev) {
document.addEventListener("DOMContentLoaded", () => {
const onload = () => {
if (typeof gradioApp === "function") {
let target = gradioApp().getElementById("txt2img_results")
if (!target) {
setTimeout(onload, 5);
setTimeout(onload, 3000);
return
}
target.addEventListener("drop", onGalleryDrop)
@ -303,17 +321,151 @@ document.addEventListener("DOMContentLoaded", () => {
if (gradioApp().getElementById("panoviewer-iframe")) {
openpanoramajs();
} else {
setTimeout(onload, 10);
setTimeout(onload, 3000);
}
/* do the toolbox tango */
gradioApp().querySelectorAll("#PanoramaViewer_ToolBox div ~ div").forEach((e) => {
const options = {
attributes: true
}
function callback(mutationList, observer) {
mutationList.forEach(function (mutation) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
mutation.target.parentElement.style.flex = target.classList.contains("!hidden") ? "100%" : "auto"
}
})
}
const observer = new MutationObserver(callback)
observer.observe(e, options)
})
}
else {
setTimeout(onload, 3);
setTimeout(onload, 3000);
}
};
onload();
});
/* routine based on jerx/github, gpl3 */
function convertto_cubemap() {
panorama_get_image_from_gallery()
.then((dataURL) => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const outCanvas = document.createElement('canvas')
const settings = {
cubeRotation: "180",
interpolation: "lanczos",
format: "png"
};
const facePositions = {
pz: { x: 1, y: 1 },
nz: { x: 3, y: 1 },
px: { x: 2, y: 1 },
nx: { x: 0, y: 1 },
py: { x: 1, y: 0 },
ny: { x: 1, y: 2 },
};
const cubeOrgX = 4
const cubeOrgY = 3
function loadImage(dataURL) {
const img = new Image();
img.src = dataURL
img.addEventListener('load', () => {
const { width, height } = img;
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0);
const data = ctx.getImageData(0, 0, width, height);
outCanvas.width = width
outCanvas.height = (width / cubeOrgX) * cubeOrgY
processImage(data);
});
}
let finished = 0;
let workers = [];
function processImage(data) {
for (let worker of workers) {
worker.terminate();
}
for (let [faceName, position] of Object.entries(facePositions)) {
renderFace(data, faceName);
}
}
function renderFace(data, faceName) {
const options = {
type: "panorama/",
data: data,
face: faceName,
rotation: Math.PI * settings.cubeRotation / 180,
interpolation: settings.interpolation,
};
let worker
try {
throw new Error();
} catch (error) {
const stack = error.stack;
const matches = stack.match(/(file:\/\/.*\/)panoramaviewer-ext\.js/);
//if (matches && matches.length > 1 ) {
//const scriptPath = matches[1];
const scriptPath = "http://localhost:7860/file=S:/KI/stable-diffusion-webui1111/extensions/stable-diffusion-webui-panorama-3dviewer/javascript/"
const workerPath = new URL('e2c.js', scriptPath).href;
worker = new Worker(workerPath);
//}
}
const placeTile = (data) => {
const ctx = outCanvas.getContext('2d');
ctx.putImageData(data.data.data,
facePositions[data.data.faceName].x * outCanvas.width / cubeOrgX,
facePositions[data.data.faceName].y * outCanvas.height / cubeOrgY)
finished++;
if (finished === 6) {
finished = 0;
workers = [];
outCanvas.toBlob(function (blob) {
if (blob instanceof Blob) {
data = { files: [blob] };
var event = document.createEvent('MouseEvent');
event.dataTransfer = data;
onGalleryDrop(event)
}
else {
console.log("no blob from toBlob?!")
}
}, 'image/png');
}
};
worker.onmessage = placeTile
worker.postMessage(options)
workers.push(worker)
}
loadImage(dataURL)
})
}

View File

@ -26,14 +26,16 @@ def data_url_to_image(data_url):
def onPanModeChange(m):
print ("mode changed to"+str(m))
def add_tab():
with gr.Blocks(analytics_enabled=False) as ui:
with gr.Column():
selectedPanMode = gr.Dropdown(choices=["Equirectangular", "Cubemap: Polyhedron net","Equi Video"],value="Equirectangular",label="Select projection mode", elem_id="panoviewer_mode")
# selectedPanMode = gr.Dropdown(choices=["Equirectangular", "Cubemap: Polyhedron net","Equi Video"],value="Equirectangular",label="Select projection mode", elem_id="panoviewer_mode")
# selectedPanMode.change(onPanModeChange, inputs=[selectedPanMode],outputs=[], _js="panorama_change_mode")
upload_button = gr.UploadButton("Upload movie...", file_types=["video"], file_count="single")
upload_button.upload(fn=None, inputs=upload_button, outputs=None, _js="setPanoFromDroppedFile")
gr.HTML(value=f"<iframe id=\"panoviewer-iframe\" class=\"border-2 border-gray-200\" src=\"{iframesrc}\" title='description'></iframe>")
selectedPanMode.change(onPanModeChange, inputs=[selectedPanMode],outputs=[], _js="panorama_change_mode")
return [(ui, "Panorama Viewer", "panorama-3dviewer")]
@ -59,19 +61,28 @@ def after_component(component, **kwargs):
#send2tab_button = gr.Button ("Pano \U0001F440", elem_id=f"sendto_panorama_button") # 👀
#send2tab_button.click(None, [], None, _js="() => panorama_send_gallery('WebUI Resource')")
#send2tab_button.__setattr__("class","gr-button")
if component.parent.elem_id :
suffix = component.parent.elem_id
else :
suffix = "_dummy_suffix"
if (not component.parent.elem_id): return
if (component.parent.elem_id == "image_buttons_txt2img" or component.parent.elem_id == "image_buttons_img2img" or component.parent.elem_id == "image_buttons_extras"):
suffix = component.parent.elem_id
else:
return
# if (suffix):
view_gallery_button = gr.Button ("Pano \U0001F310", elem_id="sendto_panogallery_button_"+suffix) # 🌐
view_cube_button = gr.Button ("Pano \U0001F9CA", elem_id="sendto_panogallery_cube_button_"+ suffix) # 🧊
view_gallery_button.click (None, [],None, _js="panorama_here(\""+iframesrc_gal+"\",\"\",\""+view_gallery_button.elem_id+"\")" )
view_cube_button.click (None, [],None, _js="panorama_here(\""+iframesrc_gal+"\",\"cubemap\",\""+view_cube_button.elem_id+"\")" )
gallery_input_ondrop = gr.Textbox(visible=False, elem_id="gallery_input_ondrop_"+ suffix)
gallery_input_ondrop.style(container=False)
with gr.Accordion("Panorama", open=False, elem_id="PanoramaViewer_ToolBox", visible=True):
with gr.Row():
view_gallery_button = gr.Button ("\U0001F310", variant="tool", elem_id="sendto_panogallery_button_"+suffix) # 🌐
view_cube_button = gr.Button ("\U0001F9CA", variant="tool",elem_id="sendto_panogallery_cube_button_"+ suffix) # 🧊
view_gallery_button.click (None, [],None, _js="panorama_here(\""+iframesrc_gal+"\",\"\",\""+view_gallery_button.elem_id+"\")" )
view_cube_button.click (None, [],None, _js="panorama_here(\""+iframesrc_gal+"\",\"cubemap\",\""+view_cube_button.elem_id+"\")" )
#╬═
conv_cubemap_gallery_button = gr.Button ("\U0000271C", variant="tool", elem_id="convertto_cubemap_button"+suffix) #✜
conv_cubemap_gallery_button.click (None, [],None, _js="convertto_cubemap" )
close_panoviewer = gr.Button("\U0000274C", variant="tool") # ❌
close_panoviewer.click(None,[],None,_js="panorama_here(\"""\",\"\",\"""\")" )
gallery_input_ondrop = gr.Textbox(visible=False, elem_id="gallery_input_ondrop_"+ suffix)
gallery_input_ondrop.style(container=False)
if (gallery_input_ondrop and txt2img_gallery_component):
gallery_input_ondrop.change(fn=dropHandleGallery, inputs=[gallery_input_ondrop], outputs=[txt2img_gallery_component])

View File

@ -28,8 +28,8 @@ const panoviewer = new PhotoSphereViewer.Viewer({
adapter: [PhotoSphereViewer.EquirectangularVideoAdapter, {
muted: true,
}],
caption: 'Ayutthaya <b>&copy; meetle</b>',
loadingImg: baseUrl + 'loader.gif',
caption: 'Demo sweeping insects',
//loadingImg: baseUrl + 'loader.gif',
touchmoveTwoFingers: true,
mousewheelCtrlKey: false,
navbar: 'video caption settings fullscreen',