code: def simplify_merge_adjacent(self):
      if self.shape_len == 0:
          return
      shapes, strides = [x.shape for x in self.sts], [
          x.real_strides() for x in self.sts
      ]
  
      # if it's an image, insert fake strides such that this fusion doesn't happen across image axes
      if isinstance(self.bufs[0].dtype, ImageDType):
          base_shape = self.bufs[0].dtype.shape
          if shape_idx_groups := get_contraction(
              self.output_shape, base_shape
          ):
              special_strides: Tuple[int, ...] = tuple()
              for i, g in enumerate(shape_idx_groups):
                  shape_piece = tuple(self.output_shape[x] for x in g)
                  assert (
                      prod(shape_piece) == base_shape[i]
                  ), f"get_contraction was wrong? {shape_piece} != {base_shape[i]}"
                  special_strides += strides_for_shape(shape_piece)
              # adding the fake image shape
              shapes.append(self.output_shape)
              strides.append(special_strides)
  
      # merge dimensions if we can, multi get_shape_strides
      # NOTE: this does not always preserve the reduce dimension
      # TODO: move this into shapetracker, with tests!
      rets = [
          [(shapes[j][0], strides[j][0])] for j in range(len(shapes))
      ]
      for i in range(1, len(shapes[0])):
          can_merge = []
          for j in range(len(shapes)):
              # TODO: added the always mergeability of 1s, is this right? if so, add to shapetracker in the 1 case
              can_merge.append(
                  strides[j][i] is not None
                  and (
                      (
                          strides[j][i] != 0
                          and rets[j][-1][1]
                          == shapes[j][i] * cast(int, strides[j][i])
                      )
                      or (strides[j][i] == 0 and rets[j][-1][1] == 0)
                  )
              )  # noqa: E501
          # more can merge than this
          mergeable = all(can_merge) and i != self.first_reduce
          for j in range(len(shapes)):
              if mergeable:
                  rets[j][-1] = (
                      rets[j][-1][0] * shapes[j][i],
                      strides[j][i],
                  )
              else:
                  rets[j].append((shapes[j][i], strides[j][i]))
  
      # do the reshapes
      for i, x in enumerate(rets[: len(self.sts)]):
          self.sts[i] = self.sts[i].reshape(tuple([y[0] for y in x]))
  
imports: from __future__ import annotations
  import os, math, itertools
  from typing import NamedTuple, Optional, List, Tuple, cast, Dict, Union
  from tinygrad.ops import LazyOp, FlopCounter, get_lazyop_info, UnaryOps, BinaryOps, ReduceOps, MemBuffer, ConstBuffer, BufferOps
  from tinygrad.device import Device, Compiled
  from tinygrad.dtype import dtypes, ImageDType, DType
  from tinygrad.helpers import dedup, colored, ansilen, getenv, prod, DEBUG, round_up, all_int
  from tinygrad.shape.shapetracker import ShapeTracker, get_contraction
  from tinygrad.shape.symbolic import sint
  from tinygrad.shape.view import View, strides_for_shape
  from dataclasses import dataclass
  from enum import Enum, auto
label_desc: Use image node_id 22 for calling the Kernel obj's underlying member method code with example usage: obj.simplify_merge_adjacent() without return types