Why does imhist() do this?

31 views (last 30 days)
DGM
DGM on 18 May 2024
Edited: DGM on 24 May 2024
I thought I had asked this once before, but maybe it was a fever dream. It's hard to tell at this point.
IPT imhist() is a convenience tool for creating histograms of grayscale image data. It bins the image data such that the end bins are centered on the ends of the interval implied by the numeric class of the data (e.g. [0 1] for 'double'). It displays the histogram using a stem() plot, with one stem in the center of each histogram bin.
This much might be disagreeable, since the end bins are effectively half-width, but let's accept the choice to align the bin centers to the interval limits instead of aligning the bin edges.
What I can't understand is the colorbar. Beneath the stem plot is a grayscale colorbar showing the progression N gray levels corresponding to the N histogram bins. The problem is twofold:
  1. While the histogram bin centers are aligned to the interval limits (and cannot be changed), the gray segments of the colorbar have their edges aligned with the interval limits -- and they can't be changed either. The two are always misaligned.
  2. The actual gray values used in the colorbar correspond to the upper edge of where the histogram bins would be if they were edge-aligned, but they're not. The first half of the gray segments don't even correspond to the bin they represent. The asymmetry makes plots with small N extra nonsensical.
So I put together a thing for visual emphasis and figured I'd run it here to see if it's just my old version. It's not.
% some inputs
inpict = rand(500);
n = 5;
% imhist() can either give outputs or plot.
% it can't do both, so we have to call it twice.
imhist(inpict,n); hold on
[counts centers] = imhist(inpict,n);
% find the axes since it won't give them to us
hax = findobj(get(gcf,'children'),'type','axes');
% figure out the bin edges from the centers,
% since it won't give us edges either
dx = diff(centers(1:2));
xr = [centers(1)-dx/2 centers(end)+dx/2];
yr = ylim(hax(2));
% create two images:
% top is a smooth sweep from black to white.
% bottom corresponds to the center of each histogram bin.
% the two images should periodically match at each bin center.
smoothramp = repmat(linspace(xr(1),xr(2),100),[1 1 3]);
binramp = repmat(centers.',[1 1 3]);
binramp = imresize(binramp,[1 size(smoothramp,2)],'nearest');
% concatenate the two stripe images
% clamp as necessary for legacy versions
compramp = min(max([binramp; smoothramp],0),1);
% put the composite image behind the stem plot
hi = image(xr,yr,compramp,'parent',hax(2));
uistack(hi,'bottom')
% find the stem plot and make it fat so it's easier to see
hst = findobj(hax(2),'type','stem');
set(hst,'linewidth',3)
% draw a solid gray circle above each stem,
% such that the circle color is taken directly from the stem position
for k = 1:n
hp = plot(hax(2),centers(k),yr(2)*0.67,'.');
set(hp,'color',[1 1 1]*centers(k));
set(hp,'markersize',60);
end
So we have a stem plot, two images, and circular plot markers that all agree, but the color bar is off doing its own thing. The gray level in the first two colorbar segments isn't even in the corresponding histogram bin.
Apparently this is the way imhist() has done it for at least the last 15 years, so is there actually a reason for it, or is it just one of those forever-bugs?
I'm in the middle of trying to write my way around MIMT's usage of imhist(), and I'm inclined to just take a step back and make a complete replacement instead.
  3 Comments
DGM
DGM on 19 May 2024
Well I suppose I'm setting myself up for disappointment when I ask "why" things were written a certain way.
I was silently expecting that someone might point out a reason that had to do with indexed color support. I hadn't really thought it fully through, but maybe that would explain the motivation to center histogram bins (e.g. on integer values), and it might explain the reason to compromise on colorbar segment alignment (i.e. so that each segment is fully visible). The wrong (1:N)/N gray levels could then just be a mistake made in making scaled color images work with a tool that was originally made with focus on indexed color.
I hadn't actually tested that, since I had no intention of using imhist() for indexed color. So I did, and found out that it's broken in different ways.
% this image has 11 colored blobs on a black background (12 colors)
[Xuint CT] = imread('indexedblobs.png');
imshow(Xuint,CT)
% since Xuint is a uint-class index array, the indices are zero-based.
% that should be okay, since imhist() explicitly supports uint8 indexed images:
% "An input indexed image can be uint8, uint16, single, double, or logical."
unique(Xuint).'
ans = 1x12
0 1 2 3 4 5 6 7 8 9 10 11
<mw-icon class=""></mw-icon>
<mw-icon class=""></mw-icon>
% ... but it doesn't actually. CT(1,:) is black. There is no white in CT,
% and CT(11,:) isn't even visible.
imhist(Xuint,CT)
% what happens if it's a float-class index array?
% the MATLAB convention for float indexed images is to use one-based indexing
Xfloat = double(Xuint) + 1;
% well that works
imhist(Xfloat,CT)
% so does that mean that imhist() is breaking convention by
% treating integer-class index arrays as being 1-based?
Xmalformed = Xuint + 1;
% no, it's just an OBOE in generating the color stripe.
imhist(Xmalformed,CT)
Warning: Input has out-of-range values.
Also, the idea that the colorbar/histogram misalignment was a compromise for indexed color representations turns out to be wrong, since now the colorbar is aligned.
It might seem that I'm wasting my time to continue this on the forum, but someone else is likely to find the same issue(s), and it's easier to write it all out and demonstrate it here as a primary reference in the bug report. It also proves that it has nothing to do with my installation or version.
I've already started getting rid of imhist() in the MIMT passthrough tool, so I'll just make the replacement however I want. That way I can always know who to blame for bugs. :)
DGM
DGM on 20 May 2024
Bug report submitted; replacement almost done.
FWIW, the inconsistency in the colorbar alignment between scaled and indexed images is actually a consequence of the bug with its mishandling of integer indexed images. So it's actually trying to generate the colorbar misaligned as usual (but offset by one), then it ends up overwriting the xlim of the colorbar axes anyway, because I guess either someone forgot that the xlim properties were linked, or they added that link and never checked to see what it broke.
It really was the better choice to just start over.

