Real-time pixel plotting (Langton's Ant) for a beginner

82 views
Skip to first unread message

JSwift2046

unread,
May 26, 2018, 10:39:07 PM5/26/18
to pyqtgraph
Hi, sorry for such a beginner's question, but I've only been using Python for a couple of weeks, and almost entirely figuring out Keras.

But I already love Pyqt, and thanks for developing such a great package. I think this will be a really simple one, but could anyone advise a sensible way to write this?

This runs a simple animated Langton's Ant demo, but very slowly, because every update I'm using view.addItem – which is surely not the best way to plot real-time pixels. But very simple to write and look at. Thanks so much – this has been driving me slightly crazy.

from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
import pyqtgraph as pg
import sys

direction = 1
x = 250
y = 250

app = QtGui.QApplication([])

w = pg.GraphicsView()
w.show()
w.resize(500,500)
w.setWindowTitle('Ant demo')

view = pg.ViewBox()
w.setCentralItem(view)
view.setAspectLocked(True)

screen = np.zeros((500,500))
img = pg.ImageItem(screen)
view.addItem(img)

img.setLevels([0, 1])

def update():
    global screen, img, x, y, direction, w

    hello = screen[x,y]

    if hello == 1:
        direction += 1
        screen[x,y] = 0
    else:
        direction -= 1
        screen[x,y] = 1

    if direction == 0:
        direction = 4
    if direction == 5:
        direction = 1

    if direction == 1:
        y -= 1
    elif direction == 2:
        x += 1
    elif direction == 3:
        y += 1
    elif direction == 4:
        x -= 1

    img = pg.ImageItem(screen)
    view.addItem(img)

timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(1)

if __name__ == '__main__':
    import sys
    if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
        QtGui.QApplication.instance().exec_()

Patrick

unread,
May 28, 2018, 4:34:37 AM5/28/18
to pyqtgraph
Hi,

You just need to update the image data for the ImageItem (your code keeps adding many new ImageItems to the scene!). Change:

img = pg.ImageItem(screen)
view
.addItem(img)

to

img.setImage(screen)

and it will run a lot faster.


(Not related to your issue, but ultimately you'd like to put all this in, say, an AntDemo class extending a QWidget or similar. Then you can stop using global variables which are not the best way to be passing data around. But for a simple example like this, globals are easy.)

Patrick

JSwift2046

unread,
May 28, 2018, 12:45:03 PM5/28/18
to pyqtgraph
Hi Patrick,

Exactly what I was looking for – I'd really hoped there was an instruction like that I was missing .. That's going to make things much easier going forwards.

Thanks so much :)

JSwift2046

unread,
Jun 2, 2018, 3:38:07 PM6/2/18
to pyqtgraph
Hi again,

I don't suppose you might have a moment to help me out again, re: your wise suggestion of putting things in a Class?

I couldn't find an example that quite did the same thing, so I'm prodding around haphazardly trying to get this to work – any help hugely appreciated! At the moment it seems to do nothing, and I'm sure there's a very obvious reason for that.

Thank you so much



from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
import pyqtgraph as pg
import sys

class AntDemo(QWidget):

def __init__(self):
super().__init__()
self.initUI()

def initUI(self):
self.app = QtGui.QApplication([])

w = pg.GraphicsView(self)
w.show()
w.resize(500,500)
w.setWindowTitle('Ant demo')

view = pg.ViewBox()
w.setCentralItem(view)
view.setAspectLocked(True)

screen = np.zeros((500,500))
img = pg.ImageItem(screen)
view.addItem(img)
ptr = 1
img.setLevels([0, 1])
direction = 1
x = 250
y = 250

def update():

hello = screen[x,y]

ptr += 1
if hello == 1:
direction += 1
screen[x,y] = 0
else:
direction -= 1
screen[x,y] = 1

if direction == 0:
direction = 4
if direction == 5:
direction = 1

if direction == 1:
y -= 1
elif direction == 2:
x += 1
elif direction == 3:
y += 1
elif direction == 4:
x -= 1

if (ptr % 60) == 0:
img.setImage(screen)

timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(0)



if __name__ == '__main__':
    
    app = QApplication(sys.argv)
    ex = AntDemo()
    sys.exit(app.exec_())






On Monday, 28 May 2018 09:34:37 UTC+1, Patrick wrote:

Patrick

unread,
Jun 3, 2018, 11:59:33 PM6/3/18
to pyqtgraph
Two main points:

A widget can be added to another widget or window etc. If you show() a bare widget, it will be placed in its own window automatically. You don't need to make your own window or anything.

Encapsulating in a class means you don't use global namespace variables for data specific to that instance of the object. Hence all the "self.x" etc, as x, y, the image etc are all specific to one particular instance of the AntDemo object. (You should be able to place several of these side by side in a window and they will be completely independent.)

I have also added a simple widget layout to demo how a composite widget can be put together to add functionality to your widget. (If you just wanted to display the image only, then you could extend pyqtgraph.GraphicsView instead, which itself also indirectly an extension of a widget).



from PyQt5 import QtCore, QtGui, QtWidgets
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
import pyqtgraph as pg
import sys

class AntDemo(QtWidgets.QWidget):

    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):

        # Manually lay out display area and controls for the widget
        # Could also use Qt Designer .ui file and uic module/pyuic5 utility
        self.horizontalLayout = QtWidgets.QHBoxLayout(self)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.plot = pg.GraphicsView(self)
        self.horizontalLayout.addWidget(self.plot)
        self.verticalLayout = QtWidgets.QVBoxLayout()
        self.verticalLayout.setObjectName("verticalLayout")
        self.pushButton = QtWidgets.QPushButton(self)
        self.pushButton.setObjectName("pushButton")
        self.verticalLayout.addWidget(self.pushButton)
        spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
        self.verticalLayout.addItem(spacerItem)
        self.horizontalLayout.addLayout(self.verticalLayout)
        self.pushButton.setText("&Clear")

        # Connect signals
        self.pushButton.clicked.connect(self.clear)

        # Set up the plot and image buffer
        self.screen = np.zeros((500, 500))
        self.x = 250
        self.y = 250
        self.direction = 1
        self.img = pg.ImageItem(self.screen)
        self.plot.addItem(self.img)
        self.plot.setMinimumSize(*self.screen.shape)

        # Configure the update timer
        self.timer = QtCore.QTimer()
        self.timer.timeout.connect(self.update)
        self.timer.start(0)

    def update(self):

        if self.screen[self.x, self.y] == 1:
            self.direction += 1
            self.screen[self.x, self.y] = 0
        else:
            self.direction -= 1
            self.screen[self.x, self.y] = 1

        if self.direction == 0:
            self.direction = 4
        if self.direction == 5:
            self.direction = 1

        if self.direction == 1:
            self.y -= 1
        elif self.direction == 2:
            self.x += 1
        elif self.direction == 3:
            self.y += 1
        elif self.direction == 4:
            self.x -= 1

        # Wrap coordinates around the image box
        self.x = self.x % self.screen.shape[0]
        self.y = self.y % self.screen.shape[1]
        
        self.img.setImage(self.screen)

    def clear(self):
        self.screen = np.zeros((500, 500))

