Merge pull request #25 from GeorgLegato/sendto_outpaint_as_cubemap
Convert to Cubemap, Toolbox, panorama viewer tab back as video viewer onlypull/27/head
commit
c087c85e75
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
})
|
||||
}
|
||||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ const panoviewer = new PhotoSphereViewer.Viewer({
|
|||
adapter: [PhotoSphereViewer.EquirectangularVideoAdapter, {
|
||||
muted: true,
|
||||
}],
|
||||
caption: 'Ayutthaya <b>© meetle</b>',
|
||||
loadingImg: baseUrl + 'loader.gif',
|
||||
caption: 'Demo sweeping insects',
|
||||
//loadingImg: baseUrl + 'loader.gif',
|
||||
touchmoveTwoFingers: true,
|
||||
mousewheelCtrlKey: false,
|
||||
navbar: 'video caption settings fullscreen',
|
||||
|
|
|
|||
Loading…
Reference in New Issue