Optimal way to load multiple instances of the same image - python

I am messing around with Kivy, and trying to get some game like application with some kind of 2D grid. In my design each cell oh grid should have its own graphical representation, depending on whats inside.
Below is my current simple code which just creates grid and insert some images in each cell.
class MyWidget(Widget):
def __init__(self,images,*args, **kwargs):
super(MyWidget, self).__init__(*args, **kwargs)
self.images = []
for img in images:
self.add_image(img)
self.bind(pos=self.callback,size=self.callback)
def add_image(self,image):
self.images.append(Image(source=image,allow_stretch = True,keep_ratio = False))
self.add_widget(self.images[-1])
def callback(self,instance,value):
for image in instance.images:
image.pos = instance.pos
image.size = instance.size
class StartScreen(Screen):
def __init__(self,**kwargs):
super(StartScreen, self).__init__(**kwargs)
i = 10
self.layout = GridLayout(cols=i)
self.add_widget(self.layout)
for i in range(i*i):
self.layout.add_widget(MyWidget(['./images/grass.png','./images/bug1.png']))
class TestApp(App):
def build(self):
return StartScreen()
The problem is that while all images are the same, thay are loaded into memory again for each cell, from what I can see. It is not efficient, especially when there will be 10000 cells or so.
I have tried to add the same image to each cell, but it turns out that each widget can have only one parent.
I also have tried to initialize new images with texture of one already initialized, but that bringed no improve.
I tried to get texture of loaded image, and then create rectangle with it as a texture. Something like this:
def add_image(self,texture):
with self.canvas:
rect = Rectangle(texture=texture,pos=self.pos, size=self.size)
self.rects.append(rect)
where texture is:
Image(source='./images/grass.png',allow_stretch = True,keep_ratio = False).texture
It have improved memory usage (from 430MB to 160MB for 10000 cells with 200kB images. But still, this is quite much for two images. :)
My question: is there more efficient way of creating 2D grid with a lot of repeating images, in Kivy?
Maybe my approach to the problem is flawed - I have really no expirence in creating games...

The memory issue you see is from your widgets not from mismanagement of textures, Image textures in kivy are cached using kivy's internal cache mechanism so if you try and load a image 100 times in one minute kivy will just re-use the existing texture from cache.
This cache does timeout after a set time though and would be re-loaded from disk after this timeout. Image widget for this reason has a reload method and a nocache property that can be set. You can set the cache manually ::
from kivy.cache import Cache
Cache._categories['kv.image']['limit'] = 0
Cache._categories['kv.texture']['limit'] = 0
For games though one should always try and use Atlas, it's cache doesn't timeout, the whole mechanism is geared towards allowing you better performance and texture management. Other advantages include one single texture to be uploaded to the gpu = huge improvement in upload time, read from disk time is reduced considerably.
Every widget no matter how trivial has overhead and when dealing with 10,000+ iterations of the same Widget. You are bound to get memory usage issues. Widgets aren't suitable for such a usage. You should look at using repeating textures,
manipulating texture coordinates, drawing straight onto the canvas without using widgets. Something like (untested)::
texture = Image('grid.jpg').texture
texture.wrap = 'repeat'
texture.uvsize = (20, 20)
with self.canvas:
Color(1, 1, 1)
Rectangle(pos=(0, 0), size=(2000, 2000), texture=texture)
You might want to look at KivEnt and cymunk as physics engines if you are interested in writing games.

Related

Remove empty space at bottom of QTableWidget

