├── .gitignore ├── LICENSE.md ├── README.md ├── catalogues ├── HeadCraters.csv └── LROCCraters.csv ├── docs ├── CNN1.png ├── CNN2.png └── Using Zenodo Data.ipynb ├── get_unique_craters.py ├── input_data_gen.py ├── model_train.py ├── requirements.txt ├── run_get_unique_craters.py ├── run_input_data_gen.py ├── run_model_train.py ├── tests ├── LunarLROLrocKaguya_1180mperpix_downsamp.png ├── README.md ├── sample_crater_csv.hdf5 ├── sample_crater_csv_metadata.hdf5 ├── sample_template_match.hdf5 ├── test_get_unique_craters.py ├── test_input_data_gen.py ├── test_model_train.py └── test_utils.py └── utils ├── __init__.py ├── processing.py ├── template_match_target.py └── transform.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | *.py[cod] 3 | *.a 4 | *.o 5 | *.so 6 | __pycache__ 7 | 8 | # ipynb stuff 9 | .ipynb_checkpoints/ 10 | docs/.ipynb_checkpoints/ 11 | 12 | # Other 13 | .cache 14 | .ipynb_checkpoints/ 15 | *~ 16 | 17 | # Mac OSX 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2018 [Ari Silburt, Charles Zhu] 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeepMoon - Lunar Crater Counting Through Deep Learning 2 | Center for Planetary Sciences / Department of Astronomy & Astrophysics / Canadian Institute for Theoretical Astrophysics 3 | University of Toronto 4 | 5 | DeepMoon is a TensorFlow-based pipeline for training a convolutional neural 6 | network (CNN) to recognize craters on the Moon, and determine their positions and 7 | radii. It is the companion repo to the paper 8 | [Lunar Crater Identification via Deep Learning](https://arxiv.org/abs/1803.02192), which 9 | describes the motivation and development of the code, as well as results. 10 | 11 | ## Getting Started 12 | 13 | ### Overview 14 | 15 | The DeepMoon pipeline trains a neural net using data derived from a global 16 | digital elevation map (DEM) and catalogue of craters. The code is divided into 17 | three parts. The first generates a set images of the Moon randomly cropped 18 | from the DEM, with corresponding crater positions and radii. The second 19 | trains a convnet using this data. The third validates the convnet's 20 | predictions. 21 | 22 | To first order, our CNN activates regions with high negative gradients, i.e. 23 | pixels that decrease in value as you move across the image. Below illustrates 24 | two examples of this, the first is a genuine DEM Lunar image from our dataset, 25 | the second is a sample image taken from the web. 26 | ![CNN1](docs/CNN1.png?raw=true) 27 | ![CNN2](docs/CNN2.png?raw=true) 28 | 29 | ### Dependences 30 | 31 | DeepMoon requires the following packages to function: 32 | 33 | - [Python](https://www.python.org/) version 2.7 or 3.5+ 34 | - [Cartopy](http://scitools.org.uk/cartopy/) >= 0.14.2. Cartopy itself has a 35 | number of [dependencies](http://scitools.org.uk/cartopy/docs/latest/installing.html#installing), 36 | including the GEOS and Proj.4.x libraries. (For Ubuntu systems, these can be 37 | installed through the `libgeos++-dev` and `libproj-dev` packages, 38 | respectively.) 39 | - [h5py](http://www.h5py.org/) >= 2.6.0 40 | - [Keras](https://keras.io/) 1.2.2 [(documentation)](https://faroit.github.io/keras-docs/1.2.2/); 41 | also tested with Keras >= 2.0.2 42 | - [Numpy](http://www.numpy.org/) >= 1.12 43 | - [OpenCV](https://pypi.python.org/pypi/opencv-python) >= 3.2.0.6 44 | - [*pandas*](https://pandas.pydata.org/) >= 0.19.1 45 | - [Pillow](https://python-pillow.org/) >= 3.1.2 46 | - [PyTables](http://www.pytables.org/) >=3.4.2 47 | - [TensorFlow](https://www.tensorflow.org/) 0.10.0rc0, also tested with 48 | TensorFlow >= 1.0 49 | 50 | This list can also be found in the `requirements.txt`. 51 | 52 | ### Data Sources 53 | Our train, validation and test datasets, global DEM, post-processed 54 | crater distribution on the test set, best model, and sample output 55 | images can be found [on Zenodo](https://doi.org/10.5281/zenodo.1133969). 56 | 57 | Examples of how to read these data can be found in the 58 | `docs/Using Zenodo Data.ipynb` IPython notebook. 59 | 60 | #### Digital Elevation Maps 61 | 62 | We use the [LRO-Kaguya merged 59 m/pixel DEM][lola dem]. The DEM was 63 | downsampled to 118 m/pixel and converted to 16-bit GeoTiff with the USGS 64 | Astrogeology Cloud Processing service, and then rescaled to 8-bit PNG using 65 | the [GDAL](http://www.gdal.org/) library: 66 | 67 | ``` 68 | gdal_translate -of PNG -scale -21138 21138 -co worldfile=no 69 | LunarLROLrocKaguya_118mperpix_int16.tif LunarLROLrocKaguya_118mperpix.png 70 | ``` 71 | 72 | #### Crater Catalogues 73 | 74 | For the ground truth longitude / latitude locations and sizes of craters, we 75 | combine the [LROC Craters 5 to 20 km diameter][lroc cat] dataset with the 76 | [Head et al. 2010 >= 20 km diameter][head cat] one ([alternate download 77 | link][head cat2]). The LROC dataset was converted from ESRI shapefile to .csv. 78 | They can be found under the `catalogues` folder of the repo, and have had their 79 | formatting slightly modified to be read into *pandas*. 80 | 81 | During initial testing, we also used the [Salamunićcar LU78287GT 82 | catalogue][sala cat]. 83 | 84 | ### Running DeepMoon 85 | 86 | Each stage of DeepMoon has a corresponding script: `run_input_data_gen.py` for 87 | generating input data, `run_model_training.py` to build and train the convnet, 88 | and `run_get_unique_craters.py` to validate predictions and generate a crater 89 | atlas. User-defined parameters, and instructions on on how to use each script, 90 | can be found in the scripts themselves. 91 | 92 | We recommend copying these scripts into a new working directory (and appending 93 | this repo to your Python path) instead of modifying them in the repo. 94 | 95 | Our model with default parameters was trained on a 16GB Tesla P100 GPU, however 96 | 12GB GPUs are more standard. Therefore, our default model may not run on many 97 | systems without reducing the batch size, number of filters, etc., which can 98 | affect final model convergence. 99 | 100 | ### Quick Usage 101 | 102 | See `docs/Using Zenodo Data.ipynb` for basic examples on generating sample 103 | datasets, loading a pre-trained CNN and using it to make predictions on 104 | samples. 105 | 106 | ## Authors 107 | 108 | * **Ari Silburt** - convnet architecture, crater extraction and post-processing 109 | [silburt](https://github.com/silburt) 110 | * **Charles Zhu** - input image generation, data ingestion and post-processing 111 | [cczhu](https://github.com/cczhu) 112 | 113 | ### Contributors 114 | 115 | * Mohamad Ali-Dib - [malidib](https://github.com/malidib/) 116 | * Kristen Menou - [kmenou](https://www.kaggle.com/kmenou) 117 | * Alan Jackson 118 | 119 | ## License 120 | 121 | Copyright 2018 Ari Silburt, Charles Zhu and contributors. 122 | 123 | DeepMoon is free software made available under the MIT License. For details see 124 | the LICENSE.md file. 125 | 126 | [lola dem]: https://astrogeology.usgs.gov/search/map/Moon/LRO/LOLA/Lunar_LRO_LrocKaguya_DEMmerge_60N60S_512ppd 127 | [lroc cat]: http://wms.lroc.asu.edu/lroc/rdr_product_select?filter%5Btext%5D=&filter%5Blat%5D=&filter%5Blon%5D=&filter%5Brad%5D=&filter%5Bwest%5D=&filter%5Beast%5D=&filter%5Bsouth%5D=&filter%5Bnorth%5D=&filter%5Btopographic%5D=either&filter%5Bprefix%5D%5B%5DSHAPEFILE&show_thumbs=0&per_page=100&commit=Search 128 | [head cat]: http://science.sciencemag.org/content/329/5998/1504/tab-figures-data 129 | [head cat2]: http://www.planetary.brown.edu/html_pages/LOLAcraters.html 130 | [sala cat]: https://astrogeology.usgs.gov/search/map/Moon/Research/Craters/GoranSalamuniccar_MoonCraters 131 | -------------------------------------------------------------------------------- /docs/CNN1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silburt/DeepMoon/0bccc9790bbf02b3c1df33bd0f452d87b136dc9d/docs/CNN1.png -------------------------------------------------------------------------------- /docs/CNN2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silburt/DeepMoon/0bccc9790bbf02b3c1df33bd0f452d87b136dc9d/docs/CNN2.png -------------------------------------------------------------------------------- /get_unique_craters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Unique Crater Distribution Functions 3 | 4 | Functions for extracting craters from model target predictions and filtering 5 | out duplicates. 6 | """ 7 | from __future__ import absolute_import, division, print_function 8 | 9 | import numpy as np 10 | import h5py 11 | import sys 12 | import utils.template_match_target as tmt 13 | import utils.processing as proc 14 | import utils.transform as trf 15 | #from keras.models import load_model 16 | 17 | ######################### 18 | def get_model_preds(CP): 19 | """Reads in or generates model predictions. 20 | 21 | Parameters 22 | ---------- 23 | CP : dict 24 | Containins directory locations for loading data and storing 25 | predictions. 26 | 27 | Returns 28 | ------- 29 | craters : h5py 30 | Model predictions. 31 | """ 32 | n_imgs, dtype = CP['n_imgs'], CP['datatype'] 33 | 34 | data = h5py.File(CP['dir_data'], 'r') 35 | 36 | Data = { 37 | dtype: [data['input_images'][:n_imgs].astype('float32'), 38 | data['target_masks'][:n_imgs].astype('float32')] 39 | } 40 | data.close() 41 | proc.preprocess(Data) 42 | 43 | model = load_model(CP['dir_model']) 44 | preds = model.predict(Data[dtype][0]) 45 | 46 | # save 47 | h5f = h5py.File(CP['dir_preds'], 'w') 48 | h5f.create_dataset(dtype, data=preds) 49 | print("Successfully generated and saved model predictions.") 50 | return preds 51 | 52 | ######################### 53 | def add_unique_craters(craters, craters_unique, thresh_longlat2, thresh_rad): 54 | """Generates unique crater distribution by filtering out duplicates. 55 | 56 | Parameters 57 | ---------- 58 | craters : array 59 | Crater tuples from a single image in the form (long, lat, radius). 60 | craters_unique : array 61 | Master array of unique crater tuples in the form (long, lat, radius) 62 | thresh_longlat2 : float. 63 | Hyperparameter that controls the minimum squared longitude/latitude 64 | difference between craters to be considered unique entries. 65 | thresh_rad : float 66 | Hyperparaeter that controls the minimum squared radius difference 67 | between craters to be considered unique entries. 68 | 69 | Returns 70 | ------- 71 | craters_unique : array 72 | Modified master array of unique crater tuples with new crater entries. 73 | """ 74 | k2d = 180. / (np.pi * 1737.4) # km to deg 75 | Long, Lat, Rad = craters_unique.T 76 | for j in range(len(craters)): 77 | lo, la, r = craters[j].T 78 | la_m = (la + Lat) / 2. 79 | minr = np.minimum(r, Rad) # be liberal when filtering dupes 80 | 81 | # duplicate filtering criteria 82 | dL = (((Long - lo) / (minr * k2d / np.cos(np.pi * la_m / 180.)))**2 83 | + ((Lat - la) / (minr * k2d))**2) 84 | dR = np.abs(Rad - r) / minr 85 | index = (dR < thresh_rad) & (dL < thresh_longlat2) 86 | 87 | if len(np.where(index == True)[0]) == 0: 88 | craters_unique = np.vstack((craters_unique, craters[j])) 89 | return craters_unique 90 | 91 | ######################### 92 | def estimate_longlatdiamkm(dim, llbd, distcoeff, coords): 93 | """First-order estimation of long/lat, and radius (km) from 94 | (Orthographic) x/y position and radius (pix). 95 | 96 | For images transformed from ~6000 pixel crops of the 30,000 pixel 97 | LROC-Kaguya DEM, this results in < ~0.4 degree latitude, <~0.2 98 | longitude offsets (~2% and ~1% of the image, respectively) and ~2% error in 99 | radius. Larger images thus may require an exact inverse transform, 100 | depending on the accuracy demanded by the user. 101 | 102 | Parameters 103 | ---------- 104 | dim : tuple or list 105 | (width, height) of input images. 106 | llbd : tuple or list 107 | Long/lat limits (long_min, long_max, lat_min, lat_max) of image. 108 | distcoeff : float 109 | Ratio between the central heights of the transformed image and original 110 | image. 111 | coords : numpy.ndarray 112 | Array of crater x coordinates, y coordinates, and pixel radii. 113 | 114 | Returns 115 | ------- 116 | craters_longlatdiamkm : numpy.ndarray 117 | Array of crater longitude, latitude and radii in km. 118 | """ 119 | # Expand coords. 120 | long_pix, lat_pix, radii_pix = coords.T 121 | 122 | # Determine radius (km). 123 | km_per_pix = 1. / trf.km2pix(dim[1], llbd[3] - llbd[2], dc=distcoeff) 124 | radii_km = radii_pix * km_per_pix 125 | 126 | # Determine long/lat. 127 | deg_per_pix = km_per_pix * 180. / (np.pi * 1737.4) 128 | long_central = 0.5 * (llbd[0] + llbd[1]) 129 | lat_central = 0.5 * (llbd[2] + llbd[3]) 130 | 131 | # Iterative method for determining latitude. 132 | lat_deg_firstest = lat_central - deg_per_pix * (lat_pix - dim[1] / 2.) 133 | latdiff = abs(lat_central - lat_deg_firstest) 134 | # Protect against latdiff = 0 situation. 135 | latdiff[latdiff < 1e-7] = 1e-7 136 | lat_deg = lat_central - (deg_per_pix * (lat_pix - dim[1] / 2.) * 137 | (np.pi * latdiff / 180.) / 138 | np.sin(np.pi * latdiff / 180.)) 139 | # Determine longitude using determined latitude. 140 | long_deg = long_central + (deg_per_pix * (long_pix - dim[0] / 2.) / 141 | np.cos(np.pi * lat_deg / 180.)) 142 | 143 | # Return combined long/lat/radius array. 144 | return np.column_stack((long_deg, lat_deg, radii_km)) 145 | 146 | 147 | def extract_unique_craters(CP, craters_unique): 148 | """Top level function that extracts craters from model predictions, 149 | converts craters from pixel to real (degree, km) coordinates, and filters 150 | out duplicate detections across images. 151 | 152 | Parameters 153 | ---------- 154 | CP : dict 155 | Crater Parameters needed to run the code. 156 | craters_unique : array 157 | Empty master array of unique crater tuples in the form 158 | (long, lat, radius). 159 | 160 | Returns 161 | ------- 162 | craters_unique : array 163 | Filled master array of unique crater tuples. 164 | """ 165 | 166 | # Load/generate model preds 167 | try: 168 | preds = h5py.File(CP['dir_preds'], 'r')[CP['datatype']] 169 | print("Loaded model predictions successfully") 170 | except: 171 | print("Couldnt load model predictions, generating") 172 | preds = get_model_preds(CP) 173 | 174 | # need for long/lat bounds 175 | P = h5py.File(CP['dir_data'], 'r') 176 | llbd, pbd, distcoeff = ('longlat_bounds', 'pix_bounds', 177 | 'pix_distortion_coefficient') 178 | #r_moon = 1737.4 179 | dim = (float(CP['dim']), float(CP['dim'])) 180 | 181 | N_matches_tot = 0 182 | for i in range(CP['n_imgs']): 183 | id = proc.get_id(i) 184 | 185 | coords = tmt.template_match_t(preds[i]) 186 | 187 | # convert, add to master dist 188 | if len(coords) > 0: 189 | 190 | new_craters_unique = estimate_longlatdiamkm( 191 | dim, P[llbd][id], P[distcoeff][id][0], coords) 192 | N_matches_tot += len(coords) 193 | 194 | # Only add unique (non-duplicate) craters 195 | if len(craters_unique) > 0: 196 | craters_unique = add_unique_craters(new_craters_unique, 197 | craters_unique, 198 | CP['llt2'], CP['rt']) 199 | else: 200 | craters_unique = np.concatenate((craters_unique, 201 | new_craters_unique)) 202 | 203 | np.save(CP['dir_result'], craters_unique) 204 | return craters_unique 205 | -------------------------------------------------------------------------------- /input_data_gen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Input Image Dataset Generator Functions 3 | 4 | Functions for generating input and target image datasets from Lunar digital 5 | elevation maps and crater catalogues. 6 | """ 7 | from __future__ import absolute_import, division, print_function 8 | 9 | import numpy as np 10 | import pandas as pd 11 | from PIL import Image 12 | import cartopy.crs as ccrs 13 | import cartopy.img_transform as cimg 14 | import collections 15 | import cv2 16 | import h5py 17 | import utils.transform as trf 18 | 19 | ########## Read Cratering CSVs ########## 20 | 21 | def ReadLROCCraterCSV(filename="catalogues/LROCCraters.csv", sortlat=True): 22 | """Reads LROC 5 - 20 km crater catalogue CSV. 23 | 24 | Parameters 25 | ---------- 26 | filename : str, optional 27 | Filepath and name of LROC csv file. Defaults to the one in the current 28 | folder. 29 | sortlat : bool, optional 30 | If `True` (default), order catalogue by latitude. 31 | 32 | Returns 33 | ------- 34 | craters : pandas.DataFrame 35 | Craters data frame. 36 | """ 37 | craters = pd.read_csv(filename, header=0, usecols=list(range(2, 6))) 38 | if sortlat: 39 | craters.sort_values(by='Lat', inplace=True) 40 | craters.reset_index(inplace=True, drop=True) 41 | 42 | return craters 43 | 44 | 45 | def ReadHeadCraterCSV(filename="catalogues/HeadCraters.csv", sortlat=True): 46 | """Reads Head et al. 2010 >= 20 km diameter crater catalogue. 47 | 48 | Parameters 49 | ---------- 50 | filename : str, optional 51 | Filepath and name of Head et al. csv file. Defaults to the one in 52 | the current folder. 53 | sortlat : bool, optional 54 | If `True` (default), order catalogue by latitude. 55 | 56 | Returns 57 | ------- 58 | craters : pandas.DataFrame 59 | Craters data frame. 60 | """ 61 | craters = pd.read_csv(filename, header=0, 62 | names=['Long', 'Lat', 'Diameter (km)']) 63 | if sortlat: 64 | craters.sort_values(by='Lat', inplace=True) 65 | craters.reset_index(inplace=True, drop=True) 66 | 67 | return craters 68 | 69 | 70 | def ReadLROCHeadCombinedCraterCSV(filelroc="catalogues/LROCCraters.csv", 71 | filehead="catalogues/HeadCraters.csv", 72 | sortlat=True): 73 | """Combines LROC 5 - 20 km crater dataset with Head >= 20 km dataset. 74 | 75 | Parameters 76 | ---------- 77 | filelroc : str, optional 78 | LROC crater file location. Defaults to the one in the current folder. 79 | filehead : str, optional 80 | Head et al. crater file location. Defaults to the one in the current 81 | folder. 82 | sortlat : bool, optional 83 | If `True` (default), order catalogue by latitude. 84 | 85 | Returns 86 | ------- 87 | craters : pandas.DataFrame 88 | Craters data frame. 89 | """ 90 | ctrs_head = ReadHeadCraterCSV(filename=filehead, sortlat=False) 91 | # Just in case. 92 | assert ctrs_head.shape == ctrs_head[ctrs_head["Diameter (km)"] > 20].shape 93 | ctrs_lroc = ReadLROCCraterCSV(filename=filelroc, sortlat=False) 94 | ctrs_lroc.drop(["tag"], axis=1, inplace=True) 95 | craters = pd.concat([ctrs_lroc, ctrs_head], axis=0, ignore_index=True, 96 | copy=True) 97 | if sortlat: 98 | craters.sort_values(by='Lat', inplace=True) 99 | craters.reset_index(inplace=True, drop=True) 100 | 101 | return craters 102 | 103 | ########## Warp Images and CSVs ########## 104 | 105 | def regrid_shape_aspect(regrid_shape, target_extent): 106 | """Helper function copied from cartopy.img_transform for resizing an image 107 | without changing its aspect ratio. 108 | 109 | Parameters 110 | ---------- 111 | regrid_shape : int or float 112 | Target length of the shorter axis (in units of pixels). 113 | target_extent : some 114 | Width and height of the target image (generally not in units of 115 | pixels). 116 | 117 | Returns 118 | ------- 119 | regrid_shape : tuple 120 | Width and height of the target image in pixels. 121 | """ 122 | if not isinstance(regrid_shape, collections.Sequence): 123 | target_size = int(regrid_shape) 124 | x_range, y_range = np.diff(target_extent)[::2] 125 | desired_aspect = x_range / y_range 126 | if x_range >= y_range: 127 | regrid_shape = (target_size * desired_aspect, target_size) 128 | else: 129 | regrid_shape = (target_size, target_size / desired_aspect) 130 | return regrid_shape 131 | 132 | 133 | def WarpImage(img, iproj, iextent, oproj, oextent, 134 | origin="upper", rgcoeff=1.2): 135 | """Warps images with cartopy.img_transform.warp_array, then plots them with 136 | imshow. Based on cartopy.mpl.geoaxes.imshow. 137 | 138 | Parameters 139 | ---------- 140 | img : numpy.ndarray 141 | Image as a 2D array. 142 | iproj : cartopy.crs.Projection instance 143 | Input coordinate system. 144 | iextent : list-like 145 | Coordinate limits (x_min, x_max, y_min, y_max) of input. 146 | oproj : cartopy.crs.Projection instance 147 | Output coordinate system. 148 | oextent : list-like 149 | Coordinate limits (x_min, x_max, y_min, y_max) of output. 150 | origin : "lower" or "upper", optional 151 | Based on imshow convention for displaying image y-axis. "upper" means 152 | [0,0] is in the upper-left corner of the image; "lower" means it's in 153 | the bottom-left. 154 | rgcoeff : float, optional 155 | Fractional size increase of transformed image height. Generically set 156 | to 1.2 to prevent loss of fidelity during transform (though some of it 157 | is inevitably lost due to warping). 158 | """ 159 | 160 | if iproj == oproj: 161 | raise Warning("Input and output transforms are identical!" 162 | "Returing input!") 163 | return img 164 | 165 | if origin == 'upper': 166 | # Regridding operation implicitly assumes origin of image is 167 | # 'lower', so adjust for that here. 168 | img = img[::-1] 169 | 170 | # rgcoeff is padding when we rescale the image later. 171 | regrid_shape = rgcoeff * min(img.shape) 172 | regrid_shape = regrid_shape_aspect(regrid_shape, 173 | oextent) 174 | 175 | # cimg.warp_array uses cimg.mesh_projection, which cannot handle any 176 | # zeros being used in iextent. Create iextent_nozeros to fix. 177 | iextent_nozeros = np.array(iextent) 178 | iextent_nozeros[iextent_nozeros == 0] = 1e-8 179 | iextent_nozeros = list(iextent_nozeros) 180 | 181 | imgout, extent = cimg.warp_array(img, 182 | source_proj=iproj, 183 | source_extent=iextent_nozeros, 184 | target_proj=oproj, 185 | target_res=regrid_shape, 186 | target_extent=oextent, 187 | mask_extrapolated=True) 188 | 189 | if origin == 'upper': 190 | # Transform back. 191 | imgout = imgout[::-1] 192 | 193 | return imgout 194 | 195 | 196 | def WarpImagePad(img, iproj, iextent, oproj, oextent, origin="upper", 197 | rgcoeff=1.2, fillbg="black"): 198 | """Wrapper for WarpImage that adds padding to warped image to make it the 199 | same size as the original. 200 | 201 | Parameters 202 | ---------- 203 | img : numpy.ndarray 204 | Image as a 2D array. 205 | iproj : cartopy.crs.Projection instance 206 | Input coordinate system. 207 | iextent : list-like 208 | Coordinate limits (x_min, x_max, y_min, y_max) of input. 209 | oproj : cartopy.crs.Projection instance 210 | Output coordinate system. 211 | oextent : list-like 212 | Coordinate limits (x_min, x_max, y_min, y_max) of output. 213 | origin : "lower" or "upper", optional 214 | Based on imshow convention for displaying image y-axis. "upper" means 215 | [0,0] is in the upper-left corner of the image; "lower" means it's in 216 | the bottom-left. 217 | rgcoeff : float, optional 218 | Fractional size increase of transformed image height. Generically set 219 | to 1.2 to prevent loss of fidelity during transform (though some of it 220 | is inevitably lost due to warping). 221 | fillbg : 'black' or 'white', optional. 222 | Fills padding with either black (0) or white (255) values. Default is 223 | black. 224 | 225 | Returns 226 | ------- 227 | imgo : PIL.Image.Image 228 | Warped image with padding 229 | imgw.size : tuple 230 | Width, height of picture without padding 231 | offset : tuple 232 | Pixel width of (left, top)-side padding 233 | """ 234 | # Based off of 236 | 237 | if type(img) == Image.Image: 238 | img = np.asanyarray(img) 239 | 240 | # Check that we haven't been given a corrupted image. 241 | assert img.sum() > 0, "Image input to WarpImagePad is blank!" 242 | 243 | # Set background colour 244 | if fillbg == "white": 245 | bgval = 255 246 | else: 247 | bgval = 0 248 | 249 | # Warp image. 250 | imgw = WarpImage(img, iproj, iextent, oproj, oextent, 251 | origin=origin, rgcoeff=rgcoeff) 252 | 253 | # Remove mask, turn image into Image.Image. 254 | imgw = np.ma.filled(imgw, fill_value=bgval) 255 | imgw = Image.fromarray(imgw, mode="L") 256 | 257 | # Resize to height of original, maintaining aspect ratio. Note 258 | # img.shape = height, width, and imgw.size and imgo.size = width, height. 259 | imgw_loh = imgw.size[0] / imgw.size[1] 260 | 261 | # If imgw is stretched horizontally compared to img. 262 | if imgw_loh > (img.shape[1] / img.shape[0]): 263 | imgw = imgw.resize([img.shape[0], 264 | int(np.round(img.shape[0] / imgw_loh))], 265 | resample=Image.NEAREST) 266 | # If imgw is stretched vertically. 267 | else: 268 | imgw = imgw.resize([int(np.round(imgw_loh * img.shape[0])), 269 | img.shape[0]], resample=Image.NEAREST) 270 | 271 | # Make background image and paste two together. 272 | imgo = Image.new('L', (img.shape[1], img.shape[0]), (bgval)) 273 | offset = ((imgo.size[0] - imgw.size[0]) // 2, 274 | (imgo.size[1] - imgw.size[1]) // 2) 275 | imgo.paste(imgw, offset) 276 | 277 | return imgo, imgw.size, offset 278 | 279 | 280 | def WarpCraterLoc(craters, geoproj, oproj, oextent, imgdim, llbd=None, 281 | origin="upper"): 282 | """Wrapper for WarpImage that adds padding to warped image to make it the 283 | same size as the original. 284 | 285 | Parameters 286 | ---------- 287 | craters : pandas.DataFrame 288 | Crater info 289 | geoproj : cartopy.crs.Geodetic instance 290 | Input lat/long coordinate system 291 | oproj : cartopy.crs.Projection instance 292 | Output coordinate system 293 | oextent : list-like 294 | Coordinate limits (x_min, x_max, y_min, y_max) 295 | of output 296 | imgdim : list, tuple or ndarray 297 | Length and height of image, in pixels 298 | llbd : list-like 299 | Long/lat limits (long_min, long_max, 300 | lat_min, lat_max) of image 301 | origin : "lower" or "upper" 302 | Based on imshow convention for displaying image y-axis. 303 | "upper" means that [0,0] is upper-left corner of image; 304 | "lower" means it is bottom-left. 305 | 306 | Returns 307 | ------- 308 | ctr_wrp : pandas.DataFrame 309 | DataFrame that includes pixel x, y positions 310 | """ 311 | 312 | # Get subset of craters within llbd limits 313 | if llbd is None: 314 | ctr_wrp = craters 315 | else: 316 | ctr_xlim = ((craters["Long"] >= llbd[0]) & 317 | (craters["Long"] <= llbd[1])) 318 | ctr_ylim = ((craters["Lat"] >= llbd[2]) & 319 | (craters["Lat"] <= llbd[3])) 320 | ctr_wrp = craters.loc[ctr_xlim & ctr_ylim, :].copy() 321 | 322 | # Get output projection coords. 323 | # [:,:2] becaus we don't need elevation data 324 | # If statement is in case ctr_wrp has nothing in it 325 | if ctr_wrp.shape[0]: 326 | ilong = ctr_wrp["Long"].as_matrix() 327 | ilat = ctr_wrp["Lat"].as_matrix() 328 | res = oproj.transform_points(x=ilong, y=ilat, 329 | src_crs=geoproj)[:, :2] 330 | 331 | # Get output 332 | ctr_wrp["x"], ctr_wrp["y"] = trf.coord2pix(res[:, 0], res[:, 1], 333 | oextent, imgdim, 334 | origin=origin) 335 | else: 336 | ctr_wrp["x"] = [] 337 | ctr_wrp["y"] = [] 338 | 339 | return ctr_wrp 340 | 341 | ############# Warp Plate Carree to Orthographic ############### 342 | 343 | def PlateCarree_to_Orthographic(img, llbd, craters, iglobe=None, 344 | ctr_sub=False, arad=1737.4, origin="upper", 345 | rgcoeff=1.2, slivercut=0.): 346 | """Transform Plate Carree image and associated csv file into Orthographic. 347 | 348 | Parameters 349 | ---------- 350 | img : PIL.Image.image or str 351 | File or filename. 352 | llbd : list-like 353 | Long/lat limits (long_min, long_max, lat_min, lat_max) of image. 354 | craters : pandas.DataFrame 355 | Craters catalogue. 356 | iglobe : cartopy.crs.Geodetic instance 357 | Globe for images. If False, defaults to spherical Moon. 358 | ctr_sub : bool, optional 359 | If `True`, assumes craters dataframe includes only craters within 360 | image. If `False` (default_, llbd used to cut craters from outside 361 | image out of (copy of) dataframe. 362 | arad : float 363 | World radius in km. Default is Moon (1737.4 km). 364 | origin : "lower" or "upper", optional 365 | Based on imshow convention for displaying image y-axis. "upper" 366 | (default) means that [0,0] is upper-left corner of image; "lower" means 367 | it is bottom-left. 368 | rgcoeff : float, optional 369 | Fractional size increase of transformed image height. By default set 370 | to 1.2 to prevent loss of fidelity during transform (though warping can 371 | be so extreme that this might be meaningless). 372 | slivercut : float from 0 to 1, optional 373 | If transformed image aspect ratio is too narrow (and would lead to a 374 | lot of padding, return null images). 375 | 376 | Returns 377 | ------- 378 | imgo : PIL.Image.image 379 | Transformed, padded image in PIL.Image format. 380 | ctr_xy : pandas.DataFrame 381 | Craters with transformed x, y pixel positions and pixel radii. 382 | distortion_coefficient : float 383 | Ratio between the central heights of the transformed image and original 384 | image. 385 | centrallonglat_xy : pandas.DataFrame 386 | xy position of the central longitude and latitude. 387 | """ 388 | 389 | # If user doesn't provide Moon globe properties. 390 | if not iglobe: 391 | iglobe = ccrs.Globe(semimajor_axis=arad*1000., 392 | semiminor_axis=arad*1000., ellipse=None) 393 | 394 | # Set up Geodetic (long/lat), Plate Carree (usually long/lat, but not when 395 | # globe != WGS84) and Orthographic projections. 396 | geoproj = ccrs.Geodetic(globe=iglobe) 397 | iproj = ccrs.PlateCarree(globe=iglobe) 398 | oproj = ccrs.Orthographic(central_longitude=np.mean(llbd[:2]), 399 | central_latitude=np.mean(llbd[2:]), 400 | globe=iglobe) 401 | 402 | # Create and transform coordinates of image corners and edge midpoints. 403 | # Due to Plate Carree and Orthographic's symmetries, max/min x/y values of 404 | # these 9 points represent extrema of the transformed image. 405 | xll = np.array([llbd[0], np.mean(llbd[:2]), llbd[1]]) 406 | yll = np.array([llbd[2], np.mean(llbd[2:]), llbd[3]]) 407 | xll, yll = np.meshgrid(xll, yll) 408 | xll = xll.ravel() 409 | yll = yll.ravel() 410 | 411 | # [:,:2] because we don't need elevation data. 412 | res = iproj.transform_points(x=xll, y=yll, src_crs=geoproj)[:, :2] 413 | iextent = [min(res[:, 0]), max(res[:, 0]), min(res[:, 1]), max(res[:, 1])] 414 | 415 | res = oproj.transform_points(x=xll, y=yll, src_crs=geoproj)[:, :2] 416 | oextent = [min(res[:, 0]), max(res[:, 0]), min(res[:, 1]), max(res[:, 1])] 417 | 418 | # Sanity check for narrow images; done before the most expensive part of 419 | # the function. 420 | oaspect = (oextent[1] - oextent[0]) / (oextent[3] - oextent[2]) 421 | if oaspect < slivercut: 422 | return [None, None] 423 | 424 | if type(img) != Image.Image: 425 | img = Image.open(img).convert("L") 426 | 427 | # Warp image. 428 | imgo, imgwshp, offset = WarpImagePad(img, iproj, iextent, oproj, oextent, 429 | origin=origin, rgcoeff=rgcoeff, 430 | fillbg="black") 431 | 432 | # Convert crater x, y position. 433 | if ctr_sub: 434 | llbd_in = None 435 | else: 436 | llbd_in = llbd 437 | ctr_xy = WarpCraterLoc(craters, geoproj, oproj, oextent, imgwshp, 438 | llbd=llbd_in, origin=origin) 439 | # Shift crater x, y positions by offset (origin doesn't matter for y-shift, 440 | # since padding is symmetric). 441 | ctr_xy.loc[:, "x"] += offset[0] 442 | ctr_xy.loc[:, "y"] += offset[1] 443 | 444 | # Pixel scale for orthographic determined (for images small enough that 445 | # tan(x) approximately equals x + 1/3x^3 + ...) by l = R_moon*theta, 446 | # where theta is the latitude extent of the centre of the image. Because 447 | # projection transform doesn't guarantee central vertical axis will keep 448 | # its pixel resolution, we need to calculate the conversion coefficient 449 | # C = (res[7,1]- res[1,1])/(oextent[3] - oextent[2]). 450 | # C0*pix height/C = theta 451 | # Where theta is the latitude extent and C0 is the theta per pixel 452 | # conversion for the Plate Carree image). Thus 453 | # l_ctr = R_moon*C0*pix_ctr/C. 454 | distortion_coefficient = ((res[7, 1] - res[1, 1]) / 455 | (oextent[3] - oextent[2])) 456 | if distortion_coefficient < 0.7: 457 | raise ValueError("Distortion Coefficient cannot be" 458 | " {0:.2f}!".format(distortion_coefficient)) 459 | pixperkm = trf.km2pix(imgo.size[1], llbd[3] - llbd[2], 460 | dc=distortion_coefficient, a=arad) 461 | ctr_xy["Diameter (pix)"] = ctr_xy["Diameter (km)"] * pixperkm 462 | 463 | # Determine x, y position of central lat/long. 464 | centrallonglat = pd.DataFrame({"Long": [xll[4]], "Lat": [yll[4]]}) 465 | centrallonglat_xy = WarpCraterLoc(centrallonglat, geoproj, oproj, oextent, 466 | imgwshp, llbd=llbd_in, origin=origin) 467 | 468 | # Shift central long/lat 469 | centrallonglat_xy.loc[:, "x"] += offset[0] 470 | centrallonglat_xy.loc[:, "y"] += offset[1] 471 | 472 | return [imgo, ctr_xy, distortion_coefficient, centrallonglat_xy] 473 | 474 | ############# Create target dataset (and helper functions) ############# 475 | 476 | def circlemaker(r=10.): 477 | """ 478 | Creates circle mask of radius r. 479 | """ 480 | # Based on 482 | 483 | # Mask grid extent (+1 to ensure we capture radius). 484 | rhext = int(r) + 1 485 | 486 | xx, yy = np.mgrid[-rhext:rhext + 1, -rhext:rhext + 1] 487 | circle = (xx**2 + yy**2) <= r**2 488 | 489 | return circle.astype(float) 490 | 491 | 492 | def ringmaker(r=10., dr=1): 493 | """ 494 | Creates ring of radius r and thickness dr. 495 | 496 | Parameters 497 | ---------- 498 | r : float 499 | Ring radius 500 | dr : int 501 | Ring thickness (cv2.circle requires int) 502 | """ 503 | # See , supplemented by 505 | # 506 | # and . 509 | 510 | # mask grid extent (dr/2 +1 to ensure we capture ring width 511 | # and radius); same philosophy as above 512 | rhext = int(np.ceil(r + dr / 2.)) + 1 513 | 514 | # cv2.circle requires integer radius 515 | mask = np.zeros([2 * rhext + 1, 2 * rhext + 1], np.uint8) 516 | 517 | # Generate ring 518 | ring = cv2.circle(mask, (rhext, rhext), int(np.round(r)), 1, thickness=dr) 519 | 520 | return ring.astype(float) 521 | 522 | 523 | def get_merge_indices(cen, imglen, ks_h, ker_shp): 524 | """Helper function that returns indices for merging stencil with base 525 | image, including edge case handling. x and y are identical, so code is 526 | axis-neutral. 527 | 528 | Assumes INTEGER values for all inputs! 529 | """ 530 | 531 | left = cen - ks_h 532 | right = cen + ks_h + 1 533 | 534 | # Handle edge cases. If left side of stencil is beyond the left end of 535 | # the image, for example, crop stencil and shift image index to lefthand 536 | # side. 537 | if left < 0: 538 | img_l = 0 539 | g_l = -left 540 | else: 541 | img_l = left 542 | g_l = 0 543 | if right > imglen: 544 | img_r = imglen 545 | g_r = ker_shp - (right - imglen) 546 | else: 547 | img_r = right 548 | g_r = ker_shp 549 | 550 | return [img_l, img_r, g_l, g_r] 551 | 552 | 553 | def make_mask(craters, img, binary=True, rings=False, ringwidth=1, 554 | truncate=True): 555 | """Makes crater mask binary image (does not yet consider crater overlap). 556 | 557 | Parameters 558 | ---------- 559 | craters : pandas.DataFrame 560 | Craters catalogue that includes pixel x and y columns. 561 | img : numpy.ndarray 562 | Original image; assumes colour channel is last axis (tf standard). 563 | binary : bool, optional 564 | If True, returns a binary image of crater masks. 565 | rings : bool, optional 566 | If True, mask uses hollow rings rather than filled circles. 567 | ringwidth : int, optional 568 | If rings is True, ringwidth sets the width (dr) of the ring. 569 | truncate : bool 570 | If True, truncate mask where image truncates. 571 | 572 | Returns 573 | ------- 574 | mask : numpy.ndarray 575 | Target mask image. 576 | """ 577 | 578 | # Load blank density map 579 | imgshape = img.shape[:2] 580 | mask = np.zeros(imgshape) 581 | cx = craters["x"].values.astype('int') 582 | cy = craters["y"].values.astype('int') 583 | radius = craters["Diameter (pix)"].values / 2. 584 | 585 | for i in range(craters.shape[0]): 586 | if rings: 587 | kernel = ringmaker(r=radius[i], dr=ringwidth) 588 | else: 589 | kernel = circlemaker(r=radius[i]) 590 | # "Dummy values" so we can use get_merge_indices 591 | kernel_support = kernel.shape[0] 592 | ks_half = kernel_support // 2 593 | 594 | # Calculate indices on image where kernel should be added 595 | [imxl, imxr, gxl, gxr] = get_merge_indices(cx[i], imgshape[1], 596 | ks_half, kernel_support) 597 | [imyl, imyr, gyl, gyr] = get_merge_indices(cy[i], imgshape[0], 598 | ks_half, kernel_support) 599 | 600 | # Add kernel to image 601 | mask[imyl:imyr, imxl:imxr] += kernel[gyl:gyr, gxl:gxr] 602 | 603 | if binary: 604 | mask = (mask > 0).astype(float) 605 | 606 | if truncate: 607 | if img.ndim == 3: 608 | mask[img[:, :, 0] == 0] = 0 609 | else: 610 | mask[img == 0] = 0 611 | 612 | return mask 613 | 614 | ############# Create dataset (and helper functions) ############# 615 | 616 | def AddPlateCarree_XY(craters, imgdim, cdim=[-180., 180., -90., 90.], 617 | origin="upper"): 618 | """Adds x and y pixel locations to craters dataframe. 619 | 620 | Parameters 621 | ---------- 622 | craters : pandas.DataFrame 623 | Crater info 624 | imgdim : list, tuple or ndarray 625 | Length and height of image, in pixels 626 | cdim : list-like, optional 627 | Coordinate limits (x_min, x_max, y_min, y_max) of image. Default is 628 | [-180., 180., -90., 90.]. 629 | origin : "upper" or "lower", optional 630 | Based on imshow convention for displaying image y-axis. 631 | "upper" means that [0,0] is upper-left corner of image; 632 | "lower" means it is bottom-left. 633 | """ 634 | x, y = trf.coord2pix(craters["Long"].as_matrix(), 635 | craters["Lat"].as_matrix(), 636 | cdim, imgdim, origin=origin) 637 | craters["x"] = x 638 | craters["y"] = y 639 | 640 | 641 | def ResampleCraters(craters, llbd, imgheight, arad=1737.4, minpix=0): 642 | """Crops crater file, and removes craters smaller than some minimum value. 643 | 644 | Parameters 645 | ---------- 646 | craters : pandas.DataFrame 647 | Crater dataframe. 648 | llbd : list-like 649 | Long/lat limits (long_min, long_max, lat_min, lat_max) of image. 650 | imgheight : int 651 | Pixel height of image. 652 | arad : float, optional 653 | World radius in km. Defaults to Moon radius (1737.4 km). 654 | minpix : int, optional 655 | Minimium crater pixel size to be included in output. Default is 0 656 | (equvalent to no cutoff). 657 | 658 | Returns 659 | ------- 660 | ctr_sub : pandas.DataFrame 661 | Cropped and filtered dataframe. 662 | """ 663 | 664 | # Get subset of craters within llbd limits. 665 | ctr_xlim = (craters["Long"] >= llbd[0]) & (craters["Long"] <= llbd[1]) 666 | ctr_ylim = (craters["Lat"] >= llbd[2]) & (craters["Lat"] <= llbd[3]) 667 | ctr_sub = craters.loc[ctr_xlim & ctr_ylim, :].copy() 668 | 669 | if minpix > 0: 670 | # Obtain pixel per km conversion factor. Use latitude because Plate 671 | # Carree doesn't distort along this axis. 672 | pixperkm = trf.km2pix(imgheight, llbd[3] - llbd[2], dc=1., a=arad) 673 | minkm = minpix / pixperkm 674 | 675 | # Remove craters smaller than pixel limit. 676 | ctr_sub = ctr_sub[ctr_sub["Diameter (km)"] >= minkm] 677 | 678 | ctr_sub.reset_index(inplace=True, drop=True) 679 | 680 | return ctr_sub 681 | 682 | 683 | def InitialImageCut(img, cdim, newcdim): 684 | """Crops image, so that the crop output can be used in GenDataset. 685 | 686 | Parameters 687 | ---------- 688 | img : PIL.Image.Image 689 | Image 690 | cdim : list-like 691 | Coordinate limits (x_min, x_max, y_min, y_max) of image. 692 | newcdim : list-like 693 | Crop boundaries (x_min, x_max, y_min, y_max). There is 694 | currently NO CHECK that newcdim is within cdim! 695 | 696 | Returns 697 | ------- 698 | img : PIL.Image.Image 699 | Cropped image 700 | """ 701 | x, y = trf.coord2pix(np.array(newcdim[:2]), np.array(newcdim[2:]), cdim, 702 | img.size, origin="upper") 703 | 704 | # y is backward since origin is upper! 705 | box = [x[0], y[1], x[1], y[0]] 706 | img = img.crop(box) 707 | img.load() 708 | 709 | return img 710 | 711 | 712 | def GenDataset(img, craters, outhead, rawlen_range=[1000, 2000], 713 | rawlen_dist='log', ilen=256, cdim=[-180., 180., -60., 60.], 714 | arad=1737.4, minpix=0, tglen=256, binary=True, rings=True, 715 | ringwidth=1, truncate=True, amt=100, istart=0, seed=None, 716 | verbose=False): 717 | """Generates random dataset from a global DEM and crater catalogue. 718 | 719 | The function randomly samples small images from a global digital elevation 720 | map (DEM) that uses a Plate Carree projection, and converts the small 721 | images to Orthographic projection. Pixel coordinates and radii of craters 722 | from the catalogue that fall within each image are placed in a 723 | corresponding Pandas dataframe. Images and dataframes are saved to disk in 724 | hdf5 format. 725 | 726 | Parameters 727 | ---------- 728 | img : PIL.Image.Image 729 | Source image. 730 | craters : pandas.DataFrame 731 | Crater catalogue .csv. 732 | outhead : str 733 | Filepath and file prefix of the image and crater table hdf5 files. 734 | rawlen_range : list-like, optional 735 | Lower and upper bounds of raw image widths, in pixels, to crop from 736 | source. To always crop the same sized image, set lower bound to the 737 | same value as the upper. Default is [300, 4000]. 738 | rawlen_dist : 'uniform' or 'log' 739 | Distribution from which to randomly sample image widths. 'uniform' is 740 | uniform sampling, and 'log' is loguniform sampling. 741 | ilen : int, optional 742 | Input image width, in pixels. Cropped images will be downsampled to 743 | this size. Default is 256. 744 | cdim : list-like, optional 745 | Coordinate limits (x_min, x_max, y_min, y_max) of image. Default is 746 | LRO-Kaguya's [-180., 180., -60., 60.]. 747 | arad : float. optional 748 | World radius in km. Defaults to Moon radius (1737.4 km). 749 | minpix : int, optional 750 | Minimum crater diameter in pixels to be included in crater list. 751 | Useful when the smallest craters in the catalogue are smaller than 1 752 | pixel in diameter. 753 | tglen : int, optional 754 | Target image width, in pixels. 755 | binary : bool, optional 756 | If True, returns a binary image of crater masks. 757 | rings : bool, optional 758 | If True, mask uses hollow rings rather than filled circles. 759 | ringwidth : int, optional 760 | If rings is True, ringwidth sets the width (dr) of the ring. 761 | truncate : bool 762 | If True, truncate mask where image truncates. 763 | amt : int, optional 764 | Number of images to produce. 100 by default. 765 | istart : int 766 | Output file starting number, when creating datasets spanning multiple 767 | files. 768 | seed : int or None 769 | np.random.seed input (for testing purposes). 770 | verbose : bool 771 | If True, prints out number of image being generated. 772 | """ 773 | 774 | # just in case we ever make this user-selectable... 775 | origin = "upper" 776 | 777 | # Seed random number generator. 778 | np.random.seed(seed) 779 | 780 | # Get craters. 781 | AddPlateCarree_XY(craters, list(img.size), cdim=cdim, origin=origin) 782 | 783 | iglobe = ccrs.Globe(semimajor_axis=arad*1000., semiminor_axis=arad*1000., 784 | ellipse=None) 785 | 786 | # Create random sampler (either uniform or loguniform). 787 | if rawlen_dist == 'log': 788 | rawlen_min = np.log10(rawlen_range[0]) 789 | rawlen_max = np.log10(rawlen_range[1]) 790 | 791 | def random_sampler(): 792 | return int(10**np.random.uniform(rawlen_min, rawlen_max)) 793 | else: 794 | 795 | def random_sampler(): 796 | return np.random.randint(rawlen_range[0], rawlen_range[1] + 1) 797 | 798 | # Initialize output hdf5s. 799 | imgs_h5 = h5py.File(outhead + '_images.hdf5', 'w') 800 | imgs_h5_inputs = imgs_h5.create_dataset("input_images", (amt, ilen, ilen), 801 | dtype='uint8') 802 | imgs_h5_inputs.attrs['definition'] = "Input image dataset." 803 | imgs_h5_tgts = imgs_h5.create_dataset("target_masks", (amt, tglen, tglen), 804 | dtype='float32') 805 | imgs_h5_tgts.attrs['definition'] = "Target mask dataset." 806 | imgs_h5_llbd = imgs_h5.create_group("longlat_bounds") 807 | imgs_h5_llbd.attrs['definition'] = ("(long min, long max, lat min, " 808 | "lat max) of the cropped image.") 809 | imgs_h5_box = imgs_h5.create_group("pix_bounds") 810 | imgs_h5_box.attrs['definition'] = ("Pixel bounds of the Global DEM region" 811 | " that was cropped for the image.") 812 | imgs_h5_dc = imgs_h5.create_group("pix_distortion_coefficient") 813 | imgs_h5_dc.attrs['definition'] = ("Distortion coefficient due to " 814 | "projection transformation.") 815 | imgs_h5_cll = imgs_h5.create_group("cll_xy") 816 | imgs_h5_cll.attrs['definition'] = ("(x, y) pixel coordinates of the " 817 | "central long / lat.") 818 | craters_h5 = pd.HDFStore(outhead + '_craters.hdf5', 'w') 819 | 820 | # Zero-padding for hdf5 keys. 821 | zeropad = int(np.log10(amt)) + 1 822 | 823 | for i in range(amt): 824 | 825 | # Current image number. 826 | img_number = "img_{i:0{zp}d}".format(i=istart + i, zp=zeropad) 827 | if verbose: 828 | print("Generating {0}".format(img_number)) 829 | 830 | # Determine image size to crop. 831 | rawlen = random_sampler() 832 | xc = np.random.randint(0, img.size[0] - rawlen) 833 | yc = np.random.randint(0, img.size[1] - rawlen) 834 | box = np.array([xc, yc, xc + rawlen, yc + rawlen], dtype='int32') 835 | 836 | # Load necessary because crop may be a lazy operation; im.load() should 837 | # copy it. See . 839 | im = img.crop(box) 840 | im.load() 841 | 842 | # Obtain long/lat bounds for coordinate transform. 843 | ix = box[::2] 844 | iy = box[1::2] 845 | llong, llat = trf.pix2coord(ix, iy, cdim, list(img.size), 846 | origin=origin) 847 | llbd = np.r_[llong, llat[::-1]] 848 | 849 | # Downsample image. 850 | im = im.resize([ilen, ilen], resample=Image.NEAREST) 851 | 852 | # Remove all craters that are too small to be seen in image. 853 | ctr_sub = ResampleCraters(craters, llbd, im.size[1], arad=arad, 854 | minpix=minpix) 855 | 856 | # Convert Plate Carree to Orthographic. 857 | [imgo, ctr_xy, distortion_coefficient, clonglat_xy] = ( 858 | PlateCarree_to_Orthographic( 859 | im, llbd, ctr_sub, iglobe=iglobe, ctr_sub=True, 860 | arad=arad, origin=origin, rgcoeff=1.2, slivercut=0.5)) 861 | 862 | if imgo is None: 863 | print("Discarding narrow image") 864 | continue 865 | 866 | imgo_arr = np.asanyarray(imgo) 867 | assert imgo_arr.sum() > 0, ("Sum of imgo is zero! There likely was " 868 | "an error in projecting the cropped " 869 | "image.") 870 | 871 | # Make target mask. Used Image.BILINEAR resampling because 872 | # Image.NEAREST creates artifacts. Try Image.LANZCOS if BILINEAR still 873 | # leaves artifacts). 874 | tgt = np.asanyarray(imgo.resize((tglen, tglen), 875 | resample=Image.BILINEAR)) 876 | mask = make_mask(ctr_xy, tgt, binary=binary, rings=rings, 877 | ringwidth=ringwidth, truncate=truncate) 878 | 879 | # Output everything to file. 880 | imgs_h5_inputs[i, ...] = imgo_arr 881 | imgs_h5_tgts[i, ...] = mask 882 | 883 | sds_box = imgs_h5_box.create_dataset(img_number, (4,), dtype='int32') 884 | sds_box[...] = box 885 | sds_llbd = imgs_h5_llbd.create_dataset(img_number, (4,), dtype='float') 886 | sds_llbd[...] = llbd 887 | sds_dc = imgs_h5_dc.create_dataset(img_number, (1,), dtype='float') 888 | sds_dc[...] = np.array([distortion_coefficient]) 889 | sds_cll = imgs_h5_cll.create_dataset(img_number, (2,), dtype='float') 890 | sds_cll[...] = clonglat_xy.loc[:, ['x', 'y']].as_matrix().ravel() 891 | 892 | craters_h5[img_number] = ctr_xy 893 | 894 | imgs_h5.flush() 895 | craters_h5.flush() 896 | 897 | imgs_h5.close() 898 | craters_h5.close() 899 | -------------------------------------------------------------------------------- /model_train.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Convolutional Neural Network Training Functions 3 | 4 | Functions for building and training a (UNET) Convolutional Neural Network on 5 | images of the Moon and binary ring targets. 6 | """ 7 | from __future__ import absolute_import, division, print_function 8 | 9 | import numpy as np 10 | import pandas as pd 11 | import h5py 12 | 13 | from keras.models import Model 14 | from keras.layers.core import Dropout, Reshape 15 | from keras.regularizers import l2 16 | 17 | from keras.optimizers import Adam 18 | from keras.callbacks import EarlyStopping 19 | from keras import backend as K 20 | K.set_image_dim_ordering('tf') 21 | 22 | import utils.template_match_target as tmt 23 | import utils.processing as proc 24 | 25 | # Check Keras version - code will switch API if needed. 26 | from keras import __version__ as keras_version 27 | k2 = True if keras_version[0] == '2' else False 28 | 29 | # If Keras is v2.x.x, create Keras 1-syntax wrappers. 30 | if not k2: 31 | from keras.layers import merge, Input 32 | from keras.layers.convolutional import (Convolution2D, MaxPooling2D, 33 | UpSampling2D) 34 | 35 | else: 36 | from keras.layers import Concatenate, Input 37 | from keras.layers.convolutional import (Conv2D, MaxPooling2D, 38 | UpSampling2D) 39 | 40 | def merge(layers, mode=None, concat_axis=None): 41 | """Wrapper for Keras 2's Concatenate class (`mode` is discarded).""" 42 | return Concatenate(axis=concat_axis)(list(layers)) 43 | 44 | def Convolution2D(n_filters, FL, FLredundant, activation=None, 45 | init=None, W_regularizer=None, border_mode=None): 46 | """Wrapper for Keras 2's Conv2D class.""" 47 | return Conv2D(n_filters, FL, activation=activation, 48 | kernel_initializer=init, 49 | kernel_regularizer=W_regularizer, 50 | padding=border_mode) 51 | 52 | 53 | ######################## 54 | def get_param_i(param, i): 55 | """Gets correct parameter for iteration i. 56 | 57 | Parameters 58 | ---------- 59 | param : list 60 | List of model hyperparameters to be iterated over. 61 | i : integer 62 | Hyperparameter iteration. 63 | 64 | Returns 65 | ------- 66 | Correct hyperparameter for iteration i. 67 | """ 68 | if len(param) > i: 69 | return param[i] 70 | else: 71 | return param[0] 72 | 73 | ######################## 74 | def custom_image_generator(data, target, batch_size=32): 75 | """Custom image generator that manipulates image/target pairs to prevent 76 | overfitting in the Convolutional Neural Network. 77 | 78 | Parameters 79 | ---------- 80 | data : array 81 | Input images. 82 | target : array 83 | Target images. 84 | batch_size : int, optional 85 | Batch size for image manipulation. 86 | 87 | Yields 88 | ------ 89 | Manipulated images and targets. 90 | 91 | """ 92 | L, W = data[0].shape[0], data[0].shape[1] 93 | while True: 94 | for i in range(0, len(data), batch_size): 95 | d, t = data[i:i + batch_size].copy(), target[i:i + batch_size].copy() 96 | 97 | # Random color inversion 98 | # for j in np.where(np.random.randint(0, 2, batch_size) == 1)[0]: 99 | # d[j][d[j] > 0.] = 1. - d[j][d[j] > 0.] 100 | 101 | # Horizontal/vertical flips 102 | for j in np.where(np.random.randint(0, 2, batch_size) == 1)[0]: 103 | d[j], t[j] = np.fliplr(d[j]), np.fliplr(t[j]) # left/right 104 | for j in np.where(np.random.randint(0, 2, batch_size) == 1)[0]: 105 | d[j], t[j] = np.flipud(d[j]), np.flipud(t[j]) # up/down 106 | 107 | # Random up/down & left/right pixel shifts, 90 degree rotations 108 | npix = 15 109 | h = np.random.randint(-npix, npix + 1, batch_size) # Horizontal shift 110 | v = np.random.randint(-npix, npix + 1, batch_size) # Vertical shift 111 | r = np.random.randint(0, 4, batch_size) # 90 degree rotations 112 | for j in range(batch_size): 113 | d[j] = np.pad(d[j], ((npix, npix), (npix, npix), (0, 0)), 114 | mode='constant')[npix + h[j]:L + h[j] + npix, 115 | npix + v[j]:W + v[j] + npix, :] 116 | t[j] = np.pad(t[j], (npix,), mode='constant')[npix + h[j]:L + h[j] + npix, 117 | npix + v[j]:W + v[j] + npix] 118 | d[j], t[j] = np.rot90(d[j], r[j]), np.rot90(t[j], r[j]) 119 | yield (d, t) 120 | 121 | ######################## 122 | def get_metrics(data, craters, dim, model, beta=1): 123 | """Function that prints pertinent metrics at the end of each epoch. 124 | 125 | Parameters 126 | ---------- 127 | data : hdf5 128 | Input images. 129 | craters : hdf5 130 | Pandas arrays of human-counted crater data. 131 | dim : int 132 | Dimension of input images (assumes square). 133 | model : keras model object 134 | Keras model 135 | beta : int, optional 136 | Beta value when calculating F-beta score. Defaults to 1. 137 | """ 138 | X, Y = data[0], data[1] 139 | 140 | # Get csvs of human-counted craters 141 | csvs = [] 142 | minrad, maxrad, cutrad, n_csvs = 3, 50, 0.8, len(X) 143 | diam = 'Diameter (pix)' 144 | for i in range(n_csvs): 145 | csv = craters[proc.get_id(i)] 146 | # remove small/large/half craters 147 | csv = csv[(csv[diam] < 2 * maxrad) & (csv[diam] > 2 * minrad)] 148 | csv = csv[(csv['x'] + cutrad * csv[diam] / 2 <= dim)] 149 | csv = csv[(csv['y'] + cutrad * csv[diam] / 2 <= dim)] 150 | csv = csv[(csv['x'] - cutrad * csv[diam] / 2 > 0)] 151 | csv = csv[(csv['y'] - cutrad * csv[diam] / 2 > 0)] 152 | if len(csv) < 3: # Exclude csvs with few craters 153 | csvs.append([-1]) 154 | else: 155 | csv_coords = np.asarray((csv['x'], csv['y'], csv[diam] / 2)).T 156 | csvs.append(csv_coords) 157 | 158 | # Calculate custom metrics 159 | print("") 160 | print("*********Custom Loss*********") 161 | recall, precision, fscore = [], [], [] 162 | frac_new, frac_new2, maxrad = [], [], [] 163 | err_lo, err_la, err_r = [], [], [] 164 | frac_duplicates = [] 165 | preds = model.predict(X) 166 | for i in range(n_csvs): 167 | if len(csvs[i]) < 3: 168 | continue 169 | (N_match, N_csv, N_detect, maxr, 170 | elo, ela, er, frac_dupes) = tmt.template_match_t2c(preds[i], csvs[i], 171 | rmv_oor_csvs=0) 172 | if N_match > 0: 173 | p = float(N_match) / float(N_match + (N_detect - N_match)) 174 | r = float(N_match) / float(N_csv) 175 | f = (1 + beta**2) * (r * p) / (p * beta**2 + r) 176 | diff = float(N_detect - N_match) 177 | fn = diff / (float(N_detect) + diff) 178 | fn2 = diff / (float(N_csv) + diff) 179 | recall.append(r) 180 | precision.append(p) 181 | fscore.append(f) 182 | frac_new.append(fn) 183 | frac_new2.append(fn2) 184 | maxrad.append(maxr) 185 | err_lo.append(elo) 186 | err_la.append(ela) 187 | err_r.append(er) 188 | frac_duplicates.append(frac_dupes) 189 | else: 190 | print("skipping iteration %d,N_csv=%d,N_detect=%d,N_match=%d" % 191 | (i, N_csv, N_detect, N_match)) 192 | 193 | print("binary XE score = %f" % model.evaluate(X, Y)) 194 | if len(recall) > 3: 195 | print("mean and std of N_match/N_csv (recall) = %f, %f" % 196 | (np.mean(recall), np.std(recall))) 197 | print("""mean and std of N_match/(N_match + (N_detect-N_match)) 198 | (precision) = %f, %f""" % (np.mean(precision), np.std(precision))) 199 | print("mean and std of F_%d score = %f, %f" % 200 | (beta, np.mean(fscore), np.std(fscore))) 201 | print("""mean and std of (N_detect - N_match)/N_detect (fraction 202 | of craters that are new) = %f, %f""" % 203 | (np.mean(frac_new), np.std(frac_new))) 204 | print("""mean and std of (N_detect - N_match)/N_csv (fraction of 205 | "craters that are new, 2) = %f, %f""" % 206 | (np.mean(frac_new2), np.std(frac_new2))) 207 | print("median and IQR fractional longitude diff = %f, 25:%f, 75:%f" % 208 | (np.median(err_lo), np.percentile(err_lo, 25), 209 | np.percentile(err_lo, 75))) 210 | print("median and IQR fractional latitude diff = %f, 25:%f, 75:%f" % 211 | (np.median(err_la), np.percentile(err_la, 25), 212 | np.percentile(err_la, 75))) 213 | print("median and IQR fractional radius diff = %f, 25:%f, 75:%f" % 214 | (np.median(err_r), np.percentile(err_r, 25), 215 | np.percentile(err_r, 75))) 216 | print("mean and std of frac_duplicates: %f, %f" % 217 | (np.mean(frac_duplicates), np.std(frac_duplicates))) 218 | print("""mean and std of maximum detected pixel radius in an image = 219 | %f, %f""" % (np.mean(maxrad), np.std(maxrad))) 220 | print("""absolute maximum detected pixel radius over all images = 221 | %f""" % np.max(maxrad)) 222 | print("") 223 | 224 | ######################## 225 | def build_model(dim, learn_rate, lmbda, drop, FL, init, n_filters): 226 | """Function that builds the (UNET) convolutional neural network. 227 | 228 | Parameters 229 | ---------- 230 | dim : int 231 | Dimension of input images (assumes square). 232 | learn_rate : float 233 | Learning rate. 234 | lmbda : float 235 | Convolution2D regularization parameter. 236 | drop : float 237 | Dropout fraction. 238 | FL : int 239 | Filter length. 240 | init : string 241 | Weight initialization type. 242 | n_filters : int 243 | Number of filters in each layer. 244 | 245 | Returns 246 | ------- 247 | model : keras model object 248 | Constructed Keras model. 249 | """ 250 | print('Making UNET model...') 251 | img_input = Input(batch_shape=(None, dim, dim, 1)) 252 | 253 | a1 = Convolution2D(n_filters, FL, FL, activation='relu', init=init, 254 | W_regularizer=l2(lmbda), border_mode='same')(img_input) 255 | a1 = Convolution2D(n_filters, FL, FL, activation='relu', init=init, 256 | W_regularizer=l2(lmbda), border_mode='same')(a1) 257 | a1P = MaxPooling2D((2, 2), strides=(2, 2))(a1) 258 | 259 | a2 = Convolution2D(n_filters * 2, FL, FL, activation='relu', init=init, 260 | W_regularizer=l2(lmbda), border_mode='same')(a1P) 261 | a2 = Convolution2D(n_filters * 2, FL, FL, activation='relu', init=init, 262 | W_regularizer=l2(lmbda), border_mode='same')(a2) 263 | a2P = MaxPooling2D((2, 2), strides=(2, 2))(a2) 264 | 265 | a3 = Convolution2D(n_filters * 4, FL, FL, activation='relu', init=init, 266 | W_regularizer=l2(lmbda), border_mode='same')(a2P) 267 | a3 = Convolution2D(n_filters * 4, FL, FL, activation='relu', init=init, 268 | W_regularizer=l2(lmbda), border_mode='same')(a3) 269 | a3P = MaxPooling2D((2, 2), strides=(2, 2),)(a3) 270 | 271 | u = Convolution2D(n_filters * 4, FL, FL, activation='relu', init=init, 272 | W_regularizer=l2(lmbda), border_mode='same')(a3P) 273 | u = Convolution2D(n_filters * 4, FL, FL, activation='relu', init=init, 274 | W_regularizer=l2(lmbda), border_mode='same')(u) 275 | 276 | u = UpSampling2D((2, 2))(u) 277 | u = merge((a3, u), mode='concat', concat_axis=3) 278 | u = Dropout(drop)(u) 279 | u = Convolution2D(n_filters * 2, FL, FL, activation='relu', init=init, 280 | W_regularizer=l2(lmbda), border_mode='same')(u) 281 | u = Convolution2D(n_filters * 2, FL, FL, activation='relu', init=init, 282 | W_regularizer=l2(lmbda), border_mode='same')(u) 283 | 284 | u = UpSampling2D((2, 2))(u) 285 | u = merge((a2, u), mode='concat', concat_axis=3) 286 | u = Dropout(drop)(u) 287 | u = Convolution2D(n_filters, FL, FL, activation='relu', init=init, 288 | W_regularizer=l2(lmbda), border_mode='same')(u) 289 | u = Convolution2D(n_filters, FL, FL, activation='relu', init=init, 290 | W_regularizer=l2(lmbda), border_mode='same')(u) 291 | 292 | u = UpSampling2D((2, 2))(u) 293 | u = merge((a1, u), mode='concat', concat_axis=3) 294 | u = Dropout(drop)(u) 295 | u = Convolution2D(n_filters, FL, FL, activation='relu', init=init, 296 | W_regularizer=l2(lmbda), border_mode='same')(u) 297 | u = Convolution2D(n_filters, FL, FL, activation='relu', init=init, 298 | W_regularizer=l2(lmbda), border_mode='same')(u) 299 | 300 | # Final output 301 | final_activation = 'sigmoid' 302 | u = Convolution2D(1, 1, 1, activation=final_activation, init=init, 303 | W_regularizer=l2(lmbda), border_mode='same')(u) 304 | u = Reshape((dim, dim))(u) 305 | if k2: 306 | model = Model(inputs=img_input, outputs=u) 307 | else: 308 | model = Model(input=img_input, output=u) 309 | 310 | optimizer = Adam(lr=learn_rate) 311 | model.compile(loss='binary_crossentropy', optimizer=optimizer) 312 | print(model.summary()) 313 | 314 | return model 315 | 316 | ######################## 317 | def train_and_test_model(Data, Craters, MP, i_MP): 318 | """Function that trains, tests and saves the model, printing out metrics 319 | after each model. 320 | 321 | Parameters 322 | ---------- 323 | Data : dict 324 | Inputs and Target Moon data. 325 | Craters : dict 326 | Human-counted crater data. 327 | MP : dict 328 | Contains all relevant parameters. 329 | i_MP : int 330 | Iteration number (when iterating over hypers). 331 | """ 332 | # Static params 333 | dim, nb_epoch, bs = MP['dim'], MP['epochs'], MP['bs'] 334 | 335 | # Iterating params 336 | FL = get_param_i(MP['filter_length'], i_MP) 337 | learn_rate = get_param_i(MP['lr'], i_MP) 338 | n_filters = get_param_i(MP['n_filters'], i_MP) 339 | init = get_param_i(MP['init'], i_MP) 340 | lmbda = get_param_i(MP['lambda'], i_MP) 341 | drop = get_param_i(MP['dropout'], i_MP) 342 | 343 | # Build model 344 | model = build_model(dim, learn_rate, lmbda, drop, FL, init, n_filters) 345 | 346 | # Main loop 347 | n_samples = MP['n_train'] 348 | for nb in range(nb_epoch): 349 | if k2: 350 | model.fit_generator( 351 | custom_image_generator(Data['train'][0], Data['train'][1], 352 | batch_size=bs), 353 | steps_per_epoch=n_samples/bs, epochs=1, verbose=1, 354 | # validation_data=(Data['dev'][0],Data['dev'][1]), #no gen 355 | validation_data=custom_image_generator(Data['dev'][0], 356 | Data['dev'][1], 357 | batch_size=bs), 358 | validation_steps=n_samples, 359 | callbacks=[ 360 | EarlyStopping(monitor='val_loss', patience=3, verbose=0)]) 361 | else: 362 | model.fit_generator( 363 | custom_image_generator(Data['train'][0], Data['train'][1], 364 | batch_size=bs), 365 | samples_per_epoch=n_samples, nb_epoch=1, verbose=1, 366 | # validation_data=(Data['dev'][0],Data['dev'][1]), #no gen 367 | validation_data=custom_image_generator(Data['dev'][0], 368 | Data['dev'][1], 369 | batch_size=bs), 370 | nb_val_samples=n_samples, 371 | callbacks=[ 372 | EarlyStopping(monitor='val_loss', patience=3, verbose=0)]) 373 | 374 | get_metrics(Data['dev'], Craters['dev'], dim, model) 375 | 376 | if MP['save_models'] == 1: 377 | model.save(MP['save_dir']) 378 | 379 | print("###################################") 380 | print("##########END_OF_RUN_INFO##########") 381 | print("""learning_rate=%e, batch_size=%d, filter_length=%e, n_epoch=%d 382 | n_train=%d, img_dimensions=%d, init=%s, n_filters=%d, lambda=%e 383 | dropout=%f""" % (learn_rate, bs, FL, nb_epoch, MP['n_train'], 384 | MP['dim'], init, n_filters, lmbda, drop)) 385 | get_metrics(Data['test'], Craters['test'], dim, model) 386 | print("###################################") 387 | print("###################################") 388 | 389 | ######################## 390 | def get_models(MP): 391 | """Top-level function that loads data files and calls train_and_test_model. 392 | 393 | Parameters 394 | ---------- 395 | MP : dict 396 | Model Parameters. 397 | """ 398 | dir = MP['dir'] 399 | n_train, n_dev, n_test = MP['n_train'], MP['n_dev'], MP['n_test'] 400 | 401 | # Load data 402 | train = h5py.File('%strain_images.hdf5' % dir, 'r') 403 | dev = h5py.File('%sdev_images.hdf5' % dir, 'r') 404 | test = h5py.File('%stest_images.hdf5' % dir, 'r') 405 | Data = { 406 | 'train': [train['input_images'][:n_train].astype('float32'), 407 | train['target_masks'][:n_train].astype('float32')], 408 | 'dev': [dev['input_images'][:n_dev].astype('float32'), 409 | dev['target_masks'][:n_dev].astype('float32')], 410 | 'test': [test['input_images'][:n_test].astype('float32'), 411 | test['target_masks'][:n_test].astype('float32')] 412 | } 413 | train.close() 414 | dev.close() 415 | test.close() 416 | 417 | # Rescale, normalize, add extra dim 418 | proc.preprocess(Data) 419 | 420 | # Load ground-truth craters 421 | Craters = { 422 | 'train': pd.HDFStore('%strain_craters.hdf5' % dir, 'r'), 423 | 'dev': pd.HDFStore('%sdev_craters.hdf5' % dir, 'r'), 424 | 'test': pd.HDFStore('%stest_craters.hdf5' % dir, 'r') 425 | } 426 | 427 | # Iterate over parameters 428 | for i in range(MP['N_runs']): 429 | train_and_test_model(Data, Craters, MP, i) 430 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Cartopy==0.14.2 2 | h5py==2.6.0 3 | Keras==1.2.2 4 | numpy==1.12.1 5 | opencv-python==3.2.0.6 6 | pandas==0.19.1 7 | Pillow==3.1.2 8 | scikit-image==0.12.3 9 | tables==3.4.2 10 | tensorflow==0.10.0rc0 -------------------------------------------------------------------------------- /run_get_unique_craters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Run/Obtain Unique Crater Distribution 3 | 4 | Execute extracting craters from model target predictions and filtering 5 | out duplicates. 6 | """ 7 | import get_unique_craters as guc 8 | import sys 9 | import numpy as np 10 | 11 | # Crater Parameters 12 | CP = {} 13 | 14 | # Image width/height, assuming square images. 15 | CP['dim'] = 256 16 | 17 | # Data type - train, dev, test 18 | CP['datatype'] = 'test' 19 | 20 | # Number of images to extract craters from 21 | CP['n_imgs'] = 30000 22 | 23 | # Hyperparameters 24 | CP['llt2'] = float(sys.argv[1]) # D_{L,L} from Silburt et. al (2017) 25 | CP['rt2'] = float(sys.argv[2]) # D_{R} from Silburt et. al (2017) 26 | 27 | # Location of model to generate predictions (if they don't exist yet) 28 | CP['dir_model'] = 'models/model.h5' 29 | 30 | # Location of where hdf5 data images are stored 31 | CP['dir_data'] = 'catalogues/%s_images.hdf5' % CP['datatype'] 32 | 33 | # Location of where model predictions are/will be stored 34 | CP['dir_preds'] = 'catalogues/%s_preds_n%d.hdf5' % (CP['datatype'], 35 | CP['n_imgs']) 36 | 37 | # Location of where final unique crater distribution will be stored 38 | CP['dir_result'] = 'catalogues/%s_craterdist.npy' % (CP['datatype']) 39 | 40 | if __name__ == '__main__': 41 | craters_unique = np.empty([0, 3]) 42 | craters_unique = guc.extract_unique_craters(CP, craters_unique) 43 | -------------------------------------------------------------------------------- /run_input_data_gen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Input Image Dataset Generator 3 | 4 | Script for generating input datasets from Lunar global digital elevation maps 5 | (DEMs) and crater catalogs. 6 | 7 | This script is designed to use the LRO-Kaguya DEM and a combination of the 8 | LOLA-LROC 5 - 20 km and Head et al. 2010 >=20 km crater catalogs. It 9 | generates a randomized set of small (projection-corrected) images and 10 | corresponding crater targets. The input and target image sets are stored as 11 | hdf5 files. The longitude and latitude limits of each image is included in the 12 | input set file, and tables of the craters in each image are stored in a 13 | separate Pandas HDFStore hdf5 file. 14 | 15 | The script's parameters are located under the Global Variables. We recommend 16 | making a copy of this script when generating a dataset. 17 | 18 | MPI4py can be used to generate multiple hdf5 files simultaneously - each thread 19 | writes `amt` number of images to its own file. 20 | """ 21 | 22 | ########## Imports ########## 23 | 24 | # Python 2.7 compatibility. 25 | from __future__ import absolute_import, division, print_function 26 | 27 | from PIL import Image 28 | import input_data_gen as igen 29 | import time 30 | 31 | ########## Global Variables ########## 32 | 33 | # Use MPI4py? Set this to False if it's not supposed by the system. 34 | use_mpi4py = False 35 | 36 | # Source image path. 37 | source_image_path = "LunarLROLrocKaguya_118mperpix.png" 38 | 39 | # LROC crater catalog csv path. 40 | lroc_csv_path = "./catalogues/LROCCraters.csv" 41 | 42 | # Head et al. catalog csv path. 43 | head_csv_path = "./catalogues/HeadCraters.csv" 44 | 45 | # Output filepath and file header. Eg. if outhead = "./input_data/train", 46 | # files will have extension "./out/train_inputs.hdf5" and 47 | # "./out/train_targets.hdf5" 48 | outhead = "./input_data/train" 49 | 50 | # Number of images to make (if using MPI4py, number of image per thread to 51 | # make). 52 | amt = 30000 53 | 54 | # Range of image widths, in pixels, to crop from source image (input images 55 | # will be scaled down to ilen). For Orthogonal projection, larger images are 56 | # distorted at their edges, so there is some trade-off between ensuring images 57 | # have minimal distortion, and including the largest craters in the image. 58 | rawlen_range = [500, 6500] 59 | 60 | # Distribution to sample from rawlen_range - "uniform" for uniform, and "log" 61 | # for loguniform. 62 | rawlen_dist = 'log' 63 | 64 | # Size of input images. 65 | ilen = 256 66 | 67 | # Size of target images. 68 | tglen = 256 69 | 70 | # [Min long, max long, min lat, max lat] dimensions of source image. 71 | source_cdim = [-180., 180., -60., 60.] 72 | 73 | # [Min long, max long, min lat, max lat] dimensions of the region of the source 74 | # to use when randomly cropping. Used to distinguish training from test sets. 75 | sub_cdim = [-180., 180., -60., 60.] 76 | 77 | # Minimum pixel diameter of craters to include in in the target. 78 | minpix = 1. 79 | 80 | # Radius of the world in km (1737.4 for Moon). 81 | R_km = 1737.4 82 | 83 | ### Target mask arguments. ### 84 | 85 | # If True, truncate mask where image has padding. 86 | truncate = True 87 | 88 | # If rings = True, thickness of ring in pixels. 89 | ringwidth = 1 90 | 91 | # If True, script prints out the image it's currently working on. 92 | verbose = True 93 | 94 | ########## Script ########## 95 | 96 | if __name__ == '__main__': 97 | 98 | start_time = time.time() 99 | 100 | # Utilize mpi4py for multithreaded processing. 101 | if use_mpi4py: 102 | from mpi4py import MPI 103 | comm = MPI.COMM_WORLD 104 | rank = comm.Get_rank() 105 | size = comm.Get_size() 106 | print("Thread {0} of {1}".format(rank, size)) 107 | istart = rank * amt 108 | else: 109 | istart = 0 110 | 111 | # Read source image and crater catalogs. 112 | img = Image.open(source_image_path).convert("L") 113 | craters = igen.ReadLROCHeadCombinedCraterCSV(filelroc=lroc_csv_path, 114 | filehead=head_csv_path) 115 | 116 | # Sample subset of image. Co-opt igen.ResampleCraters to remove all 117 | # craters beyond cdim (either sub or source). 118 | if sub_cdim != source_cdim: 119 | img = igen.InitialImageCut(img, source_cdim, sub_cdim) 120 | # This always works, since sub_cdim < source_cdim. 121 | craters = igen.ResampleCraters(craters, sub_cdim, None, arad=R_km) 122 | 123 | # Generate input images. 124 | igen.GenDataset(img, craters, outhead, rawlen_range=rawlen_range, 125 | rawlen_dist=rawlen_dist, ilen=ilen, cdim=sub_cdim, 126 | arad=R_km, minpix=minpix, tglen=tglen, binary=True, 127 | rings=True, ringwidth=ringwidth, truncate=truncate, 128 | amt=amt, istart=istart, verbose=verbose) 129 | 130 | elapsed_time = time.time() - start_time 131 | if verbose: 132 | print("Time elapsed: {0:.1f} min".format(elapsed_time / 60.)) 133 | -------------------------------------------------------------------------------- /run_model_train.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Run Convolutional Neural Network Training 3 | 4 | Execute the training of a (UNET) Convolutional Neural Network on 5 | images of the Moon and binary ring targets. 6 | """ 7 | 8 | import model_train as mt 9 | 10 | # Model Parameters 11 | MP = {} 12 | 13 | # Directory of train/dev/test image and crater hdf5 files. 14 | MP['dir'] = 'catalogues/' 15 | 16 | # Image width/height, assuming square images. 17 | MP['dim'] = 256 18 | 19 | # Batch size: smaller values = less memory but less accurate gradient estimate 20 | MP['bs'] = 8 21 | 22 | # Number of training epochs. 23 | MP['epochs'] = 4 24 | 25 | # Number of train/valid/test samples, needs to be a multiple of batch size. 26 | MP['n_train'] = 30000 27 | MP['n_dev'] = 5000 28 | MP['n_test'] = 5000 29 | 30 | # Save model (binary flag) and directory. 31 | MP['save_models'] = 1 32 | MP['save_dir'] = 'models/model.h5' 33 | 34 | # Model Parameters (to potentially iterate over, keep in lists). 35 | MP['N_runs'] = 1 # Number of runs 36 | MP['filter_length'] = [3] # Filter length 37 | MP['lr'] = [0.0001] # Learning rate 38 | MP['n_filters'] = [112] # Number of filters 39 | MP['init'] = ['he_normal'] # Weight initialization 40 | MP['lambda'] = [1e-6] # Weight regularization 41 | MP['dropout'] = [0.15] # Dropout fraction 42 | 43 | # Iterating over parameters example. 44 | # MP['N_runs'] = 2 45 | # MP['lambda']=[1e-4,1e-4] 46 | 47 | if __name__ == '__main__': 48 | mt.get_models(MP) 49 | -------------------------------------------------------------------------------- /tests/LunarLROLrocKaguya_1180mperpix_downsamp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silburt/DeepMoon/0bccc9790bbf02b3c1df33bd0f452d87b136dc9d/tests/LunarLROLrocKaguya_1180mperpix_downsamp.png -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # DeepMoon Test Suite 2 | 3 | ## Dependences 4 | 5 | Running the DeepMoon test suite requires the following packages (in addition to 6 | those required by DeepMoon itself): 7 | 8 | - [pytest](https://pypi.python.org/pypi/pytest) >= 3.11 9 | 10 | ## Running the Test Suite 11 | 12 | Each file in the folder is a self-contained test script for the main functions 13 | and utilities of DeepMoon. To run them, use `pytest` on the command line, eg. 14 | 15 | ``` 16 | pytest test_get_unique_craters.py 17 | ``` 18 | 19 | ## Sample Files Used By the Test Suite 20 | 21 | - `LunarLROLrocKaguya_1180mperpix_downsamp.png`: the LROC-Kaguya DEM, 22 | downsampled to 9216 x 3072 pixels, or 1180 m per pixel. 23 | - `sample_template_match.hdf5`: Contains two csv crater ground truth arrays 24 | and corresponding CNN-predicted targets. 25 | - `sample_crater_csv.hdf5`: Pandas HDFStore file containing the crater 26 | data table for the 6th image (`img_00005`) in the test dataset. 27 | - `sample_crater_csv_metadata.hdf5`: metadata for the 6th image in the test 28 | dataset. 29 | -------------------------------------------------------------------------------- /tests/sample_crater_csv.hdf5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silburt/DeepMoon/0bccc9790bbf02b3c1df33bd0f452d87b136dc9d/tests/sample_crater_csv.hdf5 -------------------------------------------------------------------------------- /tests/sample_crater_csv_metadata.hdf5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silburt/DeepMoon/0bccc9790bbf02b3c1df33bd0f452d87b136dc9d/tests/sample_crater_csv_metadata.hdf5 -------------------------------------------------------------------------------- /tests/sample_template_match.hdf5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silburt/DeepMoon/0bccc9790bbf02b3c1df33bd0f452d87b136dc9d/tests/sample_template_match.hdf5 -------------------------------------------------------------------------------- /tests/test_get_unique_craters.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | import numpy as np 3 | import h5py 4 | import pandas as pd 5 | import sys 6 | sys.path.append('../') 7 | import get_unique_craters as guc 8 | 9 | 10 | class TestLongLatEstimation(object): 11 | 12 | def setup(self): 13 | ctrs = pd.HDFStore('./sample_crater_csv.hdf5', 'r') 14 | ctrs_meta = h5py.File('./sample_crater_csv_metadata.hdf5', 'r') 15 | 16 | self.craters = ctrs['craters'] 17 | self.dim = (256, 256) 18 | self.llbd = ctrs_meta['longlat_bounds'][...] 19 | self.dc = ctrs_meta['pix_distortion_coefficient'][...] 20 | 21 | ctrs.close() 22 | ctrs_meta.close() 23 | 24 | def test_estimate_longlatdiamkm(self): 25 | coords = self.craters[['x', 'y', 'Radius (pix)']].as_matrix() 26 | craters_unique = guc.estimate_longlatdiamkm( 27 | self.dim, self.llbd, self.dc, coords) 28 | # Check that estimate is same as predictions in sample_crater_csv.hdf5. 29 | assert np.all(np.isclose(craters_unique[:, 0], 30 | self.craters['Predicted Long'], 31 | atol=0., rtol=1e-10)) 32 | assert np.all(np.isclose(craters_unique[:, 1], 33 | self.craters['Predicted Lat'], 34 | atol=0., rtol=1e-10)) 35 | assert np.all(np.isclose(craters_unique[:, 2], 36 | self.craters['Predicted Radius (km)'], 37 | atol=0., rtol=1e-10)) 38 | # Check that estimate is within expected tolerance from ground truth 39 | # values in sample_crater_csv.hdf5. 40 | assert np.all(abs(craters_unique[:, 0] - self.craters['Long']) / 41 | (self.llbd[1] - self.llbd[0]) < 0.01) 42 | assert np.all(abs(craters_unique[:, 1] - self.craters['Lat']) / 43 | (self.llbd[3] - self.llbd[2]) < 0.02) 44 | # Radius is exact, since we use the inverse estimation from km to pix 45 | # to get the ground truth crater pixel radii/diameters in 46 | # input_data_gen.py. 47 | assert np.all(np.isclose(craters_unique[:, 2], 48 | self.craters['Radius (km)'], 49 | atol=0., rtol=1e-10)) 50 | -------------------------------------------------------------------------------- /tests/test_input_data_gen.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | import sys 3 | import pytest 4 | import pandas as pd 5 | import numpy as np 6 | import cv2 7 | import h5py 8 | import cartopy.crs as ccrs 9 | import cartopy.img_transform as cimg 10 | from PIL import Image 11 | sys.path.append('../') 12 | import input_data_gen as igen 13 | import utils.transform as trf 14 | 15 | 16 | class TestCatalogue(object): 17 | """Tests crater catalogues.""" 18 | 19 | def setup(self): 20 | # Head et al. dataset. 21 | head = pd.read_csv('../catalogues/HeadCraters.csv', header=0, 22 | names=['Long', 'Lat', 'Diameter (km)']) 23 | lroc = pd.read_csv('../catalogues/LROCCraters.csv', 24 | usecols=range(2, 5), header=0) 25 | 26 | self.lrochead_t = pd.concat([lroc, head], axis=0, ignore_index=True, 27 | copy=True) 28 | self.lrochead_t.sort_values(by='Lat', inplace=True) 29 | self.lrochead_t.reset_index(inplace=True, drop=True) 30 | 31 | def test_dataframes_equal(self): 32 | lrochead = igen.ReadLROCHeadCombinedCraterCSV( 33 | filelroc="../catalogues/LROCCraters.csv", 34 | filehead="../catalogues/HeadCraters.csv", 35 | sortlat=True) 36 | lrochead_nosort = igen.ReadLROCHeadCombinedCraterCSV( 37 | filelroc="../catalogues/LROCCraters.csv", 38 | filehead="../catalogues/HeadCraters.csv", 39 | sortlat=False) 40 | 41 | assert np.all(lrochead == self.lrochead_t) 42 | assert not np.all(lrochead == lrochead_nosort) 43 | 44 | 45 | class TestImageTransforms(object): 46 | """Tests image transform functions.""" 47 | 48 | def setup(self): 49 | # Image length. 50 | self.imlen = 256 51 | 52 | # Image. 53 | self.img = Image.open( 54 | "LunarLROLrocKaguya_1180mperpix_downsamp.png").convert("L") 55 | self.imgsize = self.img.size 56 | 57 | # Crater catalogue. 58 | self.craters = igen.ReadLROCHeadCombinedCraterCSV( 59 | filelroc="../catalogues/LROCCraters.csv", 60 | filehead="../catalogues/HeadCraters.csv", 61 | sortlat=True) 62 | 63 | # Long/lat limits 64 | self.cdim = [-180., 180., -60., 60.] 65 | 66 | # Coordinate systems. 67 | self.iglobe = ccrs.Globe(semimajor_axis=1737400, 68 | semiminor_axis=1737400, 69 | ellipse=None) 70 | self.geoproj = ccrs.Geodetic(globe=self.iglobe) 71 | self.iproj = ccrs.PlateCarree(globe=self.iglobe) 72 | 73 | def get_llbd(self, box): 74 | llong, llat = trf.pix2coord(box[::2], box[1::2], self.cdim, 75 | self.imgsize, origin="upper") 76 | return np.r_[llong, llat[::-1]] 77 | 78 | def get_extents(self, llbd, geoproj, iproj, oproj): 79 | """Calculates image boundaries within projections' coordinates.""" 80 | 81 | # Get min, mean and max long and lat. 82 | longbd = np.array([llbd[0], np.mean(llbd[:2]), llbd[1]]) 83 | latbd = np.array([llbd[2], np.mean(llbd[2:]), llbd[3]]) 84 | # Make a grid of these, then unravel. 85 | longbd, latbd = np.meshgrid(longbd, latbd) 86 | longbd = longbd.ravel() 87 | latbd = latbd.ravel() 88 | 89 | # [:,:2] because we don't need elevation data. 90 | ires = iproj.transform_points( 91 | x=longbd, y=latbd, src_crs=geoproj)[:, :2] 92 | iextent = [min(ires[:, 0]), max(ires[:, 0]), 93 | min(ires[:, 1]), max(ires[:, 1])] 94 | # Remove zeros from iextent. 95 | iextent = [1e-8 if iext == 0 else iext for iext in iextent] 96 | 97 | ores = oproj.transform_points( 98 | x=longbd, y=latbd, src_crs=geoproj)[:, :2] 99 | oextent = [min(ores[:, 0]), max(ores[:, 0]), 100 | min(ores[:, 1]), max(ores[:, 1])] 101 | 102 | return iextent, oextent, ores 103 | 104 | def test_regrid_shape_aspect(self): 105 | # Wide image with x / y aspect ratio of 2.25. 106 | target_extent = 1737 * np.array([0.7, 1.6, 0.4, 0.8]) 107 | regrid_shape = igen.regrid_shape_aspect(self.imlen, target_extent) 108 | assert int(regrid_shape[1]) == self.imlen 109 | assert regrid_shape[0] / regrid_shape[1] == ( 110 | (target_extent[1] - target_extent[0]) / 111 | (target_extent[3] - target_extent[2])) 112 | 113 | # Tall image with x / y aspect ratio of 3.777... 114 | target_extent = 1377 * np.array([0.2, 1.1, 0.4, 3.8]) 115 | regrid_shape = igen.regrid_shape_aspect(self.imlen, target_extent) 116 | assert int(regrid_shape[0]) == self.imlen 117 | regrid_aspect = regrid_shape[0] / regrid_shape[1] 118 | expected_aspect = ((target_extent[1] - target_extent[0]) / 119 | (target_extent[3] - target_extent[2])) 120 | assert np.isclose(regrid_aspect, expected_aspect, rtol=1e-8, atol=0.) 121 | 122 | @pytest.mark.parametrize("box", ([1197, 430, 1506, 739], 123 | [5356, 603, 5827, 1074], 124 | [813, 916, 1118, 1221], 125 | [5662, 2287, 6018, 2643], 126 | [420, 1627, 814, 2021])) 127 | def test_warpimage(self, box): 128 | """Test image warping and padding. 129 | 130 | Output of this function was tested by visual inspection. 131 | """ 132 | box = np.array(box, dtype='int32') 133 | # Crop image. 134 | img = np.asanyarray(self.img.crop(box)) 135 | 136 | # Determine long/lat and output projection. 137 | llbd = self.get_llbd(box) 138 | oproj = ccrs.Orthographic(central_longitude=np.mean(llbd[:2]), 139 | central_latitude=np.mean(llbd[2:]), 140 | globe=self.iglobe) 141 | 142 | # Determine coordinates of image limits in input and output projection 143 | # coordinates. 144 | iextent, oextent, ores = self.get_extents(llbd, self.geoproj, 145 | self.iproj, oproj) 146 | 147 | regrid_shape = 1.2 * min(img.shape) 148 | regrid_shape = igen.regrid_shape_aspect(regrid_shape, oextent) 149 | 150 | imgout, ext = cimg.warp_array(img[::-1], 151 | source_proj=self.iproj, 152 | source_extent=iextent, 153 | target_proj=oproj, 154 | target_res=regrid_shape, 155 | target_extent=oextent, 156 | mask_extrapolated=True) 157 | imgout = np.ma.filled(imgout[::-1], fill_value=0) 158 | 159 | # Obtain image from igen.WarpImage. 160 | imgout_WarpImage = igen.WarpImage(img, self.iproj, iextent, oproj, 161 | oextent, origin="upper", rgcoeff=1.2) 162 | imgout_WarpImage = np.ma.filled(imgout_WarpImage, fill_value=0) 163 | 164 | # Test that WarpImage gives the same result as this function. 165 | assert np.all(imgout == imgout_WarpImage) 166 | 167 | # Pad image. 168 | imgw = Image.fromarray(imgout, mode="L") 169 | imgw_loh = imgw.size[0] / imgw.size[1] 170 | if imgw_loh > (img.shape[1] / img.shape[0]): 171 | imgw = imgw.resize([img.shape[0], 172 | int(np.round(img.shape[0] / imgw_loh))], 173 | resample=Image.NEAREST) 174 | else: 175 | imgw = imgw.resize([int(np.round(imgw_loh * img.shape[0])), 176 | img.shape[0]], resample=Image.NEAREST) 177 | imgpad = Image.new('L', (img.shape[1], img.shape[0]), (0)) 178 | offset = ((imgpad.size[0] - imgw.size[0]) // 2, 179 | (imgpad.size[1] - imgw.size[1]) // 2) 180 | imgpad.paste(imgw, offset) 181 | 182 | # Obtain image from igen.WarpImagePad. 183 | imgout_WarpImagePad, WIPsize, WIPoffset = igen.WarpImagePad( 184 | img, self.iproj, iextent, oproj, oextent, origin="upper", 185 | rgcoeff=1.2, fillbg="black") 186 | 187 | # Test that WarpImagePad gives the same result as this function. 188 | assert np.all(np.asanyarray(imgpad) == 189 | np.asanyarray(imgout_WarpImagePad)) 190 | assert WIPsize == imgw.size 191 | assert offset == WIPoffset 192 | 193 | @pytest.mark.parametrize("box", ([1197, 430, 1506, 739], 194 | [5356, 603, 5827, 1074], 195 | [813, 916, 1118, 1221], 196 | [5662, 2287, 6018, 2643], 197 | [420, 1627, 814, 2021])) 198 | def test_warpcraters(self, box): 199 | """Test image warping and padding. 200 | 201 | Output of this function was tested by visual inspection. 202 | """ 203 | box = np.array(box, dtype='int32') 204 | # Crop image. 205 | img = np.asanyarray(self.img.crop(box)) 206 | 207 | # Determine long/lat and output projection. 208 | llbd = self.get_llbd(box) 209 | oproj = ccrs.Orthographic(central_longitude=np.mean(llbd[:2]), 210 | central_latitude=np.mean(llbd[2:]), 211 | globe=self.iglobe) 212 | 213 | # Determine coordinates of image limits in input and output projection 214 | # coordinates. 215 | iextent, oextent, ores = self.get_extents(llbd, self.geoproj, 216 | self.iproj, oproj) 217 | 218 | # Obtain image from igen.WarpImagePad. 219 | imgout_WarpImagePad, WIPsize, WIPoffset = igen.WarpImagePad( 220 | img, self.iproj, iextent, oproj, oextent, origin="upper", 221 | rgcoeff=1.2, fillbg="black") 222 | 223 | ctr_xlim = ((self.craters["Long"] >= llbd[0]) & 224 | (self.craters["Long"] <= llbd[1])) 225 | ctr_ylim = ((self.craters["Lat"] >= llbd[2]) & 226 | (self.craters["Lat"] <= llbd[3])) 227 | ctr_wrp = self.craters.loc[ctr_xlim & ctr_ylim, :].copy() 228 | 229 | # Get output projection coords. 230 | # [:,:2] becaus we don't need elevation data 231 | # If statement is in case ctr_wrp has nothing in it 232 | if ctr_wrp.shape[0]: 233 | ilong = ctr_wrp["Long"].as_matrix() 234 | ilat = ctr_wrp["Lat"].as_matrix() 235 | res = oproj.transform_points(x=ilong, y=ilat, 236 | src_crs=self.geoproj)[:, :2] 237 | 238 | # Get output 239 | ctr_wrp["x"], ctr_wrp["y"] = trf.coord2pix(res[:, 0], res[:, 1], 240 | oextent, WIPsize, 241 | origin="upper") 242 | else: 243 | ctr_wrp["x"] = [] 244 | ctr_wrp["y"] = [] 245 | 246 | ctr_wrpctrloc = igen.WarpCraterLoc(self.craters, self.geoproj, oproj, 247 | oextent, WIPsize, llbd=llbd, 248 | origin="upper") 249 | 250 | assert np.all(ctr_wrp == ctr_wrpctrloc) 251 | 252 | @pytest.mark.parametrize("box", ([1197, 430, 1506, 739], 253 | [5356, 603, 5827, 1074], 254 | [813, 916, 1118, 1221], 255 | [5662, 2287, 6018, 2643], 256 | [420, 1627, 814, 2021])) 257 | def test_platecarreetoorthographic(self, box): 258 | """Test full Plate Carree to orthographic transform. 259 | 260 | Assumes input_data_gen's image and crater position warping 261 | functions are correct. Output of this function was tested by visual 262 | inspection. 263 | """ 264 | 265 | box = np.array(box, dtype='int32') 266 | # Crop image. 267 | img = np.asanyarray(self.img.crop(box)) 268 | 269 | # Determine long/lat and output projection. 270 | llbd = self.get_llbd(box) 271 | oproj = ccrs.Orthographic(central_longitude=np.mean(llbd[:2]), 272 | central_latitude=np.mean(llbd[2:]), 273 | globe=self.iglobe) 274 | 275 | # Determine coordinates of image limits in input and output projection 276 | # coordinates. 277 | iextent, oextent, ores = self.get_extents(llbd, self.geoproj, 278 | self.iproj, oproj) 279 | 280 | # Obtain image from igen.WarpImagePad. 281 | imgo, imgwshp, offset = igen.WarpImagePad( 282 | img, self.iproj, iextent, oproj, oextent, origin="upper", 283 | rgcoeff=1.2, fillbg="black") 284 | 285 | ctr_xy = igen.WarpCraterLoc(self.craters, self.geoproj, oproj, 286 | oextent, imgwshp, llbd=llbd, 287 | origin="upper") 288 | ctr_xy.loc[:, "x"] += offset[0] 289 | ctr_xy.loc[:, "y"] += offset[1] 290 | 291 | distortion_coefficient = ((ores[7, 1] - ores[1, 1]) / 292 | (oextent[3] - oextent[2])) 293 | pixperkm = trf.km2pix(imgo.size[1], llbd[3] - llbd[2], 294 | dc=distortion_coefficient, a=1737.4) 295 | ctr_xy["Diameter (pix)"] = ctr_xy["Diameter (km)"] * pixperkm 296 | 297 | 298 | # Determine x, y position of central lat/long. 299 | centrallonglat = pd.DataFrame({"Long": [np.mean(llbd[:2])], 300 | "Lat": [np.mean(llbd[2:])]}) 301 | centrallonglat_xy = igen.WarpCraterLoc(centrallonglat, self.geoproj, 302 | oproj, oextent, imgwshp, 303 | llbd=llbd, origin="upper") 304 | centrallonglat_xy.loc[:, "x"] += offset[0] 305 | centrallonglat_xy.loc[:, "y"] += offset[1] 306 | 307 | img_pc, ctr_pc, dc_pc, cll_pc = igen.PlateCarree_to_Orthographic( 308 | self.img.crop(box), llbd, self.craters, iglobe=self.iglobe, 309 | ctr_sub=False, arad=1737.4, origin="upper", rgcoeff=1.2, 310 | slivercut=0.) 311 | 312 | assert np.all(np.asanyarray(img_pc) == np.asanyarray(imgo)) 313 | assert np.all(ctr_pc == ctr_xy) 314 | assert dc_pc == distortion_coefficient 315 | assert np.all(cll_pc == centrallonglat_xy) 316 | 317 | 318 | class TestMaskMaking(object): 319 | """Tests mask making functions.""" 320 | 321 | def setup(self): 322 | # Image length. 323 | self.imlen = 256 324 | 325 | # Dummy image. 326 | self.img = np.ones([self.imlen, self.imlen]) 327 | 328 | # Fake craters. 329 | crat_x = np.array([128, 50, 2, 4, 167, 72, 254, 1]) 330 | crat_y = np.array([128, 50, 1, 191, 3, 255, 254, 255]) 331 | crat_d = np.array([131, 7, 12, 38, 64, 4, 3, 72]) 332 | self.craters = pd.DataFrame([crat_x, crat_y, crat_d]).T 333 | self.craters.rename(columns={0: "x", 1: "y", 2: "Diameter (pix)"}, 334 | inplace=True) 335 | 336 | @pytest.mark.parametrize("r", (2, 17, 240, 1)) 337 | def test_circlemaker(self, r): 338 | circle = igen.circlemaker(r=r) 339 | midpt = circle.shape[0] // 2 340 | nx, ny = np.mgrid[-midpt:midpt + 1, -midpt:midpt + 1] 341 | circle_comp = (nx**2 + ny**2 <= r**2) 342 | assert np.all(circle_comp == circle) 343 | 344 | @pytest.mark.parametrize("r, dr", ((2, 1), (17, 4), (240, 3), (1, 1))) 345 | def test_ringmaker(self, r, dr): 346 | ring = igen.ringmaker(r=r, dr=dr) 347 | midpt_corr = ring.shape[0] // 2 348 | ring_comp = cv2.circle(np.zeros_like(ring), (midpt_corr, midpt_corr), 349 | int(np.round(r)), 1, thickness=dr) 350 | assert np.all(ring == ring_comp) 351 | 352 | def test_makerings(self): 353 | mask = np.zeros_like(self.img) 354 | radius = self.craters["Diameter (pix)"].values / 2. 355 | 356 | for i in range(self.craters.shape[0]): 357 | radius = self.craters.loc[i, "Diameter (pix)"] / 2. 358 | kernel = igen.ringmaker(r=radius, dr=1) 359 | # "Dummy values" so we can use get_merge_indices 360 | kernel_support = kernel.shape[0] 361 | ks_half = kernel_support // 2 362 | 363 | # Calculate indices on image where kernel should be added 364 | [imxl, imxr, gxl, gxr] = igen.get_merge_indices( 365 | self.craters.loc[i, "x"], self.img.shape[1], ks_half, 366 | kernel_support) 367 | [imyl, imyr, gyl, gyr] = igen.get_merge_indices( 368 | self.craters.loc[i, "y"], self.img.shape[0], ks_half, 369 | kernel_support) 370 | 371 | # Add kernel to image 372 | mask[imyl:imyr, imxl:imxr] += kernel[gyl:gyr, gxl:gxr] 373 | 374 | mask = (mask > 0).astype(float) 375 | 376 | mask_mm = igen.make_mask(self.craters, self.img, binary=True, 377 | rings=True, ringwidth=1, truncate=False) 378 | 379 | assert np.all(mask == mask_mm) 380 | 381 | 382 | class TestAuxFunctions(object): 383 | """Tests helper functions for run_input_data_gen.py.""" 384 | 385 | def setup(self): 386 | # Image length. 387 | self.imlen = 256 388 | 389 | # Image. 390 | self.img = Image.open( 391 | "LunarLROLrocKaguya_1180mperpix_downsamp.png").convert("L") 392 | self.imgsize = self.img.size 393 | 394 | # Crater catalogue. 395 | self.craters = igen.ReadLROCHeadCombinedCraterCSV( 396 | filelroc="../catalogues/LROCCraters.csv", 397 | filehead="../catalogues/HeadCraters.csv", 398 | sortlat=True) 399 | 400 | # Long/lat limits 401 | self.cdim = [-180., 180., -60., 60.] 402 | 403 | # Coordinate systems. 404 | self.iglobe = ccrs.Globe(semimajor_axis=1737400, 405 | semiminor_axis=1737400, 406 | ellipse=None) 407 | 408 | @pytest.mark.parametrize("llbd, minpix", 409 | (([-30., 30., -30., 30.], 0), 410 | ([-127., 0., 35., 87.], 3), 411 | ([-117.54, 120., -18., 90.], 2), 412 | ([117.54, 120., -90., 0.], 0), 413 | ([-180., 180., -60., 60.], 10))) 414 | def test_resamplecraters(self, llbd, minpix): 415 | ctr_xlim = ((self.craters['Long'] >= llbd[0]) & 416 | (self.craters['Long'] <= llbd[1])) 417 | ctr_ylim = ((self.craters['Lat'] >= llbd[2]) & 418 | (self.craters['Lat'] <= llbd[3])) 419 | ctr_sub = self.craters.loc[ctr_xlim & ctr_ylim, :] 420 | imgheight = int(3000. * (llbd[3] - llbd[2]) / 180.) 421 | pixperkm = trf.km2pix(imgheight, llbd[3] - llbd[2]) 422 | minkm = minpix / pixperkm 423 | ctr_sub = ctr_sub[ctr_sub['Diameter (km)'] >= minkm] 424 | ctr_sub.reset_index(drop=True, inplace=True) 425 | ctr_rs = igen.ResampleCraters(self.craters, llbd, imgheight, 426 | minpix=minpix) 427 | assert np.all(ctr_rs == ctr_sub) 428 | 429 | @pytest.mark.parametrize("newcdim, newpixdim", 430 | (([-180., 0., 0., 60.], (4608, 1536)), 431 | ([-60., 60., -60., 60.], (3072, 3072)))) 432 | def test_initialimagecrop(self, newcdim, newpixdim): 433 | cropped_img = igen.InitialImageCut(self.img, self.cdim, newcdim) 434 | assert cropped_img.size == newpixdim 435 | 436 | 437 | class TestGenDataset(object): 438 | """Test dataset generation function.""" 439 | 440 | def setup(self): 441 | # Seed. 442 | self.seed = 1337 443 | 444 | # Image length. 445 | self.imlen = 256 446 | 447 | # Image. 448 | self.img = Image.open( 449 | "LunarLROLrocKaguya_1180mperpix_downsamp.png").convert("L") 450 | self.imgsize = self.img.size 451 | 452 | # Crater catalogue. 453 | self.craters = igen.ReadLROCHeadCombinedCraterCSV( 454 | filelroc="../catalogues/LROCCraters.csv", 455 | filehead="../catalogues/HeadCraters.csv", 456 | sortlat=True) 457 | 458 | # Long/lat limits 459 | self.cdim = [-180., 180., -60., 60.] 460 | 461 | # Coordinate systems. 462 | self.iglobe = ccrs.Globe(semimajor_axis=1737400, 463 | semiminor_axis=1737400, 464 | ellipse=None) 465 | 466 | @pytest.mark.parametrize("ringwidth", (1, 2)) 467 | def test_gendataset(self, tmpdir, ringwidth): 468 | amt = 10 469 | zeropad = 2 470 | outhead = str(tmpdir.join('gentest')) 471 | 472 | igen.GenDataset(self.img, self.craters, outhead, 473 | rawlen_range=[300, 1000], rawlen_dist='log', 474 | ilen=self.imlen, tglen=self.imlen, cdim=self.cdim, 475 | minpix=1, ringwidth=ringwidth, amt=amt, istart=0, 476 | seed=self.seed) 477 | 478 | imgs_h5 = h5py.File(outhead + '_images.hdf5', 'r') 479 | craters_h5 = pd.HDFStore(outhead + '_craters.hdf5', 'r') 480 | 481 | for i in range(amt): 482 | # Find image number. 483 | img_number = "img_{i:0{zp}d}".format(i=i, zp=zeropad) 484 | 485 | # Load box. 486 | box = np.array(imgs_h5['pix_bounds'][img_number][...]) 487 | 488 | im = self.img.crop(box) 489 | im.load() 490 | 491 | # Obtain long/lat bounds for coordinate transform. 492 | ix = box[::2] 493 | iy = box[1::2] 494 | llong, llat = trf.pix2coord(ix, iy, self.cdim, list(self.img.size), 495 | origin='upper') 496 | llbd = np.r_[llong, llat[::-1]] 497 | 498 | # Downsample image. 499 | im = im.resize([self.imlen, self.imlen], resample=Image.NEAREST) 500 | 501 | # Remove all craters that are too small to be seen in image. 502 | ctr_sub = igen.ResampleCraters(self.craters, llbd, im.size[1], 503 | minpix=1) 504 | 505 | # Convert Plate Carree to Orthographic. 506 | [imgo, ctr_xy, distortion_coefficient, clonglat_xy] = ( 507 | igen.PlateCarree_to_Orthographic( 508 | im, llbd, ctr_sub, iglobe=self.iglobe, ctr_sub=True, 509 | slivercut=0.5)) 510 | imgo_arr = np.asanyarray(imgo) 511 | 512 | # Make target mask. 513 | tgt = np.asanyarray(imgo.resize((self.imlen, self.imlen), 514 | resample=Image.BILINEAR)) 515 | mask = igen.make_mask(ctr_xy, tgt, binary=True, rings=True, 516 | ringwidth=ringwidth, truncate=True) 517 | 518 | assert np.all(imgo_arr == imgs_h5['input_images'][i, ...]) 519 | assert np.all(mask == imgs_h5['target_masks'][i, ...]) 520 | assert np.all(llbd == imgs_h5['longlat_bounds'][img_number][...]) 521 | assert (distortion_coefficient == 522 | imgs_h5['pix_distortion_coefficient'][img_number][0]) 523 | assert np.all(clonglat_xy[["x", "y"]] == 524 | imgs_h5['cll_xy'][img_number][...]) 525 | assert np.all(ctr_xy == craters_h5[img_number]) 526 | 527 | imgs_h5.close() 528 | craters_h5.close() 529 | -------------------------------------------------------------------------------- /tests/test_model_train.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | import numpy as np 3 | from keras import backend as K 4 | import sys 5 | sys.path.append('../') 6 | import model_train as mt 7 | 8 | 9 | class TestModelTrain(): 10 | """Tests model building.""" 11 | 12 | def test_build_model(self): 13 | dim = 256 14 | FL = 3 15 | learn_rate = 0.0001 16 | n_filters = 112 17 | init = 'he_normal' 18 | lmbda = 1e-06 19 | drop = 0.15 20 | 21 | model = mt.build_model(dim, learn_rate, lmbda, drop, FL, init, 22 | n_filters) 23 | 24 | # Following https://stackoverflow.com/questions/45046525/keras-number-of-trainable-parameters-in-model 25 | trainable_count = int(np.sum([K.count_params(p) for p in 26 | set(model.trainable_weights)])) 27 | non_trainable_count = int(np.sum([K.count_params(p) for p in 28 | set(model.non_trainable_weights)])) 29 | assert trainable_count + non_trainable_count == 10278017 30 | assert trainable_count == 10278017 31 | assert non_trainable_count == 0 32 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | import numpy as np 3 | import h5py 4 | import pytest 5 | import sys 6 | sys.path.append('../') 7 | import utils.template_match_target as tmt 8 | import utils.transform as trf 9 | 10 | 11 | class TestCraterExtraction(object): 12 | def setup(self): 13 | sample = h5py.File('sample_template_match.hdf5', 'r') 14 | csv = sample['csv'][...].T 15 | self.pred = sample['pred'][...] 16 | # self.coordcsv = np.array((csv[0], csv[1], csv[2] / 2.)).T 17 | self.pixcsv = np.array((csv[3], csv[4], csv[5] / 2.)).T 18 | sample.close() 19 | 20 | def test_extract(self): 21 | (N_match, N_csv, N_detect, maxr, 22 | elo, ela, er, csv_duplicates) = tmt.template_match_t2c( 23 | self.pred, self.pixcsv, minrad=8, maxrad=11) 24 | assert N_match == 1 25 | assert elo < 0.1 26 | assert ela < 0.1 27 | assert er < 0.1 28 | 29 | 30 | class TestCoordinateTransforms(object): 31 | """Tests pix2coord and coord2pix.""" 32 | 33 | def setup(self): 34 | np.random.seed(9590) 35 | origin = np.random.uniform(-30, 30, 1000) 36 | extent = np.random.uniform(0, 45, 1000) 37 | self.cdim = [origin[0], origin[0] + extent[0], 38 | origin[1], origin[1] + extent[1]] 39 | self.imgdim = np.random.randint(100, high=200, size=1000) 40 | 41 | self.cx = np.array( 42 | [self.cdim[1], np.random.uniform(self.cdim[0] + 1, self.cdim[1])]) 43 | self.cy = np.array( 44 | [self.cdim[3], np.random.uniform(self.cdim[2] + 1, self.cdim[3])]) 45 | 46 | @pytest.mark.parametrize('origin', ('lower', 'upper')) 47 | def test_coord2pix(self, origin): 48 | x_gt = (self.imgdim[0] * 49 | (self.cx - self.cdim[0]) / (self.cdim[1] - self.cdim[0])) 50 | y_gt = (self.imgdim[1] * 51 | (self.cy - self.cdim[2]) / (self.cdim[3] - self.cdim[2])) 52 | yi_gt = (self.imgdim[1] * 53 | (self.cdim[3] - self.cy) / (self.cdim[3] - self.cdim[2])) 54 | 55 | x, y = trf.coord2pix(self.cx, self.cy, self.cdim, 56 | self.imgdim, origin=origin) 57 | if origin == "upper": 58 | y_gt_curr = yi_gt 59 | else: 60 | y_gt_curr = y_gt 61 | xy = np.r_[x, y] 62 | xy_gt = np.r_[x_gt, y_gt_curr] 63 | assert np.all(np.isclose(xy, xy_gt, rtol=1e-7, atol=1e-10)) 64 | 65 | @pytest.mark.parametrize('origin', ('lower', 'upper')) 66 | def test_pix2coord(self, origin): 67 | x, y = trf.coord2pix(self.cx, self.cy, self.cdim, 68 | self.imgdim, origin=origin) 69 | cx, cy = trf.pix2coord(x, y, self.cdim, self.imgdim, 70 | origin=origin) 71 | cxy = np.r_[cx, cy] 72 | cxy_gt = np.r_[self.cx, self.cy] 73 | assert np.all(np.isclose(cxy, cxy_gt, rtol=1e-7, atol=1e-10)) 74 | 75 | @pytest.mark.parametrize("imgheight, latextent, dc", [ 76 | (1500., 180., 0.5), 77 | (312., 17.1, 0.7), 78 | (1138., 15.3, 0.931), 79 | (6500., 34.5, 0.878), 80 | (3317., 22.8, 0.946), 81 | (2407., 45.9832, 0.7324809721)]) 82 | def test_km2pix(self, imgheight, latextent, dc): 83 | mypixperkm = (180. / (np.pi * 1737.4)) * (imgheight * dc / latextent) 84 | pixperkm = trf.km2pix(imgheight, latextent, dc=dc, a=1737.4) 85 | assert np.isclose(mypixperkm, pixperkm, rtol=1e-10, atol=0.) 86 | # degperpix used in get_unique_craters. 87 | degperpix = (180. / (np.pi * 1737.4)) / pixperkm 88 | mydegperpix = latextent / imgheight / dc 89 | assert np.isclose(degperpix, mydegperpix, rtol=1e-10, atol=0.) 90 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silburt/DeepMoon/0bccc9790bbf02b3c1df33bd0f452d87b136dc9d/utils/__init__.py -------------------------------------------------------------------------------- /utils/processing.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def preprocess(Data, dim=256, low=0.1, hi=1.0): 4 | """Normalize and rescale (and optionally invert) images. 5 | 6 | Parameters 7 | ---------- 8 | Data : hdf5 9 | Data array. 10 | dim : integer, optional 11 | Dimensions of images, assumes square. 12 | low : float, optional 13 | Minimum rescale value. Default is 0.1 since background pixels are 0. 14 | hi : float, optional 15 | Maximum rescale value. 16 | """ 17 | for key in Data: 18 | Data[key][0] = Data[key][0].reshape(len(Data[key][0]), dim, dim, 1) 19 | for i, img in enumerate(Data[key][0]): 20 | img = img / 255. 21 | # img[img > 0.] = 1. - img[img > 0.] #inv color 22 | minn, maxx = np.min(img[img > 0]), np.max(img[img > 0]) 23 | img[img > 0] = low + (img[img > 0] - minn) * (hi - low) / (maxx - minn) 24 | Data[key][0][i] = img 25 | 26 | def get_id(i, zeropad=5): 27 | """Properly indexes hdf5 files. 28 | 29 | Parameters 30 | ---------- 31 | i : int 32 | Image number to be indexed. 33 | zeropad : integer, optional 34 | Number of zeros to pad string. 35 | 36 | Returns 37 | ------- 38 | String of hdf5 index. 39 | """ 40 | return 'img_{i:0{zp}d}'.format(i=i, zp=zeropad) 41 | -------------------------------------------------------------------------------- /utils/template_match_target.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from skimage.feature import match_template 3 | import cv2 4 | 5 | ##################################### 6 | """ 7 | Tuned Crater Detection Hyperparameters 8 | -------------------------------------- 9 | minrad, maxrad : ints 10 | radius range in match_template to search over. 11 | longlat_thresh2, rad_thresh : floats 12 | if ((x1-x2)^2 + (y1-y2)^2) / min(r1,r2)^2 < longlat_thresh2 and 13 | abs(r1-r2) / min(r1,r2) < rad_thresh, remove (x2,y2,r2) circle (it is 14 | a duplicate of another crater candidate). In addition, when matching 15 | CNN-detected rings to corresponding csvs (i.e. template_match_t2c), 16 | the same criteria is used to determine a match. 17 | template_thresh : float 18 | 0-1 range. If match_template probability > template_thresh, count as 19 | detection. 20 | target_thresh : float 21 | 0-1 range. target[target > target_thresh] = 1, otherwise 0 22 | """ 23 | minrad_ = 5 24 | maxrad_ = 40 25 | longlat_thresh2_ = 1.8 26 | rad_thresh_ = 1.0 27 | template_thresh_ = 0.5 28 | target_thresh_ = 0.1 29 | 30 | ##################################### 31 | def template_match_t(target, minrad=minrad_, maxrad=maxrad_, 32 | longlat_thresh2=longlat_thresh2_, rad_thresh=rad_thresh_, 33 | template_thresh=template_thresh_, 34 | target_thresh=target_thresh_): 35 | """Extracts crater coordinates (in pixels) from a CNN-predicted target by 36 | iteratively sliding rings through the image via match_template from 37 | scikit-image. 38 | 39 | Parameters 40 | ---------- 41 | target : array 42 | CNN-predicted target. 43 | minrad : integer 44 | Minimum ring radius to search target over. 45 | maxrad : integer 46 | Maximum ring radius to search target over. 47 | longlat_thresh2 : float 48 | Minimum squared longitude/latitude difference between craters to be 49 | considered distinct detections. 50 | rad_thresh : float 51 | Minimum fractional radius difference between craters to be considered 52 | distinct detections. 53 | template_thresh : float 54 | Minimum match_template correlation coefficient to count as a detected 55 | crater. 56 | target_thresh : float 57 | Value between 0-1. All pixels > target_thresh are set to 1, and 58 | otherwise set to 0. 59 | 60 | Returns 61 | ------- 62 | coords : array 63 | Pixel coordinates of successfully detected craters in predicted target. 64 | """ 65 | 66 | # thickness of rings for template match 67 | rw = 2 68 | 69 | # threshold target 70 | target[target >= target_thresh] = 1 71 | target[target < target_thresh] = 0 72 | 73 | radii = np.arange(minrad, maxrad + 1, 1, dtype=int) 74 | coords = [] # coordinates extracted from template matching 75 | corr = [] # correlation coefficient for coordinates set 76 | for r in radii: 77 | # template 78 | n = 2 * (r + rw + 1) 79 | template = np.zeros((n, n)) 80 | cv2.circle(template, (r + rw + 1, r + rw + 1), r, 1, rw) 81 | 82 | # template match - result is nxn array of probabilities 83 | result = match_template(target, template, pad_input=True) 84 | index_r = np.where(result > template_thresh) 85 | coords_r = np.asarray(list(zip(*index_r))) 86 | corr_r = np.asarray(result[index_r]) 87 | 88 | # store x,y,r 89 | if len(coords_r) > 0: 90 | for c in coords_r: 91 | coords.append([c[1], c[0], r]) 92 | for l in corr_r: 93 | corr.append(np.abs(l)) 94 | 95 | # remove duplicates from template matching at neighboring radii/locations 96 | coords, corr = np.asarray(coords), np.asarray(corr) 97 | i, N = 0, len(coords) 98 | while i < N: 99 | Long, Lat, Rad = coords.T 100 | lo, la, r = coords[i] 101 | minr = np.minimum(r, Rad) 102 | 103 | dL = ((Long - lo)**2 + (Lat - la)**2) / minr**2 104 | dR = abs(Rad - r) / minr 105 | index = (dR < rad_thresh) & (dL < longlat_thresh2) 106 | if len(np.where(index == True)[0]) > 1: 107 | # replace current coord with max match probability coord in 108 | # duplicate list 109 | coords_i = coords[np.where(index == True)] 110 | corr_i = corr[np.where(index == True)] 111 | coords[i] = coords_i[corr_i == np.max(corr_i)][0] 112 | index[i] = False 113 | coords = coords[np.where(index == False)] 114 | N, i = len(coords), i + 1 115 | 116 | return coords 117 | 118 | 119 | def template_match_t2c(target, csv_coords, minrad=minrad_, maxrad=maxrad_, 120 | longlat_thresh2=longlat_thresh2_, 121 | rad_thresh=rad_thresh_, template_thresh=template_thresh_, 122 | target_thresh=target_thresh_, rmv_oor_csvs=0): 123 | """Extracts crater coordinates (in pixels) from a CNN-predicted target and 124 | compares the resulting detections to the corresponding human-counted crater 125 | data. 126 | 127 | Parameters 128 | ---------- 129 | target : array 130 | CNN-predicted target. 131 | csv_coords : array 132 | Human-counted crater coordinates (in pixel units). 133 | minrad : integer 134 | Minimum ring radius to search target over. 135 | maxrad : integer 136 | Maximum ring radius to search target over. 137 | longlat_thresh2 : float 138 | Minimum squared longitude/latitude difference between craters to be 139 | considered distinct detections. 140 | rad_thresh : float 141 | Minimum fractional radius difference between craters to be considered 142 | distinct detections. 143 | template_thresh : float 144 | Minimum match_template correlation coefficient to count as a detected 145 | crater. 146 | target_thresh : float 147 | Value between 0-1. All pixels > target_thresh are set to 1, and 148 | otherwise set to 0. 149 | rmv_oor_csvs : boolean, flag 150 | If set to 1, remove craters from the csv that are outside your 151 | detectable range. 152 | 153 | Returns 154 | ------- 155 | N_match : int 156 | Number of crater matches between your target and csv. 157 | N_csv : int 158 | Number of csv entries 159 | N_detect : int 160 | Total number of detected craters from target. 161 | maxr : int 162 | Radius of largest crater extracted from target. 163 | err_lo : float 164 | Mean longitude error between detected craters and csvs. 165 | err_la : float 166 | Mean latitude error between detected craters and csvs. 167 | err_r : float 168 | Mean radius error between detected craters and csvs. 169 | frac_dupes : float 170 | Fraction of craters with multiple csv matches. 171 | """ 172 | # get coordinates from template matching 173 | templ_coords = template_match_t(target, minrad, maxrad, longlat_thresh2, 174 | rad_thresh, template_thresh, target_thresh) 175 | 176 | # find max detected crater radius 177 | maxr = 0 178 | if len(templ_coords > 0): 179 | maxr = np.max(templ_coords.T[2]) 180 | 181 | # compare template-matched results to ground truth csv input data 182 | N_match = 0 183 | frac_dupes = 0 184 | err_lo, err_la, err_r = 0, 0, 0 185 | N_csv, N_detect = len(csv_coords), len(templ_coords) 186 | for lo, la, r in templ_coords: 187 | Long, Lat, Rad = csv_coords.T 188 | minr = np.minimum(r, Rad) 189 | 190 | dL = ((Long - lo)**2 + (Lat - la)**2) / minr**2 191 | dR = abs(Rad - r) / minr 192 | index = (dR < rad_thresh) & (dL < longlat_thresh2) 193 | index_True = np.where(index == True)[0] 194 | N = len(index_True) 195 | if N >= 1: 196 | Lo, La, R = csv_coords[index_True[0]].T 197 | meanr = (R + r) / 2. 198 | err_lo += abs(Lo - lo) / meanr 199 | err_la += abs(La - la) / meanr 200 | err_r += abs(R - r) / meanr 201 | if N > 1: # duplicate entries hurt recall 202 | frac_dupes += (N-1) / float(len(templ_coords)) 203 | N_match += min(1, N) 204 | # remove csv(s) so it can't be re-matched again 205 | csv_coords = csv_coords[np.where(index == False)] 206 | if len(csv_coords) == 0: 207 | break 208 | 209 | if rmv_oor_csvs == 1: 210 | upper = 15 211 | lower = minrad_ 212 | N_large_unmatched = len(np.where((csv_coords.T[2] > upper) | 213 | (csv_coords.T[2] < lower))[0]) 214 | if N_large_unmatched < N_csv: 215 | N_csv -= N_large_unmatched 216 | 217 | if N_match >= 1: 218 | err_lo = err_lo / N_match 219 | err_la = err_la / N_match 220 | err_r = err_r / N_match 221 | 222 | return N_match, N_csv, N_detect, maxr, err_lo, err_la, err_r, frac_dupes 223 | -------------------------------------------------------------------------------- /utils/transform.py: -------------------------------------------------------------------------------- 1 | """Coordinate Transform Functions 2 | 3 | Functions for coordinate transforms, used by input_data_gen.py functions. 4 | """ 5 | import numpy as np 6 | 7 | ########## Coordinates to pixels projections ########## 8 | 9 | def coord2pix(cx, cy, cdim, imgdim, origin="upper"): 10 | """Converts coordinate x/y to image pixel locations. 11 | 12 | Parameters 13 | ---------- 14 | cx : float or ndarray 15 | Coordinate x. 16 | cy : float or ndarray 17 | Coordinate y. 18 | cdim : list-like 19 | Coordinate limits (x_min, x_max, y_min, y_max) of image. 20 | imgdim : list, tuple or ndarray 21 | Length and height of image, in pixels. 22 | origin : 'upper' or 'lower', optional 23 | Based on imshow convention for displaying image y-axis. 'upper' means 24 | that [0, 0] is upper-left corner of image; 'lower' means it is 25 | bottom-left. 26 | 27 | Returns 28 | ------- 29 | x : float or ndarray 30 | Pixel x positions. 31 | y : float or ndarray 32 | Pixel y positions. 33 | """ 34 | 35 | x = imgdim[0] * (cx - cdim[0]) / (cdim[1] - cdim[0]) 36 | 37 | if origin == "lower": 38 | y = imgdim[1] * (cy - cdim[2]) / (cdim[3] - cdim[2]) 39 | else: 40 | y = imgdim[1] * (cdim[3] - cy) / (cdim[3] - cdim[2]) 41 | 42 | return x, y 43 | 44 | 45 | def pix2coord(x, y, cdim, imgdim, origin="upper"): 46 | """Converts image pixel locations to Plate Carree lat/long. Assumes 47 | central meridian is at 0 (so long in [-180, 180)). 48 | 49 | Parameters 50 | ---------- 51 | x : float or ndarray 52 | Pixel x positions. 53 | y : float or ndarray 54 | Pixel y positions. 55 | cdim : list-like 56 | Coordinate limits (x_min, x_max, y_min, y_max) of image. 57 | imgdim : list, tuple or ndarray 58 | Length and height of image, in pixels. 59 | origin : 'upper' or 'lower', optional 60 | Based on imshow convention for displaying image y-axis. 'upper' means 61 | that [0, 0] is upper-left corner of image; 'lower' means it is 62 | bottom-left. 63 | 64 | Returns 65 | ------- 66 | cx : float or ndarray 67 | Coordinate x. 68 | cy : float or ndarray 69 | Coordinate y. 70 | """ 71 | 72 | cx = (x / imgdim[0]) * (cdim[1] - cdim[0]) + cdim[0] 73 | 74 | if origin == "lower": 75 | cy = (y / imgdim[1]) * (cdim[3] - cdim[2]) + cdim[2] 76 | else: 77 | cy = cdim[3] - (y / imgdim[1]) * (cdim[3] - cdim[2]) 78 | 79 | return cx, cy 80 | 81 | ########## Metres to pixels conversion ########## 82 | 83 | def km2pix(imgheight, latextent, dc=1., a=1737.4): 84 | """Returns conversion from km to pixels (i.e. pix / km). 85 | 86 | Parameters 87 | ---------- 88 | imgheight : float 89 | Height of image in pixels. 90 | latextent : float 91 | Latitude extent of image in degrees. 92 | dc : float from 0 to 1, optional 93 | Scaling factor for distortions. 94 | a : float, optional 95 | World radius in km. Default is Moon (1737.4 km). 96 | 97 | Returns 98 | ------- 99 | km2pix : float 100 | Conversion factor pix/km 101 | """ 102 | return (180. / np.pi) * imgheight * dc / latextent / a 103 | --------------------------------------------------------------------------------