build-candidate-imagery-plates.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
  1. """Local TMS imagery audit plates; preserve unmarked image next to surveyed outlines."""
  2. import json, math, hashlib
  3. from pathlib import Path
  4. from PIL import Image, ImageDraw, ImageFont
  5. ROOT = Path('.scratch/cockpit-shell-panel-layout/verify/artifacts/t35')
  6. AUDIT = ROOT / 'large-building-audit/audit.json'
  7. OUT = ROOT / 'large-building-audit/imagery-plates'
  8. SOURCE = Path('D:/GIS/GIS/yuanlingPic3')
  9. ZOOM = 19
  10. N = 2 ** ZOOM
  11. def pixel(lon, lat):
  12. return ((lon + 180) / 360 * N * 256,
  13. (1 - math.asinh(math.tan(math.radians(lat))) / math.pi) / 2 * N * 256)
  14. def rings(geometry):
  15. if geometry['type'] == 'Polygon': return geometry['coordinates']
  16. if geometry['type'] == 'MultiPolygon': return [r for p in geometry['coordinates'] for r in p]
  17. raise ValueError(geometry['type'])
  18. OUT.mkdir(parents=True, exist_ok=True)
  19. candidates = sorted((b for b in json.loads(AUDIT.read_text())['candidates'] if b['tier']=='urban'), key=lambda b: (-b['areaSqm'], b['id']))[:10]
  20. font = ImageFont.truetype('C:/Windows/Fonts/arial.ttf', 18)
  21. records = []
  22. for b in candidates:
  23. paths = [list(map(lambda p: pixel(*p[:2]), r)) for r in rings(b['geometry'])]
  24. points = [p for r in paths for p in r]
  25. x0 = math.floor(min(p[0] for p in points)) - 70
  26. y0 = math.floor(min(p[1] for p in points)) - 70
  27. x1 = math.ceil(max(p[0] for p in points)) + 70
  28. y1 = math.ceil(max(p[1] for p in points)) + 70
  29. minx, miny = x0//256, y0//256
  30. maxx, maxy = (x1-1)//256, (y1-1)//256
  31. mosaic = Image.new('RGB', ((maxx-minx+1)*256, (maxy-miny+1)*256))
  32. sources = []
  33. for x in range(minx, maxx+1):
  34. for y in range(miny, maxy+1):
  35. path = SOURCE / str(ZOOM) / str(x) / f'{N-1-y}.png'
  36. data = path.read_bytes()
  37. with Image.open(path) as tile:
  38. assert tile.size == (256,256)
  39. mosaic.paste(tile.convert('RGB'), ((x-minx)*256,(y-miny)*256))
  40. sources.append({'path': str(path), 'sha256': hashlib.sha256(data).hexdigest(), 'xyz': [ZOOM,x,y], 'tmsY':N-1-y})
  41. image = mosaic.crop((x0-minx*256,y0-miny*256,x1-minx*256,y1-miny*256))
  42. marked = image.copy()
  43. draw = ImageDraw.Draw(marked)
  44. for r in paths: draw.line([(x-x0,y-y0) for x,y in r], fill='#00ffff',width=3)
  45. w,h = image.size
  46. plate = Image.new('RGB',(w*2+12,h+65),'#101927')
  47. plate.paste(image,(0,65)); plate.paste(marked,(w+12,65))
  48. draw = ImageDraw.Draw(plate)
  49. draw.text((8,5),f"{b['id']} | {b['areaSqm']} m2 | survey relief {b.get('terrainRelief')} m",fill='white',font=font)
  50. draw.text((8,34),'Satellite source (north up)',fill='white',font=font)
  51. draw.text((w+20,34),'Same source + cyan JMD survey outline',fill='white',font=font)
  52. path = OUT / f"{b['id']}.png"
  53. plate.save(path)
  54. records.append({'id':b['id'],'areaSqm':b['areaSqm'],'terrainRelief':b.get('terrainRelief'),'plate':str(path),'pixelBBox':[x0,y0,x1,y1],'sourceTiles':sources,'geometrySha256':hashlib.sha256(json.dumps(b['geometry'],sort_keys=True).encode()).hexdigest(),'interpretation':None})
  55. (OUT/'manifest.json').write_text(json.dumps({'sourceAuditSha256':hashlib.sha256(AUDIT.read_bytes()).hexdigest(),'zoom':ZOOM,'candidateCount':len(records),'plates':records,'modelChanged':False,'satelliteCaptureDateKnown':False},indent=2),encoding='utf8')
  56. print(json.dumps({'passed':True,'plates':len(records),'output':str(OUT),'modelChanged':False}))