4-bit Grayscale Image Processor

┌───────────────────────────────────────┐
│             ImageProcess4             │
│         Author: Phil Pilgrim          │
│ (c) Copyright 2011 Bueno Systems, Inc.│
│   See end of file for terms of use.   │
└───────────────────────────────────────┘

Revision History

2006.07.17: v0.1 Alpha release.
2014.01.16: v1.0 Initial release.

Contact Information

Phil Pilgrim: propeller@phipi.com

Introduction

This module provides drawing and image processing functions for 4-bit grayscale images, such as those obtained by the PropCAM (TM). It launches one additional cog for performing the image processing functions. The coordinate system used by this module assumes that (0,0) is the upper-leftmost point. X increases from left to right. Y increases from top to bottom. In any procedure call where either (x0, y0) or (x1, y1) lies outside the overall bounds of the display buffer, no action is taken and zero is returned.

Constants


SOURCE CODE...
  SETBUF        = $00
  ITERATE       = $80
  NO_OP         = $30
  WR1           = $02
  WR0           = $01


Constants for User's Program

SOURCE CODE...
  
  LIN           = $00           'Iterate over a straight line.
  RECT          = $10           'Iterate over a rectangular area.
  BORDER        = $20           'Iterate along the border of a "blob".

  EODIF         = $08           'Even/odd differential: Pix(x,y) is ||(Pix(x, y & $fe) - Pix(x, y | 1))
  FIND_ONE      = $04           'Stop iterating when the first pixel meeting the mask condition is found.

  UNDEF         = $8000_0000    'Undefined: Use for replacement values you don't want written.


Instance Variables


SOURCE CODE...
  word width, height
  word stats[22]                'Image stats array
  byte started                  'CogNo + 1 of started cog.


Public Spin Methods

Start and Stop Methods

This group of methods starts and stops the image processor.

start(gray_buf_addr)

Start this object and the cog associated with it.

Parameters:
gray_buf_addr: the address of the 4-bit-deep grayscale buffer in which the work will be done. The first word of this buffer contains the height in pixels. The second word contains the width in pixels. The remaining width * height / 2 bytes are the buffer area. The buffer should be on a long boundary.
Return: true on success; false, otherwise.
Example: success := img.start(@gray_buf)
Start the object, pointing to an image buffer at gray_buf. Store result in success.

SOURCE CODE...
PUB start(gray_buf_addr) 

  stop
  if started := cognew(@entry, @_command) + 1
    command(SETBUF << 24 | gray_buf_addr, 0, 0 )
    width := word[gray_buf_addr][1]
    height := word[gray_buf_addr]
    return true
  else
    return false


stop

Stop the object, and release its associated cog.

Return: true if the object had been started; false, otherwise.
Example: img.stop
Stop the object.

SOURCE CODE...
PUB stop 

  if started
    cogstop(started - 1)
    started~
    return true
  else
    return false


Image Process Methods

This group of methods analyzes areas within the image buffer and, optionally, modifies the pixels it encounters.

box(px0, py0, px1, py1, pix1, pix0, condx)

Draw a box (rectangle outline) with corners at (px0, py0) and (px1, py1). As the box is drawn, pixel values are replaced with pix1 or pix0, depending on whether its corresponding mask bit is a 1 or 0, respectively. If the replacement value is UNDEF, the pixel value is not replaced.

