skimage.util.montage
Нюансы следующие:
1) Большинство алгоритмов из библиотек чудовищно медленные. Это касается и PIL, и scikit-image, и tensorflow. Например, я написал аналог tf.image.non_max_suppression, который работает в 5 тысяч (!) раз быстрее. Делайте профайлинг кода, сравнивайте время исполнения и разные варианты решения задачи. Местами очень помогает @jit и @njit из numba.
2) Если обрабатываете изображения в цикле, заранее аллоцируйте память под результат. Например, во многих функциях OpenCV есть параметр out, для которого можно заранее создать numpy.array с заданными shape и dtype.
3) Если собираетесь работать с частями изображения, особенно с вырезкой фрагментов, сдвигом/поворотом и вообще аугментацией для нейросетей, используйте сразу матричные преобразования cv2.warpAffine() - это в разы быстрее, чем отдельные последовательные преобразования.
4) Под Linux лучше ставить не pillow, а pillow-simd (pip install pillow-simd) - можете найти статью на Хабре.
5) Очень полезная функция skimage.util.view_as_windows, и вообще разберитесь с numpy array views - это позволит избежать лишнего копирования данных. Также рекомендую разобраться с numpy.lib.stride_tricks.as_strided (лютый мозговынос; упомянутая выше функция view_as_windows - её гуманная версия).
6) Гуглинг преобразований depth_to_space и space_to_depth (такие функции есть в Tensorflow).
7) Очень полезная штука cv2.filter2D().