Sign in to comment.

Answers (2)

Alex Taylor
Alex Taylor on 22 May 2024
Edited: Alex Taylor on 22 May 2024
The function imhist is one of the older functions in the Image Processing Toolbox and we on the development team are aware that imhist has a variety of questionable design choices including but not limited to:
  • Definition of histogram based on bin centers vs. bin edges.
  • Use of number of left hand side arguments to infer whether user wants plotting vs. programatic histogram binning (e.g. histcounts). This results in the behavior noted above where two calls to imhist were required to plot and compute the histogram counts and the plotting can't use the count information.
For these items, I would recommend in the near term using the modern histogram display and computation functions in MATLAB: histogram and histcounts. They can be used to address both of the behaviors above.
The main value add at this point of imhist is its visualization of image gray levels in the context of a histogram, which is not something trivial to reproduce with histogram and histcounts today. That and I suppose indexed image support for people who work with indexed images. There is also the matter of default binning behavior as a function of input datatype, so a few things.
I consider the two behaviors you noted in the misalignment of the colorbar with respect to the histogram bin center definition (and stem placement) and certainly the indexed image behavior colorbar behavior of displaying colors not present in the data in the colorbar to be a bug and will track them as such.
I appreciate the detailed discussion here and in particular your histogram code as a means of communicating your desired behavior. I can't provide any short term fixes or workarounds for the moment with these issues but we do take your bug report seriously and intend to fix both the bugs and some of the UX choices described above in a future release of the product.
  1 Comment
DGM
DGM on 24 May 2024
Considering the date in the file is 1992, the history of the behavior of imhist() is almost a curiosity in itself at this point. Then again, I don't need any more rabbit holes to go down at the moment. I'm glad I'm not losing my marbles anyway.

