Skip to content

Commit

Permalink
Merge branch 'blender-2.8'
Browse files Browse the repository at this point in the history
  • Loading branch information
stuarta0 committed Aug 3, 2019
2 parents 7d8228e + 23bd804 commit 9edcd19
Show file tree
Hide file tree
Showing 13 changed files with 121 additions and 49 deletions.
21 changes: 11 additions & 10 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "Dense Reconstruction",
"author": "Stuart Attenborrow",
"version": (1, 0, 0),
"blender": (2, 79, 0),
"blender": (2, 80, 0),
"location": "Properties > Scene",
"description": "Provides the ability to generate dense point clouds using various photogrammetry tools, from inputs including Blender's motion tracking output",
"wiki_url": "https://www.github.com/stuarta0/blender-photogrammetry",
Expand All @@ -12,6 +12,7 @@
import platform
import os
from importlib import import_module
from typing import Dict, List

import bpy
from bpy.props import PointerProperty, IntProperty, FloatProperty, StringProperty, EnumProperty, BoolProperty
Expand All @@ -25,32 +26,32 @@
# if they're not a python module they'll fail to import and we'll skip them
modules = [name for name in os.listdir(os.path.dirname(__file__)) if os.path.isdir(os.path.join(os.path.dirname(__file__), name))]

inputs = {}
outputs = {}
binaries = []
inputs: Dict[str, PhotogrammetryModule] = {}
outputs: Dict[str, PhotogrammetryModule] = {}
binaries: List[str] = []
for m in modules:
try:
currentModule = import_module('.{}'.format(m), __name__)
currentModule = import_module(f'.{m}', __name__)
importer = getattr(currentModule, 'importer', None)
exporter = getattr(currentModule, 'exporter', None)
binaryNames = getattr(currentModule, 'binaries', [])
if not (importer or exporter):
raise AttributeError('Attributes "importer" and/or "exporter" must be defined in module')
except Exception as ex:
print('Problem importing photogrammetry module "{}": {}'.format(m, ex))
print(f'Problem importing photogrammetry module "{m}": {ex}')
continue

currentBinaries = [get_binary_path(get_binpath_for_module(m), name) for name in binaryNames]
if any([not binpath for binpath in currentBinaries]):
print('Photogrammetry module "{}" specified binaries that could not be found {}'.format(m, currentModule.binaries))
print(f'Photogrammetry module "{m}" specified binaries that could not be found {currentModule.binaries}')
continue
for binpath in currentBinaries:
binaries.append(binpath)

if importer:
inputs.setdefault('in_{}'.format(m), importer)
inputs.setdefault(f'in_{m}', importer)
if exporter:
outputs.setdefault('out_{}'.format(m), exporter)
outputs.setdefault(f'out_{m}', exporter)


class PHOTOGRAMMETRY_OT_process(bpy.types.Operator):
Expand Down Expand Up @@ -104,6 +105,7 @@ def execute(self, context):
# out_colmap: PointerProperty(type=PHOTOGRAMMETRY_PG_colmap)

def draw_master(self, layout):
layout.use_property_split = True # Active single-column layout
layout.prop(self, 'input')
try:
getattr(self, self.input).draw(layout)
Expand Down Expand Up @@ -165,7 +167,6 @@ def draw(self, context):

def register():
for cls in classes:
print('register_class({})'.format(cls))
bpy.utils.register_class(cls)
bpy.types.Scene.photogrammetry = PointerProperty(type=PHOTOGRAMMETRY_PG_master)

Expand Down
4 changes: 2 additions & 2 deletions blender/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from .extract import extract
from .groups import PHOTOGRAMMETRY_PG_input_blender
from .groups import PHOTOGRAMMETRY_PG_input_blender, PHOTOGRAMMETRY_PG_output_blender
from .load import load
from ..utils import PhotogrammetryModule

