Source code for raytraverse.lightpoint.srcviewpoint

# -*- coding: utf-8 -*-
# Copyright (c) 2020 Stephen Wasilewski, HSLU and EPFL
# =======================================================================
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# =======================================================================

import numpy as np

from raytraverse import io
from raytraverse.evaluate import retina
from raytraverse.mapper import ViewMapper
from scipy.interpolate import LinearNDInterpolator
from scipy.spatial import ConvexHull
from scipy.ndimage import gaussian_filter
from shapely.geometry import Polygon


[docs]class SrcViewPoint(object): """interface for sun view data"""
[docs] @staticmethod def offset(points, target): hull = ConvexHull(points) tr = np.sqrt(target/np.pi) while abs(hull.volume - target)/target > .02: r = np.sqrt(hull.volume/np.pi) offset = tr - r p = Polygon(hull.points[hull.vertices]) b = p.boundary.parallel_offset(offset, join_style=2) hull = ConvexHull(np.array(b.xy).T) return hull.points[hull.vertices]
def __init__(self, scene, vecs, lum, pt=(0, 0, 0), posidx=0, src='sunview', res=64, srcomega=6.796702357283834e-05): #: raytraverse.scene.Scene self.scene = scene #: int: index for point self.posidx = posidx #: np.array: point location self.pt = np.asarray(pt).flatten()[0:3] #: str: source key self.src = src #: np.array: individual vectors that hit the source (pixels) self.raster = vecs #: float: source luminance (average) self.lum = lum #: float: source radius self.radius = (srcomega/np.pi)**.5 # 2*np.pi*(1 - np.cos(0.533*np.pi/360)) self.omega = srcomega*vecs.shape[0]/(res * res) self.vec = np.average(vecs, 0) @property def vm(self): return ViewMapper(self.vec, .533) def _to_pix(self, atv, vm, res): if atv > 90: ppix = vm.ivm.ray2pixel(self.raster, res) omegap = vm.ivm.pixel2omega(ppix + .5, res) ppix[:, 0] += res else: ppix = vm.ray2pixel(self.raster, res) omegap = vm.pixel2omega(ppix + .5, res) rec = np.core.records.fromarrays(ppix.T) px, i, cnt = np.unique(rec, return_index=True, return_counts=True) cnt = cnt.astype(float) omegap = omegap[i] px = px.tolist() return px, omegap, cnt def _smudge(self, px, cnt, omegap, omegasp): """hack to ensure equal energy and max luminance)""" ocnt = cnt - (omegap/omegasp) smdg = np.sum(ocnt[ocnt > 0]) room = -np.sum(ocnt[ocnt < 0]) # reduce oversamples cnt[ocnt > 0] = omegap[ocnt > 0]/omegasp # allocate to undersamples if room > smdg: cnt[ocnt < 0] = (np.sum(cnt[ocnt < 0]) + smdg)/np.sum(ocnt < 0) return None # construct hull for interpolation else: target = self.omega/np.average(omegap) target = np.square(np.sqrt(target/np.pi) + .5) * np.pi hullpoints = SrcViewPoint.offset(px, target) return hullpoints
[docs] def add_to_img(self, img, vecs, mask=None, coefs=1, vm=None): if vm is None: vm = ViewMapper(self.vec, .533) res = img.shape[-1] atv = vm.degrees(self.vec)[0] if atv <= vm.viewangle * vm.aspect / 2: px, omegap, cnt = self._to_pix(atv, vm, res) omegasp = self.omega / self.raster.shape[0] clum = coefs * self.lum*cnt*omegasp/omegap i2 = np.zeros(img.shape[-2:]) hullpoints = self._smudge(px, cnt, omegap, omegasp) if hullpoints is not None: ppix = vm.ray2pixel(vecs, res, integer=False) pomega = vm.pixel2omega(ppix, res) # interpolate within original bounds interp = LinearNDInterpolator(px, clum, fill_value=0) luma = interp(ppix) # interpolate within expanded bounds to find perimeter clum = np.concatenate((clum, np.full(len(hullpoints), coefs * self.lum))) xy = np.concatenate((px, hullpoints)) interp = LinearNDInterpolator(xy, clum, fill_value=0) lumb = interp(ppix) # isolate perimeter corona = np.logical_and(lumb > 0, luma == 0) if np.sum(corona) > 0: target = self.omega*self.lum*coefs current = np.sum(pomega*luma) gap = (target - current)/np.sum(pomega[corona]) luma[corona] = gap * coefs * self.lum if len(img.shape) > 2: fe = img.shape[0] i2[mask[1][::fe], mask[2][::fe]] = luma else: i2[mask] = luma else: px = tuple(zip(*px)) i2[px] += clum if vm.viewangle >= 10: r = res/vm.viewangle*.0625 i2 = gaussian_filter(i2, r, truncate=8) img += np.maximum(i2-img, 0)
@staticmethod def _hpsf(x, fwhm=0.183333): hm = np.square(fwhm/2) return hm/(np.square(x) + hm) @staticmethod def _inv_hpsf(y, fwhm=0.183333): hm = np.square(fwhm/2) return np.sqrt(hm/y - hm) def _blur_sun(self, lmax, lmin=279.33, fwhm=0.183333): r0 = np.sqrt(self.omega/np.pi) * 180/np.pi lmax = max(min(lmax, lmin/self._hpsf(1)), lmin) r = r0 + self._inv_hpsf(lmin/lmax, fwhm) return np.square(r/r0)
[docs] def evaluate(self, sunval, vm=None, blursun=False): tol = np.sqrt(self.omega/np.pi) if (vm is None or vm.aspect == 2 or vm.in_view(self.vec, indices=False, tol=tol)[0]): svlm = self.lum * sunval svo = self.omega if blursun: ogas = np.atleast_1d(retina.blur_sun(self.omega, svlm)) svlm = svlm / ogas svo = svo * ogas[0] svlm = np.atleast_1d(np.squeeze(svlm)) return self.vec.reshape(-1, 3), np.array([svo]), svlm else: return self.vec.reshape(-1, 3), np.zeros(1), np.zeros(1)
[docs] def direct_view(self, res=80): vm = ViewMapper(self.vec, .666) vecs = vm.pixelrays(res) img = np.zeros((res, res)) mask = vm.in_view(vecs) self.add_to_img(img, vecs[mask], mask, vm=vm) outf = f"{self.scene.outdir}_{self.src}_{self.posidx:06d}.hdr" vstr = ('VIEW= -vta -vv {0} -vh {0} -vd {1} {2} {3}' ' -vp {4} {5} {6}'.format(vm.viewangle, *vm.dxyz, *self.pt)) io.array2hdr(img, outf, [vstr]) return outf