I'm working on a custom tiled map loader. Seems to work fine, I don't get any errors, but the screen only shows up 1 tile of each type.
this is the file structure:
/main.py
/other/render2.py
/other/render.py
here's the render2.py file:
import pyglet, json
from pyglet.window import key
from pyglet.gl import *
from ConfigParser import SafeConfigParser
from cocos.layer import *
from cocos.batch import *
from cocos.sprite import Sprite
class renderer( Layer ):
#init function
def __init__(self):
super( renderer, self ).__init__()
#call function, returns the map as a list of sprites, and coordinates
def __call__(self, mapname):
#runs the map file parser
parser = SafeConfigParser()
#reads the map file
try:
world = parser.read('maps/'+mapname+'.txt')
print world
except IOError:
return
#These variables the config from the map file
tileSize = int(parser.get('config', 'tilesize'))
layers = int(parser.get('config', 'layers'))
mapList = []
#the super mega advanced operation to render the mapList
for i in range(0,layers):
layer = json.loads(parser.get('layer'+str(i), 'map'))
tileType = parser.get('layer'+str(i), 'tiletype')
nTiles = int(parser.get('layer'+str(i), 'tiles'))
tileSet = []
#this over here loads all 16 tiles of one type into tileSet
for n in range(0, nTiles):
tileSet.append(Sprite("image/tiles/"+tileType+"/"+str(n)+".png", scale = 1, anchor = (0,0)))
for x in range(0, len(layer)):
for y in range(0, len(layer[x])):
X = (x*tileSize)
Y = (y*tileSize)
total = [tileSet[layer[x][y]], i, X, Y]
print layer[x][y], tileSet[layer[x][y]]
mapList.append(total)
return mapList
This is an example of what this returns :
[<cocos.sprite.Sprite object at 0x060910B0>, 0, 0,0 ]
[<cocos.sprite.Sprite object at 0x060910B0> , 0, 64,64 ]
It returns a huge list with a lot of sublists like these in it.
when I call it from the main.py file, it only draws the last tile of each kind.
here's the main.py file:
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
import pyglet
import threading,time
from pyglet import clock
from pyglet.gl import *
from cocos.director import *
from cocos.menu import *
from cocos.scene import *
from cocos.layer import *
from cocos.actions import *
from cocos.batch import *
from cocos.sprite import Sprite
from other.render2 import renderer
import random; rr = random.randrange
class Background(ScrollableLayer):
def __init__(self):
super(Background, self).__init__()
world = renderer()
bg = world('sampleidea')
batch = BatchNode()
for i in range(0, len(bg)):
l= bg[i][1]
x= bg[i][2]
y= bg[i][3]
spr = bg[i][0]
spr.position =(x,y)
batch.add(spr, z = l)
self.add(batch)
class Menu(Layer):
def __init__(self):
super(Menu, self).__init__()
title = Sprite('image/title.png' )
title.position = (400,520)
self.add( title )
def start():
director.set_depth_test()
background = Background()
menu = Menu()
scene = Scene(background, menu)
return scene
def init():
director.init( do_not_scale=True, resizable=True, width=1280, height=720)
def run(scene):
director.run( scene )
if __name__ == "__main__":
init()
s = start()
run(s)
What am I doing wrong? I have an older render.py, which does work, but I remade it since it loaded each sprite file for each tile. That took way to long to load on big maps.
This is the old render.py I've been using before.
It's quite different since it used different map files too.
import pyglet, json
from pyglet.window import key
from pyglet.gl import *
from ConfigParser import SafeConfigParser
from cocos.layer import *
from cocos.batch import *
from cocos.sprite import Sprite
class renderer( Layer ):
def __init__(self):
super( renderer, self ).__init__()
def __call__(self, mapname):
parser = SafeConfigParser()
try:
world = parser.read('maps/'+mapname+'.txt')
print world
except IOError:
print("No world file!")
return
tilesize = json.loads(parser.get('data', 'tilesize'))
world = json.loads(parser.get('data', 'world'))
maplist = []
for l in range(len(world)):
for x in range(len(world[l])):
for y in range(len(world[l][x])):
if world[l][x][y] != None:
foldername = str(world[l][x][y][0])
imagename = str(world[l][x][y][1])
spr = Sprite("image/tiles/"+foldername+"/"+imagename+".png", scale = 1, anchor = (0,0))
X = (x*tilesize)
Y = (y*tilesize)
total = [spr, l, X, Y]
maplist.append(total)
return maplist
Is it possible to make the new "render" to work?
The problem is that my new optimized "renderer" creates a bunch of
cocos.sprite.Sprite objects, instead of just loading Image files as i thought it would. The code in my question only repositioned the same sprite object over and over again this way. To solve this, the way to do it is by opening the image with pyglet.image.load(), and creating sprite objects with that.
example:
f = pyglet.image.load('sprite.png')
batch = CocosNode()
batch.position = 50, 100
add(batch)
for i in range(0, 200):
test = Sprite(f)
test.position = i*10,i*10
batch.add( test )
Related
I am creating a program which opens a world map in a window using Zelle's graphics.py. It has one function which draws dots on the map, and another function which undraws those dots after they are on the screen for 1 second (which are stored in a list after being drawn). I want these functions to work concurrently, but when the addDots() function is called in a thread it won't draw the dot in the window, it just stalls. Here is the module which I run:
import thread
import threading
import time
import random
import sys
sys.path.append('..')
from Display import map
import tester
import datetime
dots = list(())
def deleteDots():
while 1==1:
tF = datetime.datetime.now()
a = 0
for i in range(len(dots)):
tD = tF - dots[i-a][2]
tD = int(str(tD)[5:7])
if tD >= 1:
map.deletePoint(dots[i-a][0],dots[i-a][1])
dots.pop(i-a)
a = a+1
def addDots():
oldResponseCount = tester.getResponseCount()
oldResponseCount = int(str(oldResponseCount))
while 1==1:
print(oldResponseCount)
newResponseCount = tester.getResponseCount()
newResponseCount = int(str(newResponseCount))
print(newResponseCount)
if(newResponseCount != oldResponseCount):
difference = newResponseCount - oldResponseCount
for i in range(difference):
lat = random.randint(-90,90)
long = random.randint(-180,180)
map.drawPoint(lat,long)
tI = datetime.datetime.now()
dots.append([lat,long,tI])
oldResponseCount = newResponseCount
if __name__ == '__main__':
threading.Thread(target=addDots).start()
threading.Thread(target=deleteDots).start()
And here is the map module which draws the map on a graphics window and contains the functions to plot and delete a point:
from graphics import *
import math
import images
size = 0.6
Circles = list(())
win = GraphWin("My Window", 1920*size, 1080*size)
win.setBackground('blue')
images.test(size)
myImage = Image(Point(960*size,540*size), "../Display/temp.gif")
myImage.draw(win)
import time
def drawPoint(lat,long):
x = int(long*5.3+960)*size
y = int(lat*(-5.92)+540)*size
pt = Point(x,y)
cir = Circle(pt,5)
cir.setFill(color_rgb(255,0,0))
Circles.append([cir,x,y])
cir.draw(win)
def deletePoint(lat,long):
x = int(long*5.3+960)*size
y = int(lat*(-5.92)+540)*size
for c in Circles:
if c[1]==x and c[2]==y:
c[0].undraw()
How should I go about doing this?
There are a couple of issues that have to be addressed. First, any graphics.py commands that invoke tkinter (i.e. commands that cause something to be drawn/undrawn) must be issued by the primary (main) thread. So we need the secondary threads to communicate drawing requests to the primary thread.
Second, you have both your secondary threads modifying the Circles and dots lists -- you need to syncronize (lock) access to these lists so that only one thread at a time can modify or iterate them.
Below is my rework of your code as an example. I've eliminated map and tester routines as I'm just putting dots up on a window with one thread and deleting them after they are a second old from another thread:
from threading import Thread, Lock
from queue import Queue # use for thread-safe communications
from random import randint
import time
from graphics import *
def drawPoint(lat, long):
x = int(long * 5.3 + 960)
y = int(lat * -5.92 + 540)
point = Point(x, y)
circle = Circle(point, 5)
circle.setFill(color_rgb(255, 0, 0))
circles_lock.acquire()
circles.append(circle)
circles_lock.release()
actions.put((circle.draw, win))
def deletePoint(lat, long):
global circles
x = int(long * 5.3 + 960)
y = int(lat * -5.92 + 540)
keep_circles = []
circles_lock.acquire()
for circle in circles:
center = circle.getCenter()
if center.getX() == x and center.getY() == y:
actions.put((circle.undraw,))
else:
keep_circles.append(circle)
circles = keep_circles
circles_lock.release()
def deleteDots():
global dots
while True:
keep_dots = []
dots_lock.acquire()
now = time.time()
for dot in dots:
lat, long, then = dot
if now - then >= 1.0:
deletePoint(lat, long)
else:
keep_dots.append(dot)
dots = keep_dots
dots_lock.release()
time.sleep(0.5)
def addDots():
while True:
lat = randint(-90, 90)
long = randint(-180, 180)
drawPoint(lat, long)
dots_lock.acquire()
dots.append((lat, long, time.time()))
dots_lock.release()
time.sleep(0.25)
win = GraphWin("My Window", 1920, 1080)
circles = []
circles_lock = Lock()
dots = []
dots_lock = Lock()
actions = Queue()
Thread(target=addDots, daemon=True).start()
Thread(target=deleteDots, daemon=True).start()
while True:
if not actions.empty():
action, *arguments = actions.get()
action(*arguments)
time.sleep(0.125)
I am trying to make an image mosaic generator using pyvips. So basically, given an image (called original in the following) create a new, bigger, image that resembles the original one except each pixel (or more realistically groups of pixels) are replaced by smaller distinct image tiles.
I was drawn to pyvips because it is said it can handle huge images and that it can process images without having to load them completely into memory.
However, I am having an issue creating a blank mosaic to then populate with tile images.
In the code below I try joining tiles together row by row to create a mosaic but this code unfortunately eats through my RAM and always segfaults.
import os
import pyvips
from os.path import join
from scipy.spatial import cKDTree
class Mosaic(object):
def __init__(self, dir_path, original_path, tree=None, averages=None):
self.dir_path = dir_path
self.original = original_path
self.tree = tree
if averages:
self.averages = averages
else:
self.averages = {}
def get_image(self, path):
return pyvips.Image.new_from_file(path, access="sequential")
def build_tree(self):
for root, dirs, files in os.walk(self.dir_path):
print('Loading images from', root, '...')
for file_name in files:
path = join(root, file_name)
try:
image = pyvips.Image.new_from_file(path)
self.averages[self.avg_rgb(image)] = path
except pyvips.error.Error:
print('File', path, 'not recognized as an image.')
self.tree = cKDTree(self.averages.keys())
print('Loaded', len(self.averages), 'images.')
def avg_rgb(self, image):
m = image.stats()
return tuple(m(4,i)[0] for i in range(1,4))
def get_tile_name(self, patch):
avg = self.avg_rgb(patch)
index = self.tree.query(avg)[1]
return self.averages[tuple(self.tree.data[index])]
def get_tile(self, x, y, step):
patch = self.get_image(self.original).crop(x, y, step, step)
patch_name = self.get_tile_name(patch)
return pyvips.Image.new_from_file(patch_name, access="sequential")
def make_mosaic(self, tile_num, tile_size, mosaic_path):
original = self.get_image(self.original)
mosaic = None
step = min(original.height, original.width) / tile_num
for y in range(0, original.height, step):
mosaic_row = None
print('Building row', y/step, '/', original.height/step)
for x in range(0, original.width, step):
tile = self.get_tile(x, y, step)
tile = tile.resize(float(tile_size) / float(min(tile.width, tile.height)))
tile = tile.crop(0, 0, tile_size, tile_size)
#mosaic.draw_image(tile, x, y)
mosaic_row = tile if not mosaic_row else mosaic_row.join(tile, "horizontal")
mosaic = mosaic_row if not mosaic else mosaic.join(mosaic_row, "vertical")
mosaic.write_to_file(mosaic_path)
I have also tried creating a mosaic by resizing the original image and then using draw_image like the following but this also crashes.
mosaic = self.get_image(self.original).resize(tile_size)
mosaic.draw_image(tile, x, y)
Finally, I have tried creating the mosaic from new_temp_file and I am having trouble writing to the temp image.
How can I make this mosaic program work?
libvips uses a recursive algorithm to work out which pixels to compute next, so for very long pipelines you can overflow the C stack and get a crash.
The simplest solution would be to use arrayjoin. This is a libvips operator which can join many images in a single call:
http://jcupitt.github.io/libvips/API/current/libvips-conversion.html#vips-arrayjoin
There's an example on the libvips github of using it to join 30,000 images at once:
https://github.com/jcupitt/libvips/issues/471
(though that's using the previous version of the libvips Python binding)
I adapted your program to use arrayjoin, and changed the way it loaded images. I noticed you were also reloading the original image for each output tile, so removing that gave a nice speedup.
#!/usr/bin/python2
from __future__ import print_function
import os
import sys
import pyvips
from os.path import join
from scipy.spatial import cKDTree
class Mosaic(object):
def __init__(self, dir_path, original_path, tile_size=128, tree=None, averages=None):
self.dir_path = dir_path
self.original_path = original_path
self.tile_size = tile_size
self.tree = tree
if averages:
self.averages = averages
else:
self.averages = {}
def avg_rgb(self, image):
m = image.stats()
return tuple(m(4,i)[0] for i in range(1,4))
def build_tree(self):
for root, dirs, files in os.walk(self.dir_path):
print('Loading images from', root, '...')
for file_name in files:
path = join(root, file_name)
try:
# load image as a square image of size tile_size X tile_size
tile = pyvips.Image.thumbnail(path, self.tile_size,
height=self.tile_size,
crop='centre')
# render into memory
tile = tile.copy_memory()
self.averages[self.avg_rgb(tile)] = tile
except pyvips.error.Error:
print('File', path, 'not recognized as an image.')
self.tree = cKDTree(self.averages.keys())
print('Loaded', len(self.averages), 'images.')
def fetch_tree(self, patch):
avg = self.avg_rgb(patch)
index = self.tree.query(avg)[1]
return self.averages[tuple(self.tree.data[index])]
def make_mosaic(self, tile_num, mosaic_path):
mosaic = None
original = pyvips.Image.new_from_file(self.original_path)
step = min(original.height, original.width) / tile_num
tiles_across = original.width / step
tiles_down = original.height / step
tiles = []
for y in range(0, tiles_down):
print('Building row', y, '/', tiles_down)
for x in range(0, tiles_across):
patch = original.crop(x * step, y * step,
min(step, original.width - x * step),
min(step, original.height - y * step))
tile = self.fetch_tree(patch)
tiles.append(tile)
mosaic = pyvips.Image.arrayjoin(tiles, across=tiles_across)
print('writing ', mosaic_path)
mosaic.write_to_file(mosaic_path)
mosaic = Mosaic(sys.argv[1], sys.argv[2])
mosaic.build_tree()
mosaic.make_mosaic(200, sys.argv[3])
I can run it like this:
$ time ./mosaic2.py samples/ k2.jpg x.png
Loading images from samples/ ...
Loaded 228 images.
Building row 0 / 292
...
Building row 291 / 292
writing x.png
real 7m19.333s
user 7m27.322s
sys 0m30.578s
making a 26496 x 37376 pixel image, in this case, and it runs in about 150mb of memory.
I am creating a program (to test a theory), and to get the data I need, I need a program to run as fast as possible.
Here's the problem - I have made it as fast as I could manage and it is still to slow. It is using a very small amount of my computer's RAM and CPU capacity. I am running the program with PyCharm 2017 Community Edition.
The code is below; How would I further optimize or change this to make it run faster?
Main:
from functions import *
from graphics import *
import time
Alpha = True
x = timestamp()
while Alpha:
master = GraphWin(title="Image", width=512, height=512)
build_image(master)
getter(master, x)
x = timestamp()
time.sleep(3)
master.close()
Module "Functions":
from graphics import *
import random
from PIL import ImageGrab
def build_image(window):
for i in range(513):
for j in range(513):
fig = Rectangle(Point(j, i), Point(j + 1, i + 1))
color = random.randrange(256)
fig.setFill(color_rgb(color, color, color))
fig.setOutline(color_rgb(color, color, color))
fig.draw(window)
def getter(widget, counter):
x = widget.winfo_rootx()+widget.winfo_x()
y = widget.winfo_rooty()+widget.winfo_y()
x1 = x+widget.winfo_width()
y1 = y+widget.winfo_height()
ImageGrab.grab().crop((x, y, x1, y1)).save("{}.png".format(str(counter)))
def timestamp():
timelist = time.gmtime()
filename = ("Image" + "_" + str(timelist[0]) + "_" + str(timelist[1]) + "_" + str(timelist[2]) + "_" +
str(timelist[3]) + "_" + str(timelist[4]) + "_" + str(timelist[5]) + "_UTC")
return filename
Note: Module "Graphics" is a module that allows for easy manipulation of Tkinter.
Your slowness is probably from treating the pixels as rectangles in your window.
If all you want to do is generate random images, you can skip the window part. I found this code laying about, after not too much ducking:
from PIL import Image
import random
def drawImage():
testImage = Image.new("RGB", (600,600), (255,255,255))
pixel = testImage.load()
for x in range(600):
for y in range(600):
red = random.randrange(0,255)
blue = random.randrange(0,255)
green = random.randrange(0,255)
pixel[x,y]=(red,blue,green)
return testImage
def main():
finalImage = drawImage()
finalImage.save("finalImage.jpg")
Use a profiler to see where your program is fast/slow. Here is a profile wrapper you can use on your functions to see what is taking too long in your program.
def line_profiler(view=None, extra_view=None):
import line_profiler
def wrapper(view):
def wrapped(*args, **kwargs):
prof = line_profiler.LineProfiler()
prof.add_function(view)
if extra_view:
[prof.add_function(v) for v in extra_view]
with prof:
resp = view(*args, **kwargs)
prof.print_stats()
return resp
return wrapped
if view:
return wrapper(view)
return wrapper
Now how to use it
#line_profiler
def simple():
print("Hello")
print("World")
Now when you run your function, you will get a printout of how long everything takes.
You might need to do pip install line_profiler
this may be a bit faster if you use numpy. loops inside loops will kill your speed.
from PIL import Image
import numpy as np
def drawImage():
return Image.fromarray(np.random.randint(255, size=(600, 600, 3)).astype(np.uint8))
Since you do a lot of independent tasks, you could benefit from parallelism. Something like:
from concurrent.futures import ThreadPoolExecutor
def build_image(window, start, end, step):
for i in range(start, end, step):
for j in range(end):
fig = Rectangle(Point(j, i), Point(j + 1, i + 1))
color = random.randrange(256)
fig.setFill(color_rgb(color, color, color))
fig.setOutline(color_rgb(color, color, color))
fig.draw(window)
max_workers = 8
with ThreadPoolExecutor(max_workers=max_workers) as executor:
for id in range(max_workers):
executor.submit(build_image, window, id, 513, max_workers)
Here's the error:
File "/Users/KarenLee/Desktop/temp/worldmodel.py", line 76, in update_on_time
obj = VeinAction(entity, image_store)
NameError: global name 'VeinAction' is not defined
And here is my code (this is in the file "actions.py"):
import entities
import worldmodel
import pygame
import math
import random
import point
import image_store
BLOB_RATE_SCALE = 4
BLOB_ANIMATION_RATE_SCALE = 50
BLOB_ANIMATION_MIN = 1
BLOB_ANIMATION_MAX = 3
FREEZE_ANIMATION_RATE = 100
FREEZE_STEPS = 4
ORE_CORRUPT_MIN = 20000
ORE_CORRUPT_MAX = 30000
QUAKE_STEPS = 10
QUAKE_DURATION = 1100
QUAKE_ANIMATION_RATE = 100
VEIN_SPAWN_DELAY = 500
VEIN_RATE_MIN = 8000
VEIN_RATE_MAX = 17000
WYVERN_RATE_MIN = 200
WYVERN_RATE_MAX = 600
WYVERN_ANIMATION_RATE = 100
class VeinAction:
def __init__(self, entity, image_store):
self.entity = entity
self.image_store = image_store
def vein_action(self, world, action, ticks):
entity = self.entity
open_pt = find_open_around(world, entities.get_position(entity),
entities.get_resource_distance(entity))
if open_pt:
ore = create_ore(world,
"ore - " + entities.get_name(entity) + " - " + str(ticks),
open_pt, ticks, action.image_store)
worldmodel.add_entity(world, ore)
tiles = [open_pt]
else:
tiles = []
schedule_action(world, entity, VeinAction(entity, action.image_store),
ticks + entities.get_rate(entity))
return tiles
def vein_take_action(self, world, action, ticks):
entities.remove_pending_action(self.entity, action)
if isinstance(action, VeinAction):
return self.vein_action(world, action, ticks)
And this is in the file "worldmodel.py":
import entities
import pygame
import ordered_list
import actions
import occ_grid
import point
class WorldModel:
def __init__(self, num_rows, num_cols, background):
self.background = occ_grid.Grid(num_cols, num_rows, background)
self.num_rows = num_rows
self.num_cols = num_cols
self.occupancy = occ_grid.Grid(num_cols, num_rows, None)
self.entities = []
self.action_queue = ordered_list.OrderedList()
def update_on_time(world, ticks):
tiles = []
next = world.action_queue.head()
obj = VeinAction(entity, image_store)
while next and next.ord < ticks:
world.action_queue.pop()
tiles.extend(obj.vein_take_action(world, next.item, ticks))
tiles.extend(actions.take_action(world, next.item, ticks))
next = world.action_queue.head()
return tiles
The error message comes from the update_on_time function in "worldmodel.py". I thought that this was how you would call a method from a class in a different file in a function, but it doesn't work! What is the correct way to do this? Or, is it possible to do this? Thanks in advance.
You imported the module actions which contains the class VeinAction. However, Python does not know this. You need to tell Python where VeinAction is located by adding actions. before it:
obj = actions.VeinAction(entity, image_store)
That, or you could import VeinAction directly:
from actions import VeinAction
Either way, you need to make sure that Python can find the class VeinAction.
Using the minimal example below, the line plot of a large (some 110k points) plot I get (with python 2.7, numpy 1.5.1, chaco/enable/traits 4.3.0) is this:
However, that is bizarre, because it is a line plot, and there shouldn't be any filled areas in there? Especially since the data is sawtooth-ish signal? It's as if there is a line at y~=37XX, above which there is color filling?! But sure enough, if I zoom into an area, I get the rendering I expect - without the unexpected fill:
Is this a bug - or is there something I'm doing wrong? I tried to use use_downsampling, but it makes no difference...
The test code:
import numpy as np
import numpy.random as npr
from pprint import pprint
from traits.api import HasTraits, Instance
from chaco.api import Plot, ArrayPlotData, VPlotContainer
from traitsui.api import View, Item
from enable.component_editor import ComponentEditor
from chaco.tools.api import PanTool, BetterSelectingZoom
tlen = 112607
alr = npr.randint(0, 4000, tlen)
tx = np.arange(0.0, 30.0-0.00001, 30.0/tlen)
ty = np.arange(0, tlen, 1) % 10000 + alr
pprint(len(ty))
class ChacoTest(HasTraits):
container = Instance(VPlotContainer)
traits_view = View(
Item('container', editor=ComponentEditor(), show_label=False),
width=800, height=500, resizable=True,
title="Chaco Test"
)
def __init__(self):
super(ChacoTest, self).__init__()
pprint(ty)
self.plotdata = ArrayPlotData(x = tx, y = ty)
self.plotobj = Plot(self.plotdata)
self.plotA = self.plotobj.plot(("x", "y"), type="line", color=(0,0.99,0), spacing=0, padding=0, alpha=0.7, use_downsampling=True)
self.container = VPlotContainer(self.plotobj, spacing=5, padding=5, bgcolor="lightgray")
#~ container.add(plot)
self.plotobj.tools.append(PanTool(self.plotobj))
self.plotobj.overlays.append(BetterSelectingZoom(self.plotobj))
if __name__ == "__main__":
ChacoTest().configure_traits()
I am able to reproduce the error and talking with John Wiggins (maintainer of Enable), it is a bug in kiva (which chaco uses to paint on the screen):
https://github.com/enthought/enable
The good news is that this is a bug in one of the kiva backend that you can use. So to go around the issue, you can run your script choosing a different backend:
ETS_TOOLKIT=qt4.qpainter python <NAME OF YOUR SCRIPT>
if you use qpainter or quartz, the plot looks (on my machine) as expected. If you choose qt4.image (the Agg backend), you will reproduce the issue. Unfortunately, the Agg backend is the default one. To change that, you can set the ETS_TOOLKIT environment variable to that value:
export ETS_TOOLKIT=qt4.qpainter
The bad news is that fixing this isn't going to be an easy task. Please feel free to report the bug in github (again https://github.com/enthought/enable) if you want to be involved in this. If you don't, I will log it in the next couple of days. Thanks for reporting it!
Just a note - I found this:
[Enthought-Dev] is chaco faster than matplotlib
I recall reading somewhere that you are expected to implement the
_downsample method because the optimal algorithm depends on the type
of data you're collecting.
And as I couldn't find any examples with _downsample implementation other than decimated_plot.py referred in that post, which isn't standalone - I tried and built a standalone example, included below.
The example basically has messed up drag and zoom, (plot disappears if you go out of range, or stretches upon a drag move) - and it starts zoomed in; but it is possible to zoom it out in the range shown in the OP - and then it displays the exact same plot rendering problem. So downsampling isn't the solution per se, so this is likely a bug?
import numpy as np
import numpy.random as npr
from pprint import pprint
from traits.api import HasTraits, Instance
from chaco.api import Plot, ArrayPlotData, VPlotContainer
from traitsui.api import View, Item
from enable.component_editor import ComponentEditor
from chaco.tools.api import PanTool, BetterSelectingZoom
#
from chaco.api import BaseXYPlot, LinearMapper, AbstractPlotData
from enable.api import black_color_trait, LineStyle
from traits.api import Float, Enum, Int, Str, Trait, Event, Property, Array, cached_property, Bool, Dict
from chaco.abstract_mapper import AbstractMapper
from chaco.abstract_data_source import AbstractDataSource
from chaco.array_data_source import ArrayDataSource
from chaco.data_range_1d import DataRange1D
tlen = 112607
alr = npr.randint(0, 4000, tlen)
tx = np.arange(0.0, 30.0-0.00001, 30.0/tlen)
ty = np.arange(0, tlen, 1) % 10000 + alr
pprint(len(ty))
class ChacoTest(HasTraits):
container = Instance(VPlotContainer)
traits_view = View(
Item('container', editor=ComponentEditor(), show_label=False),
width=800, height=500, resizable=True,
title="Chaco Test"
)
downsampling_cutoff = Int(4)
def __init__(self):
super(ChacoTest, self).__init__()
pprint(ty)
self.plotdata = ArrayPlotData(x = tx, y = ty)
self.plotobj = TimeSeriesPlot(self.plotdata)
self.plotobj.setplotranges("x", "y")
self.container = VPlotContainer(self.plotobj, spacing=5, padding=5, bgcolor="lightgray")
self.plotobj.tools.append(PanTool(self.plotobj))
self.plotobj.overlays.append(BetterSelectingZoom(self.plotobj))
# decimate from:
# https://bitbucket.org/mjrosen/neurobehavior/raw/097ef3719d1263a8b303d29c31ab71b6e792ab04/cns/widgets/views/decimated_plot.py
def decimate(data, screen_width, downsampling_cutoff=4, mode='extremes'):
data_width = data.shape[-1]
downsample = np.floor((data_width/screen_width)/4.)
if downsample > downsampling_cutoff:
return globals()['decimate_'+mode](data, downsample)
else:
return data
def decimate_extremes(data, downsample):
last_dim = data.ndim
offset = data.shape[-1] % downsample
if data.ndim == 2:
shape = (len(data), -1, downsample)
else:
shape = (-1, downsample)
data = data[..., offset:].reshape(shape).copy()
data_min = data.min(last_dim)
data_max = data.max(last_dim)
return data_min, data_max
def decimate_mean(data, downsample):
offset = len(data) % downsample
if data.ndim == 2:
shape = (-1, downsample, data.shape[-1])
else:
shape = (-1, downsample)
data = data[offset:].reshape(shape).copy()
return data.mean(1)
# based on class from decimated_plot.py, also
# neurobehavior/cns/chaco_exts/timeseries_plot.py ;
# + some other code from chaco
class TimeSeriesPlot(BaseXYPlot):
color = black_color_trait
line_width = Float(1.0)
line_style = LineStyle
reference = Enum('most_recent', 'trigger')
traits_view = View("color#", "line_width")
downsampling_cutoff = Int(100)
signal_trait = "updated"
decimate_mode = Str('extremes')
ch_index = Trait(None, Int, None)
# Mapping of data names from self.data to their respective datasources.
datasources = Dict(Str, Instance(AbstractDataSource))
index_mapper = Instance(AbstractMapper)
value_mapper = Instance(AbstractMapper)
def __init__(self, data=None, **kwargs):
super(TimeSeriesPlot, self).__init__(**kwargs)
self._index_mapper_changed(None, self.index_mapper)
self.setplotdata(data)
self._plot_ui_info = None
return
def setplotdata(self, data):
if data is not None:
if isinstance(data, AbstractPlotData):
self.data = data
elif type(data) in (ndarray, tuple, list):
self.data = ArrayPlotData(data)
else:
raise ValueError, "Don't know how to create PlotData for data" \
"of type " + str(type(data))
def setplotranges(self, index_name, value_name):
self.index_name = index_name
self.value_name = value_name
index = self._get_or_create_datasource(index_name)
value = self._get_or_create_datasource(value_name)
if not(self.index_mapper):
imap = LinearMapper()#(range=self.index_range)
self.index_mapper = imap
if not(self.value_mapper):
vmap = LinearMapper()#(range=self.value_range)
self.value_mapper = vmap
if not(self.index_range): self.index_range = DataRange1D() # calls index_mapper
if not(self.value_range): self.value_range = DataRange1D()
self.index_range.add(index) # calls index_mapper!
self.value_range.add(value)
# now do it (right?):
self.index_mapper = LinearMapper(range=self.index_range)
self.value_mapper = LinearMapper(range=self.value_range)
def _get_or_create_datasource(self, name):
if name not in self.datasources:
data = self.data.get_data(name)
if type(data) in (list, tuple):
data = array(data)
if isinstance(data, np.ndarray):
if len(data.shape) == 1:
ds = ArrayDataSource(data, sort_order="none")
elif len(data.shape) == 2:
ds = ImageData(data=data, value_depth=1)
elif len(data.shape) == 3:
if data.shape[2] in (3,4):
ds = ImageData(data=data, value_depth=int(data.shape[2]))
else:
raise ValueError("Unhandled array shape in creating new plot: " \
+ str(data.shape))
elif isinstance(data, AbstractDataSource):
ds = data
else:
raise ValueError("Couldn't create datasource for data of type " + \
str(type(data)))
self.datasources[name] = ds
return self.datasources[name]
def get_screen_points(self):
self._gather_points()
return self._downsample()
def _data_changed(self):
self.invalidate_draw()
self._cache_valid = False
self._screen_cache_valid = False
self.request_redraw()
def _gather_points(self):
if not self._cache_valid:
range = self.index_mapper.range
#if self.reference == 'most_recent':
# values, t_lb, t_ub = self.get_recent_range(range.low, range.high)
#else:
# values, t_lb, t_ub = self.get_range(range.low, range.high, -1)
values, t_lb, t_ub = self.data[self.value_name][range.low:range.high], range.low, range.high
#if self.ch_index is None:
# self._cached_data = values
#else:
# #self._cached_data = values[:,self.ch_index]
self._cached_data = values
self._cached_data_bounds = t_lb, t_ub
self._cache_valid = True
self._screen_cache_valid = False
def _downsample(self):
if not self._screen_cache_valid:
val_pts = self._cached_data
screen_min, screen_max = self.index_mapper.screen_bounds
screen_width = screen_max-screen_min
values = decimate(val_pts, screen_width, self.downsampling_cutoff,
self.decimate_mode)
if type(values) == type(()):
n = len(values[0])
s_val_min = self.value_mapper.map_screen(values[0])
s_val_max = self.value_mapper.map_screen(values[1])
self._cached_screen_data = s_val_min, s_val_max
else:
s_val_pts = self.value_mapper.map_screen(values)
self._cached_screen_data = s_val_pts
n = len(values)
t = np.linspace(*self._cached_data_bounds, num=n)
t_screen = self.index_mapper.map_screen(t)
self._cached_screen_index = t_screen
self._screen_cache_valid = True
return [self._cached_screen_index, self._cached_screen_data]
def _render(self, gc, points):
idx, val = points
if len(idx) == 0:
return
gc.save_state()
gc.set_antialias(True)
gc.clip_to_rect(self.x, self.y, self.width, self.height)
gc.set_stroke_color(self.color_)
gc.set_line_width(self.line_width)
#gc.set_line_width(5)
gc.begin_path()
#if len(val) == 2:
if type(val) == type(()):
starts = np.column_stack((idx, val[0]))
ends = np.column_stack((idx, val[1]))
gc.line_set(starts, ends)
else:
gc.lines(np.column_stack((idx, val)))
gc.stroke_path()
self._draw_default_axes(gc)
gc.restore_state()
if __name__ == "__main__":
ChacoTest().configure_traits()