Change all pixels that lie in a given color range to a new color











up vote
3
down vote

favorite












This is what I have so far but it's not perfect... Any input would be very helpful!



def picture_reset_pixels(filename, from_color, to_color, target_color):
img = Image.open(filename)
pixels = list(img.getdata())
red_list=
for i in pixels:
R= i[0]
G=i[1]
B=i[2]

if R >= from_color[0] and R<= to_color[0] and G >=from_color[1]
and G<= to_color[1] and B >= from_color[2] and B<=
to_color[2]:
red_list.append(target_color)
else:
red_list.append((round(R),round(G),round(B)))

red_image = Image.new(img.mode,img.size)
red_image.putdata(red_list)
return red_image









share|improve this question




















  • 4




    Your if statement is split over three lines, in a way that I think is not valid in Python. If you put brackets around it, then you can break it up across lines: stackoverflow.com/questions/5253348/…
    – Cris Luengo
    Nov 16 at 23:00








  • 2




    Also clarify what imports apply. What are Image and round?
    – 200_success
    Nov 17 at 1:27






  • 1




    @200_success round is a built-in function. For the Image my guess is it comes from pillow.
    – Arthur Havlicek
    Nov 17 at 2:12












  • This question is lacking context. What are the used imports and what is the code supposed to do?
    – Mast
    12 hours ago















up vote
3
down vote

favorite












This is what I have so far but it's not perfect... Any input would be very helpful!



def picture_reset_pixels(filename, from_color, to_color, target_color):
img = Image.open(filename)
pixels = list(img.getdata())
red_list=
for i in pixels:
R= i[0]
G=i[1]
B=i[2]

if R >= from_color[0] and R<= to_color[0] and G >=from_color[1]
and G<= to_color[1] and B >= from_color[2] and B<=
to_color[2]:
red_list.append(target_color)
else:
red_list.append((round(R),round(G),round(B)))

red_image = Image.new(img.mode,img.size)
red_image.putdata(red_list)
return red_image









share|improve this question




















  • 4




    Your if statement is split over three lines, in a way that I think is not valid in Python. If you put brackets around it, then you can break it up across lines: stackoverflow.com/questions/5253348/…
    – Cris Luengo
    Nov 16 at 23:00








  • 2




    Also clarify what imports apply. What are Image and round?
    – 200_success
    Nov 17 at 1:27






  • 1




    @200_success round is a built-in function. For the Image my guess is it comes from pillow.
    – Arthur Havlicek
    Nov 17 at 2:12












  • This question is lacking context. What are the used imports and what is the code supposed to do?
    – Mast
    12 hours ago













up vote
3
down vote

favorite









up vote
3
down vote

favorite











This is what I have so far but it's not perfect... Any input would be very helpful!



def picture_reset_pixels(filename, from_color, to_color, target_color):
img = Image.open(filename)
pixels = list(img.getdata())
red_list=
for i in pixels:
R= i[0]
G=i[1]
B=i[2]

if R >= from_color[0] and R<= to_color[0] and G >=from_color[1]
and G<= to_color[1] and B >= from_color[2] and B<=
to_color[2]:
red_list.append(target_color)
else:
red_list.append((round(R),round(G),round(B)))

red_image = Image.new(img.mode,img.size)
red_image.putdata(red_list)
return red_image









share|improve this question















This is what I have so far but it's not perfect... Any input would be very helpful!



def picture_reset_pixels(filename, from_color, to_color, target_color):
img = Image.open(filename)
pixels = list(img.getdata())
red_list=
for i in pixels:
R= i[0]
G=i[1]
B=i[2]

if R >= from_color[0] and R<= to_color[0] and G >=from_color[1]
and G<= to_color[1] and B >= from_color[2] and B<=
to_color[2]:
red_list.append(target_color)
else:
red_list.append((round(R),round(G),round(B)))

red_image = Image.new(img.mode,img.size)
red_image.putdata(red_list)
return red_image






python python-3.x image






share|improve this question















share|improve this question













share|improve this question




share|improve this question








edited Nov 17 at 1:25









200_success

127k15148411




127k15148411










asked Nov 16 at 20:47









Victoria

191




191








  • 4




    Your if statement is split over three lines, in a way that I think is not valid in Python. If you put brackets around it, then you can break it up across lines: stackoverflow.com/questions/5253348/…
    – Cris Luengo
    Nov 16 at 23:00








  • 2




    Also clarify what imports apply. What are Image and round?
    – 200_success
    Nov 17 at 1:27






  • 1




    @200_success round is a built-in function. For the Image my guess is it comes from pillow.
    – Arthur Havlicek
    Nov 17 at 2:12












  • This question is lacking context. What are the used imports and what is the code supposed to do?
    – Mast
    12 hours ago














  • 4




    Your if statement is split over three lines, in a way that I think is not valid in Python. If you put brackets around it, then you can break it up across lines: stackoverflow.com/questions/5253348/…
    – Cris Luengo
    Nov 16 at 23:00








  • 2




    Also clarify what imports apply. What are Image and round?
    – 200_success
    Nov 17 at 1:27






  • 1




    @200_success round is a built-in function. For the Image my guess is it comes from pillow.
    – Arthur Havlicek
    Nov 17 at 2:12












  • This question is lacking context. What are the used imports and what is the code supposed to do?
    – Mast
    12 hours ago