Parameters:
px0: X coordinate of the box's first corner.
py0: Y coordinate of the box's first corner.
px1: X coordinate of the box's opposite corner.
py1: Y coordinate of the box's opposite corner.
pix1: The value to replace 1-masked pixels with, or UNDEF for no replacements.
pix0: The value to replace 0-masked pixels with, or UNDEF for no replacements.
condx: The 16-bit mask used to determine pixel type (high bit for 15; low bit for 0).
Return: The number of 1-masked pixels encountered.
Example: size := img.draw_box(50, 50, 80, 80, 15, img#UNDEF, %1111_1111_0000_0000)
Along the edge of the box bounded by (50, 50) and (80, 80) replace all pixels having values > 7 with 15. Return the number of pixels whose values were > 7 and assign to size.

SOURCE CODE...
PUB box(px0, py0, px1, py1, pix1, pix0, condx) : sum | p

  p := px0 <# px1
  px1 #>= px0 <# width - 1
  px0 := p #> 0
  p := py0 <# py1
  py1 #>= py0 <# height - 1
  py0 := p #> 0
  sum := line(px0, py0, px0, py1, pix1, pix0, condx, 0)
  if (px1 > px0)
    sum += line(px1, py0, px1, py1, pix1, pix0, condx, 0)
  if (px1 - px0 > 1)
    sum += line(px0 + 1, py0, px1 - 1, py0, pix1, pix0, condx, 0)
    if (py1 > py0)
      sum += line(px0 + 1, py1, px1 - 1, py1, pix1, pix0, condx, 0)


crosshair(px, py, size, pix1, pix0, condx)

Draw a crosshair centered on point (px, py) extending by size in each direction. When the crosshair is drawn, its pixel value is replaced with pix1 or pix0, depending on whether its corresponding mask bit is a 1 or 0, respectively. If the replacement value is UNDEF, the pixel value is not replaced.

Parameters:
px: X-coordinate of crosshair.
py: Y-coordinate of crosshair.
size: How much to extend crosshair from center in each direction.
pix1: The value to replace 1-masked pixels with, or UNDEF for no replacements.
pix0: The value to replace 0-masked pixels with, or UNDEF for no replacements.
condx: The 16-bit mask used to determine pixel type (high bit for 15; low bit for 0).
Return: The number of 1-masked pixels encountered.
Example: img.draw_crosshair(64, 64, 2, 0, 15, %1111_1111_0000_0000)
Draw a crosshair at location (64, 64) 5 units high and wide (+2 in each direction). Replace light-colored pixels (>7) with black (0); dark-colored pixels with white (15).

SOURCE CODE...
PUB crosshair(px, py, size, pix1, pix0, condx) : sum

  ||size
  sum := line(px - size #> 0, py, px + size <# width - 1, py, pix1, pix0, condx, 0)
  if (size > 0)
    sum += line(px, py - size #> 0, px, py -1 <# height - 1, pix1, pix0, condx, 0)
    sum += line(px, py + 1 #> 0, px, py + size <# height - 1, pix1, pix0, condx, 0)


point(px, py, pix1, pix0, condx)

Draw a point at (px, py). When the point is drawn, its pixel value is replaced with pix1 or pix0, depending on whether its corresponding mask bit is a 1 or 0, respectively. If the replacement value is UNDEF, the pixel value is not replaced.

Parameters:
px: X-coordinate of crosshair.
py: Y-coordinate of crsoohair.
pix1: The value to replace 1-masked pixels with, or UNDEF for no replacements.
pix0: The value to replace 0-masked pixels with, or UNDEF for no replacements.
condx: The 16-bit mask used to determine pixel type (high bit for 15; low bit for 0).
Return: The number of 1-masked pixels encountered.
Example: img.draw_point(45, 32, 7, img#UNDEF, $ffff)
Draw a point a location (45, 32) with a pixel value of 7, regardless of what pixel value it replaces.

SOURCE CODE...
PUB point(px, py, pix1, pix0, condx)

  return line(px, py, px, py, pix1, pix0, condx, 0)


line(px0, py0, px1, py1, pix1, pix0, condx, stats_addr)

Draw a line beginning at (px0, py0) and ending at (px1, py1), inclusive. As the line is drawn, pixel values are replaced with pix1 or pix0, depending on whether its corresponding mask bit is a 1 or 0, respectively. If the replacement value is UNDEF, the pixel value is not replaced.

Parameters:
px0: X coordinate of the line's first end.
py0: Y coordinate of the line's first end.
px1: X coordinate of the line's opposite end.
py1: Y coordinate of the line's opposite end.
pix1: The value to replace 1-masked pixels with, or UNDEF for no replacements.
pix0: The value to replace 0-masked pixels with, or UNDEF for no replacements.
condx: The 16-bit mask used to determine pixel type (high bit for 15; low bit for 0).
stats_addr: Address of a 22-word word-aligned array, or zero. (See do below for an explanation.)
Return: The number of 1-masked pixels encountered.
Example: dark := img.line(0, 7, 127, 7, img#UNDEF, img#UNDEF, %0000_0000_0000_0011, 0)
Count the number of dark pixels (<2) in a horizontal line from X = 0 to X = 127 at height Y = 7. Assign the count to dark.

SOURCE CODE...
PUB line(px0, py0, px1, py1, pix1, pix0, condx, stats_addr)

  return do(LIN, px0, py0, px1, py1, pix1, pix0, condx, stats_addr)


filled_box(px0, py0, px1, py1, pix1, pix0, condx, stats_addr)

Draw a filled rectangle with corners at (px0, py0) and (px1, py1). As the rectangle is drawn, pixel values are replaced with pix1 or pix0, depending on whether its corresponding mask bit is a 1 or 0, respectively. If the replacement value is UNDEF, the pixel value is not replaced.

px0: X coordinate of the rectangle's first corner.
py0: Y coordinate of the rectangle's first corner.
px1: X coordinate of the rectangle's opposite corner.
py1: Y coordinate of the rectangle's opposite corner.
pix1: The value to replace 1-masked pixels with, or UNDEF for no replacements.
pix0: The value to replace 0-masked pixels with, or UNDEF for no replacements.
condx: The 16-bit mask used to determine pixel type (high bit for 15; low bit for 0).
stats_addr: Address of a 22-word word-aligned array, or zero. (See do below for an explanation.)
Return: The number of 1-masked pixels encountered.
Example: count := img.draw_rectangle(0, 0, 127, 95, 15, img#UNDEF, %1111_1111_0000_0000, @img_stats)
Replace all the bright pixels (>7) in a 128x96-pixel image with white, returning the number of bright pixels replaced and assigning it to count. Image stats are also stored in the img_stats array.

SOURCE CODE...
PUB filled_box(px0, py0, px1, py1, pix1, pix0, condx, stats_addr)

  return do(RECT, px0, py0, px1, py1, pix1, pix0, condx, px0 << 24 | py0 << 16 | stats_addr)


outline(px, py, px0, py0, px1, py1, new1, condx, stats_addr)

Trace the outline of a "blob" that contains point (px, py) and is bounded by the rectangle (px0, py0), (px1, py1). Points interior to the blob are defined by the 16-bit condx mask: Pixel values corresponding to 1's are interior points; to 0's, exterior. (Points outside the bounding rectangle are deemed to be exterior points.) Point (px,py) must be an interior point, or the function will return zero. Otherwise the function scans right for the first exterior point, then follows the boundary around the blob until a full circuit has been made. If the circuit traversed defines an interior hole instead of outside boundary, further scans to the right are made until an outside boundary is found. The function will then return the number of boundary points traversed (i.e. the perimeter of the "blob"). Perimeter points may be replaced by the value new1, unless it is UNDEF. new1 should be an "interior" value, or else this function will malfunction. (See "do" below for a description of stats_addr.)

Parameters:
px: X coordinate of starting point, interior to the blob.
py: Y coordinate of starting point, interior to the blob.
px0: X coordinate of the bounding rectangle's first corner.
py0: Y coordinate of the bounding rectangle's first corner.
px1: X coordinate of the bounding rectangle's opposite corner.
py1: Y coordinate of the bounding rectangle's opposite corner.
new1: Value to replace perimeter pixels with, or UNDEF.
condx: The 16-bit mask used to determine interior pixels (high bit for 15; low bit for 0).
stats_addr: Address of a 22-word word-aligned array, or zero. (See do below for an explanation.)
Return: The perimeter of the blob.
Example: img.outline(50, 50, 0, 0, 127, 95, 0, %0000_0000_1111_1111, @img_stats)
Starting at interior point (50, 50) in a bounding box defined by corners (0, 0), (127, 95) find and follow an edge between interior dark pixels (<8) and exterior bright pixels. Replace the perimeter pixels with black (0). Record the image stats in the array img_stats.

SOURCE CODE...
PUB outline(px, py, px0, py0, px1, py1, new1, condx, stats_addr) : retn

  stats_addr &= $ffff
  px &= $ff
  py &= $ff
  repeat while (retn := do(BORDER, px0, py0, px1, py1, UNDEF, UNDEF, condx, px << 24 | py << 16 | stats_addr)) < 0
    do(LIN | FIND_ONE, px, py, px1, py, UNDEF, UNDEF, !condx, @stats)
    do(LIN | FIND_ONE, py, px1, py, UNDEF, UNDEF, condx, stats[16], @stats)
    px := stats[16]
  if new1 <> UNDEF
    retn := do(BORDER, px0, py0, px1, py1, new1, UNDEF, condx, px << 24 | py << 16 | stats_addr)


do(comd, px0, py0, px1, py1, pix1, pix0, condx, stats_addr)

This is the general user-accessible function to use when the above convenience functions fall short. It iterates over the chosen area, optionally replacing pixel values as it goes. If a non-zero word address is provided in the lower 16 bits of stats_addr, it will fill the array there with information about the area traversed.

Parameters:
comd: LIN, RECT, or BORDER, optionally ORed with EODIF and/or FIND_ONE. This selects the type and behavior of the iterator.
The LIN iterator covers a straight line beginning at (px0, py0) and ending at (px1, py1).
The RECT iterator covers a rectangular area bounded by (px0, py0) and (px1, py1). An optional starting point can be specified in the upper 16 bits of stats_addr as x << 24 | y << 16. This can be used to resume an iteration that was stopped with the FIND_ONE modifier. The starting point will be (x #> px0, y #> py0), so if x and y are zero, the starting point is (px0, py0).
The BORDER iterator starts at (x, y) embedded in stats_addr, as above for RECT. It behaves as described above for "outline" except that it does not keep searching if it finds a "hole". In the case of a hole, it returns a negative count of the perimeter pixels.
EODIF selects even/odd differential mode. In this mode a pixel's value is taken to be absolute value of the difference between it and its even/odd mate in the y direction. In this mode, LIN and BORDER examine all pixels in their path; RECT examines only pixels in odd-numbered rows to avoid redundancy and save time. Only pixels in odd-numbered rows can have their values replaced in this mode.
FIND_ONE stops the iterator when the first pixel corresponding to a "1" in the condition mask is found.
px0: X coordinate of the bounding rectangle's first corner.
py0: Y coordinate of the bounding rectangle's first corner.
px1: X coordinate of the bounding rectangle's opposite corner.
py1: Y coordinate of the bounding rectangle's opposite corner.
new1: Value to replace perimeter pixels with, or UNDEF.
condx: The 16-bit mask used to determine interior pixels (high bit for 15; low bit for 0).
stats_addr: Optionally define the (x, y) starting point in its upper 16 bits, and a word-aligned address for a stats array in its lower 16 bits. The stats, if specified, will be written to 22 words, beginning at the specified address as follows:
Words  0 - 15 (histogram): A count of how many pixels examined have each corresponding value.
Words 16 - 19 (bounding box): Corner coordinates (xleft, ytop), (xright, ybottom) of the smallest rectangle containing the examined pixels whose intensities correspond to ones in the condition mask.
Words 20 - 21 (centroid): The average coordinates (xavg, yavg) of the examined pixels whose intensities correspond to ones in the condition mask.
Return: The number of iterated-over pixels whose intensities correspond to ones in the condition mask.
Example: changes := img.do(img#RECT | img#EODIF, 0, 0, 127, 95, img#UNDEF, %1111_1111_1111_0000, 0)
Count the number of pixels in even rows that differ from their odd-row counterparts by 4 or more. Assign the result to changes.

SOURCE CODE...
PUB do(comd, px0, py0, px1, py1, pix1, pix0, condx, stats_addr)

  return command(((comd | ITERATE) & $ff | WR1 & (pix1 <> UNDEF) | WR0 & (pix0 <> UNDEF)) << 24 | (pix1 & $0f) << 20 | (pix0 & $0f) << 16 | condx & $ffff, px0 << 24 | (py0 & $ff) << 16 | (px1 & $ff) << 8 | py1 & $ff, stats_addr) 


Private Spin Methods


command(cmd_n_mask, x0y0x1y1, xy_init_stats_ptr)

This private method is the direct interface to the PASM code.

SOURCE CODE...
PRI command(cmd_n_mask, x0y0x1y1, xy_init_stats_ptr)

  if started
    _params0 := x0y0x1y1
    _params1 := xy_init_stats_ptr
    _command := cmd_n_mask
    repeat while _command
    return _params0
  else
    return 0


PASM Code/Class Variables


SOURCE CODE...
              org       0
entry         mov       xy_ptr,par
              add       xy_ptr,#4
              mov       ptr_ptr,par
              add       ptr_ptr,#8 
              
get_cmd       rdlong    cmd,par wz
        if_z  jmp       #get_cmd

              rdlong    x0,xy_ptr
              mov       y0,x0
              mov       x1,x0
              mov       y1,x0
              shr       x0,#24
              shr       y0,#16
              and       y0,#$ff
              shr       x1,#8
              and       x1,#$ff
              and       y1,#$ff
              mov       ret_val,#0
              mov       mask,cmd
              and       mask,_0xffff
              shr       cmd,#16
              mov       replace0,cmd
              and       replace0,#$0f
              mov       replace1,cmd
              shr       replace1,#4
              and       replace1,#$0f
              shr       cmd,#8
              rdlong    out_ptr,ptr_ptr
              cmp       cmd,#SETBUF wz
        if_nz jmp       #chkrange

              rdword    high,mask
              add       mask,#2
              rdword    wide,mask
              mov       dwide,wide
              shr       dwide,#1
              add       mask,#2
              mov       buf_addr,mask
              jmp       #all_done              
        
chkrange      cmp       x0,wide wc
        if_c  cmp       y0,high wc
        if_c  cmp       x1,wide wc
        if_c  cmp       y1,high wc
        if_nc jmp       #all_done

'_________________________________________________________

do_iterate    mov       x,x0
              mov       y,y0
              and       cmd,#$7f
              cmp       cmd,#RECT wc
        if_nc call      #start_xy
              call      #pixaddr
              mov       difx,x1
              sub       difx,x0 wc,wz
        if_z  mov       dx,#0
        if_nz negc      dx,#1
        if_nz negc      difx,difx
              mov       countx,difx
              add       countx,#1

              mov       dify,y1
              sub       dify,y  wc,wz
        if_z  mov       dy,#0
        if_nz negc      dy,#1
        if_nz negc      dify,dify
              mov       county,dify
              add       county,#1
              negc      dyaddr,dwide
 
              movd      :clr_lp,#histo
              mov       s0,#22
              
:clr_lp       mov       0-0,#0
              add       :clr_lp,_0x200
              djnz      s0,#:clr_lp
              
              sub       minx,#1
              sub       miny,#1

              cmp       cmd,#RECT wc
        if_c  jmp       #iter_line
              cmp       cmd,#BORDER wc
        if_c  jmp       #iter_rect
              cmp       cmd,#NO_OP wc
        if_c  jmp       #iter_outline
              jmp       #done

'_________________________________________________________

iter_line     min       countx,county
              mov       s0,difx
              shr       s0,#1
              mov       s1,dify
              shr       s1,#1
              mov       s2,difx
              max       s2,dify

:loop         call      #do_opr
              sub       s0,s2 wc,wz
    if_c_or_z add       s0,difx
    if_c_or_z add       pix_addr,dyaddr
    if_c_or_z add       y,dy
              sub       s1,s2 wc,wz
 if_nc_and_nz jmp       #:next
 
              add       s1,dify
              test      x,#1 wz
              mov       dx,dx wc
    if_c_eq_z add       pix_addr,dx
              add       x,dx
:next         djnz      countx,#:loop

              jmp       #done

'_________________________________________________________

iter_rect     mov       s2,dyaddr
              mov       s0,x0
              shr       s0,#1
              add       dyaddr,s0
              mov       s0,x1
              shr       s0,#1
              sub       dyaddr,s0
              test      y,#1 wz
              test      cmd,#EODIF wc
  if_nz_or_nc jmp       #:start_ok

              add       y,dy
              mov       x,x0
              sub       county,#1
              call      #pixaddr              
  
:start_ok     mov       countx,x1
              sub       countx,x
              abs       countx,countx
              jmp       #:starty              
              
:loopy        test      y,#1 wz
              test      cmd,#EODIF wc
   if_z_and_c add       pix_addr,s2
   if_z_and_c jmp       #:next_y
    
              mov       x,x0
              mov       countx,difx
:starty       add       countx,#1
              jmp       #:do_opr

:loopx        test      x,#1 wz
              mov       dx,dx wc
    if_c_eq_z add       pix_addr,dx
              add       x,dx
               
:do_opr       call      #do_opr
              djnz      countx,#:loopx
              
              add       pix_addr,dyaddr
:next_y       add       y,dy
              djnz      county,#:loopy

              jmp       #done

'_________________________________________________________

iter_outline  call      #try_pix                'Does the pixel at (x,y) meet the blob conditions?
         if_z jmp       #done                   '  No:  Nothing left to do.

:find_edge    call      #move_rt                '  Yes: Move one pixel to the right. Still inside the blob?
        if_nz jmp       #:find_edge             '         Yes: Do it again.

              call      #move_lf                '         No:  Move back onto the blob pixel.
              mov       s0,#2                   '              Set initial direction to 'left'.
              mov       dify,#0                 '              Clear winding number.
              
:follow_lp    sub       s0,#1                   'Following outline with right hand on wall. Try turning right first.
              and       s0,#3
              mov       s1,s0                   'Remember direction.
              mov       s2,#1                   'First turn is right for winding number.
              
:turn_lp      call      #move                   'Move to an adjacent pixel. Is it "blob"?
        if_nz jmp       #:inside                

              call      #move_back              '  No:  Move back to blob pixel.
              sub       s2,#1
              add       s0,#1                   '       Try next direction to the left of what we tried.
              and       s0,#3
              cmp       s0,s1 wz                '       Have we already tried this direction?
        if_nz jmp       #:turn_lp               '         No:  Go back and try it.

              call      #do_pix1                '         Yes: Stuck in a one-pixel blob. What are the odds?
              jmp       #:done                  '              Nowhere else to go, so quit.
        
:inside       test      ret_val,ret_val wz      '  Yes: Have we processed a blob pixel yet?
        if_nz jmp       #:not_first

              mov       dx,x                    '         No:  This is the first. Remember this position...
              mov       dy,y
              mov       difx,s0                 '              ...and how we got here.
              jmp       #:do_pix

:not_first    sub       dify,s2                 '         Yes: Add turn value to winding number.
              cmp       x,dx wz                 '              Did we start here...
         if_z cmp       y,dy wz
         if_z cmp       s0,difx wz              '              ...coming from the same direction?
         if_z jmp       #:done                  '                Yes: We're done.
         
:do_pix       call      #do_pix1                '  Process this pixel.
              jmp       #:follow_lp             '  Follow the wall some more.

:done         mov       dify,dify wc            '  Return perimeter with sign of winding number.
              negc      ret_val,ret_val
              
'_________________________________________________________

done          wrlong    ret_val,xy_ptr
              and       out_ptr,_0xffff wz
         if_z jmp       #all_done

              mov       s0,avgx
              mov       s1,ret_val
              call      #divide
              mov       avgx,s0
              mov       s0,avgy
              mov       s1,ret_val
              call      #divide
              mov       avgy,s0
              movd      :xfer_loop,#histo
              mov       s0,#22
              
:xfer_loop    wrword    0-0,out_ptr
              add       :xfer_loop,_0x200
              add       out_ptr,#2
              djnz      s0,#:xfer_loop              
                      
all_done      wrlong    zero, par
              jmp       #get_cmd

'_________________________________________________________ 
              
do_opr        call      #get_pix
              add       p1,#histo
              movd      :inc_histo,p1
              sub       p1,#histo
:inc_histo    add       0-0,#1
         if_z jmp       #do_pix0

do_pix1       add       ret_val,#1
              add       avgx,x
              add       avgy,y
              min       maxx,x
              max       minx,x
              min       maxy,y
              max       miny,y
              test      cmd,#WR1 wz
         if_z jmp       #_qonly
         
              mov       p1,replace1
              jmp       #_write

do_pix0       test      cmd,#WR0 wz
         if_z jmp       do_opr_ret
         
              mov       p1,replace0
                            
_write        test      y,#1 wc
              test      cmd,#EODIF wz
 if_nc_and_nz jmp       do_opr_ret
 
              andn      pixpair,#$0f
              or        pixpair,p1
              test      x,#1 wz
        if_nz rol       pixpair,#4
              wrbyte    pixpair,pix_addr
              cmp       ret_val,#1 wc
         if_c jmp       do_opr_ret

_qonly        test      cmd,#FIND_ONE wz
        if_nz jmp       #done
              
do_opr_ret
do_pix1_ret
do_pix0_ret   ret

'_________________________________________________________

move_back     xor       s0,#2
              call      #move
              xor       s0,#2
move_back_ret ret

move          and       s0,#3 wz
         if_z jmp       #move_rt
              cmp       s0,#1 wz
         if_z jmp       #move_up
              cmp       s0,#2 wz
         if_z jmp       #move_lf

move_dn       add       y,#1
              add       pix_addr,dwide
              jmp       #try_pix

move_rt       add       x,#1
              test      x,#1 wz
         if_z add       pix_addr,#1
              jmp       #try_pix

move_lf       sub       x,#1
              test      x,#1 wz
        if_nz sub       pix_addr,#1
              jmp       #try_pix

move_up       sub       y,#1
              sub       pix_addr,dwide
              
try_pix       test      x,#0 wz
              cmp       x,x0 wc
        if_nc cmp       x1,x wc
        if_nc cmp       y,y0 wc
        if_nc cmp       y1,y wc
         if_c jmp       try_pix_ret        

get_pix       call      #_get_pix
              test      cmd,#EODIF wc
        if_nc jmp       get_pix_ret

              mov       p0,pixpair
              mov       p3,p1
              test      y,#1 wc
              sumc      pix_addr,dwide
              call      #_get_pix
              sumnc     pix_addr,dwide
              sub       p1,p3
              abs       p1,p1
              call      #test_pix
              mov       pixpair,p0              
move_ret
move_rt_ret
move_lf_ret
move_up_ret
move_dn_ret         
try_pix_ret
get_pix_ret   ret

_get_pix      rdbyte    pixpair,pix_addr
              test      x,#1 wz
        if_nz ror       pixpair,#4
              mov       p1,pixpair
              and       p1,#$0f
test_pix      mov       p2,#1
              shl       p2,p1
              test      p2,mask wz
test_pix_ret
_get_pix_ret  ret

'_________________________________________________________

start_xy      mov       x,out_ptr
              shr       x,#24
              mov       y,out_ptr
              shr       y,#16
              and       y,#$ff
              cmp       x1,x0 wc
        if_nc min       x,x0
         if_c max       x,x0
         if_c min       x,x1
        if_nc max       x,x1
              cmp       y1,y0 wc
        if_nc min       y,y0
         if_c max       y,y0
         if_c min       y,y1
        if_nc max       y,y1
start_xy_ret  ret

'_________________________________________________________

pixaddr       mov       pix_addr,y
              mov       p2,#16
              shl       wide,#16
              shr       pix_addr,#1 wc
              
:mult_lp if_c add       pix_addr,wide wc
              rcr       pix_addr,#1 wc
              djnz      p2,#:mult_lp
              
              add       pix_addr,x
              shr       pix_addr,#1
              add       pix_addr,buf_addr
              shr       wide,#16
pixaddr_ret   ret

'_________________________________________________________

divide        shl       s1,#15
              mov       s2,#16

:loop         cmpsub    s0,s1 wc
              rcl       s0,#1
              djnz      s2,#:loop

              and       s0,_0xffff
divide_ret    ret

'_________________________________________________________

zero          long      0
_0x200        long      $200
_0xffff       long      $ffff
_command      long      0
_params0      long      0
_params1      long      0

cmd           res       1
xy_ptr        res       1
ptr_ptr       res       1

replace0      res       1
replace1      res       1
mask          res       1

x0            res       1
y0            res       1
x1            res       1
y1            res       1
x             res       1
y             res       1
dx            res       1
dy            res       1
dyaddr        res       1
dwide         res       1
difx          res       1
dify          res       1
countx        res       1
county        res       1

pix_addr      res       1

s0            res       1
s1            res       1
s2            res       1

pixpair       res       1
p0            res       1
p1            res       1
p2            res       1
p3            res       1

buf_addr      res       1
wide          res       1
high          res       1

ret_val       res       1
out_ptr       res       1

histo         res       16
minx          res       1
miny          res       1
maxx          res       1
maxy          res       1
avgx          res       1
avgy          res       1


License

┌──────────────────────────────────────────────────────────────────────────────────────┐
│                            TERMS OF USE: MIT License                                 │                                                            
├──────────────────────────────────────────────────────────────────────────────────────┤
│Permission is hereby granted, free of charge, to any person obtaining a copy of this  │
│software and associated documentation files (the "Software"), to deal in the Software │
│without restriction, including without limitation the rights to use, copy, modify,    │
│merge, publish, distribute, sublicense, and/or sell copies of the Software, and to    │
│permit persons to whom the Software is furnished to do so, subject to the following   │
│conditions:                                                                           │
│                                                                                      │
│The above copyright notice and this permission notice shall be included in all copies │
│or substantial portions of the Software.                                              │
│                                                                                      │
│THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,   │
│INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A         │
│PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT    │
│HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF  │
│CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE  │
│OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                                         │
└──────────────────────────────────────────────────────────────────────────────────────┘