I'm using PySide6 6.4.1 to build a table widget that automatically resizes to the number of rows. Here's a minimal example:
from PySide6.QtWidgets import *
class MW(QWidget):
def __init__(self):
super().__init__()
self.button = QPushButton("Test")
self.table = QTableWidget(self)
self.table.setColumnCount(1)
self.table.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents)
self.setLayout(QVBoxLayout(self))
self.layout().addWidget(self.button)
self.layout().addWidget(self.table)
self.button.clicked.connect(self.test)
return
def test(self):
self.table.insertRow(0)
self.table.setItem(0, 0, QTableWidgetItem("new item"))
self.table.adjustSize()
self.adjustSize()
return
app = QApplication()
mw = MW()
mw.show()
app.exec()
Somehow this always leaves a bit of empty space at the bottom of the table. How do I get rid of this space without doing manual resizing?
(Nevermind the weird font size, it's a known bug when using UI scaling. I've adjusted the font size manually as well and it doesn't get rid of this problem.)
Qt item views inherit from QAbstractScrollArea, which has some peculiar size related aspects:
it has an Expanding size policy that tells the parent layout it can use as much space as possible, possibly increasing the available space at initialization;
it has a minimumSizeHint() that always includes a minimum reasonable size allowing showing the scroll bars (even if they are not visible);
if the sizeAdjustPolicy is AdjustToContents it's also based on the viewport size hint;
It's also mandatory to consider a fundamental aspect about scroll areas: size management is a tricky subject, and some level of compromise is necessary most of the times. This is the case whenever the scroll bars potentially change the available size of the viewport (the part of the widget that is able to scroll), which is the default behavior of Qt in most systems, unless the scroll bars are always hidden/visible or they are transient (they "overlay" above the viewport without affecting its available visible size).
To clarify this aspect, consider a scroll area with content that has a minimum size of 100x100 and scroll bars that have a default extent (width for the vertical one, height for the horizontal) of 20: if the height hint of the content is changed to 110, then you'd theoretically need an area of 100x110. But Qt needs to know the hints before laying out widgets and setting their geometries. This means that you cannot know if the scroll bars have to be shown before the widget is finally laid out, but that hint is required to lay out the widget itself. So, recursion.
Qt layout management is a system that is far from perfect, but I doubt that there is one, at least considering normal UI management (don't consider web layouts: their concept is based on different assumption, most importantly the fact that the whole "window" has potentially infinite dimensions). This is an aspect that must be always considered, especially if the shown contents are set to adapt their size based on the contents; it's the case of fitInView() of QGraphicsView or the known issues of QLayout with rich text based widgets.
Qt doesn't provide "foolproof" solutions for these aspects, because its layout management doesn't allow it as it has been implemented primarily considering performance and usability: the UI has to work and be responsive before being "fancy".
It's one of the reasons for which it's almost impossible to have real fixed-aspect-ratio widgets or windows. You can work around it, but at some point you'll have some inconsistencies, and you have to live with that. Also consider that this kind of behavior is generally not very UX-friendly. UI elements that resize themselves (and, consequentially, alter the whole layout) at anytime are usually annoying and very user-unfriendly, especially if they displace their or other contents: it's like having a car that constantly moves the driving controls depending on the amount of passengers.
That said, it's not impossible to have a partially working solution.
The requirements are to:
override minimumSizeHint(), so that a minimal reasonable size is always returned;
override sizeHint() that is used to adjust the widget (and parents) based on the contents of the view;
change the vertical size policy of the table to Preferred, which will tell the layout manager that the height of the size hint will be considered as default, still allowing it to expand in case other items in the layout don't use the remaining space, and eventually shrink it if required;
eventually do the same for the horizontal policy in order to adapt it to the actual horizontal header size, otherwise use self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch), but be aware that this might complicate things (see the note after the code);
class TableWidget(QTableWidget):
def sizeHint(self):
hHeader = self.horizontalHeader()
vHeader = self.verticalHeader()
f = self.frameWidth() * 2
# the simple solution is to get the length, but this might be a problem
# whenever *any* section of the header is set to Stretch
targetWidth = width = f + hHeader.length()
# a possible alternative (but still far from perfect):
width = f
for c in range(self.columnCount()):
if hHeader.isSectionHidden(c):
continue
width += self.sizeHintForColumn(c)
targetWidth = width
if not vHeader.isHidden():
width += vHeader.width()
hpol = self.horizontalScrollBarPolicy()
height = f + vHeader.length() + hHeader.height()
if (
hpol != Qt.ScrollBarAlwaysOff
and not self.horizontalScrollBar().isHidden()
and (
hpol == Qt.ScrollBarAlwaysOn
and hHeader.length() + f < targetWidth
)
):
height += self.horizontalScrollBar().sizeHint().height()
return QSize(width, height)
def minimumSizeHint(self):
hint = self.sizeHint()
minHint = super().minimumSizeHint()
return QSize(
min(minHint.width(), hint.width()),
min(super().minimumSizeHint().height(), hint.height())
)
class MW(QWidget):
def __init__(self):
# ...
pol = self.table.sizePolicy()
pol.setVerticalPolicy(QSizePolicy.Preferred)
self.table.setSizePolicy(pol)
Be aware that the above doesn't solve all problems. It might work fine for a QTableView having just one column or when using the default interactive (or fixed) section resize mode, but whenever you set different resize modes for each column the result may be wrong.
In order to provide a finer resize, you'll need to do much complex computations that take into account each section resize mode for the horizontal header, the default/minimum/maximum and eventually the hint based on the content.
Further notes: 1. calling adjustSize() on the parent is normally enough, it's not necessary to do it on the children; 2. self.setLayout(QVBoxLayout(self)) is pointless, the self argument already sets the layout; just use layout = QVBoxLayout(self) and use that as a local variable to add widgets; 3. in Python the return at the end of a function is always implicit, you shall not add it as it's useless, redundant and distracting.

Show image on screen with ursina

I'm making a 3D FPS in Ursina, and I'd like to have my arm skin with the weapon as an image, not an actual 3D model. Does anyone know how to do this ? I tried with Animate from the documentation, but this loads an image as an object in my scene.
What I could do is define a quad with player as parent, and positional arguments, so that it follows me and I see it at the right place, but even this wouldn't work as the texture argument doesn't accept gifs.
So, does anyone know how to do that ?
You can load an animated gif with Animation() which creates an entity. As part of the interface you'll want to attach it to the UI:
from ursina import *
app = Ursina()
gif = 'animation.gif'
a = Animation(gif, parent=camera.ui)
a.scale /= 5 # adjust right size to your needs
a.position = (0.5, -0.5) # lower right of the screen
app.run()
You will need the imageio Python package installed to load gifs.
OLD ANSWER
The Entity you use for the gun has to be anchored to the interface using its parent parameter. Here's an example for a Minecraft-style hand (just a block really):
class Hand(Entity):
def __init__(self):
super().__init__(
parent=camera.ui,
model='cube',
position=Vec2(0.5, -0.3),
scale=(0.2, 0.2, 0.5),
rotation=(150, -30, 0)
)
The important part is parent=camera.ui
Sadly, I cannot seem to find a way to play gifs. I know, #Jan Wilamowski updated his answer but, in my project, that does NOT work. You CAN show static images on this screen, though, by making a quad entity with an image texture, as shown:
class yourimage(Entity):
def __init__(self):
super().__init__(
parent=camera.ui,
model='quad',
#size, position, and rotate your image here
texture = 'yourimage')
yourimage()

Kivy drawn items 'disappearing' (or possible render order problem)

(this is a repost from https://groups.google.com/forum/?utm_medium=email&utm_source=footer#!msg/kivy-users/0B5GYWkTIwE/-OQYp_ykCQAJ)
Very occasionally our kivy-based UI glitches in a very odd way: the system remains responsive and the displays of buttons and text work but it seems that all "drawn" items disappear, i.e. items from kivy.graphics.
Here is the main control screen in it's proper state:
and when it goes wrong it changes to:
All the items rendered with the kivy.graphics objects have gone!
This is using 1.9.1-1build3 (in Ubuntu) and 1.9.1-dev (from kivypie on an RPi)
Does anyone have a clue as to what might be causing this or how to track it down? It seems to happen on touch events, but it is so rare it's been impossible so far to find any commonality and there is no error report output to stdout/stderr or the .kivy/log/
Here's a section of code showing how elements are being drawn:
class LineWidget(GUIWidget):
"""A line widget. A simple sequence of points"""
def __init__(self, points, width, **kwargs):
GUIWidget.__init__(self, **kwargs)
self.points = points
with self.canvas:
self.line_color = theme.system.line.Color()
self.line = Line(width=width)
self.bind(size=self.update_rect, pos=self.update_rect)
def update_rect(self, instance, value):
"""Update the point positions."""
self.pos = instance.pos
if self.points:
self.line.points = self.get_points(self.points)
def set_color(self, color):
"""Change the color"""
self.line_color.rgb = color
and the blue/grey background is rendered:
with self.canvas.before:
if background_color is None:
background_color = theme.primary.black
self._background_color = Color(*background_color)
self._background_rectangle = Rectangle(size=self.size, pos=self.pos)
In my post to the kivy group, it has been suggested that maybe some race condition has happened resulting in the native kivy objects being rendered on top of mine and to try using canvas.after, but unless I can find a way to provoke the problem I'll never know if this has worked.
Is it possible that the render order (Z-index) has changed?
Is it possible there is some race condition?
The problem happens so rarely it has been impossible so far to
provoke the problem (100s of random clicks on widgets is the only
way) - is there some programmatic method to try and force the issue?
what debug could I put in to try and find out what might be
happening?
Thanks,

trying to draw over sprite or change picture pyglet

I am trying to learn pyglet and practice some python coding with a questionnaire thing, but I am unable to find a way to make the background picture be removed or drawn on top of or something for 10 seconds. I am new and am lacking in a lot of the knowledge I would need, thank you for helping!
import pyglet
from pyglet.window import Window
from pyglet.window import key
from pyglet import image
import time
card1 = False
cat_image = pyglet.image.load("cat.png")
dog_image = pyglet.image.load("dog.png")
image = pyglet.image.load("backg.png")
background_sprite = pyglet.sprite.Sprite(image)
cat = pyglet.sprite.Sprite(cat_image)
dog = pyglet.sprite.Sprite(dog_image)
window = pyglet.window.Window(638, 404, "Life")
mouse_pos_x = 0
mouse_pos_y = 0
catmeme = pyglet.image.load("catmeme.png")
sprite_catmeme = pyglet.sprite.Sprite(catmeme)
#window.event
def on_draw():
window.clear()
background_sprite.draw()
card_draw1(63, 192, 385, 192)
def card1():
while time.time() < (time.time() + 10):
window.clear()
sprite_catmeme.draw()
#window.event
def card_draw1(x1, y1, x2, y2):
cat.set_position(x1, y1)
dog.set_position(x2, y2)
cat.draw()
dog.draw()
def card_draw2():
pass
#window.event
def on_mouse_press(x, y, button, modifiers):
if x > cat.x and x < (cat.x + cat.width):
if y > cat.y and y < (cat.y + cat.height):
card1()
game = True
while game:
on_draw()
pyglet.app.run()
There's a few flaws in the order and in which you do things.
I will try my best to describe them and give you a piece of code that might work better for what your need is.
I also think your description of the problem is a bit of an XY Problem which is quite common when asking for help on complex matters where you think you're close to a solution, so you're asking for help on the solution you've come up with and not the problem.
I'm assuming you want to show a "Splash screen" for 10 seconds, which happens to be your background? And then present the cat.png and dog.png ontop of it, correct?
If that's the case, here's where you probably need to change things in order for it to work:
The draw() function
It doesn't really update the screen much, it simply adds things to the graphical memory. What updates the screen is you or something telling the graphics library that you're done adding things to the screen and it's time to update everything you've .draw()'n. So the last thing you need in the loop would be window.flip() in order for the things you've drawn to actually show.
Your things might show if you try to wiggle the window around, it should trigger a re-draw of the scene because of how the internal mechanics of pyglet work..
If you don't call .flip() - odds are probable that the redraw() call will never occur - which again, is a internal mechanism of Pyglet/GL that tells the graphics card that something has been updated, we're done updating and it's time to redraw the scene.
a scene
This is the word most commonly used for what the user is seeing.
I'll probably throw this around a lot in my text, so it's good to know that this is what the user is seeing, not what you've .draw()'n or what's been deleted, it's the last current rendering of the graphics card to the monitor.
But because of how graphical buffers work we've might have removed or added content to the memory without actually drawing it yet. Keep this in mind.
The pyglet.app.run() call
This is a never ending loop in itself, so having that in a while game: loop doesn't really make sense because .run() will "hang" your entire application, any code you want to execute needs to be in def on_draw or an event that is generated from within the graphical code itself.
To better understand this, have a look at my code, i've pasted it around a couple of times here on SO over the years and it's a basic model of two custom classes that inherits the behavior of Pyglet but lets you design your own classes to behave slightly differently.
And most of the functionality is under on_??? functions, which is almost always a function used to catch Events. Pyglet has a lot of these built in, and we're going to override them with our own (but the names must be the same)
import pyglet
from pyglet.gl import *
key = pyglet.window.key
class CustomSprite(pyglet.sprite.Sprite):
def __init__(self, texture_file, x=0, y=0):
## Must load the texture as a image resource before initializing class:Sprite()
self.texture = pyglet.image.load(texture_file)
super(CustomSprite, self).__init__(self.texture)
self.x = x
self.y = y
def _draw(self):
self.draw()
class MainScreen(pyglet.window.Window):
def __init__ (self):
super(MainScreen, self).__init__(800, 600, fullscreen = False)
self.x, self.y = 0, 0
self.bg = CustomSprite('bg.jpg')
self.sprites = {}
self.alive = 1
def on_draw(self):
self.render()
def on_close(self):
self.alive = 0
def on_key_press(self, symbol, modifiers):
if symbol == key.ESCAPE: # [ESC]
self.alive = 0
elif symbol == key.C:
print('Rendering cat')
self.sprites['cat'] = CustomSprite('cat.png', x=10, y=10)
elif symbol == key.D:
self.sprites['dog'] = CustomSprite('dog.png', x=100, y=100)
def render(self):
self.clear()
self.bg.draw()
for sprite_name, sprite_obj in self.sprites.items():
sprite_obj._draw()
self.flip()
def run(self):
while self.alive == 1:
self.render()
# -----------> This is key <----------
# This is what replaces pyglet.app.run()
# but is required for the GUI to not freeze
#
event = self.dispatch_events()
x = MainScreen()
x.run()
Now, this code is kept simple on purpose, the full code I usually paste on SO can be found at Torxed/PygletGui, the gui.py is where most of this comes from and it's the main loop.
What I do here is simply replace the Decorators by using "actual" functions inside a class. The class itself inherits the functions from a traditional pyglet.window.Window, and as soon as you name the functions the same as the inherited onces, you replace the core functionality of Window() with whatever you decide.. In this case, i mimic the same functions but add a few of my own.
on_key_press
One such example is on_key_press(), which normally just contain a pass call and does nothing, here, we check if key.C is pressed, and if so - we add a item to self.sprites.. self.sprites just so happen to be in our render() loop, anything in there will be rendered ontop of a background.
Here's the pictures I used:
(named bg.jpg, cat.png, dog.png - note the different file endings)
class:CustomSprite
CustomSprite is a very simple class designed to make your life easier at this point, nothing else. It's very limited in functionality but the little it do is awesome.
It's soul purpose is to take a file name, load it as an image and you can treat the object like a traditional pyglet.sprite.Sprite, meaning you can move it around and manipulate it in many ways.
It saves a few lines of code having to load all the images you need and as you can see in gui_classes_generic.py you can add a heap of functions that's "invisible" and normally not readily availbale to a normal sprite class.
I use this a bunch! But the code gets complicated real fast so I kept this post simple on purpose.
the flip function
Even in my class, I still need to use flip() in order to update the contents of the screen. This is because .clear() clears the window as you would expect, that also triggers a redraw of the scene.
bg.draw() might in some cases trigger a redraw if the data is big enough or if something else happens, for instance you move the window.
but calling .flip() will tell the GL backend to force a redraw.
Further optimizations
There's a thing called batched rendering, basically the graphic card is designed to take enormous ammounts of data and render it in one go, so calling .draw() on several items will only clog the CPU before the GPU even gets a chance to shine. Read more about Batched rendering and graphics! It will save you a lot of frame rates.
Another thing is to keep as little functionality as possible in the render() loop and use the event triggers as your main source of coding style.
Pyglet does a good job of being fast, especially if you only do things on event driven tasks.
Try to avoid timers, but if you really do need to use time for things, such as removing cat.png after a certain ammount of time, use the clock/time event to call a function that removes the cat. Do not try to use your own t = time() style of code unless you know where you're putting it and why. There's a good timer, I rarely use it.. But you should if you're starting off.
This has been one hell of a wall of text, I hope it educated you some what in the life of graphics and stuff. Keep going, it's a hurdle to get into this kind of stuff but it's quite rewarding once you've mastered it (I still haven't) :)

