def links_to_geofile(links, dims, gt, crs, path_export): """ Saves the links of the network to a georeferencedshapefile or geojson. Computed link properties are saved as attributes when available. Note that the 'wid_pix' property, which stores the width at each pixel along the link, may be truncated depending on its length and the filetype. The filetype is specified by the export path. Parameters ---------- links : dict Network links and associated properties. dims : tuple (nrows, ncols) of the original mask from which links were derived. gt : tuple GDAL geotransform of the original mask from which links were derived. crs : pyrpoj.CRS CRS object specifying the coordinate reference system of the original mask from which links were derived. path_export : str Path, including extension, specifying where to save the links export. Returns ------- None. """ # Create line objects to write to shapefile all_links = [] for link in links['idx']: xy = np.unravel_index(link, dims) x, y = gu.xy_to_coords(xy[1], xy[0], gt) all_links.append(LineString(zip(x, y))) # Create GeoDataFrame for storing geometries and attributes gdf = gpd.GeoDataFrame(geometry=all_links) gdf.crs = crs # Store attributes as strings (numpy types give fiona trouble) dontstore = ['idx', 'n_networks'] storekeys = [k for k in links.keys() if k not in dontstore] storekeys = [k for k in storekeys if len(links[k]) == len(links['id'])] store_as_num = ['id', 'flux', 'logflux'] for k in storekeys: if k in store_as_num: gdf[k] = [c for c in links[k]] elif k == 'wid_pix': gdf[k] = [ str(c.tolist()).replace('[', '').replace(']', '') for c in links[k] ] else: gdf[k] = [ str(c).replace('[', '').replace(']', '') for c in links[k] ] # Write geodataframe to file gdf.to_file(path_export, driver=get_driver(path_export))
def nodes_to_shapefile(nodes, dims, gt, epsg, outpath): # Create point objects to write to shapefile if 'id' not in nodes.keys(): ids = list(range(0, len(nodes['idx']))) else: ids = nodes['id'] all_nodes = [] for node in nodes['idx']: pt = ogr.Geometry(type=ogr.wkbPoint) xy = np.unravel_index(node, dims) ll = gu.xy_to_coords(xy[1] + .5, xy[0] + .5, gt, inputEPSG=epsg, outputEPSG=epsg)[0] pt.AddPoint_2D(ll[1], ll[0]) all_nodes.append(pt) # Write the shapefile driver = ogr.GetDriverByName('ESRI Shapefile') datasource = driver.CreateDataSource(outpath) srs = osr.SpatialReference() srs.ImportFromEPSG(epsg) layer = datasource.CreateLayer("Nodes", srs, ogr.wkbPoint) defn = layer.GetLayerDefn() idField = ogr.FieldDefn('id', ogr.OFTInteger) connField = ogr.FieldDefn('conn', ogr.OFTString) layer.CreateField(idField) layer.CreateField(connField) for p, i in zip(all_nodes, ids): # Create a new feature (attribute and geometry) feat = ogr.Feature(defn) feat.SetField('id', int(i)) fieldstr = str(nodes['conn'][nodes['id'].index(i)]) fieldstr = fieldstr[1:-1] feat.SetField('conn', fieldstr) # Make a geometry geom = ogr.CreateGeometryFromWkb(p.ExportToWkb()) feat.SetGeometry(geom) layer.CreateFeature(feat) feat = geom = None # destroy these # Save and close everything datasource = layer = feat = geom = None
def compute_centerline(self): """ Computes the centerline of the holes-filled river binary image. """ logger.info('Computing centerline...') centerline_pix, valley_centerline_widths = ru.mask_to_centerline(self.Imask, self.exit_sides) self.max_valley_width_pixels = np.max(valley_centerline_widths) self.centerline = gu.xy_to_coords(centerline_pix[:,0], centerline_pix[:,1], self.gt) logger.info('centerline computation is done.')
def nodes_to_geofile(nodes, dims, gt, crs, path_export): """ Saves the nodes of the network to a georeferencedshapefile or geojson. Computed node properties are appended as attributes when available. The filetype is specified by the export path. Parameters ---------- nodes : dict Network nodes and associated properties. dims : tuple (nrows, ncols) of the original mask from which nodes were derived. gt : tuple GDAL geotransform of the original mask from which nodes were derived. crs : pyrpoj.CRS CRS object specifying the coordinate reference system of the original mask from which nodes were derived. path_export : str Path, including extension, where to save the nodes export. Returns ------- None. """ nodexy = np.unravel_index(nodes['idx'], dims) x, y = gu.xy_to_coords(nodexy[1], nodexy[0], gt) all_nodes = [Point(x, y) for x, y in zip(x, y)] # Create GeoDataFrame for storing geometries and attributes gdf = gpd.GeoDataFrame(geometry=all_nodes) gdf.crs = crs # Store attributes as strings (numpy types give fiona trouble) dontstore = ['idx'] storekeys = [ k for k in nodes.keys() if len(nodes[k]) == len(nodes['id']) and k not in dontstore ] store_as_num = ['id', 'idx', 'logflux', 'flux', 'outletflux'] for k in storekeys: if k in store_as_num: gdf[k] = [c for c in nodes[k]] else: gdf[k] = [ str(c).replace('[', '').replace(']', '') for c in nodes[k] ] # Write geodataframe to file gdf.to_file(path_export, driver=get_driver(path_export))
def compute_centerline(self): """ Computes the centerline of the holes-filled river binary image. """ if self.verbose is True: print('Computing centerline...', end='') centerline_pix, valley_centerline_widths = ru.mask_to_centerline( self.Imask, self.exit_sides) self.max_valley_width_pixels = np.max(valley_centerline_widths) self.centerline = gu.xy_to_coords(centerline_pix[:, 0], centerline_pix[:, 1], self.gt) if self.verbose is True: print('done.')
def links_to_shapefile(links, dims, gt, EPSG, outpath): # Create line objects to write to shapefile all_links = [] for link in links['idx']: line = ogr.Geometry(type=ogr.wkbLineString) for pix in link: xy = np.unravel_index(pix, dims) ll = gu.xy_to_coords(xy[1] + .5, xy[0] + .5, gt, inputEPSG=4326, outputEPSG=4326)[0] line.AddPoint_2D(ll[1], ll[0]) all_links.append(line) # Write the shapefile driver = ogr.GetDriverByName('ESRI Shapefile') datasource = driver.CreateDataSource(outpath) srs = osr.SpatialReference() srs.ImportFromEPSG(EPSG) layer = datasource.CreateLayer("Links", srs, ogr.wkbLineString) defn = layer.GetLayerDefn() idField = ogr.FieldDefn('id', ogr.OFTInteger) layer.CreateField(idField) usField = ogr.FieldDefn('us node', ogr.OFTInteger) dsField = ogr.FieldDefn('ds node', ogr.OFTInteger) layer.CreateField(usField) layer.CreateField(dsField) # Include other attributes if available if 'len' in links.keys(): lenField = ogr.FieldDefn('length', ogr.OFTReal) layer.CreateField(lenField) if 'wid' in links.keys(): lenField = ogr.FieldDefn('width', ogr.OFTReal) layer.CreateField(lenField) if 'len_adj' in links.keys(): widField = ogr.FieldDefn('len_adj', ogr.OFTReal) layer.CreateField(widField) if 'wid_adj' in links.keys(): widField = ogr.FieldDefn('width_adj', ogr.OFTReal) layer.CreateField(widField) usnodes = [c[0] for c in links['conn']] dsnodes = [c[1] for c in links['conn']] for i, p in enumerate(all_links): # Create a new feature (attribute and geometry) feat = ogr.Feature(defn) feat.SetField('id', int(links['id'][i])) # Set upstream and downstream node attributes fieldstr = str(usnodes[i]) feat.SetField('us node', fieldstr) fieldstr = str(dsnodes[i]) feat.SetField('ds node', fieldstr) # Set other attributes if available if 'len' in links.keys(): fieldstr = str(links['len'][i]) feat.SetField('length', fieldstr) if 'len_adj' in links.keys(): fieldstr = str(links['len_adj'][i]) feat.SetField('len_adj', fieldstr) if 'wid' in links.keys(): fieldstr = str(links['wid'][i]) feat.SetField('width', fieldstr) if 'wid_adj' in links.keys(): fieldstr = str(links['wid_adj'][i]) feat.SetField('wid_adj', fieldstr) # Make a geometry geom = ogr.CreateGeometryFromWkb(p.ExportToWkb()) feat.SetGeometry(geom) layer.CreateFeature(feat) feat = geom = None # destroy these # Save and close everything datasource = layer = feat = geom = None
def get_island_properties(Imask, pixlen, pixarea, crs, gt, props, connectivity=2): """Get island properties.""" # maxwidth is an additional property if 'maxwidth' in props: props.remove('maxwidth') do_maxwidth = True else: do_maxwidth = False # Need perimeter to make island polygons if 'perimeter' not in props: props.append('perimeter') # Pad by one pixel to help identify and remove the outer portion of # the channel netowrk Imaskpad = np.array(np.pad(Imask, 1, mode='constant'), dtype=bool) Imp_invert = np.invert(Imaskpad) rp_islands, Ilabeled = iu.regionprops(Imp_invert, props=props, connectivity=connectivity) # Make polygons of the island perimeters # Also get ids to match the labeled image pgons = [] ids = [] for ip, p in enumerate(rp_islands['perimeter']): ids.append(Ilabeled[p[0][0], p[0][1]]) # Store the index p = np.vstack((p, p[0])) # Close the polygon # Adjust for the single-pixel padding we added to the image cr = gu.xy_to_coords(p[:, 1] - 1, p[:, 0] - 1, gt) # Special cases: where the island is two pixels or less, we use the # corner coordinates rather than the center coordinates to define # the polygon. if len(cr[0]) <= 2: pixgon = [pixagon(cc, rc, pixlen) for cc, rc, in zip(cr[0], cr[1])] if len(pixgon) > 1: pixgon = cascaded_union(pixgon) pgons.append(pixgon) else: pgons.append(Polygon(zip(cr[0], cr[1]))) # Do maximum width if requested if do_maxwidth: Idist = distance_transform_edt(Imp_invert) maxwids = [] for i in ids: maxwids.append(np.max(Idist[Ilabeled == i]) * 2 * pixlen) # Convert requested properties to proper units if 'area' in props: rp_islands['area'] = rp_islands['area'] * pixarea if 'major_axis_length' in props: rp_islands[ 'major_axis_length'] = rp_islands['major_axis_length'] * pixlen if 'minor_axis_length' in props: rp_islands[ 'minor_axis_length'] = rp_islands['minor_axis_length'] * pixlen if 'perim_len' in props: rp_islands['perim_len'] = rp_islands['perim_len'] * pixlen if 'convex_area' in props: rp_islands['convex_area'] = rp_islands['convex_area'] * pixarea # Need to change 'area' key as it's a function in geopandas if 'area' in rp_islands: rp_islands['Area'] = rp_islands.pop('area') # Create islands geodataframe gdf_dict = { k: rp_islands[k] for k in rp_islands if k not in ['coords', 'perimeter', 'centroid'] } gdf_dict['geometry'] = pgons gdf_dict['id'] = ids if do_maxwidth: gdf_dict['maxwid'] = maxwids gdf = gpd.GeoDataFrame(gdf_dict) gdf.crs = crs # Identify and remove the border blob border_id = Ilabeled[0][0] Ilabeled[Ilabeled == border_id] = 0 gdf = gdf[gdf.id.values != border_id] # Put 'id' column in front colnames = [k for k in gdf.keys()] colnames.remove('id') colnames.insert(0, 'id') gdf = gdf[colnames] return gdf, Ilabeled[1:-1, 1:-1]
def dir_centerline(links, nodes, meshpolys, meshlines, Imask, gt, pixlen): """ Guess flow directions of links in a braided river channel. Guesses the flow direction of links in a braided river channel network by exploiting a "valleyline" centerline. Two metrics are computed to help guess the correct direction. The first is the number of centerline transects (meshlines) that the link crosses. The second is the local angle of the centerline compared to the link's angle. These metrics are appended to the links dictionary as links['cldist'] and links['clangs']. Parameters ---------- links : dict Network links and associated properties. nodes : dict Network nodes and associated properties. meshpolys : list List of shapely.geometry.Polygons that define the valleyline mesh. meshlines : list List of shapely.geometry.LineStrings that define the valleyline mesh. Imask : np.array Binary mask of the network. gt : tuple gdal-type GeoTransform of the original binary mask. pixlen : float Length resolution of each pixel. Returns ------- links : dict Network links and associated properties with 'cldists' and 'clangs' attributes appended. """ # alg = 20 alg = dy.algmap('cl_dist_guess') # Create geodataframes for intersecting meshpolys with nodes mp_gdf = gpd.GeoDataFrame(geometry=[Polygon(mp) for mp in meshpolys]) rc = np.unravel_index(nodes['idx'], Imask.shape) nodecoords = gu.xy_to_coords(rc[1], rc[0], gt) node_gdf = gpd.GeoDataFrame( geometry=[Point(x, y) for x, y in zip(nodecoords[0], nodecoords[1])], index=nodes['id']) # Determine which meshpoly each node lies within intersect = gpd.sjoin(node_gdf, mp_gdf, op='intersects', rsuffix='right') # Compute guess and certainty, where certainty is how many transects apart # the link endpoints are (longer=more certain) cldists = np.zeros((len(links['id']), 1)) for i, lconn in enumerate(links['conn']): try: first = intersect.loc[lconn[0]].index_right second = intersect.loc[lconn[1]].index_right cldists[i] = second - first except KeyError: pass for i, c in enumerate(cldists): if c != 0: if c > 0: links['guess'][i].append(links['conn'][i][0]) links['guess_alg'][i].append(alg) elif c < 0: links['guess'][i].append(links['conn'][i][-1]) links['guess_alg'][i].append(alg) # Save the distances for certainty links['cldists'] = np.abs(cldists) # Compute guesses based on how the link aligns with the local centerline # direction # alg = 21 alg = dy.algmap('cl_ang_guess') clangs = np.ones((len(links['id']), 1)) * np.nan for i, (lconn, lidx) in enumerate(zip(links['conn'], links['idx'])): # Get coordinates of link endpoints rc = np.unravel_index([lidx[0], lidx[-1]], Imask.shape) try: # Try is because some points may not lie within the mesh polygons # Get coordinates of centerline midpoints first = intersect.loc[lconn[0]].index_right second = intersect.loc[lconn[1]].index_right if first > second: first, second = second, first first_mp = np.mean(np.array(meshlines[first]), axis=0) # midpoint second_mp = np.mean(np.array(meshlines[second + 1]), axis=0) # midpoint except KeyError: continue # Centerline vector cl_vec = second_mp - first_mp cl_vec = cl_vec / np.sqrt(np.sum(cl_vec**2)) # Link vectors - as-is and flipped (reversed) link_vec = dy.get_link_vector(links, nodes, links['id'][i], Imask.shape, pixlen=pixlen) link_vec_rev = -link_vec # Compute interior radians between centerline vector and link vector # (then again with link vector flipped) lva = np.math.atan2(np.linalg.det([cl_vec, link_vec]), np.dot(cl_vec, link_vec)) lvar = np.math.atan2(np.linalg.det([cl_vec, link_vec_rev]), np.dot(cl_vec, link_vec_rev)) # Save the maximum angle clangs[i] = np.min(np.abs([lva, lvar])) # Make a guess; smaller interior angle (i.e. link direction that aligns # best with local centerline direction) guesses the link orientation if np.abs(lvar) < np.abs(lva): links['guess'][i].append(links['conn'][i][1]) links['guess_alg'][i].append(alg) else: links['guess'][i].append(links['conn'][i][0]) links['guess_alg'][i].append(alg) links['clangs'] = clangs return links