importer = PhotogrammetryModule('Blender Motion Tracking', 'Use tracking data from current scene', PHOTOGRAMMETRY_PG_input_blender, extract)
exporter = PhotogrammetryModule('Blender', 'Import data into current scene', None, load)
exporter = PhotogrammetryModule('Blender', 'Import data into current scene', PHOTOGRAMMETRY_PG_output_blender, load)
10 changes: 5 additions & 5 deletions blender/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def create_render_scene(scene, clip):
sc.frame_start = scene.frame_start
sc.frame_end = scene.frame_end

sc.objects.link(scene.camera)
sc.collection.objects.link(scene.camera)
sc.camera = scene.camera
sc.active_clip = clip

Expand Down Expand Up @@ -112,8 +112,8 @@ def extract(properties, *args, **kwargs):
# <blender source>/release/scripts/startup/bl_operators/clip.py, CLIP_OT_bundles_to_mesh()
reconstruction = tracking.objects.active.reconstruction
framenr = scene.frame_current - clip.frame_start + 1
reconstructed_matrix = reconstruction.cameras.matrix_from_frame(framenr)
mw = scene.camera.matrix_world * reconstructed_matrix.inverted()
reconstructed_matrix = reconstruction.cameras.matrix_from_frame(frame=framenr)
mw = scene.camera.matrix_world @ reconstructed_matrix.inverted()

