back to index

How to use Color Histograms for Image Retrieval


Chapters

0:0 Intro
1:23 What are Color Histograms?
8:39 How to Built Color Histograms
16:56 Using OpenCV calcHist
20:36 Image Retrieval
27:37 Pros and Cons
30:40 Final Points

Whisper Transcript | Transcript Only Page

00:00:00.000 | Browsing, searching and retrieving images has never been a particularly easy thing to do.
00:00:06.800 | Traditionally, many technologies relied on manually annotated metadata and searching
00:00:16.000 | through that using the more established or better established text retrieval techniques.
00:00:23.840 | This approach works when you have a data set that has high quality annotation, but
00:00:31.120 | this is really not the case for most larger data sets. That means that any large image data set
00:00:40.480 | has to rely on content-based image retrieval. Search with content-based image retrieval relies
00:00:48.240 | on searching through the content of the image rather than relying on any metadata. Content can
00:00:54.960 | be color, shapes or textures, or with some of the latest advances in machine learning, deep learning,
00:01:02.160 | we're actually able to search based more on the meaning of an image, like the human meaning,
00:01:08.000 | rather than even the textures or edges in the image or so on. But today, we're not going to
00:01:15.040 | be looking at that, we're going to be looking at one of the earliest methods for image retrieval,
00:01:20.480 | which is color histograms. Color histograms represent one of the earliest content-based
00:01:28.320 | image retrieval techniques, and they allow us to search through images and retrieve similar
00:01:33.680 | images based on their color profiles. And we can see an example of what we're going to learn about
00:01:39.760 | here. So we have this query image, it's just a city, you can see it's very blue and orange,
00:01:45.280 | and to the right here, we have this, it was basically, this is what the color histogram is.
00:01:51.520 | Now, when we actually build our color histograms and search with them, we are going to convert
00:01:57.920 | them into a single vector, so an image embedding. And with that image embedding, we're going to
00:02:04.400 | return these five images. Now, this is using a very small dataset. We have, I think it is literally
00:02:11.680 | 21 images, it's tiny. So with that in mind, this is returning pretty relevant images when we speak
00:02:20.160 | about the color content of these images. They all seem to have this similar sort of aesthetic,
00:02:26.560 | a lot of blues and oranges. So this is pretty much what we're going to do. And underneath these
00:02:31.600 | images, you can actually see the color profiles as well. So the one to the right is actually just
00:02:37.120 | a duplicate image, it's the same image. But then the other ones, we can see that they all have this
00:02:42.480 | kind of, or some of them have this peak in the red, and then they're pretty flat with the rest
00:02:48.400 | of the colors. So this example demonstrates the core idea of color histograms. That is, we take
00:02:55.040 | an image, translate it into the histograms that you saw, which we then translate into a image
00:03:00.800 | embedding vector, and then use our color profile embedding to retrieve other similar images based
00:03:08.480 | on their color profiles. Now, there are many pros and cons to this technique. I mean, as you'd expect,
00:03:14.320 | it's one of the earliest techniques for image retrieval, but there are a lot of benefits. And
00:03:20.640 | we'll discuss a lot of those as we go through this video and just understand how to actually
00:03:26.960 | implement a color histogram. It'll become quite clear what the pros and cons are, if you haven't
00:03:31.600 | figured a few of those out already. Now, we're going to work through two notebooks. The first
00:03:36.880 | one's just going to show us how we actually build color histograms, so we can actually see step by
00:03:41.120 | step the actual process. And the second one is where we'll implement the search component. Now,
00:03:47.200 | you can find links to both of these notebooks in the description of this video. So if you want to
00:03:53.120 | follow along, go ahead, open those, and you'll be able to go through the code live. So a few things
00:03:59.680 | that we need to install. So just pip install, we have OpenCV library, NumPy, and datasets. Now,
00:04:06.560 | OpenCV is just a public computer vision library. NumPy, I'm sure you probably know what it is.
00:04:12.960 | It's just focused on numerical operations for arrays. And datasets is HuggingFace datasets.
00:04:21.040 | HuggingFace is like an NLP library, and datasets is their way of allowing people to store datasets
00:04:30.320 | and download them super easily. So for us, we are going to use that to get this image set here. Now,
00:04:38.880 | you can use your own images if you like. You don't have to do this one, but this is what I'm going
00:04:43.920 | to go with. And this is, like I said, it's, I think, 21 images. It's really not that many,
00:04:51.120 | but it's all we need for this example. So you can load the dataset. So from datasets, load dataset,
00:04:58.080 | and we have this pinecone image set. If you'd like to see where this dataset is, you can go
00:05:04.640 | to HuggingFace.co/datasets, and then you just type in the dataset name that you see here,
00:05:10.640 | pinecone/image set. And it will take you to where, yeah, this website here, which is where
00:05:16.480 | this dataset is actually hosted. Now inside, actually, let me run this. So inside here,
00:05:25.520 | we have this image bytes feature, and this is just a base 64 encoded representation
00:05:33.840 | of our image bytes. So when we download these, we need to decode them, again, using base 64.
00:05:43.200 | So we do that here. So we're just going to create this processing function,
00:05:47.440 | and we're going to decode everything. I don't think there's really much to go through there,
00:05:54.880 | but from that, we'll get these images. Okay. And we can actually check. So let's just see
00:06:02.320 | how many images there are. 21. Okay. And this should align to appear as always number of rows
00:06:09.760 | 21. Okay. Cool. And we can display the images now with matplotlib. And you see we get this image
00:06:20.560 | with these three dogs, but they're pretty blue. That's because we have loaded them,
00:06:28.480 | or we've decoded them using the OpenCV library. And OpenCV library, it reads images using the
00:06:37.120 | color channels, blue, green, and red in that order. And I don't know why they do that. It's
00:06:43.040 | kind of the opposite way of what most things do, which is red, green, and blue, or RGB.
00:06:48.000 | So what we need to do is actually flip those color channels if we'd like to see the true color
00:06:53.680 | version of this image. So let me show you. So at the moment, this is a shape of that image. So
00:07:03.760 | this image is actually an array. These two here are like the actual pixel values. So you can see
00:07:11.280 | two, five, six, zero at the bottom here. And then you can see one, six, zero, zero over here.
00:07:19.120 | Okay. So it's the Y, Y axis or the height of the image, and this is the width of the image.
00:07:25.600 | And then this three, these are the color channels. So it's the blue, green, and red color channels.
00:07:32.720 | And here are the color values. So blue, green, and red for the very top left pixel,
00:07:45.360 | right up there in the corner. Okay. So we can see these values, by the way, so they go from
00:07:53.200 | zero, which is no color up to 255, which is full color. And we'll have a look at that in a moment.
00:08:00.000 | I'll show you what I mean. So first let's just flip those color channels. So you do NP flip.
00:08:06.720 | We are going to flip the whole array, but we are going to flip it from axis two, which is a color
00:08:14.640 | channel axis. So let's do that. And you can see the shape is exactly the same because all we've
00:08:21.600 | done is flipped it, but these, the pixel values here, so it was blue, green, red is now red,
00:08:29.680 | green, blue. So now we can visualize that and we actually get the actual picture, which is these
00:08:36.560 | three dobs that are very much not blue. So first we're going to go through building a histogram
00:08:42.240 | the slow way, just so we can understand exactly what is actually going on. So let's take a look
00:08:47.760 | at image zero again. So the three dobs, and we're going to have a look at pixel zero. Okay. So we
00:08:54.240 | have those, those values. Again, this is the other way around. So it's blue, green, red.
00:08:59.760 | Now each pixel, like I said, has those blue, green, red activation values from zero, no color
00:09:08.000 | to two, five, five max color. So if you had, let's say these were zero, zero, and zero,
00:09:16.080 | there's no color or there. That means we would get just black. Okay. Cause there's no color.
00:09:23.040 | If we had two, five, five, two, five, five, two, five, five, then we have white, which is just
00:09:29.920 | all the color you can possibly have in one. Okay. And in between them, you have everything.
00:09:37.760 | So here's a few examples. We have blue, green, red, and a few other things. And I'm just going
00:09:43.280 | to plot those in this array here. And I'll show you. Okay. So we can see we have blue, green,
00:09:53.120 | red, and so on and so on. Okay. And you can see all I'm doing is swapping the two, five, fives
00:09:59.680 | for the orange one. We've got like half of the green color in there as well, but that's pretty
00:10:07.760 | much all we're doing. So every single color can be represented by these values. So back to the
00:10:15.760 | previous values, we have that, the top left corner pixel of that picture of three doves,
00:10:23.840 | we have blue, green, and red. So you can think, okay, blue, green, and red, that means it's going
00:10:29.040 | to be sort of a greeny blue color, which is going to be quite neutral because you have all of those
00:10:35.840 | colors kind of in the middle between zero and two, five, five. So we can see that here. So this top
00:10:44.400 | left block here is actually that pixel, right? Because here, I'm just displaying the three by
00:10:54.400 | three pixels in the top left corner of the image. Okay. And this is a true color image, by the way,
00:11:00.720 | that's why it's RGB image. So that's the one we flipped. Now what we are going to want to do when
00:11:09.520 | we're comparing these images is actually, we don't want an array, we want a vector. So I'm going to
00:11:17.680 | reshape this. Okay. And now we have these 409, or four and a little bit million values, which is
00:11:31.840 | just all of the rows in our image, or sorry, yeah, all the rows concatenated together. Okay. That's
00:11:41.440 | what this reshape is doing. Now, if we plot that again, we can still see that those top three
00:11:48.640 | values are the same. Okay. But there's no more rows underneath those pixels anymore. It's just a
00:11:55.520 | single row. Okay. Now, okay, we have this one row, but it's still actually an array because we have
00:12:02.400 | the three color channels. So we need to also extract those out. Okay. And now we literally
00:12:10.240 | just have three vectors that represent our image. And we can visualize each of these with a
00:12:16.000 | histogram. So let's do that. And this is the color profile, the red, green, and blue color profile of
00:12:26.880 | that image of the three dobs. So what we have on the X axis here is the color activation value from
00:12:34.880 | zero up to 255. And then what we have on the Y axis here is the count of the number of pixels
00:12:42.080 | that have that particular value. So, I mean, we can see this is a pretty neutral color. Okay.
00:12:48.160 | Most of the values, and this is probably the case for most images as well. Most of the values are
00:12:53.680 | kind of in the middle point. So it's pretty neutral. You can see that there's a lot of
00:13:00.800 | pixels here that don't have any blue in whatsoever. But beyond that, I don't think there's as much to
00:13:07.280 | note from there. Okay. So let's put everything we've done so far into a single function so that
00:13:14.800 | we can replicate these charts for a few images. So what did we do before? We had our image.
00:13:21.760 | We can also change the number of bins that we use. So you see up here where we've got
00:13:26.800 | an individual bin for every single color activation value. We can push those together
00:13:32.400 | so that we have less bins and we are going to do that later on because it doesn't really affect
00:13:37.200 | the retrieval performance that much unless you go really low. So first we're going to convert
00:13:44.000 | to a true color image. So from BGR to RGB, I'm just going to show the image so we can see what's
00:13:50.160 | actually happening whenever we call this function. Convert it into a vector with the three channels.
00:13:56.480 | And then what we're going to do is, so here I'm breaking the values or dividing the values by the
00:14:07.840 | number of bins and then converting them back to integer values. This is basically just a really
00:14:13.520 | quick way of creating the bins. So if I divided this by two, for example, so I had two bins here
00:14:23.120 | we'd get 128 in this division parameter. And we'd divide everything by 128. So we're kind of
00:14:32.880 | pushing everything together, all these pixel values into discrete categories or bins.
00:14:43.680 | And then we want to get the red, green, blue channels. And then we plot it. So I'm going to
00:14:48.400 | run that and we're just going to try it on a few images. So we'll start with this one.
00:14:52.720 | So we have this image of a city and this is, I think, where you can see color histograms are a
00:15:00.480 | bit more interesting than the last one. So it's a very blue image. And we can see that here,
00:15:06.080 | like it is super blue. And then we look at the histograms and the blue histogram, there's a lot
00:15:11.040 | of high values for blue pixels over here. Now, what you can also do, which I mentioned before,
00:15:19.280 | is use the bins parameter. So we just want, let's say we want 64 bins here.
00:15:24.160 | And you can see that we have these sort of bars now where you can really see those before. That's
00:15:32.240 | because we have 64 values and we can reduce that even more. Let's say we wanted to go really low
00:15:37.760 | and we want to go to two. Okay. And we just get these two bins now. Okay. And you can still see
00:15:45.920 | even from these two bins, it's a very blue image, but of course that's a little bit too much. So
00:15:51.760 | we'll stick with sort of values between 32, 64, because you can actually see what's going on with
00:15:58.400 | that. Now, if you have a look at this image, we can see there's very little blue in this image.
00:16:05.920 | It's a lot of green, a little bit of red as well. If you take a look, we can see that almost all
00:16:15.200 | of the blue values are pushed right to the left, which means there's very few high value blue
00:16:21.360 | pixels in this image. We can do this again here. We see, okay, again, it's very green. So don't
00:16:30.000 | really get those blues and we can keep going through all of that. See a few more. And this
00:16:37.280 | one, this one's also interesting because it's a very color specific image. It's a lot of orange
00:16:43.440 | there. And you can kind of see that because it just got these really big spikes in particular
00:16:49.680 | areas of your histograms. So, well, we can see that represented by the histograms.
00:16:56.240 | Now that's a slow way of building histograms. Like I said before, we've gone through that to
00:17:04.160 | understand exactly what is going on, but it's not the most efficient way of doing it because
00:17:09.200 | there are already functions for creating histograms built into the OpenCV library.
00:17:15.440 | So let's have a look at how we would use that. So we have this CV2, we've imported the CV library
00:17:24.160 | earlier, just here. So just import CV2. And then what we're going to do is we create, we use this
00:17:32.560 | calc_hist function. We pass in an image, we pass in the color channel. We can only do this one color
00:17:40.160 | channel at a time. Whether you want to mask anything, I'll explain that in a minute and so on.
00:17:45.440 | I'll explain the rest in a moment as well. So we'll run that and you see that we get this 64 by 1
00:17:52.640 | shape. And this is actually our histogram. So we don't even need to use, before we're using
00:17:58.000 | matplotlib, the histogram function there. Now we don't even need to use that. We can just plot this
00:18:03.680 | directly. Now there are a few things in here. So we have this calc_hist function. What are all these
00:18:10.560 | values? So we have images, channels, mask_hist_size and ranges. What does that mean exactly?
00:18:18.160 | So the images. So this is a list of CV2 loaded images with the channels blue, green and red.
00:18:27.600 | So if you look up here, that's why I've taken the red histogram from the third channel or position
00:18:34.800 | two and green obviously in one and blue in zero. It can load multiple images. So that's why we have
00:18:42.080 | put that single image inside the square brackets, where is it? Here. And then we have channels. So
00:18:52.480 | this is what channels you want to create your histogram for. I'm wanting to extract one at a
00:18:58.720 | time here. So I've said, okay, I want the red channel or the channel in position two and so on.
00:19:06.400 | Mask is another image or array which just consists of zeros and ones. Now that allows us to mask a
00:19:15.920 | part of the image if we would like to. So imagine you had half of the images zeros, half the image
00:19:23.200 | is ones. It means that you would literally remove half of the image when you add that mask to this.
00:19:30.240 | Okay. Imagine you multiply all the values in your image by the zero ones, the ones that become zero
00:19:37.040 | there, you can't see them anymore because their color activations are zero. Okay. So they just
00:19:41.840 | become black. That's how it works. And then bins is the number of bins that we'd like to add in
00:19:49.440 | there like we did before. And then the histogram range is the range of color values that we would
00:19:55.920 | expect because we're using RGB, we expect zero to 255. So rewrite zero to 256 because the top value
00:20:06.240 | is not inclusive. So that is not included. So actually just goes up to 255. Okay. And let's
00:20:12.240 | have a look at what we get from that. So like I said, we don't need to use the histogram plot from
00:20:17.360 | that plot lib anymore. We can just plot it directly and yeah, we get this. Okay. So I think this is,
00:20:25.040 | is it the last image? Yeah. So it's this image here. You can see that we get the same thing.
00:20:32.080 | So that's the end of the building histograms notebook. Let's have a look at how we actually
00:20:38.080 | create our embeddings with this and then how we actually search using these color histograms.
00:20:45.120 | Okay. So we're now in this search histogram notebook and what we're going to do is create
00:20:50.080 | a function to basically do everything we've done so far. So using the CV2CalcHistForth function,
00:20:57.920 | we're going to use that. And then we're going to actually concatenate the red, green, and blue
00:21:03.520 | channels into a single vector. So you've seen this before. We have red, green, and blue.
00:21:09.840 | Then we concatenate red, green, and blue together along axis zero. And then we reshape. So this is
00:21:16.800 | a minus one. That's just to remove, when you run this, there's always like an extra dimension in
00:21:22.560 | there. So we're just removing that extra dimension. Now, if we run that, we should see that we get
00:21:28.880 | this. So we have a 96 dimension vector. Now, why is it 96? So we have, we just set the default
00:21:37.760 | number of bins to 32 at the top here. So that means we'll get 32 values for the red channel,
00:21:42.560 | for the green value and the blue channel, and then we concatenate them all together. So in the end,
00:21:47.360 | we have all those 32s together, three of them. So we get a vector of dimension 96.
00:21:53.440 | So if you also imagine this, so let me visualize it even. So we run this. You can see here,
00:22:02.480 | we have the vector, the values from up to 32 here from 32 to 64, and from 64 to 96. And these are
00:22:12.800 | red, green, and blue. And we get this. It's the same thing as what we saw before, but we just
00:22:18.640 | separated them all into a vector rather than a array with three color channels. Now let's go
00:22:26.800 | through and we'll just use this loop to do that for all of our images. So we're going to create
00:22:30.880 | all of these image vectors and we can compare vectors with Euclidean distance, although I found
00:22:38.800 | this didn't work very well, at least not compared to cosine similarity, which we calculated like
00:22:45.920 | this. So what we're going to do is use cosine similarity to find the most similar matches for
00:22:54.800 | each image, and we're going to pull this within a function to keep everything clean. During
00:23:01.040 | visualization, we're also going to use the deflect arrays, so the true color arrays.
00:23:06.080 | So I can actually see what they are a little bit better. So I'm going to run that. If I go through
00:23:14.960 | here, all we're doing, we're getting the query vector, which is marked by this index value here.
00:23:21.040 | So for example, those three dogs, they were position zero within our images. So if I wanted
00:23:26.880 | to use that as our query vector, I would just pass zero to this function here, and that will
00:23:33.200 | retrieve the query vector from the image vectors we've already created. Now I'm going to go through
00:23:38.160 | and I'm going to calculate the distance between that query vector and that query image and every
00:23:44.080 | other vector within our image vectors. That also includes the same image. I mean, it's not hard to
00:23:51.920 | remove that from here, but just for the sake of simplicity, I've kept it in. Plus it tells us if
00:23:57.040 | this is actually working, because we should always return that one as the most similar image.
00:24:02.720 | And then we're using this NumPy arg partition function, which is just based on the distances
00:24:12.080 | we calculate, it's going to retrieve those indexes. And then we can use those indexes
00:24:19.280 | to retrieve the images, the actual images themselves that are the most similar. So if
00:24:26.960 | we just run that using the dog image, we see that we returned dog image first, because that's the,
00:24:32.560 | oh, sorry, this is reversed. So this is actually the first most similar image. And
00:24:39.920 | that is the dog image itself. And then we have these other ones. We didn't know what those are.
00:24:46.000 | So let's go ahead. I'm going to write another function. This is not so important, but this
00:24:51.120 | just going to help us visualize these results. So I'm just, there's a lot of NumPy here. We don't
00:24:57.680 | really need to go through this, but you can go through this code if you want to. So run that,
00:25:04.480 | and then I am going to get some results, same as what we saw before. And I just want to visualize
00:25:10.560 | those. So here I'm actually doing it for another image, so we can ignore that bit there. So for
00:25:17.280 | image six. Okay, cool. So we have, this is the image that we're using as our query. And you can
00:25:26.160 | see, this is what I showed you at the start. So it's the city image. And we have these, this is
00:25:32.560 | the color histogram for it. And then these are the images that it's returning. So very similar in
00:25:39.520 | terms of the color scheme for each of these. Let's try another one. Okay. So this one, I mean,
00:25:48.160 | it's pretty obvious what this should return, probably another image. As we can see, like that
00:25:53.280 | one. So we have this yellow background with the dog, and then we have the yellow background with
00:25:57.760 | a cat. Okay. And then there's some other images. These ones are not really that similar, but there
00:26:03.200 | aren't that many images in this dataset. So that's why there aren't any others that are more similar.
00:26:10.640 | And this one, I think is really good because we can really see that these two color histograms
00:26:20.000 | are very aligned, like they're very similar. And if we want to just have a look at, so at
00:26:26.480 | the moment we're using all of the, we're using a default number of bins. I'm not sure what the
00:26:32.720 | value is. Anyway, we can modify the number of bins. So we'll go 96. Let's see what we get.
00:26:40.480 | Okay. And we get this. So now we're modifying the bins. We're still returning the same images. So
00:26:49.040 | there isn't, I think one of these might be slightly different.
00:26:52.240 | Okay. So this one here, actually reducing the number of bins here did damage the quality of
00:27:06.320 | this retrieval. So in this case, we still have that relevant image, but it's, we have these
00:27:12.880 | other images that are actually being placed before it, which is surprising, but it's just
00:27:19.280 | one of the limitations of this technique is it's not particularly robust. Now this is,
00:27:27.920 | this is how we would actually search using these color histograms. Now I think it's probably quite
00:27:34.960 | clear what some limitations of this are. This retrieval technique isn't perfect. And I think
00:27:41.600 | these results highlight some of those drawbacks. So the key limitation here is like, okay, we come
00:27:48.480 | up here and we can see, yeah, we were getting, we're returning this image of a cat with a yellow
00:27:54.560 | background. But what if for your use case, you're, you're not actually looking for the similar color.
00:28:01.680 | You're actually looking more for similar content. So you'd actually want to return, okay, this is a
00:28:07.440 | dog. I want to return another dog. So probably this image over here, in that case, this is a
00:28:13.440 | really bad technique because it's basing everything purely on the color. It's not even looking, it's
00:28:18.880 | not looking at textures and not looking at the edges of an image. It's not looking at what is
00:28:24.480 | inside the image. It's just looking at the color profile of each image. So it's very limited with
00:28:30.800 | that. Now there was further work on color histograms to improve some of these. So for example,
00:28:37.440 | different techniques that consider the texture of images, they consider the edges within the images
00:28:44.960 | and a few other things. But still all of these things, they're not going to get you as far as
00:28:51.920 | some of the more recent deep learning methods that allow you to actually consider what is inside the
00:28:57.440 | image from a very sort of human perspective. But that being said, if you just want to return images
00:29:07.040 | or retrieve images that have a similar sort of aesthetic to a particular image that you're
00:29:11.440 | searching with, say even a similar color profile, like we saw with the earlier results up here,
00:29:18.080 | we are returning pictures that kind of look like the first image, like the query image. So that's
00:29:26.800 | one pro to using this technique. Another one is that it's incredibly easy to implement. We've just
00:29:35.040 | done it. And in reality, we don't even need to go through that sort of slow building a histogram
00:29:40.400 | part. We can just do it super quickly using OpenCV. And another key benefit is the results
00:29:49.120 | are very interpretable. So with a lot of deep learning methods, you have a black box, you put
00:29:57.200 | in some data, you return some results. Why did you get those results? A lot of the time you don't
00:30:02.800 | really know. With this, you know exactly why you're returning a particular result. You can see
00:30:08.640 | that the color profile is very similar to the one, the particular color profile that you're querying
00:30:13.600 | with. So we understand why that's actually happening here. It's not a black box like neural
00:30:18.880 | networks. So with all that in mind, this approach to image embedding and retrieving images is great
00:30:28.560 | if your use case is looking at the aesthetics or the color profile of images. If you want something
00:30:36.400 | more advanced, of course, you're going to want to go for something else. Now that's it for this
00:30:41.600 | video. I hope this sort of introduction to 1D earlier, image embedding techniques and methods
00:30:51.600 | for content-based image retrieval has been interesting. But for now, we're going to leave
00:30:56.480 | it there. So thank you very much for watching and I will see you again in the next one. Bye.