英文:
How to save a 1-channel array as an image with proper color limit
问题
I am trying to save a sequence of 1-channel array into grayscale images. These images are supposed to be masks for segmentation.
The issue I am facing is inconsistent colors for the same pixel value in different images:
When I add additional circles to this image, then the saved image has a different set of colors for previously present circles.
Ideally, I want them to have consistent colors throughout my entire dataset as each circle represents a unique class of label.
So my question is how can I save these grayscale images with consistent color code?
Added code snippet to generate the above image
import numpy as np
import cv2
import matplotlib.image as mpimg
image = np.zeros(shape=[256, 256], dtype=np.uint8)
cv2.circle(image, center=(50, 50), radius=10, color=(2, 2), thickness= -1)
cv2.circle(image, center=(100, 100), radius=10, color=(3, 3), thickness= -1)
cv2.circle(image, center=(150, 150), radius=10, color=(4, 4), thickness= -1)
cv2.circle(image, center=(75, 200), radius=10, color=(5, 5), thickness= -1)
cv2.circle(image, center=(200, 89), radius=10, color=(6, 6), thickness= -1)
# additional circles (they are part of Image #2)
cv2.circle(image, center=(21, 230), radius=5, color=(7, 7), thickness= -1)
cv2.circle(image, center=(149, 250), radius=5, color=(12, 12), thickness= -1)
mpimg.imsave('image.jpg',image)
英文:
I am trying to save a sequence of 1-channel array into grayscale images. These images are supposed to be masks for segmentation.
The issue I am facing is inconsistent colors for same pixel value in different images:
When I add additional circles to this image, then saved image has different set of colors for previous present circles.
Ideally I want them to have consistent colors throughout my entire dataset as each circle represent a unique class of label.
So my question is how can I save these grayscale images with consistent color code?
Added code snippet to generate above image
import numpy as np
import cv2
import matplotlib.image as mpimg
image = np.zeros(shape=[256, 256], dtype=np.uint8)
cv2.circle(image, center=(50, 50), radius=10, color=(2, 2), thickness= -1)
cv2.circle(image, center=(100, 100), radius=10, color=(3, 3), thickness= -1)
cv2.circle(image, center=(150, 150), radius=10, color=(4, 4), thickness= -1)
cv2.circle(image, center=(75, 200), radius=10, color=(5, 5), thickness= -1)
cv2.circle(image, center=(200, 89), radius=10, color=(6, 6), thickness= -1)
# additional circles (they are part of Image #2)
cv2.circle(image, center=(21, 230), radius=5, color=(7, 7), thickness= -1)
cv2.circle(image, center=(149, 250), radius=5, color=(12, 12), thickness= -1)
mpimg.imsave('image.jpg',image)
答案1
得分: 3
以下是翻译好的部分:
首先一些提示...
-
你不能将JPEG格式用于分类数据。JPEG是一种用于照片的有损格式,允许更改像素以使其在重新阅读时“看起来相似”,但实际上会有所不同。请使用无损PNG格式用于类数据。请参阅答案末尾的演示。
-
每个像素位置都用单个数字表示你的类别,这意味着你需要单通道图像,也就是灰度图像。这意味着在绘制圆圈时,应该将颜色指定为单个数字,而不是2个数字的元组。
-
没有必要引入不必要的依赖项 - 你已经在使用OpenCV,所以读取和写入图像也应使用它。
这意味着你的代码应该更像这样:
#!/usr/bin/env python3
import numpy as np
import cv2 as cv
image = np.zeros(shape=[256, 256], dtype=np.uint8)
cv.circle(image, center=(50, 50), radius=10, color=2, thickness= -1)
cv.circle(image, center=(100, 100), radius=10, color=3, thickness= -1)
cv.circle(image, center=(150, 150), radius=10, color=4, thickness= -1)
cv.circle(image, center=(75, 200), radius=10, color=5, thickness= -1)
cv.circle(image, center=(200, 89), radius=10, color=6, thickness= -1)
# additional circles (they are part of Image #2)
cv.circle(image, center=(21, 230), radius=5, color=7, thickness= -1)
cv.circle(image, center=(149, 250), radius=5, color=12, thickness= -1)
cv.imwrite('classes.png', image)
这将以非常低的对比度保存你的图像 - 所有像素将在0..255的可能范围内的0..12范围内,因此它们将很暗,但仍然包含你需要的信息。
要进行可视化,你需要进行对比度拉伸或将数据归一化到整个范围。你可以使用ImageMagick来实现:
magick classes.png -normalize classes-norm.png
如果你不想使用ImageMagick进行归一化,可以在OpenCV中进行等效操作:
normalizedImg = cv.normalize(img, None, 0, 255, cv.NORM_MINMAX)
如果你想将自己的颜色映射应用于数据,可以使用LUT "Lookup Table" 来实现,如下所示:
#!/usr/bin/env python3
import sys
import cv2 as cv
import numpy as np
if __name__ == "__main__":
# 检查提供的文件名
if len(sys.argv) != 2:
sys.exit('Usage: argv[0] filename')
# 读取灰度类图像
image = cv.imread(sys.argv[1], cv.IMREAD_GRAYSCALE)
# 创建具有3个(RGB)条目的256个条目的调色板,最初看起来像灰度
pal = np.fromfunction(lambda i,j: i, (256,3), dtype=np.uint8)
# 覆盖从灰色到新颜色的任何所需颜色
pal[0] = [0,0,0] # 0 映射到黑色
pal[1] = [255,0,0] # 1 映射到红色
pal[2] = [0,255,0] # 2 映射到绿色
pal[3] = [0,0,255] # 3 映射到蓝色
pal[4] = [255,255,0] # 4 映射到黄色
pal[5] = [0,255,255] # 5 映射到青色
pal[6] = [255,0,255] # 6 映射到洋红
pal[7] = [128,0,0] # 7 映射到深红
pal[8] = [0,128,0] # 8 映射到深绿
pal[9] = [0,0,128] # 9 映射到深蓝
# 转换LUT为OpenCV的BGR顺序
pal = pal[:,::-1]
# 在LUT中查找每个像素
result = pal[image]
# 保存结果
cv.imwrite('result.png', result)
请注意,与默认LUT中设置的暗灰色(12,12,12)不匹配,因此与类12对应的圆圈不显示出来。
请注意,上述颜色化的图像现在是3通道的RGB图像,与单通道灰度classes.png
不同。
请注意,如果超过255个类别并且使用np.uint16
等,你将需要重新审视代码。
以下是关于JPEG对数据的影响的小演示。首先,我们生成一个100x100的图像,并填充随机噪声,即随机颜色。然后将颜色数量减少到64,并首先保存结果为PNG,然后再次保存相同的数据为JPEG:
magick -size 100x100 xc: +noise random -colors 64 -write a.png a.jpg
PNG
JPEG
它们看起来相同,对吧?现在让我们数一下每个图像中的唯一颜色数:
magick a.png a.jpg -format "%f: %k\n" info:
结果
a.png: 64
a.jpg: 9682 # 哎呀!看看JPEG做了什么。
<details>
<summary>英文:</summary>
First some tips...
* You cannot use JPEG format for classification data. JPEG is a **lossy** format for **photographs** and is allowed to change your pixels for something that *"looks pretty similar"* but will be different when you re-read it. Use lossless PNG format for class data. See demonstration at end of answer.
* Your classes are each represented by a single number at each pixel location - that means you need a single-channel image, a.k.a. greyscale. That means you should specify colours as a single number when drawing circles, rather than as a tuple of 2 numbers.
* There's no point introducing unnecessary dependencies - you are already using **OpenCV** so read and write your images with that.
These mean your code should look more like this:
#!/usr/bin/env python3
import numpy as np
import cv2 as cv
image = np.zeros(shape=[256, 256], dtype=np.uint8)
cv.circle(image, center=(50, 50), radius=10, color=2, thickness= -1)
cv.circle(image, center=(100, 100), radius=10, color=3, thickness= -1)
cv.circle(image, center=(150, 150), radius=10, color=4, thickness= -1)
cv.circle(image, center=(75, 200), radius=10, color=5, thickness= -1)
cv.circle(image, center=(200, 89), radius=10, color=6, thickness= -1)
# additional circles (they are part of Image #2)
cv.circle(image, center=(21, 230), radius=5, color=7, thickness= -1)
cv.circle(image, center=(149, 250), radius=5, color=12, thickness= -1)
cv.imwrite('classes.png', image)
This will save your images with very low contrast - all pixels will be in the range 0..12 out of a possible range of 0..255, so they will be dark, but still contain the information you need.
[![enter image description here][1]][1]
---
In order to visualise, you will need to contrast-stretch, or normalise your data to the full range. You can do that with **ImageMagick** very simply:
magick classes.png -normalize classes-norm.png
[![enter image description here][2]][2]
If you don't want to use **ImageMagick** to normalize, the equivalent operation in **OpenCV** is:
normalizedImg = cv.normalize(img, None, 0, 255, cv.NORM_MINMAX)
---
If you want to apply your own colormap to the data, you can do that using a LUT *"Lookup Table"* like this:
#!/usr/bin/env python3
import sys
import cv2 as cv
import numpy as np
if __name__ == "__main__":
# Check filename supplied
if len(sys.argv) != 2:
sys.exit('Usage: argv[0] filename')
# Read greyscale class image
image = cv.imread(sys.argv[1], cv.IMREAD_GRAYSCALE)
# Create a 256 entry palette each with 3 (RGB) entries, initially looking like greyscale
pal = np.fromfunction(lambda i,j: i, (256,3), dtype=np.uint8)
# Overwrite any desired colours from grey to new colour
pal[0] = [0,0,0] # 0 maps to black
pal[1] = [255,0,0] # 1 maps to red
pal[2] = [0,255,0] # 2 maps to green
pal[3] = [0,0,255] # 3 maps to blue
pal[4] = [255,255,0] # 4 maps to yellow
pal[5] = [0,255,255] # 5 maps to cyan
pal[6] = [255,0,255] # 6 maps to magenta
pal[7] = [128,0,0] # 7 maps to dark red
pal[8] = [0,128,0] # 8 maps to dark green
pal[9] = [0,0,128] # 9 maps to dark blue
# Convert LUT to BGR order for OpenCV
pal = pal[:,::-1]
# Look up each pixel in the LUT
result = pal[image]
# Save result
cv.imwrite('result.png', result)
[![enter image description here][3]][3]
Obviously you could equally store the LUT in a JSON or a CSV and load it from there rather than putting it in the Python code.
---
Note that the circle corresponding to class 12 doesn't show up because I was too lazy to remap that away from the dark grey I set in the default LUT (12,12,12).
Note that the image as colourised above is now a 3-channel RGB image, in contrast to the single channel, greyscale `classes.png`.
---
Note that you will need to revisit the code if you exceed 255 classes and use `np.uint16` and so on.
---
Here's a little demonstration of what JPEG does to your data. First we generate a 100x100 image and fill it with random noise, i.e. random colours. Then reduce the number of colours to 64 and save the result first as a PNG, and then save the same data again as JPEG:
magick -size 100x100 xc: +noise random -colors 64 -write a.png a.jpg
**PNG**
[![enter image description here][4]][4]
**JPEG**
[![enter image description here][5]][5]
They look the same, don't they. Now let's count the number of unique colours in each:
magick a.png a.jpg -format "%f: %k\n" info:
**Result**
a.png: 64
a.jpg: 9682 # OOPS! Look what JPEG did.
Why does JPEG do that? Because the file is 60% smaller but the images look the same:
-rw-r--r--@ 1 mark staff 46256 9 Jun 11:04 a.png
-rw-r--r--@ 1 mark staff 18991 9 Jun 11:04 a.jpg
[1]: https://i.stack.imgur.com/qZDoq.png
[2]: https://i.stack.imgur.com/lyNxm.png
[3]: https://i.stack.imgur.com/6SwBT.png
[4]: https://i.stack.imgur.com/WeB77.png
[5]: https://i.stack.imgur.com/Hxz79.jpg
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论