4




4




Your if statement is split over three lines, in a way that I think is not valid in Python. If you put brackets around it, then you can break it up across lines: stackoverflow.com/questions/5253348/…
– Cris Luengo
Nov 16 at 23:00






Your if statement is split over three lines, in a way that I think is not valid in Python. If you put brackets around it, then you can break it up across lines: stackoverflow.com/questions/5253348/…
– Cris Luengo
Nov 16 at 23:00






2




2




Also clarify what imports apply. What are Image and round?
– 200_success
Nov 17 at 1:27




Also clarify what imports apply. What are Image and round?
– 200_success
Nov 17 at 1:27




1




1




@200_success round is a built-in function. For the Image my guess is it comes from pillow.
– Arthur Havlicek
Nov 17 at 2:12






@200_success round is a built-in function. For the Image my guess is it comes from pillow.
– Arthur Havlicek
Nov 17 at 2:12














This question is lacking context. What are the used imports and what is the code supposed to do?
– Mast
12 hours ago




This question is lacking context. What are the used imports and what is the code supposed to do?
– Mast
12 hours ago










3 Answers
3






active

oldest

votes

















up vote
2
down vote













Improving the general implementation



Generally we want to avoid appending to a list too many times if we can help it. This is because lists are dynamic arrays, so appends can be unnecessarily expensive (though the dynamic part helps with the efficiency). Instead, when we know the size of the input set (i.e. the number of pixels in the image), we can take advantage of that information. But in this case, that will be unnecessary because Image provides us with some handy helper methods to handle this very situation:



from PIL import Image

def picture_reset_pixels(filename, from_color, to_color, target_color):
img = Image.open(filename)
pixels = img.load()
for i in range(img.size[0]):
for j in range(img.size[1]):
if all(from_color[k] <= pixels[i, j][k] <= to_color[k] for k in range(3)):
pixels[i, j] = target_color + (255,)
return img


