vesuvius.finalize_outputs --mode multiclass writes a volume documented as [softmax_c0...softmax_cN, argmax], but the uint8 conversion rescales that array by a factor that depends on the contents of each chunk, so neither the probabilities nor the class indices survive.
finalize_outputs.py, in apply_finalization:
softmax = exp_logits / np.sum(exp_logits, axis=0, keepdims=True)
output_data = np.concatenate([softmax, argmax], axis=0) # [0,1] concatenated with [0, C-1]
...
min_val = output_np.min()
max_val = output_np.max()
if min_val < max_val:
output_np = ((output_np - min_val) / (max_val - min_val) * 255).astype(np.uint8)
max_val is the largest class index present in that chunk, so the scale factor changes from chunk to chunk.
Reproduction on a 4-class volume, calling the function directly:
chunk A (contains classes 0 and 3):
argmax values 0, 3 -> written as 0, 255
softmax probability 1.000 -> written as 85
chunk B (contains classes 0 and 1):
argmax values 0, 1 -> written as 0, 255
softmax probability 1.000 -> written as 255
So class 1 and class 3 are both stored as 255, a probability of 1.0 is stored as 85 in one chunk and 255 in another, and the encoding is discontinuous at every chunk boundary. The same path is used by the fused blend_and_finalize (blending.py). With --threshold in multiclass the output is argmax only, and that too is rescaled per chunk, so class indices are not recoverable there either.
A fix would quantize the two parts on fixed, chunk-independent scales, e.g.
softmax_u8 = np.clip(softmax * 255.0, 0, 255).astype(np.uint8) # same scale as the binary path
argmax_u8 = argmax.astype(np.uint8) # raw class index
output_np = np.concatenate([softmax_u8, argmax_u8], axis=0)
which keeps probabilities comparable across chunks and leaves class indices readable, at the cost of changing the byte values existing multiclass artifacts carry. Happy to send that as a PR if the encoding change is acceptable — or a variant that keeps a single scale but documents it, if downstream consumers already depend on the current bytes.
vesuvius.finalize_outputs --mode multiclasswrites a volume documented as[softmax_c0...softmax_cN, argmax], but the uint8 conversion rescales that array by a factor that depends on the contents of each chunk, so neither the probabilities nor the class indices survive.finalize_outputs.py, inapply_finalization:max_valis the largest class index present in that chunk, so the scale factor changes from chunk to chunk.Reproduction on a 4-class volume, calling the function directly:
So class 1 and class 3 are both stored as 255, a probability of 1.0 is stored as 85 in one chunk and 255 in another, and the encoding is discontinuous at every chunk boundary. The same path is used by the fused
blend_and_finalize(blending.py). With--thresholdin multiclass the output is argmax only, and that too is rescaled per chunk, so class indices are not recoverable there either.A fix would quantize the two parts on fixed, chunk-independent scales, e.g.
which keeps probabilities comparable across chunks and leaves class indices readable, at the cost of changing the byte values existing multiclass artifacts carry. Happy to send that as a PR if the encoding change is acceptable — or a variant that keeps a single scale but documents it, if downstream consumers already depend on the current bytes.