Sign in to comment.


DGM
DGM on 20 May 2024
Edited: DGM on 24 May 2024
We'll see what shakes on the bug report.
I was already writing around imhist() in the MIMT tool, expecting to come back and make the fallback/passthrough paths congruent. Since all I had to do was gut the passthrough portion, I really didn't have much left to do other than improve my expectations for everything else and throw together some webdocs. I even added the axes links and event listeners like I suppose I should have in the first place (but never do).
So allow me to introduce the now-misleadingly-named imhistFB(). This used to be a passthrough to IPT imhist() with a partial fallback implementation for use when IPT was unavailable to MIMT. It doesn't use any IPT stuff anymore, but I'm not going to change the name (yet).
Given either explicit or implicit interval limits, MIMT imhistFB() can either center-align or edge-align the end bins. In both cases, the colorbar is aligned to the histogram bins, and the default gray levels correspond to the center of the relevant bin.
%% demonstrate bin alignment
clc; clf; clearvars
inpict = rand(500);
n = 5;
subplot(2,1,1)
imhistFB(inpict,n,'style','bar','binalignment','center'); % imhist()-style
subplot(2,1,2)
imhistFB(inpict,n,'style','bar','binalignment','edge');
As the above example shows, imhistFB() can do various plot styles. While stem() plots might be nice for large N, for small N, bars are a lot easier to read. A filled patch plot might look nice with a custom colormap.
inpict = imread('cameraman.tif');
n = 32;
subplot(3,1,1)
imhistFB(inpict,n,'style','stem'); % imhist()-style
subplot(3,1,2)
imhistFB(inpict,n,'style','bar');
subplot(3,1,3)
imhistFB(inpict,n,'style','patch');
You don't have to fiddle around with trying to find the axes and data range to reset the ylim if you want to see the peaks. If you did want to get the handles or the data in order to do something more complex, imhistFB() can optionally return counts, bin centers, bin edges, and both axes handles -- even when plotting, so the baloney with having to run imhist() twice or trying to externally calculate edges from centers isn't a thing anymore. The fact that that can be avoided makes complicated manipulation a lot easier to make consistent across versions. If you write code around using imhist() to draw a histogram, and then you rely on querying the properties of the underlying stem() object as I've seen recommended, your code will break in older versions.
%% demonstrate yscale styles
clc; clf; clearvars
inpict = imread('cameraman.tif');
n = 256;
subplot(2,1,1)
imhistFB(inpict,n,'yscale','tight'); % imhist()-style
subplot(2,1,2)
imhistFB(inpict,n,'yscale','full');
Lastly, I never intended to support indexed-color images, but it turns out that if you write things to be flexible, you can make it work just fine. I could probably add a 'indexed' flag to automatically preselect an appropriate interval for the 'range' parameter. Maybe I'll do that later.
While imhistFB() does return handles, I'm just not going to bother here, since imhist() won't anyway.
%% imhistFB() was never meant to support indexed-color images,
% but it still can do a better job than imhist() can.
clc; clf; clearvars
% this is a uint8 indexed-color image with 11 colored
% blobs on a black background (12 colors)
[Xuint CT] = imread('indexedblobs.png');
% this is a float-class copy of the same image
% offset by one, as is convention.
Xfloat = double(Xuint)+1;
% EXAMPLE 1: imhist()
% due to a bug, imhist() does not actually handle
% integer-class indexed images correctly, so it needs to be float.
% while colorbar and histogram bins were misaligned for scaled images,
% they will be aligned for no apparent reason if a colortable is provided.
% the end bins appear as half-width as expected.
subplot(4,2,1)
imhist(Xuint,CT) % uint-class (wrong colorbar)
subplot(4,2,2)
imhist(Xfloat,CT) % float-class
% EXAMPLE 2: imhistFB() with 'center' alignment similar to imhist()
% use the 'colortable' parameter to display an integer-class indexed image
% obviously, the only thing you'd need to do for a properly-offset
% float-class indexed image is to shift 'range' by 1
subplot(4,2,3)
nct = size(CT,1);
imhistFB(Xuint,nct,'colortable',CT,'range',[0 nct-1]) % uint-class
subplot(4,2,4)
imhistFB(Xfloat,nct,'colortable',CT,'range',[1 nct]) % float-class
% EXAMPLE 3: imhistFB() with 'edge' alignment
% do the same thing, but the colorbar segments all appear with uniform width.
subplot(4,2,5)
imhistFB(Xuint,nct,'colortable',CT,'range',[0 nct]-0.5,'binalignment','edge') % uint-class
subplot(4,2,6)
imhistFB(Xfloat,nct,'colortable',CT,'range',[0 nct]+0.5,'binalignment','edge') % float-class
% EXAMPLE 4: imhistFB() with 'center' alignment and setting xlim
% example 3 could also have been done with the same inputs as example 2
% and subsequent manipulation of the x-limits of both axes.
% this is what would need to be done for IPT imhist(),
% though remember that imhist()'s axes don't have linked xticks.
subplot(4,2,7)
imhistFB(Xuint,nct,'colortable',CT,'range',[0 nct-1]) % uint-class
subplot(4,2,8)
imhistFB(Xfloat,nct,'colortable',CT,'range',[1 nct]) % float-class
hax = findobj(gcf,'type','axes');
set(hax(4),'xlim',[0 nct]-0.5) % uint
set(hax(2),'xlim',[0 nct]+0.5) % float
% make sure we're seeing all the integer-value ticks for this short CT
% note that here and in example 4, i'm only updating the histogram axes
% imhistFB() axes (both the stripe and histogram) are linked on 'xlim' and 'xtick'
set(hax(4:4:12),'xtick',0:nct-1) % for any example using uint
set(hax(2:4:12),'xtick',1:nct) % for any example using float
% the imhist() axes are only linked on 'xlim', but not 'xtick',
% so we need to update both axes explicitly
set(hax(15:16),'xtick',0:nct-1) % for any example using uint
set(hax(13:14),'xtick',1:nct) % for any example using float
% set up some labels
title('uint-class','parent',hax(16))
title('float-class','parent',hax(14))
ylabel('Example 1','parent',hax(16),'fontweight','bold')
ylabel('Example 2','parent',hax(12),'fontweight','bold')
ylabel('Example 3','parent',hax(8),'fontweight','bold')
ylabel('Example 4','parent',hax(4),'fontweight','bold')
I've tested back to R2009b, so it should be usable for most people, even without IPT. Since I have now cursed myself by posting, I await discovering new bugs.
Using imhistFB() to represent uint12/int12-scale image data stored in a wider integer class, compared to the difficulty of other methods:
The motivation to add features to imhistFB() was to support the needs of the color histogram tool I was making. The ability to use custom interval limits and colormaps was important. Here's an example:
  2 Comments
Josh
Josh on 21 May 2024
Edited: Josh on 21 May 2024
respect on all the exploration, insight, and knowledge sharing...seems like you uncovered a fairly significant bug that's been present for 15+ years?
DGM
DGM on 21 May 2024
I haven't bothered to suss out the details, since debugging imhist() wouldn't solve the usability problem -- but I'm still not sure if it's more than one bug, or if it's a set of seemingly different symptoms caused by a common cause. The oldest version I have is R2009b, and imhist() is old. If there was an edit that broke intended behavior, it happened a long time ago. On the other side, I can't really even be sure whether aspects of the behavior were intended or not.
That said, it's not really terribly surprising that a bug like this could hide for a long time. I imagine 90% of imhist() usage either doesn't involve the plot generation, or it uses the default 256 bins, at which point, the misalignment isn't visually noticeable.

Sign in to comment.

Categories

Find more on Data Distribution Plots in Help Center and File Exchange

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!