for cid, f in enumerate(frame_range):
# render each movie clip frame to jpeg still
Expand All @@ -140,7 +140,7 @@ def extract(properties, *args, **kwargs):
R = scene.camera.matrix_world.to_euler('XYZ').to_matrix()
R.transpose()
c = scene.camera.matrix_world.translation.copy()
t = -1 * R * c
t = -1 * R @ c
cameras.setdefault(cid, {
'filename': filename,
'frame': f,
Expand Down Expand Up @@ -179,7 +179,7 @@ def extract(properties, *args, **kwargs):
trackers = {}
for idx, track in enumerate(active_tracks):
trackers[idx] = {
'co': tuple(mw * track.bundle),
'co': tuple(mw @ track.bundle),
'rgb': (0, 0, 0),
}
# loop over every camera that this track is visible in
Expand Down
25 changes: 20 additions & 5 deletions blender/groups.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
import os
import bpy
from bpy.props import StringProperty, IntProperty
from bpy.types import PropertyGroup
from bpy.props import StringProperty, IntProperty, BoolProperty, FloatProperty, EnumProperty
from bpy.types import PropertyGroup, Panel


class PHOTOGRAMMETRY_PG_input_blender(PropertyGroup):
clip = StringProperty(name='Movie Clip')
frame_step = IntProperty(name='Frame Step', description='Number of frames to skip when exporting', default=1)
dirpath = StringProperty(name='Image Directory', subtype='DIR_PATH', default=os.path.join('//renderoutput', 'photogrammetry'))
clip: StringProperty(name='Movie Clip')
frame_step: IntProperty(name='Frame Step', description='Number of frames to skip when exporting', default=1, min=1)
dirpath: StringProperty(name='Image Directory', subtype='DIR_PATH', default=os.path.join('//renderoutput', 'photogrammetry'))

def draw(self, layout):
layout.prop(self, 'dirpath')
layout.prop_search(self, 'clip', bpy.data, 'movieclips')
layout.prop(self, 'frame_step')

class PHOTOGRAMMETRY_PG_output_blender(PropertyGroup):
update_render_size: BoolProperty(name='Update render size', description="Update the active scene's render size to the first image size", default=True)
relative_paths: BoolProperty(name='Use relative paths for images', description='When adding background images for cameras, link images using relative paths', default=True)
camera_alpha: FloatProperty(name='Camera Background Alpha', default=0.5, min=0, max=1)
camera_display_depth: EnumProperty(items=[
('BACK', 'Back', 'Display image behind the 3D objects'),
('FRONT', 'Front', 'Display image in front of the 3D objects'),
], name='Camera Background Display', default='BACK')

def draw(self, layout):
layout.prop(self, 'update_render_size')
layout.prop(self, 'relative_paths')
layout.prop(self, 'camera_alpha')
layout.prop(self, 'camera_display_depth', expand=True)
27 changes: 21 additions & 6 deletions blender/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@
import bmesh
from mathutils import Vector, Matrix

from ..utils import set_active_collection


def load(properties, data, *args, **kwargs):
""" Imports photogrammetry data into the current scene.
"""
scene = kwargs.get('scene', None)
if not scene:
raise Exception('Scene required to load data in blender.load')
collection = set_active_collection(**kwargs)
camera_collection = set_active_collection(name='Cameras', parent=collection, **kwargs)

if 'resolution' in data:
if 'resolution' in data and properties.update_render_size:
scene.render.resolution_x, scene.render.resolution_y = data['resolution']

for cid, camera in data['cameras'].items():
Expand All @@ -23,13 +27,24 @@ def load(properties, data, *args, **kwargs):
# https://github.com/simonfuhrmann/mve/wiki/Math-Cookbook
# t = -R * c
# where c is the real world position as I've calculated, and t is the camera location stored in bundle.out
translation = -1 * mrot * Vector(camera['t'])
translation = -1 * mrot @ Vector(camera['t'])

# create and link camera
name = os.path.splitext(os.path.basename(camera['filename']))[0]
cdata = bpy.data.cameras.new(name)
cam = bpy.data.objects.new(name, cdata)
scene.objects.link(cam)
camera_collection.objects.link(cam)

# add background images per camera!
cdata.show_background_images = True
bg = cdata.background_images.new()
image_path = camera['filename']
if properties.relative_paths:
image_path = bpy.path.relpath(image_path)
img = bpy.data.images.load(image_path)
bg.image = img
bg.alpha = properties.camera_alpha
bg.display_depth = properties.camera_display_depth

# set parameters
cam.location = translation
Expand All @@ -44,9 +59,9 @@ def load(properties, data, *args, **kwargs):
mesh = bpy.data.meshes.new("PhotogrammetryPoints")
obj = bpy.data.objects.new("PhotogrammetryPoints", mesh)

scene.objects.link(obj)
scene.objects.active = obj
obj.select = True
collection.objects.link(obj)
scene.view_layers[0].objects.active = obj
obj.select_set(True)

# add all coords from bundler points as vertices
bm = bmesh.new()
Expand Down
2 changes: 1 addition & 1 deletion bundler/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@


class PHOTOGRAMMETRY_PG_bundler(PropertyGroup):
dirpath = StringProperty(name='Bundler Data Directory', subtype='DIR_PATH', default='//bundler')
dirpath: StringProperty(name='Bundler Data Directory', subtype='DIR_PATH', default='//bundler')

def draw(self, layout):
layout.prop(self, 'dirpath')
6 changes: 3 additions & 3 deletions colmap/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@


class PHOTOGRAMMETRY_PG_colmap(PropertyGroup):
dirpath = StringProperty(name='Workspace Directory', subtype='DIR_PATH', default='//colmap')
max_image_size = IntProperty(name='Max Image Size', subtype='PIXEL', default=0, min=0, description='If you run out of GPU memory during reconstruction, you can reduce the maximum image size by setting this option (0px means no limit)')
import_points = BoolProperty(name='Import point cloud after reconstruction', default=False)
dirpath: StringProperty(name='Workspace Directory', subtype='DIR_PATH', default='//colmap')
max_image_size: IntProperty(name='Max Image Size', subtype='PIXEL', default=0, min=0, description='If you run out of GPU memory during reconstruction, you can reduce the maximum image size by setting this option (0px means no limit)')
import_points: BoolProperty(name='Import point cloud after reconstruction', default=False)

def draw(self, layout):
layout.prop(self, 'dirpath')
Expand Down
9 changes: 5 additions & 4 deletions colmap/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import bpy

from ..pmvs.load import prepare_workspace
from ..utils import get_binpath_for_module, get_binary_path
from ..utils import set_active_collection, get_binpath_for_module, get_binary_path


class PMVSProperties(object):
Expand All @@ -26,8 +26,8 @@ def load(properties, data, *args, **kwargs):
env = os.environ.copy()
if platform.system().lower() == 'windows':
env.update({
'PATH': "{binpath}\lib;{path}".format(binpath=binpath, path=env.get('PATH', '')),
'QT_PLUGIN_PATH': "{binpath}\lib;{qt_plugin_path}".format(binpath=binpath, qt_plugin_path=env.get('QT_PLUGIN_PATH', ''))
'PATH': f"{binpath}\lib;{env.get('PATH', '')}",
'QT_PLUGIN_PATH': f"{binpath}\lib;{env.get('QT_PLUGIN_PATH', '')}"
})

# running COLMAP requires transforming to PMVS first
Expand Down Expand Up @@ -63,4 +63,5 @@ def load(properties, data, *args, **kwargs):
raise Exception('COLMAP stereo_fusion failed, see system console for details')

if os.path.exists(model) and properties.import_points:
bpy.ops.import_mesh.ply(filepath=model)
set_active_collection(**kwargs)
bpy.ops.import_mesh.ply(filepath=model)
2 changes: 1 addition & 1 deletion imagemodeler/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def extract(properties, *args, **kwargs):
'k': (-float(intrinsics['rd']),) * 3,
'c': extrinsics['T'],
'R': tuple(map(tuple, tuple(extrinsics['R']))),
't': tuple(-1 * extrinsics['R'] * extrinsics['T']),
't': tuple(-1 * extrinsics['R'] @ extrinsics['T']),
})

