Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.Sign up
Convert image to grayscale #7
I'm new to crystal. I tried to use stumpy_png to convert an image to grayscale. The code I created looks like this:
require "stumpy_png" canvas = StumpyPNG.read("image.png") (0...canvas.width).each do |x| (0...canvas.height).each do |y| color = canvas[x, y] g = 0_u32 g = (g + color.r + color.g + color.b) / 3 canvas[x, y] = StumpyPNG::RGBA.from_gray_n(g, 16) end end StumpyPNG.write(canvas, "output.png")
Note: I know that grayscale convertion is a bit more complicated than the average value of 3 colors, but this is a simple example.
As you can see, the code looks very complicated. It seems that I cannot assign the value to the color directly (
All this makes me think that I'm doing something wrong. I found some occurences of the term 'grayscale' in stumpy_png repo, but I don't really know how to use it. Could you please provide an example of how to correctly (that is: efficiently and maybe in a less verbose way) solve this problem?
Some things I would like to know are:
I'll try to answer your questions in order:
About the bad performance:
Can you upload the image your are using somewhere
Are you compiling your program with the
Maybe it is possible to implement the
canvas.map! do |color| g = (g + color.r + color.g + color.b) / 3 StumpyPNG::RGBA.from_gray_n(g, 16) end
Additionally, if you come up with a nice function to convert colors to grayscale,
Would you find those changes useful?
Thanks for the
from PIL import Image im = Image.open('image.png') im_gray = im.convert('L') im_gray.save('output.png')
I am disappointed by the performance. I guess it is the result of using colors as structs and creating a new one for each pixel.
Please don't get me wrong. Your shard creates a very useful abstraction for images, and it's great that you help Crystal community by creating tools like this one. But still, Crystal was supposed to be efficient like C while still having a high level syntax like Ruby (I am not a Rubyist, but I could appreciate a language with both these advantages). It seems however that, at least in this case, Crystal is neither high level (the code is quite verbose) nor efficient (compared with Python, which is supposed to be slower than anything you can compile!).
Let's leave the verbosity of the code for a moment. Do you think something can be done here about the performance? Some preferably small change to stumpy_png itselt or to my code that would boost it up a bit?
Regarding your question, for the moment I don't want to create a PR for
I inserted some code to get the elapsed time after each of the operations
This is not very scientific but by the looks of it,
On the other hand loading and saving the image is much faster in python,
Additionally the output png of the python version is only ~240kb in size
I would say that is definitely the fault of my code
This lib it's still in a very early stage of development
Your measurements seem to be consistent with my observations. Initially I was printing the width and height of the loaded image (to check if the loading process was successful) and it took a moment for the output to appear. So it seems that the loading process itself takes a lot of time.
Your shard is one of two that enable image processing. The other one still has
Crystal is already 2 years old (5 if you count the initial version). I don't have much experience with young programming languages, but I thought that the number of available tools would be higher at this stage of development. Perhaps I was wrong and it takes more time. Or perhaps I chose the wrong topic (I guess HTTP server is something that Crystal programmers need more and it would be more intensively developed).
I just wanted a quick dive in Crystal to check how it works, how the code looks like and what is the performance. It seems though that this language is a bit too young to be fully useful. I've had some issues with installing shards, and now the most popular image processing lib turns out to be in a very early stage of development. It seems that Crystal is still a language that needs a lot of contributions. I might help in the future, but that was not the purpose of this little experiment.
Thank you very much for your help and quick answers! It seems that great community is something that Crystal does not lack :) I hope that you will continue working on this lib and make it faster and easier to use than PIL :)
Wow, judging from some comments in the source code
I guess these kinds of performance optimizations
(It seems @l3kn beat me for a few seconds :-D)
@pfertyk Hi! I've profiled your program and timed each of the parts (read, transform, write), here are the results:
That means that transforming the PNG isn't the bottleneck. I didn't think it was because an RGBA is a struct of four UInt16, so in total it's 64 bytes so it's a word and passing that around should be very fast.
We still need to optimize the read and write parts. I believe it's possible, I think there are many intermediate memory allocations that could be avoided. As with every language, you can write slow code in it, using a language doesn't guarantee good efficiency.
I'll try to see how this can be done (I really like optimizing programs and efficient code, and I really enjoy programs where you can see something nice as the output, such as an image ^_^)
After this comment (by @l3kn) I had no doubt that the processing itself is not a problem here. But the problem for me (as a developer) is that the library does not perform as well as expected. It doesn't matter if the problem is processing the image or reading/writing it. I wanted to convert images efficiently and I couldn't do it.
That is nothing that a good contributor couldn't fix, of course :) I can't wait to see how stumpy_png will work after your modifications, @asterite. Image processing is a domain that could really benefit from high performance.
referenced this issue
Nov 28, 2016
Now there are some ways to make the code a bit less verbose
require "stumpy_png" canvas = StumpyPNG.read("image.png") canvas.map! do |color| g = (color.r.to_u32 + color.g + color.b) / 3 StumpyPNG::RGBA.new(g.to_u16) end StumpyPNG.write(canvas, "output.png")
and (thank to @asterite) the performance is a lot better.
Would it help to have a
require "stumpy_png" canvas = StumpyPNG.read("image.png") canvas.map! &.to_gray StumpyPNG.write(canvas, "output.png")
That way all the "verbosity" would be gone and it would be almost the same as the python version. I don't know if
@l3kn I tried your code, but I got this error:
Could you help me with this?
That was it, thanks! The code works and takes around 0.48s to execute.
I've got to say, I'm impressed. In just a few days you guys doubled the speed and improved the readability of the code. Crystal has a really amazing community :)
Do you think it would be possible to make this program work faster than the one in Python (so less than 0.2s)? It is a bit weird for a compiled language to lose the speed competition with Python ;)
@asterite One of the next features I'd like to implement
This would require methods like
I'm pretty sure it is possible (and going to happen soon) ;)
One of the reasons the python version is faster
If I take out the grayscale conversion in both programs
@pfertyk Also remember that PIL (or Pillow, as I understand) has most of the core of the processing code written in C. Check the project, click on the colored bar bellow "commits", "branches", etc, you'll see: Python 59.2%, C 39.8%, etc. The conversion code is here. So this is Crystal vs. C, really, so it's no wonder C is faster right now, specially with 15 years ahead of us :-)
I added the ability specify the color mode and bit depth of the resulting image,
require "stumpy_png" canvas = StumpyPNG.read("image.png") # No need to convert to grayscale here StumpyPNG.write(canvas, "output.png", color_type: :grayscale, bit_depth: 8)
The python version seems to be using some other method (not averaging red, green and blue)
The updated (very scientific) benchmarks are:
So the crystal version is now (for this specific use case)
Currently I'm not 100% sure why the 16bit version in crystal is faster,