How to change a material texture per object?
So, I ended up with a script which clones all the materials, both cycles and blender internal (all the materials in the scene render ok in both engines). To set image id I used custom properties. The actual image id calculation seems complicated, but there are tiles in the scene which are parented to some hosts, and they all are duplicated within their respective parents, so it's much easier to handle only parents' properties instead of each child. When resetting materials back it just clears all the extra references, so blender will kill those orphans itself on save / revert.



Here is the script, in case somebody will face the same problem.

#----- scene-tools.py
'''Some tools to automate this very scene tasks.
   @author (c) LV A.S. Mechanic. Kharkov city
'''
import bpy
import glob

# base names of objects of interest
TILE_NAMES = ('tileCA','tileCB','tileCC')

IMAGE_PATH = r'//textures\img'          # blender notation
IMAGE_MASK = 'img_*.jpg'

PROP_ID_PARENT = 'host_id'             # custom property name for image Id calculation
PROP_ID_OBJECT = 'image_id'

# (Cycles)
TILE_MAT_NAME = 'tile-face'             # tile material (cycles)
TILE_MAT_SLOT = 0                       # material slot id to work with
NODE_IMG_NAME = 'ImgTex'                # image texture node name in the material
IMG_NAME = 'img_'                       # image base name

# (Blender Internal)
TILE_MAT_NAME_BI = ('tile-face-bi')     # tile material (BI)
NODE_MAT_BI = 'MatBi'                   # BI material input node name
TILE_TEX_SLOT_BI = 0                    # texture slot id within Bi matertial to work with

def get_my_tiles():
    ''' this returns a list of tuples for all the tiles in the scene.
        (0 - object; 1 - parent id; 2 - object id; 3 - real image id)
    '''
    _res = []
    scn = bpy.context.scene
    _maxs = {}   # here we store max obj_id within parent_id
    _pids = []   # parents' ids
    for obj in scn.objects:
        if (obj.type == 'MESH'):
            for _base in TILE_NAMES:
                if (obj.name.startswith(_base)):
                    # create descriptor and get everything (no real id yet)
                    try:
                        _pid = int(obj.parent[PROP_ID_PARENT])
                    except:
                        _pid = 0
                    try:
                        _oid = int(obj[PROP_ID_OBJECT])
                    except:
                        _oid = 0
                    # update maximums
                    _maxv = _maxs.get(_pid)
                    _maxs[_pid] = (_oid if _maxv == None else max(_maxv, _oid))
                    # update pids
                    if not (_pid in _pids): _pids.append(_pid)
                    # store values
                    _d = tuple((obj,_pid,_oid))
                    _res.append(_d)
                    #print('found object: {}'.format(obj.name))
                    break
    # now we calculate the real image id for each object
    # get sorted _pids
    _pids.sort()
    # replace _maxs values with start offset
    _offset = 0
    for _pid in _pids:
        _maxv = _maxs[_pid]
        _maxs[_pid] = _offset
        _offset += _maxv + 1
    # and calc real id with id offsets
    _res1 = []
    for _d in _res:
        _res1.append(_d+(_d[2]+_maxs[_d[1]],))
    '''    
    print("--------------- final tiles")
    _n = 0
    for _d in _res1:
        print(_n," : (", _d[0], _d[1], _d[2], _d[3], ")")
        _n += 1
    '''
    return _res1


def error_stop(msg):
    ''' show message and exit with error '''
    raise Exception(msg)


def get_filenames(path, mask):
    ''' returns sorted list of files matching the mask in given path '''
    _path = bpy.path.abspath(path)
    #print('path: ', _path)
    _files = glob.glob(_path+"\\"+mask)
    if len(_files)&gt;0:
        _files.sort()
    return _files


def name_with_id(name, id):
    ''' composes indexed name with a base name and an id (name.00N)'''
    return name + "." + "{:03d}".format(id)


def reset_tile_materials():
    """ Resets materials to defaults for all the tiles in the scene """
    # first find the defaults
    mat_default = bpy.data.materials[TILE_MAT_NAME]
    if (mat_default is None):
        error_stop('Material {} not found!'.format(TILE_MAT_NAME))
    mat_default_bi = bpy.data.materials[TILE_MAT_NAME_BI]
    if (mat_default_bi is None):
        error_stop('Material (Bi) {} not found!'.format(TILE_MAT_NAME_BI))
    tex_default = mat_default_bi.texture_slots[TILE_TEX_SLOT_BI].texture
    img_default = tex_default.image
    # get all the tiles
    my_tiles = get_my_tiles()
    if (len(my_tiles) == 0):
        print('Tiles not found. There is nothing to do.')
        return
    # set default material for all
    print('Setting defaults for {} found objects'.format(len(my_tiles)))
    for _d in my_tiles:
        obj = _d[0]
        # reset all possible references
        mat = obj.material_slots[TILE_MAT_SLOT].material
        mat_bi = mat.node_tree.nodes[NODE_MAT_BI].material
        tex = mat_bi.texture_slots[TILE_TEX_SLOT_BI].texture
        tex.image = img_default
        mat_bi.texture_slots[TILE_TEX_SLOT_BI].texture = tex_default
        mat.node_tree.nodes[NODE_MAT_BI].material = mat_default_bi
        mat.node_tree.nodes[NODE_IMG_NAME].image = img_default
        obj.material_slots[TILE_MAT_SLOT].material = mat_default
    print('All tile materials'' defaults set.') 