if __name__ == '__main__':
    
    app = QtGui.QApplication(sys.argv)
    ex = AntDemo()
    ex.show()
    sys.exit(app.exec_())

JSwift2046

unread,
Jun 4, 2018, 2:22:31 AM6/4/18
to pyqtgraph
Thank you SO MUCH, Patrick.

I've just been experimenting with QT Designer today, but this brings everything together in a way that would've taken me forever to figure out – so it's hugely appreciated. And I think the window view will be much more appropriate for adding information and interactive tools for what I'm looking to do.

And presumably if I wanted to have multiple Ants in the same window, sharing the screen buffer and interacting with each others' pixels, it would be best to put the Ant instructions in a function within Update, and call it from a for i in range num_of_ants, and pass x[i] and y[i] pointers back and forth to keep track of their positions?
Thanks so much again,

JSwift2046

unread,
Jun 4, 2018, 2:30:22 AM6/4/18
to pyqtgraph

This was a nice surprise btw.


I added a few lines to have it update the screen every 60 cycles (for speed):


self.x = self.x % self.screen.shape[0]

        self.y = self.y % self.screen.shape[1]

        

        self.ptr += 1

        if (self.ptr % 60) == 0:

            self.img.setImage(self.screen)


With a ptr set up in the initUI

self.direction = 1

        self.ptr = 0

        self.img = pg.ImageItem(self.screen)


And the screen wraps, when they go out of bounds – neat!





Reply all
Reply to author
Forward
0 new messages