👁️ ხილვადობა (Viewshed)¶
ხედვის არე (viewshed) არის რუკის ყველა ის ადგილი, რომელიც მოცემული წერტილიდან ჩანს. ამას იყენებენ სმარტფონის ანძების განლაგებაში, ხანძარსაწინააღმდეგო კოშკურების დაგეგმვაში, ქარის ტურბინების ვიზუალური გავლენის შეფასებაში და ტურისტული ხედების ანალიზში. ყველაფერი ერთ კითხვაზე დგას: ხედავს თუ არა წერტილი A წერტილ B-ს, თუ რელიეფი ხელს უშლის?
💡 იდეა: ხედვის ხაზი (line of sight)¶
A-დან B-მდე გავავლოთ სწორი ხაზი და ვნახოთ, რელიეფი რომელიმე წერტილში ამ ხაზს ზემოთ ხომ არ ადის. ამისთვის რელიეფის ყოველ უჯრაზე ვითვლით დახრილობას დამკვირვებლის თვალიდან:
სამიზნე ჩანს მაშინ და მხოლოდ მაშინ, თუ მისი დახრილობა ყველა შუალედური უჯრის დახრილობაზე არანაკლებია. ასე ვინახავთ მხოლოდ ერთ რიცხვს, ზრდადი მაქსიმუმის სახით (max_slope), და ყოველ უჯრაზე ერთი შედარება გვაქვს.
უჯრებს ხაზზე ბრეზენჰემის ალგორითმი (Bresenham) ირჩევს: ის რასტრზე სწორი ხაზის უახლოეს უჯრებს აბრუნებს მხოლოდ მთელი რიცხვებით.
🎮 სცადეთ თქვენც¶
დააჭირეთ რუკას დამკვირვებლის გადასაადგილებლად. კურსორის ქვეშ ხედვის ხაზი გაივლება და ქვემოთ გამოჩნდება რელიეფის პროფილი.
დაკვირვება
- დამკვირვებლის სიმაღლის ზრდა (კოშკური, ანძა) ხედვის არეს იმატებს, თუმცა ერთნაირი სისწრაფით არა: პირველი ათი მეტრი უფრო ეფექტურია, ვიდრე მომდევნო ორმოცი (იხ. დავალება 1).
- სამიზნის სიმაღლე ეხმარება: შენობა ან ანძა მაშინაც ჩანს, თუ მისი ძირი ჩრდილშია.
🐍 რეალიზაცია სუფთა Python-ით¶
გამოყენება¶
import math
from view import *
def terr(r, c):
return 120 + 60 * math.sin(c / 9) * math.cos(r / 7) + 35 * math.sin((r + c) / 13) + 25 * math.cos(c / 4.5) * math.sin(r / 5)
dem = [[terr(r, c) for c in range(60)] for r in range(40)] # 40 x 60, უჯრა 30 მ
vs = viewshed(dem, (20, 30), h_obs=10)
print('ხილულია:', sum(map(sum, vs)), 'უჯრა 2400-დან')
print(bresenham(0, 0, 3, 7))
შემოწმება
სამი მარტივი შემთხვევა ისე იქცევა, როგორც უნდა: სრულიად ბრტყელ რელიეფზე ყველა უჯრა ჩანს; ერთ კედელზე (სიმაღლე 100) თავად კედელი ჩანს, მის უკან მყოფი უჯრები კი არა, ხოლო კედლამდე ყველაფერი ჩანს. JavaScript-ის ვერსია, რომელიც დემოშია, იგივე რიცხვებს იძლევა (1000, 1091, 1385 უჯრა 1.7, 10 და 50 მ-ზე).
სირთულე
ყოველი უჯრისთვის ხაზი O(L) ბიჯია (L რასტრის სიგრძეა), ამიტომ მთელი ხედვის არე O(N·L). ეს მარტივი „ყველა ხაზი ცალ-ცალკე" ვერსიაა. R2/R3 და sweep-line ალგორითმები (მაგ. GRASS-ში) ხედვის არეს გაცილებით სწრაფად იღებენ, ზოგს ინფორმაციას ხაზებს შორის ინახავენ.
📚 იგივე ბიბლიოთეკით¶
-oz დამკვირვებლის სიმაღლეა, -tz სამიზნის, -md მაქსიმალური მანძილი. ნაგულისხმევად GDAL ითვალისწინებს დედამიწის სიმრუდესაც (-cc კოეფიციენტი).
რა არის შემოწმებული
ბიბლიოთეკების ფრაგმენტები არ გამიშვია ამ გარემოში (GDAL და QGIS არ არის დაყენებული). პარამეტრების სახელები დოკუმენტაციასთან შეამოწმეთ, განსაკუთრებით GDAL-ის ვერსიაზე, რადგან gdal_viewshed ახალი ბრძანებაა.
✍️ დავალებები¶
დავალება 1: სიმაღლის გავლენა
გამოთვალეთ ხილული უჯრების წილი (20, 30)-დან დამკვირვებლის სიმაღლეებზე 0, 2, 10, 50 მეტრი.
ამოხსნა
ზრდა არაწრფივია: 0-დან 10 მეტრამდე +5.6 პროცენტული პუნქტი, 10-დან 50-მდე +12.2, თუმცა ყოველი მეტრი ნაკლებს იძლევა (ეფექტი სიმაღლის მეტრზე: ~0.56 პ.პ. პირველ 10 მ-ზე, ~0.31 მომდევნო 40 მ-ზე). ამიტომ „კიდევ ერთი მეტრი" გვიან ნაკლებად ღირს.დავალება 2: არის თუ არა ხედვა სიმეტრიული?
თუ A ხედავს B-ს, ხედავს თუ არა B A-ს? შეამოწმეთ 2 000 შემთხვევით წყვილზე (ორივე სიმაღლე 0).
ამოხსნა
import random
random.seed(1)
asym = 0
N = 2000
for _ in range(N):
a = (random.randrange(40), random.randrange(60))
b = (random.randrange(40), random.randrange(60))
if line_of_sight(dem, a, b, 0, 0) != line_of_sight(dem, b, a, 0, 0):
asym += 1
print('ასიმეტრიული წყვილი:', asym, 'of', N)
A-დან B-მდე და B-დან A-მდე ბრეზენჰემი სხვადასხვა უჯრებს ირჩევს, ამიტომ 1.4% წყვილში პასუხი განსხვავდება. თუ სიმეტრია გჭირდებათ (მაგ. ორმხრივი რადიოკავშირი), ორივე მიმართულება შეამოწმეთ და „და" ოპერაცია გამოიყენეთ. დამკვირვებლისა და სამიზნის სხვადასხვა სიმაღლეც ასიმეტრიას იძლევა.
დავალება 3: დედამიწის სიმრუდე
დაამატეთ line_of_sight-ს ვერსია, რომელიც მანძილზე d სიმაღლეს ამცირებს (1 − k)·d² / (2R)-ით (R = 6 371 km, რეფრაქციის k = 0.13). შეადარეთ ხილული უჯრები უჯრის ზომებზე 30, 300 და 1000 მ (იგივე რელიეფის სიმაღლეებით).
ამოხსნა
R_EARTH = 6371000.0
K_REFR = 0.13
def los_curved(dem, obs, target, h_obs, h_target=0.0, cell=30.0):
(r0, c0), (r1, c1) = obs, target
z0 = dem[r0][c0] + h_obs
drop = lambda d: (1 - K_REFR) * d * d / (2 * R_EARTH)
max_slope = -math.inf
for r, c in bresenham(r0, c0, r1, c1)[1:]:
d = math.hypot(r - r0, c - c0) * cell
if (r, c) == target:
return (dem[r][c] + h_target - drop(d) - z0) / d >= max_slope
max_slope = max(max_slope, (dem[r][c] - drop(d) - z0) / d)
return True
obs = (20, 30)
for cell in (30, 300, 1000):
flat = sum(sum(line_of_sight(dem, obs, (r, c), 10, 0, cell) for c in range(60)) for r in range(40))
curv = sum(sum(los_curved(dem, obs, (r, c), 10, 0, cell) for c in range(60)) for r in range(40))
print(f'უჯრა {cell:4} მ: სიმრუდის გარეშე {flat}, სიმრუდით {curv}')
დავალება 4: სად დავდგათ კოშკურები?
აირჩიეთ 4 წერტილი (ბადე ყოველ 5 უჯრაზე, სიმაღლე 10 მ) ისე, რომ ერთად რაც შეიძლება მეტი ტერიტორია დაფარონ. გამოიყენეთ ხარბი ალგორითმი: ყოველ ბიჯზე ის წერტილი, რომელიც ჯერ დაუფარავს ყველაზე მეტს მატებს.
ამოხსნა
def greedy(dem, k, h=10, step=5):
cands = [(r, c) for r in range(0, 40, step) for c in range(0, 60, step)]
vs = {p: {(r, c) for r in range(40) for c in range(60) if line_of_sight(dem, p, (r, c), h)} for p in cands}
chosen, covered = [], set()
for _ in range(k):
best = max(cands, key=lambda p: len(vs[p] - covered))
chosen.append(best)
covered |= vs[best]
print(f' + {best}: ჯამური დაფარვა {100 * len(covered) / total:.1f}%')
return chosen
greedy(dem, 4)