def find_or_create_image(name, img_ref, filepath):
    ''' it finds the image by name. (if not found it's created by copying img_ref),
    then given path will be set as the image filepath '''
    img = bpy.data.images.get(name)
    if img == None:
        img = img_ref.copy()
        img.name = name
    img.filepath = filepath
    return img

def find_or_create_material(base_name, base_name_bi, id, mat_ref, mat_ref_bi):
    ''' it finds the material by name. (if not found ot's created by copying mat_ref),
    then the image will be set as the image material's image '''
    # image is already ok, hust find it
    img = bpy.data.images[name_with_id(IMG_NAME, id)]
    # first handle Bi material
    tex_ref = mat_ref_bi.texture_slots[TILE_TEX_SLOT_BI].texture
    # find or create a texture
    tex = bpy.data.textures.get(name_with_id(base_name_bi, id))
    if tex == None:
        tex = tex_ref.copy()
        tex.name = name_with_id(base_name_bi, id)
    # put needed image to the new texture
    tex.image = img
    # now find bi material
    mat_bi = bpy.data.materials.get(name_with_id(base_name_bi, id))
    if mat_bi == None:
        mat_bi = mat_ref_bi.copy()
        mat_bi.name = name_with_id(base_name_bi, id)
    # set new texture
    mat_bi.texture_slots[TILE_TEX_SLOT_BI].texture = tex
    # now cycles
    mat = bpy.data.materials.get(name_with_id(base_name, id))
    if mat == None:
        mat = mat_ref.copy()
        mat.name = name_with_id(base_name, id)
    node = mat.node_tree.nodes[NODE_IMG_NAME]
    node.image = bpy.data.images[name_with_id(IMG_NAME, id)]
    # and set bi material to Bi node
    node_bi = mat.node_tree.nodes[NODE_MAT_BI]
    node_bi.material = mat_bi
    return mat

def create_tile_materials():
    ''' this creates a bunch of materials for the tiles, and sets images from the img directory '''
    # first get all the image file names available
    print("Getting available images' names")
    img_files = get_filenames(IMAGE_PATH, IMAGE_MASK)
    if (len(img_files) == 0):
        error_stop('No imahes found in '+IMAGE_PATH)
    print("Found {} images to process".format(len(img_files)))
    # find all the tiles
    my_tiles = get_my_tiles()
    if (len(my_tiles) == 0):
        print('Tiles not found. There is nothing to do.')
        return
    # count images needed (with max image id)
    _max_id = 0
    for _d in my_tiles: _max_id = max(_max_id, _d[3])
    print("It's needed {} images for {} objects".format(_max_id + 1, len(my_tiles)))
    if (len(img_files) &lt; (_max_id + 1)):
        error_stop("There is not enough images ({} of {})!".format(len(img_files), _max_id + 1))
    # create images copying from the orignial one
    ids = []
    for _d in my_tiles:
        _id = _d[3]
        if not _id in ids:
            ids.append(_id)
    ids.sort()
    # find the original image to copy
    mat_ref = bpy.data.materials[TILE_MAT_NAME]
    img_ref = mat_ref.node_tree.nodes[NODE_IMG_NAME].image
    # Bi
    mat_ref_bi = bpy.data.materials[TILE_MAT_NAME_BI]
    # actual creation
    for id in ids:
        find_or_create_image(name_with_id(IMG_NAME, id), img_ref, img_files[id])
    # and materials creation
    for id in ids:
        #base_name, base_name_bi, id, mat_ref, mat_ref_bi):
        find_or_create_material(TILE_MAT_NAME, TILE_MAT_NAME_BI, id, mat_ref, mat_ref_bi)
    # assigning materials to the objects
    for _d in my_tiles:
        obj = _d[0]
        id = _d[3]
        # cycles
        mat = bpy.data.materials[name_with_id(TILE_MAT_NAME, id)]
        obj.material_slots[TILE_MAT_SLOT].material = mat
    print("Materials created and assigned to the objects.")


class CreateTileMaterialsOperator(bpy.types.Operator):
    """Creates new materials with different images for all the tiles in the scene"""
    bl_idname = "scene.my_create_tile_materials"
    bl_label = "Create tile materials"

    def execute(self, context):
        create_tile_materials()
        return {'FINISHED'}

class ResetTileMaterialsOperator(bpy.types.Operator):
    """ Resets materials to defaults for all the tiles in the scene """
    bl_idname = "scene.my_reset_tile_materials"
    bl_label = "Reset tile materials"

    def execute(self, context):
        reset_tile_materials()
        return {'FINISHED'}


class MySceneToolsPanel(bpy.types.Panel):
    """Creates a Panel in the Scene properties window"""
    bl_label = "Scene tools"
    bl_idname = "SCENE_PT_MySceneTools"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "scene"

    def draw(self, context):
        layout = self.layout

        row = layout.row()
        row.operator("scene.my_create_tile_materials")

        row = layout.row()
        row.operator("scene.my_reset_tile_materials")


def register():
    bpy.utils.register_class(ResetTileMaterialsOperator)
    bpy.utils.register_class(CreateTileMaterialsOperator)
    bpy.utils.register_class(MySceneToolsPanel)


def unregister():
    bpy.utils.unregister_class(ResetTileMaterialsOperator)
    bpy.utils.unregister_class(CreateTileMaterialsOperator)
    bpy.utils.unregister_class(MySceneToolsPanel)

register()
#-----/scene-tools.py