Using Pygame, draw a copy of an image in a different location

This is kind of a follow up to this question:
Note: This in python, using the pygame library.
I'm wanting to get some clarification on this statement which is a comment from the above question:
Another important thing, don't use the same Sprite() nor the same
Rectangle in different locations. If you want to draw another copy of
the same image in a new location make a copy of the rectangle, then
create a new Sprite() object.
I have a group of game assets and I want to pick one at random to add my
sprite group. I am currently doing this as such:
class TestSprite(pygame.sprite.Sprite):
def __init__(self, strFolder):
super(TestSprite, self).__init__()
self.image = load_image(strFolder + 'x.png'))
#....
def GetRandomAsset():
rnd = random.randrange(1, 4)
if rnd == 1:
return TestSprite('assets/A/')
elif rnd == 2:
return TestSprite('assets/B/')
elif rnd == 3:
return TestSprite('assets/C/')
else:
return TestSprite('assets/D/')
#....
my_group = pygame.sprite.Group()
my_group.add(GetRandomAsset())
#.....
#Control FPS
#Check Events
#Update State
#Clear Screen
#Draw Current State
#Update Display
Everything works as expected; Every time I run the above code, a new sprite is displayed to the screen. My problem is every time i add a sprite, im having to load it from the disk, even tho im using the same 4 images over and over again.
I imagine it would be smarter for me to store all my assets in a global list called PRELOADEDASSETS. Then whenever I need one just do: my_group.add(PRELOADEDASSETS[x])
But when I try to do that, i can only have one copy of each of my assets.
How can I preload all my assets, and then grab the stored data when needed?
Thanks!
Another important thing, don't use the same Sprite() nor the same Rectangle in different locations. If you want to draw another copy of the same image in a new location make a copy of the rectangle, then create a new Sprite() object.
This is a valid advice in this context. It's of course no problem to blit a surface multiple times to different locations (using different Rects for example), but if you use a Group to handle/simplify your drawing, you need one object for each location you want to draw your image.
To answer your actual question:
You don't need to cache the Sprite instances. Just cache the Surface instances.
This way, you can have different Sprite objects that draw the same image to different locations.
A simple cache implementation could look like this:
images = {'a': load_image('assets/A/x.png'),
'b': load_image('assets/B/x.png'),
'c': load_image('assets/C/x.png'),
'd': load_image('assets/D/x.png')}
...
class TestSprite(pygame.sprite.Sprite):
def __init__(self, image_key):
super(TestSprite, self).__init__()
self.image = images[image_key]
...
def GetRandomAsset():
image_key = random.choice(images.keys())
return TestSprite(image_key)
This will load all images upfront (may or maynot what you want); adding laziness is easy, but you probably don't want possible lags (what is best depends how many images you actually load and when).
You could even just go through all folders in your game's directory and detect and load all images automatically, but such sprees are out of scope of this answer.

Categories