AFAIK there's not much you can do to further improve performances CPU sideAFAIK there's not much you can do to further improve performances CPU side, but as suggested in the comments this seems like a typical application where GPU processing could give you significant performace improvement. However doing image processing with OpenGL or Metal is in general not an easy task, specially if you're completely new to it.
Filter benchmarkstsCore Image filter benchmarks
- CPU ~ 120120ms
- GPU ~ 25ms
You can find a working sample project https://github.com/tcamin/CustomCoreImageFilteringDemo
#EDIT#**Improved CPU version**
When first answering the question I actually omitted to tell that you might get improved performances leveraging arm's SIMD instructions set (see NEON technology, ARM's blog, intrinsic functions) which allow to execute instruction on multiple data at once.
The code is pretty low level and a lot less readable than the GPU version, however it's good to know that there is also this option available. I would strongly suggest to stick with the GPU version which is far more flexible, but I was really curios to checkout how fast this implementation would result.
void process_pixels_neon_with_lut(uint8_t *src, unsigned long numPixels, uint8_t focus_r, uint8_t focus_g, uint8_t focus_b, uint8_t *gamma_lut)
{
float32x4_t y32_factor_r = vdupq_n_f32(0.2126f);
float32x4_t y32_factor_g = vdupq_n_f32(0.7152f);
float32x4_t y32_factor_b = vdupq_n_f32(0.0722f);
uint8x8_t focus8_r = vdup_n_u8(focus_r);
uint8x8_t focus8_g = vdup_n_u8(focus_g);
uint8x8_t focus8_b = vdup_n_u8(focus_b);
uint8x8_t fthrsh8 = vdup_n_u8(kFocusThreshold);
unsigned long n = numPixels / 8 + 1;
// Convert per eight pixels
while (n-- > 0)
{
uint8x8x4_t pix = vld4_u8(src);
uint8x8_t p8_r = pix.val[0];
uint8x8_t p8_g = pix.val[1];
uint8x8_t p8_b = pix.val[2];
// check if color should be in original color
uint8x8_t delta8_r = vabd_u8(p8_r, focus8_r);
uint8x8_t delta8_g = vabd_u8(p8_g, focus8_g);
uint8x8_t delta8_b = vabd_u8(p8_b, focus8_b);
uint8x8_t delta8_lt_ft_r = vclt_u8(delta8_r, fthrsh8);
uint8x8_t delta8_lt_ft_g = vclt_u8(delta8_g, fthrsh8);
uint8x8_t delta8_lt_ft_b = vclt_u8(delta8_b, fthrsh8);
uint8x8_t keep_color8 = vand_u8(delta8_lt_ft_r, vand_u8(delta8_lt_ft_g, delta8_lt_ft_b));
uint8x8_t discard_color8 = vmvn_u8(keep_color8);
// split and convert uint8x8 -> 2x float32x4_t
float32x4_t p32_low_r, p32_low_g, p32_low_b;
float32x4_t p32_high_r, p32_high_g, p32_high_b;
uint8x8_to_float32x4_t(p8_r, &p32_low_r, &p32_high_r);
uint8x8_to_float32x4_t(p8_g, &p32_low_g, &p32_high_g);
uint8x8_to_float32x4_t(p8_b, &p32_low_b, &p32_high_b);
// calculate Y
float32x4_t temp_y32_low_r = vmulq_f32(p32_low_r, y32_factor_r);
float32x4_t temp_y32_low_g = vmulq_f32(p32_low_g, y32_factor_g);
float32x4_t temp_y32_low_b = vmulq_f32(p32_low_b, y32_factor_b);
float32x4_t y32_low = vaddq_f32(temp_y32_low_r, vaddq_f32(temp_y32_low_g, temp_y32_low_b));
float32x4_t temp_y32_high_r = vmulq_f32(p32_high_r, y32_factor_r);
float32x4_t temp_y32_high_g = vmulq_f32(p32_high_g, y32_factor_g);
float32x4_t temp_y32_high_b = vmulq_f32(p32_high_b, y32_factor_b);
float32x4_t y32_high = vaddq_f32(temp_y32_high_r, vaddq_f32(temp_y32_high_g, temp_y32_high_b));
// gamma correction using lut.
for (int j = 0; j < 4; j++)
{
y32_low[j] = gamma_lut[(int)(y32_low[j] * kGammaLUTSize / 255.0)];
y32_high[j] = gamma_lut[(int)(y32_high[j] * kGammaLUTSize / 255.0)];
}
// convert back to int and merge
uint8x8_t y8;
floats32x4_to_uint8x8(y32_low, y32_high, &y8);
// merge grayscale + original rgba
uint8x8_t pix_grayscale = vand_u8(y8, discard_color8);
pix.val[0] = vadd_u8(vand_u8(p8_r, keep_color8), pix_grayscale);
pix.val[1] = vadd_u8(vand_u8(p8_g, keep_color8), pix_grayscale);
pix.val[2] = vadd_u8(vand_u8(p8_b, keep_color8), pix_grayscale);
vst4_u8(src, pix);
src += 8 * 4;
}
}
For the gamma correction I chose for simplicity (and probably speed) to use a preloaded LUT. Check the git repository for the full code.
NEON filter benchmarks
On my iPhone 5S on a 1537 × 667 pixels image I'm getting approximately a 3x speedup
- CPU ~ 120ms
- NEON ~ 40ms
On a 375 × 500 pixels image we have a 4x speedup
- CPU ~ 40ms
- NEON ~ 10ms
On small images the NEON version outperforms the GPU implementation since there's no setup overhead.