2
\$\begingroup\$

I made classes to use in generating square/rectangular shapes on a texture.

    import numpy as np
    import random

    class Tex_SHR:
        def __init__(self, resolution: set, colorGrade:str, genNum: int, genOption: int):
            self.resolution = resolution
            self.colorGrade = colorGrade #Not yet in use
            self.genNum = genNum
            self.genOption = genOption
            self.texture = None
            self.blurFeild = None #Not yet in use
            self.noiseFeild = None #Not yet in use
            self.seedVal = None #Not yet in use

        def createArray(self):
            return np.ones(shape = (self.resolution[0] , self.resolution[1]), dtype = np.int16, order = 'C')

    class Rect_GenTex_SHR(Tex_SHR):
        def __init__(self, resolution: set, colorGrade: str, genNum: int, genOption:int): 
            super().__init__(resolution, colorGrade, genNum, genOption)
            self.texture = self.createArray()

     # Might implement different rectGen Types i.e.: Near Square(Creates squares or rects close
     # to squares), square, large, large and small, small, etc.

        def genRectsNormal(self):
            xDmn = self.texture.shape[0]
            yDmn = self.texture.shape[1]
            paddingFactorX = round( xDmn/ 2)
            paddingFactorY = round( yDmn/2 )
            xRand = random.randint(0, xDmn - paddingFactorX )
            xRandMax = random.randint(xRand + 3 , xDmn)
            yRand = random.randint(0, yDmn - paddingFactorY )
            yRandMax = random.randint(yRand + 3, yDmn)

            return ((xRand, xRandMax), (yRand, yRandMax))

    #Implement different types of value generation i.e.: Close values(Values of 
    #rects don't differ by much), Far(Differ by a lot), cluster(Think about this one)
        def grab_GenType(self):
            if self.genOption == 0:
                return self.genRectsNormal()

        def generate_TEX(self):
            squares = []
            for num in range(self.genNum):
                rectValue = random.randint(0,256)
                coords = self.grab_GenType()
                squares.append((coords, rectValue))
                for xRange in range(coords[0][0], coords[0][1]):
                    for yRange in range(coords[1][0], coords[0][1]):
                        self.texture[xRange, yRange] = rectValue
                        #print("TEST: ", "\n", self.texture, "\n", coords)

b = Rect_GenTex_SHR((500,800), "Red", 100, 0)
b.generate_TEX()

I'm using numpy arrays because I want it to be useable with textures and numpy is said to faster and takes less space in memory compared to a List of Lists approach. Is numpy still the best choice?

Used the Tex_SHR class because I plan on having different classes that generate different shapes using the same set as the base class.

The dtype = np.int16 is to limit memory allocated to the array.

paddingFactorX/Y is there to keep the randomint from generating numbers really close to each other

genOption and grab_GenType() is there because I want to have different ways to generate the rectangles/squares.

The biggest issue I've had is that running the function at the end takes on average 2.45 seconds on my laptop. Even though my laptop is pretty weak I'm worried that when thrown larger textures, with larger amounts to generate it'll basically become unusable even on better machines. Would also like to make my code more "pythonic" if that's possible.

Laptop specs: 12 GB Ram Intel I5-1035G1 Quad core Windows 10

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Welcome to Code Review.

I'm afraid I don't have time for as detailed a review as I'd like as I have to go to work. But I wanted to mention one thing about idiomatic library use. When you use a big foundational library like numpy, the best practices for using it may be different from how you'd write most Python code. You need to use those best practices to get full use of the library. In the particular case of numpy, a lot of the advantage comes from telling it to do the same operation to loads of elements at the same time.

For example, instead of

for xRange in range(coords[0][0], coords[0][1]):
    for yRange in range(coords[1][0], coords[0][1]):
        self.texture[xRange, yRange] = rectValue

you could do

self.texture[coords[0][0]:coords[0][1], coords[1][0]:coords[1][1]] = rectValue.

That changes all the pixels in all those coords "at once".

If you're a programmer you'll already know that computers aren't magic, and it isn't actually all "at once". Setting a larger amount of memory takes time. Nevertheless numpy can do that operation a lot quicker than the explicit nested for loops. It takes advantage of things like simd instructions (1), memory locality (2), and compiled code (3).

Reading up on these things isn't strictly necessary, although I certainly find it helps if I understand the "magic". If anything, the reason to use something like numpy is that they take care of these details so you don't have to. The key thing is that when you use numpy, the idiomatic approach is to work with entire arrays or as large sections of them as makes sense.

(1) A modern processor can actually set between 4 and 32 of your 16 bit integer values in one instruction, whereas your loop will only set one at a time.
(2) Updating adjacent elements in an array one after another avoids much of the overhead of picking them out.
(3) Because much of numpy is actually C libraries and such, the compiler can check ahead of time that it will be safe to do the next thing. That means that, within one numpy call, it doesn't have to do a lot of the extra python stuff like GIL unlocking or reference counting to make sure it's safe to proceed.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you for the detailed explanation and suggestions. I'll read up on simd instructions and memory locality. \$\endgroup\$
    – Nyos
    Sep 16 at 1:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.