kopia lustrzana https://github.com/OpenDroneMap/ODM
Merge branch 'master' into default-opensfm
commit
65fab9fccc
49
README.md
49
README.md
|
@ -23,7 +23,7 @@ So far, it does Point Clouds, Digital Surface Models, Textured Digital Surface M
|
|||
|
||||
## QUICKSTART
|
||||
|
||||
Requires Ubuntu 14.04 or later, see https://github.com/OpenDroneMap/odm_vagrant for running on Windows in a VM
|
||||
OpenDroneMap can run natively on Ubuntu 14.04 or later, see [Build and Run Using Docker](#build-and-run-using-docker) for running on Windows / MacOS. A Vagrant VM is also available: https://github.com/OpenDroneMap/odm_vagrant.
|
||||
|
||||
*Support for Ubuntu 12.04 is currently BROKEN with the addition of OpenSfM and Ceres-Solver. It is likely to remain broken unless a champion is found to fix it.*
|
||||
|
||||
|
@ -40,7 +40,7 @@ Current version: 0.2 (this software is in beta)
|
|||
- textured mesh model: odm_texturing/odm_textured_model_geo.obj
|
||||
- point cloud (georeferenced): odm_georeferencing/odm_georeferenced_model.ply
|
||||
|
||||
See below for more detailed installation instructions.
|
||||
See [here](https://github.com/OpenDroneMap/OpenDroneMap/tree/ebaaf802a1fb50e335b3807a35d00cba1e106d11#installation) for more detailed installation instructions.
|
||||
|
||||
### Installation
|
||||
|
||||
|
@ -65,22 +65,13 @@ Note that using `run.sh` sets these temporarily in the shell.
|
|||
|
||||
### Run OpenDroneMap
|
||||
|
||||
First you need a set of images, taken from a drone or otherwise.
|
||||
|
||||
Create a project folder and places your images in an "images" directory:
|
||||
|
||||
|-- /path/to/project/
|
||||
|-- images/
|
||||
|-- img-1234.jpg
|
||||
|-- ...
|
||||
|
||||
Example data can be cloned from https://github.com/OpenDroneMap/odm_data
|
||||
First you need a set of images, taken from a drone or otherwise. Example data can be cloned from https://github.com/OpenDroneMap/odm_data
|
||||
|
||||
Then run:
|
||||
|
||||
python run.py --project-path /path/to/project
|
||||
|
||||
There are many options for tuning your project. See the [wiki](https://github.com/OpenDroneMap/OpenDroneMap/wiki/Run-Time-Parameters) or run `python run.py -h`
|
||||
python run.py --project-path /path/to/project -i /path/to/images
|
||||
|
||||
The images will be copied over to the project path so you only need to specify the `-i /path/` once. There are many options for tuning your project. See the [wiki](https://github.com/OpenDroneMap/OpenDroneMap/wiki/Run-Time-Parameters) or run `python run.py -h`
|
||||
|
||||
### View Results
|
||||
|
||||
|
@ -121,7 +112,7 @@ Any file ending in .obj or .ply can be opened and viewed in [MeshLab](http://mes
|
|||
|
||||

|
||||
|
||||
You can also view the orthophoto GeoTIFF in QGIS or other mapping software:
|
||||
You can also view the orthophoto GeoTIFF in [QGIS](http://www.qgis.org/) or other mapping software:
|
||||
|
||||

|
||||
|
||||
|
@ -133,16 +124,16 @@ has equivalent procedures for Mac OS X and Windows. See [docs.docker.com](docs.d
|
|||
OpenDroneMap is Dockerized, meaning you can use containerization to build and run it without tampering with the configuration of libraries and packages already
|
||||
installed on your machine. Docker software is free to install and use in this context. If you don't have it installed,
|
||||
see the [Docker Ubuntu installation tutorial](https://docs.docker.com/engine/installation/linux/ubuntulinux/) and follow the
|
||||
instructions through "Create a Docker group". Once Docker is installed, an OpenDroneMap Docker image can be created
|
||||
like so:
|
||||
instructions through "Create a Docker group". Once Docker is installed, the fastest way to use OpenDroneMap is to run a pre-built image by typing:
|
||||
|
||||
docker run -it --rm -v $(pwd)/images:/code/images -v $(pwd)/odm_orthophoto:/code/odm_orthophoto -v $(pwd)/odm_texturing:/code/odm_texturing opendronemap/opendronemap
|
||||
|
||||
If you want to build your own Docker image from sources, type:
|
||||
|
||||
docker build -t packages -f packages.Dockerfile .
|
||||
docker build -t odm_image .
|
||||
docker run -it --user root\
|
||||
-v $(pwd)/images:/code/images\
|
||||
-v $(pwd)/odm_orthophoto:/code/odm_orthophoto\
|
||||
-v $(pwd)/odm_texturing:/code/odm_texturing\
|
||||
--rm odm_image
|
||||
|
||||
docker build -t my_odm_image .
|
||||
docker run -it --rm -v $(pwd)/images:/code/images v $(pwd)/odm_orthophoto:/code/odm_orthophoto -v $(pwd)/odm_texturing:/code/odm_texturing my_odm_image
|
||||
|
||||
Using this method, the containerized ODM will process the images in the OpenDroneMap/images directory and output results
|
||||
to the OpenDroneMap/odm_orthophoto and OpenDroneMap/odm_texturing directories as described in the **Viewing Results** section.
|
||||
|
@ -150,13 +141,11 @@ If you want to view other results outside the Docker image simply add which dire
|
|||
established above. For example, if you're interested in the dense cloud results generated by PMVS and in the orthophoto,
|
||||
simply use the following `docker run` command after building the image:
|
||||
|
||||
docker run -it \
|
||||
-v $(pwd)/images:/code/images\
|
||||
-v $(pwd)/pmvs:/code/pmvs\
|
||||
-v $(pwd)/odm_orthophoto:/code/odm_orthophoto\
|
||||
--rm odm_image
|
||||
docker run -it --rm -v $(pwd)/images:/code/images -v $(pwd)/pmvs:/code/pmvs -v $(pwd)/odm_orthophoto:/code/odm_orthophoto my_odm_image
|
||||
|
||||
To pass in custom parameters to the run.py script, simply pass it as arguments to the `docker run` command.
|
||||
To pass in custom parameters to the run.py script, simply pass it as arguments to the `docker run` command. For example:
|
||||
|
||||
docker run -it --rm -v $(pwd)/images:/code/images v $(pwd)/odm_orthophoto:/code/odm_orthophoto -v $(pwd)/odm_texturing:/code/odm_texturing opendronemap/opendronemap --resize-to 1800 --force-ccd 6.16
|
||||
|
||||
## User Interface
|
||||
|
||||
|
|
|
@ -1141,9 +1141,40 @@ void Georef::createGeoreferencedModelFromExifData()
|
|||
|
||||
void Georef::chooseBestGCPTriplet(size_t &gcp0, size_t &gcp1, size_t &gcp2)
|
||||
{
|
||||
double minTotError = std::numeric_limits<double>::infinity();
|
||||
size_t numThreads = boost::thread::hardware_concurrency();
|
||||
boost::thread_group threads;
|
||||
std::vector<GeorefBestTriplet*> triplets;
|
||||
for(size_t t = 0; t < numThreads; ++t)
|
||||
{
|
||||
GeorefBestTriplet* triplet = new GeorefBestTriplet();
|
||||
triplets.push_back(triplet);
|
||||
threads.create_thread(boost::bind(&Georef::findBestGCPTriplet, this, boost::ref(triplet->t_), boost::ref(triplet->s_), boost::ref(triplet->p_), t, numThreads, boost::ref(triplet->err_)));
|
||||
}
|
||||
|
||||
for(size_t t = 0; t < gcps_.size(); ++t)
|
||||
threads.join_all();
|
||||
|
||||
double minTotError = std::numeric_limits<double>::infinity();
|
||||
for(size_t t = 0; t<numThreads; t++)
|
||||
{
|
||||
GeorefBestTriplet* triplet = triplets[t];
|
||||
if(minTotError > triplet->err_)
|
||||
{
|
||||
minTotError = triplet->err_;
|
||||
gcp0 = triplet->t_;
|
||||
gcp1 = triplet->s_;
|
||||
gcp2 = triplet->p_;
|
||||
}
|
||||
delete triplet;
|
||||
}
|
||||
|
||||
log_ << "Mean georeference error " << minTotError / static_cast<double>(gcps_.size()) << '\n';
|
||||
}
|
||||
|
||||
void Georef::findBestGCPTriplet(size_t &gcp0, size_t &gcp1, size_t &gcp2, size_t offset, size_t stride, double &minTotError)
|
||||
{
|
||||
minTotError = std::numeric_limits<double>::infinity();
|
||||
|
||||
for(size_t t = offset; t < gcps_.size(); t+=stride)
|
||||
{
|
||||
if (gcps_[t].use_)
|
||||
{
|
||||
|
@ -1180,14 +1211,47 @@ void Georef::chooseBestGCPTriplet(size_t &gcp0, size_t &gcp1, size_t &gcp2)
|
|||
}
|
||||
}
|
||||
}
|
||||
log_ << "Mean georeference error " << minTotError / static_cast<double>(cameras_.size()) << '\n';
|
||||
|
||||
log_ << '[' << offset+1 << " of " << stride << "] Mean georeference error " << minTotError / static_cast<double>(gcps_.size());
|
||||
log_ << " (" << gcp0 << ", " << gcp1 << ", " << gcp2 << ")\n";
|
||||
}
|
||||
|
||||
void Georef::chooseBestCameraTriplet(size_t &cam0, size_t &cam1, size_t &cam2)
|
||||
{
|
||||
size_t numThreads = boost::thread::hardware_concurrency();
|
||||
boost::thread_group threads;
|
||||
std::vector<GeorefBestTriplet*> triplets;
|
||||
for(size_t t = 0; t < numThreads; ++t)
|
||||
{
|
||||
GeorefBestTriplet* triplet = new GeorefBestTriplet();
|
||||
triplets.push_back(triplet);
|
||||
threads.create_thread(boost::bind(&Georef::findBestCameraTriplet, this, boost::ref(triplet->t_), boost::ref(triplet->s_), boost::ref(triplet->p_), t, numThreads, boost::ref(triplet->err_)));
|
||||
}
|
||||
|
||||
threads.join_all();
|
||||
|
||||
double minTotError = std::numeric_limits<double>::infinity();
|
||||
for(size_t t = 0; t<numThreads; t++)
|
||||
{
|
||||
GeorefBestTriplet* triplet = triplets[t];
|
||||
if(minTotError > triplet->err_)
|
||||
{
|
||||
minTotError = triplet->err_;
|
||||
cam0 = triplet->t_;
|
||||
cam1 = triplet->s_;
|
||||
cam2 = triplet->p_;
|
||||
}
|
||||
delete triplet;
|
||||
}
|
||||
|
||||
log_ << "Mean georeference error " << minTotError / static_cast<double>(cameras_.size()) << '\n';
|
||||
}
|
||||
|
||||
void Georef::findBestCameraTriplet(size_t &cam0, size_t &cam1, size_t &cam2, size_t offset, size_t stride, double &minTotError)
|
||||
{
|
||||
minTotError = std::numeric_limits<double>::infinity();
|
||||
|
||||
for(size_t t = 0; t < cameras_.size(); ++t)
|
||||
for(size_t t = offset; t < cameras_.size(); t+=stride)
|
||||
{
|
||||
for(size_t s = t; s < cameras_.size(); ++s)
|
||||
{
|
||||
|
@ -1215,8 +1279,9 @@ void Georef::chooseBestCameraTriplet(size_t &cam0, size_t &cam1, size_t &cam2)
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
log_ << "Mean georeference error " << minTotError / static_cast<double>(cameras_.size()) << '\n';
|
||||
|
||||
log_ << '[' << offset+1 << " of " << stride << "] Mean georeference error " << minTotError / static_cast<double>(cameras_.size());
|
||||
log_ << " (" << cam0 << ", " << cam1 << ", " << cam2 << ")\n";
|
||||
}
|
||||
|
||||
void Georef::printGeorefSystem()
|
||||
|
|
|
@ -111,6 +111,17 @@ struct GeorefCamera
|
|||
friend std::ostream& operator<<(std::ostream &os, const GeorefCamera &cam);
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief The GeorefBestTriplet struct is used to store the best triplet found.
|
||||
*/
|
||||
struct GeorefBestTriplet
|
||||
{
|
||||
size_t t_; /**< First ordinate of the best triplet found. **/
|
||||
size_t s_; /**< Second ordinate of the best triplet found. **/
|
||||
size_t p_; /**< Third ordinate of the best triplet found. **/
|
||||
double err_; /**< Error of this triplet. **/
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief The Georef class is used to transform a mesh into a georeferenced system.
|
||||
* The class reads camera positions from a bundle file.
|
||||
|
@ -196,10 +207,20 @@ private:
|
|||
*/
|
||||
void chooseBestGCPTriplet(size_t &gcp0, size_t &gcp1, size_t &gcp2);
|
||||
|
||||
/*!
|
||||
* \brief findBestGCPTriplet Partitioned version of chooseBestGCPTriplet.
|
||||
*/
|
||||
void findBestGCPTriplet(size_t &gcp0, size_t &gcp1, size_t &gcp2, size_t offset, size_t stride, double &minTotError);
|
||||
|
||||
/*!
|
||||
* \brief chooseBestCameraTriplet Chooses the best triplet of cameras to use when making the model georeferenced.
|
||||
*/
|
||||
void chooseBestCameraTriplet(size_t &cam0, size_t &cam1, size_t &cam2);
|
||||
|
||||
/*!
|
||||
* \brief findBestCameraTriplet Partitioned version of chooseBestCameraTriplet.
|
||||
*/
|
||||
void findBestCameraTriplet(size_t &cam0, size_t &cam1, size_t &cam2, size_t offset, size_t stride, double &minTotError);
|
||||
|
||||
/*!
|
||||
* \brief printGeorefSystem Prints a file containing information about the georeference system, next to the ouptut file.
|
||||
|
|
|
@ -16,6 +16,10 @@ parser = argparse.ArgumentParser(description='OpenDroneMap')
|
|||
|
||||
|
||||
def config():
|
||||
parser.add_argument('--images', '-i',
|
||||
metavar='<string>',
|
||||
help='Path to input images'),
|
||||
|
||||
parser.add_argument('--project-path',
|
||||
metavar='<string>',
|
||||
help='Path to the project to process')
|
||||
|
@ -118,6 +122,13 @@ def config():
|
|||
'images based on GPS exif data. Set to 0 to skip '
|
||||
'pre-matching. Default: %(default)s')
|
||||
|
||||
parser.add_argument('--opensfm-processes',
|
||||
metavar='<positive integer>',
|
||||
default=context.num_cores,
|
||||
type=int,
|
||||
help=('The maximum number of processes to use in dense '
|
||||
'reconstruction. Default: %(default)s'))
|
||||
|
||||
parser.add_argument('--use-pmvs',
|
||||
action='store_true',
|
||||
default=False,
|
||||
|
@ -166,7 +177,7 @@ def config():
|
|||
'value leads to more stable reconstructions, but '
|
||||
'the program becomes slower. Default: %(default)s')
|
||||
|
||||
parser.add_argument('--pmvs-minImageNum',
|
||||
parser.add_argument('--pmvs-min-images',
|
||||
metavar='<positive integer>',
|
||||
default=3,
|
||||
type=int,
|
||||
|
@ -181,14 +192,14 @@ def config():
|
|||
help=('The maximum number of cores to use in dense '
|
||||
'reconstruction. Default: %(default)s'))
|
||||
|
||||
parser.add_argument('--odm_meshing-maxVertexCount',
|
||||
parser.add_argument('--mesh-size',
|
||||
metavar='<positive integer>',
|
||||
default=100000,
|
||||
type=int,
|
||||
help=('The maximum vertex count of the output mesh '
|
||||
'Default: %(default)s'))
|
||||
|
||||
parser.add_argument('--odm_meshing-octreeDepth',
|
||||
parser.add_argument('--mesh-octree-depth',
|
||||
metavar='<positive integer>',
|
||||
default=9,
|
||||
type=int,
|
||||
|
@ -196,14 +207,14 @@ def config():
|
|||
'increase to get more vertices, recommended '
|
||||
'values are 8-12. Default: %(default)s'))
|
||||
|
||||
parser.add_argument('--odm_meshing-samplesPerNode',
|
||||
parser.add_argument('--mesh-samples',
|
||||
metavar='<float >= 1.0>',
|
||||
default=1.0,
|
||||
type=float,
|
||||
help=('Number of points per octree node, recommended '
|
||||
'and default value: %(default)s'))
|
||||
|
||||
parser.add_argument('--odm_meshing-solverDivide',
|
||||
parser.add_argument('--mesh-solver-divide',
|
||||
metavar='<positive integer>',
|
||||
default=9,
|
||||
type=int,
|
||||
|
@ -213,78 +224,64 @@ def config():
|
|||
'times slightly but helps reduce memory usage. '
|
||||
'Default: %(default)s'))
|
||||
|
||||
parser.add_argument('--mvs_texturing-dataTerm',
|
||||
parser.add_argument('--texturing-data-term',
|
||||
metavar='<string>',
|
||||
default='gmi',
|
||||
help=('Data term: [area, gmi]. Default: %(default)s'))
|
||||
help=('Data term: [area, gmi]. Default: '
|
||||
'%(default)s'))
|
||||
|
||||
parser.add_argument('--mvs_texturing-outlierRemovalType',
|
||||
parser.add_argument('--texturing-outlier-removal-type',
|
||||
metavar='<string>',
|
||||
default='none',
|
||||
help=('Type of photometric outlier removal method: '
|
||||
'[none, gauss_damping, gauss_clamping]. Default: '
|
||||
'%(default)s'))
|
||||
|
||||
parser.add_argument('--mvs_texturing-skipGeometricVisibilityTest',
|
||||
parser.add_argument('--texturing-skip-visibility-test',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help=('Skip geometric visibility test. Default: %(default)s'))
|
||||
help=('Skip geometric visibility test. Default: '
|
||||
' %(default)s'))
|
||||
|
||||
parser.add_argument('--mvs_texturing-skipGlobalSeamLeveling',
|
||||
parser.add_argument('--texturing-skip-global-seam-leveling',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help=('Skip geometric visibility test. Default: %(default)s'))
|
||||
help=('Skip global seam leveling. Useful for IR data.'
|
||||
'Default: %(default)s'))
|
||||
|
||||
parser.add_argument('--mvs_texturing-skipLocalSeamLeveling',
|
||||
parser.add_argument('--texturing-skip-local-seam-leveling',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help=('Skip local seam blending. Default: %(default)s'))
|
||||
help='Skip local seam blending. Default: %(default)s')
|
||||
|
||||
parser.add_argument('--mvs_texturing-skipHoleFilling',
|
||||
parser.add_argument('--texturing-skip-hole-filling',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help=('Skip filling of holes in the mesh. Default: %(default)s'))
|
||||
help=('Skip filling of holes in the mesh. Default: '
|
||||
' %(default)s'))
|
||||
|
||||
parser.add_argument('--mvs_texturing-keepUnseenFaces',
|
||||
parser.add_argument('--texturing-keep-unseen-faces',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help=('Keep faces in the mesh that are not seen in any camera. '
|
||||
'Default: %(default)s'))
|
||||
|
||||
# Old odm_texturing arguments
|
||||
|
||||
parser.add_argument('--odm_texturing-textureResolution',
|
||||
metavar='<positive integer>',
|
||||
default=4096,
|
||||
type=int,
|
||||
help=('The resolution of the output textures. Must be '
|
||||
'greater than textureWithSize. Default: %(default)s'))
|
||||
|
||||
parser.add_argument('--odm_texturing-textureWithSize',
|
||||
metavar='<positive integer>',
|
||||
default=3600,
|
||||
type=int,
|
||||
help=('The resolution to rescale the images performing '
|
||||
'the texturing. Default: %(default)s'))
|
||||
|
||||
# End of old odm_texturing arguments
|
||||
|
||||
parser.add_argument('--odm_georeferencing-gcpFile',
|
||||
parser.add_argument('--gcp',
|
||||
metavar='<path string>',
|
||||
default='gcp_list.txt',
|
||||
default=None,
|
||||
help=('path to the file containing the ground control '
|
||||
'points used for georeferencing. Default: '
|
||||
'%(default)s. The file needs to '
|
||||
'be on the following line format: \neasting '
|
||||
'northing height pixelrow pixelcol imagename'))
|
||||
|
||||
parser.add_argument('--odm_georeferencing-useGcp',
|
||||
parser.add_argument('--use-exif',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Enabling GCPs from the file above. The GCP file '
|
||||
'is not used by default.')
|
||||
help=('Use this tag if you have a gcp_list.txt but '
|
||||
'want to use the exif geotags instead'))
|
||||
|
||||
parser.add_argument('--odm_orthophoto-resolution',
|
||||
parser.add_argument('--orthophoto-resolution',
|
||||
metavar='<float > 0.0>',
|
||||
default=20.0,
|
||||
type=float,
|
||||
|
|
|
@ -331,9 +331,13 @@ class ODM_GeoRef(object):
|
|||
|
||||
|
||||
class ODM_Tree(object):
|
||||
def __init__(self, root_path):
|
||||
def __init__(self, root_path, images_path):
|
||||
# root path to the project
|
||||
self.root_path = io.absolute_path_file(root_path)
|
||||
if not images_path:
|
||||
self.input_images = io.join_paths(self.root_path, 'images')
|
||||
else:
|
||||
self.input_images = io.absolute_path_file(images_path)
|
||||
|
||||
# modules paths
|
||||
|
||||
|
|
5
run.py
5
run.py
|
@ -3,6 +3,7 @@
|
|||
from opendm import log
|
||||
from opendm import config
|
||||
from opendm import system
|
||||
from opendm import io
|
||||
|
||||
import sys
|
||||
import ecto
|
||||
|
@ -26,8 +27,12 @@ if __name__ == '__main__':
|
|||
# Force to provide the images path
|
||||
if args.project_path is None:
|
||||
usage()
|
||||
elif not io.dir_exists(args.project_path):
|
||||
log.ODM_WARNING('Directory %s does not exist. Creating it now.' % args.project_path)
|
||||
system.mkdir_p(os.path.abspath(args.project_path))
|
||||
|
||||
#If user asks to rerun everything, delete all of the existing progress directories.
|
||||
# TODO: Move this somewhere it's not hard-coded
|
||||
if args.rerun_all:
|
||||
os.system("rm -rf "
|
||||
+ args.project_path + "images_resize/ "
|
||||
|
|
|
@ -7,6 +7,8 @@ from opendm import context
|
|||
from opendm import io
|
||||
from opendm import types
|
||||
from opendm import log
|
||||
from opendm import system
|
||||
from shutil import copyfile
|
||||
|
||||
|
||||
def make_odm_photo(force_focal, force_ccd, path_file):
|
||||
|
@ -28,32 +30,41 @@ class ODMLoadDatasetCell(ecto.Cell):
|
|||
outputs.declare("photos", "list of ODMPhotos", [])
|
||||
|
||||
def process(self, inputs, outputs):
|
||||
# check if the extension is sopported
|
||||
# check if the extension is supported
|
||||
def supported_extension(file_name):
|
||||
(pathfn, ext) = os.path.splitext(file_name)
|
||||
return ext.lower() in context.supported_extensions
|
||||
|
||||
# Get supported images from dir
|
||||
def get_images(in_dir):
|
||||
# filter images for its extension type
|
||||
log.ODM_DEBUG(in_dir)
|
||||
return [f for f in io.get_files_list(in_dir) if supported_extension(f)]
|
||||
|
||||
log.ODM_INFO('Running ODM Load Dataset Cell')
|
||||
|
||||
# get inputs
|
||||
tree = self.inputs.tree
|
||||
|
||||
# set images directory
|
||||
images_dir = tree.dataset_resize
|
||||
# get images directory
|
||||
input_dir = tree.input_images
|
||||
images_dir = tree.dataset_raw
|
||||
resize_dir = tree.dataset_resize
|
||||
|
||||
if not io.dir_exists(images_dir):
|
||||
images_dir = tree.dataset_raw
|
||||
# Check first if a project already exists. This is a mediocre way to check, by checking the resize dir
|
||||
if io.dir_exists(resize_dir):
|
||||
log.ODM_DEBUG("resize dir: %s" % resize_dir)
|
||||
images_dir = resize_dir
|
||||
# if first time running, create project directory and copy images over to project/images
|
||||
else:
|
||||
if not io.dir_exists(images_dir):
|
||||
log.ODM_ERROR("You must put your pictures into an <images> directory")
|
||||
return ecto.QUIT
|
||||
log.ODM_INFO("Project directory %s doesn't exist. Creating it now. " % images_dir)
|
||||
system.mkdir_p(images_dir)
|
||||
copied = [copyfile(io.join_paths(input_dir, f), io.join_paths(images_dir, f)) for f in get_images(input_dir)]
|
||||
|
||||
log.ODM_DEBUG('Loading dataset from: %s' % images_dir)
|
||||
|
||||
# find files in the given directory
|
||||
files = io.get_files_list(images_dir)
|
||||
|
||||
# filter images for its extension type
|
||||
files = [f for f in files if supported_extension(f)]
|
||||
files = get_images(images_dir)
|
||||
|
||||
if files:
|
||||
# create ODMPhoto list
|
||||
|
|
|
@ -3,7 +3,6 @@ import os
|
|||
|
||||
from opendm import context
|
||||
from opendm import types
|
||||
from opendm import config
|
||||
from opendm import io
|
||||
from opendm import system
|
||||
|
||||
|
@ -45,7 +44,7 @@ class ODMApp(ecto.BlackBox):
|
|||
'opensfm': ODMOpenSfMCell(use_exif_size=False,
|
||||
feature_process_size=p.args.resize_to,
|
||||
feature_min_frames=p.args.min_num_features,
|
||||
processes=context.num_cores,
|
||||
processes=p.args.opensfm_processes,
|
||||
matching_gps_neighbors=p.args.matcher_neighbors,
|
||||
matching_gps_distance=p.args.matcher_distance),
|
||||
'slam': ODMSlamCell(),
|
||||
|
@ -54,36 +53,32 @@ class ODMApp(ecto.BlackBox):
|
|||
csize=p.args.pmvs_csize,
|
||||
thresh=p.args.pmvs_threshold,
|
||||
wsize=p.args.pmvs_wsize,
|
||||
min_imgs=p.args.pmvs_minImageNum,
|
||||
min_imgs=p.args.pmvs_min_images,
|
||||
cores=p.args.pmvs_num_cores),
|
||||
'meshing': ODMeshingCell(max_vertex=p.args.odm_meshing_maxVertexCount,
|
||||
oct_tree=p.args.odm_meshing_octreeDepth,
|
||||
samples=p.args.odm_meshing_samplesPerNode,
|
||||
solver=p.args.odm_meshing_solverDivide,
|
||||
'meshing': ODMeshingCell(max_vertex=p.args.mesh_size,
|
||||
oct_tree=p.args.mesh_octree_depth,
|
||||
samples=p.args.mesh_samples,
|
||||
solver=p.args.mesh_solver_divide,
|
||||
verbose=p.args.verbose),
|
||||
'texturing': ODMMvsTexCell(data_term=p.args.mvs_texturing_dataTerm,
|
||||
outlier_rem_type=p.args.mvs_texturing_outlierRemovalType,
|
||||
skip_vis_test=p.args.mvs_texturing_skipGeometricVisibilityTest,
|
||||
skip_glob_seam_leveling=p.args.mvs_texturing_skipGlobalSeamLeveling,
|
||||
skip_loc_seam_leveling=p.args.mvs_texturing_skipLocalSeamLeveling,
|
||||
skip_hole_fill=p.args.mvs_texturing_skipHoleFilling,
|
||||
keep_unseen_faces=p.args.mvs_texturing_keepUnseenFaces),
|
||||
# Old odm_texturing
|
||||
# 'texturing': ODMTexturingCell(resize=p.args['resize_to'],
|
||||
# resolution=p.args['odm_texturing_textureResolution'],
|
||||
'texturing': ODMMvsTexCell(data_term=p.args.texturing_data_term,
|
||||
outlier_rem_type=p.args.texturing_outlier_removal_type,
|
||||
skip_vis_test=p.args.texturing_skip_visibility_test,
|
||||
skip_glob_seam_leveling=p.args.texturing_skip_global_seam_leveling,
|
||||
skip_loc_seam_leveling=p.args.texturing_skip_local_seam_leveling,
|
||||
skip_hole_fill=p.args.texturing_skip_hole_filling,
|
||||
keep_unseen_faces=p.args.texturing_keep_unseen_faces),
|
||||
'georeferencing': ODMGeoreferencingCell(img_size=p.args.resize_to,
|
||||
gcp_file=p.args.odm_georeferencing_gcpFile,
|
||||
use_gcp=p.args.odm_georeferencing_useGcp,
|
||||
gcp_file=p.args.gcp,
|
||||
use_exif=p.args.use_exif,
|
||||
verbose=p.args.verbose),
|
||||
'orthophoto': ODMOrthoPhotoCell(resolution=p.args.odm_orthophoto_resolution,
|
||||
verbose=p.args.verbose)
|
||||
|
||||
}
|
||||
'orthophoto': ODMOrthoPhotoCell(resolution=p.args.orthophoto_resolution,
|
||||
verbose=p.args.verbose)
|
||||
}
|
||||
|
||||
return cells
|
||||
|
||||
def configure(self, p, _i, _o):
|
||||
tree = types.ODM_Tree(p.args.project_path)
|
||||
tree = types.ODM_Tree(p.args.project_path, p.args.images)
|
||||
self.tree = ecto.Constant(value=tree)
|
||||
|
||||
# TODO(dakota) put this somewhere better maybe
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import ecto
|
||||
import csv
|
||||
import os
|
||||
|
||||
from opendm import io
|
||||
from opendm import log
|
||||
|
@ -14,8 +15,8 @@ class ODMGeoreferencingCell(ecto.Cell):
|
|||
'points used for georeferencing.The file needs to '
|
||||
'be on the following line format: \neasting '
|
||||
'northing height pixelrow pixelcol imagename', 'gcp_list.txt')
|
||||
params.declare("use_gcp", 'set to true for enabling GCPs from the file above', False)
|
||||
params.declare("img_size", 'image size used in calibration', 2400)
|
||||
params.declare("use_exif", 'use exif', False)
|
||||
params.declare("verbose", 'print additional messages to console', False)
|
||||
|
||||
def declare_io(self, params, inputs, outputs):
|
||||
|
@ -26,6 +27,12 @@ class ODMGeoreferencingCell(ecto.Cell):
|
|||
outputs.declare("reconstruction", "list of ODMReconstructions", [])
|
||||
|
||||
def process(self, inputs, outputs):
|
||||
|
||||
# find a file in the root directory
|
||||
def find(file, dir):
|
||||
for root, dirs, files in os.walk(dir):
|
||||
return '/'.join((root, file)) if file in files else None
|
||||
|
||||
# Benchmarking
|
||||
start_time = system.now_raw()
|
||||
|
||||
|
@ -34,7 +41,9 @@ class ODMGeoreferencingCell(ecto.Cell):
|
|||
# get inputs
|
||||
args = self.inputs.args
|
||||
tree = self.inputs.tree
|
||||
gcpfile = io.join_paths(tree.root_path, self.params.gcp_file)
|
||||
gcpfile = io.join_paths(tree.root_path, self.params.gcp_file) \
|
||||
if self.params.gcp_file else find('gcp_list.txt', tree.root_path)
|
||||
geocreated = True
|
||||
verbose = '-verbose' if self.params.verbose else ''
|
||||
|
||||
# define paths and create working directories
|
||||
|
@ -42,34 +51,34 @@ class ODMGeoreferencingCell(ecto.Cell):
|
|||
|
||||
# in case a gcp file it's not provided, let's try to generate it using
|
||||
# images metadata. Internally calls jhead.
|
||||
if not self.params.use_gcp and \
|
||||
not io.file_exists(tree.odm_georeferencing_coords):
|
||||
log.ODM_DEBUG(self.params.gcp_file)
|
||||
if not self.params.gcp_file: # and \
|
||||
# not io.file_exists(tree.odm_georeferencing_coords):
|
||||
|
||||
log.ODM_WARNING('Warning: No coordinates file. '
|
||||
'Generating coordinates file in: %s'
|
||||
log.ODM_WARNING('No coordinates file. '
|
||||
'Generating coordinates file: %s'
|
||||
% tree.odm_georeferencing_coords)
|
||||
try:
|
||||
# odm_georeference definitions
|
||||
kwargs = {
|
||||
'bin': context.odm_modules_path,
|
||||
'imgs': tree.dataset_resize,
|
||||
'imgs_list': tree.opensfm_bundle_list,
|
||||
'coords': tree.odm_georeferencing_coords,
|
||||
'log': tree.odm_georeferencing_utm_log,
|
||||
'verbose': verbose
|
||||
}
|
||||
|
||||
# run UTM extraction binary
|
||||
system.run('{bin}/odm_extract_utm -imagesPath {imgs}/ '
|
||||
'-imageListFile {imgs_list} -outputCoordFile {coords} {verbose} '
|
||||
'-logFile {log}'.format(**kwargs))
|
||||
# odm_georeference definitions
|
||||
kwargs = {
|
||||
'bin': context.odm_modules_path,
|
||||
'imgs': tree.dataset_resize,
|
||||
'imgs_list': tree.opensfm_bundle_list,
|
||||
'coords': tree.odm_georeferencing_coords,
|
||||
'log': tree.odm_georeferencing_utm_log,
|
||||
'verbose': verbose
|
||||
}
|
||||
|
||||
# run UTM extraction binary
|
||||
extract_utm = system.run_and_return('{bin}/odm_extract_utm -imagesPath {imgs}/ '
|
||||
'-imageListFile {imgs_list} -outputCoordFile {coords} {verbose} '
|
||||
'-logFile {log}'.format(**kwargs))
|
||||
|
||||
if extract_utm != '':
|
||||
log.ODM_WARNING('Could not generate coordinates file. '
|
||||
'Ignore if there is a GCP file. Error: %s'
|
||||
% extract_utm)
|
||||
|
||||
except Exception, e:
|
||||
log.ODM_ERROR('Could not generate GCP file from images metadata.'
|
||||
'Consider rerunning with argument --odm_georeferencing-useGcp'
|
||||
' and provide a proper GCP file')
|
||||
log.ODM_ERROR(e)
|
||||
return ecto.QUIT
|
||||
|
||||
# check if we rerun cell or not
|
||||
rerun_cell = (args.rerun is not None and
|
||||
|
@ -103,47 +112,59 @@ class ODMGeoreferencingCell(ecto.Cell):
|
|||
else:
|
||||
kwargs['pc'] = tree.pmvs_model
|
||||
|
||||
if self.params.use_gcp and \
|
||||
io.file_exists(gcpfile):
|
||||
# Check to see if the GCP file exists
|
||||
|
||||
system.run('{bin}/odm_georef -bundleFile {bundle} -imagesPath {imgs} -imagesListPath {imgs_list} '
|
||||
'-bundleResizedTo {size} -inputFile {model} -outputFile {model_geo} '
|
||||
'-inputPointCloudFile {pc} -outputPointCloudFile {pc_geo} {verbose} '
|
||||
'-logFile {log} -georefFileOutputPath {geo_sys} -gcpFile {gcp} '
|
||||
'-outputCoordFile {coords}'.format(**kwargs))
|
||||
else:
|
||||
if not self.params.use_exif and (self.params.gcp_file or find('gcp_list.txt', tree.root_path)):
|
||||
log.ODM_INFO('Found %s' % gcpfile)
|
||||
try:
|
||||
system.run('{bin}/odm_georef -bundleFile {bundle} -imagesPath {imgs} -imagesListPath {imgs_list} '
|
||||
'-bundleResizedTo {size} -inputFile {model} -outputFile {model_geo} '
|
||||
'-inputPointCloudFile {pc} -outputPointCloudFile {pc_geo} {verbose} '
|
||||
'-logFile {log} -georefFileOutputPath {geo_sys} -gcpFile {gcp} '
|
||||
'-outputCoordFile {coords}'.format(**kwargs))
|
||||
except Exception:
|
||||
log.ODM_EXCEPTION('Georeferencing failed. ')
|
||||
return ecto.QUIT
|
||||
elif io.file_exists(tree.odm_georeferencing_coords):
|
||||
log.ODM_INFO('Running georeferencing with generated coords file.')
|
||||
system.run('{bin}/odm_georef -bundleFile {bundle} -inputCoordFile {coords} '
|
||||
'-inputFile {model} -outputFile {model_geo} '
|
||||
'-inputPointCloudFile {pc} -outputPointCloudFile {pc_geo} {verbose} '
|
||||
'-logFile {log} -georefFileOutputPath {geo_sys}'.format(**kwargs))
|
||||
else:
|
||||
log.ODM_WARNING('Georeferencing failed. Make sure your '
|
||||
'photos have geotags in the EXIF or you have '
|
||||
'provided a GCP file. ')
|
||||
geocreated = False # skip the rest of the georeferencing
|
||||
|
||||
# update images metadata
|
||||
geo_ref = types.ODM_GeoRef()
|
||||
geo_ref.parse_coordinate_system(tree.odm_georeferencing_coords)
|
||||
if geocreated:
|
||||
# update images metadata
|
||||
geo_ref = types.ODM_GeoRef()
|
||||
geo_ref.parse_coordinate_system(tree.odm_georeferencing_coords)
|
||||
|
||||
for idx, photo in enumerate(self.inputs.photos):
|
||||
geo_ref.utm_to_latlon(tree.odm_georeferencing_latlon, photo, idx)
|
||||
for idx, photo in enumerate(self.inputs.photos):
|
||||
geo_ref.utm_to_latlon(tree.odm_georeferencing_latlon, photo, idx)
|
||||
|
||||
# convert ply model to LAS reference system
|
||||
geo_ref.convert_to_las(tree.odm_georeferencing_model_ply_geo,
|
||||
tree.odm_georeferencing_pdal)
|
||||
# convert ply model to LAS reference system
|
||||
geo_ref.convert_to_las(tree.odm_georeferencing_model_ply_geo,
|
||||
tree.odm_georeferencing_pdal)
|
||||
|
||||
# XYZ point cloud output
|
||||
log.ODM_INFO("Creating geo-referenced CSV file (XYZ format, can be used with GRASS to create DEM)")
|
||||
with open(tree.odm_georeferencing_xyz_file, "wb") as csvfile:
|
||||
csvfile_writer = csv.writer(csvfile, delimiter=",")
|
||||
reachedpoints = False
|
||||
with open(tree.odm_georeferencing_model_ply_geo) as f:
|
||||
for lineNumber, line in enumerate(f):
|
||||
if reachedpoints:
|
||||
tokens = line.split(" ")
|
||||
csv_line = [float(tokens[0])+geo_ref.utm_east_offset,
|
||||
float(tokens[1])+geo_ref.utm_north_offset,
|
||||
tokens[2]]
|
||||
csvfile_writer.writerow(csv_line)
|
||||
if line.startswith("end_header"):
|
||||
reachedpoints = True
|
||||
csvfile.close()
|
||||
# XYZ point cloud output
|
||||
log.ODM_INFO("Creating geo-referenced CSV file (XYZ format, can be used with GRASS to create DEM)")
|
||||
with open(tree.odm_georeferencing_xyz_file, "wb") as csvfile:
|
||||
csvfile_writer = csv.writer(csvfile, delimiter=",")
|
||||
reachedpoints = False
|
||||
with open(tree.odm_georeferencing_model_ply_geo) as f:
|
||||
for lineNumber, line in enumerate(f):
|
||||
if reachedpoints:
|
||||
tokens = line.split(" ")
|
||||
csv_line = [float(tokens[0])+geo_ref.utm_east_offset,
|
||||
float(tokens[1])+geo_ref.utm_north_offset,
|
||||
tokens[2]]
|
||||
csvfile_writer.writerow(csv_line)
|
||||
if line.startswith("end_header"):
|
||||
reachedpoints = True
|
||||
csvfile.close()
|
||||
|
||||
else:
|
||||
log.ODM_WARNING('Found a valid georeferenced model in: %s'
|
||||
|
|
|
@ -22,7 +22,7 @@ class ODMOrthoPhotoCell(ecto.Cell):
|
|||
# Benchmarking
|
||||
start_time = system.now_raw()
|
||||
|
||||
log.ODM_INFO('Running OMD OrthoPhoto Cell')
|
||||
log.ODM_INFO('Running ODM Orthophoto Cell')
|
||||
|
||||
# get inputs
|
||||
args = self.inputs.args
|
||||
|
@ -44,7 +44,6 @@ class ODMOrthoPhotoCell(ecto.Cell):
|
|||
# odm_orthophoto definitions
|
||||
kwargs = {
|
||||
'bin': context.odm_modules_path,
|
||||
'model_geo': tree.odm_georeferencing_model_obj_geo,
|
||||
'log': tree.odm_orthophoto_log,
|
||||
'ortho': tree.odm_orthophoto_file,
|
||||
'corners': tree.odm_orthophoto_corners,
|
||||
|
@ -52,57 +51,66 @@ class ODMOrthoPhotoCell(ecto.Cell):
|
|||
'verbose': verbose
|
||||
}
|
||||
|
||||
kwargs['model_geo'] = tree.odm_georeferencing_model_obj_geo \
|
||||
if io.file_exists(tree.odm_georeferencing_coords) \
|
||||
else tree.odm_textured_model_obj
|
||||
|
||||
|
||||
# run odm_orthophoto
|
||||
system.run('{bin}/odm_orthophoto -inputFile {model_geo} '
|
||||
'-logFile {log} -outputFile {ortho} -resolution {res} {verbose} '
|
||||
'-outputCornerFile {corners}'.format(**kwargs))
|
||||
|
||||
# Create georeferenced GeoTiff
|
||||
geotiffcreated = False
|
||||
georef = types.ODM_GeoRef()
|
||||
# creates the coord refs # TODO I don't want to have to do this twice- after odm_georef
|
||||
georef.parse_coordinate_system(tree.odm_georeferencing_coords)
|
||||
if not io.file_exists(tree.odm_georeferencing_coords):
|
||||
log.ODM_WARNING('No coordinates file. A georeferenced raster '
|
||||
'will not be created')
|
||||
else:
|
||||
# Create georeferenced GeoTiff
|
||||
geotiffcreated = False
|
||||
georef = types.ODM_GeoRef()
|
||||
# creates the coord refs # TODO I don't want to have to do this twice- after odm_georef
|
||||
georef.parse_coordinate_system(tree.odm_georeferencing_coords)
|
||||
|
||||
if georef.epsg and georef.utm_east_offset and georef.utm_north_offset:
|
||||
ulx = uly = lrx = lry = 0.0
|
||||
with open(tree.odm_orthophoto_corners) as f:
|
||||
for lineNumber, line in enumerate(f):
|
||||
if lineNumber == 0:
|
||||
tokens = line.split(' ')
|
||||
if len(tokens) == 4:
|
||||
ulx = float(tokens[0]) + \
|
||||
float(georef.utm_east_offset)
|
||||
lry = float(tokens[1]) + \
|
||||
float(georef.utm_north_offset)
|
||||
lrx = float(tokens[2]) + \
|
||||
float(georef.utm_east_offset)
|
||||
uly = float(tokens[3]) + \
|
||||
float(georef.utm_north_offset)
|
||||
log.ODM_INFO('Creating GeoTIFF')
|
||||
if georef.epsg and georef.utm_east_offset and georef.utm_north_offset:
|
||||
ulx = uly = lrx = lry = 0.0
|
||||
with open(tree.odm_orthophoto_corners) as f:
|
||||
for lineNumber, line in enumerate(f):
|
||||
if lineNumber == 0:
|
||||
tokens = line.split(' ')
|
||||
if len(tokens) == 4:
|
||||
ulx = float(tokens[0]) + \
|
||||
float(georef.utm_east_offset)
|
||||
lry = float(tokens[1]) + \
|
||||
float(georef.utm_north_offset)
|
||||
lrx = float(tokens[2]) + \
|
||||
float(georef.utm_east_offset)
|
||||
uly = float(tokens[3]) + \
|
||||
float(georef.utm_north_offset)
|
||||
log.ODM_INFO('Creating GeoTIFF')
|
||||
|
||||
kwargs = {
|
||||
'ulx': ulx,
|
||||
'uly': uly,
|
||||
'lrx': lrx,
|
||||
'lry': lry,
|
||||
'epsg': georef.epsg,
|
||||
'png': tree.odm_orthophoto_file,
|
||||
'tiff': tree.odm_orthophoto_tif,
|
||||
'log': tree.odm_orthophoto_tif_log
|
||||
}
|
||||
kwargs = {
|
||||
'ulx': ulx,
|
||||
'uly': uly,
|
||||
'lrx': lrx,
|
||||
'lry': lry,
|
||||
'epsg': georef.epsg,
|
||||
'png': tree.odm_orthophoto_file,
|
||||
'tiff': tree.odm_orthophoto_tif,
|
||||
'log': tree.odm_orthophoto_tif_log
|
||||
}
|
||||
|
||||
system.run('gdal_translate -a_ullr {ulx} {uly} {lrx} {lry} '
|
||||
'-co TILED=yes '
|
||||
'-co COMPRESS=DEFLATE '
|
||||
'-co PREDICTOR=2 '
|
||||
'-co BLOCKXSIZE=512 '
|
||||
'-co BLOCKYSIZE=512 '
|
||||
'-co NUM_THREADS=ALL_CPUS '
|
||||
'-a_srs \"EPSG:{epsg}\" {png} {tiff} > {log}'.format(**kwargs))
|
||||
geotiffcreated = True
|
||||
if not geotiffcreated:
|
||||
log.ODM_WARNING('No geo-referenced orthophoto created due '
|
||||
'to missing geo-referencing or corner coordinates.')
|
||||
system.run('gdal_translate -a_ullr {ulx} {uly} {lrx} {lry} '
|
||||
'-co TILED=yes '
|
||||
'-co COMPRESS=DEFLATE '
|
||||
'-co PREDICTOR=2 '
|
||||
'-co BLOCKXSIZE=512 '
|
||||
'-co BLOCKYSIZE=512 '
|
||||
'-co NUM_THREADS=ALL_CPUS '
|
||||
'-a_srs \"EPSG:{epsg}\" {png} {tiff} > {log}'.format(**kwargs))
|
||||
geotiffcreated = True
|
||||
if not geotiffcreated:
|
||||
log.ODM_WARNING('No geo-referenced orthophoto created due '
|
||||
'to missing geo-referencing or corner coordinates.')
|
||||
|
||||
else:
|
||||
log.ODM_WARNING('Found a valid orthophoto in: %s' % tree.odm_orthophoto_file)
|
||||
|
|
Ładowanie…
Reference in New Issue