// Copyright (c) 2021, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package colorspace
//go:generate core generate -add-types -gosl
import (
"cogentcore.org/core/math32"
)
//gosl:start
//////// CAT02 versions
// XYZToLMS_CAT02 converts XYZ to Long, Medium, Short cone-based responses,
// using the CAT02 transform from CIECAM02 color appearance model
// (MoroneyFairchildHuntEtAl02)
func XYZToLMS_CAT02(x, y, z float32, l, m, s *float32) {
*l = 0.7328*x + 0.4296*y + -0.1624*z
*m = -0.7036*x + 1.6975*y + 0.0061*z
*s = 0.0030*x + 0.0136*y + 0.9834*z
return
}
// SRGBLinToLMS_CAT02 converts sRGB linear to Long, Medium, Short
// cone-based responses, using the CAT02 transform from CIECAM02
// color appearance model (MoroneyFairchildHuntEtAl02)
// this is good for representing adaptation but NOT apparently
// good for representing appearances
func SRGBLinToLMS_CAT02(rl, gl, bl float32, l, m, s *float32) {
*l = 0.3904054*rl + 0.54994122*gl + 0.00892632*bl
*m = 0.0708416*rl + 0.96317176*gl + 0.00135775*bl
*s = 0.0491304*rl + 0.21556128*gl + 0.9450824*bl
return
}
// SRGBToLMS_CAT02 converts sRGB to Long, Medium, Short cone-based responses,
// using the CAT02 transform from CIECAM02 color appearance model
// (MoroneyFairchildHuntEtAl02)
func SRGBToLMS_CAT02(r, g, b float32, l, m, s *float32) {
var rl, gl, bl float32
SRGBToLinear(r, g, b, &rl, &gl, &bl)
SRGBLinToLMS_CAT02(rl, gl, bl, l, m, s)
return
}
/*
// #CAT_ColorSpace convert Long, Medium, Short cone-based responses to XYZ, using the CAT02 transform from CIECAM02 color appearance model (MoroneyFairchildHuntEtAl02)
func LMSToXYZ_CAT02(l, m, s float32) (x, y, z float32) {
x = 1.096124 * l + 0.4296f * Y + -0.1624f * Z;
y = -0.7036f * X + 1.6975f * Y + 0.0061f * Z;
z = 0.0030f * X + 0.0136f * Y + 0.9834 * Z;
}
*/
//////// HPE versions
// XYZToLMS_HPE convert XYZ to Long, Medium, Short cone-based responses,
// using the Hunt-Pointer-Estevez transform.
// This is closer to the actual response functions of the L,M,S cones apparently.
func XYZToLMS_HPE(x, y, z float32, l, m, s *float32) {
*l = 0.38971*x + 0.68898*y + -0.07868*z
*m = -0.22981*x + 1.18340*y + 0.04641*z
*s = z
}
// SRGBLinToLMS_HPE converts sRGB linear to Long, Medium, Short cone-based responses,
// using the Hunt-Pointer-Estevez transform.
// This is closer to the actual response functions of the L,M,S cones apparently.
func SRGBLinToLMS_HPE(rl, gl, bl float32, l, m, s *float32) {
*l = 0.30567503*rl + 0.62274014*gl + 0.04530167*bl
*m = 0.15771291*rl + 0.7697197*gl + 0.08807348*bl
*s = 0.0193*rl + 0.1192*gl + 0.9505*bl
}
// SRGBToLMS_HPE converts sRGB to Long, Medium, Short cone-based responses,
// using the Hunt-Pointer-Estevez transform.
// This is closer to the actual response functions of the L,M,S cones apparently.
func SRGBToLMS_HPE(r, g, b float32, l, m, s *float32) {
var rl, gl, bl float32
SRGBToLinear(r, g, b, &rl, &gl, &bl)
SRGBLinToLMS_HPE(rl, gl, bl, l, m, s)
}
/*
func LMStoXYZ_HPE(float& X, float& Y, float& Z,
L, M, S) {
X = 1.096124f * L + 0.4296f * Y + -0.1624f * Z;
Y = -0.7036f * X + 1.6975f * Y + 0.0061f * Z;
Z = 0.0030f * X + 0.0136f * Y + 0.9834 * Z;
}
// #CAT_ColorSpace convert Long, Medium, Short cone-based responses to XYZ, using the Hunt-Pointer-Estevez transform -- this is closer to the actual response functions of the L,M,S cones apparently
*/
// LuminanceAdaptation implements the luminance adaptation function
// equals 1 at background luminance of 200 so we generally ignore it..
// bgLum is background luminance -- 200 default.
func LuminanceAdaptation(bgLum float32) float32 {
lum5 := 5.0 * bgLum
k := 1.0 / (lum5 + 1)
k4 := k * k * k * k
k4m1 := 1 - k4
fl := 0.2*k4*lum5 + .1*k4m1*k4m1*math32.Pow(lum5, 1.0/3.0)
return fl
}
// ResponseCompression takes a 0-1 normalized LMS value
// and performs hyperbolic response compression.
// val must ALREADY have the luminance adaptation applied to it
// using the luminance adaptation function, which is 1 at a
// background luminance level of 200 = 2, so you can skip that
// step if you assume that level of background.
func ResponseCompression(val float32) float32 {
pval := math32.Pow(val, 0.42)
rc := 0.1 + 4.0*pval/(27.13+pval)
return rc
}
// LMSToComps converts Long, Medium, Short cone-based responses
// to components incl opponents: Red - Green (LvM) and Blue - Yellow (SvLM).
// Includes the separate components in these subtractions as well
// Uses the CIECAM02 color appearance model (MoroneyFairchildHuntEtAl02)
// https://en.wikipedia.org/wiki/CIECAM02
func LMSToComps(l, m, s float32, lc, mc, sc, lmc, lvm, svlm, grey *float32) {
lrc := ResponseCompression(l)
mrc := ResponseCompression(m)
src := ResponseCompression(s)
// subtract min and mult by 6 gets values roughly into 1-0 range for L,M
*lc = 6.0 * ((lrc + (1.0/11.0)*src) - 0.109091)
*mc = 6.0 * (((12.0 / 11.0) * mrc) - 0.109091)
*lvm = *lc - *mc // red-green subtracting "criterion for unique yellow"
*lmc = 6.0 * (((1.0 / 9.0) * (lrc + mrc)) - 0.0222222)
*sc = 6.0 * (((2.0 / 9.0) * src) - 0.0222222)
*svlm = *sc - *lmc // blue-yellow contrast
*grey = (1.0 / 0.431787) * (2.0*lrc + mrc + .05*src - 0.305)
// note: last term should be: 0.725 * (1/5)^-0.2 = grey background assumption (Yb/Yw = 1/5) = 1
return
}
//gosl:end
// Copyright (c) 2021, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package colorspace
import (
"cogentcore.org/core/math32/vecint"
"cogentcore.org/lab/tensor"
)
// MacbethColors returns standard test colors,
// as a list of 24 rgb 0..255 values.
func MacbethColors() []int {
sRGBvals := []int{115, 82, 68, // 'Dark Skin';
194, 150, 130, // 'Light Skin';
98, 122, 157, // 'Blue Sky';
87, 108, 67, // 'Foliage';
133, 128, 177, // 'Blue Flower';
103, 189, 170, // 'Bluish Green';
214, 126, 44, // 'Orange';
80, 91, 166, // 'Purple Red';
193, 90, 99, // 'Moderate Red';
94, 60, 108, // 'Purple';
157, 188, 64, // 'Yellow Green';
224, 163, 46, // 'Orange Yellow';
56, 61, 150, // 'Blue';
70, 148, 73, // 'Green';
175, 54, 60, // 'Red';
231, 199, 31, // 'Yellow';
187, 86, 149, // 'Magenta';
8, 133, 161, // 'Cyan';
255, 255, 255, // 'White';
200, 200, 200, // 'Neutral 8';
160, 160, 160, // 'Neutral 65';
122, 122, 121, // 'Neutral 5';
85, 85, 85, // 'Neutral 35';
52, 52, 52,
}
return sRGBvals
}
// MacbethFloats returns tensor of float32 values in 2D tensor
// [24 colors, 3 rgb].
func MacbethFloats() *tensor.Float32 {
clr := MacbethColors()
n := len(clr)
tsr := tensor.NewFloat32(len(clr)/3, 3)
for i := range n {
tsr.Set1D(float32(clr[i])/255, i)
}
return tsr
}
// MacbethImage sets the Macbeth standard color test image to given tensor
// with given size and border width around edges.
// if img == nil it is created, and size enforced.
func MacbethImage(img *tensor.Float32, width, height, bord int) {
clrs := MacbethFloats()
nsq := vecint.Vector2i{6, 4}
numsq := nsq.X * nsq.Y
sz := vecint.Vector2i{width + bord*2 + 8, height + bord*2 + 8}
bvec := vecint.Vector2i{bord, bord}
marg := vecint.Vector2i{8, 8}
upBord := sz.Sub(bvec).Sub(marg)
netSz := vecint.Vector2i{width, height}
sqSz := netSz.Div(nsq)
if img == nil {
img = &tensor.Float32{}
}
img.SetShapeSizes(3, sz.Y, sz.X)
ic := vecint.Vector2i{}
for ic.Y = bvec.Y; ic.Y < upBord.Y; ic.Y++ {
for ic.X = bvec.X; ic.X < upBord.X; ic.X++ {
nc := ic.Sub(bvec)
sqc := nc.Div(sqSz)
sqst := sqc.Mul(sqSz).Add(bvec)
ps := ic.Sub(sqst)
if ps.X > marg.X && ps.Y > marg.Y {
clri := (nsq.Y-1-sqc.Y)*nsq.X + sqc.X
if clri < numsq {
r := clrs.Value(clri, 0)
g := clrs.Value(clri, 1)
b := clrs.Value(clri, 2)
img.Set(r, 0, ic.Y, ic.X)
img.Set(g, 1, ic.Y, ic.X)
img.Set(b, 2, ic.Y, ic.X)
}
}
}
}
}
// Copyright (c) 2021, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package colorspace
import "cogentcore.org/core/math32"
//gosl:start
// SRGBToLinearComp converts an sRGB rgb component to linear space (removes gamma).
// Used in converting from sRGB to XYZ colors.
func SRGBToLinearComp(srgb float32) float32 {
if srgb <= 0.04045 {
return srgb / 12.92
}
return math32.Pow((srgb+0.055)/1.055, 2.4)
}
// SRGBFromLinearComp converts an sRGB rgb linear component
// to non-linear (gamma corrected) sRGB value
// Used in converting from XYZ to sRGB.
func SRGBFromLinearComp(lin float32) float32 {
if lin <= 0.0031308 {
return 12.92 * lin
}
return (1.055*math32.Pow(lin, 1/2.4) + 0.055)
}
// SRGBToLinear converts set of sRGB components to linear values,
// removing gamma correction.
func SRGBToLinear(r, g, b float32, rl, gl, bl *float32) {
*rl = SRGBToLinearComp(r)
*gl = SRGBToLinearComp(g)
*bl = SRGBToLinearComp(b)
}
// SRGBFromLinear converts set of sRGB components from linear values,
// adding gamma correction.
func SRGBFromLinear(rl, gl, bl float32, r, g, b *float32) {
*r = SRGBFromLinearComp(rl)
*g = SRGBFromLinearComp(gl)
*b = SRGBFromLinearComp(bl)
return
}
// SRGBToLMSAll converts sRGB to LMS components including opponents
// using the HPE cone values: Red - Green (LvM) and Blue - Yellow (SvLM).
// Includes the separate components in these subtractions as well.
// Uses the CIECAM02 color appearance model (MoroneyFairchildHuntEtAl02)
// https://en.wikipedia.org/wiki/CIECAM02
// using the Hunt-Pointer-Estevez transform.
func SRGBToLMSAll(r, g, b float32, lc, mc, sc, lmc, lvm, svlm, grey *float32) {
var l, m, s float32
SRGBToLMS_HPE(r, g, b, &l, &m, &s) // note: HPE
LMSToComps(l, m, s, lc, mc, sc, lmc, lvm, svlm, grey)
}
//gosl:end
// Copyright (c) 2021, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package colorspace
// SRGBLinToXYZ converts sRGB linear into XYZ CIE standard color space
func SRGBLinToXYZ(rl, gl, bl float32, x, y, z *float32) {
*x = 0.4124*rl + 0.3576*gl + 0.1805*bl
*y = 0.2126*rl + 0.7152*gl + 0.0722*bl
*z = 0.0193*rl + 0.1192*gl + 0.9505*bl
}
// XYZToSRGBLin converts XYZ CIE standard color space to sRGB linear
func XYZToSRGBLin(x, y, z float32, rl, gl, bl *float32) {
*rl = 3.2406*x + -1.5372*y + -0.4986*z
*gl = -0.9689*x + 1.8758*y + 0.0415*z
*bl = 0.0557*x + -0.2040*y + 1.0570*z
}
// SRGBToXYZ converts sRGB into XYZ CIE standard color space
func SRGBToXYZ(r, g, b float32, x, y, z *float32) {
var rl, gl, bl float32
SRGBToLinear(r, g, b, &rl, &gl, &bl)
SRGBLinToXYZ(rl, gl, bl, x, y, z)
}
// XYZToSRGB converts XYZ CIE standard color space into sRGB
func XYZToSRGB(x, y, z float32, r, g, b *float32) {
var rl, gl, bl float32
XYZToSRGBLin(x, y, z, &rl, &gl, &bl)
SRGBFromLinear(rl, bl, gl, r, g, b)
return
}
// #CAT_ColorSpace renormalize XZY values relative to the D65 outdoor white light values
func XYZRenormD65(x, y, z float32, xr, yr, zr *float32) {
*xr = x * (1 / 0.95047)
*zr = z * (1 / 1.08883)
*yr = y
return
}
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
package dog provides the Difference-of-Gaussians (DoG) filter for visual and other
forms of signal processing
*/
package dog
//go:generate core generate -add-types -gosl
import (
"cogentcore.org/core/math32"
"cogentcore.org/lab/table"
"cogentcore.org/lab/tensor"
)
// dog.Filter specifies a DoG Difference of Gaussians filter function.
// On = narrower on-center peak; Off = broader surround;
// Net = On - Off difference that is selectively activated by differential
// On vs. Off activity, and is silent for any consistent uniform field.
// Can also be used for color contrast filtering with same-sized On and Off
// Gaussians, applied to different color (RGB / LMS) channels.
type Filter struct {
// On is whether this filter is active.
On bool
// Gain is the overall gain multiplier applied after dog filtering.
// Only relevant if not using renormalization on the output
// (otherwize it just gets renormed away).
Gain float32 `default:"8"`
// OnGain applies only to the on component of filter,
// which is only relevant for color contrast DoG's.
OnGain float32 `default:"1"`
// Size of the overall filter, which is the number of pixels wide
// and tall for a square matrix used to encode the filter.
// The filter is centered within this square.
// Typically an even number, min effective size ~6.
Size int
// Spacing is how far apart to space the centers of the dog filters.
// 1 = every pixel, 2 = every other pixel, etc.
// high-res should be 1 or 2, lower res can be increments therefrom.
Spacing int
// OnSigma is the Gaussian sigma for the narrower On gaussian,
// in normalized units relative to Size.
OnSigma float32 `default:"0.125"`
// OffSigma is the Gaussian sigma for the wider Off gaussian,
// in normalized units relative to Size.
OffSigma float32 `default:"0.25"`
// CircleEdge cuts off the filter (to zero) outside a circle of diameter
// = Size. Makes the filter more radially symmetric.
CircleEdge bool `default:"true"`
}
func (gf *Filter) Defaults() {
gf.On = true
gf.Gain = 8
gf.OnGain = 1
gf.Size = 12
gf.Spacing = 4
gf.OnSigma = 0.125
gf.OffSigma = 0.25
gf.CircleEdge = true
}
func (gf *Filter) Update() {
}
func (gf *Filter) ShouldDisplay(field string) bool {
switch field {
case "On":
return true
default:
return gf.On
}
}
// SetSize sets the size and spacing -- these are the main params
// that need to be varied for standard V1 dogs.
func (gf *Filter) SetSize(sz, spc int) {
gf.Size = sz
gf.Spacing = spc
}
// GaussDenSigma returns gaussian density for given value and sigma
func GaussDenSigma(x, sig float32) float32 {
x /= sig
return 0.398942280 * math32.Exp(-0.5*x*x) / sig
}
// SetSameSigma sets the On and Off sigma to the same value,
// for e.g., color contrast filtering instead of spatial on/off filtering.
// A value of 0.5 is typically used, to obtain more spatial coverage for
// broader "blob" tuning instead of spatial filtering.
func (gf *Filter) SetSameSigma(sigma float32) {
gf.OnSigma = sigma
gf.OffSigma = sigma
}
// tsr.SetShapeSizes(int(FiltersN), gf.Size, gf.Size)
// ToTensor renders dog filter into the given tensor.Tensor, which has
// 3 dimensions: FilterNo, Y, X, where Y = X = Size.
// The specified list of filters is written in given order.
func (gf *Filter) ToTensor(tsr *tensor.Float32, filters ...Filters) {
ctr := 0.5 * float32(gf.Size-1)
radius := float32(gf.Size) * 0.5
gsOn := gf.OnSigma * float32(gf.Size)
gsOff := gf.OffSigma * float32(gf.Size)
var idxs [FiltersN]int
for i := range FiltersN {
idxs[i] = -1
}
for i, fl := range filters {
idxs[fl] = i
}
var posSum, negSum, onSum, offSum float32
for y := 0; y < gf.Size; y++ {
for x := 0; x < gf.Size; x++ {
xf := float32(x) - ctr
yf := float32(y) - ctr
dist := math32.Hypot(xf, yf)
var ong, offg float32
if !(gf.CircleEdge && (dist > radius)) {
ong = GaussDenSigma(dist, gsOn)
offg = GaussDenSigma(dist, gsOff)
}
net := ong - offg
if net > 0 {
posSum += net
} else if net < 0 {
negSum += -net
}
onSum += ong
offSum += offg
if fi := idxs[Net]; fi >= 0 {
tsr.Set(net, fi, y, x)
}
if fi := idxs[On]; fi >= 0 {
tsr.Set(ong, fi, y, x)
}
if fi := idxs[Off]; fi >= 0 {
tsr.Set(offg, fi, y, x)
}
}
}
// renorm each half, separate components
for y := 0; y < gf.Size; y++ {
for x := 0; x < gf.Size; x++ {
if fi := idxs[Net]; fi >= 0 {
val := tsr.Value(fi, y, x)
if val > 0 {
val /= posSum
} else if val < 0 {
val /= negSum
}
tsr.Set(val, fi, y, x)
}
if fi := idxs[On]; fi >= 0 {
on := tsr.Value(fi, y, x)
tsr.Set(on/onSum, fi, y, x)
}
if fi := idxs[Off]; fi >= 0 {
off := tsr.Value(fi, y, x)
tsr.Set(off/offSum, fi, y, x)
}
}
}
}
// ToTable renders filters into the given table.Table
// setting a column named Version and a column named Filter
// to the filter for that version (on, off, net)
// This is useful for display and validation purposes.
func (gf *Filter) ToTable(tab *table.Table) {
tab.AddStringColumn("Version")
tab.AddFloat32Column("Filter", gf.Size, gf.Size)
tab.SetNumRows(3)
cl := tab.Columns.Values[1].(*tensor.Float32)
gf.ToTensor(cl, On, Off, Net)
nm := tab.ColumnByIndex(0)
for fn := range FiltersN {
nm.SetString(fn.String(), int(fn))
}
}
// Filters is the type of filter.
type Filters int32 //enums:enum
const (
// On is the (by convention) smaller on-center peak filter.
On Filters = iota
// Off is the larger off-center surround filter.
Off
// Net is On - Off, separately normalized so that application to
// a uniform field results in a zero. This is for spatial contrast
// filtering.
Net
)
// Code generated by "core generate -add-types -gosl"; DO NOT EDIT.
package dog
import (
"cogentcore.org/core/enums"
)
var _FiltersValues = []Filters{0, 1, 2}
// FiltersN is the highest valid value for type Filters, plus one.
//
//gosl:start
const FiltersN Filters = 3
//gosl:end
var _FiltersValueMap = map[string]Filters{`On`: 0, `Off`: 1, `Net`: 2}
var _FiltersDescMap = map[Filters]string{0: ``, 1: ``, 2: ``}
var _FiltersMap = map[Filters]string{0: `On`, 1: `Off`, 2: `Net`}
// String returns the string representation of this Filters value.
func (i Filters) String() string { return enums.String(i, _FiltersMap) }
// SetString sets the Filters value from its string representation,
// and returns an error if the string is invalid.
func (i *Filters) SetString(s string) error {
return enums.SetString(i, s, _FiltersValueMap, "Filters")
}
// Int64 returns the Filters value as an int64.
func (i Filters) Int64() int64 { return int64(i) }
// SetInt64 sets the Filters value from an int64.
func (i *Filters) SetInt64(in int64) { *i = Filters(in) }
// Desc returns the description of the Filters value.
func (i Filters) Desc() string { return enums.Desc(i, _FiltersDescMap) }
// FiltersValues returns all possible values for the type Filters.
func FiltersValues() []Filters { return _FiltersValues }
// Values returns all possible values for the type Filters.
func (i Filters) Values() []enums.Enum { return enums.Values(_FiltersValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Filters) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Filters) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Filters") }
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
//go:generate core generate -add-types
import (
"fmt"
"image"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/base/timer"
"cogentcore.org/core/core"
"cogentcore.org/core/math32"
"cogentcore.org/core/tree"
_ "cogentcore.org/lab/gosl/slbool/slboolcore" // include to get gui views
"cogentcore.org/lab/table"
"cogentcore.org/lab/tensor"
"cogentcore.org/lab/tensorcore"
_ "cogentcore.org/lab/tensorcore" // include to get gui views
"github.com/anthonynsimon/bild/transform"
"github.com/emer/v1vision/dog"
"github.com/emer/v1vision/kwta"
"github.com/emer/v1vision/v1std"
"github.com/emer/v1vision/v1vision"
)
func main() {
vi := &Vis{}
vi.Defaults()
vi.Config()
vi.Filter()
vi.ConfigGUI()
}
// Vis encapsulates specific visual processing pipeline in
// use in a given case -- can add / modify this as needed
type Vis struct { //types:add
// GPU means use gpu.
GPU bool
// name of image file to operate on.
ImageFile core.Filename
// LGN DoG filter parameters.
DoG dog.Filter
// geometry of input, output.
Geom v1vision.Geom `edit:"-"`
// kwta parameters, providing more contrast across colors.
KWTA kwta.KWTA
// target image size to use -- images will be rescaled to this size.
ImageSize image.Point
// V1 is the V1Vision filter processing system.
V1 v1vision.V1Vision `display:"no-inline"`
// DoG filter table (view only).
DoGTab table.Table `display:"no-inline"`
// current input image.
Image image.Image `display:"-"`
// input image as tensor.
ImageTsr *tensor.Float32 `display:"no-inline"`
// input image as tensor: red, green components (L,M).
ImageRGTsr *tensor.Float32 `display:"no-inline"`
// input image as tensor: blue, yellow components (S, LM).
ImageBYTsr *tensor.Float32 `display:"no-inline"`
// Out has RG, BY value outputs in 0, 1 feature positions.
Out tensor.Float32 `display:"no-inline"`
// DoGColor is an encapsulated version of this functionality,
// which we test here for comparison.
DoGColor v1std.DoGColor
// StdImage manages images for DoGColor
StdImage v1std.Image
tabView *core.Tabs
outIdx int
}
func (vi *Vis) Defaults() {
vi.GPU = true
vi.ImageFile = core.Filename("macbeth.png") // GrangerRainbow.png")
vi.DoGTab.Init()
vi.DoG.Defaults()
sz := 12 // V1mF16 typically = 12, no border -- defaults
spc := 16 // note: not 4; broader blob tuning
vi.DoG.SetSize(sz, spc)
vi.DoG.SetSameSigma(0.5) // no spatial component, just pure contrast
vi.DoG.Gain = 8 // for stronger On tuning: 4.1,On=1.2, Off: 4.4,On=0.833
vi.DoG.OnGain = 1
vi.KWTA.Defaults()
vi.KWTA.Layer.On.SetBool(false) // non-spatial, mainly for differentiation within pools
vi.KWTA.Pool.Gi = 1.2
// note: first arg is border -- we are relying on Geom
// to set border to .5 * filter size
// any further border sizes on same image need to add Geom.FilterRt!
vi.Geom.Set(math32.Vec2i(0, 0), math32.Vec2i(spc, spc), math32.Vec2i(sz, sz))
// vi.ImageSize = image.Point{128, 128}
// vi.ImageSize = image.Point{256, 256}
vi.ImageSize = image.Point{512, 512} // default here
vi.Geom.SetImageSize(vi.ImageSize)
vi.DoGColor.Defaults()
vi.StdImage.Defaults()
vi.StdImage.Size = vi.ImageSize
}
// Config sets up the V1 processing pipeline.
func (vi *Vis) Config() {
vi.V1.Init(1)
*vi.V1.NewKWTAParams() = vi.KWTA
kwtaIdx := 0
img := vi.V1.NewImage(vi.Geom.In.V())
wrap := vi.V1.NewImage(vi.Geom.In.V())
lmsRG := vi.V1.NewImage(vi.Geom.In.V())
lmsBY := vi.V1.NewImage(vi.Geom.In.V())
vi.ImageTsr = vi.V1.Images.SubSpace(img, 0).(*tensor.Float32)
vi.V1.NewWrapImage(img, 3, wrap, int(vi.Geom.FilterRt.X), &vi.Geom)
vi.ImageRGTsr = vi.V1.Images.SubSpace(lmsRG, 0).(*tensor.Float32)
vi.ImageBYTsr = vi.V1.Images.SubSpace(lmsBY, 0).(*tensor.Float32)
vi.V1.NewLMSComponents(wrap, lmsRG, lmsBY, vi.DoG.Gain, &vi.Geom)
out := vi.V1.NewValues(int(vi.Geom.Out.Y), int(vi.Geom.Out.X), 2)
dogFt := vi.V1.NewDoGOnOff(&vi.DoG, &vi.Geom)
vi.V1.NewConvolveDiff(lmsRG, v1vision.Red, lmsRG, v1vision.Green, dogFt, 0, 1, out, 0, 1, vi.DoG.OnGain, &vi.Geom)
vi.V1.NewConvolveDiff(lmsBY, v1vision.Blue, lmsBY, v1vision.Yellow, dogFt, 0, 1, out, 1, 1, vi.DoG.OnGain, &vi.Geom)
vi.outIdx = out
if vi.KWTA.On.IsTrue() {
inh := vi.V1.NewInhibs(int(vi.Geom.Out.Y), int(vi.Geom.Out.X))
vi.outIdx = vi.V1.NewKWTA(out, 0, 2, kwtaIdx, inh, &vi.Geom)
}
vi.V1.SetAsCurrent()
if vi.GPU {
vi.V1.GPUInit()
}
vi.DoGColor.Config(1, vi.StdImage.Size)
}
// OpenImage opens given filename as current image Image
// and converts to a float32 tensor for processing
func (vi *Vis) OpenImage(filepath string) error { //types:add
var err error
vi.Image, _, err = imagex.Open(filepath)
if err != nil {
return errors.Log(err)
}
isz := vi.Image.Bounds().Size()
if isz != vi.ImageSize {
vi.Image = transform.Resize(vi.Image, vi.ImageSize.X, vi.ImageSize.Y, transform.Linear)
}
img := vi.V1.Images.SubSpace(0).(*tensor.Float32)
v1vision.RGBToTensor(img, int(vi.Geom.FilterRt.X), v1vision.BottomZero, vi.Image)
return nil
}
func (vi *Vis) getTsr(idx int, tsr *tensor.Float32, y, x int32) {
out := vi.V1.Values.SubSpace(idx, 0).(*tensor.Float32)
tsr.SetShapeSizes(int(y), int(x), 2, 2)
tensor.CopyFromLargerShape(tsr, out)
}
// Filter is overall method to run filters on current image file name
// loads the image from ImageFile and then runs filters.
func (vi *Vis) Filter() error { //types:add
vi.V1.SetAsCurrent()
v1vision.UseGPU = vi.GPU
err := vi.OpenImage(string(vi.ImageFile))
if err != nil {
return errors.Log(err)
}
tmr := timer.Time{}
tmr.Start()
for range 100 {
vi.V1.Run()
// vi.V1.Run(v1vision.ValuesVar)
// note: the read sync operation is currently very slow!
// this needs to be sped up significantly! hopefully with the
// fix that they are doing for the firefox issue.
// https://bugzilla.mozilla.org/show_bug.cgi?id=1870699
}
tmr.Stop()
fmt.Println("GPU:", vi.GPU, "Time:", tmr.Total)
// image = 512, 1000 itr: CPU = 7s, GPU = 1.6s
vi.V1.Run(v1vision.ValuesVar, v1vision.ImagesVar)
vi.getTsr(vi.outIdx, &vi.Out, vi.Geom.Out.Y, vi.Geom.Out.X)
vi.DoGColor.RunImages(&vi.StdImage, vi.Image)
if vi.tabView != nil {
vi.tabView.Update()
}
return nil
}
func (vi *Vis) ConfigGUI() *core.Body {
vi.DoG.ToTable(&vi.DoGTab) // note: view only, testing
tensorcore.AddGridStylerTo(vi.ImageTsr, func(s *tensorcore.GridStyle) {
s.Image = true
s.Range.SetMin(0)
})
tensorcore.AddGridStylerTo(vi.ImageRGTsr, func(s *tensorcore.GridStyle) {
s.Image = true
s.Range.SetMin(0)
})
tensorcore.AddGridStylerTo(vi.ImageBYTsr, func(s *tensorcore.GridStyle) {
s.Image = true
s.Range.SetMin(0)
})
tensorcore.AddGridStylerTo(&vi.DoGTab, func(s *tensorcore.GridStyle) {
s.Size.Min = 16
s.Range.Set(-0.1, 0.1)
})
b := core.NewBody("lgn_dog").SetTitle("Color DoG Filtering")
sp := core.NewSplits(b)
core.NewForm(sp).SetStruct(vi)
tb := core.NewTabs(sp)
vi.tabView = tb
tf, _ := tb.NewTab("Image")
tensorcore.NewTensorGrid(tf).SetTensor(vi.ImageTsr)
tf, _ = tb.NewTab("Image RG")
tensorcore.NewTensorGrid(tf).SetTensor(vi.ImageRGTsr)
tf, _ = tb.NewTab("Image BY")
tensorcore.NewTensorGrid(tf).SetTensor(vi.ImageBYTsr)
tf, _ = tb.NewTab("DoG")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.Out)
sp.SetSplits(.3, .7)
b.AddTopBar(func(bar *core.Frame) {
core.NewToolbar(bar).Maker(func(p *tree.Plan) {
tree.Add(p, func(w *core.FuncButton) { w.SetFunc(vi.Filter) })
})
})
b.RunMainWindow()
return b
}
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
//go:generate core generate -add-types
import (
"fmt"
"image"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/base/timer"
"cogentcore.org/core/core"
"cogentcore.org/core/math32"
"cogentcore.org/core/tree"
_ "cogentcore.org/lab/gosl/slbool/slboolcore" // include to get gui views
"cogentcore.org/lab/table"
"cogentcore.org/lab/tensor"
"cogentcore.org/lab/tensorcore"
_ "cogentcore.org/lab/tensorcore" // include to get gui views
"github.com/anthonynsimon/bild/transform"
"github.com/emer/v1vision/gabor"
"github.com/emer/v1vision/kwta"
"github.com/emer/v1vision/v1std"
"github.com/emer/v1vision/v1vision"
)
func main() {
vi := &Vis{}
vi.Defaults()
vi.Config()
vi.Filter()
vi.ConfigGUI()
}
// Vis encapsulates specific visual processing pipeline in
// use in a given case -- can add / modify this as needed
type Vis struct { //types:add
// GPU means use gpu.
GPU bool
// SplitColor records separate rows in V1c simple summary for each color.
// Otherwise records the max across all colors.
SplitColor bool
// ColorGain is an extra gain for color channels, which are lower contrast in general.
ColorGain float32 `default:"8"`
// name of image file to operate on
ImageFile core.Filename
// V1 simple gabor filter parameters
V1sGabor gabor.Filter
// geometry of input, output for V1 simple-cell processing
V1sGeom v1vision.Geom `edit:"-"`
// geometry of input, output for V1 complex-cell processing from V1s inputs.
V1cGeom v1vision.Geom `edit:"-"`
// neighborhood inhibition for V1s. Each unit gets inhibition from
// same feature in nearest orthogonal neighbors.
// Reduces redundancy of feature code.
V1sNeighInhib kwta.NeighInhib
// kwta parameters for V1s
V1sKWTA kwta.KWTA
// target image size to use -- images will be rescaled to this size
ImageSize image.Point
// V1 simple gabor filter tensor
V1sGaborTsr tensor.Float32 `display:"no-inline"`
// V1 simple gabor filter table (view only)
V1sGaborTable table.Table `display:"no-inline"`
// current input image
Image image.Image `display:"-"`
// V1 is the V1Vision filter processing system
V1 v1vision.V1Vision `display:"no-inline"`
// input image as tensor: original in full color.
ImageTsr *tensor.Float32 `display:"no-inline"`
// input image as tensor: visual-system Long, Medium, Short (~R,G,B) filtered
// with R = grey, G = Red - Green, B = Blue - Yellow opponents.
ImageLMSTsr *tensor.Float32 `display:"no-inline"`
// V1 simple gabor filter output, kwta output tensor, Grey = White-Black
V1sGreyTsr tensor.Float32 `display:"no-inline"`
// V1 simple gabor filter output, kwta output tensor, Red-Green
V1sRedGreenTsr tensor.Float32 `display:"no-inline"`
// V1 simple gabor filter output, kwta output tensor, Blue-Yellow
V1sBlueYellowTsr tensor.Float32 `display:"no-inline"`
// V1 simple gabor filter output, kwta output tensor,
// Max over Grey, RedGreen, BlueYellow
V1sMaxTsr tensor.Float32 `display:"no-inline"`
// V1 complex gabor filter output, max-polarity (angle-only) features tensor
V1cMaxPolTsr tensor.Float32 `display:"no-inline"`
// V1 complex gabor filter output, max-pooled 2x2 of MaxPol tensor
V1cPolPoolTsr tensor.Float32 `display:"no-inline"`
// V1 complex length sum filter output tensor
V1cLenSumTsr tensor.Float32 `display:"no-inline"`
// V1 complex end stop filter output tensor
V1cEndStopTsr tensor.Float32 `display:"no-inline"`
// Output has the resulting V1c filter outputs, pointing to Values4D in V1.
// Inner Y, X dimensions are 5 x 4, where the 4 are the gabor angles
// (0, 45, 90, 135) and the 5 are: 1 length-sum, 2 directions of end-stop,
// and 2 polarities of V1simple, or 6 with 3 from each LMS opponent channel
// for SplitColor (9 x 4)
V1AllTsr *tensor.Float32 `display:"no-inline"`
// V1cColor is an encapsulated version of this functionality,
// which we test here for comparison.
V1cColor v1std.V1cColor
// StdImage manages images for V1cColor
StdImage v1std.Image
// V1 complex gabor filter output, un-max-pooled 2x2 of V1cPool tensor
V1cUnPoolTsr tensor.Float32 `display:"no-inline"`
// input image reconstructed from V1s tensor
ImageFromV1sTsr tensor.Float32 `display:"no-inline"`
tabView *core.Tabs
v1sIdxs [3]int
v1sMaxIdx, v1cPoolIdx, v1cMaxPolIdx, v1cPolPoolIdx, v1cLenSumIdx, v1cEndStopIdx int
}
func (vi *Vis) Defaults() {
vi.GPU = false // true
vi.ColorGain = 8
vi.SplitColor = true
vi.ImageFile = core.Filename("car_004_00001.png")
vi.V1sGabor.Defaults()
sz := 12 // V1mF16 typically = 12, no border
spc := 4
vi.V1sGabor.SetSize(sz, spc)
vi.ImageSize = image.Point{128, 128}
// vi.ImageSize = image.Point{256, 256}
// vi.ImageSize = image.Point{512, 512}
// note: first arg is border -- we are relying on Geom
// to set border to .5 * filter size
vi.V1sGeom.SetImage(math32.Vec2i(0, 0), math32.Vec2i(spc, spc), math32.Vec2i(sz, sz), vi.ImageSize)
vi.V1sNeighInhib.Defaults()
vi.V1sKWTA.Defaults()
vi.V1cColor.Defaults()
vi.StdImage.Defaults()
}
// Config sets up the V1 processing pipeline.
func (vi *Vis) Config() {
vi.V1.Init(1)
*vi.V1.NewKWTAParams() = vi.V1sKWTA
kwtaIdx := 0
_ = kwtaIdx
img := vi.V1.NewImage(vi.V1sGeom.In.V())
wrap := vi.V1.NewImage(vi.V1sGeom.In.V())
lms := vi.V1.NewImage(vi.V1sGeom.In.V())
vi.ImageTsr = vi.V1.Images.SubSpace(img, 0).(*tensor.Float32)
vi.ImageLMSTsr = vi.V1.Images.SubSpace(lms, 0).(*tensor.Float32)
avgIdx := vi.V1.NewEdgeAvg(img, 3, int(vi.V1sGeom.Border.X), &vi.V1sGeom)
vi.V1.NewFadeImage(img, 3, wrap, int(vi.V1sGeom.Border.X), avgIdx, &vi.V1sGeom)
vi.V1.NewLMSOpponents(wrap, lms, vi.ColorGain, &vi.V1sGeom)
nang := vi.V1sGabor.NAngles
// V1s simple
ftyp := vi.V1.NewFilter(nang, vi.V1sGabor.Size, vi.V1sGabor.Size)
vi.V1.GaborToFilter(ftyp, &vi.V1sGabor)
inh := vi.V1.NewInhibs(int(vi.V1sGeom.Out.Y), int(vi.V1sGeom.Out.X))
lmsMap := [3]int{1, int(v1vision.RedGreen), int(v1vision.BlueYellow)}
for irgb := range 3 {
out := vi.V1.NewConvolveImage(lms, lmsMap[irgb], ftyp, nang, vi.V1sGabor.Gain, &vi.V1sGeom)
v1out := out
if vi.V1sKWTA.On.IsTrue() {
ninh := 0
if vi.V1sNeighInhib.On {
ninh = vi.V1.NewNeighInhib4(out, nang, vi.V1sNeighInhib.Gi, &vi.V1sGeom)
}
v1out = vi.V1.NewKWTA(out, ninh, nang, kwtaIdx, inh, &vi.V1sGeom)
}
vi.v1sIdxs[irgb] = v1out
}
mcout := vi.V1.NewValues(int(vi.V1sGeom.Out.Y), int(vi.V1sGeom.Out.X), nang)
vi.v1sMaxIdx = mcout
vi.V1.NewMaxCopy(vi.v1sIdxs[0], vi.v1sIdxs[1], mcout, nang, &vi.V1sGeom)
vi.V1.NewMaxCopy(vi.v1sIdxs[2], mcout, mcout, nang, &vi.V1sGeom)
// V1c complex
vi.V1cGeom.SetFilter(math32.Vec2i(0, 0), math32.Vec2i(2, 2), math32.Vec2i(2, 2), vi.V1sGeom.Out.V())
mpout := vi.V1.NewMaxPolarity(mcout, nang, &vi.V1sGeom)
vi.v1cMaxPolIdx = mpout
pmpout := vi.V1.NewMaxPool(mpout, 1, nang, &vi.V1cGeom)
vi.v1cPolPoolIdx = pmpout
lsout := vi.V1.NewLenSum4(pmpout, nang, &vi.V1cGeom)
vi.v1cLenSumIdx = lsout
esout := vi.V1.NewEndStop4(pmpout, lsout, nang, &vi.V1cGeom)
vi.v1cEndStopIdx = esout
// To4D
out4Rows := 5
if vi.SplitColor {
out4Rows = 9
}
out4 := vi.V1.NewValues4D(int(vi.V1cGeom.Out.Y), int(vi.V1cGeom.Out.X), out4Rows, nang)
vi.V1.NewTo4D(lsout, out4, 1, nang, 0, &vi.V1cGeom)
vi.V1.NewTo4D(esout, out4, 2, nang, 1, &vi.V1cGeom)
if vi.SplitColor {
poutg := vi.V1.NewMaxPool(vi.v1sIdxs[0], 2, nang, &vi.V1cGeom)
poutrg := vi.V1.NewMaxPool(vi.v1sIdxs[1], 2, nang, &vi.V1cGeom)
poutby := vi.V1.NewMaxPool(vi.v1sIdxs[2], 2, nang, &vi.V1cGeom)
vi.V1.NewTo4D(poutg, out4, 2, nang, 3, &vi.V1cGeom)
vi.V1.NewTo4D(poutrg, out4, 2, nang, 5, &vi.V1cGeom)
vi.V1.NewTo4D(poutby, out4, 2, nang, 7, &vi.V1cGeom)
} else {
pout := vi.V1.NewMaxPool(mcout, 2, nang, &vi.V1cGeom)
vi.V1.NewTo4D(pout, out4, 2, nang, 3, &vi.V1cGeom)
}
vi.V1.SetAsCurrent()
if vi.GPU {
vi.V1.GPUInit()
}
vi.V1cColor.Config(1, vi.StdImage.Size)
}
func (vi *Vis) getTsr(idx int, tsr *tensor.Float32, y, x, pol int32) {
out := vi.V1.Values.SubSpace(idx, 0).(*tensor.Float32)
tsr.SetShapeSizes(int(y), int(x), int(pol), vi.V1sGabor.NAngles)
tensor.CopyFromLargerShape(tsr, out)
}
func (vi *Vis) getTsrOpt(idx int, tsr *tensor.Float32, y, x, pol int32) {
if idx == 0 {
return
}
vi.getTsr(idx, tsr, y, x, pol)
}
// Filter is overall method to run filters on current image file name
// loads the image from ImageFile and then runs filters
func (vi *Vis) Filter() error { //types:add
// key point here: it is not re-sending the background guys
// so the other ones from v1c are interfering.
// need a set as current that also uploads backgrounds
vi.V1.SetAsCurrent()
v1vision.UseGPU = vi.GPU
err := vi.OpenImage(string(vi.ImageFile))
if err != nil {
return errors.Log(err)
}
tmr := timer.Time{}
tmr.Start()
for range 1 {
vi.V1.Run()
// vi.V1.Run(v1vision.Values4DVar) // this is sig slower due to sync issues.
// for timing test, run without sync and assume it gets sig better.
}
tmr.Stop()
fmt.Println("GPU:", vi.GPU, "Time:", tmr.Total)
// With 10 Iters on KWTA, on MacBookPro M3Pro:
// 128 image: CPU: 6.3s, GPU: 1.67s
// 256 image: CPU: 15.5s, GPU: 913ms
// 512 image: CPU: 49.2s, GPU: 3.5s (7.9s with Values4D sync)
// note: not sending image at start is the same!
vi.V1.Run(v1vision.Values4DVar, v1vision.ValuesVar, v1vision.ImagesVar)
vi.getTsr(vi.v1sIdxs[0], &vi.V1sGreyTsr, vi.V1sGeom.Out.Y, vi.V1sGeom.Out.X, 2)
vi.getTsrOpt(vi.v1sIdxs[1], &vi.V1sRedGreenTsr, vi.V1sGeom.Out.Y, vi.V1sGeom.Out.X, 2)
vi.getTsrOpt(vi.v1sIdxs[2], &vi.V1sBlueYellowTsr, vi.V1sGeom.Out.Y, vi.V1sGeom.Out.X, 2)
vi.getTsrOpt(vi.v1sMaxIdx, &vi.V1sMaxTsr, vi.V1sGeom.Out.Y, vi.V1sGeom.Out.X, 2)
vi.getTsrOpt(vi.v1cMaxPolIdx, &vi.V1cMaxPolTsr, vi.V1sGeom.Out.Y, vi.V1sGeom.Out.X, 1)
vi.getTsrOpt(vi.v1cPolPoolIdx, &vi.V1cPolPoolTsr, vi.V1cGeom.Out.Y, vi.V1cGeom.Out.X, 1)
vi.getTsrOpt(vi.v1cLenSumIdx, &vi.V1cLenSumTsr, vi.V1cGeom.Out.Y, vi.V1cGeom.Out.X, 1)
vi.getTsrOpt(vi.v1cEndStopIdx, &vi.V1cEndStopTsr, vi.V1cGeom.Out.Y, vi.V1cGeom.Out.X, 2)
vi.V1AllTsr = vi.V1.Values4D.SubSpace(0, 0).(*tensor.Float32)
// vi.ImageFromV1Simple()
vi.V1cColor.RunImages(&vi.StdImage, vi.Image)
if vi.tabView != nil {
vi.tabView.Update()
}
return nil
}
// OpenImage opens given filename as current image Image
// and converts to a float32 tensor for processing
func (vi *Vis) OpenImage(filepath string) error { //types:add
var err error
vi.Image, _, err = imagex.Open(filepath)
if err != nil {
return errors.Log(err)
}
isz := vi.Image.Bounds().Size()
if isz != vi.ImageSize {
vi.Image = transform.Resize(vi.Image, vi.ImageSize.X, vi.ImageSize.Y, transform.Linear)
}
img := vi.V1.Images.SubSpace(0).(*tensor.Float32)
v1vision.RGBToTensor(img, int(vi.V1sGeom.FilterRt.X), v1vision.BottomZero, vi.Image)
return nil
}
// ImageFromV1Simple reverses V1Simple Gabor filtering from V1s back to input image
func (vi *Vis) ImageFromV1Simple() {
// tensor.SetShapeFrom(&vi.V1sUnPoolTsr, &vi.V1sTsr)
// vi.V1sUnPoolTsr.SetZeros()
// tensor.SetShapeFrom(&vi.ImageFromV1sTsr, &vi.ImageTsr)
// vi.ImageFromV1sTsr.SetZeros()
// v1vision.UnPool(math32.Vec2(2, 2), math32.Vec2(2, 2), &vi.V1sUnPoolTsr, &vi.V1sPoolTsr, true)
// v1vision.Deconv(&vi.V1sGeom, &vi.V1sGaborTsr, &vi.ImageFromV1sTsr, &vi.V1sUnPoolTsr, vi.V1sGabor.Gain)
// stats.UnitNormOut(&vi.ImageFromV1sTsr, &vi.ImageFromV1sTsr)
}
func (vi *Vis) ConfigGUI() *core.Body {
vi.V1sGaborTable.Init()
vi.V1sGabor.ToTable(&vi.V1sGaborTable) // note: view only, testing
tensorcore.AddGridStylerTo(vi.ImageTsr, func(s *tensorcore.GridStyle) {
s.Image = true
s.Range.SetMin(0)
})
tensorcore.AddGridStylerTo(vi.ImageLMSTsr, func(s *tensorcore.GridStyle) {
s.Image = true
s.Range.SetMin(0)
})
tensorcore.AddGridStylerTo(&vi.ImageFromV1sTsr, func(s *tensorcore.GridStyle) {
s.Image = true
s.Range.SetMin(0)
})
tensorcore.AddGridStylerTo(&vi.V1sGaborTable, func(s *tensorcore.GridStyle) {
s.Size.Min = 16
s.Range.Set(-0.05, 0.05)
})
b := core.NewBody("v1gabor").SetTitle("V1 Gabor Filtering")
sp := core.NewSplits(b)
core.NewForm(sp).SetStruct(vi)
tb := core.NewTabs(sp)
vi.tabView = tb
tf, _ := tb.NewTab("Image")
tensorcore.NewTensorGrid(tf).SetTensor(vi.ImageTsr)
tf, _ = tb.NewTab("Image LMS")
tensorcore.NewTensorGrid(tf).SetTensor(vi.ImageLMSTsr)
tf, _ = tb.NewTab("V1All")
tensorcore.NewTensorGrid(tf).SetTensor(vi.V1AllTsr)
tf, _ = tb.NewTab("V1s Grey")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.V1sGreyTsr)
tf, _ = tb.NewTab("V1s Red - Green")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.V1sRedGreenTsr)
tf, _ = tb.NewTab("V1s Blue - Yellow")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.V1sBlueYellowTsr)
tf, _ = tb.NewTab("V1s Max")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.V1sMaxTsr)
tf, _ = tb.NewTab("V1cMaxPol")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.V1cMaxPolTsr)
tf, _ = tb.NewTab("V1cPolPool")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.V1cPolPoolTsr)
tf, _ = tb.NewTab("V1cLenSum")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.V1cLenSumTsr)
tf, _ = tb.NewTab("V1cEndStop")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.V1cEndStopTsr)
sp.SetSplits(.3, .7)
b.AddTopBar(func(bar *core.Frame) {
core.NewToolbar(bar).Maker(func(p *tree.Plan) {
tree.Add(p, func(w *core.FuncButton) { w.SetFunc(vi.Filter) })
})
})
b.RunMainWindow()
return b
}
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
//go:generate core generate -add-types
import (
"fmt"
"image"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/base/timer"
"cogentcore.org/core/core"
"cogentcore.org/core/math32"
"cogentcore.org/core/tree"
"cogentcore.org/lab/table"
"cogentcore.org/lab/tensor"
"cogentcore.org/lab/tensorcore"
_ "cogentcore.org/lab/tensorcore" // include to get gui views
"github.com/anthonynsimon/bild/transform"
"github.com/emer/v1vision/dog"
"github.com/emer/v1vision/v1std"
"github.com/emer/v1vision/v1vision"
)
func main() {
vi := &Vis{}
vi.Defaults()
vi.Config()
vi.Filter()
vi.ConfigGUI()
}
// Vis encapsulates specific visual processing pipeline in
// use in a given case -- can add / modify this as needed
type Vis struct { //types:add
// GPU means use gpu.
GPU bool
// name of image file to operate on
ImageFile core.Filename
// LGN DoG filter parameters
DoG dog.Filter
// geometry of input, output
Geom v1vision.Geom `edit:"-"`
// target image size to use -- images will be rescaled to this size
ImageSize image.Point
// V1 is the V1Vision filter processing system
V1 v1vision.V1Vision `display:"no-inline"`
// DoG filter table (view only)
DoGTab table.Table `display:"no-inline"`
// current input image
Image image.Image `display:"-"`
// input image as tensor
ImageTsr *tensor.Float32 `display:"no-inline"`
// DoG filter output tensor
OutTsr tensor.Float32 `display:"no-inline"`
// DoGGrey is an encapsulated version of this functionality,
// which we test here for comparison.
DoGGrey v1std.DoGGrey
// StdImage manages images for DoGGrey
StdImage v1std.Image
tabView *core.Tabs
}
func (vi *Vis) Defaults() {
vi.GPU = true
vi.ImageFile = core.Filename("side-tee-128.png")
vi.DoGTab.Init()
vi.DoG.Defaults()
sz := 12 // V1mF16 typically = 12, no border -- defaults
spc := 4
vi.DoG.SetSize(sz, spc)
// note: first arg is border -- we are relying on Geom
// to set border to .5 * filter size
// any further border sizes on same image need to add Geom.FilterRt!
vi.Geom.Set(math32.Vec2i(0, 0), math32.Vec2i(spc, spc), math32.Vec2i(sz, sz))
vi.ImageSize = image.Point{128, 128}
// vi.ImageSize = image.Point{256, 256}
// vi.ImageSize = image.Point{512, 512}
vi.Geom.SetImageSize(vi.ImageSize)
vi.DoGGrey.Defaults()
vi.StdImage.Defaults()
}
// Config sets up the V1 processing pipeline.
func (vi *Vis) Config() {
vi.V1.Init(1)
img := vi.V1.NewImage(vi.Geom.In.V())
wrap := vi.V1.NewImage(vi.Geom.In.V())
vi.ImageTsr = vi.V1.Images.SubSpace(img, 0).(*tensor.Float32)
vi.V1.NewWrapImage(img, 0, wrap, int(vi.Geom.FilterRt.X), &vi.Geom)
_, out := vi.V1.NewDoG(wrap, 0, &vi.DoG, &vi.Geom)
// _ = out
vi.V1.NewLogValues(out, out, 1, 1.0, &vi.Geom)
vi.V1.NewNormDiv(v1vision.MaxScalar, out, out, 1, &vi.Geom)
vi.V1.SetAsCurrent()
if vi.GPU {
vi.V1.GPUInit()
}
vi.DoGGrey.Config(1, vi.StdImage.Size)
}
// OpenImage opens given filename as current image Image
// and converts to a float32 tensor for processing
func (vi *Vis) OpenImage(filepath string) error { //types:add
var err error
vi.Image, _, err = imagex.Open(filepath)
if err != nil {
return errors.Log(err)
}
isz := vi.Image.Bounds().Size()
if isz != vi.ImageSize {
vi.Image = transform.Resize(vi.Image, vi.ImageSize.X, vi.ImageSize.Y, transform.Linear)
}
img := vi.V1.Images.SubSpace(0).(*tensor.Float32)
v1vision.RGBToGrey(img, int(vi.Geom.FilterRt.X), v1vision.BottomZero, vi.Image)
return nil
}
// Filter is overall method to run filters on current image file name
// loads the image from ImageFile and then runs filters.
func (vi *Vis) Filter() error { //types:add
vi.V1.SetAsCurrent()
v1vision.UseGPU = vi.GPU
err := vi.OpenImage(string(vi.ImageFile))
if err != nil {
return errors.Log(err)
}
tmr := timer.Time{}
tmr.Start()
for range 1000 {
vi.V1.Run()
// vi.V1.Run(v1vision.ValuesVar)
// note: the read sync operation is currently very slow!
// this needs to be sped up significantly! hopefully with the
// fix that they are doing for the firefox issue.
// https://bugzilla.mozilla.org/show_bug.cgi?id=1870699
}
tmr.Stop()
fmt.Println("GPU:", vi.GPU, "Time:", tmr.Total)
// image = 128: CPU = 333ms, GPU = 198ms
// image = 256: CPU = 873ms, GPU = 313ms
// image = 512: CPU = 2.6s, GPU = 878ms
vi.V1.Run(v1vision.ValuesVar, v1vision.ImagesVar)
out := vi.V1.Values.SubSpace(0).(*tensor.Float32)
vi.OutTsr.SetShapeSizes(out.ShapeSizes()...)
vi.OutTsr.CopyFrom(out)
vi.DoGGrey.RunImages(&vi.StdImage, vi.Image)
if vi.tabView != nil {
vi.tabView.Update()
}
return nil
}
func (vi *Vis) ConfigGUI() *core.Body {
vi.DoG.ToTable(&vi.DoGTab) // note: view only, testing
tensorcore.AddGridStylerTo(vi.ImageTsr, func(s *tensorcore.GridStyle) {
s.Image = true
s.Range.SetMin(0)
})
tensorcore.AddGridStylerTo(&vi.DoGTab, func(s *tensorcore.GridStyle) {
s.Size.Min = 16
s.Range.Set(-0.1, 0.1)
})
b := core.NewBody("lgn_dog").SetTitle("LGN DoG Filtering")
sp := core.NewSplits(b)
core.NewForm(sp).SetStruct(vi)
tb := core.NewTabs(sp)
vi.tabView = tb
tf, _ := tb.NewTab("Image")
tensorcore.NewTensorGrid(tf).SetTensor(vi.ImageTsr)
tf, _ = tb.NewTab("DoG")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.OutTsr)
sp.SetSplits(.3, .7)
b.AddTopBar(func(bar *core.Frame) {
core.NewToolbar(bar).Maker(func(p *tree.Plan) {
tree.Add(p, func(w *core.FuncButton) { w.SetFunc(vi.Filter) })
})
})
b.RunMainWindow()
return b
}
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
//go:generate core generate -add-types
import (
"fmt"
"image"
"time"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/math32"
"cogentcore.org/core/tree"
"cogentcore.org/lab/table"
"cogentcore.org/lab/tensor"
"cogentcore.org/lab/tensorcore"
_ "cogentcore.org/lab/tensorcore" // include to get gui views
"github.com/emer/emergent/v2/edge"
"github.com/emer/v1vision/dog"
"github.com/emer/v1vision/motion"
"github.com/emer/v1vision/v1std"
"github.com/emer/v1vision/v1vision"
)
func main() {
vi := &Vis{}
vi.Defaults()
vi.Config()
vi.RenderFrames()
vi.ConfigGUI()
}
// Vis encapsulates specific visual processing pipeline in
// use in a given case -- can add / modify this as needed
type Vis struct { //types:add
// GPU means use gpu
GPU bool
// NFrames is the number of frames to render per trial.
NFrames int
// FrameDelay is time per frame for display updating.
FrameDelay time.Duration
// target image size to use.
ImageSize image.Point
// Bar is the size of the moving bar.
Bar image.Point
// Velocity is the motion direction vector.
Velocity math32.Vector2
// Start is the starting position.
Start math32.Vector2
// Pos is current position
Pos math32.Vector2 `edit:"-"`
// Motion filter parameters.
Motion motion.Params
// LGN DoG filter parameters
DoG dog.Filter
// geometry of input, output
Geom v1vision.Geom `edit:"-"`
// V1 is the V1Vision filter processing system
V1 v1vision.V1Vision `display:"no-inline"`
// input image as tensor
ImageTsr *tensor.Float32 `display:"no-inline"`
// DoG filter table (view only)
DoGTab table.Table `display:"no-inline"`
// DoG filter output tensor
DoGOut tensor.Float32 `display:"no-inline"`
// Fast motion integration tensor
Fast tensor.Float32 `display:"no-inline"`
// Slow motion integration tensor
Slow tensor.Float32 `display:"no-inline"`
// Star motion output tensor
Star tensor.Float32 `display:"no-inline"`
// FullField integrated output
FullField tensor.Float32 `display:"no-inline"`
// MotionDoG is a self-contained version of motion dog filtering.
MotionDoG v1std.MotionDoG
fastIdx, starIdx int
tabView *core.Tabs
}
func (vi *Vis) Defaults() {
vi.GPU = true
vi.NFrames = 16
vi.FrameDelay = 200 * time.Millisecond
vi.ImageSize = image.Point{64, 64}
vi.Bar = image.Point{8, 16}
vi.Velocity = math32.Vector2{1, 0}
vi.Start = math32.Vector2{8, 8}
vi.DoGTab.Init()
vi.Motion.Defaults()
vi.DoG.Defaults()
sz := 12 // V1mF16 typically = 12, no border
spc := 4
vi.DoG.SetSize(sz, spc)
vi.Geom.Set(math32.Vec2i(0, 0), math32.Vec2i(spc, spc), math32.Vec2i(sz, sz))
vi.Geom.SetImageSize(vi.ImageSize)
vi.MotionDoG.Defaults()
// vi.MotionDoG.GPU = false
}
func (vi *Vis) Config() {
fn := 1 // number of filters in DoG
_ = fn
vi.V1.Init(1)
img := vi.V1.NewImage(vi.Geom.In.V())
vi.ImageTsr = vi.V1.Images.SubSpace(0, 0).(*tensor.Float32)
_, out := vi.V1.NewDoG(img, 0, &vi.DoG, &vi.Geom)
vi.V1.NewLogValues(out, out, fn, 1.0, &vi.Geom)
vi.V1.NewNormDiv(v1vision.MaxScalar, out, out, fn, &vi.Geom)
vi.Motion.DoGSumScalarIndex = vi.V1.NewAggScalar(v1vision.SumScalar, out, fn, &vi.Geom)
vi.fastIdx = vi.V1.NewMotionIntegrate(out, fn, vi.Motion.FastTau, vi.Motion.SlowTau, &vi.Geom)
vi.starIdx = vi.V1.NewMotionStar(vi.fastIdx, fn, vi.Motion.Gain, &vi.Geom)
vi.Motion.FFScalarIndex = vi.V1.NewMotionFullField(vi.starIdx, fn, &vi.Geom)
vi.V1.SetAsCurrent()
if vi.GPU {
vi.V1.GPUInit()
}
vi.MotionDoG.Config(1, vi.ImageSize)
}
// RenderFrames renders the frames
func (vi *Vis) RenderFrames() { //types:add
vi.V1.ZeroValues()
vi.Motion.NormInteg = 0
vi.Pos = vi.Start
for i := range vi.NFrames {
vi.RenderFrame()
vi.Pos = vi.Pos.Add(vi.Velocity)
vi.Filter()
if vi.tabView != nil {
vi.tabView.AsyncLock()
vi.tabView.Update()
vi.tabView.AsyncUnlock()
fmt.Printf("%d\tL: %7.4g\tR: %7.4g\tB: %7.4g\tT: %7.4g\tN: %7.4g\n", i, vi.FullField.Value1D(0), vi.FullField.Value1D(1), vi.FullField.Value1D(2), vi.FullField.Value1D(3), vi.Motion.NormInteg)
time.Sleep(vi.FrameDelay)
}
}
}
// RenderFrame renders a frame
func (vi *Vis) RenderFrame() {
pad := vi.Geom.Border.V()
tensor.SetAllFloat64(vi.ImageTsr, 0)
for y := range vi.Bar.Y {
py := int(math32.Round(vi.Pos.Y))
yp, _ := edge.Edge(y+py, vi.ImageSize.Y, true)
for x := range vi.Bar.X {
px := int(math32.Round(vi.Pos.X))
xp, _ := edge.Edge(x+px, vi.ImageSize.X, true)
vi.ImageTsr.Set(1, 0, int(pad.Y)+yp, int(pad.X)+xp)
}
}
}
// Filter runs the filters on current image.
func (vi *Vis) Filter() error { //types:add
v1vision.UseGPU = vi.GPU
vi.V1.SetAsCurrent()
// note: if switching between different gpu, then zero values get copied
// up to GPU on SetAsCurrent, so need ValuesVar to synchronize.
vi.V1.Run(v1vision.ScalarsVar, v1vision.ValuesVar, v1vision.ImagesVar)
// vi.V1.Run(v1vision.ScalarsVar) // minimal fastest case
vi.Motion.FullFieldInteg(1, vi.V1.Scalars, &vi.FullField)
out := vi.V1.Values.SubSpace(0, 0).(*tensor.Float32)
vi.DoGOut.SetShapeSizes(int(vi.Geom.Out.Y), int(vi.Geom.Out.X), 2, 1)
tensor.CopyFromLargerShape(&vi.DoGOut, out)
fast := vi.V1.Values.SubSpace(vi.fastIdx, 0).(*tensor.Float32)
vi.Fast.SetShapeSizes(int(vi.Geom.Out.Y), int(vi.Geom.Out.X), 2, 1)
tensor.CopyFromLargerShape(&vi.Fast, fast)
slow := vi.V1.Values.SubSpace(vi.fastIdx+1, 0).(*tensor.Float32)
vi.Slow.SetShapeSizes(int(vi.Geom.Out.Y), int(vi.Geom.Out.X), 2, 1)
tensor.CopyFromLargerShape(&vi.Slow, slow)
star := vi.V1.Values.SubSpace(vi.starIdx, 0).(*tensor.Float32)
vi.Star.SetShapeSizes(int(vi.Geom.Out.Y-1), int(vi.Geom.Out.X-1), 2, 4)
tensor.CopyFromLargerShape(&vi.Star, star)
vi.MotionDoG.RunTensor(vi.V1.Images.SubSpace(0).(*tensor.Float32))
return nil
}
func (vi *Vis) ConfigGUI() *core.Body {
vi.DoG.ToTable(&vi.DoGTab) // note: view only, testing
tensorcore.AddGridStylerTo(&vi.ImageTsr, func(s *tensorcore.GridStyle) {
s.Image = true
s.Range.SetMin(0)
})
tensorcore.AddGridStylerTo(&vi.DoGTab, func(s *tensorcore.GridStyle) {
s.Size.Min = 16
s.Range.Set(-0.1, 0.1)
})
tensorcore.AddGridStylerTo(&vi.FullField, func(s *tensorcore.GridStyle) {
s.Range.SetMin(0)
s.Range.FixMax = false
})
b := core.NewBody("lgn_dog").SetTitle("Motion DoG Filtering")
sp := core.NewSplits(b)
core.NewForm(sp).SetStruct(vi)
tb := core.NewTabs(sp)
vi.tabView = tb
tf, _ := tb.NewTab("Star")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.Star)
tf, _ = tb.NewTab("Full Field")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.FullField)
tf, _ = tb.NewTab("Fast")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.Fast)
tf, _ = tb.NewTab("Slow")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.Slow)
tf, _ = tb.NewTab("DoG")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.DoGOut)
tf, _ = tb.NewTab("Image")
tensorcore.NewTensorGrid(tf).SetTensor(vi.ImageTsr)
sp.SetSplits(.3, .7)
b.AddTopBar(func(bar *core.Frame) {
core.NewToolbar(bar).Maker(func(p *tree.Plan) {
tree.Add(p, func(w *core.Button) {
w.SetText("Run").SetIcon(icons.PlayArrow)
w.OnClick(func(e events.Event) {
go vi.RenderFrames()
})
})
})
})
b.RunMainWindow()
return b
}
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
//go:generate core generate -add-types
import (
"fmt"
"cogentcore.org/core/base/timer"
"cogentcore.org/core/core"
"cogentcore.org/core/tree"
_ "cogentcore.org/lab/gosl/slbool/slboolcore" // include to get gui views
"cogentcore.org/lab/tensorcore"
_ "cogentcore.org/lab/tensorcore" // include to get gui views
"github.com/emer/v1vision/v1std"
)
func main() {
vi := &Vis{}
vi.Defaults()
vi.Config(1)
vi.Filter()
vi.ConfigGUI()
}
type Vis struct { //types:add
v1std.V1cMulti
// ImageFile is the name of image file to operate on.
ImageFile core.Filename
tabView *core.Tabs
}
func (vi *Vis) Defaults() {
vi.V1cMulti.Defaults()
// vi.GPU = false
vi.StdLowMed16DegZoom1()
vi.ImageFile = core.Filename("car_004_00001.png")
}
// Filter is overall method to run filters on current image file name
// loads the image from ImageFile and then runs filters
func (vi *Vis) Filter() error { //types:add
err := vi.Image.OpenImagesResize(string(vi.ImageFile))
if err != nil {
return err
}
tmr := timer.Time{}
tmr.Start()
for range 1 {
vi.RunImages(vi.Image.Images...)
}
tmr.Stop()
fmt.Println("GPU:", vi.GPU, "Time:", tmr.Total)
// 100: GPU: 1.17s, CPU: 2s (full transfers)
if vi.tabView != nil {
vi.tabView.Update()
}
return nil
}
func (vi *Vis) ConfigGUI() *core.Body {
b := core.NewBody("v1gabor").SetTitle("V1 Gabor Filtering")
sp := core.NewSplits(b)
core.NewForm(sp).SetStruct(vi)
tb := core.NewTabs(sp)
vi.tabView = tb
tf, _ := tb.NewTab("Image")
img := vi.Image.Tsr.SubSpace(0)
tensorcore.AddGridStylerTo(img, func(s *tensorcore.GridStyle) {
s.Image = true
s.Range.SetMin(0)
})
tensorcore.NewTensorGrid(tf).SetTensor(img)
for _, vp := range vi.V1cParams {
tf, _ = tb.NewTab("V1c " + vp.Name)
tensorcore.NewTensorGrid(tf).SetTensor(&vp.Output)
}
for _, vp := range vi.DoGParams {
tf, _ = tb.NewTab("DoG " + vp.Name)
tensorcore.NewTensorGrid(tf).SetTensor(&vp.Output)
}
sp.SetSplits(.3, .7)
b.AddTopBar(func(bar *core.Frame) {
core.NewToolbar(bar).Maker(func(p *tree.Plan) {
tree.Add(p, func(w *core.FuncButton) { w.SetFunc(vi.Filter) })
})
})
b.RunMainWindow()
return b
}
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
//go:generate core generate -add-types
import (
"fmt"
"image"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/base/timer"
"cogentcore.org/core/core"
"cogentcore.org/core/math32"
"cogentcore.org/core/tree"
_ "cogentcore.org/lab/gosl/slbool/slboolcore" // include to get gui views
"cogentcore.org/lab/table"
"cogentcore.org/lab/tensor"
"cogentcore.org/lab/tensorcore"
_ "cogentcore.org/lab/tensorcore" // include to get gui views
"github.com/anthonynsimon/bild/transform"
"github.com/emer/v1vision/gabor"
"github.com/emer/v1vision/kwta"
"github.com/emer/v1vision/v1std"
"github.com/emer/v1vision/v1vision"
)
func main() {
vi := &Vis{}
vi.Defaults()
vi.Config()
vi.Filter()
vi.ConfigGUI()
}
// Vis encapsulates specific visual processing pipeline in
// use in a given case -- can add / modify this as needed
type Vis struct { //types:add
// GPU means use gpu.
GPU bool
// name of image file to operate on
ImageFile core.Filename
// V1 simple gabor filter parameters
V1sGabor gabor.Filter
// geometry of input, output for V1 simple-cell processing
V1sGeom v1vision.Geom `edit:"-"`
// geometry of input, output for V1 complex-cell processing from V1s inputs.
V1cGeom v1vision.Geom `edit:"-"`
// neighborhood inhibition for V1s. Each unit gets inhibition from
// same feature in nearest orthogonal neighbors.
// Reduces redundancy of feature code.
V1sNeighInhib kwta.NeighInhib
// kwta parameters for V1s
V1sKWTA kwta.KWTA
// target image size to use -- images will be rescaled to this size
ImageSize image.Point
// V1 simple gabor filter tensor
V1sGaborTsr tensor.Float32 `display:"no-inline"`
// V1 simple gabor filter table (view only)
V1sGaborTable table.Table `display:"no-inline"`
// current input image
Image image.Image `display:"-"`
// V1 is the V1Vision filter processing system
V1 v1vision.V1Vision `display:"no-inline"`
// input image as tensor
ImageTsr *tensor.Float32 `display:"no-inline"`
// V1 simple gabor filter output tensor
V1sTsr tensor.Float32 `display:"no-inline"`
// V1 simple gabor filter output, kwta output tensor
V1sKwtaTsr tensor.Float32 `display:"no-inline"`
// V1 complex gabor filter output, max-pooled 2x2 of V1cKwta tensor
V1cPoolTsr tensor.Float32 `display:"no-inline"`
// V1 complex gabor filter output, max-polarity (angle-only) features tensor
V1cMaxPolTsr tensor.Float32 `display:"no-inline"`
// V1 complex gabor filter output, max-pooled 2x2 of MaxPol tensor
V1cPolPoolTsr tensor.Float32 `display:"no-inline"`
// V1 complex length sum filter output tensor
V1cLenSumTsr tensor.Float32 `display:"no-inline"`
// V1 complex end stop filter output tensor
V1cEndStopTsr tensor.Float32 `display:"no-inline"`
// Combined V1 output 4D tensor with 5 inner rows:
// 1 length-sum, 2 directions of end-stop, and 2 polarities of
// V1simple from V1cPoolTsr.
V1AllTsr *tensor.Float32 `display:"no-inline"`
// V1cGrey is an encapsulated version of this functionality,
// which we test here for comparison.
V1cGrey v1std.V1cGrey
// StdImage manages images for V1cGrey
StdImage v1std.Image
// V1 complex gabor filter output, un-max-pooled 2x2 of V1cPool tensor
V1cUnPoolTsr tensor.Float32 `display:"no-inline"`
// input image reconstructed from V1s tensor
ImageFromV1sTsr tensor.Float32 `display:"no-inline"`
tabView *core.Tabs
v1sOutIdx, v1sKwtaIdx int
v1cPoolIdx, v1cMaxPolIdx, v1cPolPoolIdx, v1cLenSumIdx, v1cEndStopIdx int
}
func (vi *Vis) Defaults() {
vi.GPU = true
vi.ImageFile = core.Filename("side-tee-128.png")
vi.V1sGabor.Defaults()
sz := 12 // V1mF16 typically = 12, no border
spc := 4
vi.V1sGabor.SetSize(sz, spc)
vi.ImageSize = image.Point{128, 128}
// vi.ImageSize = image.Point{256, 256}
// vi.ImageSize = image.Point{512, 512}
// note: first arg is border -- we are relying on Geom
// to set border to .5 * filter size
vi.V1sGeom.SetImage(math32.Vec2i(0, 0), math32.Vec2i(spc, spc), math32.Vec2i(sz, sz), vi.ImageSize)
vi.V1sNeighInhib.Defaults()
vi.V1sKWTA.Defaults()
// vi.V1sKWTA.Layer.On.SetBool(true)
// vi.V1sKWTA.Pool.On.SetBool(false)
vi.V1cGrey.Defaults()
vi.StdImage.Defaults()
}
// Config sets up the V1 processing pipeline.
func (vi *Vis) Config() {
vi.V1.Init(1)
*vi.V1.NewKWTAParams() = vi.V1sKWTA
kwtaIdx := 0
img := vi.V1.NewImage(vi.V1sGeom.In.V())
wrap := vi.V1.NewImage(vi.V1sGeom.In.V())
vi.ImageTsr = vi.V1.Images.SubSpace(0, 0).(*tensor.Float32)
vi.V1.NewWrapImage(img, 0, wrap, int(vi.V1sGeom.FilterRt.X), &vi.V1sGeom)
nang := vi.V1sGabor.NAngles
// V1s simple
_, out := vi.V1.NewGabor(wrap, 0, &vi.V1sGabor, &vi.V1sGeom)
v1out := out
vi.v1sOutIdx = out
if vi.V1sKWTA.On.IsTrue() {
ninh := 0
if vi.V1sNeighInhib.On {
ninh = vi.V1.NewNeighInhib4(out, nang, vi.V1sNeighInhib.Gi, &vi.V1sGeom)
}
inh := vi.V1.NewInhibs(int(vi.V1sGeom.Out.Y), int(vi.V1sGeom.Out.X))
v1out = vi.V1.NewKWTA(out, ninh, nang, kwtaIdx, inh, &vi.V1sGeom)
vi.v1sKwtaIdx = v1out
}
// V1c complex
vi.V1cGeom.SetFilter(math32.Vec2i(0, 0), math32.Vec2i(2, 2), math32.Vec2i(2, 2), vi.V1sGeom.Out.V())
pout := vi.V1.NewMaxPool(v1out, 2, nang, &vi.V1cGeom)
vi.v1cPoolIdx = pout
mpout := vi.V1.NewMaxPolarity(v1out, nang, &vi.V1sGeom)
vi.v1cMaxPolIdx = mpout
pmpout := vi.V1.NewMaxPool(mpout, 1, nang, &vi.V1cGeom)
vi.v1cPolPoolIdx = pmpout
lsout := vi.V1.NewLenSum4(pmpout, nang, &vi.V1cGeom)
vi.v1cLenSumIdx = lsout
esout := vi.V1.NewEndStop4(pmpout, lsout, nang, &vi.V1cGeom)
vi.v1cEndStopIdx = esout
// To4D
out4 := vi.V1.NewValues4D(int(vi.V1cGeom.Out.Y), int(vi.V1cGeom.Out.X), 5, nang)
vi.V1.NewTo4D(lsout, out4, 1, nang, 0, &vi.V1cGeom)
vi.V1.NewTo4D(esout, out4, 2, nang, 1, &vi.V1cGeom)
vi.V1.NewTo4D(pout, out4, 2, nang, 3, &vi.V1cGeom)
vi.V1.SetAsCurrent()
if vi.GPU {
vi.V1.GPUInit()
}
vi.V1cGrey.Config(1, vi.StdImage.Size)
}
func (vi *Vis) getTsr(idx int, tsr *tensor.Float32, y, x, pol int32) {
out := vi.V1.Values.SubSpace(idx, 0).(*tensor.Float32)
tsr.SetShapeSizes(int(y), int(x), int(pol), vi.V1sGabor.NAngles)
tensor.CopyFromLargerShape(tsr, out)
}
func (vi *Vis) getTsrOpt(idx int, tsr *tensor.Float32, y, x, pol int32) {
if idx == 0 {
return
}
vi.getTsr(idx, tsr, y, x, pol)
}
// Filter is overall method to run filters on current image file name
// loads the image from ImageFile and then runs filters
func (vi *Vis) Filter() error { //types:add
vi.V1.SetAsCurrent()
v1vision.UseGPU = vi.GPU
err := vi.OpenImage(string(vi.ImageFile))
if err != nil {
return errors.Log(err)
}
tmr := timer.Time{}
tmr.Start()
for range 1000 {
vi.V1.Run()
// vi.V1.Run(v1vision.Values4DVar) // this is sig slower due to sync issues.
// for timing test, run without sync and assume it gets sig better.
}
tmr.Stop()
fmt.Println("GPU:", vi.GPU, "Time:", tmr.Total)
// With 10 Iters on KWTA, on MacBookPro M3Pro:
// 128 image: Orig: 2.03, CPU: 2s, GPU: 870ms
// 256 image: CPU: 4.7s, GPU: 913ms
// 512 image: CPU: 14.1s, GPU: 1.23s (7.6s with Values4D sync)
// note: not sending image at start is the same!
vi.V1.Run(v1vision.Values4DVar, v1vision.ValuesVar, v1vision.ImagesVar)
vi.getTsr(vi.v1sOutIdx, &vi.V1sTsr, vi.V1sGeom.Out.Y, vi.V1sGeom.Out.X, 2)
vi.getTsrOpt(vi.v1sKwtaIdx, &vi.V1sKwtaTsr, vi.V1sGeom.Out.Y, vi.V1sGeom.Out.X, 2)
vi.getTsrOpt(vi.v1cPoolIdx, &vi.V1cPoolTsr, vi.V1cGeom.Out.Y, vi.V1cGeom.Out.X, 2)
vi.getTsrOpt(vi.v1cMaxPolIdx, &vi.V1cMaxPolTsr, vi.V1sGeom.Out.Y, vi.V1sGeom.Out.X, 1)
vi.getTsrOpt(vi.v1cPolPoolIdx, &vi.V1cPolPoolTsr, vi.V1cGeom.Out.Y, vi.V1cGeom.Out.X, 1)
vi.getTsrOpt(vi.v1cLenSumIdx, &vi.V1cLenSumTsr, vi.V1cGeom.Out.Y, vi.V1cGeom.Out.X, 1)
vi.getTsrOpt(vi.v1cEndStopIdx, &vi.V1cEndStopTsr, vi.V1cGeom.Out.Y, vi.V1cGeom.Out.X, 2)
vi.V1AllTsr = vi.V1.Values4D.SubSpace(0, 0).(*tensor.Float32)
// vi.ImageFromV1Simple()
vi.V1cGrey.RunImages(&vi.StdImage, vi.Image)
if vi.tabView != nil {
vi.tabView.Update()
}
return nil
}
// OpenImage opens given filename as current image Image
// and converts to a float32 tensor for processing
func (vi *Vis) OpenImage(filepath string) error { //types:add
var err error
vi.Image, _, err = imagex.Open(filepath)
if err != nil {
return errors.Log(err)
}
isz := vi.Image.Bounds().Size()
if isz != vi.ImageSize {
vi.Image = transform.Resize(vi.Image, vi.ImageSize.X, vi.ImageSize.Y, transform.Linear)
}
img := vi.V1.Images.SubSpace(0).(*tensor.Float32)
v1vision.RGBToGrey(img, int(vi.V1sGeom.FilterRt.X), v1vision.BottomZero, vi.Image)
return nil
}
// ImageFromV1Simple reverses V1Simple Gabor filtering from V1s back to input image
func (vi *Vis) ImageFromV1Simple() {
// tensor.SetShapeFrom(&vi.V1sUnPoolTsr, &vi.V1sTsr)
// vi.V1sUnPoolTsr.SetZeros()
// tensor.SetShapeFrom(&vi.ImageFromV1sTsr, &vi.ImageTsr)
// vi.ImageFromV1sTsr.SetZeros()
// v1vision.UnPool(math32.Vec2(2, 2), math32.Vec2(2, 2), &vi.V1sUnPoolTsr, &vi.V1sPoolTsr, true)
// v1vision.Deconv(&vi.V1sGeom, &vi.V1sGaborTsr, &vi.ImageFromV1sTsr, &vi.V1sUnPoolTsr, vi.V1sGabor.Gain)
// stats.UnitNormOut(&vi.ImageFromV1sTsr, &vi.ImageFromV1sTsr)
}
func (vi *Vis) ConfigGUI() *core.Body {
vi.V1sGaborTable.Init()
vi.V1sGabor.ToTable(&vi.V1sGaborTable) // note: view only, testing
tensorcore.AddGridStylerTo(vi.ImageTsr, func(s *tensorcore.GridStyle) {
s.Image = true
s.Range.SetMin(0)
})
tensorcore.AddGridStylerTo(&vi.ImageFromV1sTsr, func(s *tensorcore.GridStyle) {
s.Image = true
s.Range.SetMin(0)
})
tensorcore.AddGridStylerTo(&vi.V1sGaborTable, func(s *tensorcore.GridStyle) {
s.Size.Min = 16
s.Range.Set(-0.05, 0.05)
})
b := core.NewBody("v1gabor").SetTitle("V1 Gabor Filtering")
sp := core.NewSplits(b)
core.NewForm(sp).SetStruct(vi)
tb := core.NewTabs(sp)
vi.tabView = tb
tf, _ := tb.NewTab("Image")
tensorcore.NewTensorGrid(tf).SetTensor(vi.ImageTsr)
tf, _ = tb.NewTab("V1All")
tensorcore.NewTensorGrid(tf).SetTensor(vi.V1AllTsr)
tf, _ = tb.NewTab("V1s")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.V1sTsr)
tf, _ = tb.NewTab("V1sKWTA")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.V1sKwtaTsr)
tf, _ = tb.NewTab("V1cMaxPool")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.V1cPoolTsr)
tf, _ = tb.NewTab("V1cMaxPol")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.V1cMaxPolTsr)
tf, _ = tb.NewTab("V1cPolPool")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.V1cPolPoolTsr)
tf, _ = tb.NewTab("V1cLenSum")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.V1cLenSumTsr)
tf, _ = tb.NewTab("V1cEndStop")
tensorcore.NewTensorGrid(tf).SetTensor(&vi.V1cEndStopTsr)
sp.SetSplits(.3, .7)
b.AddTopBar(func(bar *core.Frame) {
core.NewToolbar(bar).Maker(func(p *tree.Plan) {
tree.Add(p, func(w *core.FuncButton) { w.SetFunc(vi.Filter) })
})
})
b.RunMainWindow()
return b
}
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package fffb
import "cogentcore.org/lab/gosl/slbool"
//go:generate core generate -add-types -gosl
//gosl:start
// FFFB parameterizes feedforward (FF) and feedback (FB) inhibition (FFFB)
// based on average (or maximum) netinput (FF) and activation (FB)
type FFFB struct {
// enable this level of inhibition
On slbool.Bool
// Gi is the overall inhibition gain. This is main parameter to adjust to change
// overall activation levels, as it scales both the the FF and FB factors uniformly.
// 1.8 for layer, 2.0 for pool by default.
Gi float32 `min:"0" default:"1.5,1.8,2"`
// FF is the overall inhibitory contribution from feedforward inhibition.
// This multiplies average netinput (i.e., synaptic drive into layer), which
// anticipates upcoming changes in excitation. If set too high, it can make
// activity slow to emerge. See also ff0 for a zero-point for this value.
FF float32 `min:"0" default:"1"`
// FB is the overall inhibitory contribution from feedback inhibition.
// This multiplies average activation, thereby reacting to layer activation
// levels and working like a thermostat (going up when the 'heat' in the layer
// is too high).
FB float32 `min:"0" default:"1"`
// FBTau is the time constant in cycles (in milliseconds typically) for integrating
// feedback inhibitory values, which prevents oscillations that otherwise occur.
// The fast default of 1.4 should be used for most cases but sometimes a slower value
// (3 or higher) can be more robust, especially when inhibition is strong or inputs
// are more rapidly changing. (Tau is roughly 2/3 of the way to asymptote).
FBTau float32 `min:"0" default:"1.4,3,5"`
// MaxVsAvg determines the proportion of the maximum vs. average netinput to use in
// the feedforward inhibition computation: 0 = all average, 1 = all max,
// and values in between = proportional mix between average and max
// (ff_netin = avg + ff_max_vs_avg * (max - avg)).
// Including more max can be beneficial especially in situations where the average
// can vary significantly but the activity should not -- max is more robust in many
// situations but less flexible and sensitive to the overall distribution.
// Max is better for cases more closely approximating single or strictly fixed
// winner-take-all behavior: 0.5 is a good compromise in many cases and generally
// requires a reduction of .1 or slightly more (up to .3-.5) from the gi value for 0.
MaxVsAvg float32 `default:"0,0.5,1"`
// FF0 is the feedforward zero point for average netinput. Below this level, no FF
// inhibition is computed based on avg netinput, and this value is subtraced from
// the FF inhib contribution above this value. The 0.1 default should be good for most
// cases (and helps FF_FB produce k-winner-take-all dynamics), but if average
// netinputs are lower than typical, you may need to lower it.
FF0 float32 `default:"0.1"`
// rate = 1 / tau
FBDt float32 `edit:"-" display:"-" json:"-" xml:"-"`
}
func (fb *FFFB) Update() {
fb.FBDt = 1 / fb.FBTau
}
func (fb *FFFB) Defaults() {
fb.Gi = 1.8
fb.FF = 1
fb.FB = 1
fb.FBTau = 1.4
fb.MaxVsAvg = 0
fb.FF0 = 0.1
fb.Update()
}
func (fb *FFFB) ShouldDisplay(field string) bool {
switch field {
case "On":
return true
default:
return fb.On.IsTrue()
}
}
// FFInhib returns the feedforward inhibition value based on
// average and max excitatory conductance within relevant scope.
func (fb *FFFB) FFInhib(avgGe, maxGe float32) float32 {
ffNetin := avgGe + fb.MaxVsAvg*(maxGe-avgGe)
var ffi float32
if ffNetin > fb.FF0 {
ffi = fb.FF * (ffNetin - fb.FF0)
}
return ffi
}
// FBInhib computes feedback inhibition value as function of average activation
func (fb *FFFB) FBInhib(avgAct float32) float32 {
return fb.FB * avgAct
}
// FBUpdt updates feedback inhibition using time-integration rate constant
func (fb *FFFB) FBUpdt(fbi float32, newFbi float32) float32 {
nfb := fbi
nfb += fb.FBDt * (newFbi - nfb)
return nfb
}
//gosl:end
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
package gabor provides a gabor filter for visual and other
forms of signal processing
*/
package gabor
//go:generate core generate -add-types -gosl
import (
"math"
"cogentcore.org/core/math32"
"cogentcore.org/lab/table"
"cogentcore.org/lab/tensor"
)
// gabor.Filter specifies a gabor filter function,
// i.e., a 2d Gaussian envelope times a sinusoidal plane wave.
// By default it produces 2 phase asymmetric edge detector filters.
type Filter struct {
// On is whether this filter active.
On bool
// Gain is the overall gain multiplier applied after filtering.
// Only relevant if not using renormalization
// (otherwize it just gets renormed away).
Gain float32 `default:"2"`
// Size of the overall filter, which is the number of pixels
// wide and tall for a square matrix used to encode the filter.
// Filter is centered within this square, typically an even number,
// min effective size ~6.
Size int
// Wavelength is the wavelength of the sine waves: number of pixels
// over which a full period of the wave takes place.
// Typically same as Size (computation adds a 2 PI factor to
// translate into pixels instead of radians).
Wavelength float32
// Spacing is how far apart to space the centers of the
// gabor filters. 1 = every pixel, 2 = every other pixel,
// etc. High-res should be 1 or 2; lower res can be increments
// therefrom.
Spacing int
// SigmaLen is the Gaussian sigma for the length dimension
// (elongated axis perpendicular to the sine waves).
// as a normalized proportion of filter Size.
SigmaLen float32 `default:"0.3"`
// SigmaWd is the gaussian sigma for the width dimension
// (in the direction of the sine waves), as a normalized proportion
// of filter size.
SigmaWd float32 `default:"0.15,0.2"`
// Phase offset for the sine wave, in degrees.
// 0 = asymmetric sine wave, 90 = symmetric cosine wave.
Phase float32 `default:"0,90"`
// CircleEdge cuts off the filter (to zero) outside a circle
// of diameter = Size. Makes the filter more radially symmetric.
CircleEdge bool `default:"true"`
// NAngles is the number of different angles of overall gabor
// filter orientation to use. First angle is always horizontal.
NAngles int `default:"4"`
}
func (gf *Filter) Defaults() {
gf.On = true
gf.Gain = 2
gf.Size = 6
gf.Spacing = 2
gf.Wavelength = 6
gf.SigmaLen = 0.3
gf.SigmaWd = 0.2
gf.Phase = 0
gf.CircleEdge = true
gf.NAngles = 4
}
func (gf *Filter) Update() {
}
func (gf *Filter) ShouldDisplay(field string) bool {
switch field {
case "On":
return true
default:
return gf.On
}
}
// SetSize sets the size and Wavelength to same value, and also sets spacing
// these are the main params that need to be varied for standard V1 gabors
func (gf *Filter) SetSize(sz, spc int) {
gf.Size = sz
gf.Wavelength = float32(sz)
gf.Spacing = spc
}
// ToTensor renders filters into the given tensor.Tensor.
// must have dimensions already set to [angle][Y][X] where Y = X = Size
func (gf *Filter) ToTensor(tsr *tensor.Float32) {
ctr := 0.5 * float32(gf.Size-1)
angInc := math.Pi / float32(gf.NAngles)
radius := float32(gf.Size) * 0.5
gsLen := gf.SigmaLen * float32(gf.Size)
gsWd := gf.SigmaWd * float32(gf.Size)
lenNorm := 1.0 / (2.0 * gsLen * gsLen)
wdNorm := 1.0 / (2.0 * gsWd * gsWd)
twoPiNorm := (2.0 * math.Pi) / gf.Wavelength
phsRad := math32.DegToRad(gf.Phase)
for ang := 0; ang < gf.NAngles; ang++ {
angf := -float32(ang) * angInc
posSum := float32(0)
negSum := float32(0)
for x := 0; x < gf.Size; x++ {
for y := 0; y < gf.Size; y++ {
xf := float32(x) - ctr
yf := float32(y) - ctr
dist := math32.Hypot(xf, yf)
val := float32(0)
if !(gf.CircleEdge && (dist > radius)) {
nx := xf*math32.Cos(angf) - yf*math32.Sin(angf)
ny := yf*math32.Cos(angf) + xf*math32.Sin(angf)
gauss := math32.Exp(-(lenNorm*(nx*nx) + wdNorm*(ny*ny)))
sin := math32.Sin(twoPiNorm*ny + phsRad)
val = gauss * sin
if val > 0 {
posSum += val
} else if val < 0 {
negSum += -val
}
}
tsr.Set(val, ang, y, x)
}
}
// renorm each half
posNorm := float32(1) / posSum
negNorm := float32(1) / negSum
for x := 0; x < gf.Size; x++ {
for y := 0; y < gf.Size; y++ {
val := tsr.Value(ang, y, x)
if val > 0 {
val *= posNorm
} else if val < 0 {
val *= negNorm
}
tsr.Set(val, ang, y, x)
}
}
}
}
// ToTable renders filters into the given table.Table
// setting a column named Angle to the angle and
// a column named Gabor to the filter for that angle.
// This is useful for display and validation purposes.
func (gf *Filter) ToTable(tab *table.Table) {
tab.AddFloat32Column("Angle")
tab.AddFloat32Column("Filter", gf.Size, gf.Size)
tab.SetNumRows(gf.NAngles)
cl := tab.Columns.Values[1].(*tensor.Float32)
gf.ToTensor(cl)
angInc := math.Pi / float32(gf.NAngles)
for ang := 0; ang < gf.NAngles; ang++ {
angf := math32.RadToDeg(-float32(ang) * angInc)
tab.ColumnByIndex(0).SetFloat1D(float64(-angf), ang)
}
}
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package kwta
//gosl:start
// Chans are ion channels used in computing point-neuron activation function
type Chans struct {
// excitatory sodium (Na) AMPA channels activated by synaptic glutamate
E float32
// constant leak (potassium, K+) channels -- determines resting potential (typically higher than resting potential of K)
L float32
// inhibitory chloride (Cl-) channels activated by synaptic GABA
I float32
// gated / active potassium channels -- typically hyperpolarizing relative to leak / rest
K float32
}
//gosl:end
// SetAll sets all the values
func (ch *Chans) SetAll(e, l, i, k float32) {
ch.E, ch.L, ch.I, ch.K = e, l, i, k
}
// SetFromOtherMinus sets all the values from other Chans minus given value
func (ch *Chans) SetFromOtherMinus(oth Chans, minus float32) {
ch.E, ch.L, ch.I, ch.K = oth.E-minus, oth.L-minus, oth.I-minus, oth.K-minus
}
// SetFromMinusOther sets all the values from given value minus other Chans
func (ch *Chans) SetFromMinusOther(minus float32, oth Chans) {
ch.E, ch.L, ch.I, ch.K = minus-oth.E, minus-oth.L, minus-oth.I, minus-oth.K
}
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package kwta
//go:generate core generate -add-types
import (
"cogentcore.org/lab/gosl/slbool"
"github.com/emer/v1vision/fffb"
"github.com/emer/v1vision/nxx1"
)
//gosl:start
//gosl:import "github.com/emer/v1vision/fffb"
//gosl:import "github.com/emer/v1vision/nxx1"
// KWTA contains all the parameters needed for computing FFFB
// (feedforward & feedback) inhibition that results in roughly
// k-Winner-Take-All behavior.
type KWTA struct {
// On is whether to run kWTA or not.
On slbool.Bool
// Iters is the maximum number of iterations to perform.
Iters int32 `default:"10"`
// Threshold on delta-activation (change in activation) for stopping
// updating of activations. Not used on GPU implementation.
DelActThr float32 `default:"0.005"`
// Time constant for integrating activation
ActTau float32 `default:"3"`
// Layer-level feedforward & feedback inhibition, applied over entire set of values.
Layer fffb.FFFB `display:"inline"`
// Pool-level (feature groups) feedforward and feedback inhibition.
// applied within inner-most dimensions inside outer 2 dimensions.
Pool fffb.FFFB `display:"inline"`
// XX1 are the Noisy X/X+1 rate code activation function parameters.
XX1 nxx1.Params `display:"inline"`
// GBar are maximal conductances levels for channels.
Gbar Chans `display:"inline"`
// Erev are reversal potentials for each channel.
Erev Chans `display:"inline"`
// Erev - Act.Thr for each channel -- used in computing GeThrFromG among others
ErevSubThr Chans `display:"-"`
// Act.Thr - Erev for each channel -- used in computing GeThrFromG among others
ThrSubErev Chans `display:"-" json:"-" xml:"-"`
ActDt float32 `display:"-"; json"-" xml"-" desc:"integration rate = 1/ tau"`
pad, pad1, pad2 float32
}
func (kp *KWTA) Defaults() {
kp.On.SetBool(true)
kp.Iters = 10 // 10 is typically sufficient.
kp.DelActThr = 0.005
kp.Layer.Defaults()
kp.Pool.Defaults()
kp.Layer.On.SetBool(true)
kp.Layer.Gi = 1.5 // from lvis
kp.Pool.On.SetBool(true)
kp.Pool.Gi = 2.0
kp.XX1.Defaults()
kp.XX1.Gain = 80 // from lvis
kp.XX1.NVar = 0.01 // from lvis
kp.ActTau = 3
kp.Gbar.SetAll(0.5, 0.1, 1.0, 1.0) // 0.5 is key for 1.0 inputs
kp.Erev.SetAll(1.0, 0.3, 0.3, 0.1)
kp.Update()
}
// Update must be called after any changes to parameters
func (kp *KWTA) Update() {
kp.Layer.Update()
kp.Pool.Update()
kp.XX1.Update()
kp.ErevSubThr.SetFromOtherMinus(kp.Erev, kp.XX1.Thr)
kp.ThrSubErev.SetFromMinusOther(kp.XX1.Thr, kp.Erev)
kp.ActDt = 1 / kp.ActTau
}
// GeThrFromG computes the threshold for Ge based on other conductances
func (kp *KWTA) GeThrFromG(gi float32) float32 {
ge := ((kp.Gbar.I*gi*kp.ErevSubThr.I + kp.Gbar.L*kp.ErevSubThr.L) / kp.ThrSubErev.E)
return ge
}
// ActFromG computes rate-coded activation Act from conductances Ge and Gi
func (kp *KWTA) ActFromG(geThr, ge, act float32, delAct *float32) float32 {
nwAct := kp.XX1.NoisyXX1(ge*kp.Gbar.E - geThr)
*delAct = kp.ActDt * (nwAct - act)
nwAct = act + *delAct
return nwAct
}
//gosl:end
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package kwta
import (
"cogentcore.org/core/math32"
"cogentcore.org/lab/tensor"
)
// NeighInhib adds an additional inhibition factor based on the same
// feature along an orthogonal angle -- assumes inner-most X axis
// represents angle of gabor or related feature.
// This helps reduce redundancy of feature code.
type NeighInhib struct {
// use neighborhood inhibition
On bool
// overall value of the inhibition -- this is what is added into the unit Gi inhibition level
Gi float32 `default:"0.6"`
}
var (
// ortho neighbor coordinates for 4 angles, also uses negated version
// .
// --- = (0,1) (X,Y)
// . /
// / = (-1,1)
// | . = (1,0)
// \
// . \ = (-1,-1)
Neigh4X = []int{0, -1, 1, -1}
Neigh4Y = []int{1, 1, 0, -1}
)
func (ni *NeighInhib) Defaults() {
ni.On = true
ni.Gi = 0.6
}
// Inhib4 computes the neighbor inhibition on activations
// into extGi. If extGi is not same shape as act, it will be
// made so (most efficient to re-use same structure).
// Act must be a 4D tensor with features as inner 2D.
// 4 version ONLY works with 4 angles (inner-most feature dimension)
func (ni *NeighInhib) Inhib4(act, extGi *tensor.Float32) {
extGi.SetShapeSizes(act.Shape().Sizes...)
gis := extGi.Values
layY := act.DimSize(0)
layX := act.DimSize(1)
plY := act.DimSize(2)
plX := act.DimSize(3)
plN := plY * plX
pi := 0
for ly := 0; ly < layY; ly++ {
for lx := 0; lx < layX; lx++ {
pui := pi * plN
ui := 0
for py := 0; py < plY; py++ {
for ang := 0; ang < plX; ang++ {
idx := pui + ui
gi := float32(0)
npX := lx + Neigh4X[ang]
npY := ly + Neigh4Y[ang]
if npX >= 0 && npX < layX && npY >= 0 && npY < layY {
gi = math32.Max(gi, ni.Gi*act.Value(npY, npX, py, ang))
}
nnX := lx - Neigh4X[ang]
nnY := ly - Neigh4Y[ang]
if nnX >= 0 && nnX < layX && nnY >= 0 && nnY < layY {
gi = math32.Max(gi, ni.Gi*act.Value(nnY, nnX, py, ang))
}
gis[idx] = gi
ui++
}
}
pi++
}
}
}
// Code generated by "core generate -add-types -gosl"; DO NOT EDIT.
package motion
import (
"cogentcore.org/core/enums"
)
var _DirectionsValues = []Directions{0, 1, 2, 3}
// DirectionsN is the highest valid value for type Directions, plus one.
//
//gosl:start
const DirectionsN Directions = 4
//gosl:end
var _DirectionsValueMap = map[string]Directions{`Left`: 0, `Right`: 1, `Down`: 2, `Up`: 3}
var _DirectionsDescMap = map[Directions]string{0: ``, 1: ``, 2: ``, 3: ``}
var _DirectionsMap = map[Directions]string{0: `Left`, 1: `Right`, 2: `Down`, 3: `Up`}
// String returns the string representation of this Directions value.
func (i Directions) String() string { return enums.String(i, _DirectionsMap) }
// SetString sets the Directions value from its string representation,
// and returns an error if the string is invalid.
func (i *Directions) SetString(s string) error {
return enums.SetString(i, s, _DirectionsValueMap, "Directions")
}
// Int64 returns the Directions value as an int64.
func (i Directions) Int64() int64 { return int64(i) }
// SetInt64 sets the Directions value from an int64.
func (i *Directions) SetInt64(in int64) { *i = Directions(in) }
// Desc returns the description of the Directions value.
func (i Directions) Desc() string { return enums.Desc(i, _DirectionsDescMap) }
// DirectionsValues returns all possible values for the type Directions.
func DirectionsValues() []Directions { return _DirectionsValues }
// Values returns all possible values for the type Directions.
func (i Directions) Values() []enums.Enum { return enums.Values(_DirectionsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Directions) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Directions) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "Directions")
}
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
package motion provides motion-filters based on retinal starburst amacrine
cells (SAC) that compute centrifugal motion flow from each point.
*/
package motion
import (
"cogentcore.org/lab/tensor"
)
//go:generate core generate -add-types -gosl
// Directions are the motion directions, in feature order,
// as represented in the Star and FullField outputs.
type Directions int32 //enums:enum
const (
Left Directions = iota
Right
Down
Up
)
// Params has the motion parameters for retinal starburst amacrine
// cells (SAC) that compute centrifugal motion flow from each point.
type Params struct {
// SlowTau is the time constant (in frames) for integrating
// slow inhibitory inputs.
SlowTau float32
// FastTau is the time constant (in frames) for integrating
// fast excitatory inputs.
FastTau float32
// Gain is multiplier on the opponent difference for Star computation.
Gain float32
// FullGain is multiplier for FullField
FullGain float32
// IntegTau is the integration time constant for integrating
// the normalization and full field values over frames, to get
// a more consistent value.
IntegTau float32
// NormInteg is the integrated normalization value -- updated in FullFieldInteg
NormInteg float32 `edit:"-"`
// DoGSumScalarIndex is the index into the V1Vision Scalars output for
// Sum of DoG activity, used for normalizing.
DoGSumScalarIndex int `edit:"-"`
// FFScalarIndex is the index into the V1Vision Scalars output for FullField
FFScalarIndex int `edit:"-"`
}
func (pr *Params) Defaults() {
// note: these values have been optimized on axon deepspace model:
pr.SlowTau = 4
pr.FastTau = 2
pr.Gain = 20
pr.FullGain = 1
pr.IntegTau = 6
}
// FullFieldInteg computes a full-field integration of instantaneous
// MotionFullField results, in scalars input at FFScalarIndex
// Resulting integ tensor is 4 values (2x2) with left, right, bottom, top units.
// integ = integrated full-field values over time
// visNormInteg = integrated visNorm, actually used for normalization
func (pr *Params) FullFieldInteg(ndata int, scalars, integ *tensor.Float32) {
idt := 1.0 / pr.IntegTau
integ.SetShapeSizes(ndata, 2, 2)
for di := range ndata {
visNorm := scalars.Value(pr.DoGSumScalarIndex, di)
if pr.NormInteg == 0 {
pr.NormInteg = visNorm
} else {
pr.NormInteg += idt * (visNorm - pr.NormInteg)
}
vnf := pr.FullGain
if pr.NormInteg > 0 {
vnf /= pr.NormInteg
}
act := func(v float32) float32 { return vnf * v }
l := scalars.Value(pr.FFScalarIndex+0, di)
r := scalars.Value(pr.FFScalarIndex+1, di)
if l > r {
l = act(l - r)
r = 0
} else {
r = act(r - l)
l = 0
}
b := scalars.Value(pr.FFScalarIndex+2, di)
u := scalars.Value(pr.FFScalarIndex+3, di)
if b > u {
b = act(b - u)
u = 0
} else {
u = act(u - b)
b = 0
}
integf := func(y, x int, v float32) {
vi := integ.Value(di, y, x)
vi += idt * (v - vi)
integ.Set(vi, di, y, x)
}
integf(0, 0, l)
integf(0, 1, r)
integf(1, 0, b)
integf(1, 1, u)
}
}
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package nproc provides number of processors using slurm env var
SLURM_CPUS_PER_TASK or runtime.NumCPU().
TODO: move this to dmem package once that is started.
*/
package nproc
import (
"os"
"runtime"
"strconv"
)
var NumCPUCache int
func NumCPU() int {
if NumCPUCache > 0 {
return NumCPUCache
}
ncs, ok := os.LookupEnv("SLURM_CPUS_PER_TASK")
if !ok {
NumCPUCache = runtime.NumCPU()
} else {
NumCPUCache, _ = strconv.Atoi(ncs)
}
return NumCPUCache
}
// ThreadNs computes number of threads and number of jobs per thread,
// based on number of cpu's and total number of jobs.
// rmdr is remainder of jobs not evenly divisible by ncpu
func ThreadNs(ncpu, njobs int) (nthrs, nper, rmdr int) {
if njobs <= ncpu {
return njobs, 1, 0
}
nthrs = ncpu
nper = njobs / ncpu
rmdr = njobs % ncpu
return
}
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package nxx1 provides the Noisy-X-over-X-plus-1 activation function that well-characterizes
the neural response function empirically, as a saturating sigmoid-like nonlinear response
with an initial largely linear regime.
The basic x/(x+1) sigmoid function is convolved with a gaussian noise kernel to produce
a better approximation of the effects of noise on neural firing -- the main effect is
to create a continuous graded early level of firing even slightly below threshold, softening
the otherwise hard transition to firing at threshold.
A hand-optimized piece-wise function approximation is used to generate the NXX1 function
instead of requiring a lookup table of the gaussian convolution. This is much easier
to use across a range of computational platforms including GPU's, and produces very similar
overall values.
*/
package nxx1
//go:generate core generate -add-types -gosl
import (
"cogentcore.org/core/math32"
)
//gosl:start
//gosl:import "cogentcore.org/core/math32"
// Params are the Noisy X/(X+1) rate-coded activation function parameters.
// This function well-characterizes the neural response function empirically,
// as a saturating sigmoid-like nonlinear response with an initial largely linear regime.
// The basic x/(x+1) sigmoid function is convolved with a gaussian noise kernel to produce
// a better approximation of the effects of noise on neural firing -- the main effect is
// to create a continuous graded early level of firing even slightly below threshold, softening
// the otherwise hard transition to firing at threshold.
// A hand-optimized piece-wise function approximation is used to generate the NXX1 function
// instead of requiring a lookup table of the gaussian convolution. This is much easier
// to use across a range of computational platforms including GPU's, and produces very similar
// overall values. abc.
type Params struct {
// threshold value Theta (Q) for firing output activation (.5 is more accurate value based on AdEx biological parameters and normalization
Thr float32 `default:"0.5"`
// gain (gamma) of the rate-coded activation functions -- 100 is default, 80 works better for larger models, and 20 is closer to the actual spiking behavior of the AdEx model -- use lower values for more graded signals, generally in lower input/sensory layers of the network
Gain float32 `default:"80,100,40,20" min:"0"`
// variance of the Gaussian noise kernel for convolving with XX1 in NOISY_XX1 and NOISY_LINEAR -- determines the level of curvature of the activation function near the threshold -- increase for more graded responding there -- note that this is not actual stochastic noise, just constant convolved gaussian smoothness to the activation function
NVar float32 `default:"0.005,0.01" min:"0"`
// threshold on activation below which the direct vm - act.thr is used -- this should be low -- once it gets active should use net - g_e_thr ge-linear dynamics (gelin)
VmActThr float32 `default:"0.01"`
// multiplier on sigmoid used for computing values for net < thr
SigMult float32 `default:"0.33" display:"-" json:"-" xml:"-"`
// power for computing sig_mult_eff as function of gain * nvar
SigMultPow float32 `default:"0.8" display:"-" json:"-" xml:"-"`
// gain multipler on (net - thr) for sigmoid used for computing values for net < thr
SigGain float32 `default:"3" display:"-" json:"-" xml:"-"`
// interpolation range above zero to use interpolation
InterpRange float32 `default:"0.01" display:"-" json:"-" xml:"-"`
// range in units of nvar over which to apply gain correction to compensate for convolution
GainCorRange float32 `default:"10" display:"-" json:"-" xml:"-"`
// gain correction multiplier -- how much to correct gains
GainCor float32 `default:"0.1" display:"-" json:"-" xml:"-"`
// sig_gain / nvar
SigGainNVar float32 `display:"-" json:"-" xml:"-"`
// overall multiplier on sigmoidal component for values below threshold = sig_mult * pow(gain * nvar, sig_mult_pow)
SigMultEff float32 `display:"-" json:"-" xml:"-"`
// 0.5 * sig_mult_eff -- used for interpolation portion
SigValAt0 float32 `display:"-" json:"-" xml:"-"`
// function value at interp_range - sig_val_at_0 -- for interpolation
InterpVal float32 `display:"-" json:"-" xml:"-"`
pad, pad1 float32
}
func (xp *Params) Update() {
xp.SigGainNVar = xp.SigGain / xp.NVar
xp.SigMultEff = xp.SigMult * math32.Pow(xp.Gain*xp.NVar, xp.SigMultPow)
xp.SigValAt0 = 0.5 * xp.SigMultEff
xp.InterpVal = xp.XX1GainCor(xp.InterpRange) - xp.SigValAt0
}
func (xp *Params) Defaults() {
xp.Thr = 0.5
xp.Gain = 100
xp.NVar = 0.005
xp.VmActThr = 0.01
xp.SigMult = 0.33
xp.SigMultPow = 0.8
xp.SigGain = 3.0
xp.InterpRange = 0.01
xp.GainCorRange = 10.0
xp.GainCor = 0.1
xp.Update()
}
// XX1 computes the basic x/(x+1) function
func (xp *Params) XX1(x float32) float32 { return x / (x + 1) }
// XX1GainCor computes x/(x+1) with gain correction within GainCorRange
// to compensate for convolution effects
func (xp *Params) XX1GainCor(x float32) float32 {
gainCorFact := (xp.GainCorRange - (x / xp.NVar)) / xp.GainCorRange
if gainCorFact < 0 {
return xp.XX1(xp.Gain * x)
}
newGain := xp.Gain * (1 - xp.GainCor*gainCorFact)
return xp.XX1(newGain * x)
}
// NoisyXX1 computes the Noisy x/(x+1) function -- directly computes close approximation
// to x/(x+1) convolved with a gaussian noise function with variance nvar.
// No need for a lookup table -- very reasonable approximation for standard range of parameters
// (nvar = .01 or less -- higher values of nvar are less accurate with large gains,
// but ok for lower gains)
func (xp *Params) NoisyXX1(x float32) float32 {
if x < 0 { // sigmoidal for < 0
ex := -(x * xp.SigGainNVar)
if ex > 50 {
return 0
}
return xp.SigMultEff / (1 + math32.FastExp(ex))
} else if x < xp.InterpRange {
interp := 1 - ((xp.InterpRange - x) / xp.InterpRange)
return xp.SigValAt0 + interp*xp.InterpVal
} else {
return xp.XX1GainCor(x)
}
}
// X11GainCorGain computes x/(x+1) with gain correction within GainCorRange
// to compensate for convolution effects -- using external gain factor
func (xp *Params) XX1GainCorGain(x, gain float32) float32 {
gainCorFact := (xp.GainCorRange - (x / xp.NVar)) / xp.GainCorRange
if gainCorFact < 0 {
return xp.XX1(gain * x)
}
newGain := gain * (1 - xp.GainCor*gainCorFact)
return xp.XX1(newGain * x)
}
// NoisyXX1Gain computes the noisy x/(x+1) function -- directly computes close approximation
// to x/(x+1) convolved with a gaussian noise function with variance nvar.
// No need for a lookup table -- very reasonable approximation for standard range of parameters
// (nvar = .01 or less -- higher values of nvar are less accurate with large gains,
// but ok for lower gains). Using external gain factor.
func (xp *Params) NoisyXX1Gain(x, gain float32) float32 {
if x < xp.InterpRange {
sigMultEffArg := xp.SigMult * math32.Pow(gain*xp.NVar, xp.SigMultPow)
sigValAt0Arg := 0.5 * sigMultEffArg
if x < 0 { // sigmoidal for < 0
ex := -(x * xp.SigGainNVar)
if ex > 50 {
return 0
}
return sigMultEffArg / (1 + math32.FastExp(ex))
} else { // else x < interp_range
interp := 1 - ((xp.InterpRange - x) / xp.InterpRange)
return sigValAt0Arg + interp*xp.InterpVal
}
} else {
return xp.XX1GainCorGain(x, gain)
}
}
//gosl:end
// Copyright (c) 2025, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1std
import (
"image"
"cogentcore.org/core/math32"
"cogentcore.org/lab/tensor"
"github.com/emer/v1vision/dog"
"github.com/emer/v1vision/kwta"
"github.com/emer/v1vision/v1vision"
)
// DoGColor does color difference-of-gaussian (DoG) filtering,
// on Red - Green and Blue - Yellow opponent color contrasts,
// so that activity reflects presence of a color beyond grey baseline.
// These capture the activity of the blob chroma sensitive cells.
// Call Defaults and then set any custom params, then call Config.
// Results are in Output tensor after Run().
type DoGColor struct {
// GPU means use the GPU by default (does GPU initialization) in Config.
// To change what is actually used at the moment of running,
// set [v1vision.UseGPU].
GPU bool
// LGN DoG filter parameters. Generally have larger fields,
// and no spatial tuning (i.e., OnSigma == OffSigma), consistent
// with blob cells.
DoG dog.Filter
// Geom is geometry of input, output.
Geom v1vision.Geom `edit:"-"`
// kwta parameters, providing more contrast across colors.
KWTA kwta.KWTA
// V1 is the V1Vision filter processing system.
V1 v1vision.V1Vision `display:"no-inline"`
// Output has the resulting DoG filter outputs, pointing to Values in V1.
// [Y, X, Polarity, Feature], where Polarity = On (0) vs Off (1) stronger.
// Feature: 0 = Red vs. Green; 1 = Blue vs. Yellow.
Output *tensor.Float32 `display:"no-inline"`
outIdx int
}
func (vi *DoGColor) Defaults() {
vi.GPU = true
vi.DoG.Defaults()
vi.DoG.Gain = 8 // color channels are weaker than grey
vi.DoG.OnGain = 1
vi.DoG.SetSameSigma(0.5) // no spatial component, just pure contrast
vi.SetSize(12, 16) // V1mF16 typically = 12, no border
vi.KWTA.Defaults()
vi.KWTA.Layer.On.SetBool(false) // non-spatial, mainly for differentiation within pools
vi.KWTA.Pool.Gi = 1.2
}
// SetSize sets the V1sGabor filter size and geom spacing to given values.
// Default is 12, 16, for a medium-sized filter.
func (vi *DoGColor) SetSize(sz, spc int) {
vi.DoG.Spacing = spc
vi.DoG.Size = sz
vi.Geom.Set(math32.Vec2i(0, 0), math32.Vec2i(spc, spc), math32.Vec2i(sz, sz))
}
// Config configures the filtering pipeline with all the current parameters.
// imageSize is the _content_ size of input image that is passed to Run
// as an RGB Tensor (per [V1Vision.Images] standard format),
// (i.e., exclusive of the additional border around the image = [Image.Size]).
// The resulting Geom.Border field can be passed to [Image] methods.
// ndata = number of data-parallel inputs to process in parallel.
func (vi *DoGColor) Config(ndata int, imageSize image.Point) {
vi.Geom.SetImageSize(imageSize)
vi.V1.Init(ndata)
*vi.V1.NewKWTAParams() = vi.KWTA
kwtaIdx := 0
img := vi.V1.NewImage(vi.Geom.In.V())
wrap := vi.V1.NewImage(vi.Geom.In.V())
lmsRG := vi.V1.NewImage(vi.Geom.In.V())
lmsBY := vi.V1.NewImage(vi.Geom.In.V())
vi.V1.NewWrapImage(img, 3, wrap, int(vi.Geom.Border.X), &vi.Geom)
vi.V1.NewLMSComponents(wrap, lmsRG, lmsBY, vi.DoG.Gain, &vi.Geom)
out := vi.V1.NewValues(int(vi.Geom.Out.Y), int(vi.Geom.Out.X), 2)
dogFt := vi.V1.NewDoGOnOff(&vi.DoG, &vi.Geom)
vi.V1.NewConvolveDiff(lmsRG, v1vision.Red, lmsRG, v1vision.Green, dogFt, 0, 1, out, 0, 1, vi.DoG.OnGain, &vi.Geom)
vi.V1.NewConvolveDiff(lmsBY, v1vision.Blue, lmsBY, v1vision.Yellow, dogFt, 0, 1, out, 1, 1, vi.DoG.OnGain, &vi.Geom)
vi.outIdx = out
if vi.KWTA.On.IsTrue() {
inh := vi.V1.NewInhibs(int(vi.Geom.Out.Y), int(vi.Geom.Out.X))
vi.outIdx = vi.V1.NewKWTA(out, 0, 2, kwtaIdx, inh, &vi.Geom)
}
vi.V1.SetAsCurrent()
if vi.GPU {
vi.V1.GPUInit()
}
}
// RunImages runs the configured filtering pipeline.
// on given Image(s), using given [Image] handler.
func (vi *DoGColor) RunImages(im *Image, imgs ...image.Image) {
v1vision.UseGPU = vi.GPU
vi.V1.SetAsCurrent()
im.SetImagesRGB(&vi.V1, int(vi.Geom.Border.X), imgs...)
vi.V1.Run(v1vision.ValuesVar)
vi.Output = vi.V1.Values.SubSpace(vi.outIdx).(*tensor.Float32)
}
// Copyright (c) 2025, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1std
import (
"image"
"cogentcore.org/core/math32"
"cogentcore.org/lab/tensor"
"github.com/emer/v1vision/dog"
"github.com/emer/v1vision/v1vision"
)
// DoGGrey does greyscale difference-of-gaussian (DoG) filtering.
// Output is log-max-normalized.
// Call Defaults and then set any custom params, then call Config.
// Results are in Output tensor after Run().
type DoGGrey struct {
// GPU means use the GPU by default (does GPU initialization) in Config.
// To change what is actually used at the moment of running,
// set [v1vision.UseGPU].
GPU bool
// LGN DoG filter parameters.
DoG dog.Filter
// Geom is geometry of input, output.
Geom v1vision.Geom `edit:"-"`
// V1 is the V1Vision filter processing system.
V1 v1vision.V1Vision `display:"no-inline"`
// Output has the resulting DoG filter outputs, pointing to Values in V1.
// [Y, X, Polarity, 1], where Polarity = On (0) vs Off (1) stronger.
Output *tensor.Float32 `display:"no-inline"`
}
func (vi *DoGGrey) Defaults() {
vi.GPU = true
vi.DoG.Defaults()
vi.SetSize(12, 4)
}
// SetSize sets the V1sGabor filter size and geom spacing to given values.
// Default is 12, 4, for a medium-sized filter.
func (vi *DoGGrey) SetSize(sz, spc int) {
vi.DoG.Spacing = spc
vi.DoG.Size = sz
vi.Geom.Set(math32.Vec2i(0, 0), math32.Vec2i(spc, spc), math32.Vec2i(sz, sz))
}
// Config configures the filtering pipeline with all the current parameters.
// imageSize is the _content_ size of input image that is passed to Run
// as an RGB Tensor (per [V1Vision.Images] standard format),
// (i.e., exclusive of the additional border around the image = [Image.Size]).
// The resulting Geom.Border field can be passed to [Image] methods.
// ndata = number of data-parallel inputs to process in parallel.
func (vi *DoGGrey) Config(ndata int, imageSize image.Point) {
vi.Geom.SetImageSize(imageSize)
vi.V1.Init(ndata)
img := vi.V1.NewImage(vi.Geom.In.V())
wrap := vi.V1.NewImage(vi.Geom.In.V())
vi.V1.NewWrapImage(img, 0, wrap, int(vi.Geom.Border.X), &vi.Geom)
_, out := vi.V1.NewDoG(wrap, 0, &vi.DoG, &vi.Geom)
vi.V1.NewLogValues(out, out, 1, 1.0, &vi.Geom)
vi.V1.NewNormDiv(v1vision.MaxScalar, out, out, 1, &vi.Geom)
vi.V1.SetAsCurrent()
if vi.GPU {
vi.V1.GPUInit()
}
}
// RunImages runs the configured filtering pipeline.
// on given Image(s), using given [Image] handler.
func (vi *DoGGrey) RunImages(im *Image, imgs ...image.Image) {
v1vision.UseGPU = vi.GPU
vi.V1.SetAsCurrent()
im.SetImagesGrey(&vi.V1, int(vi.Geom.Border.X), imgs...)
vi.V1.Run(v1vision.ValuesVar)
vi.Output = vi.V1.Values.SubSpace(0).(*tensor.Float32)
}
// Copyright (c) 2025, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1std
import (
"image"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/lab/tensor"
"github.com/anthonynsimon/bild/transform"
"github.com/emer/v1vision/v1vision"
)
//go:generate core generate -add-types
// Image manages conversion of bitmap images into tensor formats for
// subsequent processing by filters.
type Image struct {
// File is the name of image file to operate on
File string
// Size is the target image size to use. Images will be rescaled to this size.
Size image.Point
// Images are the current input image(s), as Go [image.Image].
Images []image.Image `display:"-"`
// Tsr are the current input image(s) as an RGB tensor.
// This points into the V1Vision.Images input image.
Tsr *tensor.Float32 `display:"no-inline"`
}
func (vi *Image) Defaults() {
vi.Size = image.Point{128, 128}
}
// SetImagesResize sets current image(s) for processing, resizing to target size.
func (vi *Image) SetImagesResize(imgs ...image.Image) {
// todo: do this all on GPU at some point!
vi.Images = imgs
for i, im := range vi.Images {
isz := im.Bounds().Size()
if isz != vi.Size {
vi.Images[i] = transform.Resize(im, vi.Size.X, vi.Size.Y, transform.Linear)
}
}
}
// OpenImagesResize opens image(s) from given filename(s), and resizes to target size.
func (vi *Image) OpenImagesResize(fns ...string) error {
var errs []error
imgs := make([]image.Image, len(fns))
for i, fn := range fns {
img, _, err := imagex.Open(fn)
if err != nil {
errs = append(errs, err)
continue
}
imgs[i] = img
}
vi.SetImagesResize(imgs...)
return errors.Join(errs...)
}
// GetTensors gets the Images tensor at given index (typically 0).
func (vi *Image) GetTensors(v1 *v1vision.V1Vision, idx int) {
vi.Tsr = v1.Images.SubSpace(idx).(*tensor.Float32)
// todo:
// tensorcore.AddGridStylerTo(vi.Tsr, func(s *tensorcore.GridStyle) {
// s.Image = true
// s.Range.SetMin(0)
// })
}
// SetImagesRGB sets current image(s) for processing
// and converts to a float32 tensor with full RGB components.
// border is the border size to add around edges.
func (vi *Image) SetImagesRGB(v1 *v1vision.V1Vision, border int, imgs ...image.Image) {
vi.SetImagesResize(imgs...)
vi.GetTensors(v1, 0)
v1vision.RGBToTensor(vi.Tsr, border, v1vision.BottomZero, vi.Images...)
}
// SetImagesGrey sets current image(s) for processing
// and converts to a float32 tensor as greyscale image.
// border is the border size to add around edges.
func (vi *Image) SetImagesGrey(v1 *v1vision.V1Vision, border int, imgs ...image.Image) {
vi.SetImagesResize(imgs...)
vi.GetTensors(v1, 0)
v1vision.RGBToGrey(vi.Tsr, border, v1vision.BottomZero, vi.Images...)
}
// Copyright (c) 2025, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1std
import (
"image"
"cogentcore.org/core/math32"
"cogentcore.org/lab/tensor"
"github.com/emer/v1vision/dog"
"github.com/emer/v1vision/motion"
"github.com/emer/v1vision/v1vision"
)
// MotionDoG computes starburst-amacrine style motion processing and
// resulting summary full-field motion values, on greyscale
// difference-of-gaussian (DoG) filtering.
// Call Defaults and then set any custom params, then call Config.
// Results are in Output tensor after Run().
type MotionDoG struct {
// GPU means use the GPU by default (does GPU initialization) in Config.
// To change what is actually used at the moment of running,
// set [v1vision.UseGPU].
GPU bool
// LGN DoG filter parameters.
DoG dog.Filter
// Motion filter parameters.
Motion motion.Params
// Geom is geometry of input, output.
Geom v1vision.Geom `edit:"-"`
// FullField has the integrated FullField output: [NData, 2, 2].
// Use [motion.Directions] for 1D indexes (is 2x2 for [L,R][D,U]).
FullField tensor.Float32 `display:"no-inline"`
// GetStar retrieves the star values. Otherwise, just the full-field.
GetStar bool
// V1 is the V1Vision filter processing system.
V1 v1vision.V1Vision `display:"no-inline"`
// Star has the star values, if GetStar is true,
// pointing to Values in V1.
// [NData, Y, X, Polarity, 4], where Polarity is DoG polarity, and 4 is for
// Left, Right, Down, Up.
Star *tensor.Float32 `display:"no-inline"`
}
func (vi *MotionDoG) Defaults() {
vi.GPU = true
vi.DoG.Defaults()
vi.Motion.Defaults()
vi.SetSize(12, 4)
}
// SetSize sets the V1sGabor filter size and geom spacing to given values.
// Default is 12, 4, for a medium-sized filter.
func (vi *MotionDoG) SetSize(sz, spc int) {
vi.DoG.Spacing = spc
vi.DoG.Size = sz
vi.Geom.Set(math32.Vec2i(0, 0), math32.Vec2i(spc, spc), math32.Vec2i(sz, sz))
}
// Config configures the filtering pipeline with all the current parameters.
// imageSize is the _content_ size of input image that is passed
// to RunImage as an RGB Tensor (per [V1Vision.Images] standard format),
// (i.e., exclusive of the additional border around the image = [Image.Size]).
// ndata = number of data-parallel inputs to process in parallel.
func (vi *MotionDoG) Config(ndata int, imageSize image.Point) {
vi.Geom.SetImageSize(imageSize)
vi.FullField.SetShapeSizes(ndata, 2, 2)
fn := 1 // number of filters in DoG
vi.V1.Init(ndata)
img := vi.V1.NewImage(vi.Geom.In.V())
wrap := vi.V1.NewImage(vi.Geom.In.V())
vi.V1.NewWrapImage(img, 0, wrap, int(vi.Geom.Border.X), &vi.Geom)
_, out := vi.V1.NewDoG(wrap, 0, &vi.DoG, &vi.Geom)
vi.V1.NewLogValues(out, out, fn, 1.0, &vi.Geom)
vi.V1.NewNormDiv(v1vision.MaxScalar, out, out, fn, &vi.Geom)
vi.Motion.DoGSumScalarIndex = vi.V1.NewAggScalar(v1vision.SumScalar, out, fn, &vi.Geom)
fastIdx := vi.V1.NewMotionIntegrate(out, fn, vi.Motion.FastTau, vi.Motion.SlowTau, &vi.Geom)
starIdx := vi.V1.NewMotionStar(fastIdx, fn, vi.Motion.Gain, &vi.Geom)
vi.Motion.FFScalarIndex = vi.V1.NewMotionFullField(starIdx, fn, &vi.Geom)
if vi.GetStar {
out4 := vi.V1.NewValues4D(int(vi.Geom.Out.Y), int(vi.Geom.Out.X), 2, 4)
vi.Star.SetShapeSizes(ndata, int(vi.Geom.Out.Y), int(vi.Geom.Out.X), 2, 4)
vi.V1.NewTo4D(starIdx, out4, 2, 4, 0, &vi.Geom)
}
vi.V1.SetAsCurrent()
if vi.GPU {
vi.V1.GPUInit()
}
}
// RunImages runs the configured filtering pipeline
// on given Image(s), using given [Image] handler.
func (vi *MotionDoG) RunImages(im *Image, imgs ...image.Image) {
im.SetImagesGrey(&vi.V1, int(vi.Geom.Border.X), imgs...)
vi.Run()
}
// Run runs the configured filtering pipeline
// on given Image tensor.
func (vi *MotionDoG) RunTensor(tsr *tensor.Float32) {
itsr := vi.V1.Images.SubSpace(0).(*tensor.Float32)
itsr.CopyFrom(tsr)
vi.Run()
}
// Run runs the configured filtering pipeline.
// image in vi.V1.Images[0] must already have been set.
func (vi *MotionDoG) Run() {
v1vision.UseGPU = vi.GPU
vi.V1.SetAsCurrent()
vals := []v1vision.GPUVars{v1vision.ScalarsVar, v1vision.ValuesVar}
if vi.GetStar {
vals = append(vals, v1vision.Values4DVar)
}
vi.V1.Run(vals...)
vi.Motion.FullFieldInteg(vi.V1.NData, vi.V1.Scalars, &vi.FullField)
if vi.GetStar { // assumes star at 0
vi.Star = vi.V1.Values4D.SubSpace(0).(*tensor.Float32)
}
}
// Init resets all motion integration values to 0.
func (vi *MotionDoG) Init() {
vi.V1.SetAsCurrent()
tensor.SetAllFloat64(vi.V1.Values, 0)
tensor.SetAllFloat64(vi.V1.Scalars, 0)
tensor.SetAllFloat64(&vi.FullField, 0)
vi.Motion.NormInteg = 0
vi.V1.ToGPUInfra()
}
// Copyright (c) 2025, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1std
import (
"image"
"cogentcore.org/core/math32"
"cogentcore.org/lab/tensor"
"github.com/emer/v1vision/gabor"
"github.com/emer/v1vision/kwta"
"github.com/emer/v1vision/v1vision"
)
// V1cColor does color V1 complex (V1c) filtering, starting with
// simple cells (V1s) and adding length sum and end stopping.
// KWTA inhibition operates on the V1s step.
// Call Defaults and then set any custom params, then call Config.
// Results are in Output tensor after Run(), which has a 4D shape.
type V1cColor struct {
// GPU means use the GPU by default (does GPU initialization) in Config.
// To change what is actually used at the moment of running,
// set [v1vision.UseGPU].
GPU bool
// SplitColor records separate rows in V1c simple summary for each color.
// Otherwise records the max across all colors.
SplitColor bool
// ColorGain is an extra gain for color channels,
// which are lower contrast in general.
ColorGain float32 `default:"8"`
// V1 simple gabor filter parameters
V1sGabor gabor.Filter
// V1sNeighInhib specifies neighborhood inhibition for V1s.
// Each unit gets inhibition from same feature in nearest orthogonal
// neighbors. Reduces redundancy of feature code.
V1sNeighInhib kwta.NeighInhib
// V1sKWTA has the kwta inhibition parameters for V1s.
V1sKWTA kwta.KWTA
// geometry of input, output for V1 simple-cell processing.
V1sGeom v1vision.Geom `edit:"-"`
// geometry of input, output for V1 complex-cell processing from V1s inputs.
V1cGeom v1vision.Geom `edit:"-"`
// V1 is the V1Vision filter processing system
V1 v1vision.V1Vision `display:"no-inline"`
// Output has the resulting V1c filter outputs, pointing to Values4D in V1.
// Inner Y, X dimensions are 5 x 4, where the 4 are the gabor angles
// (0, 45, 90, 135) and the 5 are: 1 length-sum, 2 directions of end-stop,
// and 2 polarities of V1simple.
Output *tensor.Float32 `display:"no-inline"`
}
func (vi *V1cColor) Defaults() {
vi.GPU = true
vi.ColorGain = 8
vi.SplitColor = true
vi.V1sGabor.Defaults()
vi.V1sNeighInhib.Defaults()
vi.V1sKWTA.Defaults()
vi.SetSize(12, 4)
}
// SetSize sets the V1sGabor filter size and geom spacing to given values.
// Default is 12, 4, for a medium-sized filter.
func (vi *V1cColor) SetSize(sz, spc int) {
vi.V1sGabor.SetSize(sz, spc)
vi.V1sGeom.Set(math32.Vec2i(0, 0), math32.Vec2i(spc, spc), math32.Vec2i(sz, sz))
}
// Config configures the filtering pipeline with all the current parameters.
// imageSize is the _content_ size of input image that is passed to Run
// as an RGB Tensor (per [V1Vision.Images] standard format),
// (i.e., exclusive of the additional border around the image = [Image.Size]).
// The resulting Geom.Border field can be passed to [Image] methods.
// ndata = number of data-parallel inputs to process in parallel.
func (vi *V1cColor) Config(ndata int, imageSize image.Point) {
vi.V1sGeom.SetImageSize(imageSize)
vi.V1.Init(ndata)
*vi.V1.NewKWTAParams() = vi.V1sKWTA
kwtaIdx := 0
img := vi.V1.NewImage(vi.V1sGeom.In.V())
wrap := vi.V1.NewImage(vi.V1sGeom.In.V())
lms := vi.V1.NewImage(vi.V1sGeom.In.V())
avgIdx := vi.V1.NewEdgeAvg(img, 3, int(vi.V1sGeom.Border.X), &vi.V1sGeom)
vi.V1.NewFadeImage(img, 3, wrap, int(vi.V1sGeom.Border.X), avgIdx, &vi.V1sGeom)
vi.V1.NewLMSOpponents(wrap, lms, vi.ColorGain, &vi.V1sGeom)
nang := vi.V1sGabor.NAngles
// V1s simple
ftyp := vi.V1.NewFilter(nang, vi.V1sGabor.Size, vi.V1sGabor.Size)
vi.V1.GaborToFilter(ftyp, &vi.V1sGabor)
inh := vi.V1.NewInhibs(int(vi.V1sGeom.Out.Y), int(vi.V1sGeom.Out.X))
lmsMap := [3]int{1, int(v1vision.RedGreen), int(v1vision.BlueYellow)}
var v1sIdxs [3]int
for irgb := range 3 {
out := vi.V1.NewConvolveImage(lms, lmsMap[irgb], ftyp, nang, vi.V1sGabor.Gain, &vi.V1sGeom)
v1out := out
if vi.V1sKWTA.On.IsTrue() {
ninh := 0
if vi.V1sNeighInhib.On {
ninh = vi.V1.NewNeighInhib4(out, nang, vi.V1sNeighInhib.Gi, &vi.V1sGeom)
}
v1out = vi.V1.NewKWTA(out, ninh, nang, kwtaIdx, inh, &vi.V1sGeom)
}
v1sIdxs[irgb] = v1out
}
mcout := vi.V1.NewValues(int(vi.V1sGeom.Out.Y), int(vi.V1sGeom.Out.X), nang)
vi.V1.NewMaxCopy(v1sIdxs[0], v1sIdxs[1], mcout, nang, &vi.V1sGeom)
vi.V1.NewMaxCopy(v1sIdxs[2], mcout, mcout, nang, &vi.V1sGeom)
// V1c complex
vi.V1cGeom.SetFilter(math32.Vec2i(0, 0), math32.Vec2i(2, 2), math32.Vec2i(2, 2), vi.V1sGeom.Out.V())
mpout := vi.V1.NewMaxPolarity(mcout, nang, &vi.V1sGeom)
pmpout := vi.V1.NewMaxPool(mpout, 1, nang, &vi.V1cGeom)
lsout := vi.V1.NewLenSum4(pmpout, nang, &vi.V1cGeom)
esout := vi.V1.NewEndStop4(pmpout, lsout, nang, &vi.V1cGeom)
// To4D
out4Rows := 5
if vi.SplitColor {
out4Rows = 9
}
out4 := vi.V1.NewValues4D(int(vi.V1cGeom.Out.Y), int(vi.V1cGeom.Out.X), out4Rows, nang)
vi.V1.NewTo4D(lsout, out4, 1, nang, 0, &vi.V1cGeom)
vi.V1.NewTo4D(esout, out4, 2, nang, 1, &vi.V1cGeom)
if vi.SplitColor {
poutg := vi.V1.NewMaxPool(v1sIdxs[0], 2, nang, &vi.V1cGeom)
poutrg := vi.V1.NewMaxPool(v1sIdxs[1], 2, nang, &vi.V1cGeom)
poutby := vi.V1.NewMaxPool(v1sIdxs[2], 2, nang, &vi.V1cGeom)
vi.V1.NewTo4D(poutg, out4, 2, nang, 3, &vi.V1cGeom)
vi.V1.NewTo4D(poutrg, out4, 2, nang, 5, &vi.V1cGeom)
vi.V1.NewTo4D(poutby, out4, 2, nang, 7, &vi.V1cGeom)
} else {
pout := vi.V1.NewMaxPool(mcout, 2, nang, &vi.V1cGeom)
vi.V1.NewTo4D(pout, out4, 2, nang, 3, &vi.V1cGeom)
}
vi.V1.SetAsCurrent()
if vi.GPU {
vi.V1.GPUInit()
}
}
// RunImages runs the configured filtering pipeline.
// on given Image(s), using given [Image] handler.
func (vi *V1cColor) RunImages(im *Image, imgs ...image.Image) {
vi.V1.SetAsCurrent()
v1vision.UseGPU = vi.GPU
im.SetImagesRGB(&vi.V1, int(vi.V1sGeom.Border.X), imgs...)
vi.V1.Run(v1vision.Values4DVar)
vi.Output = vi.V1.Values4D.SubSpace(0).(*tensor.Float32)
}
// Copyright (c) 2025, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1std
import (
"image"
"cogentcore.org/core/math32"
"cogentcore.org/lab/tensor"
"github.com/emer/v1vision/gabor"
"github.com/emer/v1vision/kwta"
"github.com/emer/v1vision/v1vision"
)
// V1cGrey does greyscale V1 complex (V1c) filtering, starting with
// simple cells (V1s) and adding length sum and end stopping.
// KWTA inhibition operates on the V1s step.
// Call Defaults and then set any custom params, then call Config.
// Results are in Output tensor after Run(), which has a 4D shape.
type V1cGrey struct {
// GPU means use the GPU by default (does GPU initialization) in Config.
// To change what is actually used at the moment of running,
// set [v1vision.UseGPU].
GPU bool
// V1 simple gabor filter parameters
V1sGabor gabor.Filter
// V1sNeighInhib specifies neighborhood inhibition for V1s.
// Each unit gets inhibition from same feature in nearest orthogonal
// neighbors. Reduces redundancy of feature code.
V1sNeighInhib kwta.NeighInhib
// V1sKWTA has the kwta inhibition parameters for V1s.
V1sKWTA kwta.KWTA
// geometry of input, output for V1 simple-cell processing.
V1sGeom v1vision.Geom `edit:"-"`
// geometry of input, output for V1 complex-cell processing from V1s inputs.
V1cGeom v1vision.Geom `edit:"-"`
// V1 is the V1Vision filter processing system
V1 v1vision.V1Vision `display:"no-inline"`
// Output has the resulting V1c filter outputs, pointing to Values4D in V1.
// Inner Y, X dimensions are 5 x 4, where the 4 are the gabor angles
// (0, 45, 90, 135) and the 5 are: 1 length-sum, 2 directions of end-stop,
// and 2 polarities of V1simple.
Output *tensor.Float32 `display:"no-inline"`
}
func (vi *V1cGrey) Defaults() {
vi.GPU = true
vi.V1sGabor.Defaults()
vi.V1sNeighInhib.Defaults()
vi.V1sKWTA.Defaults()
vi.SetSize(12, 4)
}
// SetSize sets the V1sGabor filter size and geom spacing to given values.
// Default is 12, 4, for a medium-sized filter.
func (vi *V1cGrey) SetSize(sz, spc int) {
vi.V1sGabor.SetSize(sz, spc)
vi.V1sGeom.Set(math32.Vec2i(0, 0), math32.Vec2i(spc, spc), math32.Vec2i(sz, sz))
}
// Config configures the filtering pipeline with all the current parameters.
// imageSize is the _content_ size of input image that is passed to Run
// as an RGB Tensor (per [V1Vision.Images] standard format),
// (i.e., exclusive of the additional border around the image = [Image.Size]).
// The resulting Geom.Border field can be passed to [Image] methods.
// ndata = number of data-parallel inputs to process in parallel.
func (vi *V1cGrey) Config(ndata int, imageSize image.Point) {
vi.V1sGeom.SetImageSize(imageSize)
vi.V1.Init(ndata)
*vi.V1.NewKWTAParams() = vi.V1sKWTA
kwtaIdx := 0
img := vi.V1.NewImage(vi.V1sGeom.In.V())
wrap := vi.V1.NewImage(vi.V1sGeom.In.V())
vi.V1.NewWrapImage(img, 0, wrap, int(vi.V1sGeom.Border.X), &vi.V1sGeom)
nang := vi.V1sGabor.NAngles
// V1s simple
_, out := vi.V1.NewGabor(wrap, 0, &vi.V1sGabor, &vi.V1sGeom)
v1out := out
if vi.V1sKWTA.On.IsTrue() {
ninh := 0
if vi.V1sNeighInhib.On {
ninh = vi.V1.NewNeighInhib4(out, nang, vi.V1sNeighInhib.Gi, &vi.V1sGeom)
}
inh := vi.V1.NewInhibs(int(vi.V1sGeom.Out.Y), int(vi.V1sGeom.Out.X))
v1out = vi.V1.NewKWTA(out, ninh, nang, kwtaIdx, inh, &vi.V1sGeom)
}
// V1c complex
vi.V1cGeom.SetFilter(math32.Vec2i(0, 0), math32.Vec2i(2, 2), math32.Vec2i(2, 2), vi.V1sGeom.Out.V())
pout := vi.V1.NewMaxPool(v1out, 2, nang, &vi.V1cGeom)
mpout := vi.V1.NewMaxPolarity(v1out, nang, &vi.V1sGeom)
pmpout := vi.V1.NewMaxPool(mpout, 1, nang, &vi.V1cGeom)
lsout := vi.V1.NewLenSum4(pmpout, nang, &vi.V1cGeom)
esout := vi.V1.NewEndStop4(pmpout, lsout, nang, &vi.V1cGeom)
// To4D
out4 := vi.V1.NewValues4D(int(vi.V1cGeom.Out.Y), int(vi.V1cGeom.Out.X), 5, nang)
vi.V1.NewTo4D(lsout, out4, 1, nang, 0, &vi.V1cGeom)
vi.V1.NewTo4D(esout, out4, 2, nang, 1, &vi.V1cGeom)
vi.V1.NewTo4D(pout, out4, 2, nang, 3, &vi.V1cGeom)
vi.V1.SetAsCurrent()
if vi.GPU {
vi.V1.GPUInit()
}
}
// RunImages runs the configured filtering pipeline.
// on given Image(s), using given [Image] handler.
func (vi *V1cGrey) RunImages(im *Image, imgs ...image.Image) {
vi.V1.SetAsCurrent()
v1vision.UseGPU = vi.GPU
im.SetImagesGrey(&vi.V1, int(vi.V1sGeom.Border.X), imgs...)
vi.V1.Run(v1vision.Values4DVar)
vi.Output = vi.V1.Values4D.SubSpace(0).(*tensor.Float32)
}
// Copyright (c) 2025, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1std
import (
"image"
"cogentcore.org/core/math32"
"cogentcore.org/lab/tensor"
"github.com/emer/v1vision/dog"
"github.com/emer/v1vision/gabor"
"github.com/emer/v1vision/kwta"
"github.com/emer/v1vision/v1vision"
)
// V1cParams has the parameters for a given size of V1c.
type V1cParams struct {
// Name is the name of this size.
Name string
// V1 simple gabor filter parameters.
V1sGabor gabor.Filter
// Zoom is the zoom factor: divides effective image size in setting params.
Zoom float32
// geometry of input, output for V1 simple-cell processing.
V1sGeom v1vision.Geom `edit:"-"`
// geometry of input, output for V1 complex-cell processing from V1s inputs.
V1cGeom v1vision.Geom `edit:"-"`
// Output contains this 4D filter output, in correct shape.
Output tensor.Float32
// Values4D index of output.
OutIdx int
gaborIdx int
}
// Config configures geometry and filter sizes. The border is used directly, and
// should be consistent within each over field-of-view size.
func (vp *V1cParams) Config(nm string, zoom float32, border, v1sSize, v1sSpace int) *V1cParams {
vp.Name = nm
vp.Zoom = zoom
vp.V1sGabor.Defaults()
vp.V1sGabor.SetSize(v1sSize, v1sSpace)
vp.V1sGeom.Set(math32.Vec2i(border, border), math32.Vec2i(v1sSpace, v1sSpace), math32.Vec2i(v1sSize, v1sSize))
return vp
}
// SetImageSize sets the image size accordingly, dividing by Zoom factor.
func (vp *V1cParams) SetImageSize(imageSize image.Point) {
isz := math32.FromPoint(imageSize).DivScalar(vp.Zoom).ToPointRound()
vp.V1sGeom.SetImageSize(isz)
}
func (vp *V1cParams) V1Config(vi *V1cMulti, lms, kwtaIdx int) {
nang := vp.V1sGabor.NAngles
// V1s simple
ftyp := vi.V1.NewFilter(nang, vp.V1sGabor.Size, vp.V1sGabor.Size)
vp.gaborIdx = ftyp
vi.V1.GaborToFilter(ftyp, &vp.V1sGabor)
inh := vi.V1.NewInhibs(int(vp.V1sGeom.Out.Y), int(vp.V1sGeom.Out.X))
lmsMap := [3]int{1, int(v1vision.RedGreen), int(v1vision.BlueYellow)}
var v1sIdxs [3]int
for irgb := range 3 {
out := vi.V1.NewConvolveImage(lms, lmsMap[irgb], ftyp, nang, vp.V1sGabor.Gain, &vp.V1sGeom)
v1out := out
if vi.V1sKWTA.On.IsTrue() {
ninh := 0
if vi.V1sNeighInhib.On {
ninh = vi.V1.NewNeighInhib4(out, nang, vi.V1sNeighInhib.Gi, &vp.V1sGeom)
}
v1out = vi.V1.NewKWTA(out, ninh, nang, kwtaIdx, inh, &vp.V1sGeom)
}
v1sIdxs[irgb] = v1out
}
mcout := vi.V1.NewValues(int(vp.V1sGeom.Out.Y), int(vp.V1sGeom.Out.X), nang)
vi.V1.NewMaxCopy(v1sIdxs[0], v1sIdxs[1], mcout, nang, &vp.V1sGeom)
vi.V1.NewMaxCopy(v1sIdxs[2], mcout, mcout, nang, &vp.V1sGeom)
// V1c complex
vp.V1cGeom.SetFilter(math32.Vec2i(0, 0), math32.Vec2i(2, 2), math32.Vec2i(2, 2), vp.V1sGeom.Out.V())
mpout := vi.V1.NewMaxPolarity(mcout, nang, &vp.V1sGeom)
pmpout := vi.V1.NewMaxPool(mpout, 1, nang, &vp.V1cGeom)
lsout := vi.V1.NewLenSum4(pmpout, nang, &vp.V1cGeom)
esout := vi.V1.NewEndStop4(pmpout, lsout, nang, &vp.V1cGeom)
// To4D
out4Rows := vi.Out4Rows()
out4 := vi.V1.NewValues4D(int(vp.V1cGeom.Out.Y), int(vp.V1cGeom.Out.X), out4Rows, nang)
vp.OutIdx = out4
vp.Output.SetShapeSizes(vi.V1.NData, int(vp.V1cGeom.Out.Y), int(vp.V1cGeom.Out.X), out4Rows, nang)
vi.V1.NewTo4D(lsout, out4, 1, nang, 0, &vp.V1cGeom)
vi.V1.NewTo4D(esout, out4, 2, nang, 1, &vp.V1cGeom)
if vi.SplitColor {
poutg := vi.V1.NewMaxPool(v1sIdxs[0], 2, nang, &vp.V1cGeom)
poutrg := vi.V1.NewMaxPool(v1sIdxs[1], 2, nang, &vp.V1cGeom)
poutby := vi.V1.NewMaxPool(v1sIdxs[2], 2, nang, &vp.V1cGeom)
vi.V1.NewTo4D(poutg, out4, 2, nang, 3, &vp.V1cGeom)
vi.V1.NewTo4D(poutrg, out4, 2, nang, 5, &vp.V1cGeom)
vi.V1.NewTo4D(poutby, out4, 2, nang, 7, &vp.V1cGeom)
} else {
pout := vi.V1.NewMaxPool(mcout, 2, nang, &vp.V1cGeom)
vi.V1.NewTo4D(pout, out4, 2, nang, 3, &vp.V1cGeom)
}
}
func (vp *V1cParams) UpdateFilter(vi *V1cMulti) {
vi.V1.GaborToFilter(vp.gaborIdx, &vp.V1sGabor)
}
// SetOutput sets the output for this filter.
func (vp *V1cParams) SetOutput(vi *V1cMulti) {
out := vi.V1.Values4D.SubSpace(vp.OutIdx).(*tensor.Float32)
tensor.CopyFromLargerShape(&vp.Output, out)
}
//////// DoG Color
// DoGColorParams has the parameters for a given size of DoG color.
type DoGColorParams struct {
// Name is the name of this size.
Name string
// DoG color filter parameters. Generally have larger fields,
// and no spatial tuning (i.e., OnSigma == OffSigma), consistent
// with blob cells.
DoG dog.Filter
// Zoom is the zoom factor: divides effective image size in setting params.
Zoom float32
// geometry of DoG color contrast outputs.
Geom v1vision.Geom `edit:"-"`
// Output contains this 4D filter output, in correct shape.
Output tensor.Float32
// Values4D indexes of output.
OutIdx int
dogIdx int
}
// Config configures geometry and filter sizes. The border is used directly, and
// should be consistent within each over field-of-view size.
func (vi *DoGColorParams) Config(nm string, zoom float32, border, dogSize int) *DoGColorParams {
vi.Name = nm
vi.Zoom = zoom
vi.DoG.Spacing = dogSize
vi.DoG.Size = dogSize
vi.DoG.Gain = 8 // color channels are weaker than grey
vi.DoG.OnGain = 1 // balanced
vi.DoG.SetSameSigma(0.5) // no spatial component, just pure contrast
vi.Geom.Set(math32.Vec2i(border, border), math32.Vec2i(dogSize, dogSize), math32.Vec2i(dogSize, dogSize))
return vi
}
// SetImageSize sets the image size accordingly, dividing by Zoom factor.
func (vp *DoGColorParams) SetImageSize(imageSize image.Point) {
isz := math32.FromPoint(imageSize).DivScalar(vp.Zoom).ToPointRound()
vp.Geom.SetImageSize(isz)
}
func (vp *DoGColorParams) V1Config(vi *V1cMulti, lmsRG, lmsBY, kwtaIdx int) {
out := vi.V1.NewValues(int(vp.Geom.Out.Y), int(vp.Geom.Out.X), 2)
dogFt := vi.V1.NewDoGOnOff(&vp.DoG, &vp.Geom)
vp.dogIdx = dogFt
vi.V1.NewConvolveDiff(lmsRG, v1vision.Red, lmsRG, v1vision.Green, dogFt, 0, 1, out, 0, 1, vp.DoG.OnGain, &vp.Geom)
vi.V1.NewConvolveDiff(lmsBY, v1vision.Blue, lmsBY, v1vision.Yellow, dogFt, 0, 1, out, 1, 1, vp.DoG.OnGain, &vp.Geom)
if vi.DoGKWTA.On.IsTrue() {
inh := vi.V1.NewInhibs(int(vp.Geom.Out.Y), int(vp.Geom.Out.X))
out = vi.V1.NewKWTA(out, 0, 2, kwtaIdx, inh, &vp.Geom)
}
// To4D
out4 := vi.V1.NewValues4D(int(vp.Geom.Out.Y), int(vp.Geom.Out.X), 2, 2)
vp.OutIdx = out4
vp.Output.SetShapeSizes(vi.V1.NData, int(vp.Geom.Out.Y), int(vp.Geom.Out.X), 2, 2)
vi.V1.NewTo4D(out, out4, 2, 2, 0, &vp.Geom)
}
func (vp *DoGColorParams) UpdateFilter(vi *V1cMulti) {
vi.V1.DoGOnOffToFilter(vp.dogIdx, &vp.DoG)
}
// SetOutput sets the output for this filter.
func (vp *DoGColorParams) SetOutput(vi *V1cMulti) {
out := vi.V1.Values4D.SubSpace(vp.OutIdx).(*tensor.Float32)
tensor.CopyFromLargerShape(&vp.Output, out)
}
//////// V1cMulti
// V1cMulti does color V1 complex (V1c) filtering and DoG color filtering
// across multiple different resolutions and filter sizes.
// V1c starts with simple cells (V1s) and adds length sum and end stopping.
// KWTA inhibition operates on the V1s step. DoG does Red-Green and Blue-Yellow
// color contrasts, capturing the chromatic response properties of color blob cells.
// Call Defaults and then set any custom params, then call Config.
// Results are in Output tensor after Run(), which has a 4D shape.
type V1cMulti struct {
// GPU means use the GPU by default (does GPU initialization) in Config.
// To change what is actually used at the moment of running,
// set [v1vision.UseGPU].
GPU bool
// SplitColor records separate rows in V1c simple summary for each color.
// Otherwise records the max across all colors.
SplitColor bool
// ColorGain is an extra gain for color channels,
// which are lower contrast in general.
ColorGain float32 `default:"8"`
// V1sNeighInhib specifies neighborhood inhibition for V1s.
// Each unit gets inhibition from same feature in nearest orthogonal
// neighbors. Reduces redundancy of feature code.
V1sNeighInhib kwta.NeighInhib
// V1sKWTA has the kwta inhibition parameters for V1s.
V1sKWTA kwta.KWTA
// DoGKWTA has the kwta inhibition parameters for DoG Color blobs.
DoGKWTA kwta.KWTA
// V1cParams has the configured geometries for different V1c sizes.
V1cParams []*V1cParams
// DoGParams has the configured geometries for different DoG color
// sizes.
DoGParams []*DoGColorParams
// V1 is the V1Vision filter processing system
V1 v1vision.V1Vision `display:"no-inline"`
// Image manages images.
Image Image
}
func (vi *V1cMulti) Defaults() {
vi.GPU = true
vi.ColorGain = 8
vi.SplitColor = true
vi.Image.Defaults()
vi.V1sNeighInhib.Defaults()
vi.V1sKWTA.Defaults()
vi.DoGKWTA.Defaults()
vi.DoGKWTA.Layer.On.SetBool(false) // non-spatial, mainly for differentiation within pools
vi.DoGKWTA.Pool.Gi = 1.2
}
func (vi *V1cMulti) AddV1cParams() *V1cParams {
gm := &V1cParams{}
vi.V1cParams = append(vi.V1cParams, gm)
return gm
}
func (vi *V1cMulti) AddDoGParams() *DoGColorParams {
gm := &DoGColorParams{}
vi.DoGParams = append(vi.DoGParams, gm)
return gm
}
// StdLowMed16DegZoom1 configures a standard 16 degree parafovial
// field of view (FOV), with Low and Medium resolution V1c filters
// and 1 level of spatial zoom (8 degrees),
// Along with corresponding low and medium resolution color DoGs.
// This operates on 128x128 image content.
func (vi *V1cMulti) StdLowMed16DegZoom1() {
vi.Image.Size = image.Point{128, 128}
// target full wrap/pad image size = 128 + 12 * 2 = 152
vi.AddV1cParams().Config("L16", 1, 12, 24, 8) // 128 / 8 = 16
vi.AddV1cParams().Config("M16", 1, 12, 12, 4) // 128 / 4 = 32
// vi.AddV1cParams().Config("H16", 1, 12, 6, 2) // not used in LVis small
// 64 + 44*2 = 152
vi.AddV1cParams().Config("M8", 2, 44, 12, 4)
vi.AddV1cParams().Config("H8", 2, 44, 6, 2)
vi.AddDoGParams().Config("L16", 1, 12, 16)
vi.AddDoGParams().Config("M16", 1, 12, 8)
vi.AddDoGParams().Config("L8", 2, 44, 8)
vi.AddDoGParams().Config("M8", 2, 44, 4)
}
// StdLowMed16DegNoDoG configures a standard 16 degree parafovial
// field of view (FOV), with Low and Medium resolution V1c filters.
// This operates on 128x128 image content.
func (vi *V1cMulti) StdLowMed16DegNoDoG() {
vi.Image.Size = image.Point{128, 128}
// target full wrap/pad image size = 128 + 12 * 2 = 152
vi.AddV1cParams().Config("L16", 1, 12, 24, 8) // 128 / 8 = 16
vi.AddV1cParams().Config("M16", 1, 12, 12, 4) // 128 / 4 = 32
}
func (vi *V1cMulti) Out4Rows() int {
out4Rows := 5
if vi.SplitColor {
out4Rows = 9
}
return out4Rows
}
// Config configures the filtering pipeline with all the current parameters.
// ndata = number of data-parallel inputs to process in parallel.
func (vi *V1cMulti) Config(ndata int) {
for _, vp := range vi.V1cParams {
vp.SetImageSize(vi.Image.Size)
}
for _, vp := range vi.DoGParams {
vp.SetImageSize(vi.Image.Size)
}
v1sGeom := &vi.V1cParams[0].V1sGeom
inSz := v1sGeom.In.V()
vi.V1.Init(ndata)
*vi.V1.NewKWTAParams() = vi.V1sKWTA
v1sKwtaIdx := 0
*vi.V1.NewKWTAParams() = vi.DoGKWTA
dogKwtaIdx := 1
img := vi.V1.NewImage(inSz)
wrap := vi.V1.NewImage(inSz)
lmsOp := vi.V1.NewImage(inSz)
lmsRG := vi.V1.NewImage(inSz)
lmsBY := vi.V1.NewImage(inSz)
_, _ = lmsRG, lmsBY
avgIdx := vi.V1.NewEdgeAvg(img, 3, int(v1sGeom.Border.X), v1sGeom)
vi.V1.NewFadeImage(img, 3, wrap, int(v1sGeom.Border.X), avgIdx, v1sGeom)
vi.V1.NewLMSOpponents(wrap, lmsOp, vi.ColorGain, v1sGeom)
if len(vi.DoGParams) > 0 {
dogGeom := &vi.DoGParams[0].Geom
vi.V1.NewLMSComponents(wrap, lmsRG, lmsBY, vi.ColorGain, dogGeom)
}
for _, vp := range vi.V1cParams {
vp.V1Config(vi, lmsOp, v1sKwtaIdx)
}
for _, vp := range vi.DoGParams {
vp.V1Config(vi, lmsRG, lmsBY, dogKwtaIdx)
}
// critical to go back and fix all the filters.
for _, vp := range vi.V1cParams {
vp.UpdateFilter(vi)
}
for _, vp := range vi.DoGParams {
vp.UpdateFilter(vi)
}
vi.V1.SetAsCurrent()
if vi.GPU {
vi.V1.GPUInit()
}
}
// RunImages runs the configured filtering pipeline.
// on given Image(s), using given [Image] handler.
func (vi *V1cMulti) RunImages(imgs ...image.Image) {
vi.V1.SetAsCurrent()
v1vision.UseGPU = vi.GPU
v1sGeom := &vi.V1cParams[0].V1sGeom
vi.Image.SetImagesRGB(&vi.V1, int(v1sGeom.Border.X), imgs...)
vi.V1.Run(v1vision.Values4DVar)
for _, vp := range vi.V1cParams {
vp.SetOutput(vi)
}
for _, vp := range vi.DoGParams {
vp.SetOutput(vi)
}
}
// Code generated by "goal build"; DO NOT EDIT.
//line complex.goal:1
// Copyright (c) 2025, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1vision
// NewLenSum4 adds a [LenSum4] operation, from in value -> out value.
// fn is number of filters (innermost values dimension) -- must be 4!.
// Operates on [MaxPolarity] output so only uses 0 polarity value.
// Output size is geom.Out, fn. Returns out index.
func (vv *V1Vision) NewLenSum4(in, fn int, geom *Geom) int {
if fn != 4 {
panic("only 4 angles are currently supported for LenSum4!")
}
op := vv.NewOp()
op.Op = LenSum4
out := vv.NewValues(int(geom.Out.Y), int(geom.Out.X), fn)
op.RunN = uint32(geom.Out.Y * geom.Out.X * int32(fn))
op.InValue = int32(in)
op.OutValue = int32(out)
op.FilterN = int32(fn)
op.Geom = *geom
return out
}
// NewEndStop4 adds a [EndStop4] operation, from in value -> out value.
// fn is number of filters (innermost values dimension) -- must be 4!.
// in = [MaxPolarity] output, inLenSum = output of [LenSum4] (required!)
// Output size is geom.Out, fn, with polarity = direction. Returns out index.
func (vv *V1Vision) NewEndStop4(in, inLenSum, fn int, geom *Geom) int {
if fn != 4 {
panic("only 4 angles are currently supported for EndStop4!")
}
op := vv.NewOp()
op.Op = EndStop4
out := vv.NewValues(int(geom.Out.Y), int(geom.Out.X), fn)
op.RunN = uint32(geom.Out.Y * geom.Out.X * int32(fn) * 2)
op.InValue = int32(in)
op.InValue2 = int32(inLenSum)
op.OutValue = int32(out)
op.FilterN = int32(fn)
op.Geom = *geom
return out
}
//gosl:start
// LenSum4 is kernel.
func (op *Op) LenSum4(i, ni int32) {
szX := op.Geom.Out.X
szY := op.Geom.Out.Y
ang := i % op.FilterN // inner
ii := i / op.FilterN
yo := ii / szX
xo := ii % szX
var ox, oy int32
LenSumOffsets(ang, &ox, &oy)
norm := float32(1) / 3
ctr := Values.Value(int(op.InValue), int(ni), int(yo), int(xo), int(0), int(ang))
lp := float32(0)
ln := float32(0)
lpX := xo + ox
lpY := yo + oy
if lpX >= 0 && lpX < szX && lpY >= 0 && lpY < szY {
lp = Values.Value(int(op.InValue), int(ni), int(lpY), int(lpX), int(0), int(ang))
}
lnX := xo - ox
lnY := yo - oy
if lnX >= 0 && lnX < szX && lnY >= 0 && lnY < szY {
ln = Values.Value(int(op.InValue), int(ni), int(lnY), int(lnX), int(0), int(ang))
}
ls := norm * (ctr + lp + ln)
Values.Set(ls, int(op.OutValue), int(ni), int(yo), int(xo), int(0), int(ang))
}
// EndStop4 is kernel.
func (op *Op) EndStop4(i, ni int32) {
szX := op.Geom.Out.X
szY := op.Geom.Out.Y
ang := i % op.FilterN // inner
pii := i / op.FilterN
pi := pii % 2 // plus-minus
ii := pii / 2
yo := ii / szX
xo := ii % szX
var ox, oy int32
LenSumOffsets(ang, &ox, &oy)
dsign := int32(1)
if pi > 0 {
dsign = -1
}
ls := float32(0)
// length-sum point is "left" (negative) direction from ctr
lnX := xo - dsign*ox
lnY := yo - dsign*oy
if lnX >= 0 && lnX < szX && lnY >= 0 && lnY < szY {
ls = Values.Value(int(op.InValue2), int(ni), int(lnY), int(lnX), int(0), int(ang))
}
offMax := float32(0)
for oi := int32(0); oi < 3; oi++ {
var ox, oy int32
EndStopOffsets(ang, oi, &ox, &oy)
ofX := xo + dsign*ox
ofY := yo + dsign*oy
if ofX >= 0 && ofX < szX && ofY >= 0 && ofY < szY {
off := Values.Value(int(op.InValue), int(ni), int(ofY), int(ofX), int(0), int(ang))
offMax = max(offMax, off)
}
}
es := ls - offMax // simple diff
if es < 0.2 { // note: builtin threshold
es = 0
}
Values.Set(es, int(op.OutValue), int(ni), int(yo), int(xo), int(pi), int(ang))
}
// Line4X = []int{1, 1, 0, 1}
// Line4Y = []int{0, 1, 1, -1}
// linear neighbor coordinates for 4 angles, also uses negated version
// -- = (1,0) (X,Y)
// / = (1,1)
// | = (0,1)
// \ = (1,-1)
func LenSumOffsets(ang int32, ox, oy *int32) {
switch ang {
case 2:
*ox = 0
default:
*ox = 1
}
switch ang {
case 1, 2:
*oy = 1
case 3:
*oy = -1
default:
*oy = 0
}
}
// EndStopOff4X = []int{
// 1, 1, 1,
// 0, 1, 1,
// -1, 0, 1,
// 0, 1, 1}
// EndStopOff4Y = []int{
// 1, 0, -1,
// 1, 1, 0,
// 1, 1, 1,
// -1, -1, 0}
// end-stop off coordinates for 4 angles, also uses negated versions
// these go with the negative versions of Line4X (i.e., are in same dir)
// -- | = (1,1), (1,0), (1,-1) (X,Y)
// --|
// / = (0,1), (1,1), (1,0)
// ---
//
// | = (-1,1), (0,1), (1,1)
//
// \ = (0,-1), (1,-1), (1,0)
// --|
func EndStopOffsets(ang, oi int32, ox, oy *int32) {
i := ang*3 + oi
switch i {
case 6:
*ox = -1
case 3, 7, 9:
*ox = 0
default:
*ox = 1
}
switch i {
case 1, 5, 11:
*oy = 0
case 2, 9, 10:
*oy = -1
default:
*oy = 1
}
}
//gosl:end
// Code generated by "goal build"; DO NOT EDIT.
//line convolve.goal:1
// Copyright (c) 2025, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1vision
// NewConvolveImage adds a [ConvolveImage] operation,
// operating on given image input index and rgb pane,
// and given filter type and number of filters, applying given gain factor.
// Adds a output values of shape [geom.Out.Y, .X, 2, fn] and returns index.
// The input Image *must* have border (padding) so that filters are
// applied without any bounds checking: wrapping etc is all
// done in the padding process, which is much more efficient.
func (vv *V1Vision) NewConvolveImage(in, irgb, ftyp, fn int, gain float32, geom *Geom) int {
op := vv.NewOp()
op.Op = ConvolveImage
out := vv.NewValues(int(geom.Out.Y), int(geom.Out.X), fn)
op.RunN = uint32(geom.Out.Y * geom.Out.X * int32(fn))
op.InImage = int32(in)
op.InImageRGB = int32(irgb)
op.OutValue = int32(out)
op.FilterType = int32(ftyp)
op.FilterN = int32(fn)
op.FloatArg1 = gain
op.Geom = *geom
return out
}
// NewConvolveDiff adds a [ConvolveDiff] operation,
// operating on given image, rgb pane inputs (1 = on, 2 = off),
// and given filter type and filter index within that type,
// outputs to given values, into outfi filter index (both polarities).
// gain applies to everything and gainOn applies to positive values.
// Filters must have geom.FilterSize size.
// The input Image *must* have border (padding) so that filters are
// applied without any bounds checking: wrapping etc is all
// done in the padding process, which is much more efficient.
func (vv *V1Vision) NewConvolveDiff(in1, rgb1, in2, rgb2, ftyp, fidx1, fidx2, out, outfi int, gain, gainOn float32, geom *Geom) int {
op := vv.NewOp()
op.Op = ConvolveDiff
op.RunN = uint32(geom.Out.Y * geom.Out.X)
op.InImage = int32(in1)
op.InImageRGB = int32(rgb1)
op.InValue2 = int32(in2)
op.OutImage2 = int32(rgb2)
op.FilterType = int32(ftyp)
op.FilterN = int32(fidx1)
op.IntArg1 = int32(fidx2)
op.FloatArg1 = gain
op.FloatArg2 = gainOn
op.OutValue = int32(out)
op.OutScalar = int32(outfi)
op.Geom = *geom
return out
}
//gosl:start
// ConvolveImage is the kernel for Convolve on Image data.
func (op *Op) ConvolveImage(i, ni int32) {
fi := i % op.FilterN // inner
ii := i / op.FilterN
yo := ii / op.Geom.Out.X
xo := ii % op.Geom.Out.X
istX := op.Geom.Border.X - op.Geom.FilterLt.X
istY := op.Geom.Border.Y - op.Geom.FilterLt.Y
yi := int(istY + yo*op.Geom.Spacing.Y)
xi := int(istX + xo*op.Geom.Spacing.X)
fyn := int(op.Geom.FilterSize.Y)
fxn := int(op.Geom.FilterSize.X)
sum := float32(0)
for fy := range fyn {
for fx := range fxn {
iv := Images.Value(int(op.InImage), int(ni), int(op.InImageRGB), int(yi+fy), int(xi+fx))
fv := Filters.Value(int(op.FilterType), int(fi), int(fy), int(fx))
sum += fv * iv
}
}
sum *= op.FloatArg1
if sum > 0 {
Values.Set(sum, int(op.OutValue), int(ni), int(yo), int(xo), int(0), int(fi))
Values.Set(0.0, int(op.OutValue), int(ni), int(yo), int(xo), int(1), int(fi))
} else {
Values.Set(0.0, int(op.OutValue), int(ni), int(yo), int(xo), int(0), int(fi))
Values.Set(-sum, int(op.OutValue), int(ni), int(yo), int(xo), int(1), int(fi))
}
}
// ConvolveDiff is the kernel.
func (op *Op) ConvolveDiff(i, ni int32) {
yo := i / op.Geom.Out.X
xo := i % op.Geom.Out.X
fi := op.OutScalar
istX := op.Geom.Border.X - op.Geom.FilterLt.X
istY := op.Geom.Border.Y - op.Geom.FilterLt.Y
yi := int(istY + yo*op.Geom.Spacing.Y)
xi := int(istX + xo*op.Geom.Spacing.X)
fyn := int(op.Geom.FilterSize.Y)
fxn := int(op.Geom.FilterSize.X)
sumOn := float32(0)
sumOff := float32(0)
for fy := range fyn {
for fx := range fxn {
iv1 := Images.Value(int(op.InImage), int(ni), int(op.InImageRGB), int(yi+fy), int(xi+fx))
iv2 := Images.Value(int(op.InValue2), int(ni), int(op.OutImage2), int(yi+fy), int(xi+fx))
fv1 := Filters.Value(int(op.FilterType), int(op.FilterN), int(fy), int(fx))
fv2 := Filters.Value(int(op.FilterType), int(op.IntArg1), int(fy), int(fx))
sumOn += fv1 * iv1
sumOff += fv2 * iv2
}
}
diff := op.FloatArg1 * (op.FloatArg2*sumOn - sumOff)
if diff > 0 {
Values.Set(diff, int(op.OutValue), int(ni), int(yo), int(xo), int(0), int(fi))
Values.Set(0.0, int(op.OutValue), int(ni), int(yo), int(xo), int(1), int(fi))
} else {
Values.Set(-diff, int(op.OutValue), int(ni), int(yo), int(xo), int(1), int(fi))
Values.Set(0.0, int(op.OutValue), int(ni), int(yo), int(xo), int(0), int(fi))
}
}
//gosl:end
// Copyright (c) 2025, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1vision
import (
"cogentcore.org/lab/tensor"
"github.com/emer/v1vision/dog"
)
// NewDoG adds given [dog.Filter] Net filter to Filters, for
// spatial contrast filtering.
// Returns the filter type index in Filters and the output values
// configured for storing the output of running these filters,
// per the given [Geom] output size. Adds a [ConvolveImage] operation
// for this DoG filtering step, from given input image index,
// and irgb color channel (0-2).
func (vv *V1Vision) NewDoG(in, irgb int, df *dog.Filter, geom *Geom) (ftyp, out int) {
ftyp = vv.NewFilter(1, df.Size, df.Size)
vv.DoGToFilter(ftyp, df)
out = vv.NewConvolveImage(in, irgb, ftyp, 1, df.Gain, geom)
return
}
// DoGToFilter sets the given [dog.Filter] Net filter to given
// filter type index. If more filters are added after NewDoG is called
// then need to go back at the end and call all the ToFilter methods,
// in case the filters tensor has been resized.
func (vv *V1Vision) DoGToFilter(ftyp int, df *dog.Filter) {
flt := vv.Filters.SubSpace(ftyp).(*tensor.Float32)
df.ToTensor(flt, dog.Net)
}
// NewDoGOnOff adds given [dog.Filter] On and Off filters to Filters,
// for color contrast filtering, applying On and Off to different color
// channels. Returns the filter type index.
func (vv *V1Vision) NewDoGOnOff(df *dog.Filter, geom *Geom) int {
ftyp := vv.NewFilter(2, df.Size, df.Size)
vv.DoGOnOffToFilter(ftyp, df)
return ftyp
}
// DoGOnOffToFilter sets the given [dog.Filter] On and Off filters to given
// filter type index. If more filters are added after NewDoGOnOff is called
// then need to go back at the end and call all the ToFilter methods,
// in case the filters tensor has been resized.
func (vv *V1Vision) DoGOnOffToFilter(ftyp int, df *dog.Filter) {
flt := vv.Filters.SubSpace(ftyp).(*tensor.Float32)
df.ToTensor(flt, dog.On, dog.Off)
}
// Code generated by "core generate -add-types -gosl"; DO NOT EDIT.
package v1vision
import (
"cogentcore.org/core/enums"
)
var _GPUVarsValues = []GPUVars{0, 1, 2, 3, 4, 5, 6, 7}
// GPUVarsN is the highest valid value for type GPUVars, plus one.
//
//gosl:start
const GPUVarsN GPUVars = 8
//gosl:end
var _GPUVarsValueMap = map[string]GPUVars{`CurOpVar`: 0, `KWTAsVar`: 1, `FiltersVar`: 2, `ImagesVar`: 3, `ValuesVar`: 4, `Values4DVar`: 5, `ScalarsVar`: 6, `InhibsVar`: 7}
var _GPUVarsDescMap = map[GPUVars]string{0: ``, 1: ``, 2: ``, 3: ``, 4: ``, 5: ``, 6: ``, 7: ``}
var _GPUVarsMap = map[GPUVars]string{0: `CurOpVar`, 1: `KWTAsVar`, 2: `FiltersVar`, 3: `ImagesVar`, 4: `ValuesVar`, 5: `Values4DVar`, 6: `ScalarsVar`, 7: `InhibsVar`}
// String returns the string representation of this GPUVars value.
func (i GPUVars) String() string { return enums.String(i, _GPUVarsMap) }
// SetString sets the GPUVars value from its string representation,
// and returns an error if the string is invalid.
func (i *GPUVars) SetString(s string) error {
return enums.SetString(i, s, _GPUVarsValueMap, "GPUVars")
}
// Int64 returns the GPUVars value as an int64.
func (i GPUVars) Int64() int64 { return int64(i) }
// SetInt64 sets the GPUVars value from an int64.
func (i *GPUVars) SetInt64(in int64) { *i = GPUVars(in) }
// Desc returns the description of the GPUVars value.
func (i GPUVars) Desc() string { return enums.Desc(i, _GPUVarsDescMap) }
// GPUVarsValues returns all possible values for the type GPUVars.
func GPUVarsValues() []GPUVars { return _GPUVarsValues }
// Values returns all possible values for the type GPUVars.
func (i GPUVars) Values() []enums.Enum { return enums.Values(_GPUVarsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i GPUVars) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *GPUVars) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "GPUVars") }
var _InhibVarsValues = []InhibVars{0, 1, 2, 3, 4, 5, 6, 7, 8}
// InhibVarsN is the highest valid value for type InhibVars, plus one.
//
//gosl:start
const InhibVarsN InhibVars = 9
//gosl:end
var _InhibVarsValueMap = map[string]InhibVars{`FFi`: 0, `FBi`: 1, `Gi`: 2, `GiOrig`: 3, `LayGi`: 4, `GeAvg`: 5, `GeMax`: 6, `ActAvg`: 7, `ActMax`: 8}
var _InhibVarsDescMap = map[InhibVars]string{0: `computed feedforward inhibition`, 1: `computed feedback inhibition (total)`, 2: `overall value of the inhibition. This is what is added into the unit Gi inhibition level (along with any synaptic unit-driven inhibition)`, 3: `original value of the inhibition (before pool or other effects)`, 4: `for pools, this is the layer-level inhibition that is MAX'd with the pool-level inhibition to produce the net inhibition.`, 5: `average Ge excitatory conductance values, which drive FF inhibition`, 6: `max Ge excitatory conductance values, which drive FF inhibition`, 7: `average Act activation values, which drive FB inhibition`, 8: `max Act activation values, which drive FB inhibition`}
var _InhibVarsMap = map[InhibVars]string{0: `FFi`, 1: `FBi`, 2: `Gi`, 3: `GiOrig`, 4: `LayGi`, 5: `GeAvg`, 6: `GeMax`, 7: `ActAvg`, 8: `ActMax`}
// String returns the string representation of this InhibVars value.
func (i InhibVars) String() string { return enums.String(i, _InhibVarsMap) }
// SetString sets the InhibVars value from its string representation,
// and returns an error if the string is invalid.
func (i *InhibVars) SetString(s string) error {
return enums.SetString(i, s, _InhibVarsValueMap, "InhibVars")
}
// Int64 returns the InhibVars value as an int64.
func (i InhibVars) Int64() int64 { return int64(i) }
// SetInt64 sets the InhibVars value from an int64.
func (i *InhibVars) SetInt64(in int64) { *i = InhibVars(in) }
// Desc returns the description of the InhibVars value.
func (i InhibVars) Desc() string { return enums.Desc(i, _InhibVarsDescMap) }
// InhibVarsValues returns all possible values for the type InhibVars.
func InhibVarsValues() []InhibVars { return _InhibVarsValues }
// Values returns all possible values for the type InhibVars.
func (i InhibVars) Values() []enums.Enum { return enums.Values(_InhibVarsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i InhibVars) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *InhibVars) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "InhibVars")
}
var _OperationsValues = []Operations{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}
// OperationsN is the highest valid value for type Operations, plus one.
//
//gosl:start
const OperationsN Operations = 24
//gosl:end
var _OperationsValueMap = map[string]Operations{`NoOp`: 0, `WrapPad`: 1, `EdgeAvg`: 2, `FadePad`: 3, `LMSOpponents`: 4, `LMSComponents`: 5, `ConvolveImage`: 6, `ConvolveDiff`: 7, `LogValues`: 8, `MaxScalar`: 9, `SumScalar`: 10, `MeanScalar`: 11, `NormDiv`: 12, `NeighInhib4`: 13, `KWTAInhib`: 14, `MaxPool`: 15, `MaxPolarity`: 16, `MaxCopy`: 17, `LenSum4`: 18, `EndStop4`: 19, `To4D`: 20, `MotionIntegrate`: 21, `MotionStar`: 22, `MotionFullField`: 23}
var _OperationsDescMap = map[Operations]string{0: ``, 1: `WrapPad wraps given padding width of float32 image around sides i.e., padding for left side of image is the (mirrored) bits from the right side of image, etc. InImage -> OutImage, over InImageRGB (if 3, does all).`, 2: `EdgeAvg computes the average r,g,b values around the edges of an image, storing into Scalars. These are then used for FadePad.`, 3: `FadePad wraps given padding width of float32 image around sides i.e., padding for left side of image is the (mirrored) bits from the right side of image, etc, and fades result toward average edge value (passed in as arg). InImage -> OutImage, over InImageRGB (if 3, does all).`, 4: `LMSOpponents computes Long-Medium-Short (RGB) perceptually-based color opponent values from InImage -> OutImage. 0 = RedGreen (L-M), 1 = White-Black (grey), 2 = BlueYellow (S-(LM)),`, 5: `LMSComponents computes Long-Medium-Short (RGB) perceptually-based color component values from InImage -> OutImage1, OutImage2. For each image, the organization of components is designed to align with the RGB components, using grey to fill in the extra bit. Image1: 0 = Red (L), 1 = Green (M), 2 = Grey Image2: 0 = Yellow (LM), 1 = Grey, 2 = Blue (S),`, 6: `ConvolveImage applies a filter to Image, writing to Values. InImage -> OutValue, using FilterType, FilterN`, 7: `ConvolveDiff applies two different filters to two different [Image, component] inputs, computing their difference, with positive values in 0 and negative values in 1 polarity, at given feature dimension (innermost Values dimension). This is used to compute e.g., on-center DoG to one color component minus off-center to another component.`, 8: `LogValues sets values to 1 + log of values * Gain. InValue -> OutValue (can be the same).`, 9: `MaxScalar computes Max over values. InValue = values, OutScalar = result.`, 10: `SumScalar computes Sum over values InValue = values, OutScalar = result.`, 11: `MeanScalar computes Mean over values InValue = values, OutScalar = result.`, 12: `NormDiv normalizes values by scalar InValue -> OutValue (can be same), InScalar = norm factor.`, 13: `NeighInhib4 computes neighbor inhibition, as an optional preliminary step prior to KWTA. Currently only works with 4 angles (n features=4). Each unit gets inhibition from same feature in nearest orthogonal neighbors. Reduces redundancy of feature code.`, 14: `KWTAInhib computes k-winners-take-all inhibition, rate-code version, based on overall levels of activity, over multiple iterations.`, 15: `MaxPool performs max-pooling over given pool size and spacing, effectively reducing the dimensionality of the output by the spacing factor. Size must = spacing or 2 * spacing.`, 16: `MaxPolarity performs max-pooling over the polarity (on vs. off) dimension.`, 17: `MaxCopy performs simple max over 2 different values, for aggregating different channels (e.g., colors) into a summary, without changing the dimensionality.`, 18: `LenSum4 performs V1 complex-cell length-summing, extending the receptive field along the orientation angle one step. Works on output from [MaxPolarity] (first polarity dimension), only for the 4 angles case.`, 19: `EndStop4 performs V1 complex-cell end-stop, detecting an orthoginal angle at the end of a length-sum line. Only for the 4 angles case.`, 20: `To4D copies from Values to Values4D for aggregating final results across multiple feature dimensions (e.g., for assembling full V1 complex).`, 21: `MotionIntegrate does fast and slow motion integration from values to values: InValue -> OutValue (should be different)`, 22: `MotionStar computes starburst-style motion on integrated fast and slow input values. Result is 4 * FilterN filter outputs, for Left, Right, Down, Up motion directions. InValue -> OutValue (different, X and Y are -1 in output).`, 23: `MotionFullField computes full-field summary of output from MotionStar, into 4 Scalars for Left, Right, Down, Up. Opposite directions compete. OutScalar[0-3] = instantaneous full-field values per this frame OutScalar[4-7] = integrated full-field values over time`}
var _OperationsMap = map[Operations]string{0: `NoOp`, 1: `WrapPad`, 2: `EdgeAvg`, 3: `FadePad`, 4: `LMSOpponents`, 5: `LMSComponents`, 6: `ConvolveImage`, 7: `ConvolveDiff`, 8: `LogValues`, 9: `MaxScalar`, 10: `SumScalar`, 11: `MeanScalar`, 12: `NormDiv`, 13: `NeighInhib4`, 14: `KWTAInhib`, 15: `MaxPool`, 16: `MaxPolarity`, 17: `MaxCopy`, 18: `LenSum4`, 19: `EndStop4`, 20: `To4D`, 21: `MotionIntegrate`, 22: `MotionStar`, 23: `MotionFullField`}
// String returns the string representation of this Operations value.
func (i Operations) String() string { return enums.String(i, _OperationsMap) }
// SetString sets the Operations value from its string representation,
// and returns an error if the string is invalid.
func (i *Operations) SetString(s string) error {
return enums.SetString(i, s, _OperationsValueMap, "Operations")
}
// Int64 returns the Operations value as an int64.
func (i Operations) Int64() int64 { return int64(i) }
// SetInt64 sets the Operations value from an int64.
func (i *Operations) SetInt64(in int64) { *i = Operations(in) }
// Desc returns the description of the Operations value.
func (i Operations) Desc() string { return enums.Desc(i, _OperationsDescMap) }
// OperationsValues returns all possible values for the type Operations.
func OperationsValues() []Operations { return _OperationsValues }
// Values returns all possible values for the type Operations.
func (i Operations) Values() []enums.Enum { return enums.Values(_OperationsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Operations) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Operations) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "Operations")
}
// Copyright (c) 2025, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1vision
import (
"cogentcore.org/lab/tensor"
"github.com/emer/v1vision/gabor"
)
// NewGabor adds given [gabor.Filter] to Filters, returning the
// filter type index in Filters and the output values configured
// for storing the output of running these filters, per the
// given [Geom] output size. Adds a [ConvolveImage] operation
// for this Gabor filtering step, from given input image index,
// and irgb color channel (0-2).
func (vv *V1Vision) NewGabor(in, irgb int, gf *gabor.Filter, geom *Geom) (ftyp, out int) {
ftyp = vv.NewFilter(gf.NAngles, gf.Size, gf.Size)
vv.GaborToFilter(ftyp, gf)
out = vv.NewConvolveImage(in, irgb, ftyp, gf.NAngles, gf.Gain, geom)
return
}
// GaborToFilter sets the given [gabor.Filter] filter to given
// filter type index. If more filters are added after NewGabor called
// then need to go back at the end and call all the ToFilter methods,
// in case the filters tensor has been resized.
func (vv *V1Vision) GaborToFilter(ftyp int, gf *gabor.Filter) {
flt := vv.Filters.SubSpace(ftyp).(*tensor.Float32)
gf.ToTensor(flt)
}
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1vision
import (
"image"
"cogentcore.org/core/math32"
"cogentcore.org/lab/gosl/slvec"
)
//gosl:start
// Geom contains the filtering geometry info for a given filter pass.
type Geom struct {
// size of input -- computed from image or set
In slvec.Vector2i
// size of output -- computed
Out slvec.Vector2i
// starting border into image -- must be >= FilterRt for images.
Border slvec.Vector2i
// spacing -- number of pixels to skip in each direction
Spacing slvec.Vector2i
// full size of filter
FilterSize slvec.Vector2i
// computed size of left/top size of filter
FilterLt slvec.Vector2i
// computed size of right/bottom size of filter (FilterSize - FilterLeft)
FilterRt slvec.Vector2i
}
//gosl:end
// Set sets the geometry params for convolution.
func (ge *Geom) Set(border, spacing, filtSz math32.Vector2i) {
ge.Border.SetV(border)
ge.Spacing.SetV(spacing)
ge.FilterSize.SetV(filtSz)
ge.UpdtFilter()
}
// SetImage sets the geometry params for image convolution.
// Calls Set and SetImageSize.
func (ge *Geom) SetImage(border, spacing, filtSz math32.Vector2i, imSize image.Point) {
ge.Set(border, spacing, filtSz)
ge.SetImageSize(imSize)
}
// SetFilter sets the geometry params for misc filtering on a
// given input size. Calls Set and SetInputSize. Does not set
// borders to fit the filter half-width.
func (ge *Geom) SetFilter(border, spacing, filtSz, inSize math32.Vector2i) {
ge.Set(border, spacing, filtSz)
ge.SetInputSize(inSize)
}
// LeftHalf returns the left / top half of a filter
func LeftHalf(x int32) int32 {
if x%2 == 0 {
return x / 2
}
return (x - 1) / 2
}
// UpdtFilter updates filter sizes, and ensures that Border >= FilterRt
func (ge *Geom) UpdtFilter() {
ge.FilterLt.X = LeftHalf(ge.FilterSize.X)
ge.FilterLt.Y = LeftHalf(ge.FilterSize.Y)
ge.FilterRt.SetV(ge.FilterSize.V().Sub(ge.FilterLt.V()))
}
// BorderMinFilter sets the border size to be at least FilterRt.
// This is needed for convolutional operations on images for example.
func (ge *Geom) BorderMinFilter() {
if ge.Border.X < ge.FilterRt.X {
ge.Border.X = ge.FilterRt.X
}
if ge.Border.Y < ge.FilterRt.Y {
ge.Border.Y = ge.FilterRt.Y
}
}
// SetImageSize sets the original image input size that excludes
// the border size, so this adds 2* border to that for the total
// input size. First calls BorderMinFilter to ensure filter fits.
func (ge *Geom) SetImageSize(imSize image.Point) {
ge.BorderMinFilter()
in := math32.Vec2i(imSize.X, imSize.Y)
b2 := ge.Border.V().MulScalar(2)
av := in.Add(b2)
ge.In.SetV(av)
ge.Out.SetV(in.Div(ge.Spacing.V()))
}
// SetInputSize sets the input size, and computes output from that.
// Out = In / Spacing
func (ge *Geom) SetInputSize(inSize math32.Vector2i) {
ge.In.SetV(inSize)
b2 := ge.Border.V().MulScalar(2)
av := ge.In.V().Sub(b2)
ge.Out.SetV(av.Div(ge.Spacing.V()))
}
// Code generated by "gosl"; DO NOT EDIT
package v1vision
import (
"embed"
"fmt"
"math"
"unsafe"
"cogentcore.org/core/gpu"
"cogentcore.org/lab/tensor"
)
//go:embed shaders/*.wgsl
var shaders embed.FS
var (
// GPUInitialized is true once the GPU system has been initialized.
// Prevents multiple initializations.
GPUInitialized bool
// ComputeGPU is the compute gpu device.
// Set this prior to calling GPUInit() to use an existing device.
ComputeGPU *gpu.GPU
// BorrowedGPU is true if our ComputeGPU is set externally,
// versus created specifically for this system. If external,
// we don't release it.
BorrowedGPU bool
// UseGPU indicates whether to use GPU vs. CPU.
UseGPU bool
)
// GPUSystem is a GPU compute System with kernels operating on the
// same set of data variables.
var GPUSystem *gpu.ComputeSystem
// GPUVars is an enum for GPU variables, for specifying what to sync.
type GPUVars int32 //enums:enum
const (
CurOpVar GPUVars = 0
KWTAsVar GPUVars = 1
FiltersVar GPUVars = 2
ImagesVar GPUVars = 3
ValuesVar GPUVars = 4
Values4DVar GPUVars = 5
ScalarsVar GPUVars = 6
InhibsVar GPUVars = 7
)
// Tensor stride variables
var TensorStrides tensor.Uint32
// GPUInit initializes the GPU compute system,
// configuring system(s), variables and kernels.
// It is safe to call multiple times: detects if already run.
func GPUInit() {
if GPUInitialized {
return
}
GPUInitialized = true
if ComputeGPU == nil { // set prior to this call to use an external
ComputeGPU = gpu.NewComputeGPU()
} else {
BorrowedGPU = true
}
gp := ComputeGPU
_ = fmt.Sprintf("%g",math.NaN()) // keep imports happy
{
sy := gpu.NewComputeSystem(gp, "Default")
GPUSystem = sy
vars := sy.Vars()
{
sgp := vars.AddGroup(gpu.Storage, "Params")
var vr *gpu.Var
_ = vr
vr = sgp.Add("TensorStrides", gpu.Uint32, 1, gpu.ComputeShader)
vr.ReadOnly = true
vr = sgp.AddStruct("CurOp", int(unsafe.Sizeof(Op{})), 1, gpu.ComputeShader)
vr.ReadOnly = true
vr = sgp.AddStruct("KWTAs", int(unsafe.Sizeof(KWTA{})), 1, gpu.ComputeShader)
vr.ReadOnly = true
sgp.SetNValues(1)
}
{
sgp := vars.AddGroup(gpu.Storage, "Filters")
var vr *gpu.Var
_ = vr
vr = sgp.Add("Filters", gpu.Float32, 1, gpu.ComputeShader)
vr.ReadOnly = true
sgp.SetNValues(1)
}
{
sgp := vars.AddGroup(gpu.Storage, "Data")
var vr *gpu.Var
_ = vr
vr = sgp.Add("Images", gpu.Float32, 1, gpu.ComputeShader)
vr = sgp.Add("Values", gpu.Float32, 1, gpu.ComputeShader)
vr = sgp.Add("Values4D", gpu.Float32, 1, gpu.ComputeShader)
vr = sgp.Add("Scalars", gpu.Float32, 1, gpu.ComputeShader)
vr = sgp.Add("Inhibs", gpu.Float32, 1, gpu.ComputeShader)
sgp.SetNValues(1)
}
var pl *gpu.ComputePipeline
pl = gpu.NewComputePipelineShaderFS(shaders, "shaders/DoCurOp.wgsl", sy)
pl.AddVarUsed(0, "TensorStrides")
pl.AddVarUsed(0, "CurOp")
pl.AddVarUsed(1, "Filters")
pl.AddVarUsed(2, "Images")
pl.AddVarUsed(2, "Scalars")
pl.AddVarUsed(2, "Values")
pl.AddVarUsed(2, "Values4D")
pl = gpu.NewComputePipelineShaderFS(shaders, "shaders/EdgeAverage.wgsl", sy)
pl.AddVarUsed(0, "TensorStrides")
pl.AddVarUsed(0, "CurOp")
pl.AddVarUsed(2, "Images")
pl.AddVarUsed(2, "Scalars")
pl = gpu.NewComputePipelineShaderFS(shaders, "shaders/KWTAInitLayer.wgsl", sy)
pl.AddVarUsed(0, "TensorStrides")
pl.AddVarUsed(0, "CurOp")
pl.AddVarUsed(2, "Inhibs")
pl = gpu.NewComputePipelineShaderFS(shaders, "shaders/KWTAInitPool.wgsl", sy)
pl.AddVarUsed(0, "TensorStrides")
pl.AddVarUsed(0, "CurOp")
pl.AddVarUsed(2, "Inhibs")
pl.AddVarUsed(2, "Values")
pl = gpu.NewComputePipelineShaderFS(shaders, "shaders/KWTAIterLayerX.wgsl", sy)
pl.AddVarUsed(0, "TensorStrides")
pl.AddVarUsed(0, "CurOp")
pl.AddVarUsed(2, "Inhibs")
pl = gpu.NewComputePipelineShaderFS(shaders, "shaders/KWTAIterLayerY.wgsl", sy)
pl.AddVarUsed(0, "TensorStrides")
pl.AddVarUsed(0, "CurOp")
pl.AddVarUsed(2, "Inhibs")
pl.AddVarUsed(0, "KWTAs")
pl = gpu.NewComputePipelineShaderFS(shaders, "shaders/KWTAIterPool.wgsl", sy)
pl.AddVarUsed(0, "TensorStrides")
pl.AddVarUsed(0, "CurOp")
pl.AddVarUsed(2, "Inhibs")
pl.AddVarUsed(0, "KWTAs")
pl.AddVarUsed(2, "Values")
pl = gpu.NewComputePipelineShaderFS(shaders, "shaders/MaxScalarX.wgsl", sy)
pl.AddVarUsed(0, "TensorStrides")
pl.AddVarUsed(0, "CurOp")
pl.AddVarUsed(2, "Values")
pl = gpu.NewComputePipelineShaderFS(shaders, "shaders/MaxScalarY.wgsl", sy)
pl.AddVarUsed(0, "TensorStrides")
pl.AddVarUsed(0, "CurOp")
pl.AddVarUsed(2, "Scalars")
pl.AddVarUsed(2, "Values")
pl = gpu.NewComputePipelineShaderFS(shaders, "shaders/MeanScalarY.wgsl", sy)
pl.AddVarUsed(0, "TensorStrides")
pl.AddVarUsed(0, "CurOp")
pl.AddVarUsed(2, "Scalars")
pl.AddVarUsed(2, "Values")
pl = gpu.NewComputePipelineShaderFS(shaders, "shaders/MotionFullFieldX.wgsl", sy)
pl.AddVarUsed(0, "TensorStrides")
pl.AddVarUsed(0, "CurOp")
pl.AddVarUsed(2, "Values")
pl = gpu.NewComputePipelineShaderFS(shaders, "shaders/MotionFullFieldY.wgsl", sy)
pl.AddVarUsed(0, "TensorStrides")
pl.AddVarUsed(0, "CurOp")
pl.AddVarUsed(2, "Scalars")
pl.AddVarUsed(2, "Values")
pl = gpu.NewComputePipelineShaderFS(shaders, "shaders/SumScalarX.wgsl", sy)
pl.AddVarUsed(0, "TensorStrides")
pl.AddVarUsed(0, "CurOp")
pl.AddVarUsed(2, "Values")
pl = gpu.NewComputePipelineShaderFS(shaders, "shaders/SumScalarY.wgsl", sy)
pl.AddVarUsed(0, "TensorStrides")
pl.AddVarUsed(0, "CurOp")
pl.AddVarUsed(2, "Scalars")
pl.AddVarUsed(2, "Values")
sy.Config()
}
}
// GPURelease releases the GPU compute system resources.
// Call this at program exit.
func GPURelease() {
if GPUSystem != nil {
GPUSystem.Release()
GPUSystem = nil
}
if !BorrowedGPU && ComputeGPU != nil {
ComputeGPU.Release()
}
ComputeGPU = nil
}
// RunDoCurOp runs the DoCurOp kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// Can call multiple Run* kernels in a row, which are then all launched
// in the same command submission on the GPU, which is by far the most efficient.
// MUST call RunDone (with optional vars to sync) after all Run calls.
// Alternatively, a single-shot RunOneDoCurOp call does Run and Done for a
// single run-and-sync case.
func RunDoCurOp(n int) {
if UseGPU {
RunDoCurOpGPU(n)
} else {
RunDoCurOpCPU(n)
}
}
// RunDoCurOpGPU runs the DoCurOp kernel on the GPU. See [RunDoCurOp] for more info.
func RunDoCurOpGPU(n int) {
sy := GPUSystem
pl := sy.ComputePipelines["DoCurOp"]
ce, _ := sy.BeginComputePass()
pl.Dispatch1D(ce, n, 64)
}
// RunDoCurOpCPU runs the DoCurOp kernel on the CPU.
func RunDoCurOpCPU(n int) {
gpu.VectorizeFunc(0, n, DoCurOp)
}
// RunOneDoCurOp runs the DoCurOp kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// This version then calls RunDone with the given variables to sync
// after the Run, for a single-shot Run-and-Done call. If multiple kernels
// can be run in sequence, it is much more efficient to do multiple Run*
// calls followed by a RunDone call.
func RunOneDoCurOp(n int, syncVars ...GPUVars) {
if UseGPU {
RunDoCurOpGPU(n)
RunDone(syncVars...)
} else {
RunDoCurOpCPU(n)
}
}
// RunEdgeAverage runs the EdgeAverage kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// Can call multiple Run* kernels in a row, which are then all launched
// in the same command submission on the GPU, which is by far the most efficient.
// MUST call RunDone (with optional vars to sync) after all Run calls.
// Alternatively, a single-shot RunOneEdgeAverage call does Run and Done for a
// single run-and-sync case.
func RunEdgeAverage(n int) {
if UseGPU {
RunEdgeAverageGPU(n)
} else {
RunEdgeAverageCPU(n)
}
}
// RunEdgeAverageGPU runs the EdgeAverage kernel on the GPU. See [RunEdgeAverage] for more info.
func RunEdgeAverageGPU(n int) {
sy := GPUSystem
pl := sy.ComputePipelines["EdgeAverage"]
ce, _ := sy.BeginComputePass()
pl.Dispatch1D(ce, n, 64)
}
// RunEdgeAverageCPU runs the EdgeAverage kernel on the CPU.
func RunEdgeAverageCPU(n int) {
gpu.VectorizeFunc(0, n, EdgeAverage)
}
// RunOneEdgeAverage runs the EdgeAverage kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// This version then calls RunDone with the given variables to sync
// after the Run, for a single-shot Run-and-Done call. If multiple kernels
// can be run in sequence, it is much more efficient to do multiple Run*
// calls followed by a RunDone call.
func RunOneEdgeAverage(n int, syncVars ...GPUVars) {
if UseGPU {
RunEdgeAverageGPU(n)
RunDone(syncVars...)
} else {
RunEdgeAverageCPU(n)
}
}
// RunKWTAInitLayer runs the KWTAInitLayer kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// Can call multiple Run* kernels in a row, which are then all launched
// in the same command submission on the GPU, which is by far the most efficient.
// MUST call RunDone (with optional vars to sync) after all Run calls.
// Alternatively, a single-shot RunOneKWTAInitLayer call does Run and Done for a
// single run-and-sync case.
func RunKWTAInitLayer(n int) {
if UseGPU {
RunKWTAInitLayerGPU(n)
} else {
RunKWTAInitLayerCPU(n)
}
}
// RunKWTAInitLayerGPU runs the KWTAInitLayer kernel on the GPU. See [RunKWTAInitLayer] for more info.
func RunKWTAInitLayerGPU(n int) {
sy := GPUSystem
pl := sy.ComputePipelines["KWTAInitLayer"]
ce, _ := sy.BeginComputePass()
pl.Dispatch1D(ce, n, 64)
}
// RunKWTAInitLayerCPU runs the KWTAInitLayer kernel on the CPU.
func RunKWTAInitLayerCPU(n int) {
gpu.VectorizeFunc(0, n, KWTAInitLayer)
}
// RunOneKWTAInitLayer runs the KWTAInitLayer kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// This version then calls RunDone with the given variables to sync
// after the Run, for a single-shot Run-and-Done call. If multiple kernels
// can be run in sequence, it is much more efficient to do multiple Run*
// calls followed by a RunDone call.
func RunOneKWTAInitLayer(n int, syncVars ...GPUVars) {
if UseGPU {
RunKWTAInitLayerGPU(n)
RunDone(syncVars...)
} else {
RunKWTAInitLayerCPU(n)
}
}
// RunKWTAInitPool runs the KWTAInitPool kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// Can call multiple Run* kernels in a row, which are then all launched
// in the same command submission on the GPU, which is by far the most efficient.
// MUST call RunDone (with optional vars to sync) after all Run calls.
// Alternatively, a single-shot RunOneKWTAInitPool call does Run and Done for a
// single run-and-sync case.
func RunKWTAInitPool(n int) {
if UseGPU {
RunKWTAInitPoolGPU(n)
} else {
RunKWTAInitPoolCPU(n)
}
}
// RunKWTAInitPoolGPU runs the KWTAInitPool kernel on the GPU. See [RunKWTAInitPool] for more info.
func RunKWTAInitPoolGPU(n int) {
sy := GPUSystem
pl := sy.ComputePipelines["KWTAInitPool"]
ce, _ := sy.BeginComputePass()
pl.Dispatch1D(ce, n, 64)
}
// RunKWTAInitPoolCPU runs the KWTAInitPool kernel on the CPU.
func RunKWTAInitPoolCPU(n int) {
gpu.VectorizeFunc(0, n, KWTAInitPool)
}
// RunOneKWTAInitPool runs the KWTAInitPool kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// This version then calls RunDone with the given variables to sync
// after the Run, for a single-shot Run-and-Done call. If multiple kernels
// can be run in sequence, it is much more efficient to do multiple Run*
// calls followed by a RunDone call.
func RunOneKWTAInitPool(n int, syncVars ...GPUVars) {
if UseGPU {
RunKWTAInitPoolGPU(n)
RunDone(syncVars...)
} else {
RunKWTAInitPoolCPU(n)
}
}
// RunKWTAIterLayerX runs the KWTAIterLayerX kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// Can call multiple Run* kernels in a row, which are then all launched
// in the same command submission on the GPU, which is by far the most efficient.
// MUST call RunDone (with optional vars to sync) after all Run calls.
// Alternatively, a single-shot RunOneKWTAIterLayerX call does Run and Done for a
// single run-and-sync case.
func RunKWTAIterLayerX(n int) {
if UseGPU {
RunKWTAIterLayerXGPU(n)
} else {
RunKWTAIterLayerXCPU(n)
}
}
// RunKWTAIterLayerXGPU runs the KWTAIterLayerX kernel on the GPU. See [RunKWTAIterLayerX] for more info.
func RunKWTAIterLayerXGPU(n int) {
sy := GPUSystem
pl := sy.ComputePipelines["KWTAIterLayerX"]
ce, _ := sy.BeginComputePass()
pl.Dispatch1D(ce, n, 64)
}
// RunKWTAIterLayerXCPU runs the KWTAIterLayerX kernel on the CPU.
func RunKWTAIterLayerXCPU(n int) {
gpu.VectorizeFunc(0, n, KWTAIterLayerX)
}
// RunOneKWTAIterLayerX runs the KWTAIterLayerX kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// This version then calls RunDone with the given variables to sync
// after the Run, for a single-shot Run-and-Done call. If multiple kernels
// can be run in sequence, it is much more efficient to do multiple Run*
// calls followed by a RunDone call.
func RunOneKWTAIterLayerX(n int, syncVars ...GPUVars) {
if UseGPU {
RunKWTAIterLayerXGPU(n)
RunDone(syncVars...)
} else {
RunKWTAIterLayerXCPU(n)
}
}
// RunKWTAIterLayerY runs the KWTAIterLayerY kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// Can call multiple Run* kernels in a row, which are then all launched
// in the same command submission on the GPU, which is by far the most efficient.
// MUST call RunDone (with optional vars to sync) after all Run calls.
// Alternatively, a single-shot RunOneKWTAIterLayerY call does Run and Done for a
// single run-and-sync case.
func RunKWTAIterLayerY(n int) {
if UseGPU {
RunKWTAIterLayerYGPU(n)
} else {
RunKWTAIterLayerYCPU(n)
}
}
// RunKWTAIterLayerYGPU runs the KWTAIterLayerY kernel on the GPU. See [RunKWTAIterLayerY] for more info.
func RunKWTAIterLayerYGPU(n int) {
sy := GPUSystem
pl := sy.ComputePipelines["KWTAIterLayerY"]
ce, _ := sy.BeginComputePass()
pl.Dispatch1D(ce, n, 64)
}
// RunKWTAIterLayerYCPU runs the KWTAIterLayerY kernel on the CPU.
func RunKWTAIterLayerYCPU(n int) {
gpu.VectorizeFunc(0, n, KWTAIterLayerY)
}
// RunOneKWTAIterLayerY runs the KWTAIterLayerY kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// This version then calls RunDone with the given variables to sync
// after the Run, for a single-shot Run-and-Done call. If multiple kernels
// can be run in sequence, it is much more efficient to do multiple Run*
// calls followed by a RunDone call.
func RunOneKWTAIterLayerY(n int, syncVars ...GPUVars) {
if UseGPU {
RunKWTAIterLayerYGPU(n)
RunDone(syncVars...)
} else {
RunKWTAIterLayerYCPU(n)
}
}
// RunKWTAIterPool runs the KWTAIterPool kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// Can call multiple Run* kernels in a row, which are then all launched
// in the same command submission on the GPU, which is by far the most efficient.
// MUST call RunDone (with optional vars to sync) after all Run calls.
// Alternatively, a single-shot RunOneKWTAIterPool call does Run and Done for a
// single run-and-sync case.
func RunKWTAIterPool(n int) {
if UseGPU {
RunKWTAIterPoolGPU(n)
} else {
RunKWTAIterPoolCPU(n)
}
}
// RunKWTAIterPoolGPU runs the KWTAIterPool kernel on the GPU. See [RunKWTAIterPool] for more info.
func RunKWTAIterPoolGPU(n int) {
sy := GPUSystem
pl := sy.ComputePipelines["KWTAIterPool"]
ce, _ := sy.BeginComputePass()
pl.Dispatch1D(ce, n, 64)
}
// RunKWTAIterPoolCPU runs the KWTAIterPool kernel on the CPU.
func RunKWTAIterPoolCPU(n int) {
gpu.VectorizeFunc(0, n, KWTAIterPool)
}
// RunOneKWTAIterPool runs the KWTAIterPool kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// This version then calls RunDone with the given variables to sync
// after the Run, for a single-shot Run-and-Done call. If multiple kernels
// can be run in sequence, it is much more efficient to do multiple Run*
// calls followed by a RunDone call.
func RunOneKWTAIterPool(n int, syncVars ...GPUVars) {
if UseGPU {
RunKWTAIterPoolGPU(n)
RunDone(syncVars...)
} else {
RunKWTAIterPoolCPU(n)
}
}
// RunMaxScalarX runs the MaxScalarX kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// Can call multiple Run* kernels in a row, which are then all launched
// in the same command submission on the GPU, which is by far the most efficient.
// MUST call RunDone (with optional vars to sync) after all Run calls.
// Alternatively, a single-shot RunOneMaxScalarX call does Run and Done for a
// single run-and-sync case.
func RunMaxScalarX(n int) {
if UseGPU {
RunMaxScalarXGPU(n)
} else {
RunMaxScalarXCPU(n)
}
}
// RunMaxScalarXGPU runs the MaxScalarX kernel on the GPU. See [RunMaxScalarX] for more info.
func RunMaxScalarXGPU(n int) {
sy := GPUSystem
pl := sy.ComputePipelines["MaxScalarX"]
ce, _ := sy.BeginComputePass()
pl.Dispatch1D(ce, n, 64)
}
// RunMaxScalarXCPU runs the MaxScalarX kernel on the CPU.
func RunMaxScalarXCPU(n int) {
gpu.VectorizeFunc(0, n, MaxScalarX)
}
// RunOneMaxScalarX runs the MaxScalarX kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// This version then calls RunDone with the given variables to sync
// after the Run, for a single-shot Run-and-Done call. If multiple kernels
// can be run in sequence, it is much more efficient to do multiple Run*
// calls followed by a RunDone call.
func RunOneMaxScalarX(n int, syncVars ...GPUVars) {
if UseGPU {
RunMaxScalarXGPU(n)
RunDone(syncVars...)
} else {
RunMaxScalarXCPU(n)
}
}
// RunMaxScalarY runs the MaxScalarY kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// Can call multiple Run* kernels in a row, which are then all launched
// in the same command submission on the GPU, which is by far the most efficient.
// MUST call RunDone (with optional vars to sync) after all Run calls.
// Alternatively, a single-shot RunOneMaxScalarY call does Run and Done for a
// single run-and-sync case.
func RunMaxScalarY(n int) {
if UseGPU {
RunMaxScalarYGPU(n)
} else {
RunMaxScalarYCPU(n)
}
}
// RunMaxScalarYGPU runs the MaxScalarY kernel on the GPU. See [RunMaxScalarY] for more info.
func RunMaxScalarYGPU(n int) {
sy := GPUSystem
pl := sy.ComputePipelines["MaxScalarY"]
ce, _ := sy.BeginComputePass()
pl.Dispatch1D(ce, n, 64)
}
// RunMaxScalarYCPU runs the MaxScalarY kernel on the CPU.
func RunMaxScalarYCPU(n int) {
gpu.VectorizeFunc(0, n, MaxScalarY)
}
// RunOneMaxScalarY runs the MaxScalarY kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// This version then calls RunDone with the given variables to sync
// after the Run, for a single-shot Run-and-Done call. If multiple kernels
// can be run in sequence, it is much more efficient to do multiple Run*
// calls followed by a RunDone call.
func RunOneMaxScalarY(n int, syncVars ...GPUVars) {
if UseGPU {
RunMaxScalarYGPU(n)
RunDone(syncVars...)
} else {
RunMaxScalarYCPU(n)
}
}
// RunMeanScalarY runs the MeanScalarY kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// Can call multiple Run* kernels in a row, which are then all launched
// in the same command submission on the GPU, which is by far the most efficient.
// MUST call RunDone (with optional vars to sync) after all Run calls.
// Alternatively, a single-shot RunOneMeanScalarY call does Run and Done for a
// single run-and-sync case.
func RunMeanScalarY(n int) {
if UseGPU {
RunMeanScalarYGPU(n)
} else {
RunMeanScalarYCPU(n)
}
}
// RunMeanScalarYGPU runs the MeanScalarY kernel on the GPU. See [RunMeanScalarY] for more info.
func RunMeanScalarYGPU(n int) {
sy := GPUSystem
pl := sy.ComputePipelines["MeanScalarY"]
ce, _ := sy.BeginComputePass()
pl.Dispatch1D(ce, n, 64)
}
// RunMeanScalarYCPU runs the MeanScalarY kernel on the CPU.
func RunMeanScalarYCPU(n int) {
gpu.VectorizeFunc(0, n, MeanScalarY)
}
// RunOneMeanScalarY runs the MeanScalarY kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// This version then calls RunDone with the given variables to sync
// after the Run, for a single-shot Run-and-Done call. If multiple kernels
// can be run in sequence, it is much more efficient to do multiple Run*
// calls followed by a RunDone call.
func RunOneMeanScalarY(n int, syncVars ...GPUVars) {
if UseGPU {
RunMeanScalarYGPU(n)
RunDone(syncVars...)
} else {
RunMeanScalarYCPU(n)
}
}
// RunMotionFullFieldX runs the MotionFullFieldX kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// Can call multiple Run* kernels in a row, which are then all launched
// in the same command submission on the GPU, which is by far the most efficient.
// MUST call RunDone (with optional vars to sync) after all Run calls.
// Alternatively, a single-shot RunOneMotionFullFieldX call does Run and Done for a
// single run-and-sync case.
func RunMotionFullFieldX(n int) {
if UseGPU {
RunMotionFullFieldXGPU(n)
} else {
RunMotionFullFieldXCPU(n)
}
}
// RunMotionFullFieldXGPU runs the MotionFullFieldX kernel on the GPU. See [RunMotionFullFieldX] for more info.
func RunMotionFullFieldXGPU(n int) {
sy := GPUSystem
pl := sy.ComputePipelines["MotionFullFieldX"]
ce, _ := sy.BeginComputePass()
pl.Dispatch1D(ce, n, 64)
}
// RunMotionFullFieldXCPU runs the MotionFullFieldX kernel on the CPU.
func RunMotionFullFieldXCPU(n int) {
gpu.VectorizeFunc(0, n, MotionFullFieldX)
}
// RunOneMotionFullFieldX runs the MotionFullFieldX kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// This version then calls RunDone with the given variables to sync
// after the Run, for a single-shot Run-and-Done call. If multiple kernels
// can be run in sequence, it is much more efficient to do multiple Run*
// calls followed by a RunDone call.
func RunOneMotionFullFieldX(n int, syncVars ...GPUVars) {
if UseGPU {
RunMotionFullFieldXGPU(n)
RunDone(syncVars...)
} else {
RunMotionFullFieldXCPU(n)
}
}
// RunMotionFullFieldY runs the MotionFullFieldY kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// Can call multiple Run* kernels in a row, which are then all launched
// in the same command submission on the GPU, which is by far the most efficient.
// MUST call RunDone (with optional vars to sync) after all Run calls.
// Alternatively, a single-shot RunOneMotionFullFieldY call does Run and Done for a
// single run-and-sync case.
func RunMotionFullFieldY(n int) {
if UseGPU {
RunMotionFullFieldYGPU(n)
} else {
RunMotionFullFieldYCPU(n)
}
}
// RunMotionFullFieldYGPU runs the MotionFullFieldY kernel on the GPU. See [RunMotionFullFieldY] for more info.
func RunMotionFullFieldYGPU(n int) {
sy := GPUSystem
pl := sy.ComputePipelines["MotionFullFieldY"]
ce, _ := sy.BeginComputePass()
pl.Dispatch1D(ce, n, 64)
}
// RunMotionFullFieldYCPU runs the MotionFullFieldY kernel on the CPU.
func RunMotionFullFieldYCPU(n int) {
gpu.VectorizeFunc(0, n, MotionFullFieldY)
}
// RunOneMotionFullFieldY runs the MotionFullFieldY kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// This version then calls RunDone with the given variables to sync
// after the Run, for a single-shot Run-and-Done call. If multiple kernels
// can be run in sequence, it is much more efficient to do multiple Run*
// calls followed by a RunDone call.
func RunOneMotionFullFieldY(n int, syncVars ...GPUVars) {
if UseGPU {
RunMotionFullFieldYGPU(n)
RunDone(syncVars...)
} else {
RunMotionFullFieldYCPU(n)
}
}
// RunSumScalarX runs the SumScalarX kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// Can call multiple Run* kernels in a row, which are then all launched
// in the same command submission on the GPU, which is by far the most efficient.
// MUST call RunDone (with optional vars to sync) after all Run calls.
// Alternatively, a single-shot RunOneSumScalarX call does Run and Done for a
// single run-and-sync case.
func RunSumScalarX(n int) {
if UseGPU {
RunSumScalarXGPU(n)
} else {
RunSumScalarXCPU(n)
}
}
// RunSumScalarXGPU runs the SumScalarX kernel on the GPU. See [RunSumScalarX] for more info.
func RunSumScalarXGPU(n int) {
sy := GPUSystem
pl := sy.ComputePipelines["SumScalarX"]
ce, _ := sy.BeginComputePass()
pl.Dispatch1D(ce, n, 64)
}
// RunSumScalarXCPU runs the SumScalarX kernel on the CPU.
func RunSumScalarXCPU(n int) {
gpu.VectorizeFunc(0, n, SumScalarX)
}
// RunOneSumScalarX runs the SumScalarX kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// This version then calls RunDone with the given variables to sync
// after the Run, for a single-shot Run-and-Done call. If multiple kernels
// can be run in sequence, it is much more efficient to do multiple Run*
// calls followed by a RunDone call.
func RunOneSumScalarX(n int, syncVars ...GPUVars) {
if UseGPU {
RunSumScalarXGPU(n)
RunDone(syncVars...)
} else {
RunSumScalarXCPU(n)
}
}
// RunSumScalarY runs the SumScalarY kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// Can call multiple Run* kernels in a row, which are then all launched
// in the same command submission on the GPU, which is by far the most efficient.
// MUST call RunDone (with optional vars to sync) after all Run calls.
// Alternatively, a single-shot RunOneSumScalarY call does Run and Done for a
// single run-and-sync case.
func RunSumScalarY(n int) {
if UseGPU {
RunSumScalarYGPU(n)
} else {
RunSumScalarYCPU(n)
}
}
// RunSumScalarYGPU runs the SumScalarY kernel on the GPU. See [RunSumScalarY] for more info.
func RunSumScalarYGPU(n int) {
sy := GPUSystem
pl := sy.ComputePipelines["SumScalarY"]
ce, _ := sy.BeginComputePass()
pl.Dispatch1D(ce, n, 64)
}
// RunSumScalarYCPU runs the SumScalarY kernel on the CPU.
func RunSumScalarYCPU(n int) {
gpu.VectorizeFunc(0, n, SumScalarY)
}
// RunOneSumScalarY runs the SumScalarY kernel with given number of elements,
// on either the CPU or GPU depending on the UseGPU variable.
// This version then calls RunDone with the given variables to sync
// after the Run, for a single-shot Run-and-Done call. If multiple kernels
// can be run in sequence, it is much more efficient to do multiple Run*
// calls followed by a RunDone call.
func RunOneSumScalarY(n int, syncVars ...GPUVars) {
if UseGPU {
RunSumScalarYGPU(n)
RunDone(syncVars...)
} else {
RunSumScalarYCPU(n)
}
}
// RunDone must be called after Run* calls to start compute kernels.
// This actually submits the kernel jobs to the GPU, and adds commands
// to synchronize the given variables back from the GPU to the CPU.
// After this function completes, the GPU results will be available in
// the specified variables.
func RunDone(syncVars ...GPUVars) {
if !UseGPU {
return
}
sy := GPUSystem
sy.ComputeEncoder.End()
ReadFromGPU(syncVars...)
sy.EndComputePass()
SyncFromGPU(syncVars...)
}
// ToGPU copies given variables to the GPU for the system.
func ToGPU(vars ...GPUVars) {
if !UseGPU {
return
}
sy := GPUSystem
syVars := sy.Vars()
for _, vr := range vars {
switch vr {
case CurOpVar:
v, _ := syVars.ValueByIndex(0, "CurOp", 0)
gpu.SetValueFrom(v, CurOp)
case KWTAsVar:
v, _ := syVars.ValueByIndex(0, "KWTAs", 0)
gpu.SetValueFrom(v, KWTAs)
case FiltersVar:
v, _ := syVars.ValueByIndex(1, "Filters", 0)
gpu.SetValueFrom(v, Filters.Values)
case ImagesVar:
v, _ := syVars.ValueByIndex(2, "Images", 0)
gpu.SetValueFrom(v, Images.Values)
case ValuesVar:
v, _ := syVars.ValueByIndex(2, "Values", 0)
gpu.SetValueFrom(v, Values.Values)
case Values4DVar:
v, _ := syVars.ValueByIndex(2, "Values4D", 0)
gpu.SetValueFrom(v, Values4D.Values)
case ScalarsVar:
v, _ := syVars.ValueByIndex(2, "Scalars", 0)
gpu.SetValueFrom(v, Scalars.Values)
case InhibsVar:
v, _ := syVars.ValueByIndex(2, "Inhibs", 0)
gpu.SetValueFrom(v, Inhibs.Values)
}
}
}
// RunGPUSync can be called to synchronize data between CPU and GPU.
// Any prior ToGPU* calls will execute to send data to the GPU,
// and any subsequent RunDone* calls will copy data back from the GPU.
func RunGPUSync() {
if !UseGPU {
return
}
sy := GPUSystem
sy.BeginComputePass()
}
// ToGPUTensorStrides gets tensor strides and starts copying to the GPU.
func ToGPUTensorStrides() {
if !UseGPU {
return
}
sy := GPUSystem
syVars := sy.Vars()
TensorStrides.SetShapeSizes(60)
TensorStrides.SetInt1D(Filters.Shape().Strides[0], 0)
TensorStrides.SetInt1D(Filters.Shape().Strides[1], 1)
TensorStrides.SetInt1D(Filters.Shape().Strides[2], 2)
TensorStrides.SetInt1D(Filters.Shape().Strides[3], 3)
TensorStrides.SetInt1D(Images.Shape().Strides[0], 10)
TensorStrides.SetInt1D(Images.Shape().Strides[1], 11)
TensorStrides.SetInt1D(Images.Shape().Strides[2], 12)
TensorStrides.SetInt1D(Images.Shape().Strides[3], 13)
TensorStrides.SetInt1D(Images.Shape().Strides[4], 14)
TensorStrides.SetInt1D(Values.Shape().Strides[0], 20)
TensorStrides.SetInt1D(Values.Shape().Strides[1], 21)
TensorStrides.SetInt1D(Values.Shape().Strides[2], 22)
TensorStrides.SetInt1D(Values.Shape().Strides[3], 23)
TensorStrides.SetInt1D(Values.Shape().Strides[4], 24)
TensorStrides.SetInt1D(Values.Shape().Strides[5], 25)
TensorStrides.SetInt1D(Values4D.Shape().Strides[0], 30)
TensorStrides.SetInt1D(Values4D.Shape().Strides[1], 31)
TensorStrides.SetInt1D(Values4D.Shape().Strides[2], 32)
TensorStrides.SetInt1D(Values4D.Shape().Strides[3], 33)
TensorStrides.SetInt1D(Values4D.Shape().Strides[4], 34)
TensorStrides.SetInt1D(Values4D.Shape().Strides[5], 35)
TensorStrides.SetInt1D(Scalars.Shape().Strides[0], 40)
TensorStrides.SetInt1D(Scalars.Shape().Strides[1], 41)
TensorStrides.SetInt1D(Inhibs.Shape().Strides[0], 50)
TensorStrides.SetInt1D(Inhibs.Shape().Strides[1], 51)
TensorStrides.SetInt1D(Inhibs.Shape().Strides[2], 52)
TensorStrides.SetInt1D(Inhibs.Shape().Strides[3], 53)
TensorStrides.SetInt1D(Inhibs.Shape().Strides[4], 54)
v, _ := syVars.ValueByIndex(0, "TensorStrides", 0)
gpu.SetValueFrom(v, TensorStrides.Values)
}
// ReadFromGPU starts the process of copying vars to the GPU.
func ReadFromGPU(vars ...GPUVars) {
sy := GPUSystem
syVars := sy.Vars()
for _, vr := range vars {
switch vr {
case CurOpVar:
v, _ := syVars.ValueByIndex(0, "CurOp", 0)
v.GPUToRead(sy.CommandEncoder)
case KWTAsVar:
v, _ := syVars.ValueByIndex(0, "KWTAs", 0)
v.GPUToRead(sy.CommandEncoder)
case FiltersVar:
v, _ := syVars.ValueByIndex(1, "Filters", 0)
v.GPUToRead(sy.CommandEncoder)
case ImagesVar:
v, _ := syVars.ValueByIndex(2, "Images", 0)
v.GPUToRead(sy.CommandEncoder)
case ValuesVar:
v, _ := syVars.ValueByIndex(2, "Values", 0)
v.GPUToRead(sy.CommandEncoder)
case Values4DVar:
v, _ := syVars.ValueByIndex(2, "Values4D", 0)
v.GPUToRead(sy.CommandEncoder)
case ScalarsVar:
v, _ := syVars.ValueByIndex(2, "Scalars", 0)
v.GPUToRead(sy.CommandEncoder)
case InhibsVar:
v, _ := syVars.ValueByIndex(2, "Inhibs", 0)
v.GPUToRead(sy.CommandEncoder)
}
}
}
// SyncFromGPU synchronizes vars from the GPU to the actual variable.
func SyncFromGPU(vars ...GPUVars) {
sy := GPUSystem
syVars := sy.Vars()
for _, vr := range vars {
switch vr {
case CurOpVar:
v, _ := syVars.ValueByIndex(0, "CurOp", 0)
v.ReadSync()
gpu.ReadToBytes(v, CurOp)
case KWTAsVar:
v, _ := syVars.ValueByIndex(0, "KWTAs", 0)
v.ReadSync()
gpu.ReadToBytes(v, KWTAs)
case FiltersVar:
v, _ := syVars.ValueByIndex(1, "Filters", 0)
v.ReadSync()
gpu.ReadToBytes(v, Filters.Values)
case ImagesVar:
v, _ := syVars.ValueByIndex(2, "Images", 0)
v.ReadSync()
gpu.ReadToBytes(v, Images.Values)
case ValuesVar:
v, _ := syVars.ValueByIndex(2, "Values", 0)
v.ReadSync()
gpu.ReadToBytes(v, Values.Values)
case Values4DVar:
v, _ := syVars.ValueByIndex(2, "Values4D", 0)
v.ReadSync()
gpu.ReadToBytes(v, Values4D.Values)
case ScalarsVar:
v, _ := syVars.ValueByIndex(2, "Scalars", 0)
v.ReadSync()
gpu.ReadToBytes(v, Scalars.Values)
case InhibsVar:
v, _ := syVars.ValueByIndex(2, "Inhibs", 0)
v.ReadSync()
gpu.ReadToBytes(v, Inhibs.Values)
}
}
}
// GetCurOp returns a pointer to the given global variable:
// [CurOp] []Op at given index. This directly processed in the GPU code,
// so this function call is an equivalent for the CPU.
func GetCurOp(idx uint32) *Op {
return &CurOp[idx]
}
// GetKWTAs returns a pointer to the given global variable:
// [KWTAs] []KWTA at given index. This directly processed in the GPU code,
// so this function call is an equivalent for the CPU.
func GetKWTAs(idx uint32) *KWTA {
return &KWTAs[idx]
}
// Code generated by "goal build"; DO NOT EDIT.
//line image.goal:1
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1vision
import (
// "fmt"
"image"
"image/color"
"cogentcore.org/core/colors"
"cogentcore.org/core/gpu"
"cogentcore.org/core/math32"
"cogentcore.org/lab/gosl/slmath"
"cogentcore.org/lab/tensor"
"github.com/emer/v1vision/colorspace"
)
const (
// TopZero is arg to pass to image routines to put Y=0 at top of tensors.
// Default is bottom.
TopZero = true
// BottomZero is arg to pass to image routines to put Y=0 at bottom of tensors.
// This is the default.
BottomZero = false
)
// NewWrapImage configures a new WrapPad operation for given input image
// and RGB index (3 = all). Output goes to another image.
// padWidth is the border padding around edges.
func (vv *V1Vision) NewWrapImage(in, irgb, out, padWidth int, geom *Geom) {
op := vv.NewOp()
op.Op = WrapPad
nin := geom.In.Y * geom.In.X
if irgb == 3 {
nin *= 3
}
op.RunN = uint32(nin)
op.InImage = int32(in)
op.InImageRGB = int32(irgb)
op.OutImage = int32(out)
op.IntArg1 = int32(padWidth)
op.Geom = *geom
}
// NewEdgeAvg configures a new EdgeAvg operation for given input image
// and RGB index (3 = all). Output goes to another image.
// padWidth is the border padding around edges. r,g,b are edge average
// values that are faded into (can set FloatArg1-3 values later too).
// Returns the starting index of the r,g,b values for the scalar outputs.
func (vv *V1Vision) NewEdgeAvg(in, irgb, padWidth int, geom *Geom) int {
op := vv.NewOp()
op.Op = EdgeAvg
op.RunN = 1
op.InImage = int32(in)
op.InImageRGB = int32(irgb)
op.IntArg1 = int32(padWidth)
op.Geom = *geom
out := vv.NewScalar(3)
op.OutScalar = int32(out)
return out
}
// NewFadeImage configures a new FadePad operation for given input image
// and RGB index (3 = all). Output goes to another image.
// padWidth is the border padding around edges. r,g,b values to fade to
// are starting at scalarIn: use EdgeAvg to get from image.
// Returns the index of Op
func (vv *V1Vision) NewFadeImage(in, irgb, out, padWidth, scalarIn int, geom *Geom) {
op := vv.NewOp()
op.Op = FadePad
nin := geom.In.Y * geom.In.X
if irgb == 3 {
nin *= 3
}
op.RunN = uint32(nin)
op.InImage = int32(in)
op.InImageRGB = int32(irgb)
op.OutImage = int32(out)
op.IntArg1 = int32(padWidth)
op.InScalar = int32(scalarIn)
op.Geom = *geom
}
const (
// Red is stored in R=0 component of first LMS image
Red = 0
// Green is stored in G=1 component of first LMS image
Green = 1
// Blue is stored in B=2 component of second LMS image
Blue = 2
// Yellow is stored in R=0 component of second LMS image
Yellow = 0
// Red-Green stored in R=0
RedGreen = 0
// Blue-Yellow stored in B=2
BlueYellow = 2
)
// NewLMSOpponents configures a new LMSOpponents operation for given input image,
// setting output index:
// 0 = RedGreen (L-M), 1 = White-Black (grey), 2 = BlueYellow (S-(LM)),
// so that resulting image is visually sensible when displayed as a standard RGB
// image (which it is not!).
// gain is a multiplier factor for the color contrasts relative to grey
// because they tend to be weaker.
func (vv *V1Vision) NewLMSOpponents(in, out int, gain float32, geom *Geom) {
op := vv.NewOp()
op.Op = LMSOpponents
nin := geom.In.Y * geom.In.X
op.RunN = uint32(nin)
op.InImage = int32(in)
op.OutImage = int32(out)
op.FloatArg1 = gain
op.Geom = *geom
}
// NewLMSComponents configures a new LMSComponents operation
// for given input image, storing results into two output images, with
// the 4 different color components distributed across the R,G,B components
// along with Grey = White-Black, so that resulting image is visually
// sensible when displayed as a standard RGB image (which it is not!)
// Image1: 0 = Red (L), 1 = Green (M), 2 = Grey
// Image2: 0 = Yellow (LM), 1 = Grey, 2 = Blue (S),
func (vv *V1Vision) NewLMSComponents(in, out1, out2 int, gainS float32, geom *Geom) {
op := vv.NewOp()
op.Op = LMSComponents
nin := geom.In.Y * geom.In.X
op.RunN = uint32(nin)
op.InImage = int32(in)
op.OutImage = int32(out1)
op.OutImage2 = int32(out2)
op.FloatArg1 = gainS
op.Geom = *geom
}
//gosl:start
//gosl:import "cogentcore.org/lab/gosl/slmath"
//gosl:import "github.com/emer/v1vision/colorspace"
// WrapPad is the kernel for WrapPad.
// wraps given padding width of float32 image around sides
// i.e., padding for left side of image is the (mirrored) bits
// from the right side of image, etc.
func (op *Op) WrapPad(i, ni int32) {
ii := i
ri := op.InImageRGB
if ri == 3 {
xy := op.Geom.In.X * op.Geom.In.Y
ri = i / xy
ii = i % xy
}
y := ii / op.Geom.In.X
x := ii % op.Geom.In.X
padWidth := op.IntArg1
uY := op.Geom.In.Y - padWidth
uX := op.Geom.In.X - padWidth
sy := y
if y < padWidth {
sy = uY - (padWidth - y)
} else if y >= uY {
sy = padWidth + (y - uY)
}
sx := x
if x < padWidth {
sx = uX - (padWidth - x)
} else if x >= uX {
sx = padWidth + (x - uX)
}
iv := Images.Value(int(op.InImage), int(ni), int(ri), int(sy), int(sx))
Images.Set(iv, int(op.OutImage), int(ni), int(ri), int(y), int(x))
}
// FadePad is the kernel for FadePad.
// wraps given padding width of float32 image around sides
// i.e., padding for left side of image is the (mirrored) bits
// from the right side of image, etc.
func (op *Op) FadePad(i, ni int32) {
ii := i
ri := op.InImageRGB
avg := Scalars.Value(int(op.InScalar), int(ni))
if ri == 3 {
xy := op.Geom.In.X * op.Geom.In.Y
ri = i / xy
ii = i % xy
avg = Scalars.Value(int(int32(op.InScalar)+ri), int(ni))
}
y := ii / op.Geom.In.X
x := ii % op.Geom.In.X
padWidth := op.IntArg1
uY := op.Geom.In.Y - padWidth
uX := op.Geom.In.X - padWidth
sy := y
p := float32(1)
if y < padWidth {
p = float32(y) / float32(padWidth)
sy = uY - (padWidth - y)
} else if y >= uY {
p = 1.0 - float32(y-uY)/float32(padWidth)
sy = padWidth + (y - uY)
}
sx := x
if x < padWidth {
p = min(p, float32(x)/float32(padWidth))
sx = uX - (padWidth - x)
} else if x >= uX {
p = min(p, 1.0-float32(x-uX)/float32(padWidth))
sx = padWidth + (x - uX)
}
pavg := (1.0 - p) * avg
iv := Images.Value(int(op.InImage), int(ni), int(ri), int(sy), int(sx))
Images.Set(p*iv+pavg, int(op.OutImage), int(ni), int(ri), int(y), int(x))
}
// EdgeAverage returns the average value around the effective edge of RGB image
// at padWidth in from each side. i = NData
func EdgeAverage(i uint32) { //gosl:kernel
op := GetCurOp(0)
if i >= op.NData {
return
}
ni := int32(i)
padWidth := op.IntArg1
sY := op.Geom.In.Y - 2*padWidth
sX := op.Geom.In.X - 2*padWidth
var avg math32.Vector3
for y := range sY {
oy := y + padWidth
for rgb := range int32(3) {
v := slmath.Dim3(avg, rgb) + Images.Value(int(op.InImage), int(ni), int(rgb), int(oy), int(padWidth)) + Images.Value(int(op.InImage), int(ni), int(rgb), int(oy), int(padWidth+sX-1))
avg = slmath.SetDim3(avg, rgb, v)
}
}
for x := range sX {
ox := x + padWidth
for rgb := range int32(3) {
v := slmath.Dim3(avg, rgb) + Images.Value(int(op.InImage), int(ni), int(rgb), int(padWidth), int(ox)) + Images.Value(int(op.InImage), int(ni), int(rgb), int(padWidth+sY-1), int(ox))
avg = slmath.SetDim3(avg, rgb, v)
}
}
n := 2 * (sX + sY)
for rgb := range int32(3) {
Scalars.Set(slmath.Dim3(avg, rgb)/float32(n), int(int32(op.OutScalar)+rgb), int(ni))
}
}
// LMSOpponents is the kernel for LMSOpponents.
func (op *Op) LMSOpponents(i, ni int32) {
y := i / op.Geom.In.X
x := i % op.Geom.In.X
r := Images.Value(int(op.InImage), int(ni), int(0), int(y), int(x))
g := Images.Value(int(op.InImage), int(ni), int(1), int(y), int(x))
b := Images.Value(int(op.InImage), int(ni), int(2), int(y), int(x))
var lc, mc, sc, lmc, lvm, svlm, grey float32
colorspace.SRGBToLMSAll(r, g, b, &lc, &mc, &sc, &lmc, &lvm, &svlm, &grey)
Images.Set(op.FloatArg1*lvm, int(op.OutImage), int(ni), int(0), int(y), int(x)) // RedGreen
Images.Set(grey, int(op.OutImage), int(ni), int(1), int(y), int(x))
Images.Set(op.FloatArg1*svlm, int(op.OutImage), int(ni), int(2), int(y), int(x)) // BlueYellow
}
// LMSComponents is the kernel for LMSComponents.
func (op *Op) LMSComponents(i, ni int32) {
y := i / op.Geom.In.X
x := i % op.Geom.In.X
r := Images.Value(int(op.InImage), int(ni), int(0), int(y), int(x))
g := Images.Value(int(op.InImage), int(ni), int(1), int(y), int(x))
b := Images.Value(int(op.InImage), int(ni), int(2), int(y), int(x))
var lc, mc, sc, lmc, lvm, svlm, grey float32
colorspace.SRGBToLMSAll(r, g, b, &lc, &mc, &sc, &lmc, &lvm, &svlm, &grey)
Images.Set(op.FloatArg1*lc, int(op.OutImage), int(ni), int(0), int(y), int(x)) // Red
Images.Set(op.FloatArg1*mc, int(op.OutImage), int(ni), int(1), int(y), int(x)) // Green
Images.Set(grey, int(op.OutImage), int(ni), int(2), int(y), int(x))
Images.Set(op.FloatArg1*lmc, int(op.OutImage2), int(ni), int(0), int(y), int(x)) // Yellow
Images.Set(grey, int(op.OutImage2), int(ni), int(1), int(y), int(x))
Images.Set(op.FloatArg1*sc, int(op.OutImage2), int(ni), int(2), int(y), int(x)) // Blue
}
//gosl:end
// RGBToTensor converts RGB input image(s) to an RGB tensor
// with outer dimension as RGB components.
// There must be NData images for writing to the V1Vision.Images,
// and the size of the tensor must already be properly configured to hold
// the resulting images.
// padWidth is the amount of padding to add on all sides.
// topZero retains the Y=0 value at the top of the tensor --
// otherwise it is flipped with Y=0 at the bottom to be consistent
// with the emergent / OpenGL standard coordinate system
func RGBToTensor(tsr *tensor.Float32, padWidth int, topZero bool, imgs ...image.Image) {
bd := imgs[0].Bounds()
sz := bd.Size()
np := sz.X * sz.Y
// note: can't resize b/c typically a subspace of images.
// tsr.SetShapeSizes(4, len(imgs), sz.Y+2*padWidth, sz.X+2*padWidth) // must be already!
// todo: do this on GPU
kernel := func(i uint32) {
ri := int(i) % np
ni := int(i) / np
x := ri % sz.X
y := ri / sz.X
sy := y
if !topZero {
sy = (sz.Y - 1) - y
}
cv := imgs[ni].At(bd.Min.X+x, bd.Min.Y+sy)
r, g, b, _ := colors.ToFloat32(cv)
tsr.Set(r, int(ni), int(0), int(y+padWidth), int(x+padWidth))
tsr.Set(g, int(ni), int(1), int(y+padWidth), int(x+padWidth))
tsr.Set(b, int(ni), int(2), int(y+padWidth), int(x+padWidth))
}
gpu.VectorizeFunc(0, np*len(imgs), kernel)
}
// RGBToGrey converts an RGB input image to a greyscale tensor
// in preparation for processing. Writes to first (red) component.
// There must be NData images for writing to the V1Vision.Images,
// and the size of the tensor must already be properly configured to hold
// the resulting images.
// padWidth is the amount of padding to add on all sides.
// topZero retains the Y=0 value at the top of the tensor --
// otherwise it is flipped with Y=0 at the bottom to be consistent
// with the emergent standard coordinate system.
// Tensor must already be set to source size + 2*padWidth!
func RGBToGrey(tsr *tensor.Float32, padWidth int, topZero bool, imgs ...image.Image) {
bd := imgs[0].Bounds()
sz := bd.Size()
np := sz.X * sz.Y
// note: can't resize b/c typically a subspace of images.
// tsr.SetShapeSizes(4, len(imgs), sz.Y+2*padWidth, sz.X+2*padWidth) // must be already!
// todo: do this on GPU
kernel := func(i uint32) {
ri := int(i) % np
ni := int(i) / np
x := ri % sz.X
y := ri / sz.X
sy := y
if !topZero {
sy = (sz.Y - 1) - y
}
cv := imgs[ni].At(bd.Min.X+x, bd.Min.Y+sy)
r, g, b, _ := colors.ToFloat32(cv)
gv := (r + g + b) / 3
tsr.Set(gv, int(ni), int(0), int(y+padWidth), int(x+padWidth))
}
gpu.VectorizeFunc(0, np*len(imgs), kernel)
}
// RGBTensorToImage converts an RGB tensor to image -- uses
// existing image if it is of correct size, otherwise makes a new one.
// tensor must have outer dimension as RGB components.
// padWidth is the amount of padding to subtract from all sides.
// topZero retains the Y=0 value at the top of the tensor --
// otherwise it is flipped with Y=0 at the bottom to be consistent
// with the emergent / OpenGL standard coordinate system
func RGBTensorToImage(img *image.RGBA, tsr *tensor.Float32, padWidth int, topZero bool) *image.RGBA {
var sz image.Point
sz.Y = tsr.DimSize(1) - 2*padWidth
sz.X = tsr.DimSize(2) - 2*padWidth
if img == nil {
img = image.NewRGBA(image.Rectangle{Max: sz})
} else {
isz := img.Bounds().Size()
if isz != sz {
img = image.NewRGBA(image.Rectangle{Max: sz})
}
}
for y := 0; y < sz.Y; y++ {
for x := 0; x < sz.X; x++ {
sy := y
if !topZero {
sy = (sz.Y - 1) - y
}
r := tsr.Value(0, y+padWidth, x+padWidth)
g := tsr.Value(1, y+padWidth, x+padWidth)
b := tsr.Value(2, y+padWidth, x+padWidth)
ri := uint8(r * 255)
gi := uint8(g * 255)
bi := uint8(b * 255)
img.Set(x, sy, color.RGBA{ri, gi, bi, 255})
}
}
return img
}
// GreyTensorToImage converts a greyscale tensor to image -- uses
// existing img if it is of correct size, otherwise makes a new one.
// padWidth is the amount of padding to subtract from all sides.
// topZero retains the Y=0 value at the top of the tensor --
// otherwise it is flipped with Y=0 at the bottom to be consistent
// with the emergent / OpenGL standard coordinate system
func GreyTensorToImage(img *image.Gray, tsr *tensor.Float32, padWidth int, topZero bool) *image.Gray {
var sz image.Point
sz.Y = tsr.DimSize(0) - 2*padWidth
sz.X = tsr.DimSize(1) - 2*padWidth
if img == nil {
img = image.NewGray(image.Rectangle{Max: sz})
} else {
isz := img.Bounds().Size()
if isz != sz {
img = image.NewGray(image.Rectangle{Max: sz})
}
}
for y := 0; y < sz.Y; y++ {
for x := 0; x < sz.X; x++ {
sy := y
if !topZero {
sy = (sz.Y - 1) - y
}
cv := tsr.Value(y+padWidth, x+padWidth)
iv := uint8(cv * 255)
img.Set(x, sy, color.Gray{iv})
}
}
return img
}
// Code generated by "goal build"; DO NOT EDIT.
//line kwta.goal:1
// Copyright (c) 2025, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1vision
import (
"github.com/emer/v1vision/kwta"
)
// alias so it works locally too.
type KWTA = kwta.KWTA
// NewNeighInhib4 adds a [NeighInhib4] operation, from in value -> out value.
// fn is number of filters (innermost values dimension). must be 4!
// gi is inhibition strength.
// Output value has additional inhibition for active neighbors of same filter index.
// returns out index.
func (vv *V1Vision) NewNeighInhib4(in, fn int, gi float32, geom *Geom) int {
if fn != 4 {
panic("only 4 angles are currently supported for NeighInhib4!")
}
op := vv.NewOp()
op.Op = NeighInhib4
out := vv.NewNeighInhibOutput(fn, geom)
op.RunN = uint32(geom.Out.Y * geom.Out.X * int32(fn) * 2)
op.InValue = int32(in)
op.OutValue = int32(out)
op.FilterN = int32(fn)
op.FloatArg1 = gi
op.Geom = *geom
return out
}
// NewNeighInhibOutput add Values for a [NeighInhib] operation.
// fn is number of filters (innermost values dimension). returns out index.
func (vv *V1Vision) NewNeighInhibOutput(fn int, geom *Geom) int {
return vv.NewValues(int(geom.Out.Y), int(geom.Out.X), fn)
}
// NewKWTA adds a [KWTAInhib] operation, on Values data.
// in = raw initial inputs, inExtGi = extra Gi inhibition
// typically from [NeighInhib4] -- if 0 then not used.
// fn is number of filters (innermost values dimension).
// geom.Out is the size of the outer Y,X dimensions, and
// FilterSize is the inner Y,X dimensions.
// Allocates a Inhibs to hold the inhibition compute values,
// including an additional Y row for the layer-level values at the end.
func (vv *V1Vision) NewKWTA(in, inExtGi, fn, kwtaIdx, inhIdx int, geom *Geom) int {
op := vv.NewOp()
op.Op = KWTAInhib
out := vv.NewValues(int(geom.Out.Y), int(geom.Out.X), fn)
op.RunN = uint32(geom.Out.Y * geom.Out.X)
op.InValue = int32(in)
op.InValue2 = int32(inExtGi)
op.OutValue = int32(out)
op.Inhibs = int32(inhIdx)
op.FilterN = int32(fn)
op.KWTA = int32(kwtaIdx)
op.Geom = *geom
return out
}
//gosl:start
// NeighInhib4 is kernel.
func (op *Op) NeighInhib4(i, ni int32) {
ang := i % op.FilterN // inner
pii := i / op.FilterN
pi := pii % 2 // plus-minus
ii := pii / 2
yo := ii / op.Geom.Out.X
xo := ii % op.Geom.Out.X
gi := float32(0)
var ox, oy int32
NeighInhibOffsets(ang, &ox, &oy)
npX := xo + ox
npY := yo + oy
if npX >= 0 && npX < op.Geom.Out.X && npY >= 0 && npY < op.Geom.Out.Y {
v := Values.Value(int(op.InValue), int(ni), int(npY), int(npX), int(pi), int(ang))
gi = max(gi, v)
}
npX = xo - ox
npY = yo - oy
if npX >= 0 && npX < op.Geom.Out.X && npY >= 0 && npY < op.Geom.Out.Y {
v := Values.Value(int(op.InValue), int(ni), int(npY), int(npX), int(pi), int(ang))
gi = max(gi, v)
}
Values.Set(op.FloatArg1*gi, int(op.OutValue), int(ni), int(yo), int(xo), int(pi), int(ang))
}
// KWTAInitPool is the kernel to initialize KWTA process, on Values data.
// i = op.Geom.Out.Y * X. 2 x FilterN is inner 2 dims. Operates on Inhibs.
// InValue = raw initial activations (ge)
// OutValue = acts (output result)
func KWTAInitPool(i uint32) { //gosl:kernel
op := GetCurOp(0)
if i >= op.RunN*op.NData {
return
}
ri := int32(i % op.RunN)
ni := int32(i / op.RunN)
yo := ri / op.Geom.Out.X
xo := ri % op.Geom.Out.X
pn := 2 * op.FilterN
geAvg := float32(0)
geMax := float32(0)
for py := range 2 { // for 4D, FilterSize.Y
for px := range op.FilterN {
ge := Values.Value(int(op.InValue), int(ni), int(yo), int(xo), int(py), int(px))
geAvg += ge
geMax = max(geMax, ge)
}
}
for i := range InhibVarsN {
Inhibs.Set(0.0, int(op.Inhibs), int(ni), int(yo), int(xo), int(int(i)))
}
Inhibs.Set(geAvg/float32(pn), int(op.Inhibs), int(ni), int(yo), int(xo), int(GeAvg))
Inhibs.Set(geMax, int(op.Inhibs), int(ni), int(yo), int(xo), int(GeMax))
}
// KWTAInitLayer is the kernel to initialize KWTA process for layer
// on Values data. Run = 1*NData only. Operates on Inhibs.
func KWTAInitLayer(i uint32) { //gosl:kernel
op := GetCurOp(0)
if i > op.NData {
return
}
ni := int32(i)
lyi := op.Geom.Out.Y
for j := range InhibVarsN {
Inhibs.Set(0.0, int(op.Inhibs), int(ni), int(lyi), int(0), int(j))
}
}
// KWTAIterLayerX is the kernel to iterate KWTA process at layer,
// first pass, operating on X dimension.
// i = op.Geom.Out.Y * X. FilterSize is inner 2 dims.
// Operates on Inhibs updated from pool-level.
// Call this first then IterPool
func KWTAIterLayerX(i uint32) { //gosl:kernel
op := GetCurOp(0)
szY := op.Geom.Out.Y
szX := op.Geom.Out.X
if i >= uint32(szY)*op.NData {
return
}
yo := int32(i) % szY
ni := int32(i) / szY
ln := float32(szY)
geAvg := float32(0)
geMax := float32(0)
actAvg := float32(0)
actMax := float32(0)
for xo := range szX {
gavg := Inhibs.Value(int(op.Inhibs), int(ni), int(yo), int(xo), int(GeAvg))
gmx := Inhibs.Value(int(op.Inhibs), int(ni), int(yo), int(xo), int(GeMax))
aavg := Inhibs.Value(int(op.Inhibs), int(ni), int(yo), int(xo), int(ActAvg))
amx := Inhibs.Value(int(op.Inhibs), int(ni), int(yo), int(xo), int(ActMax))
geAvg += gavg
geMax = max(geMax, gmx)
actAvg += aavg
actMax = max(actMax, amx)
}
geAvg /= ln
actAvg /= ln
Inhibs.Set(geAvg, int(op.Inhibs), int(ni), int(yo), int(szX), int(GeAvg))
Inhibs.Set(geMax, int(op.Inhibs), int(ni), int(yo), int(szX), int(GeMax))
Inhibs.Set(actAvg, int(op.Inhibs), int(ni), int(yo), int(szX), int(ActAvg))
Inhibs.Set(actMax, int(op.Inhibs), int(ni), int(yo), int(szX), int(ActMax))
}
// KWTAIterLayerY is the kernel to iterate KWTA process at layer.
// i = NData.
// Operates on Inhibs updated from pool-level.
// Call this first then IterPool
func KWTAIterLayerY(i uint32) { //gosl:kernel
op := GetCurOp(0)
if i >= op.NData {
return
}
ni := int32(i)
szY := op.Geom.Out.Y
szX := op.Geom.Out.X
ln := float32(szY)
geAvg := float32(0)
geMax := float32(0)
actAvg := float32(0)
actMax := float32(0)
for yo := range szY {
gavg := Inhibs.Value(int(op.Inhibs), int(ni), int(yo), int(szX), int(GeAvg))
gmx := Inhibs.Value(int(op.Inhibs), int(ni), int(yo), int(szX), int(GeMax))
aavg := Inhibs.Value(int(op.Inhibs), int(ni), int(yo), int(szX), int(ActAvg))
amx := Inhibs.Value(int(op.Inhibs), int(ni), int(yo), int(szX), int(ActMax))
geAvg += gavg
geMax = max(geMax, gmx)
actAvg += aavg
actMax = max(actMax, amx)
}
geAvg /= ln
actAvg /= ln
Inhibs.Set(geAvg, int(op.Inhibs), int(ni), int(szY), int(0), int(GeAvg))
Inhibs.Set(geMax, int(op.Inhibs), int(ni), int(szY), int(0), int(GeMax))
Inhibs.Set(actAvg, int(op.Inhibs), int(ni), int(szY), int(0), int(ActAvg))
Inhibs.Set(actMax, int(op.Inhibs), int(ni), int(szY), int(0), int(ActMax))
kp := GetKWTAs(uint32(op.KWTA))
fbi := Inhibs.Value(int(op.Inhibs), int(ni), int(szY), int(0), int(FBi))
ffi := kp.Layer.FFInhib(geAvg, geMax)
newFBi := kp.Layer.FBInhib(actAvg)
fbi = kp.Layer.FBUpdt(fbi, newFBi)
if kp.Layer.On.IsFalse() {
ffi = float32(0.0)
fbi = 0.0
}
Inhibs.Set(ffi, int(op.Inhibs), int(ni), int(szY), int(0), int(FFi))
Inhibs.Set(fbi, int(op.Inhibs), int(ni), int(szY), int(0), int(FBi))
gi := kp.Layer.Gi * (ffi + fbi)
Inhibs.Set(gi, int(op.Inhibs), int(ni), int(szY), int(0), int(Gi))
Inhibs.Set(gi, int(op.Inhibs), int(ni), int(szY), int(0), int(GiOrig))
}
// KWTAIterPool is the kernel to iterate KWTA process for Pools.
// i = op.Geom.Out.Y * X. FilterSize is inner 2 dims. Operates on Inhibs.
// InValue = raw initial activations (ge)
// InValue2 = extra Gi values, if non-0
// OutValue = acts (output result)
func KWTAIterPool(i uint32) { //gosl:kernel
op := GetCurOp(0)
if i >= op.RunN*op.NData {
return
}
ri := int32(i % op.RunN)
ni := int32(i / op.RunN)
szY := op.Geom.Out.Y
szX := op.Geom.Out.X
yo := ri / szX
xo := ri % szX
layGi := Inhibs.Value(int(op.Inhibs), int(ni), int(szY), int(0), int(Gi))
kp := GetKWTAs(uint32(op.KWTA))
pn := 2 * op.FilterN
// pn := op.Geom.FilterSize.Y * op.Geom.FilterSize.X
geAvg := Inhibs.Value(int(op.Inhibs), int(ni), int(yo), int(xo), int(GeAvg))
geMax := Inhibs.Value(int(op.Inhibs), int(ni), int(yo), int(xo), int(GeMax))
actAvg := Inhibs.Value(int(op.Inhibs), int(ni), int(yo), int(xo), int(ActAvg))
fbi := Inhibs.Value(int(op.Inhibs), int(ni), int(yo), int(xo), int(FBi))
ffi := kp.Pool.FFInhib(geAvg, geMax)
newFBi := kp.Pool.FBInhib(actAvg)
fbi = kp.Pool.FBUpdt(fbi, newFBi)
if kp.Pool.On.IsFalse() {
ffi = float32(0.0)
fbi = 0.0
}
Inhibs.Set(ffi, int(op.Inhibs), int(ni), int(yo), int(xo), int(FFi))
Inhibs.Set(fbi, int(op.Inhibs), int(ni), int(yo), int(xo), int(FBi))
gi := kp.Pool.Gi * (ffi + fbi)
Inhibs.Set(gi, int(op.Inhibs), int(ni), int(yo), int(xo), int(Gi))
Inhibs.Set(gi, int(op.Inhibs), int(ni), int(yo), int(xo), int(GiOrig))
giPool := max(layGi, gi)
actAvg = 0.0
actMax := float32(0.0)
for py := range 2 { // op.Geom.FilterSize.Y {
for px := range op.FilterN {
pgi := giPool
if op.InValue2 > 0 {
eIn := Values.Value(int(op.InValue2), int(ni), int(yo), int(xo), int(py), int(px))
eGi := kp.Pool.Gi * kp.Pool.FFInhib(eIn, eIn)
pgi = max(pgi, eGi)
}
geThr := kp.GeThrFromG(pgi)
ge := Values.Value(int(op.InValue), int(ni), int(yo), int(xo), int(py), int(px))
act := Values.Value(int(op.OutValue), int(ni), int(yo), int(xo), int(py), int(px))
geAvg += ge
geMax = max(geMax, ge)
delAct := float32(0)
nwAct := kp.ActFromG(geThr, ge, act, &delAct)
// maxDelAct = max(maxDelAct, math32.Abs(delAct)) // todo
Values.Set(nwAct, int(op.OutValue), int(ni), int(yo), int(xo), int(py), int(px))
actAvg += nwAct
actMax = max(actMax, nwAct)
}
}
Inhibs.Set(actAvg/float32(pn), int(op.Inhibs), int(ni), int(yo), int(xo), int(ActAvg))
Inhibs.Set(actMax, int(op.Inhibs), int(ni), int(yo), int(xo), int(ActMax))
}
// Neigh4X = []int{0, -1, 1, -1}
// Neigh4Y = []int{1, 1, 0, -1}
// ortho neighbor coordinates for 4 angles, also uses negated version
//
// .
//
// --- = (0,1) (X,Y)
// . /
//
// / = (-1,1)
//
// | . = (1,0)
//
// \
//
// . \ = (-1,-1)
func NeighInhibOffsets(ang int32, ox, oy *int32) {
switch ang {
case 1, 3:
*ox = -1
case 2:
*ox = 1
default:
*ox = 0
}
switch ang {
case 0, 1:
*oy = 1
case 3:
*oy = -1
default:
*oy = 0
}
}
//gosl:end
// Code generated by "goal build"; DO NOT EDIT.
//line logrenorm.goal:1
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1vision
import (
"cogentcore.org/core/math32"
)
// NewLogValues adds a [LogValues] operation,
// which sets [Values] to 1 + log of input Values.
// in = input Values, out = output (can be same as in),
// fn = number of filters, Geom Out sizes are used for indexing.
func (vv *V1Vision) NewLogValues(in, out, fn int, gain float32, geom *Geom) {
op := vv.NewOp()
op.Op = LogValues
op.RunN = uint32(geom.Out.Y * geom.Out.X * int32(fn) * 2)
op.InValue = int32(in)
op.OutValue = int32(out)
op.FilterN = int32(fn)
op.FloatArg1 = gain
op.Geom = *geom
}
// NewNormDiv adds a [NormDiv] operation which normalizes value
// by [Scalars] computed from NewAggScalar with given aggOp
// in = input Values. Allocates output Values and scalars as needed.
// fn = number of filters, Geom Out sizes are used for indexing.
func (vv *V1Vision) NewNormDiv(aggOp Operations, in, out, fn int, geom *Geom) {
scout := vv.NewAggScalar(aggOp, in, fn, geom)
op := vv.NewOp()
op.Op = NormDiv
op.RunN = uint32(geom.Out.Y * geom.Out.X * int32(fn) * 2)
op.InValue = int32(in)
op.OutValue = int32(out)
op.InScalar = int32(scout)
op.FilterN = int32(fn)
op.Geom = *geom
}
//gosl:start
// LogValues is the kernel for LogValues.
func (op *Op) LogValues(i, ni int32) {
fi := i % op.FilterN // inner
pii := i / op.FilterN
pi := pii % 2 // plus-minus
ii := pii / 2
yo := ii / op.Geom.Out.X
xo := ii % op.Geom.Out.X
lg := op.FloatArg1 * math32.Log(1.0+Values.Value(int(op.InValue), int(ni), int(yo), int(xo), int(pi), int(fi)))
Values.Set(lg, int(op.OutValue), int(ni), int(yo), int(xo), int(pi), int(fi))
}
// NormDiv is the kernel for NormDiv
func (op *Op) NormDiv(i, ni int32) {
fi := i % op.FilterN // inner
pii := i / op.FilterN
pi := pii % 2 // plus-minus
ii := pii / 2
yo := ii / op.Geom.Out.X
xo := ii % op.Geom.Out.X
sc := Scalars.Value(int(op.InScalar), int(ni))
v := Values.Value(int(op.InValue), int(ni), int(yo), int(xo), int(pi), int(fi))
if sc != 0 {
v /= sc
}
Values.Set(v, int(op.OutValue), int(ni), int(yo), int(xo), int(pi), int(fi))
}
//gosl:end
// Code generated by "goal build"; DO NOT EDIT.
//line maxpool.goal:1
// Copyright (c) 2025, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1vision
// NewMaxPool adds a [MaxPool] operation, from in value -> out values.
// fn is number of filters (innermost values dimension),
// pn is number of polarities (1 or 2),
// geom.In is the size of the input values, and geom.Out is the output,
// where geom is: Out = (In / Spacing), and FilterSize <= Spacing.
// (e.g., use geom.SetFilter). returns index of new output.
func (vv *V1Vision) NewMaxPool(in, pn, fn int, geom *Geom) int {
op := vv.NewOp()
op.Op = MaxPool
out := vv.NewValues(int(geom.Out.Y), int(geom.Out.X), fn)
op.RunN = uint32(geom.Out.Y * geom.Out.X * int32(fn) * int32(pn))
op.InValue = int32(in)
op.OutValue = int32(out)
op.FilterN = int32(fn)
op.IntArg1 = int32(pn)
op.Geom = *geom
return out
}
// NewMaxPolarity adds a [MaxPolarity] operation, from in value -> out values.
// fn is number of filters (innermost values dimension).
// geom.Out is the size of both input and output,
// just maxes over the polarity dimension. returns index of new output.
func (vv *V1Vision) NewMaxPolarity(in, fn int, geom *Geom) int {
op := vv.NewOp()
op.Op = MaxPolarity
out := vv.NewValues(int(geom.Out.Y), int(geom.Out.X), fn)
op.RunN = uint32(geom.Out.Y * geom.Out.X * int32(fn))
op.InValue = int32(in)
op.OutValue = int32(out)
op.FilterN = int32(fn)
op.Geom = *geom
return out
}
// NewMaxCopy adds a [MaxCopy] operation, as max(in1, in2) values -> out values.
// fn is number of filters (innermost values dimension).
// geom.Out is the size of the input and output values.
// Must create output in advance to support multi-max (max on prior max).
func (vv *V1Vision) NewMaxCopy(in1, in2, out, fn int, geom *Geom) {
op := vv.NewOp()
op.Op = MaxCopy
op.RunN = uint32(geom.Out.Y * geom.Out.X * int32(fn) * 2)
op.InValue = int32(in1)
op.InValue2 = int32(in2)
op.OutValue = int32(out)
op.FilterN = int32(fn)
op.Geom = *geom
}
//gosl:start
// MaxPool is kernel.
func (op *Op) MaxPool(i, ni int32) {
szX := op.Geom.Out.X
fY := op.Geom.FilterSize.Y
fX := op.Geom.FilterSize.X
fi := i % op.FilterN // inner
pii := i / op.FilterN
pi := pii % op.IntArg1 // plus-minus
ii := pii / op.IntArg1
yo := ii / szX
xo := ii % szX
iy := yo * op.Geom.Spacing.Y
ix := xo * op.Geom.Spacing.X
mx := float32(0)
for py := range fY {
for px := range fX {
iv := Values.Value(int(op.InValue), int(ni), int(iy+py), int(ix+px), int(pi), int(fi))
if iv > mx {
mx = iv
}
}
}
Values.Set(mx, int(op.OutValue), int(ni), int(yo), int(xo), int(pi), int(fi))
}
// MaxPolarity is kernel.
func (op *Op) MaxPolarity(i, ni int32) {
szX := op.Geom.Out.X
fi := i % op.FilterN // inner
ii := i / op.FilterN
yo := ii / szX
xo := ii % szX
mx := float32(0)
for pi := range 2 {
iv := Values.Value(int(op.InValue), int(ni), int(yo), int(xo), int(pi), int(fi))
if iv > mx {
mx = iv
}
}
Values.Set(mx, int(op.OutValue), int(ni), int(yo), int(xo), int(0), int(fi))
}
// MaxCopy is kernel.
func (op *Op) MaxCopy(i, ni int32) {
szX := op.Geom.Out.X
fi := i % op.FilterN // inner
pii := i / op.FilterN
pi := pii % 2 // plus-minus
ii := pii / 2
yo := ii / szX
xo := ii % szX
i1 := Values.Value(int(op.InValue), int(ni), int(yo), int(xo), int(pi), int(fi))
i2 := Values.Value(int(op.InValue2), int(ni), int(yo), int(xo), int(pi), int(fi))
Values.Set(max(i1, i2), int(op.OutValue), int(ni), int(yo), int(xo), int(pi), int(fi))
}
//gosl:end
// Code generated by "goal build"; DO NOT EDIT.
//line motion.goal:1
// Copyright (c) 2025, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1vision
// NewMotionIntegrate adds a [MotionIntegrate] operation,
// operating on given values input index, with given number of filters.
// fastTau and slowTau are the tau time constants for integrating.
// Adds two new Values for output: Fast and Slow, index of Fast returned.
func (vv *V1Vision) NewMotionIntegrate(in, fn int, fastTau, slowTau float32, geom *Geom) int {
fast := vv.NewValues(int(geom.Out.Y), int(geom.Out.X), fn)
vv.NewValues(int(geom.Out.Y), int(geom.Out.X), fn) // slow
op := vv.NewOp()
op.Op = MotionIntegrate
op.RunN = uint32(geom.Out.Y * geom.Out.X * int32(fn) * 2)
op.InValue = int32(in)
op.OutValue = int32(fast)
op.FilterN = int32(fn)
op.FloatArg1 = 1.0 / fastTau
op.FloatArg2 = 1.0 / slowTau
op.Geom = *geom
return fast
}
// NewMotionStar adds a [MotionStar] operation,
// operating on given values input index = fast, +1 = slow,
// with given number of original input filters.
// Adds new Values for output, nf = orig nf * 4 (left, right, down, up),
// index returned.
func (vv *V1Vision) NewMotionStar(in, fn int, gain float32, geom *Geom) int {
nfn := fn * 4
oy := int(geom.Out.Y - 1)
ox := int(geom.Out.X - 1)
out := vv.NewValues(oy, ox, nfn)
op := vv.NewOp()
op.Op = MotionStar
op.RunN = uint32(oy * ox * int(fn*2) * 2) // lr,du dir for run
op.InValue = int32(in)
op.OutValue = int32(out)
op.FilterN = int32(fn * 2)
op.FloatArg1 = gain
op.Geom = *geom
return out
}
// NewMotionFullField adds a [MotionFullField] operation,
// operating on given values input index = star output.
// with given number of original input filters (same as arg for Star).
// Adds 4 new Scalar outputs for instantaneous motion output.
// Allocates an intermediate OutValue for 2-phase integration process.
// starting Scalar index returned.
func (vv *V1Vision) NewMotionFullField(in, fn int, geom *Geom) int {
out := vv.NewScalar(4)
op := vv.NewOp()
op.Op = MotionFullField
oy := int(geom.Out.Y - 1)
op.RunN = uint32(2 * oy) // first pass N
op.InValue = int32(in)
op.OutValue = int32(vv.NewValues(oy, 1, 4))
op.OutScalar = int32(out)
op.FilterN = int32(fn)
op.Geom = *geom
return out
}
//gosl:start
// MotionIntegrate is the kernel.
func (op *Op) MotionIntegrate(i, ni int32) {
fi := i % op.FilterN // inner
pii := i / op.FilterN
pi := pii % 2 // plus-minus
ii := pii / 2
yo := ii / op.Geom.Out.X
xo := ii % op.Geom.Out.X
v := Values.Value(int(op.InValue), int(ni), int(yo), int(xo), int(pi), int(fi))
f := Values.Value(int(op.OutValue), int(ni), int(yo), int(xo), int(pi), int(fi))
s := Values.Value(int(op.OutValue+1), int(ni), int(yo), int(xo), int(pi), int(fi))
if v > f {
f = v
} else {
f += op.FloatArg1 * (v - f)
}
if v > s {
s = v
} else {
s += op.FloatArg2 * (v - s)
}
Values.Set(f, int(op.OutValue), int(ni), int(yo), int(xo), int(pi), int(fi))
Values.Set(s, int(op.OutValue+1), int(ni), int(yo), int(xo), int(pi), int(fi))
}
// MotionStar is the kernel.
func (op *Op) MotionStar(i, ni int32) {
szX := op.Geom.Out.X - 1
fi := i % op.FilterN // FilterN = orig fn * 2
pii := i / op.FilterN
pi := pii % 2 // plus-minus
ii := pii / 2
yo := ii / szX
xo := ii % szX
fio := fi / 2 // original feature
dir := fi % 2 // direction: left-right, down-up
doff := fio*4 + dir*2
var yoff, xoff int32 // offset to next (right or up)
if dir == 0 {
xoff = 1
} else {
yoff = 1
}
cf := Values.Value(int(op.InValue), int(ni), int(yo), int(xo), int(pi), int(fio)) // fast
nf := Values.Value(int(op.InValue), int(ni), int(yo+yoff), int(xo+xoff), int(pi), int(fio)) // next
cs := Values.Value(int(op.InValue+1), int(ni), int(yo), int(xo), int(pi), int(fio)) // slow
ns := Values.Value(int(op.InValue+1), int(ni), int(yo+yoff), int(xo+xoff), int(pi), int(fio)) // next
minact := min(min(min(cf, cs), nf), ns)
cd := cf - cs
nd := nf - ns
v := op.FloatArg1 * (cd - nd)
if v >= 0 { // delta bigger on current than next
Values.Set(minact*v, int(op.OutValue), int(ni), int(yo), int(xo), int(pi), int(doff)) // 0 = left/down
Values.Set(0.0, int(op.OutValue), int(ni), int(yo), int(xo), int(pi), int(doff+1))
} else {
Values.Set(-minact*v, int(op.OutValue), int(ni), int(yo), int(xo), int(pi), int(doff+1)) // 1 = right/up
Values.Set(0.0, int(op.OutValue), int(ni), int(yo), int(xo), int(pi), int(doff))
}
}
// MotionFullFieldX is the kernel: i = 2 * Y, first pass, FilterN = orig filtn
func MotionFullFieldX(i uint32) { //gosl:kernel
op := GetCurOp(0)
if i >= op.RunN*op.NData {
return
}
ri := int32(i % op.RunN)
ni := int32(i / op.RunN)
szX := op.Geom.Out.X - 1
fno := op.FilterN // original features
dir := ri % 2
yo := ri / 2
doff := dir * 2
csum := float32(0)
nsum := float32(0)
for xo := range szX {
for pi := range 2 { // pos / neg
for fi := range fno { // original features
dfo := fi*4 + doff
c := Values.Value(int(op.InValue), int(ni), int(yo), int(xo), int(pi), int(dfo)) // left, down
n := Values.Value(int(op.InValue), int(ni), int(yo), int(xo), int(pi), int(dfo+1)) // right, up
v := c - n
if v >= 0 {
csum += v
} else {
nsum += -v
}
}
}
}
Values.Set(csum, int(op.OutValue), int(ni), int(yo), int(0), int(0), int(doff))
Values.Set(nsum, int(op.OutValue), int(ni), int(yo), int(0), int(0), int(doff+1))
}
// MotionFullFieldY is the kernel: i = 2*NData, second pass
func MotionFullFieldY(i uint32) { //gosl:kernel
op := GetCurOp(0)
if i >= 2*op.NData {
return
}
ri := int32(i) % 2
ni := int32(i) / 2
szY := op.Geom.Out.Y - 1
dir := ri
doff := dir * 2
csum := float32(0)
nsum := float32(0)
for y := range szY {
c := Values.Value(int(op.OutValue), int(ni), int(y), int(0), int(0), int(doff))
n := Values.Value(int(op.OutValue), int(ni), int(y), int(0), int(0), int(doff+1))
csum += c
nsum += n
}
Scalars.Set(csum, int(op.OutScalar+doff), int(ni))
Scalars.Set(nsum, int(op.OutScalar+doff+1), int(ni))
}
//gosl:end
// Copyright (c) 2025, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1vision
//gosl:start
// Operations are the operations that can be performed.
type Operations int32 //enums:enum
const (
NoOp Operations = iota
// WrapPad wraps given padding width of float32 image around sides
// i.e., padding for left side of image is the (mirrored) bits
// from the right side of image, etc.
// InImage -> OutImage, over InImageRGB (if 3, does all).
WrapPad
// EdgeAvg computes the average r,g,b values around the edges of an image,
// storing into Scalars. These are then used for FadePad.
EdgeAvg
// FadePad wraps given padding width of float32 image around sides
// i.e., padding for left side of image is the (mirrored) bits
// from the right side of image, etc, and fades result toward average
// edge value (passed in as arg).
// InImage -> OutImage, over InImageRGB (if 3, does all).
FadePad
// LMSOpponents computes Long-Medium-Short (RGB) perceptually-based
// color opponent values from InImage -> OutImage.
// 0 = RedGreen (L-M), 1 = White-Black (grey), 2 = BlueYellow (S-(LM)),
LMSOpponents
// LMSComponents computes Long-Medium-Short (RGB) perceptually-based
// color component values from InImage -> OutImage1, OutImage2.
// For each image, the organization of components is designed to
// align with the RGB components, using grey to fill in the extra bit.
// Image1: 0 = Red (L), 1 = Green (M), 2 = Grey
// Image2: 0 = Yellow (LM), 1 = Grey, 2 = Blue (S),
LMSComponents
// ConvolveImage applies a filter to Image, writing to Values.
// InImage -> OutValue, using FilterType, FilterN
ConvolveImage
// ConvolveDiff applies two different filters to two different
// [Image, component] inputs, computing their difference,
// with positive values in 0 and negative values in 1 polarity,
// at given feature dimension (innermost Values dimension).
// This is used to compute e.g., on-center DoG to one color component
// minus off-center to another component.
ConvolveDiff
// LogValues sets values to 1 + log of values * Gain.
// InValue -> OutValue (can be the same).
LogValues
// MaxScalar computes Max over values.
// InValue = values, OutScalar = result.
MaxScalar
// SumScalar computes Sum over values
// InValue = values, OutScalar = result.
SumScalar
// MeanScalar computes Mean over values
// InValue = values, OutScalar = result.
MeanScalar
// NormDiv normalizes values by scalar
// InValue -> OutValue (can be same), InScalar = norm factor.
NormDiv
// NeighInhib4 computes neighbor inhibition, as an optional preliminary
// step prior to KWTA. Currently only works with 4 angles (n features=4).
// Each unit gets inhibition from same feature in nearest orthogonal neighbors.
// Reduces redundancy of feature code.
NeighInhib4
// KWTAInhib computes k-winners-take-all inhibition, rate-code version,
// based on overall levels of activity, over multiple iterations.
KWTAInhib
// MaxPool performs max-pooling over given pool size and spacing,
// effectively reducing the dimensionality of the output by the
// spacing factor. Size must = spacing or 2 * spacing.
MaxPool
// MaxPolarity performs max-pooling over the polarity (on vs. off)
// dimension.
MaxPolarity
// MaxCopy performs simple max over 2 different values, for
// aggregating different channels (e.g., colors) into a summary,
// without changing the dimensionality.
MaxCopy
// LenSum4 performs V1 complex-cell length-summing, extending the
// receptive field along the orientation angle one step.
// Works on output from [MaxPolarity] (first polarity dimension),
// only for the 4 angles case.
LenSum4
// EndStop4 performs V1 complex-cell end-stop, detecting an orthoginal
// angle at the end of a length-sum line. Only for the 4 angles case.
EndStop4
// To4D copies from Values to Values4D for aggregating final results
// across multiple feature dimensions (e.g., for assembling full V1 complex).
To4D
// MotionIntegrate does fast and slow motion integration from
// values to values: InValue -> OutValue (should be different)
MotionIntegrate
// MotionStar computes starburst-style motion on integrated
// fast and slow input values. Result is 4 * FilterN filter
// outputs, for Left, Right, Down, Up motion directions.
// InValue -> OutValue (different, X and Y are -1 in output).
MotionStar
// MotionFullField computes full-field summary of output from
// MotionStar, into 4 Scalars for Left, Right, Down, Up.
// Opposite directions compete.
// OutScalar[0-3] = instantaneous full-field values per this frame
// OutScalar[4-7] = integrated full-field values over time
MotionFullField
)
// Op specifies an operation to perform.
// The full computational sequence is specified as a sequence of operations.
// This allows a full processing path to proceed with minimal transfers.
type Op struct {
// Op is the operation to perform on this step
Op Operations
// NData is the number of data-parallel copies of everything to process
// at once. Copied from V1Vision at op creation time.
NData uint32
// RunN is the total number of processors to deploy for this run
// (i.e., the loop N for data parallel for loop, logically).
// Actual run value will be * NData as well.
RunN uint32
// InImage is the index of an image to process as an input.
InImage int32
// InImageRGB is the RGB value to process of input image (0-2).
// If 3, then all RGB are processed in one op (e.g., WrapPad)
InImageRGB int32
// InValue is the Values index input to use.
InValue int32
// InValue2 is the second Values index input to use, where needed.
InValue2 int32
// OutValue is the Values index output to write to.
OutValue int32
// OutValue4D is the Values4D index output to write to.
OutValue4D int32
// OutImage is the index of an image to send output for image ops.
OutImage int32
// OutImage2 is the index of a second image to send output for image ops.
OutImage2 int32
// FilterType is the type index of Filters to use.
FilterType int32
// FilterN is the number of filters within the FilterType to use.
FilterN int32
// FloatArg1 is a float argument -- e.g., used for gain multiplier
// factor to apply.
FloatArg1 float32
// FloatArg2 is a float argument
FloatArg2 float32
// FloatArg3 is a float argument
FloatArg3 float32
// IntArg1 is an arbitrary integer arg, used for different ops.
// e.g., PadWidth in WrapPad
IntArg1 int32
// InScalar is the Scalars index input to read from.
InScalar int32
// OutScalar is the Scalars index output to write to.
OutScalar int32
// Inhibs is the index of the Inhibs state variables to use.
Inhibs int32
// KWTA is the index of the KWTA parameters to use.
KWTA int32
pad, pad1, pad2 int32
// Geom is the geometry to use for this operation.
Geom Geom
}
// Run runs the operation on given run input index and NData index.
// (already range checked).
func (op *Op) Run(ri, ni int32) {
switch op.Op {
case ConvolveImage:
op.ConvolveImage(ri, ni)
case ConvolveDiff:
op.ConvolveDiff(ri, ni)
case WrapPad:
op.WrapPad(ri, ni)
case FadePad:
op.FadePad(ri, ni)
case LMSOpponents:
op.LMSOpponents(ri, ni)
case LMSComponents:
op.LMSComponents(ri, ni)
case LogValues:
op.LogValues(ri, ni)
case NormDiv:
op.NormDiv(ri, ni)
case NeighInhib4:
op.NeighInhib4(ri, ni)
case MaxPool:
op.MaxPool(ri, ni)
case MaxPolarity:
op.MaxPolarity(ri, ni)
case MaxCopy:
op.MaxCopy(ri, ni)
case LenSum4:
op.LenSum4(ri, ni)
case EndStop4:
op.EndStop4(ri, ni)
case To4D:
op.To4D(ri, ni)
case MotionIntegrate:
op.MotionIntegrate(ri, ni)
case MotionStar:
op.MotionStar(ri, ni)
default:
}
}
func DoCurOp(i uint32) { //gosl:kernel
op := GetCurOp(0)
if i >= op.RunN*op.NData {
return
}
ri := int32(i % op.RunN)
ni := int32(i / op.RunN)
op.Run(ri, ni)
}
//gosl:end
// RunOps runs all the operations.
func (vv *V1Vision) RunOps() {
nops := len(vv.Ops)
for i := range nops {
vv.CurOp[0] = vv.Ops[i]
ToGPU(CurOpVar)
op := &vv.Ops[i]
switch op.Op {
case EdgeAvg:
RunEdgeAverage(int(op.NData))
case MaxScalar:
RunMaxScalarX(int(op.RunN) * vv.NData)
RunMaxScalarY(vv.NData)
case SumScalar:
RunSumScalarX(int(op.RunN) * vv.NData)
RunSumScalarY(vv.NData)
case MeanScalar:
RunSumScalarX(int(op.RunN) * vv.NData)
RunMeanScalarY(vv.NData)
case KWTAInhib:
kp := &vv.KWTAs[op.KWTA]
RunKWTAInitLayer(vv.NData)
RunKWTAInitPool(int(op.RunN) * vv.NData)
for range kp.Iters {
RunKWTAIterLayerX(int(op.Geom.Out.Y) * vv.NData)
RunKWTAIterLayerY(vv.NData)
RunKWTAIterPool(int(op.RunN) * vv.NData)
}
case MotionFullField:
RunMotionFullFieldX(int(op.RunN) * vv.NData)
RunMotionFullFieldY(2 * vv.NData)
default:
RunDoCurOp(int(op.RunN) * vv.NData)
}
if i < nops-1 {
RunDone() // must wait to send next op
}
}
}
// Code generated by "goal build"; DO NOT EDIT.
//line scalar.goal:1
// Copyright (c) 2025, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1vision
// NewAggScalar adds a [SumScalar], [MeanScalar], or [MaxScalar] operation,
// which computes a [Scalars] output efficiently. Adds a Scalar output
// and returns index.
// in = input Values. Allocates output Values and scalars as needed.
// fn = number of filters, Geom Out sizes are used for indexing.
func (vv *V1Vision) NewAggScalar(aggOp Operations, in, fn int, geom *Geom) int {
op := vv.NewOp()
op.Op = aggOp
op.RunN = uint32(geom.Out.Y) // aggregate over Y
op.InValue = int32(in)
yout := vv.NewValues(int(geom.Out.Y), 1, 1) // intermediate y output
op.OutValue = int32(yout)
out := vv.NewScalar(1)
op.OutScalar = int32(out)
op.FilterN = int32(fn)
op.Geom = *geom
return out
}
//gosl:start
// note: multiple sub-steps for scalar integration is slower up to 512 x 512 v1gabor images
// const ScalarSteps = 2
//
// func ScalarStart(i, n int32) int32 {
// ssz := n / ScalarSteps
// return i * ssz
// }
//
// func ScalarEnd(i, n int32) int32 {
// ssz := n / ScalarSteps
// if i == n-1 {
// return n
// }
// return (i+1) * ssz
// }
// MaxScalarX is the first kernel for MaxScalar,
// operating over X rows.
func MaxScalarX(i uint32) { //gosl:kernel
op := GetCurOp(0)
if i >= op.RunN*op.NData {
return
}
ri := int32(i % op.RunN)
ni := int32(i / op.RunN)
mx := float32(0)
for x := range op.Geom.Out.X {
for pi := range 2 {
for fi := range op.FilterN {
v := Values.Value(int(op.InValue), int(ni), int(ri), int(x), int(pi), int(fi))
mx = max(mx, v)
}
}
}
Values.Set(mx, int(op.OutValue), int(ni), int(ri), int(0), int(0), int(0))
}
// MaxScalarY is the second kernel for MaxScalar.
// operating over Y intermediate sum.
func MaxScalarY(i uint32) { //gosl:kernel
op := GetCurOp(0)
if i >= op.NData {
return
}
ni := int32(i)
mx := float32(0)
for y := range op.Geom.Out.Y {
v := Values.Value(int(op.OutValue), int(ni), int(y), int(0), int(0), int(0))
mx = max(mx, v)
}
Scalars.Set(mx, int(op.OutScalar), int(ni))
}
// SumScalarX is the first kernel for SumScalar,
// operating over X rows.
func SumScalarX(i uint32) { //gosl:kernel
op := GetCurOp(0)
if i >= op.RunN*op.NData {
return
}
ri := int32(i % op.RunN)
ni := int32(i / op.RunN)
sum := float32(0)
for x := range op.Geom.Out.X {
for pi := range 2 {
for fi := range op.FilterN {
v := Values.Value(int(op.InValue), int(ni), int(ri), int(x), int(pi), int(fi))
sum += v
}
}
}
Values.Set(sum, int(op.OutValue), int(ni), int(ri), int(0), int(0), int(0))
}
// SumScalarY is the second kernel for SumScalar.
// operating over Y intermediate sum.
func SumScalarY(i uint32) { //gosl:kernel
op := GetCurOp(0)
if i >= op.NData {
return
}
ni := int32(i)
sum := float32(0)
for y := range op.Geom.Out.Y {
v := Values.Value(int(op.OutValue), int(ni), int(y), int(0), int(0), int(0))
sum += v
}
Scalars.Set(sum, int(op.OutScalar), int(ni))
}
// MeanScalarY is the second kernel for MeanScalar.
// operating over Y intermediate sum.
func MeanScalarY(i uint32) { //gosl:kernel
op := GetCurOp(0)
if i >= op.NData {
return
}
ni := int32(i)
sum := float32(0)
for y := range op.Geom.Out.Y {
v := Values.Value(int(op.OutValue), int(ni), int(y), int(0), int(0), int(0))
sum += v
}
sum /= float32(op.Geom.Out.Y * op.Geom.Out.X * op.FilterN * 2)
Scalars.Set(sum, int(op.OutScalar), int(ni))
}
//gosl:end
// Code generated by "goal build"; DO NOT EDIT.
//line to4d.goal:1
// Copyright (c) 2025, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1vision
import (
"cogentcore.org/lab/tensor"
)
// NewTo4D adds a [To4D] operation, from in value -> out values4D.
// pn is number of polarities (1 or 2),
// fn is number of filters (innermost values dimension),
// toY is starting inner Y dimension offset in Values4D to copy to.
// Values4D must be allocated in advance.
// geom.Out is outer pool sizes to copy from.
// sets geom.FilterSize to X = fn, Y = pn.
func (vv *V1Vision) NewTo4D(in, out, pn, fn, toY int, geom *Geom) int {
op := vv.NewOp()
op.Op = To4D
op.RunN = uint32(geom.Out.Y * geom.Out.X * int32(fn) * int32(pn))
op.InValue = int32(in)
op.OutValue4D = int32(out)
op.IntArg1 = int32(toY)
op.Geom = *geom
op.Geom.FilterSize.X = int32(fn)
op.Geom.FilterSize.Y = int32(pn)
return out
}
//gosl:start
// To4D is kernel.
func (op *Op) To4D(i, ni int32) {
fY := op.Geom.FilterSize.Y
fX := op.Geom.FilterSize.X
szX := op.Geom.Out.X
fi := i % fX
pii := i / fX
pi := pii % fY // polarity
ii := pii / fY
yo := ii / szX
xo := ii % szX
toY := op.IntArg1
iv := Values.Value(int(op.InValue), int(ni), int(yo), int(xo), int(pi), int(fi))
Values4D.Set(iv, int(op.OutValue4D), int(ni), int(yo), int(xo), int(toY+pi), int(fi))
}
//gosl:end
// OuterAgg does simple aggregation of outer-most dimension from tensor
// into another 4D tensor, with Y, X as outer-most two dimensions,
// starting at given inner-most feature offset, and inner row-wise offset.
// inner row-wise dimension maps the outer-most dimension of source tensor.
// no bounds checking is done on output so it will just fail if
// there isn't enough room -- allocate the output size before calling!
func OuterAgg(innerPos, rowOff int, src, out *tensor.Float32) {
nout := src.DimSize(0)
ny := src.DimSize(1)
nx := src.DimSize(2)
for y := 0; y < ny; y++ {
for x := 0; x < nx; x++ {
for f := 0; f < nout; f++ {
sv := src.Value(f, y, x)
out.Set(sv, y, x, rowOff+f, innerPos)
}
}
}
}
// Copyright (c) 2025, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package v1vision
import (
"cogentcore.org/core/math32"
"cogentcore.org/lab/tensor"
"github.com/emer/v1vision/kwta"
)
//go:generate core generate -add-types -gosl
// V1Vision specifies a sequence of operations to perform on image
// input data, to simulate V1-level visual processing.
// The pipeline supports NData parallel data replications of everything.
type V1Vision struct {
// NData is the number of data-parallel copies of everything to process
// at once. Should be consistent throughout the stack. Copied into Ops
// so it is available on the GPU.
NData int
// Ops are the sequence of operations to perform, called in order.
Ops []Op
// CurOp is the current operation to perform.
CurOp []Op
// KWTAs are KWTA inhibition parameters that can be used.
KWTAs []kwta.KWTA
// Filters are one general stack of rendered filters, sized to the max of each
// of the inner dimensional values: [FilterTypes][FilterN][Y][X]
// FilterTypes = different filter types (DoG, Gabor, etc)
// FilterN = number of filters within the group (On, Off, angle, etc)
// Y, X = sizes.
Filters *tensor.Float32
// Images are float-valued image data: [ImageNo][NData][RGB][Y][X],
// sized to the max of each inner-dimensional value (RGB=3
// if more needed, use additional ImageNo)
Images *tensor.Float32
// Values are intermediate input / output data:
// [ValueNo][NData][Y][X][Polarity][FilterN]
// where FilterN corresponds to the different filters applied or other such data,
// and Polarity is 0 for positive (on) values and 1 for negative (off) values.
Values *tensor.Float32
// Values4D are 4D aggregated data (e.g., outputs):
// [ValueNo][NData][PoolY][PoolX][UnitY][UnitX]
Values4D *tensor.Float32
// Scalars are scalar values for Sum, Max summary stats etc.
// More efficient to use these versus using large Values allocations.
// [values][NData]
Scalars *tensor.Float32
// Inhibs are [KWTAInhib] inhibitory state values:
// [InhibNo][NData][PoolY][PoolX][InhibVarsN]
Inhibs *tensor.Float32
}
// Init makes initial versions of all variables.
// Takes the number of data-parallel inputs to process in parallel.
func (vv *V1Vision) Init(ndata int) {
vv.NData = max(1, ndata)
vv.Ops = []Op{}
vv.CurOp = make([]Op, 1)
vv.Filters = tensor.NewFloat32(0, 1, 1, 1)
vv.Images = tensor.NewFloat32(0, vv.NData, 3, 1, 1)
vv.Values = tensor.NewFloat32(0, vv.NData, 1, 1, 2, 1)
vv.Values4D = tensor.NewFloat32(0, vv.NData, 1, 1, 1, 1)
vv.Scalars = tensor.NewFloat32(0, vv.NData)
vv.Inhibs = tensor.NewFloat32(0, vv.NData, 1, 1, int(InhibVarsN))
}
// NewOp adds a new [Op]
func (vv *V1Vision) NewOp() *Op {
n := len(vv.Ops)
vv.Ops = append(vv.Ops, Op{NData: uint32(vv.NData)})
return &vv.Ops[n]
}
// NewKWTAParams adds new [kwta.KWTA] params, initialized with defaults
func (vv *V1Vision) NewKWTAParams() *kwta.KWTA {
n := len(vv.KWTAs)
vv.KWTAs = append(vv.KWTAs, kwta.KWTA{})
kv := &vv.KWTAs[n]
kv.Defaults()
return kv
}
// NewImage adds a new image of given size. returns image index.
func (vv *V1Vision) NewImage(size math32.Vector2i) int {
sizes := vv.Images.ShapeSizes()
n := sizes[0]
vv.Images.SetShapeSizes(n+1, vv.NData, 3, max(int(size.Y), sizes[3]), max(int(size.X), sizes[4]))
return n
}
// NewValues adds a new Values of given sizes. returns value index.
func (vv *V1Vision) NewValues(y, x, filtN int) int {
sizes := vv.Values.ShapeSizes()
n := sizes[0]
vv.Values.SetShapeSizes(n+1, vv.NData, max(y, sizes[2]), max(x, sizes[3]), 2, max(filtN, sizes[5]))
return n
}
// NewValues4D adds a new Values4D of given sizes. returns value index.
func (vv *V1Vision) NewValues4D(gpY, gpX, y, x int) int {
sizes := vv.Values4D.ShapeSizes()
n := sizes[0]
vv.Values4D.SetShapeSizes(n+1, vv.NData, max(gpY, sizes[2]), max(gpX, sizes[3]), max(y, sizes[4]), max(x, sizes[5]))
return n
}
// NewScalar adds given number of new Scalar(s), returning starting index.
func (vv *V1Vision) NewScalar(addN int) int {
sizes := vv.Scalars.ShapeSizes()
n := sizes[0]
vv.Scalars.SetShapeSizes(n+addN, vv.NData)
return n
}
// NewFilter adds a new Filters of given sizes. returns filter index.
// Note: if later adding filters of larger sizes, then initial filter data
// can be skewed, and you need to re-set it.
func (vv *V1Vision) NewFilter(filtN, y, x int) int {
sizes := vv.Filters.ShapeSizes()
n := sizes[0]
vv.Filters.SetShapeSizes(n+1, max(filtN, sizes[1]), max(y, sizes[2]), max(x, sizes[3]))
return n
}
// NewInhibs adds a new Inhibs of given pool sizes. returns index.
// Allocates 1 larger than pool size, as is actually needed.
func (vv *V1Vision) NewInhibs(py, px int) int {
sizes := vv.Inhibs.ShapeSizes()
n := sizes[0]
vv.Inhibs.SetShapeSizes(n+1, vv.NData, max(py+1, sizes[2]), max(px+1, sizes[3]), int(InhibVarsN))
return n
}
// SetAsCurrent sets these as the current global values that are
// processed in the code (on the GPU). If this was not the setter of
// the current variables, then the infrastructure variables are copied up
// to the GPU. It is thus best to have everything in one configuration to
// avoid switching costs.
func (vv *V1Vision) SetAsCurrent() {
isCur := (Values == vv.Values)
CurOp = vv.CurOp
KWTAs = vv.KWTAs
Filters = vv.Filters
Images = vv.Images
Values = vv.Values
Values4D = vv.Values4D
Scalars = vv.Scalars
Inhibs = vv.Inhibs
if GPUInitialized && !isCur {
vv.ToGPUInfra()
}
}
// GPUInit initializes the GPU and transfers Ops and Filters.
// Should have already called SetAsCurrent (needed for CPU and GPU).
func (vv *V1Vision) GPUInit() {
GPUInit()
UseGPU = true
vv.ToGPUInfra()
}
// ToGPUInfra copies all the infrastructure for these filters up to
// the GPU. This is done in GPUInit, and
func (vv *V1Vision) ToGPUInfra() {
ToGPUTensorStrides()
ToGPU(CurOpVar, FiltersVar)
if len(vv.KWTAs) > 0 {
ToGPU(KWTAsVar)
}
// note: essential to copy up to GPU to init variable size.
if vv.Values.Len() > 0 {
ToGPU(ValuesVar)
}
if vv.Values4D.Len() > 0 {
ToGPU(Values4DVar)
}
if vv.Scalars.Len() > 0 {
ToGPU(ScalarsVar)
}
if vv.Inhibs.Len() > 0 {
ToGPU(InhibsVar)
}
}
func ImagesToGPU() {
ToGPU(ImagesVar)
}
// Run transfers Images to GPU, does RunOps, retrieving the
// specified set of variables back from the GPU (if GPU running).
func (vv *V1Vision) Run(vars ...GPUVars) {
ImagesToGPU()
vv.RunOps()
RunDone(vars...)
}
// ZeroValues sets all the values to zero.
// Useful when there are integrated accumulating values (e.g., motion).
func (vv *V1Vision) ZeroValues() {
tensor.SetAllFloat64(vv.Values, 0)
ToGPU(ValuesVar)
}
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package vxform
import (
"image"
"math"
"github.com/anthonynsimon/bild/clone"
"github.com/anthonynsimon/bild/transform"
)
// XFormImage transforms given image according to given parameters
// Transformations are performed as: rotation, scale, then translation.
// Scaling retain the current image size, filling border with current border
// if scaling to a smaller size.
func XFormImage(img image.Image, trX, trY, sc, rot float32) *image.RGBA {
cimg := img
if rot != 0 {
cimg = RotImage(cimg, rot)
}
if sc != 1 && sc > 0 {
cimg = ScaleImage(cimg, sc)
}
if trX != 0 || trY != 0 {
cimg = TransImage(cimg, trX, trY)
}
return cimg.(*image.RGBA)
}
// RotImage rotates image by given number of degrees
func RotImage(img image.Image, rot float32) *image.RGBA {
return transform.Rotate(img, float64(rot), nil) // default options: center, crop
}
// ScaleImage scales image by given number of degrees
// retaining the current image size, and filling border with current border
// if scaling to a smaller size.
func ScaleImage(img image.Image, sc float32) *image.RGBA {
sz := img.Bounds().Size()
nsz := sz
nsz.X = int(math.Round(float64(nsz.X) * float64(sc)))
nsz.Y = int(math.Round(float64(nsz.Y) * float64(sc)))
simg := transform.Resize(img, nsz.X, nsz.Y, transform.Linear)
if sc < 1 {
psz := sz.Sub(nsz).Div(2)
simg = clone.Pad(simg, psz.X, psz.Y, clone.EdgeExtend)
rsz := nsz.Add(psz).Add(psz)
if rsz != sz {
simg = transform.Crop(simg, image.Rectangle{Max: sz})
}
}
return simg
}
// TransImage translates image in each axis by given proportion of image half-size
// i.e., 1 = move from center to edge
func TransImage(img image.Image, trX, trY float32) *image.RGBA {
sz := img.Bounds().Size()
off := sz
off.X = int(math.Round(0.5 * float64(off.X) * float64(trX)))
off.Y = int(math.Round(0.5 * float64(off.Y) * float64(trY)))
return transform.Translate(img, off.X, off.Y)
}
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package vxform
import (
"cogentcore.org/core/math32/minmax"
"cogentcore.org/lab/base/randx"
)
// Rand specifies random transforms
type Rand struct {
// min -- max range of X-axis (horizontal) translations to generate (as proportion of image size)
TransX minmax.F32
// min -- max range of Y-axis (vertical) translations to generate (as proportion of image size)
TransY minmax.F32
// min -- max range of scales to generate
Scale minmax.F32
// min -- max range of rotations to generate (in degrees)
Rot minmax.F32
}
// Gen Generates new random transform values
func (rx *Rand) Gen(xf *XForm, rnd *randx.SysRand) {
trX := rx.TransX.ProjValue(rnd.Float32())
trY := rx.TransY.ProjValue(rnd.Float32())
sc := rx.Scale.ProjValue(rnd.Float32())
rt := rx.Rot.ProjValue(rnd.Float32())
xf.Set(trX, trY, sc, rt)
}
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package vxform
//go:generate core generate -add-types -gosl
import (
"fmt"
"image"
"github.com/emer/emergent/v2/env"
)
// XForm represents current and previous visual transformation values
// and can apply current values to transform an image.
// Transformations are performed as: rotation, scale, then translation.
// Scaling crops to retain the current image size.
type XForm struct {
// current, prv X-axis (horizontal) translation value, as proportion of image half-size (i.e., 1 = move from center to edge)
TransX env.CurPrev[float32]
// current, prv Y-axis (horizontal) translation value, as proportion of image half-size (i.e., 1 = move from center to edge)
TransY env.CurPrev[float32]
// current, prv scale value
Scale env.CurPrev[float32]
// current, prv rotation value, in degrees
Rot env.CurPrev[float32]
}
// Set updates current values
func (xf *XForm) Set(trX, trY, sc, rot float32) {
xf.TransX.Set(trX)
xf.TransY.Set(trY)
xf.Scale.Set(sc)
xf.Rot.Set(rot)
}
// Image transforms given image according to current parameters
func (xf *XForm) Image(img image.Image) *image.RGBA {
return XFormImage(img, xf.TransX.Cur, xf.TransY.Cur, xf.Scale.Cur, xf.Rot.Cur)
}
func (xf *XForm) String() string {
return fmt.Sprintf("tX: %.4f, tY: %.4f, Sc: %.4f, Rt: %.4f", xf.TransX.Cur, xf.TransY.Cur, xf.Scale.Cur, xf.Rot.Cur)
}