当前位置: 首页 > news >正文

16bit转8bit的常见方法(图像归一化)

文章目录

  • 16-bit转8-bit的常用方法
    • 一、数据类型转换:image.astype(np.uint8) —— 若数值 x 超出 0-255 范围,则取模运算。如:x = 600 % 256 = 88
    • 二、截断函数:np.clip().astype(np.uint8) —— 若数值 x 超出 0-255 范围,则>255=255,<0=0。如:x = 600 => 255
    • 三、线性缩放
      • (1)固定比例(×) —— 适用于像素值均匀分布的高动态范围图像,对高对比度图像不够灵活,损失信息。
      • (2)动态比例(√) —— 适用于对比度较低的图像,能提升细节。
    • 四、图像归一化:[0,1 或 [0,255]
      • (1)手动归一化(√)
      • (2)OpenCV归一化:cv2.normalize()
      • (3)百分比缩放:np.percentile()(√)
    • 五、Fiji - Auto - B&C:min + max

将16-bit图像转换为8-bit图像是图像处理中的常见操作,目的是将原图像的像素值压缩至0-255范围。这种转换有助于减少存储空间、适应常见显示设备,并压缩动态范围。虽然16-bit图像能提供更丰富的细节和灰度级别,但其占用的内存和存储空间较大。通过转换为8-bit图像,可以有效降低数据量,满足日常应用需求,同时保持足够的视觉效果。

  • 数据位数(bit) —— 描述了可以用多少个二进制位来表示一个数值。
    • 16位:范围[0,216-1],即[0,65535]
    • 8位: 范围[0,28-1],即[0,255]
  • 图像概念:
    • 16-bit 图像:每个像素的值由 16位 二进制数表示,范围为 [0, 65535]。这种高位深的图像可以表示更多的灰度级别,适用于医学影像、遥感图像等需要高动态范围和精细色彩细节的应用。
    • 8-bit 图像:每个像素的值由 8位 二进制数表示,范围为 [0, 255]。8位图像的动态范围较小,但在许多普通的显示设备和图像处理应用中已经足够。8位图像广泛用于一般的灰度图像、彩色图像等。
  • 图像转换:由于 8-bit 图像 的像素范围(0-255)比 16-bit 图像 的像素范围(0-65535)要小得多,因此,16-bit 图像转换为 8-bit 图像时需要做 缩放 或 归一化 操作,将其像素值映射到适当的范围。

16-bit转8-bit的常用方法

在这里插入图片描述


"""
输入图像:
[a, b]
[c, d]

扩展后的图像:
[a, 0, b, 0]
[0, 0, 0, 0]
[c, 0, d, 0]
[0, 0, 0, 0]

卷积核(3x3):
[w, x, y]
[z, p, q]
[r, s, t]
"""
import numpy as np
import cv2

def normalize(x, pmin=2, pmax=99.8, axis=None, clip=True, eps=1e-20, dtype=np.float32):
    """Percentile-based image normalization."""
    mi = np.percentile(x, pmin, axis=axis, keepdims=True)
    ma = np.percentile(x, pmax, axis=axis, keepdims=True)
    return normalize_mi_ma(x, mi, ma, clip=clip, eps=eps, dtype=dtype)


def normalize_mi_ma(x, mi, ma, clip=False, eps=1e-20, dtype=np.float32):
    if dtype is not None:
        x = x.astype(dtype, copy=False)
        mi = dtype(mi) if np.isscalar(mi) else mi.astype(dtype, copy=False)
        ma = dtype(ma) if np.isscalar(ma) else ma.astype(dtype, copy=False)
        eps = dtype(eps)

    try:
        import numexpr
        x = numexpr.evaluate("(x - mi) / ( ma - mi + eps )")
    except ImportError:
        x = (x - mi) / (ma - mi + eps)

    if clip:
        x = np.clip(x, 0, 1)

    return x

###############################################################################################
import tifffile
image_16bit = tifffile.imread(r"561result-1-part-1.tif")
image_16bit = image_16bit[0:100, 0:100]

# image_16bit = np.array([[260, 50, 600], [400, 500, 300]], dtype=np.uint16)
###############################################################################################
"""(1)数据类型转换(强转)"""
image_8bit_astype = image_16bit.astype(np.uint8)

"""(2)截断函数"""
image_8bit_clip = np.clip(image_16bit, 0, 255).astype(np.uint8)
###############################################################################################
min_value = np.min(image_16bit)  # 获取图像的最小值
max_value = np.max(image_16bit)  # 获取图像的最大值

"""(3.1)线性缩放-(固定)比例"""
scale_factor = 255.0 / 65535.0  # (固定)缩放比例
image_8bit_scaled_G = (image_16bit * scale_factor).astype(np.uint8)

"""(3.2)线性缩放-(动态)比例"""
if max_value != min_value:
    scale = 255.0 / (max_value - min_value)  # (动态)缩放比例
else:
    scale = 1  # 或者其他特殊处理
image_8bit_scaled_D = np.clip(image_16bit * scale + 0.5, 0, 255).astype(np.uint8)
###############################################################################################
"""(4.1)手动归一化"""
image_8bit_manual = ((image_16bit - min_value) / (max_value - min_value) * 255).astype(np.uint8)

"""(4.2)cv2.normalize归一化"""
image_8bit_cv2 = cv2.normalize(image_16bit, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)

"""(4.3)percentile归一化函数"""
data_nor = normalize(image_16bit, pmin=2, pmax=99.8, clip=True, dtype=np.float32)
image_8bit_percentile = np.clip(data_nor * 255, 0, 255).astype(np.uint8)

print("image_16bit:", image_16bit)  # [[260  50 600] [400 500 300]]
print("image_8bit_astype:", image_8bit_astype)  # [[  4  50  88] [144 244  44]]
print("image_8bit_clip:", image_8bit_clip)  # [[255  50 255] [255 255 255]]
print("image_8bit_scaled_G:", image_8bit_scaled_G)  # [[1 0 2] [1 1 1]]
print("image_8bit_scaled_D:", image_8bit_scaled_D)  # [[121  23 255] [185 232 139]]
print("image_8bit_manual:", image_8bit_manual)  # [[ 97   0 255] [162 208 115]]
print("image_8bit_cv2:", image_8bit_cv2)  # [[ 97   0 255] [162 209 116]]
print("image_8bit_percentile:", image_8bit_percentile)  # [[0.35795453 0.         1.        ] [0.62310606 0.8125     0.43371212]]


import matplotlib.pyplot as plt
plt.figure(figsize=(10, 10))
plt.subplot(2, 4, 1), plt.imshow(image_16bit, cmap="gray"), plt.title("image_16bit"), plt.axis("off")
plt.subplot(2, 4, 2), plt.imshow(image_8bit_astype, cmap="gray"), plt.title("image_8bit_astype"), plt.axis("off")
plt.subplot(2, 4, 3), plt.imshow(image_8bit_clip, cmap="gray"), plt.title("image_8bit_clip"), plt.axis("off")
plt.subplot(2, 4, 4), plt.imshow(image_8bit_scaled_G, cmap="gray"), plt.title("image_8bit_scaled_G"), plt.axis("off")
plt.subplot(2, 4, 5), plt.imshow(image_8bit_scaled_D, cmap="gray"), plt.title("image_8bit_scaled_D"), plt.axis("off")
plt.subplot(2, 4, 6), plt.imshow(image_8bit_manual, cmap="gray"), plt.title("image_8bit_manual"), plt.axis("off")
plt.subplot(2, 4, 7), plt.imshow(image_8bit_cv2, cmap="gray"), plt.title("image_8bit_cv2"), plt.axis("off")
plt.subplot(2, 4, 8), plt.imshow(image_8bit_percentile, cmap="gray"), plt.title("image_8bit_percentile"), plt.axis("off")
plt.show()

# import napari
# viewer = napari.Viewer()  # 创建napari视图
# viewer.add_image(image_16bit, name="image_16bit")
# viewer.add_image(image_8bit_astype, name="image_8bit_astype")
# viewer.add_image(image_8bit_clip, name="image_8bit_clip")
# viewer.add_image(image_8bit_scaled_G, name="image_8bit_scaled_G")
# viewer.add_image(image_8bit_scaled_D, name="image_8bit_scaled_D")
# viewer.add_image(image_8bit_manual, name="image_8bit_manual")
# viewer.add_image(image_8bit_cv2, name="image_8bit_cv2")
# viewer.grid.enabled = not viewer.grid.enabled  # 切换到网格模式
# napari.run()  # 启动 napari 事件循环,使得窗口保持打开并可交互。

一、数据类型转换:image.astype(np.uint8) —— 若数值 x 超出 0-255 范围,则取模运算。如:x = 600 % 256 = 88

import numpy as np

image_16bit = np.array([[260, 50, 600], [400, 500, 300]], dtype=np.uint16)
image_8bit = image_16bit.astype(np.uint8)
print("image_8bit:", image_8bit)  # [[  4  50  88] [144 244  44]]

二、截断函数:np.clip().astype(np.uint8) —— 若数值 x 超出 0-255 范围,则>255=255,<0=0。如:x = 600 => 255

import numpy as np

image_16bit = np.array([[260, 50, 600], [400, 500, 300]], dtype=np.uint16)
image_8bit = np.clip(image_16bit, 0, 255).astype(np.uint8)
print("image_8bit:", image_8bit)  # [[255  50 255] [255 255 255]]

三、线性缩放

(1)固定比例(×) —— 适用于像素值均匀分布的高动态范围图像,对高对比度图像不够灵活,损失信息。

import numpy as np

image_16bit = np.array([[260, 50, 600], [400, 500, 300]]).astype(np.uint16)
scale_factor = 255.0 / 65535.0  # (固定)缩放比例
image_8bit = (image_16bit * scale_factor * 255).astype(np.uint8)
print("image_8bit:", image_8bit)  # [[  1  49  83] [140 240  41]]

(2)动态比例(√) —— 适用于对比度较低的图像,能提升细节。

import numpy as np

image_16bit = np.array([[260, 50, 600], [400, 500, 300]]).astype(np.uint16)
scale_factor = 255.0 / (np.max(image_16bit) - np.min(image_16bit))  # (动态)缩放比例
image_8bit = np.clip(image_16bit * scale_factor + 0.5, 0, 255).astype(np.uint8)
print("image_8bit:", image_8bit)  # [[121  23 255] [185 232 139]]

四、图像归一化:[0,1 或 [0,255]

(1)手动归一化(√)

import numpy as np

image_16bit = np.array([[260, 50, 600], [400, 500, 300]]).astype(np.uint16)
image_8bit = np.clip((image_16bit - np.min(image_16bit)) / (np.max(image_16bit) - np.min(image_16bit)) * 255, 0, 255).astype(np.uint8)
print("image_8bit:", image_8bit)  # [[ 97   0 255] [162 209 116]]

(2)OpenCV归一化:cv2.normalize()

import numpy as np
import cv2

image_16bit = np.array([[260, 50, 600], [400, 500, 300]]).astype(np.uint16)
image_8bit = cv2.normalize(image_16bit, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
print("image_8bit:", image_8bit)  # [[ 97   0 255] [162 209 116]]

(3)百分比缩放:np.percentile()(√)

百分比缩放(Percentile Stretching):通过选择图像的百分比值,通常选择图像中的前 99% 或 95% 像素值作为实际的范围,然后将这些值线性映射到0-255范围。这种方法可以避免因极端像素值(如噪声或过曝光区域)影响结果。

import numpy as np


def normalize(x, pmin=2, pmax=99.8, axis=None, clip=True, eps=1e-20, dtype=np.float32):
    """Percentile-based image normalization."""
    mi = np.percentile(x, pmin, axis=axis, keepdims=True)
    ma = np.percentile(x, pmax, axis=axis, keepdims=True)
    return normalize_mi_ma(x, mi, ma, clip=clip, eps=eps, dtype=dtype)


def normalize_mi_ma(x, mi, ma, clip=False, eps=1e-20, dtype=np.float32):
    if dtype is not None:
        x = x.astype(dtype, copy=False)
        mi = dtype(mi) if np.isscalar(mi) else mi.astype(dtype, copy=False)
        ma = dtype(ma) if np.isscalar(ma) else ma.astype(dtype, copy=False)
        eps = dtype(eps)

    try:
        import numexpr
        x = numexpr.evaluate("(x - mi) / ( ma - mi + eps )")
    except ImportError:
        x = (x - mi) / (ma - mi + eps)

    if clip:
        x = np.clip(x, 0, 1)

    return x


if __name__ == '__main__':
    data = np.array([[260, 50, 600], [400, 500, 300]]).astype(np.uint16)
    data_nor = normalize(data, pmin=2, pmax=99.8, clip=True, dtype=np.float32)
    data_nor8bit = np.clip(data_nor * 255, 0, 255).astype(np.uint8)
    print(data_nor)  # [[0.35795453 0.         1.        ] [0.62310606 0.8125     0.43371212]] 
    print(data_nor8bit)  # [[ 91   0 255] [158 207 110]] 

五、Fiji - Auto - B&C:min + max

(1)提取 TIFF 图像页的元数据标签:min + max
(2)Fiji - JAVA:ImageProcessor convertToByteProcessor()
(3)Fiji - Auto - B&C(对比度函数)

import numpy as np
import tifffile


def auto(data, num_bins=256, times=1):
    """
    函数功能: Fiji - Brightness/Contrast
    输入参数: data         ———— 2D or 3D图像
            num_bins      ———— bin数量
            times         ———— auto次数
    备注:若为3D图像,需要提取具有一定意义的帧图像为参考帧。
    """
    print(f"Fiji - Brightness/Contrast - Auto(times={times}).")

    if data.ndim == 3:
        # 在3D图像中(背景为黑,前景为白),计算每一帧图像的像素值总和,并以sum最大为参考帧。
        effective_frame = []
        for index, frame in enumerate(data):
            effective_frame.append(np.sum(frame))
            # # (部分)全黑的帧图像会出现(部分)像素值>0,但远小于50。
            # if frame.max() > 50:
            #     data = frame
            #     break
        frame_max_value = max(effective_frame)
        frame_max_index = effective_frame.index(frame_max_value)
        data = data[frame_max_index]
        print(f"frame_max_index={frame_max_index}")

    limit = data.size / 10
    auto_threshold = 0
    min_value, max_value = 0, 0
    for i in range(times):
        if auto_threshold < 10:
            auto_threshold = 5000
        else:
            auto_threshold /= 2
        data_min, data_max = data.min(), data.max()
        threshold = data.size / auto_threshold
        hist, bins = np.histogram(data, bins=num_bins)
        hist[hist > limit] = 0
        bin_size = (data_max - data_min) / num_bins
        x = np.where(hist > threshold)[0]
        if len(x) <= 1:
            return data_min, data_max

        min_bin, max_bin = x[0], x[-1]
        min_value = data_min + min_bin * bin_size
        max_value = data_min + max_bin * bin_size
    return min_value, max_value


def convert_short_to_byte(pixels16, min_val, max_val, do_scaling=True):
    """(复现函数)BIRDS-JAVA: ImageProcessor convertToByteProcessor() """
    # 使用掩码0xffff来提取16位像素值的低16位(低16位的像素值保留,高于16位的部分置0)。0xffff是一个16位的二进制数,所有位均为1。
    # pixels8 = (pixels16 & 0xffff).astype(np.int16)  # 执行按位与操作。
    # pixels8_min_before, pixels8_min_before = np.min(pixels8), np.max(pixels8)

    if do_scaling:
        """
        任务:image = image - value
        已知:image为uint16数组, value为正数且大于image的最小值(min_image)
        输出:最小值的结果(整数溢出)=(65535 + min_image) - value
        """
        pixels_result = np.where(pixels16 >= min_val, pixels16 - min_val, 0).astype(np.uint16)  # 对于每个像素值,若像素值大于等于min_val,则减去min_val,否则置为0。
        scale = 256.0 / (max_val - min_val + 1)  # 计算缩放比例
        pixels8 = np.clip(pixels_result * scale + 0.5, 0, 255).astype(np.uint8)  # 截断函数 + 数据类型转换
    else:
        pixels8 = np.clip(pixels16, 0, 255).astype(np.uint8)  # 使用NumPy的矢量化操作,避免循环
    return pixels8


def tiff_pages_tags_min_max(tiff_path):
    """"提取 TIFF 图像页的元数据标签:min + max"""
    import re
    TIFF = tifffile.TiffFile(tiff_path)  # 打开 TIFF 文件
    tiff_pages_tags = TIFF.pages[0].tags  # 获取第一页的元数据标签

    min_value, max_value = 0, 0
    # 检查是否存在 ImageDescription 标签
    if 'ImageDescription' in tiff_pages_tags:
        image_description_tag = tiff_pages_tags['ImageDescription']
        image_description = image_description_tag.value
        # 使用正则表达式匹配 min 和 max 属性的值,直接匹配 µ 字符
        match = re.search(r'min\s*=\s*([0-9.]+)\s*max\s*=\s*([0-9.]+)', image_description)
        if match:
            min_value = round(float(match.group(1)))
            max_value = round(float(match.group(2)))
            if min_value == max_value:  # 属性存在但未赋值: min=0.0 + max=0.0
                stack_image = tifffile.imread(tiff_path)
                min_value, max_value = auto(stack_image)
                min_value = round(min_value)
                max_value = round(max_value)
            print(f"image: min_gray_value={min_value}, max_gray_value={max_value}")
        else:
            print(f"No match found in image_description: {image_description}")
            stack_image = tifffile.imread(tiff_path)
            min_value, max_value = auto(stack_image)
            min_value = round(min_value)
            max_value = round(max_value)
            print(f"image: min_gray_value={min_value}, max_gray_value={max_value}")
    return min_value, max_value


if __name__ == "__main__":
    # (1)生成1000个介于0到65535之间的随机整数
    image_16bit = np.random.randint(0, 65536, 1000).reshape(10, 10, 10).astype(np.uint16)
    save_path = "image.tif"
    tifffile.imwrite(save_path, image_16bit)  # 保存图像为TIFF

    # (2)提取 TIFF 图像页的元数据标签:min + max
    tag_min_value, tag_max_value = tiff_pages_tags_min_max(save_path)
    print(f"min ={tag_min_value}, max ={tag_max_value}")

    # (3)16bit转8bit
    load_image = tifffile.imread(save_path)  # 加载图像数据
    image_8bit = convert_short_to_byte(load_image, tag_min_value, tag_max_value, do_scaling=True)
    print(f"16位图像 ={image_16bit.shape}, min ={np.min(image_16bit)}, max ={np.max(image_16bit)}")
    print(f" 8位图像 ={image_8bit.shape}, min ={np.min(image_8bit)}, max ={np.max(image_8bit)}")

    # (4)可视化
    import napari
    viewer = napari.Viewer()  # 创建napari视图
    viewer.add_image(image_16bit, name="image_16bit", colormap='gray')  # 添加图像(指定红色)
    viewer.add_image(image_8bit, name="image_8bit", colormap='gray')  # 添加图像(指定红色)
    viewer.dims.ndisplay = 3  # 切换到n维显示模式
    viewer.grid.enabled = not viewer.grid.enabled  # 切换网格模式
    napari.run()  # 显示napari图形界面
"""
No match found in image_description: {"shape": [10, 10, 10]}
min =650, max =64795

16位图像 =(10, 10, 10), min =8, max =65519
 8位图像 =(10, 10, 10), min =0, max =255
"""

相关文章:

  • 深入理解浏览器的 Cookie:全面解析与实践指南
  • UNITY 屏幕UI自适应
  • Ubuntu下解压ZIP压缩文件出现中文乱码问题的综合解决方案
  • python提升图片清晰度
  • 【学习】对抗训练-WGAN
  • RTT添加一个RTC时钟驱动,以DS1307为例
  • C语言超详细指针知识(三)
  • Java设计模式实战:装饰模式在星巴克咖啡系统中的应用
  • 【JavaScript】二十、电梯导航栏的实现
  • C++之二叉搜索树
  • arcgis几何与游标(1)
  • 【NLP】 自然语言处理笔记
  • Spring-AI-alibaba 结构化输出
  • 技术视界 | 人形机器人运动控制框架详解:解锁智能机器人的“灵动”密码
  • 如何使用maxscale实现mysql读写分离
  • Java 反序列化之 XStream 反序列化
  • [c语言日记]轮转数组算法(力扣189)
  • JavaScript中的运算符与语句:深入理解编程的基础构建块
  • CentOS下,Xftp中文文件名乱码的处理方式
  • 【第42节】windows双机调试环境搭建和SEH原理剖析
  • 生态环境部谈拿手持式仪器到海边测辐射:不能测量水中放射性核素含量
  • 驯服象牙塔:美国政府对大学的战争是一场善恶对抗吗
  • 2024年我国数字阅读用户规模达6.7亿
  • 裁员15%、撤销132个机构,美国务院将全面重组
  • 东方富海陈玮: 什么样的创业者能让天使投资人愿意下注
  • 打造“朋友圈”,“淘书乐”为旧书找“新朋友”