作者:郭子豪 中国地质大学(武汉)研究生 HPSCIL Urban Comp 城市之光团队成员
近年来,随着数字技术的飞速发展和城市化进程的加速,街景图像在捕捉城市空间信息方面发挥着越来越重要的作用。大规模的城市更新和建设项目导致城市景观和建筑风貌的日益复杂化,这对于决策者和城市规划者提出了新的挑战。如何准确、高效地捕捉和分析街景图像中的城市地理信息,为城市规划、交通管理和环境监测提供有力的支持,成为了当下的迫切问题。
计算机视觉技术作为一种强大的图像处理工具,已经逐渐成为解决这些问题的关键。通过对街景图像进行深度学习和分析,计算机视觉能够自动识别和标注图像中的各种物体、建筑和地理特征,提供更为精细和全面的城市空间描述。这些信息可以帮助我们更好地理解城市的空间结构、交通流量和环境变化,为城市规划、交通优化和环境保护提供科学的依据。
随着互联网和大数据技术的不断发展,越来越多的街景图像和相关数据被集中在数字平台上。这些数据不仅丰富了我们对城市空间的理解,还为计算机视觉算法的训练和优化提供了宝贵的资源。例如,通过分析街景图像中的交通流量、人流分布和建筑类型,我们可以更准确地预测城市的发展趋势,制定更为合理和可持续的规划策略。
绿视率是衡量城市绿色覆盖面积与总体城市面积之比,是评估城市绿化程度和环境质量的重要指标。已有研究表明,街景图像在计算城市绿视率上具有重要的应用价值。通过分析街景图像中的各像素信息,计算机视觉可以准确计算出绿视率,为城市绿化规划和生态保护提供科学依据。
本期,我们将基于和鲸 ModelWhale 平台,手把手教大家动手学习如何利用接近图像进行城市绿化率分析,在这里我们将向大家演示街景数据的爬取、读取、处理、以及可视化分析等一套完整的基本流程。我们将学习内容分为了三个 notebook:
核心挑战包括:
本节使用镜像为 Python 3.7 ,使用的计算资源是 2 核 8G CPU 资源,Kernel 类型为 Python3。使用的镜像很基础,爬取街景数据不涉及 GPU 的使用,只使用 CPU 资源就可以了。
街景图像是通过特定的图像采集设备(如摄像头)捕捉的城市街道、建筑和周围环境的全景图像。这些图像提供了对城市空间的直观和全面的视角,捕捉到的是日常生活中的实际场景和人们的活动。街景图像为城市规划师提供了宝贵的信息,帮助他们了解现有的城市结构和环境条件。通过分析图像,可以更好地评估现有的交通流量、建筑布局和绿化情况,从而制定更为合理和可持续的城市规划策略。
传统方式获取街景图像通常需要复杂的设备和大量的人力资源,这包括专业的摄像车辆、高精度的摄像头以及多次实地采集。这种方法不仅耗时耗力,还可能受到天气、交通和其他不可控因素的影响,限制了数据的获取和更新速度。
相比之下,使用网络爬虫获取街景图像提供了一种更为简单和便捷的解决方案。网络爬虫可以自动化地浏览和下载在线地图服务(如 Google 地图、百度地图等)上的街景图像,无需人工干预和实地采集。这种方法不仅能够大大提高数据获取的效率,还可以在短时间内获取大量的图像数据,满足各种分析和应用的需求。
本节利用百度地图 API ,实现批量抓取武汉市街景数据。那如何简单地爬取街景数据呢?
个人 AK 码:可以理解为秘钥,需要进行百度地图开放平台开发者认证即可获得。
进入 平台官网,注册登录百度账号后完成开发者认证,创建应用---->填写应用名称---->应用类型选择浏览器端---->refer 白名单输入“*”---->创建应用成功---->得到密匙(AK)
【平台官网】https://lbs.baidu.com/
在应用类型处选择浏览器端,在白名单处填写“*”,表示允许所有网站调用。
创建完成后,点击提交,获取得到自己的 ak。
在进行爬取之前,我们需要做好准备工作,先导入需要的库,其中,最重要的库就是 requests 库。我们提供的数据为高德坐标系,需要转换成wgs84坐标系,这里并不需要掌握坐标转换代码。
requests 是一个 Python 第三方库,专为简化 HTTP 请求而设计。该库提供了一套直观的 API,使得与 HTTP 服务进行交互变得高效而简便。其设计遵循了 HTTP 协议的标准,支持多种常见 HTTP 方法,包括 GET、POST、PUT 和 DELETE 等。
import re, os
import json
import requests
import time, glob
import csv
import traceback
import math
# 定义常量
x_pi = math.pi * 3000.0 / 180.0
pi = math.pi
a = 6378245.0 # 长半轴
ee = 0.00669342162296594323 # 扁率
# 定义将GCJ02坐标转换为WGS84坐标的函数
def gcj02_to_wgs84(lng, lat):
# 计算偏移量
dlat = _transformlat(lng - 105.0, lat - 35.0)
dlng = _transformlng(lng - 105.0, lat - 35.0)
# 将偏移量转换为经纬度的增量
radlat = lat / 180.0 * math.pi
magic = math.sin(radlat)
magic = 1 - ee * magic * magic
sqrtmagic = math.sqrt(magic)
dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * math.pi)
dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * math.pi)
# 计算WGS84坐标
mglat = lat + dlat
mglng = lng + dlng
return [lng * 2 - mglng, lat * 2 - mglat]
# 定义经纬度转换函数中的子函数
def _transformlat(lng, lat):
ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + \
0.1 * lng * lat + 0.2 * math.sqrt(math.fabs(lng))
ret += (20.0 * math.sin(6.0 * lng * math.pi) + 20.0 * math.sin(2.0 * lng * math.pi)) * 2.0 / 3.0
ret += (20.0 * math.sin(lat * math.pi) + 40.0 * math.sin(lat / 3.0 * math.pi)) * 2.0 / 3.0
ret += (160.0 * math.sin(lat / 12.0 * math.pi) + 320 * math.sin(lat * math.pi / 30.0)) * 2.0 / 3.0
return ret
# 定义经纬度转换函数中的子函数
def _transformlng(lng, lat):
ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + \
0.1 * lng * lat + 0.1 * math.sqrt(math.fabs(lng))
ret += (20.0 * math.sin(6.0 * lng * math.pi) + 20.0 * math.sin(2.0 * lng * math.pi)) * 2.0 / 3.0
ret += (20.0 * math.sin(lng * math.pi) + 40.0 * math.sin(lng / 3.0 * math.pi)) * 2.0 / 3.0
ret += (150.0 * math.sin(lng / 12.0 * math.pi) + 300.0 * math.sin(lng / 30.0 * math.pi)) * 2.0 / 3.0
return ret
# 定义函数,将指定CSV文件中的GCJ02坐标转换为WGS84坐标,并保存到另一个CSV文件中
def convert_coord(input_file, output_file):
# 打开输入CSV文件
with open(input_file, 'r', newline='') as csvfile:
reader = csv.reader(csvfile)
data = list(reader) # 读取行数据
data = data[1:] # 跳过标题行
# 替换原有列的经纬度数据
for row in data:
if len(row) < 2:
continue
lng, lat = float(row[0]), float(row[1]) # 提取经纬度数据
result = gcj02_to_wgs84(lng, lat) # 将GCJ02坐标转换为WGS84坐标
row[0], row[1] = result # 替换原有的经纬度数据列
# 检查并创建输出目录
output_dir = os.path.dirname(output_file)
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# 打开输出CSV文件
with open(output_file, 'w', newline='') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(["x", "y"]) # 写入标题行
writer.writerows(data) # 写入转换后的数据
# 调用函数进行坐标转换
input_file = r'/home/mw/input/wuhan_point5106/wuhan_point_sample2_gcj02.csv' # 输入CSV文件名
output_file = r'/home/mw/project/output_folder/wuhan_point_wgs_84.csv' # 输出CSV文件名
convert_coord(input_file, output_file)
print(f"转换完成,结果已保存到 {output_file}") # 打印转换完成的消息
在进行爬虫教学之前,我们强烈倡导遵循爬虫的专业道德准则和相关法律法规。虽然爬虫技术为我们提供了便捷地获取互联网信息的手段,但违反网站协议进行爬取可能会触犯法律,侵犯他人的合法权益,甚至面临法律追责。
本教学仅供学术和研究目的使用,我们坚决反对将所学知识用于任何商业行为或违法活动。我们鼓励学习者在使用爬虫技术时始终遵循法律规定和专业道德准则,确保行为的合法性和正当性。
网站反爬机制是为了防止自动化程序如爬虫恶意访问和抓取网站数据而采取的措施。常见的反爬手段包括 User-Agent 检测、请求频率限制等。
而 grab_img_baidu 函数与 openUrl 函数通过设置特定的 User-Agent、请求头信息以及检查响应类型来模拟浏览器行为,以规避网站的反爬机制。使我们的请求看起来更像是由真实用户发出的,从而减少被识别和阻止的风险,从而成功获取目标图片数据。
def grab_img_baidu(_url, _headers=None):
"""
从百度地图获取图片数据的函数。
参数:
_url (str): 要获取的图片的 URL。
_headers (dict, 可选): HTTP 请求的自定义头。默认为 None。
返回:
bytes: 如果成功,返回图片内容;否则返回 None。
"""
# 检查是否提供了自定义头,如果没有,则使用默认头
if _headers == None:
# 设置默认请求头
headers = {
"sec-ch-ua": '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"', # 用户代理
"Referer": "https://map.baidu.com/", # 请求来源页
"sec-ch-ua-mobile": "?0", # 移动设备的用户代理信息
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36" # 用户代理
}
else:
headers = _headers
# 使用指定的头发送 GET 请求
response = requests.get(_url, headers=headers)
# 检查响应是否成功,并且内容类型是否为 'image/jpeg'
if response.status_code == 200 and response.headers.get('Content-Type') == 'image/jpeg':
return response.content
else:
return None
def openUrl(_url):
"""
发送 HTTP GET 请求并返回响应内容。
参数:
_url (str): 要请求的 URL。
返回:
bytes: 如果成功,返回响应内容;否则返回 None。
"""
# 设置默认请求头
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36" # 用户代理
}
# 发送 GET 请求
response = requests.get(_url, headers=headers)
# 如果状态码为 200,服务器已成功处理了请求,则继续处理数据
if response.status_code == 200:
return response
.content
else:
return None
点击百度地图官网,切换为全景模式,并随机获取某位置街景图。点击 f12 打开开发者模式,在清空所有响应后,点击向前,可以看到一次完整的图片请求 url。
点击负载,查看加载字符串的具体含义。
分析该 URL 请求,并结合 API 服务网站 ,可以总结出如下初步结论
https://lbsyun.baidu.com/faq/api?title=viewstatic-base
请求影像切片所需的几个关键参数分别为: "panoid"是用于标识全景图像的标识符,"headings" 代表的是图像的方向或角度,范围从 0 到 360 度。 所以我们的街景爬取 url 设置如下,只需要得到 panoid 即可,headings 则设置为 ['0', '90', '180', '270'],可获得街景四个角度的图像。
url = 'https://mapsv0.bdimg.com/?qt=pr3d&fovy=90&quality=100&panoid=%7B%7D&heading=%7B%7D&pitch=0&width=480&height=320%27.format(panoid, headings[h])
再返回图片请求处,我们通过观察,可以发现,sid 与 panoid 相同。
而通过搜索各请求的响应,我们可以找到 sid 的请求 url。
通过多次实验,我们可以发现,该请求是通过经纬度坐标定位得到 sid 的。
至此,我们得到获取街景 panoid 的 url 以及获取街景图像的 url。
def getPanoId(_lng, _lat):
"""
获取百度街景中的 panovid。
参数:
_lng (str): 街景点的经度。
_lat (str): 街景点的纬度。
返回:
str: 成功返回 panovid,失败返回 None。
"""
# 构造请求 URL,包含经度、纬度、层级和其他查询参数
url = "https://mapsv0.bdimg.com/udt=20200825?&qt=qsdata&x=%s&y=%s&l=14&action=0&mode=day" % (
str(_lng), str(_lat))
# 发起 HTTP 请求并获取响应内容,使用 UTF-8 解码
response = openUrl(url).decode("utf8")
# 检查响应是否为空,如果为空则返回 None
if (response == None):
return None
# 定义正则表达式,用于从响应中提取 panovid
reg = r'"id":"(.+?)",'
pat = re.compile(reg)
try:
# 使用正则表达式从响应中提取 panovid
svid = re.findall(pat, response)[0]
# 返回提取到的 panovid
return svid
except:
# 如果提取失败,返回 None
return None
尽管我们已经解析获取了 url 地址,但百度街景获取时采用的是经过二次加密的百度墨卡托投影 bd09mc 坐标系,我们下载得到的路网数据一般为 wgs 坐标系。这时,就需要进行坐标转换,将我们的 wgs 坐标系转换为 bd09mc 坐标系,从而获取 panoid 地址来获取街景图像。
此处的坐标系转换,需要用到申请得到的 ak 码,通过调用百度 api 接口。
此处的接口百度开发文档中有详细的介绍
def wgs2bd09mc(wgs_x, wgs_y):
"""
将 WGS84 坐标转换为百度墨卡托坐标。
参数:
wgs_x (str): WGS84 坐标的经度。
wgs_y (str): WGS84 坐标的纬度。
返回:
tuple: 包含转换后的百度墨卡托坐标的元组 (bd09mc_x, bd09mc_y)。
"""
# 构造请求 URL
url = 'http://api.map.baidu.com/geoconv/v1/?coords={}+&from=1&to=6&output=json&ak={}'.format(
wgs_x + ',' + wgs_y, # 组合经纬度
'AxBljsdLeG0IrFxKkyD2H58gNgjuMHEm' # 在此处放置你的 AK 码
)
try:
res = openUrl(url).decode() # 发起请求并解码响应
except Exception as e:
print(f"Error fetching data from API: {e}") # 捕获异常并打印错误信息
return 0, 0
try:
temp = json.loads(res) # 尝试解析 JSON 格式的响应内容
except json.JSONDecodeError as e:
print(f"Error decoding JSON: {e}") # 捕获异常并打印错误信息
return 0, 0
bd09mc_x = 0 # 初始化百度墨卡托坐标 x
bd09mc_y = 0 # 初始化百度墨卡托坐标 y
# 检查 JSON 数据中是否包含所需的字段,并且状态码是否为 0
if 'status' in temp and temp['status'] == 0 and 'result' in temp and len(temp['result']) > 0:
bd09mc_x = temp['result'][0]['x'] # 获取百度墨卡托坐标 x
bd09mc_y = temp['result'][0]['y'] # 获取百度墨卡托坐标 y
else:
print(f"API returned unexpected data: {temp}") # 打印错误信息
return 0, 0
# 返回百度墨卡托坐标
return bd09mc_x, bd09mc_y
目前为止,我们已经做好了所有的准备工作,可以开始进行街景图像爬取啦。
这段代码从 CSV 文件中读取经纬度坐标,这里的 CSV 文件我们会提供,其是通过在 osm 路网数据采样点获取得到的。
通过百度 API 获取对应的街景图像,并将这些图像保存到指定目录。如果在下载过程中出现错误,它会记录错误信息并将这些信息保存到一个新的 CSV 文件中,方便下次收集,无svid代表该点无对应的街景图像。
if __name__ == "__main__":
# 设置文件和目录路径
root = r'/home/mw/project/output_folder/' # 根目录路径
read_fn = r'wuhan_point_wgs_84.csv' # 输入的 CSV 文件名
error_fn = r'wuhan_error.csv' # 错误记录的 CSV 文件名
dir = r'/home/mw/project/wuhan' # 输出图片保存目录
# 检查并创建输出目录
if not os.path.exists(dir):
os.makedirs(dir)
# 获取已存在的图片文件名列表
filenames_exist = glob.glob1(dir, "*.png") # 获取已存在的图片文件名列表
# 读取 CSV 文件中的数据
with open(os.path.join(root, read_fn), 'r', newline='', encoding='utf-8') as csvfile:
reader = csv.reader(csvfile)
data = list(reader)
# 存储 CSV 文件的完整路径
a = os.path.join(root, read_fn)
# 记录 CSV 文件的 header
header = data[0]
# 去掉 header,只保留数据
data = data[1:]
# 初始化错误图片列表
error_img = []
# 定义方向列表和 pitch
headings = ['0', '90', '180', '270'] # 定义方向列表
pitchs = '0' # 初始 pitch 为 0,pitch为视角俯仰度
# 只爬取前 10 个数据
for i in range(min(len(data), 10)):
print('当前处理到了第{}个点'.format(i + 1)) # 打印当前处理到的点的序号
# 从数据中获取经纬度
wgs_x, wgs_y = data[i][0], data[i][1] # 获取经纬度数据
try:
# 将 WGS-84 坐标转换为百度墨卡托坐标
bd09mc_x, bd09mc_y = wgs2bd09mc(wgs_x, wgs_y) # 调用函数进行坐标转换
print(f"Converted WGS to BD09MC: {bd09mc_x}, {bd09mc_y}") # 打印转换后的坐标
except Exception as e:
print("解析错误:" + str(e)) # 打印异常信息并继续下一次循环
error_img.append(data[i] + ["解析错误"])
continue
# 检查当前坐标下的四个方向的图片是否已经存在
flag = True # 初始化标志位
for k in range(len(headings)):
# 检查是否存在对应方向的图片文件
flag = flag and "%s_%s_%s_%s.png" % (wgs_x, wgs_y, headings[k], pitchs) in filenames_exist
# 如果所有方向的图片都存在,则跳过当前坐标
if flag:
continue
# 获取当前坐标的 panoid
panoid = getPanoId(bd09mc_x, bd09mc_y) # 获取当前坐标的 panoid
print("Panoid:")
print(panoid) # 打印 panoid
if panoid is None:
error_img.append(data[i] + ["无svid"])
continue
# 遍历每个方向,下载街景图片
for h in range(len(headings)):
save_fn = os.path.join(dir, '%s_%s_%s_%s.png' % (wgs_x, wgs_y, headings[h], pitchs)) # 设置保存图片的路径
url = 'https://mapsv0.bdimg.com/?qt=pr3d&fovy=90&quality=100&panoid={}&heading={}&pitch=0&width=480&height=320'.format(panoid, headings[h]) # 设置请求图片的 URL
img = grab_img_baidu(url) # 请求图片数据
# 如果图片下载成功,保存图片
if img is not None:
with open(save_fn, "wb") as f:
f.write(img)
print(f"储存成功: {save_fn}") # 输出储存成功消息
else:
error_img.append(data[i] + [headings[h]])
print(f"储存失败: {save_fn}") # 输出储存失败消息
# 保存错误信息
if len(error_img) > 0:
with open(os.path.join(root, error_fn), 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(header + ['error_info']) # 写入 header
writer.writerows(error_img) # 写入错误数据
print("输出错误文件")
# 爬取所有数据的代码如下:将下方代码取消注释,上方代码“ # 只爬取前 10 个数据”下的代码注释即可运行
# while True:
# try:
# for i in range(start_index,len(data)):
# print('当前处理到了第{}个点'.format(i + 1))
# # 从数据中获取经纬度
# wgs_x, wgs_y = data[i][0], data[i][1]
# try:
# # 将WGS-84坐标转换为百度墨卡托坐标
# bd09mc_x, bd09mc_y = wgs2bd09mc(wgs_x, wgs_y)
# print(f"Converted WGS to BD09MC: {bd09mc_x}, {bd09mc_y}")
# except Exception as e:
# print("解析错误:"+str(e)) # 打印异常信息并继续下一次循环
# # 检查当前坐标下的四个方向的图片是否已经存在
# flag = True
# for k in range(len(headings)):
# flag = flag and "%s_%s_%s_%s.png" % (wgs_x, wgs_y, headings[k], pitchs) in filenames_exist
# # 如果所有方向的图片都存在,则跳过当前坐标
# if flag:
# continue
# # 获取当前坐标的panoid
# panoid = getPanoId(bd09mc_x, bd09mc_y)
# print(panoid)
# if panoid is None:
# error_img.append(data[i] + ["无svid"])
# continue
# # 遍历每个方向,下载街景图片
# for h in range(len(headings)):
# save_fn = os.path.join(root, dir, '%s_%s_%s_%s.png' % (wgs_x, wgs_y, headings[h], pitchs))
# url = 'https://mapsv0.bdimg.com/?qt=pr3d&fovy=90&quality=100&panoid={}&heading={}&pitch=0&width=480&height=320'.format(panoid, headings[h])
# img = grab_img_baidu(url)
# # 如果图片下载成功,保存图片
# if img is not None:
# with open(save_fn, "wb") as f:
# f.write(img)
# #如果图片下载失败,记录错误信息
# if img is None:
# data[i].append(headings[h])
# error_img.append(data[i])
# # 更新起始索引
# start_index = i + 1
# # 捕获并处理任何异常
# except Exception as e:
# print("爬取错误:"+str(e)) # 打印异常信息
# time.sleep(300) # 休眠60秒
# # 保存失败的图片
# if len(error_img) > 0:
# with open(os.path.join(root, error_fn), 'w', newline='', encoding='utf-8') as f:
# writer = csv.writer(f)
# writer.writerow(header + ['error_info']) # 写入 header
# writer.writerows(error_img) # 写入错误数据
# print("输出错误文件")
目前我们已经爬取了街景图像,那么如何检验目前街景图象是否有效,而不是乱码或者残缺数据呢。 首先在左侧列表中的数据查看,查看变量“dir”设置的路径,本代码中“dir”为 dir = r'/home/mw/project/wuhan' # 输出图片保存目录。 首先可以查看到列表中出现数据信息,如下图。
然后根据以下代码,可以对列表中的一个图像进行检查,查看图片信息并可视化。
def check_image_info(image_path):
"""
检查图片信息并进行可视化。
参数:
image_path (str): 图片文件路径。
"""
# 检查文件是否存在
if not os.path.exists(image_path):
print("图片文件不存在")
return
# 打开图片文件
img = Image.open(image_path)
# 显示图片信息
print("图片信息:")
print(f"格式: {img.format}")
print(f"模式: {img.mode}")
print(f"尺寸: {img.size}")
# 显示图片
img.show()
# 指定图片文件路径
image_path = "/home/mw/project/wuhan/114.0446183_30.6338842_0_0.png" # 更改为你的图片文件路径,即上文的dir路径+图片名
# 检查图片信息并进行可视化
check_image_info(image_path)
本节使用镜像为 Python 3.7 ,使用的计算资源是 2 核 8G CPU 资源,Kernel 类型为 Python3。使用的镜像很基础,爬取 poi 数据不涉及 GPU 的使用,只使用 CPU 资源就可以了。
城市绿视率是基于街景图像技术的城市规划和设计指标,用于评估城市的绿化水平。绿视率是通过对街景图像进行分析,提取出绿地、植被覆盖等绿化要素,并计算它们在整体城市面积中所占的比例得到。这个指标可以帮助识别出绿地不足或分布不均匀的区域,为城市规划和设计提供科学依据。提高城市绿化水平有助于改善城市环境质量,提升居民生活质量,促进城市的可持续发展。
首先我们需要导入所需库,以下是各个库的介绍。
# 导入所需库
import os
from PIL import Image
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
绿视率计算方法采用了 Pillow 库来读取由爬虫获取的街景图像数据。首先,每张图像被打开并获取其宽度和高度,然后初始化一个绿色像素计数器。接下来,代码遍历图像中的每个像素点,并获取其 RGB 值。
在判断像素是否为绿色时,我们使用了一个简单的条件:绿色通道值(g)大于红色(r)和蓝色(b)通道值。这是一个基本的方法来识别绿色像素,当然,你可以根据需要调整这个阈值或采用更复杂的颜色空间转换和阈值处理方法。
在获取了所有绿色像素后,我们计算绿色像素占总像素数的比例,以得到绿视率。这个比例乘以 100,以便以百分比的形式表示。
对于一个给定的地点,我们获取其四个不同方向(0 度、90 度、180 度和 270 度)的街景图像,并对每张图像都进行上述的绿视率计算,从而估算出该地区的整体绿视率。
# 计算绿色像素比例函数
def calculate_green_ratio(image_path):
# 打开图像
img = Image.open(image_path)
# 获取图像的宽度和高度
width, height = img.size
# 初始化绿色像素计数
green_pixels = 0
# 遍历每个像素
for x in range(width):
for y in range(height):
# 获取像素的RGB值
r, g, b = img.getpixel((x, y))
# 判断是否为绿色像素(可以根据需要调整阈值)
if g > r and g > b:
green_pixels += 1
# 计算绿色像素占比
total_pixels = width * height
green_ratio = (green_pixels / total_pixels) * 100
return green_ratio
"visualize_green_pixels"函数首先打开指定路径的图像文件,然后将其转换为 NumPy 数组。这个 NumPy 数组表示了图像的像素数据,使我们能够更容易地进行像素级的操作。
接着,我们定义了一个布尔掩码("green_mask"),用于筛选出绿色像素。掩码基于一个简单的条件:绿色通道值(索引为 1 的通道,基于 0 索引的 RGB)大于红色和蓝色通道值。这个掩码用于选择所有绿色像素,并将其他像素设置为黑色。
然后,我们在一个 12x6 英寸大小的图形中可视化原始图像和仅包含绿色像素的图像。在左侧,我们展示了原始图像,而在右侧则是高亮显示了绿色像素的图像。
这个函数的目的是提供一个直观的方式来查看图像中的绿色像素,从而帮助我们查看目前绿视率计算效果。
# 定义一个函数用于可视化图像中的绿色像素
def visualize_green_pixels(image_path):
# 打开指定路径的图像文件
img = Image.open(image_path)
# 将图像转换为NumPy数组以便处理
img_np = np.array(img)
# 创建一个布尔掩码来标识绿色像素
# 绿色通道值(索引为1)大于红色和蓝色通道值时,掩码为True
green_mask = (img_np[:,:,1] > img_np[:,:,0]) & (img_np[:,:,1] > img_np[:,:,2])
# 使用掩码复制原始图像数组,非绿色像素设为黑色
green_pixels = img_np.copy()
green_pixels[~green_mask] = [0, 0, 0]
# 创建一个12x6英寸的新图形
plt.figure(figsize=(12, 6))
# 在第一个子图中显示原始图像
plt.subplot(1, 2, 1)
plt.imshow(img)
plt.title('Original Image') # 设置标题
plt.axis('off') # 隐藏坐标轴
# 在第二个子图中显示只包含绿色像素的图像
plt.subplot(1, 2, 2)
plt.imshow(green_pixels)
plt.title('Green Pixels') # 设置标题
plt.axis('off') # 隐藏坐标轴
# 显示图形
plt.show()
主要的两个函数我们已经写好,接下来搭建主函数,就可以开始批量进行绿视率计算啦。
# 指定图像文件夹的路径
folder_path = r"/home/mw/project/wuhan" # 替换为你的文件夹路径
# 使用列表推导式获取文件夹中所有以.jpg、.jpeg或.png结尾的图像文件
image_files = [f for f in os.listdir(folder_path) if f.endswith(('.jpg', '.jpeg', '.png'))]
# 初始化结果列表和计数器
results = []
i = 0
# 获取一个示例图像的完整路径并进行绿色像素可视化
image_example = os.path.join(folder_path, image_files[0])
visualize_green_pixels(image_example)
# 遍历文件夹中的每个图像文件
for image_file in image_files:
# 获取当前图像文件的完整路径
image_path = os.path.join(folder_path, image_file)
# 调用calculate_green_ratio函数计算当前图像的绿色像素比例
green_ratio = calculate_green_ratio(image_path)
# 将图像文件名和计算出的绿色像素比例添加到结果列表中
results.append({'Image Name': image_file, 'Green Ratio (%)': green_ratio})
# 更新处理计数器
i += 1
print("当前处理到第{}张图片".format(i))
# 将结果列表转换为DataFrame,并保存为CSV文件
df = pd.DataFrame(results)
df.to_csv('/home/mw/project/wuhan/green_ratio_results.csv', index=False)
# 输出完成信息
print("Green ratio calculation completed. Results saved to green_ratio_results.csv.")
处理得到的 csv 文件,第一列为街景图像名称,而名称含有经纬度信息,所以我们需要将经纬度提取出来,方便后续可视化。
# 输入CSV文件路径
input_csv_file = '/home/mw/project/wuhan/green_ratio_results.csv'
output_csv_file = '/home/mw/project/wuhan/green_ratio_results1.csv'
# 检查输出文件夹是否存在,如果不存在则创建
output_folder = 'output_folder'
if not os.path.exists(output_folder):
os.makedirs(output_folder)
# 读取CSV文件
df = pd.read_csv(input_csv_file)
# 将'Image Name'列按照'_'进行分割,并扩展为新的列
a = df['Image Name'].str.split('_', expand=True)
# 选择新的数据框中的前两列和原始数据框中的第二列,然后合并成新的数据框
new_df = pd.concat([a.iloc[:, :2], df.iloc[:, 1]], axis=1)
# 设置新的列名
new_df.columns = ['x', 'y', 'Green Ratio (%)']
# 保存处理后的数据到新的CSV文件
new_df.to_csv(os.path.join(output_folder, output_csv_file), index=False)
# 打印保存成功的消息
print(f"Data written to {output_csv_file} in {output_folder} successfully!")
首先下载安装 QGIS。
利用输出的 csv 文件,结合 QGIS 软件,将街景图像可视化在武汉市矢量图后的最终效果图是这样:
在课程中,我们学习了城市绿视率的计算方法以及如何利用街景图像技术来评估城市的绿化水平。绿视率是通过对街景图像进行分析,提取绿地和植被覆盖等绿化要素,并计算它们在整体城市面积中的比例来衡量。街景图像的爬取是获取评估城市绿化水平所需数据的重要步骤,需要通过爬虫技术获取大量的街景图像数据,以支持后续的分析和计算。
在作业中,我们首先要使用百度地图API接口爬取2013年的武汉市街景图像数据,其中街景图像的fov为60。这涉及到破解反爬虫机制、分析爬取数据以及对数据的简单处理。接着,我们要对爬取得到的街景图像进行语义分割,计算街景图像的天空率,即天空像素占比。这需要对图像进行处理和分析,理解图像的本质数据类型以及如何通过这种数据类型进行显示。最后,我们要根据街景图像的经纬度信息生成POI点,并在武汉市的矢量图上进行可视化,这涉及到将POI点的csv文件生成矢量文件,并在QGIS中进行可视化。
通过这些作业,我们将掌握爬虫技术、图像处理和分析方法,以及矢量数据的处理和可视化技术,从而更好地理解城市绿化水平,并为城市规划和设计提供科学依据。
本项目来自【和鲸社区】的活动【武汉大学——聚焦前沿对话未来:地理空间智能(GeoAI)最新研究进展及落地应用】中的一篇教案
【学习链接】https://www.heywhale.com/home/activity/detail/662b317aa2141d2b0bf33b49
其他教案【基于多源数据融合的土地利用分类模型】,【面向复杂城市系统的大规模物流优化算法】,【基于街景图像的武汉城市绿化空间分析】大家可以了免费报名在线学习。
可以在线运行代码哦