省型旧形国電の残影を求めて

戦前型旧形国電および鉄道と変褪色フィルム写真を中心とした写真補正編集の話題を扱います。他のサイトでは得られない、筆者独自開発の写真補正ツールや補正技法についても情報提供しています。写真補正技法への質問はコメント欄へどうぞ

ImageJ / Python プログラミング Tips: 画像 bit 深度・最大値変換サンプルプログラム

 先日、ImageJ上における 32bit 画像の扱いについて記事を書きましたが、それを基に、画像の bit 深度を変換したり、32bit画像の場合、最大値を変更したりするサンプルプログラムを書いてみました。意図通りちゃんと変換してくれます。

 

---------------

# Converting image bit depth and max value
# (c) 2023 Ohnishi, Yasuo
from ij import IJ, ImagePlus
from ij import WindowManager as WM
from ij.io import DirectoryChooser, OpenDialog
from ij.plugin import ChannelSplitter, Duplicator
from ij.process import ImageConverter
from ij.gui import GenericDialog
from loci.plugins import BF
import os
import sys
from org.python.core import codecs
codecs.setDefaultEncoding('utf-8')

def selectChannel(): 
    # dialog of selecting target channel for mask upper & lower threshold
    gd = GenericDialog("Channel Selection")
#    scolorCh = ["Lightness", "Luminosity", "Red","Green","Blue"]
    scolorCh = ["Red","Green","Blue"]
    gd.addChoice("Select Target Channel", scolorCh, scolorCh[0])
    gd.showDialog()

    colorCh =  gd.getNextChoice()
    if gd.wasCanceled():
        colorCh =  "None"

    return str(colorCh)
#--
def inputParm(Mode, L_Max): 
    # dialog of input parameters
    gd = GenericDialog("Input Parameters")
    gd.addMessage("Original Image Bitdepth: " + Mode + "  Max Lightness: " + str(L_Max))
    imageFormats = ["8-bit","16-bit","32-bit"]
    gd.addChoice("Select Convert Format", imageFormats, imageFormats[0])
    maxValues = ["1.0","256","65535","16777215"]
    gd.addChoice("Select Max Value (only available in 32bit)", maxValues, maxValues[2])

    gd.showDialog()

    imageFormat =  gd.getNextChoice()
    maxValue =  gd.getNextChoice()
    if gd.wasCanceled():
        imageFormat =  "None"
        maxValue =  "None"
    if Mode == "8-bit" and imageFormat == "16-bit":
        multiplyFactor = 256
    elif Mode == "8-bit" and imageFormat == "32-bit":
        multiplyFactor = float(maxValue) / 255
    elif Mode == "16-bit" and imageFormat == "32-bit":
        multiplyFactor = float(maxValue) / 65535
    else:
        multiplyFactor = 1.0

    return str(imageFormat), multiplyFactor

#--
window = WM.getActiveWindow()
if not window:
    od = OpenDialog("Choose a file")
    folder = od.getDirectory()
    filename = od.getFileName()
    if filename:
        path = folder + filename
        IJ.log(path)
        IJ.log(folder)
        IJ.log(filename)
# Always Open file with Bio-format Plugin
        imps = BF.openImagePlus(folder + filename) 
        imp0 = imps[0] #imp0: Original Image 
        imp0.show()
    else:
        IJ.showMessage("File is not selected.")
else: # target image is already open
    imp0 = IJ.getImage()
    imagename = imp0.getTitle()
    try:
        folder = imp0.getOriginalFileInfo().directory
    except:
        folder = IJ.getDirectory("Output_directory")
# get number of channels (Mono or RGB?)
stackSize = imp0.getStackSize() # stackSize=1: Mono / 3: RGB
# get bitdepth
bitdepth = imp0.getBitDepth()
if bitdepth == 8:
    Mode = "8-bit"
elif bitdepth ==16:
    Mode = "16-bit"
elif bitdepth ==32:
    Mode = "32-bit"
