# Requires at least Python 2.5, PIL, numpy # Minecraft save file creator from kinect images created by getSnapshot.py # By: Nathan Viniconis # # You can use this code freely without any obligation to the original or myself import colorsys import ctypes import Image import nbt import numpy import os.path import sys import random import zipfile from StringIO import StringIO from nbt import * from math import sqrt from MinecraftKinectUtils import * ####################### # Editable globals ####################### # toggles logging to console verbose = True # set this to true if you're testing the image via the mapImage.png output prior to actually editing the world (do this!) testEdit = False # true to test, false to actually modify the world! # should the colors be matched, or is this just a statue? colorMatch = True # the files that contain the depth and color information from the Kinect depthImageName = "F:\Programming\Talon\Kinect\Images\\00066OutputDepth.tiff" colorImageName = "F:\Programming\Talon\Kinect\Images\\00066OutputColorBlurPix1.png" # path to the MineCraft save directory pathToSaves = "C:\Users\Revrick\AppData\Roaming\.minecraft\saves\World2" savedZipName = "savedRegions.zip" # These are the spreads in meters that the kinect reads # ** have to change these in order to get a correct cutout of the models in the room! minmaxX = [-.75,.75] #[-.75,.75] for PC, [-1.25,1.25] for room floor = .45# .45 for at PC, .85 for room.. Note: in meters and y is inverted. minmaxZ = [.8, 1.6]# [.8,1.6]for at PC, [1.15,2.55] for room: # number of blocks to have per meter blocksPerMeter = 130 floorBlock = 55# in blocks, 0-127, 63 is sea level # the X:0,Z:0 corner of this chunk will be the center of the projection centerChunkX = -5 centerChunkZ = -6 markCenter = False # puts a column of glass at the center, used for testing # Possible blocks in (Name, ID, (RGB1,RGB2,..),Data) #RGBs are used to color match. possibleBlocks = ( \ ("Smooth Stone", 1, ( \ (125,125, 125),),0), \ ("Dirt", 3, ( \ (133,96,66),),0), \ ("Cobblestone", 4, ( \ (117,117,117),),0), \ ("Wooden Plank", 5, ( \ (156,127,78),),0), \ ("Bedrock", 7, ( \ (83,83,83),),0), \ #("Lava", 11, ( \ # (255,200,200),),0), \ ("Sand", 12, ( \ (217,210,158),),0), \ ("Gravel", 13, ( \ (136, 126, 125),),0), \ ("Gold Ore", 14, ( \ (143,139,124),),0), \ ("Iron Ore", 15, ( \ (135,130,126),),0), \ ("Coal Ore", 16, ( \ (115,115,115),),0), \ ("Wood", 17, ( \ (154,125,77),),0), \ ("Sponge", 19, ( \ (182,182,57),),0), \ #("Glass", 20, ( \ # (60,66,67),),0), \ ("White Wool", 35, ( \ (221,221,221),),0), \ ("Orange Wool", 35, ( \ (233,126,55),),1), \ ("Magenta Wool", 35, ( \ (179,75,200),),2), \ ("Light Blue Wool", 35, ( \ (103,137,211),),3), \ ("Yellow Wool", 35, ( \ (192,179,28),),4), \ ("Light Green Wool", 35, ( \ (59,187,47),),5), \ ("Pink Wool", 35, ( \ (217,132,153),),6), \ ("Dark Gray Wool", 35, ( \ (66,67,67),),7), \ ("Gray Wool", 35, ( \ (157,164,165),),8), \ ("Cyan Wool", 35, ( \ (39,116,148),),9), \ ("Purple Wool", 35, ( \ (128,53,195),),10), \ ("Blue Wool", 35, ( \ (39,51,153),),11), \ ("Brown Wool", 35, ( \ (85,51,27),),12), \ ("Dark Green Wool", 35, ( \ (55,76,24),),13), \ ("Red Wool", 35, ( \ (162,44,42),),14), \ ("Black Wool", 35, ( \ (26,23,23),),15), \ ("Gold", 41, ( \ (249,236,77),),0), \ ("Iron", 42, ( \ (230,230,230),),0), \ ("TwoHalves", 43, ( (159,159,159),),0), ("Brick", 45, ( \ (155,110,97),),0), \ #("TNT", 46, ( \ # (200,50,50),),0), \ ("Mossy Cobblestone", 48, ( \ (90,108,90),),0), \ ("Obsidian", 49, ( \ (20,18,29),),0), \ ("Diamond Ore", 56, ( \ (129,140,143),),0), \ ("Diamond Block", 57, ( \ (99,219,213),),0), \ ("Workbench", 58, ( \ (107,71,42),),0), \ ("Redstone Ore", 73, ( \ (132,107,107),),0), \ #("Ice", 79, ( \ # (125,173,255),),0), \ ("Snow Block", 80, ( \ (239,251,251),),0), \ ("Clay", 82, ( \ (158,164,176),),0), \ ("Jukebox", 84, ( \ (107,73,55),),0), \ ("Pumpkin", 86, ( \ (192,118,21),),0), \ ("Netherrack", 87, ( \ (110,53,51),),0), \ ("Soul Sand", 88, ( \ (84,64,51),),0), \ ("Glowstone", 89, ( \ (137,112,64),),0) \ ) # ///////////////////////////////////////////////////////////////////////////// # Updates a specific block in a chunk to a new type # Special is only checked if toSetBlock is None # special = 0 to clear # special = 1 for random # ///////////////////////////////////////////////////////////////////////////// def updateBlock(chunkData, Xb, Yb, Zb, toSetBlock, special=0): if (chunkData == None): print " Chunk edited before loaded!" if (Yb < 0 or Yb > 127): return # Y can go out of range, just skip the update # find the ID of the block blockID = Yb + ( Zb * 128 + ( Xb * 128 * 16) ) # determine if it's the first or second half of the bytes for the 4-bit components halfByteOrigID = int(blockID/2) lowerHalfOfOrigByte = (blockID % 2 == 0) # set the chunk to dirty as it is being modified chunkData["Dirty"] = True if (toSetBlock == None): # if a random statue block... if (special == 1): newType = random.randint(0,2) if (newType == 0): chunkData["Blocks"][blockID] = 1 # smooth stone if (newType == 1): chunkData["Blocks"][blockID] = 48 # mossy cobblestone if (newType == 2): chunkData["Blocks"][blockID] = 4 # cobblestone # otherwise, clear it else: chunkData["Blocks"][blockID] = 0 # if clearing to air, set to very bright so things are easier to see set4bitProperty(blockID, chunkData["SkyLight"], 15) else: # set to the particular block blockData = toSetBlock[3] try: chunkData["Blocks"][blockID] = toSetBlock[1] except: print "BlockID out of range: " + str(blockID) + "XYZ=" + str(Xb) + "," + str(Yb) + "," + str(Zb) sys.exit(0) # set the data of the block set4bitProperty(blockID, chunkData["Data"], toSetBlock[3]) # if not air, set the light of the block to 15! This will make # shading incorrect, but it's better than a black blob set4bitProperty(blockID, chunkData["SkyLight"], 15) return 1 # ///////////////////////////////////////////////////////////////////////////// # Calculates distance between two HLS colors # ///////////////////////////////////////////////////////////////////////////// def getColorDist(colorRGB, blockRGB): # RGB manhatten distance return sqrt( pow(colorRGB[0]-blockRGB[0],2) + pow(colorRGB[1]-blockRGB[1],2) + pow(colorRGB[2]-blockRGB[2],2)) # ///////////////////////////////////////////////////////////////////////////// # For a given RGB color, determines which block should represent it # ///////////////////////////////////////////////////////////////////////////// def getBlockFromColor(RGB): # find the closest color smallestDistIndex = -1 smallestDist = 300000 curIndex = 0 for block in possibleBlocks: for blockRGB in block[2]: curDist = getColorDist(RGB, blockRGB) if (curDist < smallestDist): smallestDist = curDist smallestDistIndex = curIndex curIndex = curIndex + 1 if (smallestDistIndex == -1): return -1 return possibleBlocks[smallestDistIndex] # ///////////////////////////////////////////////////////////////////////////// # false if the point is outside the allotted range # ///////////////////////////////////////////////////////////////////////////// def trimOutOfBounds(P3D): if (P3D[0] < minmaxX[0] or P3D[0] > minmaxX[1]): return False if (P3D[1] > floor): return False if (P3D[2] < minmaxZ[0] or P3D[2] > minmaxZ[1]): return False return True # ///////////////////////////////////////////////////////////////////////////// # given a X,Y,Z in meters, return a X,Y,Z in MC Blocks # ///////////////////////////////////////////////////////////////////////////// def getBlockCoord(P3D, blocksPerMeter, xBlockOffset, zBlockOffset): # trim any out of range if (trimOutOfBounds(P3D) == False): return None spreadX = minmaxX[1] - minmaxX[0] percX = (P3D[0] - minmaxX[0]) / spreadX maxBlocksX = int((minmaxX[1] - minmaxX[0]) * blocksPerMeter + .5) curBlockX = xBlockOffset + int(percX * maxBlocksX + .5) upAmt = floor - float(P3D[1]) if (upAmt >= 0): # we know how high it is compared to the floor, but how many blocks is that? curBlockY = int(upAmt * blocksPerMeter + .5) else: curBlockY = -1 spreadZ = minmaxZ[1] - minmaxZ[0] percZ = (P3D[2] - minmaxZ[0]) / spreadZ maxBlocksZ = int((minmaxZ[1] - minmaxZ[0]) * blocksPerMeter + .5) curBlockZ = zBlockOffset + int(percZ * maxBlocksZ + .5) return [curBlockX, curBlockY, curBlockZ] # ///////////////////////////////////////////////////////////////////////////// # Calculate the X,Y coordinate of the RGB pixel given a depth and 3D location # the depth image (info from: http://nicolas.burrus.name/index.php/Research/KinectCalibration) # ///////////////////////////////////////////////////////////////////////////// def rgb_mapped_coord(pointInfo): # extract the depth and 3D point from the info passeed in depth = pointInfo[0] P3D = (pointInfo[1],pointInfo[2],pointInfo[3]) # kinect rotation/translation matricies R = [ 9.9984628826577793e-01, 1.2635359098409581e-03, -1.7487233004436643e-02, -1.4779096108364480e-03,9.9992385683542895e-01, -1.2251380107679535e-02,1.7470421412464927e-02, 1.2275341476520762e-02,9.9977202419716948e-01 ] T = [ 1.9985242312092553e-02,-7.4423738761617583e-04,-1.0916736334336222e-02] # calc R.P3D RP3D = [ R[0]*P3D[0]+R[1]*P3D[1]+R[2]*P3D[2], R[3]*P3D[0] + R[4]*P3D[1] + R[5]*P3D[2], R[6]*P3D[0]+R[7]*P3D[1]+R[8]*P3D[2]] # calc RP3D + T RP3DT = [RP3D[0] + T[0], RP3D[1] + T[1], RP3D[2] + T[2]] # constants fx_rgb = 5.2921508098293293e+02 fy_rgb = 5.2556393630057437e+02 cx_rgb = 3.2894272028759258e+02 cy_rgb = 2.6748068171871557e+02 # calc the mapped RGB (w/round) rgbX = int((RP3DT[0] * fx_rgb / RP3DT[2]) + cx_rgb + .5) rgbY = int((RP3DT[1] * fy_rgb / RP3DT[2]) + cy_rgb + .5) return rgbX,rgbY # ///////////////////////////////////////////////////////////////////////////// # calc the real startChunks so it is centered at the NW corner of the designated of the projection # note, the X,Z coordinates are flipped in relation to the image # ///////////////////////////////////////////////////////////////////////////// def calcRelativeStartChunk(blocksPerMeter): xBlockOffset = 0 zBlockOffset = 0 # calc the real startChunkZ of the projection numBlocksX = int((minmaxX[1]-minmaxX[0]) * blocksPerMeter + .5) + 1 startChunkZ = centerChunkZ - int(int(numBlocksX/2) / 16) if ((numBlocksX/2) % 16 != 0): startChunkZ -= 1 # now see where within the chunk we should start xBlockOffset = 16 - (int(numBlocksX/2) % 16) # calc the real startChunkX of the projection numBlocksZ = int((minmaxZ[1]-minmaxZ[0]) * blocksPerMeter + .5) + 1 startChunkX = centerChunkX - int(int(numBlocksZ/2) / 16) if ((numBlocksZ/2) % 16 != 0): startChunkX -= 1 # now see where within the chunk we should start zBlockOffset = 16 - (int(numBlocksZ/2) % 16) return (startChunkX, xBlockOffset, startChunkZ, zBlockOffset) # ///////////////////////////////////////////////////////////////////////////// # Loads the images, traverses the depth and rbg images, and saves the minecraft world # ///////////////////////////////////////////////////////////////////////////// def processImages(worldPath, zipName, colorImageName, depthImageName, blocksPerMeter): if (verbose): print "\n**************" print " Begin processing of Kinect image " print " vars:" print " Color Img: " + colorImageName print " Depth Img: " + depthImageName print " Save Path: " + worldPath print " Center chunk(X,Z): (" + str(centerChunkX) + "," + str(centerChunkZ) + ")" print " Blocks per Meter: " + str(blocksPerMeter) print " Img min/max X (width in meters): (" + str(minmaxX[0]) + " , " + str(minmaxX[1]) + ")" print " Img min/max Z (depth in meters): (" + str(minmaxZ[0]) + " , " + str(minmaxZ[1]) + ")" print " Statue Floor: " + str(floorBlock) print " Saving changes: " + str(testEdit == False) print "***************\n" zipPath = worldPath + "\\" + zipName # if the saved file already exists, delete it if( os.path.isfile(zipPath) == True): print "Removing old save file..." os.remove(zipPath) # create the zip file for use backupZipFile = zipfile.ZipFile(zipPath, "w") # determine where the image drawing begins (startChunkX, xBlockOffset, startChunkZ, zBlockOffset) = calcRelativeStartChunk(blocksPerMeter) # read in the images colorImage = Image.open(colorImageName) width,height = colorImage.size # Calc how many [X,Z] (inclusive) chunks are needed xCount = int((minmaxX[1] - minmaxX[0]) * blocksPerMeter + .5)+1 zCount = int((minmaxZ[1] - minmaxZ[0]) * blocksPerMeter + .5)+1 if (verbose): print "Blocks required [X,Z]: [" + str(xCount) + "," + str(zCount) + "]" # determine how many chunks are required (Note: X,Z orientation is flipped) endChunkZ = startChunkZ + xCount/16 + 1 spreadZ = endChunkZ - startChunkX + 1 endChunkX = startChunkX + zCount/16 + 1 spreadX = endChunkX - startChunkZ + 1 # make sure all the chunks required exist in the directed too save directory if (verbose): print "Check required chunks[Xs,Zs][Xe,Ze]: [" + str(startChunkX) + "," + str(startChunkZ) + "][" + str(endChunkX) + "," + str(endChunkZ) + "]" #Load all the chunks (may want to do this in pieces if it gets large enough) chunks = getChunksFromRegionsForRange(worldPath, startChunkX, endChunkX, startChunkZ, endChunkZ, backupZipFile) if (chunks == None): print "Some required chunks don't exist!" sys.exit(0) # get all the 3D locations in (depth, X, Y, Z) all in meters for each pixel of the image points3D = getAll3dPoints(depthImageName) # create a output image for color map testing mapImage = Image.new("RGB",(xCount+17,128)) if (verbose): print "Processing Chunks..." # iterate across X first for Xp in range(width): #iterate across Y for Yp in range(height): index = Yp * width + Xp # get the X,Y,Z for this 3d location P3D = (points3D[index][1], points3D[index][2], points3D[index][3]) if (trimOutOfBounds(P3D) == False): continue # Find the real X and Y for the color image based on rectified image rgbXY = rgb_mapped_coord(points3D[index]) # If color data is out of bounds of the image, skip if (rgbXY[0] < 0 or rgbXY[1] < 0 or rgbXY[0] >= width or rgbXY[1] >= height): continue # get the RGB for this 3d location rgb = colorImage.getpixel((rgbXY[0], rgbXY[1])) # get the block coordinate for this 3D location block3D = getBlockCoord(P3D, blocksPerMeter, xBlockOffset, zBlockOffset) if (block3D == None or block3D[1] > 127 or block3D[1] < 0): continue # determine which loaded chunk the block is in Xc = block3D[2] / 16 Zc = block3D[0] / 16 try: chunk = chunks[Xc][Zc] except: print "OOR: " + str(Xc) + ":" + str(Zc) + " " + str(block3D[2]) sys.exit(0) # update that chunk if (colorMatch == True): # get the block type that matches the color block = getBlockFromColor(rgb) updateBlock(chunk, block3D[2] % 16, floorBlock + block3D[1], block3D[0] % 16, block) else: updateBlock(chunk, block3D[2] % 16, floorBlock + block3D[1], block3D[0] % 16, None, 1) # create a test image try: if (colorMatch == True): mapImage.putpixel((block3D[0],127-block3D[1]), block[2][0]) else: mapImage.putpixel((block3D[0],127-block3D[1]), (150, 150, 150)) except: print "Out of range: " + str(block3D[0]) + ":" + str(127-block3D[1]) # save and null the chunks if (testEdit == False): if (verbose): print "Saving chunks..." for curStrip in chunks: for curChunk in curStrip: saveDataForChunk(worldPath, curChunk) # save the color map out mapImage.save("mapImage.bmp", "BMP") # set time of day to noon (because i can) updateTime(worldPath) return 1 # ///////////////////////////////////////////////////////////////////////////// # Main controller, do the necessary processing # ///////////////////////////////////////////////////////////////////////////// def main(): if (testEdit): print "***** Testing: Not actually Saving!! ********" # Go through the images and modify the MineCraft save accordingly processed = processImages(pathToSaves,savedZipName, colorImageName, depthImageName, blocksPerMeter) if (processed != 1): print "Did not properly save: " + str(processed) else: print "Added successfully" if __name__ == "__main__": main()