if 'resolution' not in data:
Expand Down
6 changes: 3 additions & 3 deletions imagemodeler/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
from bpy.types import PropertyGroup

class PHOTOGRAMMETRY_PG_image_modeller(PropertyGroup):
filepath = StringProperty(name="Filepath", description="Filename of source ImageModeler file", subtype='FILE_PATH')
imagepath = StringProperty(name="Image directory", subtype='DIR_PATH', description="Path to directory containg images referenced by ImageModeler file. Defaults to same directory as ImageModeler file")
subdirs = BoolProperty(name='Search subdirectories for images', default=True)
filepath: StringProperty(name="Filepath", description="Filename of source ImageModeler file", subtype='FILE_PATH')
imagepath: StringProperty(name="Image directory", subtype='DIR_PATH', description="Path to directory containg images referenced by ImageModeler file. Defaults to same directory as ImageModeler file")
subdirs: BoolProperty(name='Search subdirectories for images', default=True)

def draw(self, layout):
layout.prop(self, 'filepath')
Expand Down
14 changes: 7 additions & 7 deletions pmvs/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@


class PHOTOGRAMMETRY_PG_pmvs(PropertyGroup):
dirpath = StringProperty(name='Workspace Directory', subtype='DIR_PATH', default='//pmvs')
level = IntProperty(name='Level', default=1, min=0, description='When level is 0, original (full) resolution images are used. When level is 1, images are halved (or 4 times less pixels). And so on')
csize = IntProperty(name='Cell Size', default=2, min=1, description='Controls the density of reconstructions. increasing the value of cell size leads to sparser reconstructions')
threshold = FloatProperty(name='Threshold', default=0.7, min=0.15, description='A patch reconstruction is accepted as a success and kept, if its associcated photometric consistency measure is above this threshold. The software repeats three iterations of the reconstruction pipeline, and this threshold is relaxed (decreased) by 0.05 at the end of each iteration')
wsize = IntProperty(name='Window Size', default=7, min=1, description='The software samples wsize x wsize pixel colors from each image to compute photometric consistency score. Increasing the value leads to more stable reconstructions, but the program becomes slower')
minImageNum = IntProperty(name='Min Image Num', default=3, min=2, description='Each 3D point must be visible in at least this many images to be reconstructed. If images are poor quality, increase this value')
import_points = BoolProperty(name='Import point cloud after reconstruction', default=False)
dirpath: StringProperty(name='Workspace Directory', subtype='DIR_PATH', default='//pmvs')
level: IntProperty(name='Level', default=1, min=0, description='When level is 0, original (full) resolution images are used. When level is 1, images are halved (or 4 times less pixels). And so on')
csize: IntProperty(name='Cell Size', default=2, min=1, description='Controls the density of reconstructions. increasing the value of cell size leads to sparser reconstructions')
threshold: FloatProperty(name='Threshold', default=0.7, min=0.15, description='A patch reconstruction is accepted as a success and kept, if its associcated photometric consistency measure is above this threshold. The software repeats three iterations of the reconstruction pipeline, and this threshold is relaxed (decreased) by 0.05 at the end of each iteration')
wsize: IntProperty(name='Window Size', default=7, min=1, description='The software samples wsize x wsize pixel colors from each image to compute photometric consistency score. Increasing the value leads to more stable reconstructions, but the program becomes slower')
minImageNum: IntProperty(name='Min Image Num', default=3, min=2, description='Each 3D point must be visible in at least this many images to be reconstructed. If images are poor quality, increase this value')
import_points: BoolProperty(name='Import point cloud after reconstruction', default=False)