# get Max value
L_Max = imp0.getProcessor().getMax()

if stackSize > 1:
    colorch = selectChannel()
    impsCh = ChannelSplitter.split(imp0) #Channel Splitting of imp0
    if colorch == "Red":
        imp1 = impsCh[0]
    elif colorch == "Green":
        imp1 = impsCh[1]
    elif colorch == "Blue":
        imp1 = impsCh[2]
    elif colorch == "None":
        sys.exit()
else:
    imp1 = Duplicator().run(imp0)
imp1.show()
imp0.hide()
imageFormat, multiplyFactor = inputParm(Mode, L_Max)
if imageFormat == "8-bit":
    ImageConverter(imp1).convertToGray8()
elif imageFormat == "16-bit":
    ImageConverter(imp1).convertToGray16()
elif imageFormat == "32-bit":
    ImageConverter(imp1).convertToGray32()
imp1.getProcessor().multiply(multiplyFactor)
if L_Max > 65536:
    L_Max = 16777215.999
elif L_Max > 256:
    L_Max = 65535.999
elif L_Max > 1.0:
    L_Max = 255.999
else:
    L_Max = 1.0
maxDispRange = L_Max * multiplyFactor
imp1.setDisplayRange(0.0, maxDispRange)
imp1.getProcessor().setLut(None)
imp1.updateAndDraw()
imp0.close()

---------------

 def selectChannnel でチャンネル選択ダイアログを、def inputParm でパラメータ入力ダイアログを定義しています。

 このプログラムを ImageJ 上で走らせるには、File → New → Script で新しいスクリプトエディタを開いて、そこに上の内容を貼り付けて走らせてください。言語は当然 Python です。

 動かすと、まずファイル選択ダイアログが出ますので、読み込むファイルを選択します。次に、チャンネル指定ダイアログが出るので、

チャンネル指定ダイアログ

 Red, Green, Blue のうちどれかを選びます。なお、モノチャンネルのグレースケール画像では以上のダイアログは表示されません。

 チャンネルを選択すると、下記のように変換指定ダイアログが出ます。変換指定ダイアログの上の部分に、現在の画像のフォーマット (bit深度及び値の最大値) が表示されます。表示が緑になっているのは、Green チャンネルを選択したので、緑のLUTがかかっているためです。

変換指定ダイアログ

 そしてその下の部分で、変換するフォーマット (8-bit, 16-bit, 32-bit のいずれか)、および変換する最大値 (1.0, 256, 65535, 16777215 のいずれか) を選択します。但し、最大値の変換は、32-bit を選んだときのみに有効で、8-bit を選んだ場合は 255, 16-bit の場合は 65535 に最大値は自動的に決定されます。

 これを選んでOKボタンを押すと、指定されたフォーマットに画像が変換されます。プログラム的には、imageFormat という変数に変換先の bit 深度が指定されていすので、それをもとに、ImageConverter を使って bit 深度を変更します。さらに multiplyFactor という変数に、ユーザの指定に従って、明度値を変更する係数を計算して取得し、画像の明度値に multiplyFactor を掛けて変更します。それと同時に、bit 深度や明度の最大値を変換したときに、真っ黒になったり真っ白になるのを防ぐために、setDisplayRange メソッドを使って表示が変わらないようにしました。そのため、変換しても特に見た目は変わりません (但し、その際にLUTを外すので、チャンネルの色は消えます)。しかし以下のようにヒストグラムを取ってみると、値が変換されているのが分かります。

変換後の画像のヒストグラムを取って値が変換されているのを確認

 前回、setDisplayRange メソッドで、表示する明度範囲を調節するのではないかと書きましたが、その通りでした。これで値を指定することで、どんな値に変換しようが見た目を変わらずに維持することができます。

--------------

[関連記事]

yasuo-ssi.hatenablog.com

 

yasuo-ssi.hatenablog.com

yasuo-ssi.hatenablog.com