zhigangjiang's picture
no message
88b0dcb
raw
history blame
30.6 kB
'''
This script is helper function for preprocessing.
Most of the code are converted from LayoutNet official's matlab code.
All functions, naming rule and data flow follow official for easier
converting and comparing.
Code is not optimized for python or numpy yet.
'''
import sys
import numpy as np
from scipy.ndimage import map_coordinates
import cv2
from pylsd import lsd
def computeUVN(n, in_, planeID):
'''
compute v given u and normal.
'''
if planeID == 2:
n = np.array([n[1], n[2], n[0]])
elif planeID == 3:
n = np.array([n[2], n[0], n[1]])
bc = n[0] * np.sin(in_) + n[1] * np.cos(in_)
bs = n[2]
out = np.arctan(-bc / (bs + 1e-9))
return out
def computeUVN_vec(n, in_, planeID):
'''
vectorization version of computeUVN
@n N x 3
@in_ MN x 1
@planeID N
'''
n = n.copy()
if (planeID == 2).sum():
n[planeID == 2] = np.roll(n[planeID == 2], 2, axis=1)
if (planeID == 3).sum():
n[planeID == 3] = np.roll(n[planeID == 3], 1, axis=1)
n = np.repeat(n, in_.shape[0] // n.shape[0], axis=0)
assert n.shape[0] == in_.shape[0]
bc = n[:, [0]] * np.sin(in_) + n[:, [1]] * np.cos(in_)
bs = n[:, [2]]
out = np.arctan(-bc / (bs + 1e-9))
return out
def xyz2uvN(xyz, planeID=1):
ID1 = (int(planeID) - 1 + 0) % 3
ID2 = (int(planeID) - 1 + 1) % 3
ID3 = (int(planeID) - 1 + 2) % 3
normXY = np.sqrt(xyz[:, [ID1]] ** 2 + xyz[:, [ID2]] ** 2)
normXY[normXY < 0.000001] = 0.000001
normXYZ = np.sqrt(xyz[:, [ID1]] ** 2 + xyz[:, [ID2]] ** 2 + xyz[:, [ID3]] ** 2)
v = np.arcsin(xyz[:, [ID3]] / normXYZ)
u = np.arcsin(xyz[:, [ID1]] / normXY)
valid = (xyz[:, [ID2]] < 0) & (u >= 0)
u[valid] = np.pi - u[valid]
valid = (xyz[:, [ID2]] < 0) & (u <= 0)
u[valid] = -np.pi - u[valid]
uv = np.hstack([u, v])
uv[np.isnan(uv[:, 0]), 0] = 0
return uv
def uv2xyzN(uv, planeID=1):
ID1 = (int(planeID) - 1 + 0) % 3
ID2 = (int(planeID) - 1 + 1) % 3
ID3 = (int(planeID) - 1 + 2) % 3
xyz = np.zeros((uv.shape[0], 3))
xyz[:, ID1] = np.cos(uv[:, 1]) * np.sin(uv[:, 0])
xyz[:, ID2] = np.cos(uv[:, 1]) * np.cos(uv[:, 0])
xyz[:, ID3] = np.sin(uv[:, 1])
return xyz
def uv2xyzN_vec(uv, planeID):
'''
vectorization version of uv2xyzN
@uv N x 2
@planeID N
'''
assert (planeID.astype(int) != planeID).sum() == 0
planeID = planeID.astype(int)
ID1 = (planeID - 1 + 0) % 3
ID2 = (planeID - 1 + 1) % 3
ID3 = (planeID - 1 + 2) % 3
ID = np.arange(len(uv))
xyz = np.zeros((len(uv), 3))
xyz[ID, ID1] = np.cos(uv[:, 1]) * np.sin(uv[:, 0])
xyz[ID, ID2] = np.cos(uv[:, 1]) * np.cos(uv[:, 0])
xyz[ID, ID3] = np.sin(uv[:, 1])
return xyz
def warpImageFast(im, XXdense, YYdense):
minX = max(1., np.floor(XXdense.min()) - 1)
minY = max(1., np.floor(YYdense.min()) - 1)
maxX = min(im.shape[1], np.ceil(XXdense.max()) + 1)
maxY = min(im.shape[0], np.ceil(YYdense.max()) + 1)
im = im[int(round(minY-1)):int(round(maxY)),
int(round(minX-1)):int(round(maxX))]
assert XXdense.shape == YYdense.shape
out_shape = XXdense.shape
coordinates = [
(YYdense - minY).reshape(-1),
(XXdense - minX).reshape(-1),
]
im_warp = np.stack([
map_coordinates(im[..., c], coordinates, order=1).reshape(out_shape)
for c in range(im.shape[-1])],
axis=-1)
return im_warp
def rotatePanorama(img, vp=None, R=None):
'''
Rotate panorama
if R is given, vp (vanishing point) will be overlooked
otherwise R is computed from vp
'''
sphereH, sphereW, C = img.shape
# new uv coordinates
TX, TY = np.meshgrid(range(1, sphereW + 1), range(1, sphereH + 1))
TX = TX.reshape(-1, 1, order='F')
TY = TY.reshape(-1, 1, order='F')
ANGx = (TX - sphereW/2 - 0.5) / sphereW * np.pi * 2
ANGy = -(TY - sphereH/2 - 0.5) / sphereH * np.pi
uvNew = np.hstack([ANGx, ANGy])
xyzNew = uv2xyzN(uvNew, 1)
# rotation matrix
if R is None:
R = np.linalg.inv(vp.T)
xyzOld = np.linalg.solve(R, xyzNew.T).T
uvOld = xyz2uvN(xyzOld, 1)
Px = (uvOld[:, 0] + np.pi) / (2*np.pi) * sphereW + 0.5
Py = (-uvOld[:, 1] + np.pi/2) / np.pi * sphereH + 0.5
Px = Px.reshape(sphereH, sphereW, order='F')
Py = Py.reshape(sphereH, sphereW, order='F')
# boundary
imgNew = np.zeros((sphereH+2, sphereW+2, C), np.float64)
imgNew[1:-1, 1:-1, :] = img
imgNew[1:-1, 0, :] = img[:, -1, :]
imgNew[1:-1, -1, :] = img[:, 0, :]
imgNew[0, 1:sphereW//2+1, :] = img[0, sphereW-1:sphereW//2-1:-1, :]
imgNew[0, sphereW//2+1:-1, :] = img[0, sphereW//2-1::-1, :]
imgNew[-1, 1:sphereW//2+1, :] = img[-1, sphereW-1:sphereW//2-1:-1, :]
imgNew[-1, sphereW//2+1:-1, :] = img[0, sphereW//2-1::-1, :]
imgNew[0, 0, :] = img[0, 0, :]
imgNew[-1, -1, :] = img[-1, -1, :]
imgNew[0, -1, :] = img[0, -1, :]
imgNew[-1, 0, :] = img[-1, 0, :]
rotImg = warpImageFast(imgNew, Px+1, Py+1)
return rotImg
def imgLookAt(im, CENTERx, CENTERy, new_imgH, fov):
sphereH = im.shape[0]
sphereW = im.shape[1]
warped_im = np.zeros((new_imgH, new_imgH, 3))
TX, TY = np.meshgrid(range(1, new_imgH + 1), range(1, new_imgH + 1))
TX = TX.reshape(-1, 1, order='F')
TY = TY.reshape(-1, 1, order='F')
TX = TX - 0.5 - new_imgH/2
TY = TY - 0.5 - new_imgH/2
r = new_imgH / 2 / np.tan(fov/2)
# convert to 3D
R = np.sqrt(TY ** 2 + r ** 2)
ANGy = np.arctan(- TY / r)
ANGy = ANGy + CENTERy
X = np.sin(ANGy) * R
Y = -np.cos(ANGy) * R
Z = TX
INDn = np.nonzero(np.abs(ANGy) > np.pi/2)
# project back to sphere
ANGx = np.arctan(Z / -Y)
RZY = np.sqrt(Z ** 2 + Y ** 2)
ANGy = np.arctan(X / RZY)
ANGx[INDn] = ANGx[INDn] + np.pi
ANGx = ANGx + CENTERx
INDy = np.nonzero(ANGy < -np.pi/2)
ANGy[INDy] = -np.pi - ANGy[INDy]
ANGx[INDy] = ANGx[INDy] + np.pi
INDx = np.nonzero(ANGx <= -np.pi); ANGx[INDx] = ANGx[INDx] + 2 * np.pi
INDx = np.nonzero(ANGx > np.pi); ANGx[INDx] = ANGx[INDx] - 2 * np.pi
INDx = np.nonzero(ANGx > np.pi); ANGx[INDx] = ANGx[INDx] - 2 * np.pi
INDx = np.nonzero(ANGx > np.pi); ANGx[INDx] = ANGx[INDx] - 2 * np.pi
Px = (ANGx + np.pi) / (2*np.pi) * sphereW + 0.5
Py = ((-ANGy) + np.pi/2) / np.pi * sphereH + 0.5
INDxx = np.nonzero(Px < 1)
Px[INDxx] = Px[INDxx] + sphereW
im = np.concatenate([im, im[:, :2]], 1)
Px = Px.reshape(new_imgH, new_imgH, order='F')
Py = Py.reshape(new_imgH, new_imgH, order='F')
warped_im = warpImageFast(im, Px, Py)
return warped_im
def separatePano(panoImg, fov, x, y, imgSize=320):
'''cut a panorama image into several separate views'''
assert x.shape == y.shape
if not isinstance(fov, np.ndarray):
fov = fov * np.ones_like(x)
sepScene = [
{
'img': imgLookAt(panoImg.copy(), xi, yi, imgSize, fovi),
'vx': xi,
'vy': yi,
'fov': fovi,
'sz': imgSize,
}
for xi, yi, fovi in zip(x, y, fov)
]
return sepScene
def lsdWrap(img):
'''
Opencv implementation of
Rafael Grompone von Gioi, Jérémie Jakubowicz, Jean-Michel Morel, and Gregory Randall,
LSD: a Line Segment Detector, Image Processing On Line, vol. 2012.
[Rafael12] http://www.ipol.im/pub/art/2012/gjmr-lsd/?utm_source=doi
@img
input image
'''
if len(img.shape) == 3:
img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
lines = lsd(img, quant=0.7)
if lines is None:
return np.zeros_like(img), np.array([])
edgeMap = np.zeros_like(img)
for i in range(lines.shape[0]):
pt1 = (int(lines[i, 0]), int(lines[i, 1]))
pt2 = (int(lines[i, 2]), int(lines[i, 3]))
width = lines[i, 4]
cv2.line(edgeMap, pt1, pt2, 255, int(np.ceil(width / 2)))
edgeList = np.concatenate([lines, np.ones_like(lines[:, :2])], 1)
return edgeMap, edgeList
def edgeFromImg2Pano(edge):
edgeList = edge['edgeLst']
if len(edgeList) == 0:
return np.array([])
vx = edge['vx']
vy = edge['vy']
fov = edge['fov']
imH, imW = edge['img'].shape
R = (imW/2) / np.tan(fov/2)
# im is the tangent plane, contacting with ball at [x0 y0 z0]
x0 = R * np.cos(vy) * np.sin(vx)
y0 = R * np.cos(vy) * np.cos(vx)
z0 = R * np.sin(vy)
vecposX = np.array([np.cos(vx), -np.sin(vx), 0])
vecposY = np.cross(np.array([x0, y0, z0]), vecposX)
vecposY = vecposY / np.sqrt(vecposY @ vecposY.T)
vecposX = vecposX.reshape(1, -1)
vecposY = vecposY.reshape(1, -1)
Xc = (0 + imW-1) / 2
Yc = (0 + imH-1) / 2
vecx1 = edgeList[:, [0]] - Xc
vecy1 = edgeList[:, [1]] - Yc
vecx2 = edgeList[:, [2]] - Xc
vecy2 = edgeList[:, [3]] - Yc
vec1 = np.tile(vecx1, [1, 3]) * vecposX + np.tile(vecy1, [1, 3]) * vecposY
vec2 = np.tile(vecx2, [1, 3]) * vecposX + np.tile(vecy2, [1, 3]) * vecposY
coord1 = [[x0, y0, z0]] + vec1
coord2 = [[x0, y0, z0]] + vec2
normal = np.cross(coord1, coord2, axis=1)
normal = normal / np.linalg.norm(normal, axis=1, keepdims=True)
panoList = np.hstack([normal, coord1, coord2, edgeList[:, [-1]]])
return panoList
def _intersection(range1, range2):
if range1[1] < range1[0]:
range11 = [range1[0], 1]
range12 = [0, range1[1]]
else:
range11 = range1
range12 = [0, 0]
if range2[1] < range2[0]:
range21 = [range2[0], 1]
range22 = [0, range2[1]]
else:
range21 = range2
range22 = [0, 0]
b = max(range11[0], range21[0]) < min(range11[1], range21[1])
if b:
return b
b2 = max(range12[0], range22[0]) < min(range12[1], range22[1])
b = b or b2
return b
def _insideRange(pt, range):
if range[1] > range[0]:
b = pt >= range[0] and pt <= range[1]
else:
b1 = pt >= range[0] and pt <= 1
b2 = pt >= 0 and pt <= range[1]
b = b1 or b2
return b
def combineEdgesN(edges):
'''
Combine some small line segments, should be very conservative
OUTPUT
lines: combined line segments
ori_lines: original line segments
line format [nx ny nz projectPlaneID umin umax LSfov score]
'''
arcList = []
for edge in edges:
panoLst = edge['panoLst']
if len(panoLst) == 0:
continue
arcList.append(panoLst)
arcList = np.vstack(arcList)
# ori lines
numLine = len(arcList)
ori_lines = np.zeros((numLine, 8))
areaXY = np.abs(arcList[:, 2])
areaYZ = np.abs(arcList[:, 0])
areaZX = np.abs(arcList[:, 1])
planeIDs = np.argmax(np.stack([areaXY, areaYZ, areaZX], -1), 1) + 1 # XY YZ ZX
for i in range(numLine):
ori_lines[i, :3] = arcList[i, :3]
ori_lines[i, 3] = planeIDs[i]
coord1 = arcList[i, 3:6]
coord2 = arcList[i, 6:9]
uv = xyz2uvN(np.stack([coord1, coord2]), planeIDs[i])
umax = uv[:, 0].max() + np.pi
umin = uv[:, 0].min() + np.pi
if umax - umin > np.pi:
ori_lines[i, 4:6] = np.array([umax, umin]) / 2 / np.pi
else:
ori_lines[i, 4:6] = np.array([umin, umax]) / 2 / np.pi
ori_lines[i, 6] = np.arccos((
np.dot(coord1, coord2) / (np.linalg.norm(coord1) * np.linalg.norm(coord2))
).clip(-1, 1))
ori_lines[i, 7] = arcList[i, 9]
# additive combination
lines = ori_lines.copy()
for _ in range(3):
numLine = len(lines)
valid_line = np.ones(numLine, bool)
for i in range(numLine):
if not valid_line[i]:
continue
dotProd = (lines[:, :3] * lines[[i], :3]).sum(1)
valid_curr = np.logical_and((np.abs(dotProd) > np.cos(np.pi / 180)), valid_line)
valid_curr[i] = False
for j in np.nonzero(valid_curr)[0]:
range1 = lines[i, 4:6]
range2 = lines[j, 4:6]
valid_rag = _intersection(range1, range2)
if not valid_rag:
continue
# combine
I = np.argmax(np.abs(lines[i, :3]))
if lines[i, I] * lines[j, I] > 0:
nc = lines[i, :3] * lines[i, 6] + lines[j, :3] * lines[j, 6]
else:
nc = lines[i, :3] * lines[i, 6] - lines[j, :3] * lines[j, 6]
nc = nc / np.linalg.norm(nc)
if _insideRange(range1[0], range2):
nrmin = range2[0]
else:
nrmin = range1[0]
if _insideRange(range1[1], range2):
nrmax = range2[1]
else:
nrmax = range1[1]
u = np.array([[nrmin], [nrmax]]) * 2 * np.pi - np.pi
v = computeUVN(nc, u, lines[i, 3])
xyz = uv2xyzN(np.hstack([u, v]), lines[i, 3])
l = np.arccos(np.dot(xyz[0, :], xyz[1, :]).clip(-1, 1))
scr = (lines[i,6]*lines[i,7] + lines[j,6]*lines[j,7]) / (lines[i,6]+lines[j,6])
lines[i] = [*nc, lines[i, 3], nrmin, nrmax, l, scr]
valid_line[j] = False
lines = lines[valid_line]
return lines, ori_lines
def icosahedron2sphere(level):
# this function use a icosahedron to sample uniformly on a sphere
a = 2 / (1 + np.sqrt(5))
M = np.array([
0, a, -1, a, 1, 0, -a, 1, 0,
0, a, 1, -a, 1, 0, a, 1, 0,
0, a, 1, 0, -a, 1, -1, 0, a,
0, a, 1, 1, 0, a, 0, -a, 1,
0, a, -1, 0, -a, -1, 1, 0, -a,
0, a, -1, -1, 0, -a, 0, -a, -1,
0, -a, 1, a, -1, 0, -a, -1, 0,
0, -a, -1, -a, -1, 0, a, -1, 0,
-a, 1, 0, -1, 0, a, -1, 0, -a,
-a, -1, 0, -1, 0, -a, -1, 0, a,
a, 1, 0, 1, 0, -a, 1, 0, a,
a, -1, 0, 1, 0, a, 1, 0, -a,
0, a, 1, -1, 0, a, -a, 1, 0,
0, a, 1, a, 1, 0, 1, 0, a,
0, a, -1, -a, 1, 0, -1, 0, -a,
0, a, -1, 1, 0, -a, a, 1, 0,
0, -a, -1, -1, 0, -a, -a, -1, 0,
0, -a, -1, a, -1, 0, 1, 0, -a,
0, -a, 1, -a, -1, 0, -1, 0, a,
0, -a, 1, 1, 0, a, a, -1, 0])
coor = M.T.reshape(3, 60, order='F').T
coor, idx = np.unique(coor, return_inverse=True, axis=0)
tri = idx.reshape(3, 20, order='F').T
# extrude
coor = list(coor / np.tile(np.linalg.norm(coor, axis=1, keepdims=True), (1, 3)))
for _ in range(level):
triN = []
for t in range(len(tri)):
n = len(coor)
coor.append((coor[tri[t, 0]] + coor[tri[t, 1]]) / 2)
coor.append((coor[tri[t, 1]] + coor[tri[t, 2]]) / 2)
coor.append((coor[tri[t, 2]] + coor[tri[t, 0]]) / 2)
triN.append([n, tri[t, 0], n+2])
triN.append([n, tri[t, 1], n+1])
triN.append([n+1, tri[t, 2], n+2])
triN.append([n, n+1, n+2])
tri = np.array(triN)
# uniquefy
coor, idx = np.unique(coor, return_inverse=True, axis=0)
tri = idx[tri]
# extrude
coor = list(coor / np.tile(np.sqrt(np.sum(coor * coor, 1, keepdims=True)), (1, 3)))
return np.array(coor), np.array(tri)
def curveFitting(inputXYZ, weight):
'''
@inputXYZ: N x 3
@weight : N x 1
'''
l = np.linalg.norm(inputXYZ, axis=1, keepdims=True)
inputXYZ = inputXYZ / l
weightXYZ = inputXYZ * weight
XX = np.sum(weightXYZ[:, 0] ** 2)
YY = np.sum(weightXYZ[:, 1] ** 2)
ZZ = np.sum(weightXYZ[:, 2] ** 2)
XY = np.sum(weightXYZ[:, 0] * weightXYZ[:, 1])
YZ = np.sum(weightXYZ[:, 1] * weightXYZ[:, 2])
ZX = np.sum(weightXYZ[:, 2] * weightXYZ[:, 0])
A = np.array([
[XX, XY, ZX],
[XY, YY, YZ],
[ZX, YZ, ZZ]])
U, S, Vh = np.linalg.svd(A)
outputNM = Vh[-1, :]
outputNM = outputNM / np.linalg.norm(outputNM)
return outputNM
def sphereHoughVote(segNormal, segLength, segScores, binRadius, orthTolerance, candiSet, force_unempty=True):
# initial guess
numLinesg = len(segNormal)
voteBinPoints = candiSet.copy()
voteBinPoints = voteBinPoints[~(voteBinPoints[:,2] < 0)]
reversValid = (segNormal[:, 2] < 0).reshape(-1)
segNormal[reversValid] = -segNormal[reversValid]
voteBinUV = xyz2uvN(voteBinPoints)
numVoteBin = len(voteBinPoints)
voteBinValues = np.zeros(numVoteBin)
for i in range(numLinesg):
tempNorm = segNormal[[i]]
tempDots = (voteBinPoints * tempNorm).sum(1)
valid = np.abs(tempDots) < np.cos((90 - binRadius) * np.pi / 180)
voteBinValues[valid] = voteBinValues[valid] + segScores[i] * segLength[i]
checkIDs1 = np.nonzero(voteBinUV[:, [1]] > np.pi / 3)[0]
voteMax = 0
checkID1Max = 0
checkID2Max = 0
checkID3Max = 0
for j in range(len(checkIDs1)):
checkID1 = checkIDs1[j]
vote1 = voteBinValues[checkID1]
if voteBinValues[checkID1] == 0 and force_unempty:
continue
checkNormal = voteBinPoints[[checkID1]]
dotProduct = (voteBinPoints * checkNormal).sum(1)
checkIDs2 = np.nonzero(np.abs(dotProduct) < np.cos((90 - orthTolerance) * np.pi / 180))[0]
for i in range(len(checkIDs2)):
checkID2 = checkIDs2[i]
if voteBinValues[checkID2] == 0 and force_unempty:
continue
vote2 = vote1 + voteBinValues[checkID2]
cpv = np.cross(voteBinPoints[checkID1], voteBinPoints[checkID2]).reshape(1, 3)
cpn = np.linalg.norm(cpv)
dotProduct = (voteBinPoints * cpv).sum(1) / cpn
checkIDs3 = np.nonzero(np.abs(dotProduct) > np.cos(orthTolerance * np.pi / 180))[0]
for k in range(len(checkIDs3)):
checkID3 = checkIDs3[k]
if voteBinValues[checkID3] == 0 and force_unempty:
continue
vote3 = vote2 + voteBinValues[checkID3]
if vote3 > voteMax:
lastStepCost = vote3 - voteMax
if voteMax != 0:
tmp = (voteBinPoints[[checkID1Max, checkID2Max, checkID3Max]] * \
voteBinPoints[[checkID1, checkID2, checkID3]]).sum(1)
lastStepAngle = np.arccos(tmp.clip(-1, 1))
else:
lastStepAngle = np.zeros(3)
checkID1Max = checkID1
checkID2Max = checkID2
checkID3Max = checkID3
voteMax = vote3
if checkID1Max == 0:
print('[WARN] sphereHoughVote: no orthogonal voting exist', file=sys.stderr)
return None, 0, 0
initXYZ = voteBinPoints[[checkID1Max, checkID2Max, checkID3Max]]
# refine
refiXYZ = np.zeros((3, 3))
dotprod = (segNormal * initXYZ[[0]]).sum(1)
valid = np.abs(dotprod) < np.cos((90 - binRadius) * np.pi / 180)
validNm = segNormal[valid]
validWt = segLength[valid] * segScores[valid]
validWt = validWt / validWt.max()
refiNM = curveFitting(validNm, validWt)
refiXYZ[0] = refiNM.copy()
dotprod = (segNormal * initXYZ[[1]]).sum(1)
valid = np.abs(dotprod) < np.cos((90 - binRadius) * np.pi / 180)
validNm = segNormal[valid]
validWt = segLength[valid] * segScores[valid]
validWt = validWt / validWt.max()
validNm = np.vstack([validNm, refiXYZ[[0]]])
validWt = np.vstack([validWt, validWt.sum(0, keepdims=1) * 0.1])
refiNM = curveFitting(validNm, validWt)
refiXYZ[1] = refiNM.copy()
refiNM = np.cross(refiXYZ[0], refiXYZ[1])
refiXYZ[2] = refiNM / np.linalg.norm(refiNM)
return refiXYZ, lastStepCost, lastStepAngle
def findMainDirectionEMA(lines):
'''compute vp from set of lines'''
# initial guess
segNormal = lines[:, :3]
segLength = lines[:, [6]]
segScores = np.ones((len(lines), 1))
shortSegValid = (segLength < 5 * np.pi / 180).reshape(-1)
segNormal = segNormal[~shortSegValid, :]
segLength = segLength[~shortSegValid]
segScores = segScores[~shortSegValid]
numLinesg = len(segNormal)
candiSet, tri = icosahedron2sphere(3)
ang = np.arccos((candiSet[tri[0,0]] * candiSet[tri[0,1]]).sum().clip(-1, 1)) / np.pi * 180
binRadius = ang / 2
initXYZ, score, angle = sphereHoughVote(segNormal, segLength, segScores, 2*binRadius, 2, candiSet)
if initXYZ is None:
print('[WARN] findMainDirectionEMA: initial failed', file=sys.stderr)
return None, score, angle
# iterative refine
iter_max = 3
candiSet, tri = icosahedron2sphere(5)
numCandi = len(candiSet)
angD = np.arccos((candiSet[tri[0, 0]] * candiSet[tri[0, 1]]).sum().clip(-1, 1)) / np.pi * 180
binRadiusD = angD / 2
curXYZ = initXYZ.copy()
tol = np.linspace(4*binRadius, 4*binRadiusD, iter_max) # shrink down ls and candi
for it in range(iter_max):
dot1 = np.abs((segNormal * curXYZ[[0]]).sum(1))
dot2 = np.abs((segNormal * curXYZ[[1]]).sum(1))
dot3 = np.abs((segNormal * curXYZ[[2]]).sum(1))
valid1 = dot1 < np.cos((90 - tol[it]) * np.pi / 180)
valid2 = dot2 < np.cos((90 - tol[it]) * np.pi / 180)
valid3 = dot3 < np.cos((90 - tol[it]) * np.pi / 180)
valid = valid1 | valid2 | valid3
if np.sum(valid) == 0:
print('[WARN] findMainDirectionEMA: zero line segments for voting', file=sys.stderr)
break
subSegNormal = segNormal[valid]
subSegLength = segLength[valid]
subSegScores = segScores[valid]
dot1 = np.abs((candiSet * curXYZ[[0]]).sum(1))
dot2 = np.abs((candiSet * curXYZ[[1]]).sum(1))
dot3 = np.abs((candiSet * curXYZ[[2]]).sum(1))
valid1 = dot1 > np.cos(tol[it] * np.pi / 180)
valid2 = dot2 > np.cos(tol[it] * np.pi / 180)
valid3 = dot3 > np.cos(tol[it] * np.pi / 180)
valid = valid1 | valid2 | valid3
if np.sum(valid) == 0:
print('[WARN] findMainDirectionEMA: zero line segments for voting', file=sys.stderr)
break
subCandiSet = candiSet[valid]
tcurXYZ, _, _ = sphereHoughVote(subSegNormal, subSegLength, subSegScores, 2*binRadiusD, 2, subCandiSet)
if tcurXYZ is None:
print('[WARN] findMainDirectionEMA: no answer found', file=sys.stderr)
break
curXYZ = tcurXYZ.copy()
mainDirect = curXYZ.copy()
mainDirect[0] = mainDirect[0] * np.sign(mainDirect[0,2])
mainDirect[1] = mainDirect[1] * np.sign(mainDirect[1,2])
mainDirect[2] = mainDirect[2] * np.sign(mainDirect[2,2])
uv = xyz2uvN(mainDirect)
I1 = np.argmax(uv[:,1])
J = np.setdiff1d(np.arange(3), I1)
I2 = np.argmin(np.abs(np.sin(uv[J,0])))
I2 = J[I2]
I3 = np.setdiff1d(np.arange(3), np.hstack([I1, I2]))
mainDirect = np.vstack([mainDirect[I1], mainDirect[I2], mainDirect[I3]])
mainDirect[0] = mainDirect[0] * np.sign(mainDirect[0,2])
mainDirect[1] = mainDirect[1] * np.sign(mainDirect[1,1])
mainDirect[2] = mainDirect[2] * np.sign(mainDirect[2,0])
mainDirect = np.vstack([mainDirect, -mainDirect])
return mainDirect, score, angle
def multi_linspace(start, stop, num):
div = (num - 1)
y = np.arange(0, num, dtype=np.float64)
steps = (stop - start) / div
return steps.reshape(-1, 1) * y + start.reshape(-1, 1)
def assignVanishingType(lines, vp, tol, area=10):
numLine = len(lines)
numVP = len(vp)
typeCost = np.zeros((numLine, numVP))
# perpendicular
for vid in range(numVP):
cosint = (lines[:, :3] * vp[[vid]]).sum(1)
typeCost[:, vid] = np.arcsin(np.abs(cosint).clip(-1, 1))
# infinity
u = np.stack([lines[:, 4], lines[:, 5]], -1)
u = u.reshape(-1, 1) * 2 * np.pi - np.pi
v = computeUVN_vec(lines[:, :3], u, lines[:, 3])
xyz = uv2xyzN_vec(np.hstack([u, v]), np.repeat(lines[:, 3], 2))
xyz = multi_linspace(xyz[0::2].reshape(-1), xyz[1::2].reshape(-1), 100)
xyz = np.vstack([blk.T for blk in np.split(xyz, numLine)])
xyz = xyz / np.linalg.norm(xyz, axis=1, keepdims=True)
for vid in range(numVP):
ang = np.arccos(np.abs((xyz * vp[[vid]]).sum(1)).clip(-1, 1))
notok = (ang < area * np.pi / 180).reshape(numLine, 100).sum(1) != 0
typeCost[notok, vid] = 100
I = typeCost.min(1)
tp = typeCost.argmin(1)
tp[I > tol] = numVP + 1
return tp, typeCost
def refitLineSegmentB(lines, vp, vpweight=0.1):
'''
Refit direction of line segments
INPUT:
lines: original line segments
vp: vannishing point
vpweight: if set to 0, lines will not change; if set to inf, lines will
be forced to pass vp
'''
numSample = 100
numLine = len(lines)
xyz = np.zeros((numSample+1, 3))
wei = np.ones((numSample+1, 1))
wei[numSample] = vpweight * numSample
lines_ali = lines.copy()
for i in range(numLine):
n = lines[i, :3]
sid = lines[i, 4] * 2 * np.pi
eid = lines[i, 5] * 2 * np.pi
if eid < sid:
x = np.linspace(sid, eid + 2 * np.pi, numSample) % (2 * np.pi)
else:
x = np.linspace(sid, eid, numSample)
u = -np.pi + x.reshape(-1, 1)
v = computeUVN(n, u, lines[i, 3])
xyz[:numSample] = uv2xyzN(np.hstack([u, v]), lines[i, 3])
xyz[numSample] = vp
outputNM = curveFitting(xyz, wei)
lines_ali[i, :3] = outputNM
return lines_ali
def paintParameterLine(parameterLine, width, height):
lines = parameterLine.copy()
panoEdgeC = np.zeros((height, width))
num_sample = max(height, width)
for i in range(len(lines)):
n = lines[i, :3]
sid = lines[i, 4] * 2 * np.pi
eid = lines[i, 5] * 2 * np.pi
if eid < sid:
x = np.linspace(sid, eid + 2 * np.pi, num_sample)
x = x % (2 * np.pi)
else:
x = np.linspace(sid, eid, num_sample)
u = -np.pi + x.reshape(-1, 1)
v = computeUVN(n, u, lines[i, 3])
xyz = uv2xyzN(np.hstack([u, v]), lines[i, 3])
uv = xyz2uvN(xyz, 1)
m = np.minimum(np.floor((uv[:,0] + np.pi) / (2 * np.pi) * width) + 1,
width).astype(np.int32)
n = np.minimum(np.floor(((np.pi / 2) - uv[:, 1]) / np.pi * height) + 1,
height).astype(np.int32)
panoEdgeC[n-1, m-1] = i
return panoEdgeC
def panoEdgeDetection(img, viewSize=320, qError=0.7, refineIter=3):
'''
line detection on panorama
INPUT:
img: image waiting for detection, double type, range 0~1
viewSize: image size of croped views
qError: set smaller if more line segment wanted
OUTPUT:
oLines: detected line segments
vp: vanishing point
views: separate views of panorama
edges: original detection of line segments in separate views
panoEdge: image for visualize line segments
'''
cutSize = viewSize
fov = np.pi / 3
xh = np.arange(-np.pi, np.pi*5/6, np.pi/6)
yh = np.zeros(xh.shape[0])
xp = np.array([-3/3, -2/3, -1/3, 0/3, 1/3, 2/3, -3/3, -2/3, -1/3, 0/3, 1/3, 2/3]) * np.pi
yp = np.array([ 1/4, 1/4, 1/4, 1/4, 1/4, 1/4, -1/4, -1/4, -1/4, -1/4, -1/4, -1/4]) * np.pi
x = np.concatenate([xh, xp, [0, 0]])
y = np.concatenate([yh, yp, [np.pi/2., -np.pi/2]])
sepScene = separatePano(img.copy(), fov, x, y, cutSize)
edge = []
for i, scene in enumerate(sepScene):
edgeMap, edgeList = lsdWrap(scene['img'])
edge.append({
'img': edgeMap,
'edgeLst': edgeList,
'vx': scene['vx'],
'vy': scene['vy'],
'fov': scene['fov'],
})
edge[-1]['panoLst'] = edgeFromImg2Pano(edge[-1])
lines, olines = combineEdgesN(edge)
clines = lines.copy()
for _ in range(refineIter):
mainDirect, score, angle = findMainDirectionEMA(clines)
tp, typeCost = assignVanishingType(lines, mainDirect[:3], 0.1, 10)
lines1 = lines[tp==0]
lines2 = lines[tp==1]
lines3 = lines[tp==2]
lines1rB = refitLineSegmentB(lines1, mainDirect[0], 0)
lines2rB = refitLineSegmentB(lines2, mainDirect[1], 0)
lines3rB = refitLineSegmentB(lines3, mainDirect[2], 0)
clines = np.vstack([lines1rB, lines2rB, lines3rB])
panoEdge1r = paintParameterLine(lines1rB, img.shape[1], img.shape[0])
panoEdge2r = paintParameterLine(lines2rB, img.shape[1], img.shape[0])
panoEdge3r = paintParameterLine(lines3rB, img.shape[1], img.shape[0])
panoEdger = np.stack([panoEdge1r, panoEdge2r, panoEdge3r], -1)
# output
olines = clines
vp = mainDirect
views = sepScene
edges = edge
panoEdge = panoEdger
return olines, vp, views, edges, panoEdge, score, angle
if __name__ == '__main__':
# disable OpenCV3's non thread safe OpenCL option
cv2.ocl.setUseOpenCL(False)
import os
import argparse
import PIL
from PIL import Image
import time
parser = argparse.ArgumentParser()
parser.add_argument('--i', required=True)
parser.add_argument('--o_prefix', required=True)
parser.add_argument('--qError', default=0.7, type=float)
parser.add_argument('--refineIter', default=3, type=int)
args = parser.parse_args()
# Read image
img_ori = np.array(Image.open(args.i).resize((1024, 512)))
# Vanishing point estimation & Line segments detection
s_time = time.time()
olines, vp, views, edges, panoEdge, score, angle = panoEdgeDetection(img_ori,
qError=args.qError,
refineIter=args.refineIter)
print('Elapsed time: %.2f' % (time.time() - s_time))
panoEdge = (panoEdge > 0)
print('Vanishing point:')
for v in vp[2::-1]:
print('%.6f %.6f %.6f' % tuple(v))
# Visualization
edg = rotatePanorama(panoEdge.astype(np.float64), vp[2::-1])
img = rotatePanorama(img_ori / 255.0, vp[2::-1])
one = img.copy() * 0.5
one[(edg > 0.5).sum(-1) > 0] = 0
one[edg[..., 0] > 0.5, 0] = 1
one[edg[..., 1] > 0.5, 1] = 1
one[edg[..., 2] > 0.5, 2] = 1
Image.fromarray((edg * 255).astype(np.uint8)).save('%s_edg.png' % args.o_prefix)
Image.fromarray((img * 255).astype(np.uint8)).save('%s_img.png' % args.o_prefix)
Image.fromarray((one * 255).astype(np.uint8)).save('%s_one.png' % args.o_prefix)