0

I'm looking for a way to efficiently find the band that has the maximum value in a large multi-band image.

For example if we had an image that looked like

band 1   band 2   band 3
[1 2 3]  [3 4 5]  [0 1 2]
[3 2 1]  [1 2 3]  [1 0 1]
[4 5 6]  [6 7 5]  [0 0 7]

I'd like to make something like

bandFind = A.bandmax()

Where bandFind would look like

band 1   band 2   band 3
[0 0 0]  [1 1 1]  [0 0 0]
[1 1 0]  [0 0 1]  [0 0 0]
[0 0 0]  [1 1 0]  [0 0 1]

Alternatively if I can get an index image I can pretty easily convert it to what I want.

I wrote a function in python that iterates through the bands, accumulating the max value and the index in a buffer, and the converts it to the kind of map shown above, but it doesn't seem very performant on large images.

  def pickMax(inImg):
    imBandSelect = pyvips.Image.black(inImg.width, inImg.height, bands=1)
    imBandMax = pyvips.Image.black(inImg.width, inImg.height, bands=1)
    for bandIndex, band in enumerate(inImg.bandsplit()):
      runningSelect = band > imBandMax
      imBandMax = runningSelect.ifthenelse(band, imBandMax)
      imBandSelect = runningSelect.ifthenelse(bandIndex, imBandSelect)

    bandList = [ (imBandSelect == bi) / 255.0 for bi in range(inImg.bands) ]
    return bandList[0].bandjoin(bandList[1:])

Update:

Thanks to @jcupitt I tried this version of the code using bandrank:

  def pickMaxUchar(inImg):
    short = (inImg.cast(pyvips.enums.BandFormat.USHORT)) << 8
    index = pyvips.Image.black(short.width, short.height, bands=1).bandjoin_const([b for b in range(1, inImg.bands)]).cast(pyvips.enums.BandFormat.UCHAR)
    combo = short | index
    list = combo.bandsplit()
    ranked = list[0].bandrank(list[1:], index=inImg.bands-1)
    rankedindex = (ranked & 255).cast(pyvips.enums.BandFormat.UCHAR)
    bandList = [ (rankedindex == bi) / 255.0 for bi in range(inImg.bands) ]
    return bandList[0].bandjoin(bandList[1:])

This now assumes the input is a char, where in my original function the input was float. Maybe there's a way to do this without re-casting the data but as I've implemented it there's zero speedup relative to my "naive" code above, so perhaps this just can't be accelerated any further.

matth
  • 563
  • 7
  • 22

1 Answers1

1

You can use bandrank for this. It's like a rank filter, so just ask for the final index.

It returns a one-band image with the selected value in each pixel and you need the index, so the trick is to hide the index in the low bits of the pixel values, and pull it out again from the result.

Something like:

#!/usr/bin/env python

import sys
import pyvips

image = pyvips.Image.new_from_file(sys.argv[1], access='sequential')

# a band index image, ie. each band contains its own index
index = (pyvips.Image.black(image.width, image.height) + [0, 1, 2]).cast("uchar")

# put that into the bottom two bits of the image
image = (image << 2) | index

# now use bandrank to find the max at each pixel
# index=2 means the max of a three band image
bands = image.bandsplit()
mx = bands[0].bandrank(bands[1:], index=2)

# and extract the index value
index = mx & 3

index.write_to_file(sys.argv[2])

With a 30,000 x 30,000 pixel jpg I see:

$ time ./rank.py ~/pics/st-francis.jpg x.v

real    0m5.296s
user    0m51.233s
sys 0m1.501s
jcupitt
  • 10,213
  • 2
  • 23
  • 39
  • Thanks for the really quick answer! That doesn't really do what I want though. I don't want the max of the band, I want a map where for each pixel I know which band held the maximum value. – matth Aug 29 '23 at 19:06
  • Interesting - I looked at `bandrank` -- I'm not quite sure I see how that would work. Again I really appreciate how response you are with all this! I'll see if I can figure it out – matth Aug 29 '23 at 19:14
  • Ok I get it. I'll update with a little code demo when I get it working – matth Aug 29 '23 at 19:44
  • I added some example code. It takes about 7s for a 30,000 x 30,000 pixel jpg on this PC, fwiw. – jcupitt Aug 30 '23 at 05:21