There was a discussion on the libvips tracker a few months ago about techniques for background removal:
https://github.com/libvips/libvips/issues/1567
Here's the filter:
#!/usr/bin/python3
import sys
import pyvips
image = pyvips.Image.new_from_file(sys.argv[1], access="sequential")
# aim for 250 for paper with low freq. removal
# ink seems to be slightly blueish
paper = 250
ink = [150, 160, 170]
# remove low frequencies .. don't need huge accuracy
low_freq = image.gaussblur(20, precision="integer")
image = image - low_freq + paper
# pull the ink down
ink_target = 30
scale = [(paper - ink_target) / (paper - i) for i in ink]
offset = [ink_target - i * s for i, s in zip(ink, scale)]
image = image * scale + offset
# find distance to white of each pixel ... small distances go to white
white = [100, 0, 0]
image = image.colourspace("lab")
d = image.dE76(white)
image = (d < 12).ifthenelse(white, image)
# boost saturation (scale ab)
image = image * [1, 2, 2]
image.write_to_file(sys.argv[2])
It removes low frequences (ie. paper folds etc.), stretches the contrast range, finds pixels close to white in CIELAB and moves them to white, and boosts saturation.
You'd probably need to tune it a bit for your use-case. Post some sample images if you need more advice.