EDIT: This answer tackles this question using extra math steps. OP has found a built-in function bpy_extras.object_utils.world_to_camera_view(scene, obj, coord)
to use instead.
I used the concept of "change of basis" from linear algebra. First, I projected the vertices of each object to the plane of the camera frustum. Then, using basis vectors that define the plane of the frustum along with the lower left corner of camera frustum, we can define the projected corner point in terms of these basis vectors and the lower left corner. The stretching scalars of these basis vectors thus tell us what the border min and max values need to be set as.
import bpy
from mathutils import Vector, Matrix
from mathutils.geometry import normal
from numpy import cross, dot
# these need to be true to set border
bpy.context.scene.render.use_border = True
bpy.context.scene.render.use_crop_to_border = True
# get corners of camera frustum
scene = bpy.context.scene
cam = bpy.data.objects['Camera']
bpy.context.view_layer.objects.active = cam
camType = bpy.context.object.data.type
# normalize to ignore camera scale
matrix = cam.matrix_world.normalized()
frame = [matrix @ v for v in cam.data.view_frame(scene=scene)]
origin = matrix.to_translation()
frame.append(origin)
# frame 0 thru 3 are corners of camera frustum
p1, p2, p3, p4 = frame[0:4]
# camera location
l = cam.location
# horizontal basis vector of camera frustum
v1 = p2 - p3
# vertical basis vector of camera frustum
v2 = p4 - p3
# get normal vector of plane
normal = cross(v1, v2)
# objects stored in collection
col = bpy.data.collections['Collection 2']
objs = col.objects
# list of horizontal scalars
s1s = []
# list of vertical scalars
s2s = []
# get each objects coordinates in terms of new basis vectors and p3
for obj in objs:
verts = [obj.matrix_world @ v.co for v in obj.data.vertices]
for v in verts:
# direction of line to be parameterized, if orthogonal, direction should be normal
if camType == 'ORTHO':
direction = normal
else:
direction = l - v
# parametric value for projection to camera
t = -(dot(normal, v) - dot(p1, normal))/dot(normal, direction)
# vert projected to camera frustum
vP = v + Vector(t * direction)
# matrix with v1 and v2 as basis vectors (has to be square in order to invert)
mtxB = Matrix((v1, v2)).to_3x3()
# find scalars to stretch basis vectors to projected point on frustum
scalars = (vP - p3) @ mtxB.inverted()
# horizontal scalar append to all horizontal scalars:
s1s.append(scalars[0])
# vertical scalar append to all vertical scalars:
s2s.append(scalars[1])
# mins and maxs
x_min, y_min, x_max, y_max = min(s1s), min(s2s), max(s1s), max(s2s)
bpy.data.scenes['Scene'].render.border_min_x = x_min
bpy.data.scenes['Scene'].render.border_min_y = y_min
bpy.data.scenes['Scene'].render.border_max_x = x_max
bpy.data.scenes['Scene'].render.border_max_y = y_max