Assigning pixels to img.load() allows us to take advantage of directly editing the image. Since copying the image into another list was unnecessary originally (to clarify, the img variable doesn't affect the file on disk that img originated from, i.e. the file at location filename), editing directly is the best way.



Edit 2: Looking at it again, I think @Reinderien's point about clarity has merit because range has an exclusive upper bound, which may be confusing because it is asymmetric relative to both of its arguments. I have edited to use the comparison operators <= and >= instead of the range check I mention ahead. The caveat only I have is that using the in range form is more similar to how I think a dedicated ColorRange object (mentioned ahead in the section Going beyond the current form) should handle a range check, since it's not necessary to expose the object's implementation details (though internally, it could still use comparison operators for the check).



Another general improvement I've taken advantage of in this implementation is using range to check if each of the RGB values of a pixel are between the two pixel boundaries of the input arguments. all() serves the purpose that the and statements in the original version served: it requires that all the pixels compared in the iterator pixels[i, j][k] in range(from_color[k], to_color[k] + 1) for k in range(3) are in the required range for the condition to be true.



Edit 1: the concern was raised in comments that testing for in range(from_color[k], to_color[k] + 1) would be extremely inefficient. However, this is mistaken and arises from a misunderstanding of how Python 3.x implement the range object. Checking if a number is in a range is a constant time operation. I would recommend reading this answer to understand Python 3.x's range implementation.



Going beyond the current form



While I've shown you a way to use your current parameter requirements in a more efficient implementation, I think there are some improvements you can make so picture_reset_pixels has more functionality. I think combining from_color, to_color, and target_color into a single dict parameter color_replacements could make this more versatile (where the key-value pairs would derive something like {(from_color, to_color): target_color}; you could even create a custom ColorRange class to encapsulate the idea of a range of colors).



There's also another issue I find with your current implementation: it can only support certain image types. I tried your algorithm on a GIF file and it failed because the pixels in a GIF file are ints, not four-tuples like PNGs. You also do not support the fourth alpha channel of PNG, because your pixel range bounds are only 3-tuples. You might want to consider investigating that further if you want to support multiple image types.






share|improve this answer























  • @Reinderien That would be true if this were Python 2.x. But in Python 3.x, range is the equivalent of Python 2.x's xrange and returns an iterator. Python 3.x also added an optimization for the .__contains__() method so that it is O(1) time.
    – Graham
    Nov 17 at 12:37












  • It smells, to me. Using a pair of inequalities is simpler, and no less legible.
    – Reinderien
    Nov 18 at 4:10










  • @Reinderien Looking at it again, I think I agree with you. The exclusive upper bound of range can be a bit confusing. And Python does have this neat interval comparison where A <= X and X <= B can be written as A <= X <= B so it's not redundant and synonymous to its math form. The caveat only I have is that using the in range form is more similar to how I think a dedicated ColorRange object should handle a range check, since it's not necessary to expose the object's implementation details (though internally, it could still use comparison operators for the check).
    – Graham
    Nov 18 at 12:27




















up vote
2
down vote













Currently you are manipulating the image one pixel at once. Instead you can work on the whole array as one when using the numpy interface:



from PIL import Image
import numpy as np

def read_image(image_path):
img = Image.open(image_path)
return np.asarray(img, dtype="int32")

def write_image(img, image_path):
img = Image.fromarray(np.asarray(img, dtype="uint8"), "RGBA")
img.save(image_path)

def picture_reset_pixels(img, from_color, to_color, target_color):
mask = (img >= from_color).all(axis=-1) & (img <= to_color).all(axis=-1)
img[mask] = target_color

if __name__ == "__main__":
from_color = (100, 100, 100, 255)
to_color = (150, 150, 150, 255)
target_color = (0, 0, 0, 255)
filename = "test.png"
img = read_image(filename)
picture_reset_pixels(img, from_color, to_color, target_color)
write_image(img, filename)


Note that I chose not to return the modified image since the function modifies it inplace. If you want to avoid that, just add img = img.copy() as the first line and add return img at the end.



Also note that this assumes that your image has an alpha channel. If not you need to change "RGBA" to "RGB" and of course have the colours be only three-tuples.






share|improve this answer






























    up vote
    1
    down vote













    The code is overall simple and good but could be improved in a few things that would make it a bit shorter and more readable





    Inside the for loop, in the if / else, you are repeating append in both of your cases. You could do instead



    to_add = (round(R), round(G), round(B))
    if condition:
    to_add = target_color
    red_list.append(to_add)




    On the condition, you can also refactor using chained comparison to improve readability, preferably in ascending order. If you break that line, do so aligning conditionals as follow :



    if from_color[0] <= R <= to_color[0] and 
    from_color[1] <= G <= to_color[1] and
    from_color[2] <= B <= to_color[2]:


    Alternatively, but sightly less explicit, you can iterate over i.



    if all((from_color[j] <= i[j] <= to_color[j] for j in range(3)))




    R, G, B initialisation can be made a single line:



    R, G, B = i


    or, if working with a 4 tuples:



    R, G, B, _ = i





    share|improve this answer










    New contributor




    Arthur Havlicek is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
    Check out our Code of Conduct.


















      Your Answer





      StackExchange.ifUsing("editor", function () {
      return StackExchange.using("mathjaxEditing", function () {
      StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
      StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
      });
      });
      }, "mathjax-editing");

      StackExchange.ifUsing("editor", function () {
      StackExchange.using("externalEditor", function () {
      StackExchange.using("snippets", function () {
      StackExchange.snippets.init();
      });
      });
      }, "code-snippets");

      StackExchange.ready(function() {
      var channelOptions = {
      tags: "".split(" "),
      id: "196"
      };
      initTagRenderer("".split(" "), "".split(" "), channelOptions);

      StackExchange.using("externalEditor", function() {
      // Have to fire editor after snippets, if snippets enabled
      if (StackExchange.settings.snippets.snippetsEnabled) {
      StackExchange.using("snippets", function() {
      createEditor();
      });
      }
      else {
      createEditor();
      }
      });

      function createEditor() {
      StackExchange.prepareEditor({
      heartbeatType: 'answer',
      convertImagesToLinks: false,
      noModals: true,
      showLowRepImageUploadWarning: true,
      reputationToPostImages: null,
      bindNavPrevention: true,
      postfix: "",
      imageUploader: {
      brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
      contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
      allowUrls: true
      },
      onDemand: true,
      discardSelector: ".discard-answer"
      ,immediatelyShowMarkdownHelp:true
      });


      }
      });














       

      draft saved


      draft discarded


















      StackExchange.ready(
      function () {
      StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f207839%2fchange-all-pixels-that-lie-in-a-given-color-range-to-a-new-color%23new-answer', 'question_page');
      }
      );

      Post as a guest















      Required, but never shown

























      3 Answers
      3






      active

      oldest

      votes








      3 Answers
      3






      active

      oldest

      votes









      active

      oldest

      votes






      active

      oldest

      votes








      up vote
      2
      down vote













      Improving the general implementation



      Generally we want to avoid appending to a list too many times if we can help it. This is because lists are dynamic arrays, so appends can be unnecessarily expensive (though the dynamic part helps with the efficiency). Instead, when we know the size of the input set (i.e. the number of pixels in the image), we can take advantage of that information. But in this case, that will be unnecessary because Image provides us with some handy helper methods to handle this very situation:



      from PIL import Image

      def picture_reset_pixels(filename, from_color, to_color, target_color):
      img = Image.open(filename)
      pixels = img.load()
      for i in range(img.size[0]):
      for j in range(img.size[1]):
      if all(from_color[k] <= pixels[i, j][k] <= to_color[k] for k in range(3)):
      pixels[i, j] = target_color + (255,)
      return img


      Assigning pixels to img.load() allows us to take advantage of directly editing the image. Since copying the image into another list was unnecessary originally (to clarify, the img variable doesn't affect the file on disk that img originated from, i.e. the file at location filename), editing directly is the best way.



      Edit 2: Looking at it again, I think @Reinderien's point about clarity has merit because range has an exclusive upper bound, which may be confusing because it is asymmetric relative to both of its arguments. I have edited to use the comparison operators <= and >= instead of the range check I mention ahead. The caveat only I have is that using the in range form is more similar to how I think a dedicated ColorRange object (mentioned ahead in the section Going beyond the current form) should handle a range check, since it's not necessary to expose the object's implementation details (though internally, it could still use comparison operators for the check).



      Another general improvement I've taken advantage of in this implementation is using range to check if each of the RGB values of a pixel are between the two pixel boundaries of the input arguments. all() serves the purpose that the and statements in the original version served: it requires that all the pixels compared in the iterator pixels[i, j][k] in range(from_color[k], to_color[k] + 1) for k in range(3) are in the required range for the condition to be true.



      Edit 1: the concern was raised in comments that testing for in range(from_color[k], to_color[k] + 1) would be extremely inefficient. However, this is mistaken and arises from a misunderstanding of how Python 3.x implement the range object. Checking if a number is in a range is a constant time operation. I would recommend reading this answer to understand Python 3.x's range implementation.



      Going beyond the current form



      While I've shown you a way to use your current parameter requirements in a more efficient implementation, I think there are some improvements you can make so picture_reset_pixels has more functionality. I think combining from_color, to_color, and target_color into a single dict parameter color_replacements could make this more versatile (where the key-value pairs would derive something like {(from_color, to_color): target_color}; you could even create a custom ColorRange class to encapsulate the idea of a range of colors).



      There's also another issue I find with your current implementation: it can only support certain image types. I tried your algorithm on a GIF file and it failed because the pixels in a GIF file are ints, not four-tuples like PNGs. You also do not support the fourth alpha channel of PNG, because your pixel range bounds are only 3-tuples. You might want to consider investigating that further if you want to support multiple image types.






      share|improve this answer























      • @Reinderien That would be true if this were Python 2.x. But in Python 3.x, range is the equivalent of Python 2.x's xrange and returns an iterator. Python 3.x also added an optimization for the .__contains__() method so that it is O(1) time.
        – Graham
        Nov 17 at 12:37












      • It smells, to me. Using a pair of inequalities is simpler, and no less legible.
        – Reinderien
        Nov 18 at 4:10










      • @Reinderien Looking at it again, I think I agree with you. The exclusive upper bound of range can be a bit confusing. And Python does have this neat interval comparison where A <= X and X <= B can be written as A <= X <= B so it's not redundant and synonymous to its math form. The caveat only I have is that using the in range form is more similar to how I think a dedicated ColorRange object should handle a range check, since it's not necessary to expose the object's implementation details (though internally, it could still use comparison operators for the check).
        – Graham
        Nov 18 at 12:27

















      up vote
      2
      down vote













      Improving the general implementation



      Generally we want to avoid appending to a list too many times if we can help it. This is because lists are dynamic arrays, so appends can be unnecessarily expensive (though the dynamic part helps with the efficiency). Instead, when we know the size of the input set (i.e. the number of pixels in the image), we can take advantage of that information. But in this case, that will be unnecessary because Image provides us with some handy helper methods to handle this very situation:



      from PIL import Image

      def picture_reset_pixels(filename, from_color, to_color, target_color):
      img = Image.open(filename)
      pixels = img.load()
      for i in range(img.size[0]):
      for j in range(img.size[1]):
      if all(from_color[k] <= pixels[i, j][k] <= to_color[k] for k in range(3)):
      pixels[i, j] = target_color + (255,)
      return img


      Assigning pixels to img.load() allows us to take advantage of directly editing the image. Since copying the image into another list was unnecessary originally (to clarify, the img variable doesn't affect the file on disk that img originated from, i.e. the file at location filename), editing directly is the best way.



      Edit 2: Looking at it again, I think @Reinderien's point about clarity has merit because range has an exclusive upper bound, which may be confusing because it is asymmetric relative to both of its arguments. I have edited to use the comparison operators <= and >= instead of the range check I mention ahead. The caveat only I have is that using the in range form is more similar to how I think a dedicated ColorRange object (mentioned ahead in the section Going beyond the current form) should handle a range check, since it's not necessary to expose the object's implementation details (though internally, it could still use comparison operators for the check).



      Another general improvement I've taken advantage of in this implementation is using range to check if each of the RGB values of a pixel are between the two pixel boundaries of the input arguments. all() serves the purpose that the and statements in the original version served: it requires that all the pixels compared in the iterator pixels[i, j][k] in range(from_color[k], to_color[k] + 1) for k in range(3) are in the required range for the condition to be true.



      Edit 1: the concern was raised in comments that testing for in range(from_color[k], to_color[k] + 1) would be extremely inefficient. However, this is mistaken and arises from a misunderstanding of how Python 3.x implement the range object. Checking if a number is in a range is a constant time operation. I would recommend reading this answer to understand Python 3.x's range implementation.



      Going beyond the current form



      While I've shown you a way to use your current parameter requirements in a more efficient implementation, I think there are some improvements you can make so picture_reset_pixels has more functionality. I think combining from_color, to_color, and target_color into a single dict parameter color_replacements could make this more versatile (where the key-value pairs would derive something like {(from_color, to_color): target_color}; you could even create a custom ColorRange class to encapsulate the idea of a range of colors).



      There's also another issue I find with your current implementation: it can only support certain image types. I tried your algorithm on a GIF file and it failed because the pixels in a GIF file are ints, not four-tuples like PNGs. You also do not support the fourth alpha channel of PNG, because your pixel range bounds are only 3-tuples. You might want to consider investigating that further if you want to support multiple image types.






      share|improve this answer























      • @Reinderien That would be true if this were Python 2.x. But in Python 3.x, range is the equivalent of Python 2.x's xrange and returns an iterator. Python 3.x also added an optimization for the .__contains__() method so that it is O(1) time.
        – Graham
        Nov 17 at 12:37












      • It smells, to me. Using a pair of inequalities is simpler, and no less legible.
        – Reinderien
        Nov 18 at 4:10










      • @Reinderien Looking at it again, I think I agree with you. The exclusive upper bound of range can be a bit confusing. And Python does have this neat interval comparison where A <= X and X <= B can be written as A <= X <= B so it's not redundant and synonymous to its math form. The caveat only I have is that using the in range form is more similar to how I think a dedicated ColorRange object should handle a range check, since it's not necessary to expose the object's implementation details (though internally, it could still use comparison operators for the check).
        – Graham
        Nov 18 at 12:27















      up vote
      2
      down vote










      up vote
      2
      down vote









      Improving the general implementation



      Generally we want to avoid appending to a list too many times if we can help it. This is because lists are dynamic arrays, so appends can be unnecessarily expensive (though the dynamic part helps with the efficiency). Instead, when we know the size of the input set (i.e. the number of pixels in the image), we can take advantage of that information. But in this case, that will be unnecessary because Image provides us with some handy helper methods to handle this very situation:



      from PIL import Image

      def picture_reset_pixels(filename, from_color, to_color, target_color):
      img = Image.open(filename)
      pixels = img.load()
      for i in range(img.size[0]):
      for j in range(img.size[1]):
      if all(from_color[k] <= pixels[i, j][k] <= to_color[k] for k in range(3)):
      pixels[i, j] = target_color + (255,)
      return img


      Assigning pixels to img.load() allows us to take advantage of directly editing the image. Since copying the image into another list was unnecessary originally (to clarify, the img variable doesn't affect the file on disk that img originated from, i.e. the file at location filename), editing directly is the best way.



      Edit 2: Looking at it again, I think @Reinderien's point about clarity has merit because range has an exclusive upper bound, which may be confusing because it is asymmetric relative to both of its arguments. I have edited to use the comparison operators <= and >= instead of the range check I mention ahead. The caveat only I have is that using the in range form is more similar to how I think a dedicated ColorRange object (mentioned ahead in the section Going beyond the current form) should handle a range check, since it's not necessary to expose the object's implementation details (though internally, it could still use comparison operators for the check).



      Another general improvement I've taken advantage of in this implementation is using range to check if each of the RGB values of a pixel are between the two pixel boundaries of the input arguments. all() serves the purpose that the and statements in the original version served: it requires that all the pixels compared in the iterator pixels[i, j][k] in range(from_color[k], to_color[k] + 1) for k in range(3) are in the required range for the condition to be true.



      Edit 1: the concern was raised in comments that testing for in range(from_color[k], to_color[k] + 1) would be extremely inefficient. However, this is mistaken and arises from a misunderstanding of how Python 3.x implement the range object. Checking if a number is in a range is a constant time operation. I would recommend reading this answer to understand Python 3.x's range implementation.



      Going beyond the current form



      While I've shown you a way to use your current parameter requirements in a more efficient implementation, I think there are some improvements you can make so picture_reset_pixels has more functionality. I think combining from_color, to_color, and target_color into a single dict parameter color_replacements could make this more versatile (where the key-value pairs would derive something like {(from_color, to_color): target_color}; you could even create a custom ColorRange class to encapsulate the idea of a range of colors).



      There's also another issue I find with your current implementation: it can only support certain image types. I tried your algorithm on a GIF file and it failed because the pixels in a GIF file are ints, not four-tuples like PNGs. You also do not support the fourth alpha channel of PNG, because your pixel range bounds are only 3-tuples. You might want to consider investigating that further if you want to support multiple image types.






      share|improve this answer














      Improving the general implementation



      Generally we want to avoid appending to a list too many times if we can help it. This is because lists are dynamic arrays, so appends can be unnecessarily expensive (though the dynamic part helps with the efficiency). Instead, when we know the size of the input set (i.e. the number of pixels in the image), we can take advantage of that information. But in this case, that will be unnecessary because Image provides us with some handy helper methods to handle this very situation:



      from PIL import Image

      def picture_reset_pixels(filename, from_color, to_color, target_color):
      img = Image.open(filename)
      pixels = img.load()
      for i in range(img.size[0]):
      for j in range(img.size[1]):
      if all(from_color[k] <= pixels[i, j][k] <= to_color[k] for k in range(3)):
      pixels[i, j] = target_color + (255,)
      return img


      Assigning pixels to img.load() allows us to take advantage of directly editing the image. Since copying the image into another list was unnecessary originally (to clarify, the img variable doesn't affect the file on disk that img originated from, i.e. the file at location filename), editing directly is the best way.



      Edit 2: Looking at it again, I think @Reinderien's point about clarity has merit because range has an exclusive upper bound, which may be confusing because it is asymmetric relative to both of its arguments. I have edited to use the comparison operators <= and >= instead of the range check I mention ahead. The caveat only I have is that using the in range form is more similar to how I think a dedicated ColorRange object (mentioned ahead in the section Going beyond the current form) should handle a range check, since it's not necessary to expose the object's implementation details (though internally, it could still use comparison operators for the check).



      Another general improvement I've taken advantage of in this implementation is using range to check if each of the RGB values of a pixel are between the two pixel boundaries of the input arguments. all() serves the purpose that the and statements in the original version served: it requires that all the pixels compared in the iterator pixels[i, j][k] in range(from_color[k], to_color[k] + 1) for k in range(3) are in the required range for the condition to be true.



      Edit 1: the concern was raised in comments that testing for in range(from_color[k], to_color[k] + 1) would be extremely inefficient. However, this is mistaken and arises from a misunderstanding of how Python 3.x implement the range object. Checking if a number is in a range is a constant time operation. I would recommend reading this answer to understand Python 3.x's range implementation.



      Going beyond the current form



      While I've shown you a way to use your current parameter requirements in a more efficient implementation, I think there are some improvements you can make so picture_reset_pixels has more functionality. I think combining from_color, to_color, and target_color into a single dict parameter color_replacements could make this more versatile (where the key-value pairs would derive something like {(from_color, to_color): target_color}; you could even create a custom ColorRange class to encapsulate the idea of a range of colors).



      There's also another issue I find with your current implementation: it can only support certain image types. I tried your algorithm on a GIF file and it failed because the pixels in a GIF file are ints, not four-tuples like PNGs. You also do not support the fourth alpha channel of PNG, because your pixel range bounds are only 3-tuples. You might want to consider investigating that further if you want to support multiple image types.







      share|improve this answer














      share|improve this answer



      share|improve this answer








      edited Nov 18 at 12:26

























      answered Nov 17 at 3:06









      Graham

      538113




      538113












      • @Reinderien That would be true if this were Python 2.x. But in Python 3.x, range is the equivalent of Python 2.x's xrange and returns an iterator. Python 3.x also added an optimization for the .__contains__() method so that it is O(1) time.
        – Graham
        Nov 17 at 12:37












      • It smells, to me. Using a pair of inequalities is simpler, and no less legible.
        – Reinderien
        Nov 18 at 4:10










      • @Reinderien Looking at it again, I think I agree with you. The exclusive upper bound of range can be a bit confusing. And Python does have this neat interval comparison where A <= X and X <= B can be written as A <= X <= B so it's not redundant and synonymous to its math form. The caveat only I have is that using the in range form is more similar to how I think a dedicated ColorRange object should handle a range check, since it's not necessary to expose the object's implementation details (though internally, it could still use comparison operators for the check).
        – Graham
        Nov 18 at 12:27




















      • @Reinderien That would be true if this were Python 2.x. But in Python 3.x, range is the equivalent of Python 2.x's xrange and returns an iterator. Python 3.x also added an optimization for the .__contains__() method so that it is O(1) time.
        – Graham
        Nov 17 at 12:37












      • It smells, to me. Using a pair of inequalities is simpler, and no less legible.
        – Reinderien
        Nov 18 at 4:10










      • @Reinderien Looking at it again, I think I agree with you. The exclusive upper bound of range can be a bit confusing. And Python does have this neat interval comparison where A <= X and X <= B can be written as A <= X <= B so it's not redundant and synonymous to its math form. The caveat only I have is that using the in range form is more similar to how I think a dedicated ColorRange object should handle a range check, since it's not necessary to expose the object's implementation details (though internally, it could still use comparison operators for the check).
        – Graham
        Nov 18 at 12:27


















      @Reinderien That would be true if this were Python 2.x. But in Python 3.x, range is the equivalent of Python 2.x's xrange and returns an iterator. Python 3.x also added an optimization for the .__contains__() method so that it is O(1) time.
      – Graham
      Nov 17 at 12:37






      @Reinderien That would be true if this were Python 2.x. But in Python 3.x, range is the equivalent of Python 2.x's xrange and returns an iterator. Python 3.x also added an optimization for the .__contains__() method so that it is O(1) time.
      – Graham
      Nov 17 at 12:37














      It smells, to me. Using a pair of inequalities is simpler, and no less legible.
      – Reinderien
      Nov 18 at 4:10




      It smells, to me. Using a pair of inequalities is simpler, and no less legible.
      – Reinderien
      Nov 18 at 4:10












      @Reinderien Looking at it again, I think I agree with you. The exclusive upper bound of range can be a bit confusing. And Python does have this neat interval comparison where A <= X and X <= B can be written as A <= X <= B so it's not redundant and synonymous to its math form. The caveat only I have is that using the in range form is more similar to how I think a dedicated ColorRange object should handle a range check, since it's not necessary to expose the object's implementation details (though internally, it could still use comparison operators for the check).
      – Graham
      Nov 18 at 12:27






      @Reinderien Looking at it again, I think I agree with you. The exclusive upper bound of range can be a bit confusing. And Python does have this neat interval comparison where A <= X and X <= B can be written as A <= X <= B so it's not redundant and synonymous to its math form. The caveat only I have is that using the in range form is more similar to how I think a dedicated ColorRange object should handle a range check, since it's not necessary to expose the object's implementation details (though internally, it could still use comparison operators for the check).
      – Graham
      Nov 18 at 12:27














      up vote
      2
      down vote













      Currently you are manipulating the image one pixel at once. Instead you can work on the whole array as one when using the numpy interface:



      from PIL import Image
      import numpy as np

      def read_image(image_path):
      img = Image.open(image_path)
      return np.asarray(img, dtype="int32")

      def write_image(img, image_path):
      img = Image.fromarray(np.asarray(img, dtype="uint8"), "RGBA")
      img.save(image_path)

      def picture_reset_pixels(img, from_color, to_color, target_color):
      mask = (img >= from_color).all(axis=-1) & (img <= to_color).all(axis=-1)
      img[mask] = target_color

      if __name__ == "__main__":
      from_color = (100, 100, 100, 255)
      to_color = (150, 150, 150, 255)
      target_color = (0, 0, 0, 255)
      filename = "test.png"
      img = read_image(filename)
      picture_reset_pixels(img, from_color, to_color, target_color)
      write_image(img, filename)


      Note that I chose not to return the modified image since the function modifies it inplace. If you want to avoid that, just add img = img.copy() as the first line and add return img at the end.



      Also note that this assumes that your image has an alpha channel. If not you need to change "RGBA" to "RGB" and of course have the colours be only three-tuples.






      share|improve this answer



























        up vote
        2
        down vote













        Currently you are manipulating the image one pixel at once. Instead you can work on the whole array as one when using the numpy interface:



        from PIL import Image
        import numpy as np

        def read_image(image_path):
        img = Image.open(image_path)
        return np.asarray(img, dtype="int32")

        def write_image(img, image_path):
        img = Image.fromarray(np.asarray(img, dtype="uint8"), "RGBA")
        img.save(image_path)

        def picture_reset_pixels(img, from_color, to_color, target_color):
        mask = (img >= from_color).all(axis=-1) & (img <= to_color).all(axis=-1)
        img[mask] = target_color

        if __name__ == "__main__":
        from_color = (100, 100, 100, 255)
        to_color = (150, 150, 150, 255)
        target_color = (0, 0, 0, 255)
        filename = "test.png"
        img = read_image(filename)
        picture_reset_pixels(img, from_color, to_color, target_color)
        write_image(img, filename)


        Note that I chose not to return the modified image since the function modifies it inplace. If you want to avoid that, just add img = img.copy() as the first line and add return img at the end.



        Also note that this assumes that your image has an alpha channel. If not you need to change "RGBA" to "RGB" and of course have the colours be only three-tuples.






        share|improve this answer

























          up vote
          2
          down vote










          up vote
          2
          down vote









          Currently you are manipulating the image one pixel at once. Instead you can work on the whole array as one when using the numpy interface:



          from PIL import Image
          import numpy as np

          def read_image(image_path):
          img = Image.open(image_path)
          return np.asarray(img, dtype="int32")

          def write_image(img, image_path):
          img = Image.fromarray(np.asarray(img, dtype="uint8"), "RGBA")
          img.save(image_path)

          def picture_reset_pixels(img, from_color, to_color, target_color):
          mask = (img >= from_color).all(axis=-1) & (img <= to_color).all(axis=-1)
          img[mask] = target_color

          if __name__ == "__main__":
          from_color = (100, 100, 100, 255)
          to_color = (150, 150, 150, 255)
          target_color = (0, 0, 0, 255)
          filename = "test.png"
          img = read_image(filename)
          picture_reset_pixels(img, from_color, to_color, target_color)
          write_image(img, filename)


          Note that I chose not to return the modified image since the function modifies it inplace. If you want to avoid that, just add img = img.copy() as the first line and add return img at the end.



          Also note that this assumes that your image has an alpha channel. If not you need to change "RGBA" to "RGB" and of course have the colours be only three-tuples.






          share|improve this answer














          Currently you are manipulating the image one pixel at once. Instead you can work on the whole array as one when using the numpy interface:



          from PIL import Image
          import numpy as np

          def read_image(image_path):
          img = Image.open(image_path)
          return np.asarray(img, dtype="int32")

          def write_image(img, image_path):
          img = Image.fromarray(np.asarray(img, dtype="uint8"), "RGBA")
          img.save(image_path)

          def picture_reset_pixels(img, from_color, to_color, target_color):
          mask = (img >= from_color).all(axis=-1) & (img <= to_color).all(axis=-1)
          img[mask] = target_color

          if __name__ == "__main__":
          from_color = (100, 100, 100, 255)
          to_color = (150, 150, 150, 255)
          target_color = (0, 0, 0, 255)
          filename = "test.png"
          img = read_image(filename)
          picture_reset_pixels(img, from_color, to_color, target_color)
          write_image(img, filename)


          Note that I chose not to return the modified image since the function modifies it inplace. If you want to avoid that, just add img = img.copy() as the first line and add return img at the end.



          Also note that this assumes that your image has an alpha channel. If not you need to change "RGBA" to "RGB" and of course have the colours be only three-tuples.







          share|improve this answer














          share|improve this answer



          share|improve this answer








          edited Nov 18 at 15:34

























          answered Nov 18 at 12:46









          Graipher

          22.2k53284




          22.2k53284






















              up vote
              1
              down vote













              The code is overall simple and good but could be improved in a few things that would make it a bit shorter and more readable





              Inside the for loop, in the if / else, you are repeating append in both of your cases. You could do instead



              to_add = (round(R), round(G), round(B))
              if condition:
              to_add = target_color
              red_list.append(to_add)




              On the condition, you can also refactor using chained comparison to improve readability, preferably in ascending order. If you break that line, do so aligning conditionals as follow :



              if from_color[0] <= R <= to_color[0] and 
              from_color[1] <= G <= to_color[1] and
              from_color[2] <= B <= to_color[2]:


              Alternatively, but sightly less explicit, you can iterate over i.



              if all((from_color[j] <= i[j] <= to_color[j] for j in range(3)))




              R, G, B initialisation can be made a single line:



              R, G, B = i


              or, if working with a 4 tuples:



              R, G, B, _ = i





              share|improve this answer










              New contributor




              Arthur Havlicek is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
              Check out our Code of Conduct.






















                up vote
                1
                down vote













                The code is overall simple and good but could be improved in a few things that would make it a bit shorter and more readable





                Inside the for loop, in the if / else, you are repeating append in both of your cases. You could do instead



                to_add = (round(R), round(G), round(B))
                if condition:
                to_add = target_color
                red_list.append(to_add)




                On the condition, you can also refactor using chained comparison to improve readability, preferably in ascending order. If you break that line, do so aligning conditionals as follow :



                if from_color[0] <= R <= to_color[0] and 
                from_color[1] <= G <= to_color[1] and
                from_color[2] <= B <= to_color[2]:


                Alternatively, but sightly less explicit, you can iterate over i.



                if all((from_color[j] <= i[j] <= to_color[j] for j in range(3)))




                R, G, B initialisation can be made a single line:



                R, G, B = i


                or, if working with a 4 tuples:



                R, G, B, _ = i





                share|improve this answer










                New contributor




                Arthur Havlicek is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
                Check out our Code of Conduct.




















                  up vote
                  1
                  down vote










                  up vote
                  1
                  down vote









                  The code is overall simple and good but could be improved in a few things that would make it a bit shorter and more readable





                  Inside the for loop, in the if / else, you are repeating append in both of your cases. You could do instead



                  to_add = (round(R), round(G), round(B))
                  if condition:
                  to_add = target_color
                  red_list.append(to_add)




                  On the condition, you can also refactor using chained comparison to improve readability, preferably in ascending order. If you break that line, do so aligning conditionals as follow :



                  if from_color[0] <= R <= to_color[0] and 
                  from_color[1] <= G <= to_color[1] and
                  from_color[2] <= B <= to_color[2]:


                  Alternatively, but sightly less explicit, you can iterate over i.



                  if all((from_color[j] <= i[j] <= to_color[j] for j in range(3)))




                  R, G, B initialisation can be made a single line:



                  R, G, B = i


                  or, if working with a 4 tuples:



                  R, G, B, _ = i





                  share|improve this answer










                  New contributor




                  Arthur Havlicek is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
                  Check out our Code of Conduct.









                  The code is overall simple and good but could be improved in a few things that would make it a bit shorter and more readable





                  Inside the for loop, in the if / else, you are repeating append in both of your cases. You could do instead



                  to_add = (round(R), round(G), round(B))
                  if condition:
                  to_add = target_color
                  red_list.append(to_add)




                  On the condition, you can also refactor using chained comparison to improve readability, preferably in ascending order. If you break that line, do so aligning conditionals as follow :



                  if from_color[0] <= R <= to_color[0] and 
                  from_color[1] <= G <= to_color[1] and
                  from_color[2] <= B <= to_color[2]:


                  Alternatively, but sightly less explicit, you can iterate over i.



                  if all((from_color[j] <= i[j] <= to_color[j] for j in range(3)))




                  R, G, B initialisation can be made a single line:



                  R, G, B = i


                  or, if working with a 4 tuples:



                  R, G, B, _ = i






                  share|improve this answer










                  New contributor




                  Arthur Havlicek is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
                  Check out our Code of Conduct.









                  share|improve this answer



                  share|improve this answer








                  edited Nov 17 at 7:53





















                  New contributor




                  Arthur Havlicek is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
                  Check out our Code of Conduct.









                  answered Nov 17 at 2:29









                  Arthur Havlicek

                  3313




                  3313




                  New contributor




                  Arthur Havlicek is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
                  Check out our Code of Conduct.





                  New contributor





                  Arthur Havlicek is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
                  Check out our Code of Conduct.






                  Arthur Havlicek is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
                  Check out our Code of Conduct.






























                       

                      draft saved


                      draft discarded



















































                       


                      draft saved


                      draft discarded














                      StackExchange.ready(
                      function () {
                      StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f207839%2fchange-all-pixels-that-lie-in-a-given-color-range-to-a-new-color%23new-answer', 'question_page');
                      }
                      );

                      Post as a guest















                      Required, but never shown





















































                      Required, but never shown














                      Required, but never shown












                      Required, but never shown







                      Required, but never shown

































                      Required, but never shown














                      Required, but never shown












                      Required, but never shown







                      Required, but never shown







                      Popular posts from this blog

                      Terni

                      A new problem with tex4ht and tikz

                      Sun Ra