diff --git a/OpenEphys.Onix1/Bno055DataFrame.cs b/OpenEphys.Onix1/Bno055DataFrame.cs index 148399f..0d4f559 100644 --- a/OpenEphys.Onix1/Bno055DataFrame.cs +++ b/OpenEphys.Onix1/Bno055DataFrame.cs @@ -51,14 +51,14 @@ internal unsafe Bno055DataFrame(ulong clock, Bno055DataPayload* payload) /// /// Gets the 3D orientation in Euler angle format with units of degrees. /// - /// + /// /// The Tait-Bryan formalism is used: /// /// Yaw: 0 to 360 degrees. /// Roll: -180 to 180 degrees /// Pitch: -90 to 90 degrees /// - /// + /// public Vector3 EulerAngle { get; } /// diff --git a/OpenEphys.Onix1/ConfigurePortController.cs b/OpenEphys.Onix1/ConfigurePortController.cs index f9587c5..faf60bc 100644 --- a/OpenEphys.Onix1/ConfigurePortController.cs +++ b/OpenEphys.Onix1/ConfigurePortController.cs @@ -31,7 +31,7 @@ protected virtual bool CheckLinkState(DeviceContext device) protected abstract bool ConfigurePortVoltage(DeviceContext device); - private bool ConfigurePortVoltageOverride(DeviceContext device, double voltage) + protected virtual bool ConfigurePortVoltageOverride(DeviceContext device, double voltage) { device.WriteRegister(PortController.PORTVOLTAGE, (uint)(voltage * 10)); Thread.Sleep(500); diff --git a/OpenEphys.Onix1/ConfigureUclaMiniscopeV4.cs b/OpenEphys.Onix1/ConfigureUclaMiniscopeV4.cs new file mode 100644 index 0000000..6ff9ba7 --- /dev/null +++ b/OpenEphys.Onix1/ConfigureUclaMiniscopeV4.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading; +using oni; + +namespace OpenEphys.Onix1 +{ + /// + /// Configures a UCLA Miniscope V4 on the specified port. + /// + /// + /// The UCLA Miniscope V4 is a miniaturized fluorescent microscope for performing single-photon calcium + /// imaging in freely moving animals. It has the following features: + /// + /// A Python-480 0.48 Megapixel CMOS image sensor. + /// A BNO055 9-axis IMU for real-time, 3D orientation tracking. + /// An electrowetting lens for remote focal plane adjustment. + /// An excitation LED with adjustable brightness control and optional exposure-driven + /// interleaving to reduce photobleaching. + /// + /// + public class ConfigureUclaMiniscopeV4 : MultiDeviceFactory + { + + const double MaxVoltage = 5.6; + + PortName port; + readonly ConfigureUclaMiniscopeV4PortController PortControl = new(); + + /// + /// Initialize a new instance of a class. + /// + public ConfigureUclaMiniscopeV4() + { + Port = PortName.PortA; + PortControl.HubConfiguration = HubConfiguration.Passthrough; + } + + /// + /// Gets or sets the Miniscope camera configuration. + /// + [Category(DevicesCategory)] + [TypeConverter(typeof(SingleDeviceFactoryConverter))] + public ConfigureUclaMiniscopeV4Camera Camera { get; set; } = new(); + + /// + /// Gets or sets the Bno055 9-axis inertial measurement unit configuration. + /// + [Category(DevicesCategory)] + [TypeConverter(typeof(PolledBno055SingleDeviceFactoryConverter))] + [Description("Specifies the configuration for the Bno055 device.")] + public ConfigurePolledBno055 Bno055 { get; set; } = + new ConfigurePolledBno055 { AxisMap = Bno055AxisMap.ZYX, AxisSign = Bno055AxisSign.MirrorX | Bno055AxisSign.MirrorY | Bno055AxisSign.MirrorZ }; + + + /// + /// Gets or sets the port. + /// + /// + /// The port is the physical connection to the ONIX breakout board and must be specified prior to operation. + /// + [Description("Specifies the physical connection of the miniscope to the ONIX breakout board.")] + [Category(ConfigurationCategory)] + public PortName Port + { + get { return port; } + set + { + port = value; + var offset = (uint)port << 8; + PortControl.DeviceAddress = (uint)port; + Camera.DeviceAddress = offset + 0; + Bno055.DeviceAddress = offset + 1; + + // Hack: we configure the camera using the port controller below. configuration is super + // unreliable, so we do a bunch of retries in the logic PortController logic. We don't want to + // reperform configuration in the Camera object after this. So we capture a reference to the + // camera here in order to inform it we have already performed configuration. + PortControl.Camera = Camera; + } + } + + /// + /// Gets or sets the port voltage override. + /// + /// + /// + /// If defined, it will override automated voltage discovery and apply the specified voltage to the miniscope. + /// If left blank, an automated headstage detection algorithm will attempt to communicate with the miniscope and + /// apply an appropriate voltage for stable operation. Because ONIX allows any coaxial tether to be used, some of + /// which are thin enough to result in a significant voltage drop, its may be required to manually specify the + /// port voltage. + /// + /// + /// Warning: this device requires 4.0 to 5.0V, measured at the miniscope, for proper operation. Supplying higher + /// voltages may result in damage. + /// + /// + [Description("If defined, it will override automated voltage discovery and apply the specified voltage " + + "to the miniscope. Warning: this device requires 4.0 to 5.0V, measured at the scope, for proper operation. " + + "Supplying higher voltages may result in damage to the miniscope.")] + [Category(ConfigurationCategory)] + public double? PortVoltage + { + get => PortControl.PortVoltage; + set => PortControl.PortVoltage = value; + } + + internal override IEnumerable GetDevices() + { + yield return PortControl; + yield return Camera; + yield return Bno055; + } + + class ConfigureUclaMiniscopeV4PortController : ConfigurePortController + { + internal ConfigureUclaMiniscopeV4Camera Camera; + + protected override bool ConfigurePortVoltage(DeviceContext device) + { + const double MinVoltage = 5.2; + const double VoltageIncrement = 0.05; + + for (var voltage = MinVoltage; voltage <= MaxVoltage; voltage += VoltageIncrement) + { + SetPortVoltage(device, voltage); + if (CheckLinkStateWithRetry(device)) + { + return true; + } + } + + return false; + } + + void SetPortVoltage(DeviceContext device, double voltage) + { + if (voltage > MaxVoltage) + { + throw new ArgumentException($"The port voltage must be set to a value less than {MaxVoltage} " + + $"volts to prevent damage to the miniscope."); + } + + const int WaitUntilVoltageSettles = 400; + device.WriteRegister(PortController.PORTVOLTAGE, 0); + Thread.Sleep(WaitUntilVoltageSettles); + device.WriteRegister(PortController.PORTVOLTAGE, (uint)(10 * voltage)); + Thread.Sleep(WaitUntilVoltageSettles); + } + + override protected bool CheckLinkState(DeviceContext device) + { + var ds90ub9x = device.Context.GetPassthroughDeviceContext(DeviceAddress << 8, typeof(DS90UB9x)); + ConfigureUclaMiniscopeV4Camera.ConfigureDeserializer(ds90ub9x); + + const int FailureToWriteRegister = -6; + try + { + ConfigureUclaMiniscopeV4Camera.ConfigureCameraSystem(ds90ub9x, Camera.FrameRate, Camera.InterleaveLed); + } + catch (ONIException ex) when (ex.Number == FailureToWriteRegister) + { + return false; + } + + var linkState = device.ReadRegister(PortController.LINKSTATE); + return (linkState & PortController.LINKSTATE_SL) != 0; + } + + bool CheckLinkStateWithRetry(DeviceContext device) + { + const int TotalTries = 10; + for (int i = 0; i < TotalTries; i++) + { + if (CheckLinkState(device)) + { + Camera.Configured = true; + return true; + } + } + + return false; + } + + override protected bool ConfigurePortVoltageOverride(DeviceContext device, double voltage) + { + const int TotalTries = 3; + for (int i = 0; i < TotalTries; i++) + { + SetPortVoltage(device, voltage); + if (CheckLinkStateWithRetry(device)) + return true; + } + + return false; + } + } + } +} diff --git a/OpenEphys.Onix1/ConfigureUclaMiniscopeV4Camera.cs b/OpenEphys.Onix1/ConfigureUclaMiniscopeV4Camera.cs new file mode 100644 index 0000000..bd78385 --- /dev/null +++ b/OpenEphys.Onix1/ConfigureUclaMiniscopeV4Camera.cs @@ -0,0 +1,358 @@ +using System; +using System.ComponentModel; +using System.Drawing.Design; +using System.Reactive.Disposables; +using System.Reactive.Subjects; +using System.Threading; +using System.Xml.Serialization; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// Configures the camera on a UCLA Miniscope V4. + /// + public class ConfigureUclaMiniscopeV4Camera : SingleDeviceFactory + { + readonly BehaviorSubject ledBrightness = new(0); + readonly BehaviorSubject sensorGain = new(UclaMiniscopeV4SensorGain.Low); + readonly BehaviorSubject liquidLensVoltage = new(47); // NB: middle of range + + /// + /// Initialize a new instance of a class. + /// + public ConfigureUclaMiniscopeV4Camera() + : base(typeof(UclaMiniscopeV4)) + { + } + + /// + /// Gets or sets a value indicating whether the camera will produce image data. + /// + /// + /// If set to true, will produce image data. If set to false, will not produce image data. + /// + [Category(ConfigurationCategory)] + [Description("Specifies whether the camera is enabled.")] + public bool Enable { get; set; } = true; + + /// + /// Gets or sets the camera video rate in frames per second. + /// + [Category(ConfigurationCategory)] + [Description("Camera video rate in frames per second.")] + public UclaMiniscopeV4FramesPerSecond FrameRate { get; set; } = UclaMiniscopeV4FramesPerSecond.Fps30Hz; + + /// + /// Gets or sets the camera sensor's analog gain. + /// + [Description("Camera sensor analog gain.")] + [Category(AcquisitionCategory)] + [Editor(DesignTypes.SliderEditor, typeof(UITypeEditor))] + public UclaMiniscopeV4SensorGain SensorGain + { + get => sensorGain.Value; + set => sensorGain.OnNext(value); + } + + /// + /// Gets or sets a value indicating whether the excitation LED should turn on only when the camera + /// shutter is open. + /// + /// + /// If set to true, the excitation LED will turn on briefly before, and turn off briefly after, the + /// camera begins photon collection on its photodiode array. If set to false, the excitation LED will + /// remain on at all times. + /// + [Category(ConfigurationCategory)] + [Description("Only turn on excitation LED during camera exposures.")] + public bool InterleaveLed { get; set; } = false; + + /// + /// Gets or sets the excitation LED brightness level (0-100%). + /// + [Description("Excitation LED brightness level (0-100%).")] + [Category(AcquisitionCategory)] + [Range(0, 100)] + [Precision(1, 1)] + [Editor(DesignTypes.SliderEditor, typeof(UITypeEditor))] + public double LedBrightness + { + get => ledBrightness.Value; + set => ledBrightness.OnNext(value); + } + + /// + /// Gets or sets the liquid lens driver voltage (Volts RMS). + /// + /// + /// The imaging focal plane is controlled by using a MAX14574 high-voltage liquid lens driver. This + /// chip produces pulse-width modulated, 5 kHz alternative electric field that deforms the miniscope's + /// liquid lens in order to change the focal plane. The strength of this field determines the degree + /// of deformation and therefore the focal depth. The default setting of 47 Volts RMS corresponds to + /// approximately mid-range. + /// + [Description("Liquid lens driver voltage (Volts RMS).")] + [Category(AcquisitionCategory)] + [Range(24.4, 69.7)] + [Precision(1, 1)] + [Editor(DesignTypes.SliderEditor, typeof(UITypeEditor))] + public double LiquidLensVoltage + { + get => liquidLensVoltage.Value; + set => liquidLensVoltage.OnNext(value); + } + + // This is a hack. The hardware is quite unreliable and requires special assistance in order to + // operate. We do a lot of retries to make this happen. Once we have successful configuration, redoing + // it can cause failure. Therefore, we allow ConfigureMiniscopeV4 to set this variable in order to + // skip redundant configuration in Process(). This is not a great solution. + [XmlIgnore] + internal bool Configured { get; set; } = false; + + /// + /// Configures the camera on a UCLA Miniscope V4. + /// + /// + /// This will schedule configuration actions to be applied by a node + /// prior to data acquisition. + /// + /// A sequence of instances that holds all + /// configuration actions. + /// + /// The original sequence but with each instance now containing + /// configuration actions required to use the miniscope's camera. + /// + public override IObservable Process(IObservable source) + { + var enable = Enable; + var deviceName = DeviceName; + var deviceAddress = DeviceAddress; + var frameRate = FrameRate; + var interleaveLed = InterleaveLed; + + return source.ConfigureDevice(context => + { + try + { + // configure device via the DS90UB9x deserializer device + var device = context.GetPassthroughDeviceContext(deviceAddress, typeof(DS90UB9x)); + + // TODO: early exit if false? + device.WriteRegister(DS90UB9x.ENABLE, enable ? 1u : 0); + + // check if configuration was already performed on this object + if (!Configured) + { + ConfigureDeserializer(device); + ConfigureCameraSystem(device, frameRate, interleaveLed); + } + + var deviceInfo = new DeviceInfo(context, DeviceType, deviceAddress); + var shutdown = Disposable.Create(() => + { + // turn off EWL + var max14574 = new I2CRegisterContext(device, UclaMiniscopeV4.Max14574Address); + max14574.WriteByte(0x03, 0x00); + + // turn off LED + var atMega = new I2CRegisterContext(device, UclaMiniscopeV4.AtMegaAddress); + atMega.WriteByte(1, 0xFF); + }); + return new CompositeDisposable( + ledBrightness.Subscribe(value => SetLedBrightness(device, value)), + sensorGain.Subscribe(value => SetSensorGain(device, value)), + liquidLensVoltage.Subscribe(value => SetLiquidLensVoltage(device, value)), + DeviceManager.RegisterDevice(deviceName, deviceInfo), + shutdown); + } + finally + { + // needs to be reconfigured past this point, cannot rely on putting this action in + // shutdown disposable since any exception before this function returns will not enforce + // this setting + Configured = false; + } + }); + } + + internal static void ConfigureDeserializer(DeviceContext device) + { + // configure deserializer + device.WriteRegister(DS90UB9x.TRIGGEROFF, 0); + device.WriteRegister(DS90UB9x.READSZ, UclaMiniscopeV4.SensorColumns); + device.WriteRegister(DS90UB9x.TRIGGER, (uint)DS90UB9xTriggerMode.HsyncEdgePositive); + device.WriteRegister(DS90UB9x.SYNCBITS, 0); + device.WriteRegister(DS90UB9x.DATAGATE, (uint)DS90UB9xDataGate.VsyncPositive); + + // NB: This is required because Bonsai is not guaranteed to capture every frame at the start of + // acquisition. For this reason, the frame start needs to be marked. + device.WriteRegister(DS90UB9x.MARK, (uint)DS90UB9xMarkMode.VsyncRising); + + // set I2C clock rate to ~100 kHz + var deserializer = new I2CRegisterContext(device, DS90UB9x.DES_ADDR); + deserializer.WriteByte((uint)DS90UB9xSerializerI2CRegister.SCLHIGH, 0x7A); + deserializer.WriteByte((uint)DS90UB9xSerializerI2CRegister.SCLLOW, 0x7A); + + // configure deserializer I2C aliases + uint coaxMode = 0x4 + (uint)DS90UB9xMode.Raw12BitLowFrequency; // 0x4 maintains coax mode + deserializer.WriteByte((uint)DS90UB9xDeserializerI2CRegister.PortMode, coaxMode); + + uint i2cAlias = UclaMiniscopeV4.AtMegaAddress << 1; + deserializer.WriteByte((uint)DS90UB9xDeserializerI2CRegister.SlaveID1, i2cAlias); + deserializer.WriteByte((uint)DS90UB9xDeserializerI2CRegister.SlaveAlias1, i2cAlias); + + i2cAlias = UclaMiniscopeV4.Tpl0102Address << 1; + deserializer.WriteByte((uint)DS90UB9xDeserializerI2CRegister.SlaveID2, i2cAlias); + deserializer.WriteByte((uint)DS90UB9xDeserializerI2CRegister.SlaveAlias2, i2cAlias); + + i2cAlias = UclaMiniscopeV4.Max14574Address << 1; + deserializer.WriteByte((uint)DS90UB9xDeserializerI2CRegister.SlaveID3, i2cAlias); + deserializer.WriteByte((uint)DS90UB9xDeserializerI2CRegister.SlaveAlias3, i2cAlias); + } + + internal static void ConfigureCameraSystem(DeviceContext device, UclaMiniscopeV4FramesPerSecond frameRate, bool interleaveLed) + { + const int WaitUntilPllSettles = 200; + + // set up Python480 + var atMega = new I2CRegisterContext(device, UclaMiniscopeV4.AtMegaAddress); + WriteCameraRegister(atMega, 16, 3); // Turn on PLL + Thread.Sleep(WaitUntilPllSettles); + WriteCameraRegister(atMega, 32, 0x7007); // Turn on clock management + Thread.Sleep(WaitUntilPllSettles); + WriteCameraRegister(atMega, 199, 666); // Defines granularity (unit = 1/PLL clock) of exposure and reset_length + WriteCameraRegister(atMega, 200, 3300); // Set frame rate to 30 Hz + WriteCameraRegister(atMega, 201, 3000); // Set Exposure + + // set up potentiometer + var tpl0102 = new I2CRegisterContext(device, UclaMiniscopeV4.Tpl0102Address); + tpl0102.WriteByte(0x00, 0x72); + tpl0102.WriteByte(0x01, 0x00); + + // turn on EWL + var max14574 = new I2CRegisterContext(device, UclaMiniscopeV4.Max14574Address); + max14574.WriteByte(0x03, 0x03); + + // configuration properties + uint shutterWidth = frameRate switch + { + UclaMiniscopeV4FramesPerSecond.Fps10Hz => 10000, + UclaMiniscopeV4FramesPerSecond.Fps15Hz => 6667, + UclaMiniscopeV4FramesPerSecond.Fps20Hz => 5000, + UclaMiniscopeV4FramesPerSecond.Fps25Hz => 4000, + UclaMiniscopeV4FramesPerSecond.Fps30Hz => 3300, + _ => 3300 + }; + + atMega.WriteByte(0x04, (uint)(interleaveLed ? 0x00 : 0x03)); + WriteCameraRegister(atMega, 200, shutterWidth); + } + + static void WriteCameraRegister(I2CRegisterContext i2c, uint register, uint value) + { + // ATMega -> Python480 passthrough protocol + var regLow = register & 0xFF; + var regHigh = (register >> 8) & 0xFF; + var valLow = value & 0xFF; + var valHigh = (value >> 8) & 0xFF; + + i2c.WriteByte(0x05, regHigh); + i2c.WriteByte(regLow, valHigh); + i2c.WriteByte(valLow, 0x00); + } + + internal static void SetLedBrightness(DeviceContext device, double percent) + { + var atMega = new I2CRegisterContext(device, UclaMiniscopeV4.AtMegaAddress); + atMega.WriteByte(0x01, (uint)((percent == 0) ? 0xFF : 0x08)); + + var tpl0102 = new I2CRegisterContext(device, UclaMiniscopeV4.Tpl0102Address); + tpl0102.WriteByte(0x01, (uint)(255 * ((100 - percent) / 100.0))); + } + + internal static void SetSensorGain(DeviceContext device, UclaMiniscopeV4SensorGain gain) + { + var atMega = new I2CRegisterContext(device, UclaMiniscopeV4.AtMegaAddress); + WriteCameraRegister(atMega, 204, (uint)gain); + } + + internal static void SetLiquidLensVoltage(DeviceContext device, double voltage) + { + var max14574 = new I2CRegisterContext(device, UclaMiniscopeV4.Max14574Address); + max14574.WriteByte(0x08, (uint)((voltage - 24.4) / 0.0445) >> 2); + max14574.WriteByte(0x09, 0x02); + } + } + + static class UclaMiniscopeV4 + { + public const int AtMegaAddress = 0x10; + public const int Tpl0102Address = 0x50; + public const int Max14574Address = 0x77; + + public const int SensorRows = 608; + public const int SensorColumns = 608; + + internal class NameConverter : DeviceNameConverter + { + public NameConverter() + : base(typeof(UclaMiniscopeV4)) + { + } + } + } + + /// + /// Specifies analog gain of the Python-480 image sensor on a UCLA Miniscope V4. + /// + public enum UclaMiniscopeV4SensorGain + { + /// + /// Specifies low gain. + /// + Low = 0x00E1, + + /// + /// Specifies medium gain. + /// + Medium = 0x00E4, + + /// + /// Specifies high gain. + /// + High = 0x0024, + } + + /// + /// Specifies the video frame rate of the Python-480 image sensor on a UCLA Miniscope V4. + /// + public enum UclaMiniscopeV4FramesPerSecond + { + /// + /// Specifies 10 frames per second. + /// + Fps10Hz, + + /// + /// Specifies 15 frames per second. + /// + Fps15Hz, + + /// + /// Specifies 20 frames per second. + /// + Fps20Hz, + + /// + /// Specifies 25 frames per second. + /// + Fps25Hz, + + /// + /// Specifies 30 frames per second. + /// + Fps30Hz, + } +} diff --git a/OpenEphys.Onix1/UclaMiniscopeV4CameraData.cs b/OpenEphys.Onix1/UclaMiniscopeV4CameraData.cs new file mode 100644 index 0000000..7baaabb --- /dev/null +++ b/OpenEphys.Onix1/UclaMiniscopeV4CameraData.cs @@ -0,0 +1,123 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using System.Runtime.InteropServices; +using Bonsai; +using OpenCV.Net; + +namespace OpenEphys.Onix1 +{ + /// + /// Produces a sequence of s from the Python-480 image sensor on a + /// UCLA Miniscope V4. + /// + public class UclaMiniscopeV4CameraData : Source + { + /// + [TypeConverter(typeof(UclaMiniscopeV4.NameConverter))] + [Description(SingleDeviceFactory.DeviceNameDescription)] + [Category(DeviceFactory.ConfigurationCategory)] + public string DeviceName { get; set; } + + /// + /// Gets or sets the data type used to represent pixel intensity values. + /// + /// + /// The UCLA Miniscope V4 uses a 10-bit image sensor. To capture images that use the full + /// ADC resolution, this value can be set to . + /// This comes at the cost of limited codec support and larger file sizes. If is selected, the two least significant bits of + /// each pixel sample will be discarded, which greatly increases codec options and reduces + /// file sizes. + /// + [Description("The bit-depth used to represent pixel intensity values.")] + [Category(DeviceFactory.ConfigurationCategory)] + public UclaMiniscopeV4ImageDepth DataType { get; set; } = UclaMiniscopeV4ImageDepth.U8; + + /// + /// Generates a sequence of s at a rate determined by . + /// + /// A sequence of s + public unsafe override IObservable Generate() + { + return DeviceManager.GetDevice(DeviceName).SelectMany(deviceInfo => + { + var device = deviceInfo.GetDeviceContext(typeof(UclaMiniscopeV4)); + var passthrough = device.GetPassthroughDeviceContext(typeof(DS90UB9x)); + var scopeData = device.Context.GetDeviceFrames(passthrough.Address); + var dataType = DataType; + + return Observable.Create(observer => + { + var sampleIndex = 0; + var imageBuffer = new short[UclaMiniscopeV4.SensorRows * UclaMiniscopeV4.SensorColumns]; + var hubClockBuffer = new ulong[UclaMiniscopeV4.SensorRows]; + var clockBuffer = new ulong[UclaMiniscopeV4.SensorRows]; + var sampleRect = new Rect(0, 1, UclaMiniscopeV4.SensorColumns, UclaMiniscopeV4.SensorRows - 1); + + var frameObserver = Observer.Create( + frame => + { + var payload = (UclaMiniscopeV4ImagerPayload*)frame.Data.ToPointer(); + + // Wait for first row + if (sampleIndex == 0 && (payload->ImageRow[0] & 0x8000) == 0) + return; + + Marshal.Copy(new IntPtr(payload->ImageRow), imageBuffer, sampleIndex * UclaMiniscopeV4.SensorColumns, UclaMiniscopeV4.SensorColumns); + hubClockBuffer[sampleIndex] = payload->HubClock; + clockBuffer[sampleIndex] = frame.Clock; + if (++sampleIndex >= UclaMiniscopeV4.SensorRows) + { + + var imageData = Mat.FromArray(imageBuffer, UclaMiniscopeV4.SensorRows, UclaMiniscopeV4.SensorColumns, Depth.U16, 1); + CV.ConvertScale(imageData.GetRow(0), imageData.GetRow(0), 1.0f, -32768f); // Get rid first row's mark bit + + switch (dataType) + { + case UclaMiniscopeV4ImageDepth.U8: + { + var eightBitImageData = new Mat(imageData.Size, Depth.U8, 1); + CV.ConvertScale(imageData, eightBitImageData, 0.25); + observer.OnNext(new UclaMiniscopeV4CameraFrame(clockBuffer, hubClockBuffer, eightBitImageData.GetImage())); + break; + } + case UclaMiniscopeV4ImageDepth.U10: + { + observer.OnNext(new UclaMiniscopeV4CameraFrame(clockBuffer, hubClockBuffer, imageData.GetImage())); + break; + } + } + + hubClockBuffer = new ulong[UclaMiniscopeV4.SensorRows]; + clockBuffer = new ulong[UclaMiniscopeV4.SensorRows]; + sampleIndex = 0; + } + }, + observer.OnError, + observer.OnCompleted); + + return scopeData.SubscribeSafe(frameObserver); + }); + }); + } + + /// + /// Specifies the bit-depth used to represent pixel intensity values. + /// + public enum UclaMiniscopeV4ImageDepth + { + /// + /// 8-bit pixel values encoded as bytes. + /// + U8, + /// + /// 10-bit pixel values encoded as unsigned 16-bit integers + /// + U10 + } + } +} diff --git a/OpenEphys.Onix1/UclaMiniscopeV4CameraFrame.cs b/OpenEphys.Onix1/UclaMiniscopeV4CameraFrame.cs new file mode 100644 index 0000000..5b3f2ca --- /dev/null +++ b/OpenEphys.Onix1/UclaMiniscopeV4CameraFrame.cs @@ -0,0 +1,35 @@ +using System.Runtime.InteropServices; +using OpenCV.Net; + +namespace OpenEphys.Onix1 +{ + /// + /// Image data produced by the Python-480 CMOS image sensor on a UCLA Miniscope V4. + /// + public class UclaMiniscopeV4CameraFrame : BufferedDataFrame + { + /// + /// Initializes a new instance of the class. + /// + /// An array of values. + /// An array of hub clock counter values. + /// A image produced by the Python-480 on a UCLA Miniscope V4. + public UclaMiniscopeV4CameraFrame(ulong[] clock, ulong[] hubClock, IplImage image) + : base (clock, hubClock) + { + Image = image; + } + + /// + /// Gets the 608x608 pixel, 10-bit, monochrome image. + /// + public IplImage Image { get; } + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + unsafe struct UclaMiniscopeV4ImagerPayload + { + public ulong HubClock; + public fixed short ImageRow[UclaMiniscopeV4.SensorColumns]; + } +}