Section 1. Comparison of West Midtown and Hapeville Census Tracts
For this assignment, I chose two Atlanta neighborhoods experiencing growth and development in interesting ways as case studies for semantic segmentation. West Midtown is the extension of the burgeoning Midtown neighborhood, the cultural center of the Atlanta metro region, while Hapeville sits at a center point of the Aerotropolis region around Hartsfield-Jackson Airport.
My theory is that West Midtown, with its proximity to Georgia Tech and recent high density development, is a more walkable neighborhood than Hapeville is, despite its location as an international entry point to the Atlanta metropolitan region. This is due to West Midtown’s density of recent housing projects, many of which cater to Tech students. Hapeville, on the other hand, historically hasn’t been a first stop for travelers to ATL’s airport, and I hypothesize that most tend to drive through it rather than spend time walking in it.
After choosing census tracts for both of these neighborhoods, I prepared Open Street map data, downloaded GSV images, and applied a computer vision technique (semantic segmentation) in Google Colab.
Finally, I will summarize and analyze the output and provide ymy findings. After applying computer vision to the images, the number of pixels in each image represented 150 categories in my data. I focused on on the following categories in my analysis: building, sky, tree, road, and sidewalk. I created interactive maps to visualize the spatial distribution of different objects, compared the mean of each category between the two Census Tracts and then drew boxplots to compare the distributions.
Section 2. OSM, GSV, and Computer Vision.
library(tidyverse)
library(tidycensus)
library(osmdata)
library(sfnetworks)
library(units)
library(sf)
library(tidygraph)
library(tmap)
library(here)
library(tigris)
library(plotly)
library(tidytransit)
library(leafsync)
library(magrittr)
library(leaflet)
library(knitr)
library(scales)
ttm()
Step 1. Get OSM data and clean it.
The getbb()
function, which we used in the class to
download OSM data, isn’t suitable for downloading just two Census
Tracts. I used an alternative method.
- Using tidycensus package, download the Census Tract polygon for Fulton and DeKalb counties.
- Extract two Census Tracts, each of which will be most walkable and least walkable Census Tracts.
- Using their bounding boxes, get OSM data.
- Convert them into sfnetwork object and clean it.
# TASK ////////////////////////////////////////////////////////////////////////
# 1. Set up your api key here
# Set Census API
tidycensus::census_api_key(Sys.getenv("CENSUS_API_KEY"))
# //TASK //////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
# Download Census Tract polygon for Fulton and DeKalb
tract <- get_acs("tract",
variables = c('tot_pop' = 'B01001_001'),
year = 2022,
state = "GA",
county = c("Fulton", "DeKalb"),
geometry = TRUE)
## Getting data from the 2018-2022 5-year ACS
## Downloading feature geometry from the Census website. To cache shapefiles for use in future sessions, set `options(tigris_use_cache = TRUE)`.
## | | | 0% | |= | 1% | |= | 2% | |== | 2% | |=== | 4% | |=== | 5% | |==== | 6% | |===== | 7% | |======= | 10% | |======== | 12% | |========= | 13% | |========== | 14% | |============ | 17% | |============= | 19% | |=============== | 21% | |================ | 22% | |================ | 23% | |=================== | 26% | |==================== | 28% | |==================== | 29% | |===================== | 30% | |======================= | 32% | |========================= | 35% | |========================== | 37% | |=========================== | 38% | |=========================== | 39% | |============================ | 40% | |============================== | 43% | |=============================== | 45% | |==================================== | 51% | |===================================== | 52% | |===================================== | 53% | |======================================== | 57% | |========================================= | 59% | |========================================== | 60% | |=============================================== | 68% | |================================================= | 71% | |======================================================= | 79% | |======================================================== | 80% | |========================================================== | 82% | |============================================================== | 88% | |============================================================== | 89% | |================================================================ | 92% | |================================================================== | 94% | |=================================================================== | 95% | |==================================================================== | 97% | |======================================================================| 99% | |======================================================================| 100%
# =========== NO MODIFY ZONE ENDS HERE ========================================
# TASK ////////////////////////////////////////////////////////////////////////
# The purpose of this TASK is to create one bounding box for walkable Census Tract and another bounding box for unwalkable Census Tract.
# As long as you generate what's needed for the subsequent codes, you are good. The numbered list of tasks below is to provide some hints.
# 1. Write the GEOID of walkable & unwalkable Census Tracts. e.g., tr1_ID <- c("13121001205", "13121001206")
# 2. Extract the selected Census Tracts using tr1_ID & tr2_ID
# 3. Create their bounding boxes using st_bbox(), and
# 4. assign them to tract_1_bb and tract_1_bb, respectively.
# For the walkable Census Tract(s)
# 1.
tr1_ID <- c("13121000601")
# 2~4
tract_1_bb <- tract %>%
filter(GEOID %in% tr1_ID) %>%
st_bbox()
# For the unwalkable Census Tract(s)
# 1.
tr2_ID <- c("13121010801")
# 2~4
tract_2_bb <- tract %>%
filter(GEOID %in% tr2_ID) %>%
st_bbox()
# //TASK //////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
# Get OSM data for the two bounding box
osm_1 <- opq(bbox = tract_1_bb) %>%
add_osm_feature(key = 'highway',
value = c("motorway", "trunk", "primary",
"secondary", "tertiary", "unclassified",
"residential")) %>%
osmdata_sf() %>%
osm_poly2line()
osm_2 <- opq(bbox = tract_2_bb) %>%
add_osm_feature(key = 'highway',
value = c("motorway", "trunk", "primary",
"secondary", "tertiary", "unclassified",
"residential")) %>%
osmdata_sf() %>%
osm_poly2line()
# =========== NO MODIFY ZONE ENDS HERE ========================================
# TASK ////////////////////////////////////////////////////////////////////////
# 1. Convert osm_1 and osm_2 to sfnetworks objects (set directed = FALSE)
# 2. Clean the network by (1) deleting parallel lines and loops, (2) create missing nodes, and (3) remove pseudo nodes,
# 3. Add a new column named length using edge_length() function.
colnames(osm_1$osm_lines)
## [1] "osm_id" "name"
## [3] "access" "addr:street"
## [5] "attribution" "bicycle"
## [7] "bridge" "change:lanes"
## [9] "cycleway" "cycleway:both"
## [11] "cycleway:both:buffer" "cycleway:left"
## [13] "cycleway:right" "cycleway:right:buffer"
## [15] "fee" "gatech:PRKG_NUM"
## [17] "HFCS" "highway"
## [19] "lane_markings" "lanes"
## [21] "lanes:backward" "lanes:forward"
## [23] "layer" "lit"
## [25] "maxspeed" "maxspeed:type"
## [27] "name_1" "note"
## [29] "oneway" "oneway:bicycle"
## [31] "parking:lane:both" "parking:lane:left"
## [33] "parking:lane:right" "placement"
## [35] "rcn_ref" "ref"
## [37] "short_name" "shoulder"
## [39] "sidewalk" "smoothness"
## [41] "source" "source:maxspeed"
## [43] "start_date" "surface"
## [45] "tiger:cfcc" "tiger:county"
## [47] "tiger:name_base" "tiger:name_base_1"
## [49] "tiger:name_base_2" "tiger:name_direction_prefix"
## [51] "tiger:name_direction_suffix" "tiger:name_direction_suffix_1"
## [53] "tiger:name_type" "tiger:name_type_1"
## [55] "tiger:reviewed" "tiger:zip_left"
## [57] "tiger:zip_left_1" "tiger:zip_right"
## [59] "turn:lanes" "turn:lanes:backward"
## [61] "turn:lanes:forward" "vehicle"
## [63] "geometry"
# For the walkable Census Tract network
net1 <- osm_1$osm_lines %>%
# Drop redundant columns
select(osm_id, highway) %>%
sfnetworks::as_sfnetwork(directed = FALSE) %>%
activate("edges") %>%
filter(!edge_is_multiple()) %>%
filter(!edge_is_loop()) %>%
convert(., sfnetworks::to_spatial_subdivision) %>%
convert(., sfnetworks::to_spatial_smooth) %>%
mutate(length = edge_length())
## Warning: to_spatial_subdivision assumes attributes are constant over geometries
# For the unwalkable Census Tract network
net2 <- osm_2$osm_lines %>%
# Drop redundant columns
select(osm_id, highway) %>%
sfnetworks::as_sfnetwork(directed = FALSE) %>%
activate("edges") %>%
filter(!edge_is_multiple()) %>%
filter(!edge_is_loop()) %>%
convert(., sfnetworks::to_spatial_subdivision) %>%
convert(., sfnetworks::to_spatial_smooth) %>%
mutate(length = edge_length())
## Warning: to_spatial_subdivision assumes attributes are constant over geometries
# //TASK //////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
# OSM for the walkable part
edges_1 <- net1 %>%
# Extract 'edges'
st_as_sf("edges") %>%
# Drop segments that are too short (100m)
mutate(length = as.vector(length)) %>%
filter(length > 100) %>%
# Add a unique ID for each edge
mutate(edge_id = seq(1,nrow(.)),
is_walkable = "walkable")
# OSM for the unwalkable part
edges_2 <- net2 %>%
# Extract 'edges'
st_as_sf("edges") %>%
# Drop segments that are too short (100m)
mutate(length = as.vector(length)) %>%
filter(length > 100) %>%
# Add a unique ID for each edge
mutate(edge_id = seq(1,nrow(.)),
is_walkable = "unwalkable")
# Merge the two
edges <- bind_rows(edges_1, edges_2)
# =========== NO MODIFY ZONE ENDS HERE ========================================
Step 2. Define getAzimuth()
function.
getAzimuth <- function(line){
# This function takes one edge (i.e., a street segment) as an input and
# outputs a data frame with four points (start, mid1, mid2, and end) and their azimuth.
# TASK ////////////////////////////////////////////////////////////////////////
# 1. From `line` object, extract the coordinates using st_coordinates() and extract the first two rows.
start_p <- line %>%
st_coordinates() %>%
.[1:2, 1:2]
# 2. Use atan2() function to calculate the azimuth in degree.
# Make sure to adjust the value such that 0 is north, 90 is east, 180 is south, and 270 is west.
start_azi <- atan2(start_p[1,"X"] - start_p[2, "X"],
start_p[1,"Y"] - start_p[2, "Y"])*180/pi
# //TASK //////////////////////////////////////////////////////////////////////
# TASK ////////////////////////////////////////////////////////////////////////
# Repeat what you did above, but for last two rows (instead of the first two rows).
# Remember to flip the azimuth so that the camera would be looking at the street that's being measured
end_p <- line %>%
st_coordinates() %>%
.[(nrow(.)-1):nrow(.),1:2]
end_azi <- atan2(end_p[1,"X"] - end_p[2, "X"],
end_p[1,"Y"] - end_p[2, "Y"])*180/pi
# Flip the azimuth so that the camera would be looking back
end_azi <- if (end_azi < 180) {end_azi + 180} else {end_azi - 180}
# //TASK //////////////////////////////////////////////////////////////////////
# TASK ////////////////////////////////////////////////////////////////////////
# 1. From `line` object, use st_line_sample() function to generate points at 0.45 and 0.55 locations. These two points will be used to calculate the azimuth.
# 2. Use st_case() function to convert 'MULTIPOINT' object to 'POINT' object.
# 3. Extract coordinates using st_coordinates().
# 4. Use atan2() functino to Calculate azimuth.
# 5. Use st_line_sample() again to generate a point at 0.5 location and get its coordinates. This point will be the location at which GSV image will be downloaded.
mid_p2 <- line %>%
st_line_sample(sample = c(0.45, 0.55)) %>%
st_cast("POINT") %>%
st_coordinates()
mid_azi <- atan2(mid_p2[2,"X"] - mid_p2[1, "X"],
mid_p2[2,"Y"] - mid_p2[1, "Y"])*180/pi
mid_p <- line %>%
st_line_sample(sample = 0.5) %>%
st_cast("POINT") %>%
st_coordinates() %>%
.[1,1:2]
# //TASK //////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
return(tribble(
~type, ~X, ~Y, ~azi,
"start", start_p[1,"X"], start_p[1,"Y"], start_azi,
"mid1", mid_p["X"], mid_p["Y"], mid_azi,
"mid2", mid_p["X"], mid_p["Y"], ifelse(mid_azi < 180, mid_azi + 180, mid_azi - 180),
"end", end_p[2,"X"], end_p[2,"Y"], end_azi))
# =========== NO MODIFY ZONE ENDS HERE ========================================
}
Step 3. Apply the function to all street segments
We can apply getAzimuth()
function to the edges object.
We finally append edges
object to make use of the columns
in edges
object (e.g., is_walkable
column).
After this code chunk, I will be ready to download GSV images.
# TASK ////////////////////////////////////////////////////////////////////////
# Apply getAzimuth() function to all edges.
# Remember that you need to pass edges object to st_geometry() before you apply getAzimuth()
edges_azi <- edges %>%
st_geometry() %>%
map_df(getAzimuth, .progress = T)
# //TASK //////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
edges_azi <- edges_azi %>%
bind_cols(edges %>%
st_drop_geometry() %>%
slice(rep(1:nrow(edges),each=4))) %>%
st_as_sf(coords = c("X", "Y"), crs = 4326, remove=FALSE) %>%
mutate(node_id = seq(1, nrow(.)))
# =========== NO MODIFY ZONE ENDS HERE ========================================
Step 4. Define a function that formats request URL and download images.
getImage <- function(iterrow){
# This function takes one row of edges_azi and downloads GSV image using the information from edges_azi.
# TASK ////////////////////////////////////////////////////////////////////////
# Finish this function definition.
# 1. Extract required information from the row of edges_azi, including
# type (i.e., start, mid1, mid2, end), location, heading, edge_id, node_id, and key.
# 2. Format the full URL and store it in `request`. Refer to this page: https://developers.google.com/maps/documentation/streetview/request-streetview
# 3. Format the full path (including the file name) of the image being downloaded and store it in `fpath`
type <- iterrow$type
location <- paste0(iterrow$Y %>% round(5), ",", iterrow$X %>% round(5))
heading <- iterrow$azi %>% round(1)
edge_id <- iterrow$edge_id
node_id <- iterrow$node_id
key <- Sys.getenv("GOOGLE_API_KEY")
# Construct API request URL
endpoint <- "https://maps.googleapis.com/maps/api/streetview" # google street view API endpoint
request <- glue::glue("{endpoint}?size=640x640&location={location}&heading={heading}&fov=90&pitch=0&key={key}")
# Construct filename
fname <- glue::glue("GSV-nid_{node_id}-eid_{edge_id}-type_{type}-Location_{location}-heading_{heading}.jpg") # Don't change this code for fname
# Set file path
fpath <- here::here("Major2_Img", fname)
download.file(request, fpath, mode = 'wb')
# //TASK //////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
# Download images
if (!file.exists(fpath)){
download.file(request, fpath, mode = 'wb')
}
# =========== NO MODIFY ZONE ENDS HERE ========================================
}
Step 5. Download GSV images
Before you download GSV images, make sure
the row number of edges_azi
is not too large! The row
number of edges_azi
will be the number of GSV images you
will be downloading. Before you download images, always double-check
your Google Cloud Console’s Billing tab to make sure that you will not
go above the free credit of $200 each month. The price is $7 per 1000
images.
Run the below code chunk (without #s):
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
# Loop!
# for (i in seq(1,nrow(edges_azi))){
# getImage(edges_azi[i,])
# }
# =========== NO MODIFY ZONE ENDS HERE ========================================
ZIP THE DOWNLOADED IMAGES AND NAME IT ‘gsv_images.zip’ FOR STEP 6.
Step 6. Apply computer vision
Now, use Google Colab to apply the semantic segmentation model. Zip your images and upload the images to your Colab session.
Step 7. Merging the processed data back to R
Once all of the images are processed and saved in your Colab session as a CSV file, download the CSV file and merge it back to edges.
# TASK ////////////////////////////////////////////////////////////////////////
# Read the downloaded CSV file from Google Colab
seg_output <- read.csv("seg_output.csv")
# //TASK ////////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
# Join the seg_output object back to edges_azi object using node_id as the join key.
edges_seg_output <- edges_azi %>%
inner_join(seg_output, by=c("node_id"="img_id")) %>%
select(type, X, Y, node_id, building, sky, tree, road, sidewalk, is_walkable) %>%
mutate(across(c(building, sky, tree, road, sidewalk), function(x) x/(640*640)))
# =========== NO MODIFY ZONE ENDS HERE ========================================
Section 3. Summarise and Analyze the Results
At the beginning of this assignment, I defined one Census Tract as walkable and the other as unwalkable. The key to the following analysis is the comparison between walkable and unwalkable Census Tracts.
Analysis 1 - Create interactive map(s) to visualize the spatial distribution of the streetscape
I created maps of the proportion of building, sky, tree, road, and sidewalk for walkable and unwalkable areas.
# TASK ////////////////////////////////////////////////////////////////////////
# Create interactive map(s) to visualize the `edges_seg_output` objects.
# As long as you can deliver the message clearly, you can use any format/package you want.
tmap_mode("view")
## tmap mode set to interactive viewing
building_w <- tm_shape(edges_seg_output %>%
filter(is_walkable == "walkable")) +
tm_dots(col = 'building', style = "pretty") +
tm_basemap(server = "Esri.WorldGrayCanvas") +
tm_layout(
legend.width = 0.25,
title.size = 1.5,
title.position = c("left", "top"),
title = "Proportion of Buildings: West Midtown",
legend.position = c("right", "top")
)
building_uw <- tm_shape(edges_seg_output %>%
filter(is_walkable == "unwalkable")) +
tm_dots(col = 'building', style = "pretty") +
tm_basemap(server = "Esri.WorldGrayCanvas") +
tm_layout(
legend.width = 0.25,
title.size = 1.5,
title.position = c("left", "top"),
title = "Proportion of Buildings: Hapeville",
legend.position = c("right", "top")
)
sky_w <- tm_shape(edges_seg_output %>%
filter(is_walkable == "walkable")) +
tm_dots(col = 'sky', style = "pretty") +
tm_basemap(server = "Esri.WorldGrayCanvas") +
tm_layout(
legend.width = 0.25,
title.size = 1.5,
title.position = c("left", "top"),
title = "Proportion of Sky: West Midtown",
legend.position = c("right", "top")
)
sky_uw <- tm_shape(edges_seg_output %>%
filter(is_walkable == "unwalkable")) +
tm_dots(col = 'sky', style = "pretty") +
tm_basemap(server = "Esri.WorldGrayCanvas") +
tm_layout(
legend.width = 0.25,
title.size = 1.5,
title.position = c("left", "top"),
title = "Proportion of Sky: Hapeville",
legend.position = c("right", "top")
)
tree_w <- tm_shape(edges_seg_output %>%
filter(is_walkable == "walkable")) +
tm_dots(col = 'tree', style = "pretty") +
tm_basemap(server = "Esri.WorldGrayCanvas") +
tm_layout(
legend.width = 0.25,
title.size = 1.5,
title.position = c("left", "top"),
title = "Proportion of Trees: West Midtown",
legend.position = c("right", "top")
)
tree_uw <- tm_shape(edges_seg_output %>%
filter(is_walkable == "unwalkable")) +
tm_dots(col = 'tree', style = "pretty") +
tm_basemap(server = "Esri.WorldGrayCanvas") +
tm_layout(
legend.width = 0.25,
title.size = 1.5,
title.position = c("left", "top"),
title = "Proportion of Trees: Hapeville",
legend.position = c("right", "top")
)
road_w <- tm_shape(edges_seg_output %>%
filter(is_walkable == "walkable")) +
tm_dots(col = 'road', style = "pretty") +
tm_basemap(server = "Esri.WorldGrayCanvas") +
tm_layout(
legend.width = 0.25,
title.size = 1.5,
title.position = c("left", "top"),
title = "Proportion of Road: West Midtown",
legend.position = c("right", "top")
)
road_uw <- tm_shape(edges_seg_output %>%
filter(is_walkable == "unwalkable")) +
tm_dots(col = 'road', style = "pretty") +
tm_basemap(server = "Esri.WorldGrayCanvas") +
tm_layout(
legend.width = 0.25,
title.size = 1.5,
title.position = c("left", "top"),
title = "Proportion of Road: Hapeville",
legend.position = c("right", "top")
)
sidewalk_w <- tm_shape(edges_seg_output %>%
filter(is_walkable == "walkable")) +
tm_dots(col = 'sidewalk', style = "pretty") +
tm_basemap(server = "Esri.WorldGrayCanvas") +
tm_layout(
legend.width = 0.25,
title.size = 1.5,
title.position = c("left", "top"),
title = "Proportion of Sidewalk: West Midtown",
legend.position = c("right", "top")
)
sidewalk_uw <- tm_shape(edges_seg_output %>%
filter(is_walkable == "unwalkable")) +
tm_dots(col = 'sidewalk', style = "pretty") +
tm_basemap(server = "Esri.WorldGrayCanvas") +
tm_layout(
legend.width = 0.25,
title.size = 1.5,
title.position = c("left", "top"),
title = "Proportion of Sidewalk: Hapeville",
legend.position = c("right", "top")
)
# //TASK //////////////////////////////////////////////////////////////////////
tmap_arrange(building_w, building_uw)
## legend.postion is used for plot mode. Use view.legend.position in tm_view to set the legend position in view mode.
## legend.postion is used for plot mode. Use view.legend.position in tm_view to set the legend position in view mode.
tmap_arrange(sky_w, sky_uw)
## legend.postion is used for plot mode. Use view.legend.position in tm_view to set the legend position in view mode.
## legend.postion is used for plot mode. Use view.legend.position in tm_view to set the legend position in view mode.
tmap_arrange(tree_w, tree_uw)
## legend.postion is used for plot mode. Use view.legend.position in tm_view to set the legend position in view mode.
## legend.postion is used for plot mode. Use view.legend.position in tm_view to set the legend position in view mode.
tmap_arrange(road_w, road_uw)
## legend.postion is used for plot mode. Use view.legend.position in tm_view to set the legend position in view mode.
## legend.postion is used for plot mode. Use view.legend.position in tm_view to set the legend position in view mode.
tmap_arrange(sidewalk_w, sidewalk_uw)
## legend.postion is used for plot mode. Use view.legend.position in tm_view to set the legend position in view mode.
## legend.postion is used for plot mode. Use view.legend.position in tm_view to set the legend position in view mode.
Comments
While Hapeville’s census tract covers a larger area, the two tracts are comparable in size and their GSV image data presents some interesting insights. The maps depicting the building and sidewalk proportions in the two regions are not surprising, indicating that West Midtown’s surge in new development has contributed to higher built up area according to the image data. Additionally, higher sidewalk values indicate a more pedestrian-friendly network in the area northwest of Georgia Tech as opposed to the region just north of Hartsfield-Jackson Airport. Furthermore, a higher proportion of sky found in Hapeville images suggest building-to-street ratio that is more in line with sprawling suburban neighborhoods rather than newer, denser ones that are building up, not out. The higher values of trees in Hapeville suggest more suburban character than West Midtown, which notably has few parks not as much greenery as other Atlanta neighborhoods with a bountiful tree canopy.
Analysis 2 - Compare the means
I then calculated the mean of the proportion of building, sky, tree, road, and sidewalk for hypothesized walkable and unwalkable census tracts.
West Midtown’s higher building percentage (10.37%) compared to Hapeville’s (3.18%) suggest higher density and thus more urbanization, which likely leads to more pedestrian-friendly outcomes. The percentages of sky and road coverage are quite similar, suggesting that despite differences in building density, both areas have a similar amount of open space above them and similar road infrastructure. Hapeville’s tree coverage (21.59%) in comparison to West Midtown (16.78%) implies a more natural environment, while West Midtown’s higher percentage of sidewalk coverage (3.94% compared to Hapeville’s 1.71%) correlates with the theory of greater walkability in the neighborhood adjacent to Georgia Tech.
Analysis 3 - Draw boxplot
The boxplots below support the findings West Midtown appears to be more built-up and pedestrian-friendly, while Hapeville seems to have more natural elements and less building density. In the boxplots, Hapeville is characterized as the “unwalkable” neighborhood, while West Midtown is listed as the “walkable” neighborhood, due to the earlier hypothesis. Due to the higher built-up area and stronger sidewalk network, West Midtown does appear to be the more walkable neighborhood based on the image data. The amount of visible sky in a neighborhood that is more built up, like West Midtown, is encouraging; however, the lack of trees indicate to urban planners a need to re-green a former industrial corner of the city.