diff --git a/javascript/e2c.js b/javascript/e2c.js new file mode 100644 index 0000000..3b02477 --- /dev/null +++ b/javascript/e2c.js @@ -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); + } + }; \ No newline at end of file diff --git a/javascript/pano_hints.js b/javascript/pano_hints.js index 6754cbf..043fdea 100644 --- a/javascript/pano_hints.js +++ b/javascript/pano_hints.js @@ -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" } diff --git a/javascript/panoramaviewer-ext.js b/javascript/panoramaviewer-ext.js index 0355fa1..ab96a27 100644 --- a/javascript/panoramaviewer-ext.js +++ b/javascript/panoramaviewer-ext.js @@ -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) + + }) +} \ No newline at end of file diff --git a/scripts/panorama-3dviewer.py b/scripts/panorama-3dviewer.py index 68ea076..416b802 100644 --- a/scripts/panorama-3dviewer.py +++ b/scripts/panorama-3dviewer.py @@ -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"") - 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]) diff --git a/scripts/tab_video.html b/scripts/tab_video.html index 452b3d9..711ff3b 100644 --- a/scripts/tab_video.html +++ b/scripts/tab_video.html @@ -28,8 +28,8 @@ const panoviewer = new PhotoSphereViewer.Viewer({ adapter: [PhotoSphereViewer.EquirectangularVideoAdapter, { muted: true, }], - caption: 'Ayutthaya © meetle', - loadingImg: baseUrl + 'loader.gif', + caption: 'Demo sweeping insects', + //loadingImg: baseUrl + 'loader.gif', touchmoveTwoFingers: true, mousewheelCtrlKey: false, navbar: 'video caption settings fullscreen',