There are some things I would comment on, outside of what DeathIncarnate said in their answer.
The is_prime
is fine, I would change two small details:
It is uncommon in Python to check the type of the input value (if not isinstance(num, int)
). If the input is not an int
, then all sorts of things are going wrong in the calling function. Is it really the responsibility of this function to avoid those? You could add an assert
, which can be disabled if necessary. But mostly you should rely on the static type checking, which is what you have the type annotations for. This happens outside of executing the Python code, and therefore does not influence the execution speed.
The range range(2, math.ceil(num ** 0.5) + 1)
is too wide. If num**0.5
is not an integer, then the square of its ceil will be larger than num
, and therefore does not need to be checked. You should use math.floor
instead.
But it generally contains too many numbers: if you can't divide by 2, you won't be able to divide by 4, 6, 8, ... This range could skip all even numbers. In fact, this range should skip all non-prime numbers, which is where my comment under your question hinted at.
The sieve of Erastothenes would be the standard way of producing a list of all primes up to a given number. It checks each new number to not be divisible by the primes found so far (in a manner of speaking, it actually doesn't use any divisions at all). In your case, you need a list of all primes up to and including x_size * y_size
. Let's call this list primes
(we'll come back to this later).
I agree with DeathIncarnate, you don't need an RGB image here, you just need a gray-scale image, which has a single channel. You can write a PNG of such a gray-scale image just fine. Because your code will be simpler, it is preferable.
To represent the image, I would use a NumPy array. Because NumPy arrays are easier to work with and more efficient to index than PIL.Image
objects. Converting between the two is trivial, typically no data needs to be copied.
Earlier I referred primes
, the list of primes. This list can be used to index into a NumPy array:
import numpy as np
# ...
img = np.zeros((y_size, x_size), dtype=np.uint8)
img.reshape(-1)[primes] = 255
That's it, that's all you need to do to create your image.
...well, in your code you start counting pixels from 1, not 0 as I did there. To subtract 1 from your primes, you can convert the list to a NumPy array first:
img.reshape(-1)[np.array(primes) - 1] = 255
Now, to save the image using Pillow, you can cast to a PIL.Image
:
Image.fromarray(img).save('primes.png')
I wanted to try these ideas out. rdesparbes posted code with a better prime list generation similar to ideas I wrote above. This reduced the computation time on my machine from the 2.8s of OP's code to 0.63s. Using a NumPy array instead of a PIL.Image
object, the time went down about 12%, to 0.55s. Implementing an actual sieve of Erastothenes (which doesn't do any divisions at all) reduced the time to 1/5th of that, 0.11s. This is about 25 times faster than OP's code.
This is the code I put together for testing. It's a bit messy, sorry for that, don't pay attention to the style, function names, or the functions themselves writing to file.
Note that about half of the 0.11s is converting the NumPy array to PIL.Image
, and saving it to file.
import math
import time
import numpy as np
from PIL import Image
# --- OP ---
def is_prime(num: int) -> bool:
if not isinstance(num, int):
raise TypeError("num must be integer!")
if num < 2:
return False
if num == 2:
return True
for i in range(2, math.ceil(num ** 0.5) + 1):
if num % i == 0:
return False
return True
def bw(i: int) -> tuple[int, int, int]:
return (255 if is_prime(i) else 0,) * 3
def original_image(x_size: int, y_size: int = 0):
if y_size == 0:
y_size = x_size
image = Image.new("RGB", (x_size, y_size), "black")
for y in range(y_size):
for x in range(x_size):
image.putpixel((x, y), bw(x + y * x_size + 1))
image.save("original_image.png")
# --- RDESPARBES https://codereview.stackexchange.com/a/284350/151754 ---
def compute_primes(maximum: int) -> list[int]:
# updated by Cris to not test even numbers, and a few other tweaks
if maximum <= 2:
return []
primes = [2]
def is_prime(number_to_test_: int) -> bool:
max_threshold = int(number_to_test_ ** 0.5)
for prime in primes:
if prime > max_threshold:
break
if number_to_test_ % prime == 0:
return False
return True
for number_to_test in range(3, maximum, 2):
if is_prime(number_to_test):
primes.append(number_to_test)
return primes
def rdesparbes_image(x_size: int, y_size: int | None = None) -> None:
if y_size is None:
y_size = x_size
primes: list[int] = compute_primes(maximum=x_size * y_size + 1) # fixed upper limit!
image = Image.new("RGB", (x_size, y_size), "black")
for prime in primes:
y, x = divmod(prime - 1, x_size)
image.putpixel((x, y), (255, 255, 255))
image.save("rdesparbes_image.png")
# --- CRIS #1: like above, but using NumPy array ---
def better_image(x_size: int, y_size: int | None = None) -> None:
if y_size is None:
y_size = x_size
primes = compute_primes(x_size * y_size + 1)
img = np.zeros((y_size, x_size), dtype=np.uint8)
img.reshape(-1)[np.array(primes) - 1] = 255
Image.fromarray(img).save('better_image.png')
# --- CRIS #2: sieve of Erastothenes ---
def sieve_image(x_size: int, y_size: int | None = None) -> None:
if y_size is None:
y_size = x_size
primes = np.ones(x_size * y_size + 1, dtype=np.bool_)
primes[0:2] = False
for ii in range(2, len(primes)):
if primes[ii]:
primes[ii**2::ii] = False
primes = np.reshape(primes[1:], (y_size, x_size)).astype(np.uint8)
primes *= 255
Image.fromarray(primes).save('sieve_image.png')
# --- MAIN ---
if __name__ == "__main__":
t = time.time()
original_image(1000)
print(f"{time.time() - t:0.3f}s")
t = time.time()
rdesparbes_image(1000)
print(f"{time.time() - t:0.3f}s")
t = time.time()
better_image(1000)
print(f"{time.time() - t:0.3f}s")
t = time.time()
sieve_image(1000)
print(f"{time.time() - t:0.3f}s")
PIL.Image.new("1", (x_size, y_size), "black")
for a 1bpp black/white format (often used for masks), s'il vous plaît! Your file will be a tiny fraction of the size. Or at the very least,PIL.Image.new("L", ...)
(for 8bpp grayscale). \$\endgroup\$