diff --git a/README.md b/README.md index 238aa8e..69db745 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ This list is not by order of importance. - [ ] Interfaces w/ Other Software (PMVS/OpenSFM/Pix4D/DroneMapper) - [x] Point Cloud - [x] Post Processing -- [ ] GCP Processing +- [x] GCP Processing (Requires 3D "ground" and 2D "image" Files) - [ ] Oblique Imagery and/or 3D Model - [x] Conform / Rename Outputs to ODM Conventions - [x] Wire Up 2D and Potree Tile Creation @@ -37,19 +37,30 @@ This list is not by order of importance. - [x] Full Integration w/ WebODM - [x] Progress Reporting - [ ] Staged Restart Ability - +- [x] Optimize Orthomosaic Generation +- [ ] Multi-Threaded Orthomosaic Generation/Seamline Feathering (BETA) + Note: This project currently creates a geo-referenced DEM and Ortho from our 4th Ave. test imagery (and most likely your imagery). The results are located in their respective directories in UTM projection. ## Test Data -[DroneMapper 4th Ave. Reservoir](https://dronemapper.com/software/4thAve.zip) - 48 Geo-tagged Images DJI Phantom 3 Advanced +[DroneMapper 4th Ave. Reservoir](https://dronemapper.com/software/4thAve.zip) - 48 Geo-Tagged Images DJI Phantom 3 Advanced ![4th_Images](docs/readme_4th_images.png) -## Results +[DroneMapper Greg 1 & 2 Reservoir](https://dronemapper.com/sample_data/) - 189 Geo-Tagged Images DJI Phantom 3 Advanced w/ Trimble 5800 Surveyed GCP Data + +![Greg1_2_Images](docs/readme_gregg12_images.png) + +## 4th Ave. Results ![4th_DEM](docs/readme_4th_DEM.PNG) ![4th_Ortho](docs/readme_4th_Ortho.PNG) +## Gregg 1 & 2 GCP Results + +![Greg12 Ortho](docs/readme_greg12_ortho_gcp.png) +![Greg12 GCP1](docs/readme_greg12_gcp1.png) + * Results clipped to an AOI and displayed using Global Mapper [GlobalMapper](https://bluemarblegeo.com) ## Mission Planning / Execution @@ -89,6 +100,70 @@ Linux users can connect to 127.0.0.1. If the computer running NodeMICMAC is using an old or 32bit CPU, you need to compile OpenDroneMap from sources and setup NodeMICMAC natively. You cannot use docker. Docker images work with CPUs with 64-bit extensions, MMX, SSE, SSE2, SSE3 and SSSE3 instruction set support or higher. Seeing a `Illegal instruction` error while processing images is an indication that your CPU is too old. +## Using Ground Control Points + +For GCP processing, you will need to include two files in txt format. Examples of the files are shown +below. + +`DroneMapperGCP_3D.txt` + +`GCPNAME UTMX UTMY Z PRECISIONXY PRECISIONZ` +```$xslt +base 250021.111 4319269.236 2593.462 0.005 0.005 +1 250002.422 4319308.241 2594.213 0.005 0.005 +pf1 250041.932 4319214.143 2590.057 0.005 0.005 +hg1 250020.983 4319214.803 2590.412 0.005 0.005 +sw1 250006.047 4319127.513 2592.616 0.005 0.005 +2 249990.82 4319134.391 2592.927 0.005 0.005 +3 249876.345 4319057.461 2593.507 0.005 0.005 +4 250175.483 4319290.858 2584.199 0.005 0.005 +hg2 250117.42 4319009.086 2565.418 0.005 0.005 +5 250114.413 4318998.234 2567.861 0.005 0.005 +sw2 250159.165 4319019.774 2567.198 0.005 0.005 +``` + +`DroneMapperGCP_2D.txt` + +`GCPNAME IMAGENAME PIXELX PIXELY` +```$xslt +1 DJI_0065.JPG 3036 1244 +1 DJI_0066.JPG 3022 1915 +1 DJI_0071.JPG 1859 1179 +1 DJI_0072.JPG 1860 1800 +1 DJI_0099.JPG 1350 1129 +1 DJI_0100.JPG 1355 1779 +2 DJI_0058.JPG 2872 1129 +2 DJI_0059.JPG 2870 1741 +2 DJI_0078.JPG 1997 1170 +2 DJI_0079.JPG 2050 1787 +2 DJI_0092.JPG 1219 1166 +2 DJI_0093.JPG 1226 1736 +3 DJI_0012.JPG 1477 458 +3 DJI_0013.JPG 1521 1051 +3 DJI_0014.JPG 1553 1677 +3 DJI_0015.JPG 1595 2295 +3 DJI_0020.JPG 1632 641 +3 DJI_0021.JPG 1673 1312 +3 DJI_0022.JPG 1699 1964 +4 DJI_0166.JPG 2402 890 +4 DJI_0167.JPG 2386 1442 +4 DJI_0168.JPG 2378 1986 +4 DJI_0173.JPG 2386 917 +4 DJI_0174.JPG 2385 1502 +4 DJI_0175.JPG 2410 2106 +5 DJI_0120.JPG 2590 788 +5 DJI_0121.JPG 2605 1286 +5 DJI_0122.JPG 2622 1808 +5 DJI_0151.JPG 2019 1093 +5 DJI_0152.JPG 2049 1589 +5 DJI_0153.JPG 2090 2061 +5 DJI_0155.JPG 1335 1365 +5 DJI_0156.JPG 1386 1832 +``` + +The files should be space delimited and can be named anything, as long as `3D` exists in the ground filename and `2D` +exists in the images filename. + ## API Options / Command Line Parameters ```bash @@ -101,19 +176,22 @@ optional arguments: Path to input images --project-path Path to the project folder + --gcp + Path to MicMac GCP txt files --max-concurrency The maximum number of cores to use in processing. Default: 4 --resize-to Scale image width for tie-point extraction. Default: 800 - --zoom The level of DEM construction. 2 means 2x native GSD. - Default: 2 Values: 1, 2, 4, 8 + --zoom The level of DEM construction. 4 means 4x native GSD. + Default: 4 Values: 1, 2, 4, 8 --matcher-distance Distance threshold in meters to find pre-matching images based on GPS exif data. Default: 0 (use auto-distance) --multi-scale Uses an image file pair based multi-scale tie-point generation routine similar to Photoscan. + --remove-ortho-tiles Remove every other ortho tile. Speeds up ortho creation and radiometric equalization. --camera-cloud Creates a sparse point cloud with camera positions --image-footprint Creates a point cloud and geojson with image footprints --ccd-width The CCD sensor width in millimeters (mm). Example: @@ -259,4 +337,4 @@ Stay current with upstream MicMac development providing an easy to use interface ## MicMac Version -Cloned: 04-26-2019 Commit: [fec03b2](https://github.com/micmacIGN/micmac/commit/fec03b2b9596886f9b929f5b663bbded3ae591c0) \ No newline at end of file +Cloned: 04-26-2019 Commit: [fec03b2](https://github.com/micmacIGN/micmac/commit/fec03b2b9596886f9b929f5b663bbded3ae591c0) diff --git a/dm/odm_options.json b/dm/odm_options.json index dd25e05..dd52d0e 100644 --- a/dm/odm_options.json +++ b/dm/odm_options.json @@ -1,7 +1,12 @@ { "--project-path": { "metavar": "", - "help": "Path to the project folder" + "help": "Path to the project folder." + }, + "--gcp": { + "default": "None", + "metavar": "", + "help": "Path to the files containing the ground control points used for georeferencing. Default: None" }, "--max-concurrency": { "default": "4", @@ -16,10 +21,10 @@ "help": "Scale image width for tie-point extraction. Default: 800" }, "--zoom": { - "default": "2", + "default": "4", "type": "", "metavar": "", - "help": "The level of DEM construction. 2 means 2x native GSD. Default: 2 Values: 1, 2, 4, 8" + "help": "The level of DEM construction. 4 means 4x native GSD. Default: 4 Values: 1, 2, 4, 8" }, "--matcher-distance": { "metavar": "", @@ -30,6 +35,11 @@ "default":"False", "help":"Uses an image file pair based multi-scale tie-point generation routine similar to Photoscan." }, + "--remove-ortho-tiles":{ + "action":"store_true", + "default":"False", + "help":"Remove every other ortho tile. Speeds up ortho creation and radiometric equalization." + }, "--camera-cloud":{ "action":"store_true", "default":"False", @@ -64,4 +74,4 @@ "version": "DroneMapper MicMac", "help": "Displays version number and exits. " } -} \ No newline at end of file +} diff --git a/dm/opendm/config.py b/dm/opendm/config.py index ee24089..ff06027 100644 --- a/dm/opendm/config.py +++ b/dm/opendm/config.py @@ -31,6 +31,10 @@ def config(): metavar='', help='Path to the project folder') + parser.add_argument('--gcp', + metavar='', + help='Path to the gcp files') + parser.add_argument('name', metavar='', type=alphanumeric_string, @@ -52,10 +56,10 @@ def config(): parser.add_argument('--zoom', metavar='', - default=2, + default=4, type=int, - help='The level of DEM construction. 2 means 2x native GSD. ' - 'Default: 2 Values: 1, 2, 4, 8') + help='The level of DEM construction. 4 means 4x native GSD. ' + 'Default: 4 Values: 1, 2, 4, 8') parser.add_argument('--matcher-distance', metavar='', @@ -69,6 +73,11 @@ def config(): help='Uses an image file pair based multi-scale tie-point ' 'generation routine similar to Photoscan.') + parser.add_argument('--remove-ortho-tiles', + action='store_true', + default=False, + help='Remove every other ortho tile. Speeds up ortho creation and radiometric equalization.') + parser.add_argument('--camera-cloud', action='store_true', default=False, @@ -104,4 +113,4 @@ def config(): 'run.py --help` for more information. ') sys.exit(1) - return args \ No newline at end of file + return args diff --git a/dm/run.py b/dm/run.py index df1e361..cc79c1c 100755 --- a/dm/run.py +++ b/dm/run.py @@ -157,6 +157,115 @@ def create_lcd(image_dir, image_ext, ccd_width, ccd_height): f.write('\n') +def convert_gcp(gcp_dir): + ''' + Convert MicMac GCP TXT files to MicMac GCP XML format + :param image_dir: path + :return: + + Expects files to be named, DroneMapperGCP_2D.txt and DroneMapperGCP_3D.txt + + DroneMapperGCP_2D.txt format (single space delimiter): + GCP IMAGENAME PIXELX PIXELY + + DroneMapperGCP_3D.txt format (single space delimiter): + GCP UTMX UTMY Z PRECISION X/Y PRECISIONZ + ''' + + log.MM_INFO('Converting GCP.') + + gcp_files = os.listdir(gcp_dir) + for file in gcp_files: + if '3d' in file.lower(): + gcp_3d_file = file + if '2d' in file.lower(): + gcp_2d_file = file + + # MicMac GCP 2D - target locations in images + # GCPNAME IMAGE PIXELX PIXELY + MM2D = namedtuple('MM2D', ['gcp', 'img', 'px', 'py']) + + with open(io.join_paths(gcp_dir, gcp_2d_file), 'r') as f2d_txt: + lines = (l.split() for l in f2d_txt.readlines()) + images = [MM2D(gcp=l[0].strip(), + img=l[1].strip(), + px=l[2].strip(), + py=l[3].strip()) + for l in lines] + + with open(io.join_paths(image_dir, 'images.xml'), 'wb') as images_xml: + images_xml.write('\n') + images_xml.write('\n') + for image in images: + log.MM_INFO('GCP in image {}'.format(image)) + gcp = image[0] + img = image[1] + px = image[2] + py = image[3] + images_xml.write('\t\n') + name_im = '\t\t {} \n'.format(img) + images_xml.write(name_im) + images_xml.write('\t\t\n') + name_pt = '\t\t\t {} \n'.format(gcp) + images_xml.write(name_pt) + pt_im = '\t\t\t {} {} \n'.format(px, py) + images_xml.write(pt_im) + images_xml.write('\t\t\n') + images_xml.write('\t\n') + images_xml.write('\n') + + # MicMac GCP 3D - real world target position on ground (UTM) + # GCPNAME UTMX UTMY Z PRECISIONXY PRECISIONZ + MM3D = namedtuple('MM3D', ['gcp', 'x', 'y', 'z', 'pxy', 'pz']) + + with open(io.join_paths(gcp_dir, gcp_3d_file), 'r') as f3d_txt: + lines = (l.split() for l in f3d_txt.readlines()) + coords = [MM3D(gcp=l[0].strip(), + x=l[1].strip(), + y=l[2].strip(), + z=l[3].strip(), + pxy=l[4].strip(), + pz=l[5].strip()) + for l in lines] + + with open(io.join_paths(image_dir, 'ground.xml'), 'wb') as ground_xml: + ground_xml.write('\n') + ground_xml.write('\n') + ground_xml.write('\t\n') + for c in coords: + log.MM_INFO('GCP on ground {}'.format(c)) + gcp = c[0] + x = c[1] + y = c[2] + z = c[3] + pxy = c[4] + pz = c[5] + ground_xml.write('\t\t\n') + pt = '\t\t\t {} {} {} \n'.format(x, y, z) + ground_xml.write(pt) + name_pt = '\t\t\t {} \n'.format(gcp) + ground_xml.write(name_pt) + precision = '\t\t\t {} {} {} \n'.format(pxy, pxy, pz) + ground_xml.write(precision) + ground_xml.write('\t\t\n') + ground_xml.write('\t\n') + ground_xml.write('\n') + + +def remove_ortho_tiles(): + ''' + Remove every other orthomosaic tile. Optimizes color balance and radiometric routine, + speeds up ortho generation using Porto/Tawny module. + ''' + ort_files = 'Ortho-MEC-Malt/Ort_*.tif' + if glob.glob(ort_files): + tiles = glob.glob(ort_files) + tiles.sort(key=lambda f: int(filter(str.isdigit, f))) + for tile in tiles[::2]: + os.remove(tile) + log.MM_INFO('Removing ortho tile {}'.format(tile)) + + # RUN if __name__ == '__main__': @@ -169,6 +278,7 @@ def create_lcd(image_dir, image_ext, ccd_width, ccd_height): project_dir = io.join_paths(args.project_path, args.name) image_dir = io.join_paths(project_dir, 'images') + gcp_dir = io.join_paths(project_dir, 'gcp') IN_DOCKER = os.environ.get('DEBIAN_FRONTEND', False) @@ -261,7 +371,11 @@ def create_lcd(image_dir, image_ext, ccd_width, ccd_height): 'ext': image_ext, 'mm3d': mm3d } - system.run('{mm3d} CenterBascule .*.{ext} RadialStd RAWGNSS_N Ground_Init_RTL'.format(**kwargs_bascule)) + if args.gcp: + convert_gcp(gcp_dir) + system.run('{mm3d} GCPBascule .*.{ext} RadialStd Ground_Init_RTL ground.xml images.xml ShowD=1'.format(**kwargs_bascule)) + else: + system.run('{mm3d} CenterBascule .*.{ext} RadialStd RAWGNSS_N Ground_Init_RTL'.format(**kwargs_bascule)) progressbc.send_update(50) @@ -270,17 +384,25 @@ def create_lcd(image_dir, image_ext, ccd_width, ccd_height): 'ext': image_ext, 'mm3d': mm3d } - system.run('{mm3d} Campari .*.{ext} Ground_Init_RTL Ground_RTL ' - 'EmGPS=[RAWGNSS_N,5] AllFree=0'.format(**kwargs_campari)) + if args.gcp: + system.run('{mm3d} Campari .*.{ext} Ground_Init_RTL Ground_RTL ' + 'GCP=[ground.xml,0.005,images.xml,0.01] NbIterEnd=6 AllFree=1 DetGCP=1'.format(**kwargs_campari)) + else: + system.run('{mm3d} Campari .*.{ext} Ground_Init_RTL Ground_RTL ' + 'EmGPS=[RAWGNSS_N,5] AllFree=0'.format(**kwargs_campari)) progressbc.send_update(60) - # change projection and system coords to UTM from relative + # change projection and system coords to UTM from relative for GPS only kwargs_chg = { 'ext': image_ext, 'mm3d': mm3d } - system.run('{mm3d} ChgSysCo .*.{ext} Ground_RTL RTLFromExif.xml@SysUTM.xml Ground_UTM'.format(**kwargs_chg)) + if args.gcp: + io.copy('Ori-Ground_RTL', 'Ori-Ground_UTM') + system.run('{mm3d} GCPCtrl .*.{ext} Ground_UTM ground.xml images.xml'.format(**kwargs_chg)) + else: + system.run('{mm3d} ChgSysCo .*.{ext} Ground_RTL RTLFromExif.xml@SysUTM.xml Ground_UTM'.format(**kwargs_chg)) progressbc.send_update(65) @@ -319,7 +441,14 @@ def create_lcd(image_dir, image_ext, ccd_width, ccd_height): progressbc.send_update(80) # build ORTHO - porto_src = '/code/micmac/include/XML_MicMac/Param-Tawny.xml' + if args.remove_ortho_tiles: + remove_ortho_tiles() + + if IN_DOCKER: + porto_src = '/code/micmac/include/XML_MicMac/Param-Tawny.xml' + else: + porto_src = '/home/drnmppr-micmac/include/XML_MicMac/Param-Tawny.xml' # for dev: locally installed micmac branch + porto_dst = 'Ortho-MEC-Malt/Param-Tawny.xml' io.copy(porto_src, porto_dst) system.run('{mm3d} Porto Ortho-MEC-Malt/Param-Tawny.xml'.format(**kwargs_malt)) diff --git a/docs/readme_greg12_gcp1.png b/docs/readme_greg12_gcp1.png new file mode 100644 index 0000000..bd6ac35 Binary files /dev/null and b/docs/readme_greg12_gcp1.png differ diff --git a/docs/readme_greg12_ortho_gcp.png b/docs/readme_greg12_ortho_gcp.png new file mode 100644 index 0000000..80bf4d2 Binary files /dev/null and b/docs/readme_greg12_ortho_gcp.png differ diff --git a/docs/readme_gregg12_images.png b/docs/readme_gregg12_images.png new file mode 100755 index 0000000..e6d3cf1 Binary files /dev/null and b/docs/readme_gregg12_images.png differ