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',