def draw(self, layout):
layout.prop(self, 'dirpath')
Expand Down
3 changes: 2 additions & 1 deletion pmvs/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import bpy

from ..bundler.load import load as load_bundler
from ..utils import get_binpath_for_module, get_binary_path
from ..utils import set_active_collection, get_binpath_for_module, get_binary_path


class BundlerProperties(object):
Expand Down Expand Up @@ -106,6 +106,7 @@ def load(properties, data, *args, **kwargs):

model = os.path.join('models', 'reconstruction.ply')
if os.path.exists(model) and properties.import_points:
set_active_collection(**kwargs)
bpy.ops.import_mesh.ply(filepath=model)


Expand Down
41 changes: 40 additions & 1 deletion utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,45 @@ def __unicode__(self):
return self.name


def find_layer_collection(layer_collection, collection_name, parent_name=None):
found = layer_collection.children.get(collection_name)
if found:
# found it, does it have the right parent?
if parent_name:
if layer_collection.name == parent_name:
return found
else:
return found
else:
# check all child layer collections for this collection
for child in layer_collection.children.values():
found = find_layer_collection(child, collection_name, parent_name)
if found:
return found
return None


def set_active_layer_collection(view_layers, collection_name, parent_name=None):
for view_layer in view_layers:
found = find_layer_collection(view_layer.layer_collection, collection_name, parent_name)
if found:
view_layer.active_layer_collection = found
return found
return None


def set_active_collection(name='Photogrammetry', parent=None, **kwargs):
scene = kwargs.get('scene', None)
if scene:
col = (parent or scene.collection).children.get(name)
if not col:
col = bpy.data.collections.new(name)
(parent or scene.collection).children.link(col)
set_active_layer_collection(scene.view_layers, col.name, parent.name if parent else None)
return col
return None


def get_binpath_for_module(module_name):
module_root = module_name
if os.path.isfile(module_name):
Expand All @@ -38,7 +77,7 @@ def get_binary_path(module_binary_path, binary_name):
if not module_binary_path:
return None
for ext in ['', '.exe']:
p = os.path.join(module_binary_path, '{binary_name}{ext}'.format(binary_name=binary_name, ext=ext))
p = os.path.join(module_binary_path, f'{binary_name}{ext}')
if os.path.exists(p):
return p
return None

0 comments on commit 9edcd19

Please sign in to comment.