I have a 1920x1440 jpg image. It has been binarized. I want to sample the values at an nxm grid of points say 50 points x 40 points. What is the most computationally efficient way to do this? Thanks.
2 Answers
If you want an n
xm
subset of pixels of a 1920x1440 image, you can try doing Downsample[img, {Floor[1920/n], Floor[1440/m]}]
—but this doesn't guarantee an n
x m
grid will result, due to the floor function being used.
Let's assume you have a list of images imgs
which are all known to be the same dimensions.
There are two ways: ImageResize[#, {n, m}, Resampling -> "NearestLeft"] & /@ imgs
, or a custom function. If we go the custom function route, we can exploit the fact that we have the same dimensions for each image:
ImageSample[imgs_List, {x_Integer, y_Integer}] :=
With[{img1 = First[imgs]},
With[{i = ImageDimensions[img1],
type = ImageType[img1],
info = Sequence @@ FilterRules[Normal@First@Information[img1], Options[Image]]},
With[{xparts = Floor@Rescale[Range[x], {1, x}, {1, First[i]}],
yparts = Floor@Rescale[Range[y], {1, y}, {1, Last[i]}]},
Image[#, type, info] & /@
((ImageData[#, type] & /@ imgs)[[All, yparts, xparts]])
]
]
]
Note that this function is not "safe" and doesn't check that all image dimensions are the same (or even that the list is made of all images). If you want to do that, you can change imgs_List
to imgs : {___Image}
and append /; Equal @@ (ImageDimensions /@ imgs)
to the definition.
This is guaranteed to get you an x
xy
-sized grid of pixels, if that's essential for you.
Surprisingly, this can be an order of magnitude faster than the built-in ImageResize
, depending on the parameters x
,y
, if you really need it. It's best to time it yourself and see how it goes, though. The speed difference might also be negligible, depending on the dimensions and the number of frames. It can also be much slower sometimes, so watch out!
Note what we're doing with the ranges, as well: we're scaling them so that the first and last pixels of the sampling grid are right in the corners. You can fiddle with the last argument of Rescale
, which tells you the range to rescale it to, if you want. You can also replace Floor
with Round
or Ceiling
. Note that ImageResize
chooses a slightly different convention: it apparently tries to sample from the center of rectangles that are i[[1]]/x
wide by i[[2]]/y
high. (Check out ImageResize[Image[Array[{#1, #2, 0} &, {100, 100}], "Byte"], {5, 5}, Resampling -> "NearestLeft"]
to see it sampling from the middle of 20x20 squares). This could be replicated here with a small adjustment to the formula.
It also converts all the ImageData
back to Image
s. If you don't need that, and are happy with the array, simply remove info = ...
and Image[#, info] & /@
.
The above assumes you're trying to get exactly an n
xm
grid of points, and that being evenly spaced in "real" space is more important than being evenly spaced in pixel values. It also assumes you want to avoid sampling close to one edge but not the opposite one; one downside of using Downsample
or simply ImageData[img][[ ;; ;; m, ;; ;; n]]
is that you'll introduce edge artifacts or not get quite an n
by m
grid, but they are far faster. There are a couple of ways to modify them to be better:
To deal with edge artifacts:
Approximate solution: Calculate an appropriate offset for the sampling, so that the sampled pixels are approximately "centered"
More exact solution: Restrict the allowed downsampling spacing: for sampling right up to the edges, only use divisors of
(effective dimension) - 1
; for sampling in "pixel block centers", only use divisors of(effective dimension)
. For this, we can crop our image dimensions slightly from(dimension)
to(effective dimension)
, which might have better divisors. (what if(dimension)
is prime?)
There are also other ways we could sample the image: randomly, from a uniform distribution on locations; randomly within each pixel block; according to different rigid patterns; etc.
I'll come back to this to see if I have anything explicit to add! :)
Hope this helps, let me know if you have any questions!
-
1$\begingroup$ Yes, this is all very helpful and informational and will help a lot during the investigation. The part about getting exactly n x m samples is also not very critical as what I am attempting to do is to get a sufficiently distributed set of samples from all over the image. However as I am new to Mathematica syntax all of this is helpful if/when I need to use these idioms, which is likely to be sooner rather than later.. $\endgroup$– NitinSep 2, 2021 at 23:35
I think Downsample[img, {50, 40}]
does it. Not sure if it's the most efficient.
-
3$\begingroup$ That should be the most efficient! in general, built-ins win. contrast
ImageData[img][[;; ;; 40, ;; ;; 50]] // Image // RepeatedTiming
withDownsample[img, {50, 40}] // RepeatedTiming
. (Note that image dimensions are in reverse order to theirImageData
array dimensions, hence the flip of 40 and 50.) $\endgroup$– thorimurAug 30, 2021 at 22:13 -
-
1$\begingroup$ Weirdly enough, I'm trying it again now, and
Downsample
is losing?? I could've sworn when i tried it the first time it won! Maybe it did, with different parameters? The lesson, I guess, is to time it yourself with your parameters, and then use the best choice! $\endgroup$– thorimurSep 2, 2021 at 21:41 -
1$\begingroup$ (Note that if you have a list of images
imgs
all of the same dimension, you can doImage /@ (ImageData /@ imgs)[[All, ;; ;; 40, ;; ;; 50]]
.) $\endgroup$– thorimurSep 2, 2021 at 21:43
img[[;; ;; 50, ;; ;; 40]]
$\endgroup$ImageData[img][[;; ;; 50, ;; ;; 40]]
. This should give you back exactly what you need. $\endgroup$Downsample[img, {n,k}]
will extract a pixel everyn
pixels horizontally andk
pixels vertically. So for instance, ifimg
is 10x10,DownSample[img, {5,5}]
will give you a 2x2 image. LikewiseDownSample[img, {1,1}]
will give you backimg
instead of a 1x1 image. Is that what you want? $\endgroup$