✂️ პოლიგონების გადაკვეთა და overlay¶
GIS-ის ერთ-ერთი ყველაზე ხშირი ოპერაციაა overlay: ორი პოლიგონური ფენის გაერთიანება, გადაკვეთა ან გამოკლება. მაგალითად: „რომელი ნაწილი მოხვდა წყალდიდობის ზონაში და საკადასტრო ნაკვეთში?", „რა ფართობი დარჩა ნაკვეთიდან დაცული ტერიტორიის გამოკლების შემდეგ?" QGIS-ის Clip, Intersection, Union და Difference სწორედ ამ ალგორითმებზე დგას.
| ოპერაცია | აღნიშვნა | შედეგი |
|---|---|---|
| გადაკვეთა | A ∩ B |
რაც ერთდროულად ორივეშია |
| გაერთიანება | A ∪ B |
რაც ერთშია მაინც |
| სხვაობა | A − B |
A-ს ის ნაწილი, რაც B-ში არ არის |
🎮 სცადეთ თქვენც¶
გადაიტანეთ წვეროები და გადართეთ ოპერაციები. ლურჯი არის A, ნარინჯისფერი B, ხოლო წითელი შედეგი.
დაკვირვება
- ერთი პოლიგონი შეიძლება ორ ან მეტ ნაწილად დაიყოს (ორი „მთვარე" ერთმანეთს ორ ადგილას კვეთს), ამიტომ შედეგი ყოველთვის რგოლების სიაა.
- თუ A მთლიანად B-შია, გადაკვეთის წერტილები არ არსებობს და ალგორითმს ცალკე შემთხვევად უნდა დავამუშაოთ.
- როცა ერთი პოლიგონი მეორეში მთლიანად ექცევა,
A − Bხვრელს იძლევა. ჩვენი ვერსია ხვრელს ცალკე რგოლად აბრუნებს და ხვრელად არ აღნიშნავს, ამიტომ დემოში ფართობს რგოლების ჩადგმულობით ვთვლით (იხ. დავალება 2).
⚙️ ორი ალგორითმი¶
Sutherland–Hodgman: გაჭრა ამოზნექილი „ფანჯრით"¶
უმარტივესი ალგორითმია: ამოზნექილი clipper-ის ყოველი გვერდისთვის სათითაოდ ვჭრით subject-ს ნახევარსიბრტყით (იგივე იდეა, რომლითაც Voronoi-ს უჯრები ავაგეთ). ყოველი წიბოსთვის ოთხი შემთხვევა: ორივე ბოლო შიგნით (დავტოვოთ), ორივე გარეთ (გამოვტოვოთ), ერთი შიგნით, ერთი გარეთ (დავტოვოთ გადაკვეთის წერტილი).
შეზღუდვა: clipper აუცილებლად ამოზნექილი უნდა იყოს, ხოლო თუ შედეგი ორი ნაწილია, ალგორითმი მათ ერთ პოლიგონად აბრუნებს ორივეს დამაკავშირებელი „უსიგანო" წიბოთი (იხ. დავალება 3).
Greiner–Hormann: ნებისმიერი ორი პოლიგონი¶
- ყველა გადაკვეთის პოვნა. ერთი პოლიგონის ყოველი წიბო შევადაროთ მეორის ყოველ წიბოს (
O(n·m)) და გადაკვეთის წერტილები ჩავსვათ ორივე პოლიგონის წვეროთა სიაში, წიბოზე პოზიციის (alpha) მიხედვით. - შესვლა/გამოსვლა. პოლიგონის გასწვრივ მოძრაობისას გადაკვეთის წერტილები მონაცვლეობით ან შესვლაა მეორე პოლიგონში, ან გამოსვლა (პირველის სტატუსს ვიგებთ იმით, პირველი წვერო მეორეშია თუ არა).
- ბილიკის აგება. გადაკვეთის წერტილიდან ვიწყებთ: თუ ის შესვლაა, მივდივართ წინ, თუ გამოსვლა, უკან, სანამ შემდეგ გადაკვეთას არ შევხვდებით, იქ მეორე პოლიგონზე გადავდივართ და ასე ვაგრძელებთ, სანამ ბილიკი არ დაიხურება.
რა ოპერაცია გვინდა, იმას შესვლა/გამოსვლა განსაზღვრავს: გადაკვეთისთვის დავტოვოთ ისე, როგორც არის; გაერთიანებისთვის ორივე პოლიგონში ეს მონიშვნები შევაბრუნოთ (ე.ი. ვმოძრაობთ გარე კონტურზე); სხვაობისთვის A − B შევაბრუნოთ მხოლოდ A-სი.
სირთულე და რეალური ბიბლიოთეკები
ჩვენი ვერსია O(n·m)-ია. GEOS, რომელიც Shapely-ს, PostGIS-სა და QGIS-ს უდგას ქვეშ, გადაკვეთებს სივრცითი ინდექსებით ეძებს და გადაგვარებულ შემთხვევებს ზუსტი პრედიკატებითა და snapping-ით წყვეტს (იხ. დავალება 4). Vatti, Martinez–Rueda და Weiler–Atherton იგივე ამოცანის სხვა ვარიანტებია.
🐍 რეალიზაცია სუფთა Python-ით¶
| clip.py | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | |
გამოყენება¶
from clip import *
A = [(0, 0), (6, 0), (6, 6), (0, 6)]
B = [(3, 3), (9, 3), (9, 9), (3, 9)]
for op in ('intersection', 'union', 'difference'):
rings = clip(A, B, op)
print(op, [round(abs(polygon_area(r)), 1) for r in rings])
შემოწმება
ჩვენი clip შედარებულია Shapely 2.0.6-თან (GEOS) 3 000 შემთხვევით წყვილზე (ვარსკვლავისებრი პოლიგონები, 4–12 წვერო) ყველა სამ ოპერაციაზე: 8 514 შემთხვევაში ფართობი ემთხვევა, 0 შემთხვევაში არ ემთხვევა. 72 შემთხვევა გამოვტოვე, რადგან შედეგი ხვრელს შეიცავდა. JavaScript-ის ვერსია, რომელიც დემოშია, Python-ისგან იდენტურ რგოლებს აბრუნებს 900 შემთხვევაზე.
📚 იგივე ბიბლიოთეკით¶
import processing
processing.run('native:intersection', {'INPUT': layer_a, 'OVERLAY': layer_b, 'OUTPUT': 'intersection.gpkg'})
processing.run('native:union', {'INPUT': layer_a, 'OVERLAY': layer_b, 'OUTPUT': 'union.gpkg'})
processing.run('native:difference', {'INPUT': layer_a, 'OVERLAY': layer_b, 'OUTPUT': 'difference.gpkg'})
რა არის შემოწმებული
Shapely-ის შედარება გავუშვი. QGIS-ის ფრაგმენტები არ გამიშვია (QGIS არ არის დაყენებული).
✍️ დავალებები¶
დავალება 1: შემოწმება ეტალონთან
დაწერეთ შემთხვევითი ტესტი: 3 000 წყვილი შემთხვევითი პოლიგონის ფართობები შეადარეთ Shapely-ს. დაგენერირება: star() ქვემოთაა.
ამოხსნა
import math
import random
from shapely.geometry import Polygon
def star(cx, cy, n, r0, r1, rnd):
angs = sorted(rnd.uniform(0, 2 * math.pi) for _ in range(n))
return [(cx + r * math.cos(a), cy + r * math.sin(a)) for a in angs for r in [rnd.uniform(r0, r1)]]
def random_pair(rnd):
A = star(0, 0, rnd.randint(4, 12), 3, 10, rnd)
B = star(rnd.uniform(-6, 6), rnd.uniform(-6, 6), rnd.randint(4, 12), 3, 10, rnd)
return A, B
rnd = random.Random(1)
ok = bad = skipped = 0
for _ in range(3000):
A, B = random_pair(rnd)
pa, pb = Polygon(A), Polygon(B)
if not (pa.is_valid and pb.is_valid):
continue
for op, ref in (('intersection', pa.intersection(pb)), ('union', pa.union(pb)), ('difference', pa.difference(pb))):
if any(len(g.interiors) for g in getattr(ref, 'geoms', [ref])):
skipped += 1 # შედეგს ხვრელი აქვს, ჩვენი ვერსია ვერ აბრუნებს
continue
area = sum(abs(polygon_area(r)) for r in clip(A, B, op))
if abs(area - ref.area) < 1e-6:
ok += 1
else:
bad += 1
print('სწორი:', ok, ', არასწორი:', bad, ', გამოტოვებული (ხვრელი):', skipped)
is_valid გამოტოვებს თვითკვეთად პოლიგონებს (star() მათ ზოგჯერ იძლევა), ვინაიდან გადაკვეთის ალგორითმები მხოლოდ სწორი (valid) პოლიგონებზე გარანტირებულად მუშაობს.
დავალება 2: ინკლუზია–ექსკლუზია და ხვრელები
თეორიულად area(A ∪ B) = area(A) + area(B) − area(A ∩ B). შეამოწმეთ ეს იგივე ტიპის 500 წყვილზე. რა შემთხვევაში არ სრულდება და რატომ?
ამოხსნა
rnd = random.Random(2)
n = fail = fail_with_hole = 0
for _ in range(500):
A, B = random_pair(rnd)
pa, pb = Polygon(A), Polygon(B)
if not (pa.is_valid and pb.is_valid):
continue
ai = sum(abs(polygon_area(r)) for r in clip(A, B, 'intersection'))
au = sum(abs(polygon_area(r)) for r in clip(A, B, 'union'))
n += 1
if abs(au - (abs(polygon_area(A)) + abs(polygon_area(B)) - ai)) > 1e-6:
fail += 1
fail_with_hole += any(len(g.interiors) for g in getattr(pa.union(pb), 'geoms', [pa.union(pb)]))
print(n, fail, fail_with_hole)
დავალება 3: რატომ არ ვიყენებთ Sutherland–Hodgman-ს ყველგან?
U-ს ფორმის პოლიგონი U = [(0,0), (9,0), (9,8), (6,8), (6,3), (3,3), (3,8), (0,8)] გაჭერით მართკუთხედით R = [(-1,5), (10,5), (10,9), (-1,9)], რომელიც ორივე „ყურს" კვეთს. შეადარეთ Greiner–Hormann-ს და Shapely-ს.
ამოხსნა
from sh import sutherland_hodgman
U = [(0, 0), (9, 0), (9, 8), (6, 8), (6, 3), (3, 3), (3, 8), (0, 8)]
R = [(-1, 5), (10, 5), (10, 9), (-1, 9)]
print('GH:', [round(abs(polygon_area(r)), 1) for r in clip(U, R, 'intersection')])
res = sutherland_hodgman(U, R)
print('S-H:', len(res), 'წვერო, ფართობი', round(abs(polygon_area(res)), 1), ', ვალიდურია:', Polygon(res).is_valid)
ref = Polygon(U).intersection(Polygon(R))
print('ნამდვილი:', ref.area, ref.geom_type)
print(res)
GH: [9.0, 9.0]
S-H: 8 წვერო, ფართობი 18.0 , ვალიდურია: False
ნამდვილი: 18.0 MultiPolygon
[(6.0, 5.0), (3.0, 5.0), (3, 8), (0, 8), (0.0, 5.0), (9.0, 5.0), (9, 8), (6, 8)]
(3,5)–(0,5)–(9,5)–(6,5) სეგმენტით არის დაკავშირებული, რომელიც ორჯერ გადის ერთსა და იმავე წიბოზე. ფართობი სწორია (18), მაგრამ პოლიგონი არასწორია (Shapely-ს არავალიდურად ითვლის). Greiner–Hormann ორ ცალკე რგოლს იძლევა.
დავალება 4: გადაგვარებული შემთხვევები
გადაკვეთეთ კვადრატები [(0,0),(4,0),(4,4),(0,4)] და [(2,0),(6,0),(6,4),(2,4)]: მათი ორი გვერდი ერთ წრფეზეა (y = 0 და y = 4). რა ფართობს აბრუნებს clip და რა უნდა იყოს? როგორ გავასწოროთ?
ამოხსნა
sq1 = [(0, 0), (4, 0), (4, 4), (0, 4)]
sq3 = [(2, 0), (6, 0), (6, 4), (2, 4)]
print(round(sum(abs(polygon_area(r)) for r in clip(sq1, sq3, 'intersection')), 3))
rnd = random.Random(0)
jit = lambda poly: [(x + rnd.uniform(-1e-7, 1e-7), y + rnd.uniform(-1e-7, 1e-7)) for x, y in poly]
print(round(sum(abs(polygon_area(r)) for r in clip(jit(sq1), jit(sq3), 'intersection')), 3))
2 ≤ x ≤ 4). ჩვენი ალგორითმი გადაკვეთებს მხოლოდ წიბოების შიგნით ეძებს (0 < t < 1), ამიტომ ამ შემთხვევაში არც ერთ გადაკვეთას ვერ პოულობს, წვერო (2, 0) კი ზუსტად სხვა პოლიგონის გვერდზე დევს და „შიგნით" ითვლება. შედეგი: მთლიანი B (16). ფართოდ გავრცელებული გამოსწორება არის კოორდინატების ძალიან მცირე შემთხვევითი „ტრიალი" (1e-7), რაც გადაგვარებას გამორიცხავს. ეს ხშირად გამოყენებული გზაა Greiner–Hormann-ის ტიპის ალგორითმებში. პროფესიული ბიბლიოთეკები კი უფრო მყარ ზუსტ არითმეტიკასა და snapping-ს იყენებენ ისე, რომ კოორდინატები არ შეიცვალოს.