From 14a0c12457024ffd8412ed833ed1eca2839fff8f Mon Sep 17 00:00:00 2001 From: Kumi Date: Thu, 26 Oct 2023 09:51:54 +0200 Subject: [PATCH] Pix360 krpano exporter - Current status --- .gitignore | 6 + LICENSE | 0 README.md | 0 pyproject.toml | 25 ++++ src/pix360_krpano/__init__.py | 0 src/pix360_krpano/modules.py | 243 ++++++++++++++++++++++++++++++++++ 6 files changed, 274 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 src/pix360_krpano/__init__.py create mode 100644 src/pix360_krpano/modules.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c54e8d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +db.sqlite3 +settings.ini +*.pyc +__pycache__/ +venv/ +.vscode/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..437a812 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "pix360_krpano" +version = "0.0.1" +authors = [ + { name="Kumi Systems e.U.", email="office@kumi.systems" }, +] +description = "krpano downloader module for PIX360" +readme = "README.md" +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[project.urls] +"Homepage" = "https://kumig.it/kumisystems/pix360-eyerevolution" +"Bug Tracker" = "https://kumig.it/kumisystems/pix360-eyerevolution/issues" + +[project.entry-points."pix360downloader"] +"pix360-krpano" = "pix360_krpano.modules:KRPanoDownloader" \ No newline at end of file diff --git a/src/pix360_krpano/__init__.py b/src/pix360_krpano/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pix360_krpano/modules.py b/src/pix360_krpano/modules.py new file mode 100644 index 0000000..ba67435 --- /dev/null +++ b/src/pix360_krpano/modules.py @@ -0,0 +1,243 @@ +from pix360core.classes import DownloaderModule, HTTPRequest, DownloadError, DEFAULT_CUBEMAP_TO_EQUIRECTANGULAR_STITCHER, DEFAULT_STITCHER +from pix360core.models import Conversion, File + +from django.core.files.base import ContentFile + +from typing import List, Tuple, Dict + +import re +import logging +import uuid + +class KRPanoDownloader(DownloaderModule): + name: str = "KRPano Downloader" + identifier: str = "systems.kumi.pix360.krpano" + + def __init__(self): + self.logger = logging.getLogger("pix360") + + REGEX_FULL: List[Tuple[str, int, Dict[str, str]]] = [ + (r"\d+/\d+/\d+_\d+\.jpg", DownloaderModule.CERTAINTY_PROBABLE, {}), + ] + + REGEX_SIMPLE: List[Tuple[str, int, Dict[str, str]]] = [ + (r"\_[frblud].jpg", DownloaderModule.CERTAINTY_PROBABLE, {}), + (r"^\d.jpg", DownloaderModule.CERTAINTY_POSSIBLE, {"tiles": "012345"}), + ] + + @classmethod + def test_url(cls, url: str) -> int: + """Test if URL looks like this module can handle it + + Args: + url (str): URL to test + + Returns: + int: Certainty level of the URL being supported by this module + CERTAINTY_UNSUPPORTED if the URL is not supported at all + CERTAINTY_POSSIBLE if the URL may be supported + CERTAINTY_PROBABLE if the URL is probably supported + """ + + for regex, certainty, kwargs in cls.REGEX_FULL: + if bool(re.search(regex, url)): + return certainty + + for regex, certainty, kwargs in cls.REGEX_SIMPLE: + if bool(re.search(regex, url)): + return certainty + + return DownloaderModule.CERTAINTY_UNSUPPORTED + + def process_conversion(self, conversion: Conversion) -> File: + """Download content from the given URL + + Args: + conversion (Conversion): Conversion object to process + + Raises: + DownloadError: If an error occurred while downloading content + + Returns: + File: File object containing the downloaded file + """ + self.logger.debug(f"Processing conversion {conversion.id} with URL {conversion.url}") + converter = KRPanoConverter(conversion) + result = converter.to_equirectangular() + self.logger.debug(f"Finished processing conversion {conversion.id} with URL {conversion.url}. Result: {result.id}") + return result + +class KRPanoConverter: + def __init__(self, conversion): + self.conversion = conversion + + self.logger = logging.getLogger("pix360") + + self.cubemap_stitcher = DEFAULT_CUBEMAP_TO_EQUIRECTANGULAR_STITCHER() + self.stitcher = DEFAULT_STITCHER() + + def url_normalize(self, url): + ''' + Takes the URL of any image in a krpano panorama and returns a string + with substitutable variables for image IDs. + + :param url: URL of an image contained in a krpano panorama + :return: string with substitutable variables or False if URL invalid + ''' + + try: + with HTTPRequest(url).open() as res: + assert res.getcode() == 200 + + parts = url.split("/") + + assert "_" in parts[-1] + parts[-1] = "%i_%i.jpg" + parts[-2] = "%i" + parts[-3] = parts[-3].rstrip("0123456789") + "%i" + + return "/".join(parts) + + except Exception as e: + return False + + def get_max_zoom(self, schema): + ''' + Takes a normalized string from krpano_normalize() and returns the maximum + zoom level available. + + :param schema: normalized URL format output by krpano_normalize() + :return: int value of largest available zoom level + ''' + + self.logger.debug(f"Entering get_max_zoom for {schema}") + + l = 0 + + while True: + try: + url = schema % (0, l+1, 0, 0) + with HTTPRequest(url).open() as res: + assert res.getcode() == 200 + l += 1 + except: + self.logger.debug(f"Max zoom is {l}") + return l + + def export(self, schema): + ''' + Takes a normalized string from krpano_normalize() and returns a list of + lists of lists containing all images fit for passing into stitch(). + + :param schema: normalized URL format output by krpano_normalize() + :return: list of lists of lists of PIL.Image() objects for multistitch() + ''' + + self.logger.debug(f"Entering export for {schema}") + + maxzoom = self.get_max_zoom(schema) + output = [] + + for tile in range(6): + t_array = [] + y = 0 + + while True: + r_array = [] + x = 0 + + while True: + try: + res = HTTPRequest(schema % (tile, maxzoom, y, x)).open() + assert res.getcode() == 200 + content = res.read() + fo = ContentFile(content, name=f"{tile}_{maxzoom}_{y}_{x}.jpg") + file = File.objects.create(conversion=self.conversion, file=fo) + r_array.append(file) + x += 1 + except Exception as e: + self.logger.debug(f"Error: {e}") + break + + if not r_array: + break + + t_array.append(r_array) + y += 1 + + output.append(t_array) + + return output + + def export_simple(self, url, tiles="frblud"): + ''' + Exports krpano panoramas which only consist of six complete tiles. Takes + the URL of one of these images and returns a list of PIL.Image objects + + :param url: URL of one of the images + :return: list of PIL.Image objects + ''' + + self.logger.debug(f"Entering export_simple for {url}") + + output = [] + + for i in tiles: + cur = url[:-5] + i + url[-4:] + res = HTTPRequest(cur).open() + assert res.getcode() == 200 + fo = ContentFile(res.read()) + file = File.objects.create(conversion=self.conversion, file=fo) + output += [file] + + return output + + def export_full(self, url: str) -> File: + self.logger.debug(f"Entering export_full for {url}") + + schema = self.url_normalize(url) + images = self.export(schema) + return self.stitcher.multistitch(images) + + def make_tiles(self, url): + ''' + Determines the type of processing needed to build the six tiles, then + creates and returns them. + + :param url: URL of any image in a krpano panorama + :return: list of stitched PIL.Image objects (back, right, front, left, top, + bottom) + ''' + + self.logger.debug(f"Entering make_tiles for {url}") + + for regex, certainty, kwargs in KRPanoDownloader.REGEX_FULL: + if bool(re.search(regex, url)): + return self.export_full(url, **kwargs) + + for regex, certainty, kwargs in KRPanoDownloader.REGEX_SIMPLE: + if bool(re.search(regex, url)): + return self.export_simple(url, **kwargs) + + raise ValueError("%s does not seem to be a valid krpano URL." % url) + + def to_equirectangular(self): + ''' + Takes the URL of any image in a krpano panorama and returns a finished + stitched image. + + :param url: Image URL + :return: PIL.Image object containing the final image + ''' + + self.logger.debug(f"Entering to_equirectangular for {self.conversion.url}") + stitched = self.make_tiles(self.conversion.url) + self.logger.debug(f"Calling cubemap_to_equirectangular for {self.conversion.url}") + + if self.conversion.properties: + rotation = self.conversion.properties.get("rotation", (0,0,0)) + else: + rotation = (0,0,0) + + function = self.cubemap_stitcher.cubemap_to_equirectangular + return function(stitched, rotation)