LittleNose Posted Wednesday at 03:22 PM Posted Wednesday at 03:22 PM (edited) First off, I want to acknowledge that what follows is not groundbreaking — I know others have achieved similar results before. But despite plenty of digging, I couldn’t find anywhere this specific approach is properly documented or explained. So I took on the challenge of figuring it out myself. The Disposition function has recently been discovered and mentioned in the Reddit forums. From what I can tell this does something similar and is far simpler. That said, I still think this method might still be useful to some in the community. It’s also about time I gave something back, since everything I’ve managed to create for my own fun has been built on the shoulders of what others have shared before me. What This Does This setup provides a way to determine the distance from any given point to the nearest map feature: forested areas, roads, towns, rivers, beaches, and so on. Once the KD-Tree is built during mission load, the actual search for nearest point is pretty much instant. Building the KD-Tree can take a few minutes depending on the number of data points. I'm sure there are lots of practical uses but I've used it primarily to spawn/move ground units without them ending up in the indestructable forest. At its core, this is just a way to calculate the distance from a position to the closest matching map feature — based on colour-coded map data. How It Works Python (setup): Take a screenshot of the map and grab the RGB values for each pixel. Match each pixel’s RGB to map coordinates. Filter the pixels based on RGB — for example, greenish colours for trees. Save the resulting coordinate data to a file. Optionally reduce the data resolution to save performance. Lua (in mission scripting): Load the pre-generated data file. Build a KD-Tree (a spatial data structure that allows fast searches). Use the KD-Tree to find the nearest matching point and measure the distance. Use this to determine proximity — e.g. “how far am I from the nearest tree?” Final Notes As must be obivous by now, I’m not a programmer. So feel free to take this and rewrite it to be cleaner, faster, or just less ugly and pass it on. Lua Code to Setup the KD-Tree --- ================= KD Tree Functions ================= --- -- Function to load data from CSV file local function LoadDataPointsTrees(filename) local points = {} local Path = "C:\\Users\\xbox\\Saved Games\\DCS\\Missions" local ok ,fileData = UTILS.LoadFromFile(Path,filename) for lineNumber = 1, #fileData do lineString=fileData[lineNumber] local easting, northing = lineString:match("([^,]+),([^,]+)") easting = tonumber(easting) northing = tonumber(northing) table.insert(points, {northing, easting}) end return points end -- ===================== Data Point Distance Function ====================== -- -- Rebuild the KDTree recursively function rebuild_kdtree(data) if data == nil then return nil end local point = data["point"] local left = rebuild_kdtree(data["left"]) local right = rebuild_kdtree(data["right"]) return KDTreeNode:new(point, left, right) end -- Define a k-d tree node local KDTreeNode = {} KDTreeNode.__index = KDTreeNode function KDTreeNode:create(point, left, right) local node = {} setmetatable(node, KDTreeNode) node.point = point node.left = left node.right = right return node end -- Helper function to slice a table local function sliceTable(tbl, startIdx, endIdx) local sliced = {} for i = startIdx, endIdx do table.insert(sliced, tbl[i]) end return sliced end -- Function to build the k-d tree local function buildKDTree(points, depth) if #points == 0 then return nil end -- Select axis based on depth local axis = depth % 2 -- Sort point list and choose median as pivot element table.sort(points, function(a, b) if axis == 0 then return a[1] < b[1] -- Compare x-coordinates else return a[2] < b[2] -- Compare y-coordinates end end) -- Choose median local median = math.floor(#points / 2) + 1 -- Create node and construct subtrees return KDTreeNode:create( points[median], buildKDTree(sliceTable(points, 1, median - 1), depth + 1), buildKDTree(sliceTable(points, median + 1, #points), depth + 1) ) end -- Function to calculate squared distance between two points local function squaredDistance(p1, p2) return ((p1[1] - p2[1])^2 + (p1[2] - p2[2])^2) end -- Nearest neighbor search in the k-d tree local function nearestNeighborSearch(node, target, depth, best, bestDist) if node == nil then return best, bestDist end local axis = depth % 2 local nextBest = best local nextBestDist = bestDist local currentDist = squaredDistance(node.point, target) if currentDist < bestDist then nextBest = node.point nextBestDist = currentDist end local direction if axis == 0 then direction = target[1] < node.point[1] else direction = target[2] < node.point[2] end local primary = direction and node.left or node.right local secondary = direction and node.right or node.left nextBest, nextBestDist = nearestNeighborSearch(primary, target, depth + 1, nextBest, nextBestDist) if (axis == 0 and (target[1] - node.point[1]) ^ 2 < nextBestDist) or (axis == 1 and (target[2] - node.point[2]) ^ 2 < nextBestDist) then nextBest, nextBestDist = nearestNeighborSearch(secondary, target, depth + 1, nextBest, nextBestDist) end return nextBest, nextBestDist end -- Function to print the k-d tree (for testing purposes) local function printKDTree(node, depth) if node == nil then return end printKDTree(node.left, depth + 1) printKDTree(node.right, depth + 1) end -- Example function to serialize a Lua table (KD-tree) to a string local function serializeTableFROMGPT(t, indent) indent = indent or 0 local result = "" local padding = string.rep(" ", indent) result = result .. "{\n" for k, v in pairs(t) do local key = (type(k) == "string" and string.format("%q", k)) or k if type(v) == "table" then result = result .. padding .. "[" .. key .. "] = " .. serializeTable(v, indent + 4) .. ",\n" else local value = (type(v) == "string" and string.format("%q", v)) or tostring(v) result = result .. padding .. "[" .. key .. "] = " .. value .. ",\n" end end result = result .. string.rep(" ", indent) .. "}" return result end -- Example function to serialize a Lua table (KD-tree) to a string local function serializeTable_close(t, indent) indent = indent or 0 local result = "" local padding = string.rep(" ", indent) result = result .. "{\n" for k, v in pairs(t) do local key = (type(k) == "string" and string.format("%q", k)) or k if type(v) == "table" then result = result .. padding .. " " .. key .. " : " .. serializeTable(v, indent + 4) .. ",\n" else local value = (type(v) == "string" and string.format("%q", v)) or tostring(v) result = result .. padding .. " " .. key .. " : " .. value .. ",\n" end end result = result .. string.rep(" ", indent) .. "}" return result end -- Example function to serialize a Lua table (KD-tree) to a string local function serializeTable(t, indent) indent = indent or 0 local result = "" local padding = string.rep(" ", indent) result = result .. "{" for k, v in pairs(t) do local key = (type(k) == "string" and string.format("%q", k)) or k if type(v) == "table" then result = result .. padding .. " " .. key .. " = " .. serializeTable(v, indent + 0) .. "," else local value = (type(v) == "string" and string.format("%q", v)) or tostring(v) result = result .. padding .. " " .. key .. " = " .. value .. "," end end result = result .. string.rep(" ", indent) .. "}" return result end -- ============================================================================================================= local function GetDistanceToTrees(targetCoord) -- Nearest neighbor search example local distSquared = nil local nearest = nil local north = math.floor(targetCoord.x) local east = math.floor(targetCoord.z) --env.info(" - target coordinate " .. north .. " ".. east) local target = {north, east} --env.info(" Test north "..target[1] .. " east ".. target[2]) if kdtree then nearest, distSquared = nearestNeighborSearch(kdtree, target, 0, kdtree.point, squaredDistance(kdtree.point, target)) --print(string.format("Nearest neighbor to (%d, %d) is (%d, %d) with squared distance %d", target[1], target[2], nearest[1], nearest[2], dist)) else env.info(" ---- KD-Tree is empty ---") end -- Returning the squared distance (as finding root takes time) return distSquared end – Set up the KD-Tree env.info(" -- KD-Tree:") env.info(" - Loading the RAW Tree Datapoints") local points = LoadDataPointsTrees(treeDataPoints) env.info(" - Building the KD Tree") kdtree = buildKDTree(points, 0) To get a distance use the following: – Using the functions : distanceToTrees = GetDistanceToTrees(randomCoordinate) Before building the KD-Tree, you need to generate the RGB data for each coordinate on the map that is of interest. The python scripts follow: The 1stPython script captures the map data coordinates and RGB for each location. The 2nd Python script filters the RGB into suitabl buckets, that you can use in the KD-Tree The 3rd Python script can be used to reduce the number of data points and thus the time taken to build the KD-tree when first running a mission. # -- ========================================== -- # -- ============ DCS Tree Mapping ============ -- # -- ============ Script One ============ -- # -- ========================================== -- # # -- Mikey, "LittleNose" # -- October 2024 # # Script to take screenshots from DCS Map and determine the terrain / features # For me it is primarily used by my mission script to determine location of the indestructable DCS trees # This can be used as a database for searching against when spawning / moving ground units # # I use the data points file to check coordinates for distances from trees # The datafile takes a long time to read into DCS script, however, it only needs to do it once per server restart. # Within the DCS mission script, KD-Trees is the function I use for searching the data points file # https://www.baeldung.com/cs/k-d-trees # # # Thanks go out to the Martin from the SDCS server. # Here on the ED Forums, it is mentioned SDCS use "Anti Tree Technology": # https://forum.dcs.world/topic/346202-strategic-dcs-a-modern-persistent-pvp-combined-arms-driven-campaign/ # Although I don't know how they do it, knowing it's possible was the inspiration to try. # In the same vein, I hope that this spurs others into refining and helping the community. # # # -- =============== Instructions =============== -- # # Check your Python has the required libraries, and pip those you don't (eg: pip install pyautogui). # Open DCS in windowed mode, position the window top left of your screen # Open Python client in non-overlapping window # Open Mission Editor in DCS, and run a blank mission. # Move the compass rose out of the way # Set the script vaiables below # - coordinates top left and bottom right for the area wanted on the map # - the size per pixel (30m to 70m works well) # Run this python script - don't touch the mouse/keyboard until complete # Your mouse pointer will move around and teh screen will likely zoom and pan too, be patient. # # The csv file created is a list of coordinates and RGB values at those coordinates # # # # # ---------------------------------------------------------------- GLOBAL USER VARIABLES ----------- # Pixel size in m. 30-70 seems to work best. targetPixelSize = 30 # Specify the top left and bottom right coordinates of the area you want to capture # Keeping this to a smaller area reduces time in capturign data, but also when starting up the KD-Tree in mission # SYRIA : COMPLETE X vert, Z horiz #targetMapCoordTopLeft = [ 300000, -500000] #targetMapCoordBtmRight = [-375000, 435000] # SYRIA : CYPRUS X vert, Z horiz targetMapCoordTopLeft = [ 81500, -332000] targetMapCoordBtmRight = [-40000, -114000] # SYRIA : CENTRAL X vert, Z horiz #targetMapCoordTopLeft = [ 23155, -67000] #targetMapCoordBtmRight = [-187000, 253000] # Change this to match your visible map area (avoid the borders etc) xWindowLocations = [60, 2400] # left, right 60, 2400 yWindowLocations = [80, 1300] # top, bottom 100, 1300 # ---------------------------------------------------------------- END OF USER VARIABLES ----------- # ---------------------------------------------------------------- SYSTEM VARIABLES ---------------- workingFolder = "C:\\Users\\xbox\\Saved Games\\DCSTrees\\" rgbPartOutputFilePrefix = "rgb_part_" rgbAllOutputFilename = "rgb_all.csv" # Distance between pixels on the screen distPerPixelTopX = 0 distPerPixelBtmX = 0 distPerPixelLeftY = 0 distPerPixelRightY = 0 xCornerCoords = [[0,0],[0,0],[0,0],[0,0]] zCornerCoords = [[0,0],[0,0],[0,0],[0,0]] # ---------------------------------------------------------------- SYSTEM IMPORTS ---------------- import os import csv import math # pip install pandas import pandas as pd import numpy as np np.set_printoptions(suppress=True) #pip install matplotlib import matplotlib.pyplot as plt #pip install pyautogui import pyautogui #pip install keyboard import keyboard #pip install pyperclip import pyperclip #pip install pynput from pynput.mouse import Controller mouse = Controller() # ---------------------------------------------------------------- FUNCTIONS ---------------- def GetAreaGameCoordinates(): #print(" - Get Coordinates of the DCS Screen Grab") global xCornerCoords, zCornerCoords # loop 4 corners cornerIndices = [[0,0],[1,0],[0,1],[1,1]] for i in range(len(cornerIndices)): ix = cornerIndices[i][0] iy = cornerIndices[i][1] # Move Mouse to corner location of screen grab pyautogui.moveTo(xWindowLocations[ix],yWindowLocations[iy]) # Get Popup with coords keyboard.press('alt') pyautogui.click() keyboard.release('alt') # copy the coordinates pyautogui.moveTo(430, 375) pyautogui.click() # close the window pyautogui.moveTo(350, 375) pyautogui.click() # pu thte text into the clipboard copiedText = pyperclip.paste().split("\n")[0] #print(" - Text copied: {}".format(copiedText)) # Manipulate teh text to get teh coordinate values copiedTextSplit = copiedText.split(" ") if "+" in copiedTextSplit[1]: xCornerCoords[i]=(int(copiedTextSplit[1].split("+")[1])) elif "-" in copiedTextSplit[1]: xCornerCoords[i]=(int("-"+copiedTextSplit[1].split("-")[1])) if "+" in copiedTextSplit[2]: zCornerCoords[i]=(int(copiedTextSplit[2].split("+")[1])) elif "-" in copiedTextSplit[2]: zCornerCoords[i]=(int("-"+copiedTextSplit[2].split("-")[1])) def GetPixelSize(): print("\n - Get the pixel size") global distPerPixelTopX, distPerPixelBtmX, distPerPixelLeftY, distPerPixelRightY print(" Northing Easting") print(" - top left coord : {}, {}".format(xCornerCoords[0], zCornerCoords[0])) print(" - top right coord : {}, {}".format(xCornerCoords[1], zCornerCoords[1])) print(" - btm left coord : {}, {}".format(xCornerCoords[2], zCornerCoords[2])) print(" - btm right coord : {}, {}".format(xCornerCoords[3], zCornerCoords[3])) # coordinates NorthingDistanceLeft = xCornerCoords[2] - xCornerCoords[0] NorthingDistanceRight = xCornerCoords[3] - xCornerCoords[1] EastingDistanceTop = zCornerCoords[1] - zCornerCoords[0] EastingDistanceBtm = zCornerCoords[3] - zCornerCoords[2] distPerPixelTopX = EastingDistanceTop / pixelsWidth distPerPixelBtmX = EastingDistanceBtm / pixelsWidth distPerPixelLeftY = NorthingDistanceLeft / pixelsHeight distPerPixelRightY = NorthingDistanceRight / pixelsHeight print(" - Size of a Pixel Top X: {:.2f} m".format(distPerPixelTopX)) print(" - Size of a Pixel Btm X: {:.2f} m".format(distPerPixelBtmX)) print(" - Size of a Pixel Left Y: {:.2f} m".format(distPerPixelLeftY)) print(" - Size of a Pixel Right Y: {:.2f} m\n".format(distPerPixelRightY)) def GrabScreen(): print(" - Grabbing Screen ") left = xWindowLocations[0] top = yWindowLocations[0] width = xWindowLocations[1] - xWindowLocations[0] +1 height = yWindowLocations[1] - yWindowLocations[0] +1 #Grab image: imageScreen = pyautogui.screenshot(region=(left,top, width,height)) print(" - Captured Screen, pixels x{},y{} to x{}, y{}".format(xWindowLocations[0], yWindowLocations[0], xWindowLocations[1], yWindowLocations[1])) # Generate a List of the RGB values in format (r,g,b,transparency): pixel_values = list(imageScreen.getdata()) rgbPartOutputFilename = rgbPartOutputFilePrefix+str(screenX)+"_"+str(screenY)+".csv" try: os.remove(rgbPartOutputFilename) print(" - Deleting Existing file {}".format(rgbPartOutputFilename)) except: pass print(" - Existing file {} not found to delete".format(rgbPartOutputFilename)) fileOpenCSV = open(rgbPartOutputFilename, "a") #Save RGB to a file: print(" - Generating RGB {}, X{} Y{}".format(loopNumber, screenX, screenY)) eastingPixelCoord = 0 northingPixelCoord = 0 for i in range(len(pixel_values)): pixelColourUnit = [0,0,0] pixelColourUnit[0] = round(pixel_values[i][0], 3) pixelColourUnit[1] = round(pixel_values[i][1], 3) pixelColourUnit[2] = round(pixel_values[i][2], 3) pix_x = i % width pix_y = int((i-pix_x)/width) eastingPixelCoord = zCornerCoords[0] + (pix_x * distPerPixelTopX) northingPixelCoord = xCornerCoords[0] + (pix_y * distPerPixelLeftY) dataString = str(pix_x) + ", " + str(pix_y) + ", " + str(eastingPixelCoord) + ", " + str(northingPixelCoord) +", "+ str(pixelColourUnit[0])+ ", " + str(pixelColourUnit[1])+ ", " + str(pixelColourUnit[2]) + "\n" fileOpenCSV.write(dataString) print(" - Screen Coordinate Top Left N{:.1f}, E{:.1f}".format((xCornerCoords[0]), (zCornerCoords[0]))) print(" - Screen Coordinate Btm Right N{:.1f}, E{:.1f}".format(northingPixelCoord, eastingPixelCoord)) fileOpenCSV.close() print(" - Saved CSV File {}, {}\n".format(loopNumber, rgbPartOutputFilename)) def MoveTheMap(): print(" - Moving the Map for the Next Screen Grab {} {}".format(screenX, screenY)) global xCornerCoords, zCornerCoords pixelX1 = (xWindowLocations[0]+xWindowLocations[1])/2 pixelY1 = (yWindowLocations[0]+yWindowLocations[1])/2 pixelX2 = (xWindowLocations[0]+xWindowLocations[1])/2 pixelY2 = (yWindowLocations[0]+yWindowLocations[1])/2 if screenX == (xSections - 1): pixelsMoveY = -(pixelsHeight + 1) pixelY1 = yWindowLocations[1] pixelY2 = yWindowLocations[0] print(" - Moving the Map Screen South, {} pixels, from {} to {}".format(pixelsMoveY, pixelY1, pixelY2)) else: if screenY%2 == 0: # then it is even numbered row (row 0 = even) pixelsMoveX = pixelsWidth + 1 pixelX1 = xWindowLocations[1] pixelX2 = xWindowLocations[0] print(" - Moving the Map Screen East, {} pixels, from {} to {}".format(pixelsMoveX, pixelX1, pixelX2)) else: # even numbered row pixelsMoveX = -(pixelsWidth + 1) pixelX1 = xWindowLocations[0] pixelX2 = xWindowLocations[1] print(" - Moving the Map Screen West, {} pixels, from {} to {}".format(pixelsMoveX, pixelX1, pixelX2)) pyautogui.moveTo(pixelX1, pixelY1) pyautogui.mouseDown(button='left') pyautogui.moveTo(pixelX2, pixelY2, 1.5) pyautogui.mouseUp(button='left') print(" - Finished moving the map") def CombineTheFiles(): print("\n---------=== Combining the separate files into one ===---------") try: print(" - Deleting Existing Combined data file {}".format(rgbAllOutputFilename)) os.remove(rgbAllOutputFilename) except: print(" - No Existing file found to delete") print(" - Combinging Files:") for i in range(xSections*ySections): fileX = i % xSections fileY = int((i-fileX) / xSections) rgbPartOutputFilename = rgbPartOutputFilePrefix+str(fileX)+"_"+str(fileY)+".csv" print(" {}: {}".format(i+1, rgbPartOutputFilename)) fileCombined = open(rgbAllOutputFilename, 'a+') # append fileToAdd = open(rgbPartOutputFilename, 'r') # read fileCombined.write(fileToAdd.read()) # combine fileToAdd.close() fileCombined.close() print(" - Completed Aggregating the Files into {}".format(rgbAllOutputFilename)) # ---------------------------------------------------------------- MAIN ---------------- print("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n") print("\n\n\n\n\n\n\n\n\n---------=== Screen Grabber and RGB Pixelator ===--------- \n") # BASIC Setup os.chdir(workingFolder) # pixels pixelsWidth = xWindowLocations[1] - xWindowLocations[0] pixelsHeight = yWindowLocations[1] - yWindowLocations[0] # ------------------------------- NEW CODE def GetCurrentZoom(): GetAreaGameCoordinates() print(" - Image Grab Size, Pixels, X: {}, Y: {}".format(pixelsWidth+1, pixelsHeight+1)) GetPixelSize() print(" - Image Grab Size, Coords, Top Left : {}, {}".format(xCornerCoords[0], zCornerCoords[0])) print(" - Image Grab Size, Coords, Btm Right: {}, {}".format(xCornerCoords[3], zCornerCoords[3])) #pyautogui.scroll(-25) for i in range(10): GetCurrentZoom() averagePixelSize = (distPerPixelTopX + distPerPixelBtmX - distPerPixelLeftY - distPerPixelRightY)/4 ratioToTargtetPixels = targetPixelSize / averagePixelSize print(" Target Size: {}, Average: {:.2f}, Pixel Ratio: {:.3f}".format(targetPixelSize, averagePixelSize, ratioToTargtetPixels)) if averagePixelSize < 0.95 * targetPixelSize: print(" --- Zoom Out, Factor: {:.3f}".format(ratioToTargtetPixels)) mouse.scroll(0, -(2*ratioToTargtetPixels)) elif averagePixelSize > 1.05 * targetPixelSize: print(" --- Zoom In, Factor: {:.3f}".format(ratioToTargtetPixels)) mouse.scroll(0, 2/ratioToTargtetPixels) else: print(" -- Target Pixel size found") break def MoveTheMapToStartPoint(): print("\n -- Moving the Map to the Start Location {} {}".format(targetMapCoordTopLeft[0], targetMapCoordTopLeft[1])) targetX = targetMapCoordTopLeft[0] targetZ = targetMapCoordTopLeft[1] print(" - Target Locations: targetX {}, targetZ {}".format(targetX, targetZ)) GetAreaGameCoordinates() currentX = xCornerCoords[0] currentZ = zCornerCoords[0] print(" - Current Locations: currentX {}, currentZ {}".format(currentX, currentZ)) pixelSize = averagePixelSize targetDeltaX = (targetX - currentX) targetDeltaZ = (targetZ - currentZ) print(" - Target Delta DeltaX {:.1f}, DeltaZ {:.1f}".format(targetDeltaX, targetDeltaZ)) targetDeltaPixelsY = (targetX - currentX)/pixelSize targetDeltaPixelsX = (targetZ - currentZ)/pixelSize print(" - Target Delta Pixels DeltaX {:.1f}, DeltaY {:.1f}".format(targetDeltaPixelsX, targetDeltaPixelsY)) deltaXMultiple = abs(targetDeltaPixelsX / (xWindowLocations[1]-xWindowLocations[0])) deltaYMultiple = abs(targetDeltaPixelsY / (yWindowLocations[1]-yWindowLocations[0])) if targetDeltaPixelsX > 0: startPixelX = xWindowLocations[1] fullPixelX = xWindowLocations[0] else: startPixelX = xWindowLocations[0] fullPixelX = xWindowLocations[1] if targetDeltaPixelsY > 0: startPixelY = yWindowLocations[0] fullPixelY = yWindowLocations[1] else: startPixelY = yWindowLocations[1] fullPixelY = yWindowLocations[0] print(" - Pixels Start - X {:.1f}, Y {:.1f}".format(startPixelX, startPixelY)) print(" - Pixels Full - X {:.1f}, Y {:.1f}".format(fullPixelX, fullPixelY)) for i in range(math.floor(deltaXMultiple)): pyautogui.moveTo(startPixelX, startPixelY) pyautogui.mouseDown(button='left') pyautogui.moveTo(fullPixelX, startPixelY, 1.5) pyautogui.mouseUp(button='left') for j in range(math.floor(deltaYMultiple)): pyautogui.moveTo(startPixelX, startPixelY) pyautogui.mouseDown(button='left') pyautogui.moveTo(startPixelX, fullPixelY, 1.5) pyautogui.mouseUp(button='left') GetAreaGameCoordinates() currentX = xCornerCoords[0] currentZ = zCornerCoords[0] print(" - Current Locations: currentX {}, currentZ {}".format(currentX, currentZ)) pixelSize = averagePixelSize targetDeltaX = (targetX - currentX) targetDeltaZ = (targetZ - currentZ) print(" - Target Delta DeltaX {:.1f}, DeltaZ {:.1f}".format(targetDeltaX, targetDeltaZ)) targetDeltaPixelsY = (targetX - currentX)/pixelSize targetDeltaPixelsX = (targetZ - currentZ)/pixelSize print(" - Target Delta Pixels DeltaX {:.1f}, DeltaY {:.1f}".format(targetDeltaPixelsX, targetDeltaPixelsY)) if targetDeltaPixelsX > 0: deltaPixelsX = min(targetDeltaPixelsX, (xWindowLocations[1]-xWindowLocations[0])) startPixelX = xWindowLocations[1] endPixelX = startPixelX - deltaPixelsX else: deltaPixelsX = max(targetDeltaPixelsX, -(xWindowLocations[1]-xWindowLocations[0])) startPixelX = xWindowLocations[0] endPixelX = startPixelX - deltaPixelsX if targetDeltaPixelsY > 0: deltaPixelsY = min(targetDeltaPixelsY, (yWindowLocations[1]-yWindowLocations[0])) startPixelY = yWindowLocations[0] endPixelY = startPixelY + deltaPixelsY else: deltaPixelsY = max(targetDeltaPixelsY, -(yWindowLocations[1]-yWindowLocations[0])) startPixelY = yWindowLocations[1] endPixelY = startPixelY + deltaPixelsY print(" - deltaPixelsX : {:.1f}".format(deltaPixelsX)) print(" - deltaPixelsY : {:.1f}".format(deltaPixelsY)) print(" - Moving the Map by DeltaX {:.1f}, DeltaY {:.1f}".format(deltaPixelsX, deltaPixelsY)) pyautogui.moveTo(startPixelX, startPixelY) pyautogui.mouseDown(button='left') pyautogui.moveTo(endPixelX, endPixelY, 1.5) pyautogui.mouseUp(button='left') GetAreaGameCoordinates() print(" - Moved the map towards start point") reqdMapWidth = targetMapCoordTopLeft[1] - targetMapCoordBtmRight[1] reqdMapHeight = targetMapCoordTopLeft[0] - targetMapCoordBtmRight[0] imageWidth = zCornerCoords[3] - zCornerCoords[0] imageHeight = xCornerCoords[0] - xCornerCoords[3] print("\n -- Required Width {:.1f},\t Height {:.1f}".format(reqdMapWidth, reqdMapHeight)) print(" -- Image Width {:.1f},\t Height {:.1f}".format(imageWidth, imageHeight)) reqdSectionsHorizontal = abs(reqdMapWidth / imageWidth) reqdSectionsVertical = abs(reqdMapHeight / imageHeight) xSections = math.ceil(reqdSectionsHorizontal) ySections = math.ceil(reqdSectionsVertical) # Set these to unity for testing a single screen cap #xSections = 1 #ySections = 1 numberOfSections = xSections * ySections print(" -- Required Snapshots from screen Horiz: {:.1f}, Vert: {:.1f}".format(reqdSectionsHorizontal, reqdSectionsVertical)) print(" Actual Snapshots from screen Horiz: {:.1f}, Vert: {:.1f}".format(xSections, ySections)) MoveTheMapToStartPoint() # ------------------------------------- END OF NEW SECTION ----------- # -- MAIN LOOP -- for loopNumber in range(numberOfSections): # variables to know the screen indexed location screenX = loopNumber % xSections screenY = int((loopNumber-screenX) / xSections) print("\n---------=== New Screen Grab, Index {} {} ===---------".format(screenX, screenY)) # run the functions GetAreaGameCoordinates() GetPixelSize() GrabScreen() if loopNumber != numberOfSections-1: MoveTheMap() else: print(" - Last Screen Complete, no need to move the map") # Gather all rgb files and gather into one file CombineTheFiles() # Filter the file, and generate a "Trees" file #FilterForTrees() print(" - Image Grab Size, Pixels, X: {}, Y: {}".format(pixelsWidth+1, pixelsHeight+1)) print(" - {} Images, Pixels, X: {}, Y: {}".format(numberOfSections, numberOfSections*(pixelsWidth+1), numberOfSections*(pixelsHeight+1))) print(" - Total Number of Pixels: {}".format(numberOfSections*(pixelsWidth+1) *(pixelsHeight+1))) print("\n ------ === Script Complete === ------\n\n") Second Python Script to Filter the data # -- ========================================== -- # -- ============ DCS Tree Mapping ============ -- # -- ============ Script Two ============ -- # -- ========================================== -- # # -- Mikey, "LittleNose" # -- July 2025 plotBuckets = False import os import csv import math workingFolder = "C:\\Users\\xbox\\Saved Games\\DCSTrees\\" os.chdir(workingFolder) rgbAllOutputFilename = "rgb_all.csv" outputFilenameWater = "filtered_water.csv" outputFilenameRiver = "filtered_river.csv" outputFilenameTrees = "filtered_trees.csv" outputFilenameTowns = "filtered_towns.csv" outputFilenameRoads = "filtered_roads.csv" outputFilenameLand = "filtered_land.csv" import os import csv import math import numpy as np import matplotlib.pyplot as plt def FilterForRGB(valR, valG, valB, fileNameOut, tolRange, tolDiff, bins=8, top_n=20, show_plot=True, save_plot=None, rgbAllOutputFilename="rgb_all.csv"): print(f"\n---------=== Filtering for RGB ===--------- {fileNameOut}") matchCount = 0 matched_colors = [] # collect (r, g, b) for matched rows try: print(f" - Deleting existing file {fileNameOut}") os.remove(fileNameOut) except FileNotFoundError: print(" - No existing file found to delete") print(" - Filtering...") with open(rgbAllOutputFilename, 'r', newline='') as infile, \ open(fileNameOut, 'w', newline='') as outfile: reader = csv.reader(infile) writer = csv.writer(outfile) for row in reader: try: east, north = round(float(row[2])), round(float(row[3])) r, g, b = float(row[4]), float(row[5]), float(row[6]) except (IndexError, ValueError): # skip malformed rows continue # Euclidean distance in RGB space distance = math.sqrt((r - valR)**2 + (g - valG)**2 + (b - valB)**2) if distance <= tolRange: absoluteRG = abs(r - g) # equivalent to sqrt((r-g)^2) if absoluteRG < tolDiff: writer.writerow([east, north]) matched_colors.append((r, g, b)) matchCount += 1 print(f" - {matchCount} points found in Filtered data in file: {fileNameOut}\n") if plotBuckets == True: if matchCount == 0: print(" - No matches; skipping histogram/plot.") return # Prepare bins: allow scalar or per-channel if isinstance(bins, int): bins_tuple = (bins, bins, bins) else: try: bins_tuple = tuple(bins) if len(bins_tuple) != 3: raise ValueError except Exception: raise ValueError("bins must be int or iterable of three ints") colors_arr = np.array(matched_colors) # shape (N, 3) # Define edges assuming RGB in [0, 255]; adjust if your scale differs edges = [np.linspace(0, 255, num=b+1) for b in bins_tuple] # 3D histogram hist, edges_out = np.histogramdd(colors_arr, bins=edges) # Flatten and get top N buckets flat_hist = hist.flatten() nonzero_idxs = np.nonzero(flat_hist)[0] if nonzero_idxs.size == 0: print(" - No nonzero buckets despite matches; unexpected. Aborting plot.") return top_n = min(top_n, nonzero_idxs.size) top_indices = np.argsort(flat_hist[nonzero_idxs])[::-1][:top_n] selected_flat_idxs = nonzero_idxs[top_indices] counts = flat_hist[selected_flat_idxs] # Compute the representative (center) color for each selected bucket bucket_shape = hist.shape # (bins_r, bins_g, bins_b) centers = [] for flat_idx in selected_flat_idxs: r_idx, g_idx, b_idx = np.unravel_index(flat_idx, bucket_shape) # compute center of each bin r_center = (edges[0][r_idx] + edges[0][r_idx + 1]) / 2 g_center = (edges[1][g_idx] + edges[1][g_idx + 1]) / 2 b_center = (edges[2][b_idx] + edges[2][b_idx + 1]) / 2 centers.append((r_center, g_center, b_center)) # Build bar chart fig, ax = plt.subplots(figsize=(max(6, top_n * 0.3), 4)) bar_positions = np.arange(len(counts)) bar_labels = [f"#{i+1}" for i in range(len(counts))] # simple labels # Normalize colors to [0,1] for matplotlib bar_colors = [(r/255, g/255, b/255) for r, g, b in centers] bars = ax.bar(bar_positions, counts, color=bar_colors, edgecolor='black') ax.set_xticks(bar_positions) ax.set_xticklabels(bar_labels, rotation=45, ha='right') ax.set_ylabel("Count in Bucket") ax.set_title(f"Top {len(counts)} RGB Buckets Passing Filter") # Annotate with hex color and count for idx, bar in enumerate(bars): r, g, b = centers[idx] rgb_label = f"({int(r)},{int(g)},{int(b)})" ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height(), f"{int(counts[idx])}\n{rgb_label}", ha='center', va='bottom', fontsize=7) plt.tight_layout() if save_plot: try: fig.savefig(save_plot, dpi=150) print(f" - Saved histogram plot to {save_plot}") except Exception as e: print(f" - Failed to save plot: {e}") if show_plot: plt.show() plt.close(fig) # Caucasus Filters #RGBWater = [126, 185, 198] #RGBRiver = [ 55, 92, 140] #RGBTrees = [ 90, 138, 93] #RGBTowns = [198, 131, 0] #RGBRoads = [170, 80, 0] # Syria Filters RGBWater = [47, 143, 143] RGBRiver = [50, 160, 220] RGBTrees = [120, 135, 60] RGBTowns = [250, 154, 61] RGBRoads = [143, 60, 30] RGBLand = [220, 111, 45] tolRGBWater = [ 20, 100] tolRGBRiver = [100, 999] tolRGBTrees = [ 75, 10] tolRGBTowns = [ 40, 75] tolRGBRoads = [ 30, 200] tolRGBLand = [175, 75] FilterForRGB(RGBWater[0], RGBWater[1], RGBWater[2], outputFilenameWater, tolRGBWater[0], tolRGBWater[1]) FilterForRGB(RGBRiver[0], RGBRiver[1], RGBRiver[2], outputFilenameRiver, tolRGBRiver[0], tolRGBRiver[1]) FilterForRGB(RGBTrees[0], RGBTrees[1], RGBTrees[2], outputFilenameTrees, tolRGBTrees[0], tolRGBTrees[1]) FilterForRGB(RGBTowns[0], RGBTowns[1], RGBTowns[2], outputFilenameTowns, tolRGBTowns[0], tolRGBTowns[1]) FilterForRGB(RGBRoads[0], RGBRoads[1], RGBRoads[2], outputFilenameRoads, tolRGBRoads[0], tolRGBRoads[1]) FilterForRGB( RGBLand[0], RGBLand[1], RGBLand[2], outputFilenameLand, tolRGBLand[0], tolRGBLand[1]) Finally, once you capture your data points, you can reduce the filesize by changing the resolution. I find 75m data points works reasonably well. # -- ========================================== -- # -- ============ DCS Tree Mapping ============ -- # -- ============ Script Three ============ -- # -- ========================================== -- import os import pandas as pd import matplotlib.pyplot as plt workingFolder = "C:\\Users\\xbox\\Saved Games\\DCSTrees\\" os.chdir(workingFolder) # Load data from input file #input_file = "filtered_water.csv" #output_file = "filtered_water_reduced.csv" #input_file = "filtered_river.csv" #output_file = "filtered_river_reduced.csv" #input_file = "filtered_trees.csv" #output_file = "filtered_trees_reduced.csv" #input_file = "filtered_towns.csv" #output_file = "filtered_towns_reduced.csv" input_file = "filtered_roads.csv" output_file = "filtered_roads_reduced.csv" grid_size = 75 # Adjust for more/less accuracy vs reduction # Read the original CSV data = pd.read_csv(input_file, header=None, names=["x", "y"]) # Dictionary to store one point per grid cell grid_dict = {} # Grid-based filtering for x, y in data.itertuples(index=False): gx = x // grid_size gy = y // grid_size key = (gx, gy) if key not in grid_dict: grid_dict[key] = (x, y) # Create reduced DataFrame reduced_df = pd.DataFrame(list(grid_dict.values()), columns=["x", "y"]) # Save to output file reduced_df.to_csv(output_file, index=False, header=False) # Step 4: Plot both original and reduced datasets side-by-side fig, axes = plt.subplots(1, 2, figsize=(14, 6), sharex=True, sharey=True) # Original data plot axes[0].scatter(data["x"], data["y"], s=1) axes[0].set_title(f"Original Data ({len(data)} points)") axes[0].set_xlabel("X") axes[0].set_ylabel("Y") axes[0].grid(True) # Reduced data plot axes[1].scatter(reduced_df["x"], reduced_df["y"], s=1, color='orange') axes[1].set_title(f"Reduced Data ({len(reduced_df)} points)") axes[1].set_xlabel("X") axes[1].grid(True) plt.suptitle("Original vs Reduced Data Points", fontsize=14) plt.tight_layout(rect=[0, 0, 1, 0.96]) plt.show() Hope you have fun. Here's an example of the Rivers in Caucasus at 100m intervals. null Edited: Updated Python script. Now the zooming, start location, and number of screen captures are automatic (1st Python script), and the filtering in the 2nd Python script has more controls added, plus it can present the colour "buckets" that can be an aid to working out the filters to use. Edited Thursday at 05:38 PM by LittleNose Updated Python Scripting. 2
LittleNose Posted Wednesday at 03:52 PM Author Posted Wednesday at 03:52 PM Attached are the Caucasus Trees dataset at 100m spacing to play with filtered_trees_reduced.zip
LittleNose Posted Wednesday at 05:21 PM Author Posted Wednesday at 05:21 PM Image showing the script avoiding trees null
buur Posted Wednesday at 05:39 PM Posted Wednesday at 05:39 PM Hm... if we can get the coordinates of all map objects, than we should be able to make ore own maps with these data ...
LittleNose Posted Thursday at 05:47 PM Author Posted Thursday at 05:47 PM I'm not sure what can be done with the data, people come up with extraordinary things sometimes. Hopefully it helps someone do something they were previously struggling with. I've updated it today to make the map capture script much easier to use. It will now auto-zoom and capture the desired area.
Recommended Posts