简介:这份基于Delphi7的工程面向需要为老系统接入海康TCG-225摄像头并实现车牌识别的开发者,适合具备Delphi基础、需要对接海康SDK的中级人员参考。内容围绕实时预览、手动抓拍、开启监听、响应抓拍事件和提取车牌号码等环节展开,给出了可运行的实现思路。压缩包共54个文件,以32个DLL、7个LIB等海康运行库和接口库为主,配有3个DCU、3个PAS及DFM、DPR等Delphi工程与单元文件,另有配置文件、属性文件和可执行文件,整体约14.25MB,便于梳理依赖关系、界面逻辑与编译结构。已有1396人学习下载。启用监听时需在摄像头端单独配置监听地址和端口,测试中以本机IP、默认端口和视频检测方式配合默认检测线,让镜头前的车牌画面触发抓拍与识别;工程中的调用方式、事件处理与配置思路可用于排错及二次开发参考。
1. 从 TCG-225 相机到 Delphi7 桌面端:预览与车牌识别到底该怎么落地
TCG-225 这类网络相机要在 Delphi7 桌面端做预览和车牌识别,真正难的不是调通一个接口,而是把设备回调、抓图、识别、落库串成一条稳定链路。现场常见的一幕是:预览窗口黑着,识别日志却还在出结果;或者白天识别正常,夜里车牌汉字全变问号。这套资源面向的就是这种“老框架接新设备”的活:用 Delphi7 做壳,完成 TCG-225 的实时预览、车牌抓图、识别结果展示和记录留存。适合手里有存量 Delphi7 系统、又不想整站重写的从业者;也适合刚接手设备联调、需要快速判断问题出在预览层还是识别层的新手。先把链路拆开,再谈参数,后面几章按预览、识别、落库、避坑、进阶的顺序往下走。
2. 预览链路拆解:TCG-225 的 RTSP 接入、解码参数与 Delphi7 双缓冲绘制
预览层最容易翻车的地方,是把“能出画面”当成“链路稳定”。TCG-225 的预览一般有两条路:一条走设备厂商 SDK 的预览回调,延迟低、能拿到原始码流,但绑定设备头文件和组件注册;另一条走 RTSP 解码,跨设备、便于本地验证,但 CPU 占用和缓冲控制要自己扛。我一般会先让两条路都能抓一帧图,再决定主链路用哪条。设备侧先把主码流和子码流分开:子码流给预览,主码流给抓图和识别,别用一路高码率流同时喂窗口和算法,否则窗口一卡,识别也跟着抖。
2.1 设备侧先对齐:主码流、子码流和抓图通道
TCG-225 上电后先别急着写代码,先把设备 Web 配置里的编码参数对齐。预览窗口用子码流,常见设置是 640×360 或 704×576、15 帧、码率 512 kbps 到 1 Mbps;抓图识别用主码流,1280×720 或 1920×1080、25 帧、I 帧间隔 50 左右。I 帧间隔太大,抓图时容易拿到 P 帧不完整的画面,表现就是花屏或半张图。抓图通道和预览通道不一定是同一个通道号,有的设备通道从 0 开始,有的从 1 开始,工程里不要写死,放到配置表里。
RTSP 地址不要硬编码在窗体里。工程里我一般放一个device.ini,至少保留 IP、端口、用户名、密码、通道号、码流类型、抓图超时这几个字段。密码含特殊字符时,RTSP URL 要做转义;命令行解码时还要注意引号包裹,否则&、?这类字符会被命令行拆开。现场调试先用手工命令抓一帧,能抓到再往 Delphi7 里搬,别一上来就在回调里查问题。
2.2 厂商 SDK 回调预览:登录、启动预览和线程边界
如果资源里走的是设备 SDK 预览,核心就三件事:初始化、登录、启动预览。下面用统一封装名代替具体导出名,实际工程里按设备头文件把Dev_Init、Dev_Login、Dev_StartPreview替换成真实函数名即可。这样写的好处是窗体逻辑不被 SDK 绑死,换设备时只改封装层。
type TDevPreviewCB = procedure(lRealHandle: LongInt; dwDataType: DWORD; pBuffer: PByte; dwBufSize: DWORD; pUser: Pointer); stdcall; function Dev_Init: Boolean; stdcall; external 'DeviceSdk.dll'; function Dev_Login(const IP: PAnsiChar; Port: Word; const User, Pwd: PAnsiChar): LongInt; stdcall; external 'DeviceSdk.dll'; function Dev_StartPreview(hLogin: LongInt; Channel: LongInt; StreamType: DWORD; hWnd: HWND; CB: TDevPreviewCB; pUser: Pointer): LongInt; stdcall; external 'DeviceSdk.dll'; procedure TMainForm.StartPreview; var hLogin: LongInt; begin if not Dev_Init then begin Log('SDK 初始化失败'); Exit; end; hLogin := Dev_Login(PAnsiChar(AnsiString(edtIP.Text)), StrToInt(edtPort.Text), PAnsiChar(AnsiString(edtUser.Text)), PAnsiChar(AnsiString(edtPwd.Text))); if hLogin < 0 then begin Log('登录失败,错误码:' + IntToStr(hLogin)); Exit; end; FPreviewHandle := Dev_StartPreview(hLogin, FChannelNo, FStreamType, imgPreview.Handle, DevPreviewCallback, nil); if FPreviewHandle < 0 then Log('启动预览失败,错误码:' + IntToStr(FPreviewHandle)); end;这段代码里,Channel按设备实际通道号填,StreamType用 0 表示主码流、1 表示子码流,具体枚举以头文件为准;hWnd是预览窗口句柄,Delphi7 里TImage没有窗口句柄,常见做法是放一个TPanel或TBitBtn取Handle,或者直接用TForm.Handle让 SDK 自己画。回调函数运行在 SDK 线程,不要在回调里直接Image1.Canvas.Draw,也不要直接改 VCL 控件属性,正确做法是PostMessage到主线程,把帧指针和大小带过去。
回调里还要注意pBuffer的生命周期。SDK 只在回调期间保证指针有效,出了回调就可能被复用。如果要排队解码,必须立刻CopyMemory到自己的缓冲区,不能只存指针。这个坑很隐蔽,表现是预览偶尔花屏,或者刚启动正常、跑几分钟就黑屏。
2.3 命令行解码兜底:抓一帧 JPEG 的超时与参数
当 SDK 组件注册不顺、或者要快速验证 TCG-225 的 RTSP 通不通时,我一般先用命令行解码抓一帧。下面这段 Delphi7 代码用CreateProcess隐藏窗口执行抓图,超时 10 秒强制结束,避免界面卡死。
function CaptureJpeg(const RtspUrl, OutFile: string): Boolean; var Cmd: string; SI: TStartupInfo; PI: TProcessInformation; WaitResult: DWORD; begin Result := False; Cmd := 'ffmpeg -rtsp_transport tcp -i "' + RtspUrl + '" ' + '-frames:v 1 -q:v 2 -y "' + OutFile + '"'; UniqueString(Cmd); ZeroMemory(@SI, SizeOf(SI)); SI.cb := SizeOf(SI); SI.dwFlags := STARTF_USESHOWWINDOW; SI.wShowWindow := SW_HIDE; if not CreateProcess(nil, PChar(Cmd), nil, nil, False, CREATE_NO_WINDOW, nil, nil, SI, PI) then begin Log('CreateProcess 失败:' + IntToStr(GetLastError)); Exit; end; try WaitResult := WaitForSingleObject(PI.hProcess, 10000); if WaitResult = WAIT_TIMEOUT then TerminateProcess(PI.hProcess, 1); Result := (WaitResult = WAIT_OBJECT_0); finally CloseHandle(PI.hThread); CloseHandle(PI.hProcess); end; end;-rtsp_transport tcp是为了避开 UDP 丢包导致的花屏,代价是延迟略高;-frames:v 1只抓一帧,适合做抓图接口和识别输入;-q:v 2是 JPEG 质量,2 到 5 之间足够车牌识别,太低会糊掉字符边缘。OutFile建议先写临时文件再重命名,避免识别服务读到半张图。这个命令只适合验证和兜底,不要拿它做 25 帧实时预览,进程开销扛不住。
2.4 双缓冲绘制:别在回调线程里碰 VCL
预览帧到 UI 这一层,Delphi7 里最稳的做法是“后台写缓冲、主线程定时取”。后台回调只负责把帧复制到后台TBitmap,主线程用TTimer每 50 到 80 毫秒交换一次前后台缓冲,再绘制到窗口。这样既避免跨线程操作 VCL,也不会每帧Create/Free造成句柄抖动。
var FrameLock: TRTLCriticalSection; BackBmp, FrontBmp: TBitmap; procedure TMainForm.PushFrame(Bmp: TBitmap); var Tmp: TBitmap; begin EnterCriticalSection(FrameLock); try BackBmp.Assign(Bmp); Tmp := BackBmp; BackBmp := FrontBmp; FrontBmp := Tmp; finally LeaveCriticalSection(FrameLock); end; end; procedure TMainForm.tmrPaintTimer(Sender: TObject); begin EnterCriticalSection(FrameLock); try imgPreview.Picture.Bitmap.Assign(FrontBmp); finally LeaveCriticalSection(FrameLock); end; end;PushFrame里做的是整帧复制,TBitmap.Assign会连带像素格式一起复制,适合 720p 以下的子码流;如果帧率很高,可以只复制一块DIB内存,再在OnPaint里StretchDIBits。UI 刷新 15 到 20 fps 就够看,没必要追 25 fps,刷新越快,主线程越容易被绘制拖住,识别线程反而抢不到 CPU。临界区里不要做耗时操作,Assign之后立刻退出,绘制阶段再锁一次,锁的粒度越小越稳。
3. 车牌识别工程化:抓图时机、识别接口与中文字符结果封装
预览通了之后,识别层不要直接拿预览窗口截图去识别。窗口截图受缩放、遮挡、UI 重绘影响,车牌像素被拉伸后识别率会掉得很明显。正确输入是主码流抓下来的原始 JPEG,或者 SDK 抓图接口返回的原始帧。抓图时机比算法参数更影响结果:车停在识别区、快门足够快、曝光不过曝,这三件事没做好,后面调置信度阈值都是玄学。识别接口不管是本地服务还是设备自带算法,Delphi7 这一侧都要把超时、重试、请求号和字符编码管住。
3.1 抓图触发:定时抓图、连续帧和评分闸门
资源里常见的做法是定时抓图,比如每 300 毫秒抓一帧,连续抓 3 帧,选质量最高的一帧送识别。这个策略比“来车就抓一张”稳,因为车牌进入画面有一个过程,单帧很容易抓到运动模糊或半遮挡。评分不用太复杂,灰度方差看清晰度,亮度均值看曝光,两个指标就能挡掉大部分废帧。
抓图频率别拍脑袋。TCG-225 主码流 25 帧时,300 毫秒大约跨 7 到 8 帧,车牌位置变化足够小;如果车速快,降到 150 到 200 毫秒,但连续抓帧数不要超过 4 帧,否则识别队列会积压。抓图文件按日期/设备号/时分秒毫秒分目录,单目录文件数控制在几千以内,不然 Windows 下列目录会明显变慢。
提示:抓图接口和预览回调不要共用同一个缓冲区。预览回调为了低延迟会复用内存,识别抓图要独立申请缓冲区,抓到后立刻落临时文件或送入队列。
3.2 识别接口封装:HTTP 超时、重试和 request_id
Delphi7 调本地识别服务,常见做法是 HTTP POST,图片用 base64 或者传本地路径。下面示例用路径方式,适合识别服务和桌面端在同一台机器;跨机部署时改成 base64 或 multipart。request_id必须由调用方生成,重试时带同一个值,服务端才能做幂等,不然一次超时会落两条重复记录。
function RecognizePlate(const JpegPath, RequestId: string; out PlateNo: WideString; out Confidence: Double): Boolean; var Http: TIdHTTP; Req, Resp: TStringStream; Json: string; begin Result := False; PlateNo := ''; Confidence := 0; Http := TIdHTTP.Create(nil); Req := TStringStream.Create(''); Resp := TStringStream.Create(''); try Http.ConnectTimeout := 2000; Http.ReadTimeout := 3000; Http.Request.ContentType := 'application/json; charset=utf-8'; Http.Request.CustomHeaders.Values['X-Request-Id'] := RequestId; Json := '{"image_path":"' + StringReplace(JpegPath, '\', '\\', [rfReplaceAll]) + '"}'; Req.WriteString(Json); try Http.Post('http://127.0.0.1:9000/plate', Req, Resp); except on E: Exception do begin Log('识别请求失败:' + E.Message + ',request_id=' + RequestId); Exit; end; end; // 实际工程用 JSON 库解析 Resp.DataString,取 plate_no、confidence Result := True; finally Resp.Free; Req.Free; Http.Free; end; end;连接超时 2 秒、读超时 3 秒,是本地服务的保守值;识别服务冷启动或大图推理时可能超过 3 秒,这时不要在主线程死等,把调用放进工作线程,主线程只收结果。重试次数我一般设 2 次,间隔 1 秒、3 秒,超过就进重试队列,不要在原地循环。request_id可以用设备号加时间戳加帧序号拼,长度控制在 64 字符以内。
3.3 中文车牌清洗:UTF-8、WideString 和数据库字符集
Delphi7 默认字符串是 ANSI,HTTP 返回 UTF-8 时,直接把Resp.DataString赋给string会出现乱码,车牌第一个汉字最容易变问号。边界上统一转成WideString,显示层用WideString,落库前再按数据库字符集转 UTF-8。下面这个转换函数在 Delphi7 里很常用,依赖MultiByteToWideChar。
function Utf8ToWide(const S: UTF8String): WideString; var Len: Integer; begin Result := ''; Len := MultiByteToWideChar(CP_UTF8, 0, PAnsiChar(S), -1, nil, 0); if Len <= 0 then Exit; SetLength(Result, Len - 1); MultiByteToWideChar(CP_UTF8, 0, PAnsiChar(S), -1, PWideChar(Result), Len); end;CP_UTF8表示按 UTF-8 解码,-1表示以零结尾计算长度;SetLength(Result, Len - 1)去掉结尾零。数据库连接不要用默认 ANSI 字符集,连接串里显式指定 UTF-8,写入用参数化 SQL,不要拼字符串。车牌颜色、置信度、抓图时间这些字段也一起存,后面排查“为什么这条没识别出来”时,光有车牌号不够。
3.4 结果结构:把置信度、图片路径和耗时一起带回
识别结果不要只返回一个字符串。工程里定义一个记录结构,把车牌号、颜色、置信度、抓图时间、图片路径、识别耗时、请求号都带上。这样 UI 展示、落库、重试队列用的是同一份数据,不会出现界面显示成功、数据库却缺字段的情况。
type TPlateResult = record RequestId: string; PlateNo: WideString; PlateColor: string; Confidence: Double; CaptureTime: TDateTime; ImagePath: string; ElapsedMs: Integer; Status: Integer; end;Status用 0 表示待处理、1 表示成功、2 表示失败,重试队列按这个状态扫。ElapsedMs从抓图开始计时,包含抓图、识别、落盘三段,后面做性能回归时直接看这个字段。ImagePath存最终路径,不存临时文件路径,临时文件在入库前就要重命名到位。
4. 结果落库与去重:把识别记录做成可追溯的证据链
识别结果落库这一步,很多工程只存车牌号和時間,出了争议查不到原图,也查不到当时用的哪一帧。我的习惯是“图片先落盘、数据库再记账、重试队列兜底”,三张表把链路串起来。表结构不用复杂,关键是request_id唯一,图片路径可追溯,失败记录能自动重跑。这样后面调识别率、查误报、给业务方解释,都有黑匣子可看,不用靠记忆复盘。
4.1 三张表:识别记录、重试队列和图片证据
识别记录表存成功结果,重试队列表存失败任务,图片证据不单独建表,直接在记录表里存路径。如果业务要求一车多图,再加一张plate_image子表,用record_id关联。下面 SQL 用通用写法,TIMESTAMP、VARCHAR按实际数据库调整。
CREATE TABLE plate_record ( id INTEGER PRIMARY KEY, request_id VARCHAR(64) NOT NULL, plate_no VARCHAR(16), plate_color VARCHAR(8), confidence DECIMAL(5,4), capture_time TIMESTAMP, image_path VARCHAR(255), elapsed_ms INTEGER, status SMALLINT DEFAULT 1 ); CREATE UNIQUE INDEX ux_plate_request ON plate_record(request_id); CREATE INDEX ix_plate_time ON plate_record(plate_no, capture_time); CREATE TABLE plate_retry ( id INTEGER PRIMARY KEY, request_id VARCHAR(64) NOT NULL, image_path VARCHAR(255), retry_count INTEGER DEFAULT 0, next_time TIMESTAMP, last_error VARCHAR(255), status SMALLINT DEFAULT 0 );ux_plate_request是幂等键,重试同一请求不会插两条;ix_plate_time给时间窗去重和查询用。plate_retry.status用 0 待重试、1 重试中、2 已放弃,扫描时只取status=0 AND next_time<=当前时间。图片路径不要存相对路径,换机器后找不到;也不要把图片二进制塞数据库,Delphi7 读大字段容易把内存撑爆。
4.2 事务写入与临时文件:先落盘再入库
抓图文件先写.tmp,识别成功后再MoveFile成正式文件名,最后开事务插记录。顺序反了会出现数据库有记录、图片却是半张图的情况,后面查证据很尴尬。下面这段是流程骨架,实际工程把INSERT换成参数化语句。
function SavePlate(const R: TPlateResult): Boolean; var TmpFile, FinalFile: string; begin Result := False; FinalFile := 'D:\plate\' + FormatDateTime('yyyymmdd', R.CaptureTime) + '\' + R.RequestId + '.jpg'; TmpFile := FinalFile + '.tmp'; ForceDirectories(ExtractFileDir(FinalFile)); if not CopyFile(PChar(R.ImagePath), PChar(TmpFile), False) then Exit; if not MoveFile(PChar(TmpFile), PChar(FinalFile)) then begin DeleteFile(TmpFile); Exit; end; // 开事务:INSERT INTO plate_record(...) VALUES(...) // 提交成功返回 True;失败则删除 FinalFile 并写 plate_retry Result := True; end;ForceDirectories保证日期目录存在,CopyFile的第三个参数False表示覆盖;MoveFile在同一分区内是原子重命名,跨分区会退化成复制,所以临时目录和正式目录放在同一个盘。事务失败时把request_id、image_path、last_error写进重试队列,不要直接丢掉。
4.3 时间窗去重与幂等:同一车牌不要刷屏
同一辆车在画面里停几秒,定时抓图会连续出好几条相同车牌。去重不要只按车牌号做唯一索引,那样同一辆车第二天再来就存不进去。常见做法是时间窗去重:同一车牌 5 秒内只保留置信度最高的一条。查询用下面 SQL,找到窗口内已有记录就比较置信度,新帧更高就更新,否则丢弃。
SELECT id, confidence FROM plate_record WHERE plate_no = :plate_no AND capture_time >= :window_start AND capture_time <= :capture_time ORDER BY confidence DESC LIMIT 1;window_start用当前抓图时间减 5 秒,plate_no用参数绑定,别拼 SQL。如果业务允许一车多拍,把窗口缩到 2 秒,或者加“车位号”字段一起判断。幂等键仍然是request_id,时间窗去重是业务层过滤,两者不冲突。
4.4 日志黑匣子:request_id 串起整条链路
日志不要只写“识别成功/失败”,至少写request_id、设备号、抓图耗时、识别耗时、落库耗时、错误码。Delphi7 里用TStringList按天写文件,或者写本地 SQLite 日志表,别用OutputDebugString当唯一日志,现场没人开调试器。排查时拿request_id一搜,从抓图到落库整条链路都能对上,这比问现场“当时什么情况”靠谱得多。
注意:日志里不要写完整密码和 RTSP URL 的认证段。设备密码进日志,后面运维交接容易出问题。
5. 避坑与排查:预览黑屏、识别掉点和长时间运行崩溃的五条记录
这条链路跑通不难,难的是跑一周不出事。下面五条是血泪经验里出现频率最高的:预览黑屏、花屏延迟、识别率忽高忽低、中文乱码、长时间运行崩溃。每条都按“现象、原因、解决”拆,现场按顺序查,基本能定位到层。
5.1 预览黑屏:通道号、码流类型和回调线程
现象:登录返回成功,预览句柄也大于等于 0,但窗口一直黑;换成抓图接口能抓到图,说明网络和认证没问题。
原因:最常见的是通道号从 0 还是从 1 开始搞错,工程里按 0 传,设备实际要 1;其次是码流类型传了主码流,但设备主码流编码格式和 SDK 解码能力不匹配;还有一种是回调线程里直接操作 VCL,主线程没收到绘制消息,看起来像黑屏。
解决:先调抓图接口,抓图通说明设备侧正常;再把通道号 0 和 1 各试一次,码流类型 0 和 1 各试一次;回调里只做CopyMemory和PostMessage,绘制放到主线程。预览窗口句柄用TPanel.Handle,不要用TImage。如果 SDK 日志里有解码错误,优先降子码流分辨率,别硬解主码流。
5.2 花屏与延迟:UDP 丢包、解码缓冲和子码流码率
现象:画面马赛克、局部绿块,或者画面比实际晚 3 到 5 秒;抓图偶尔半张图。
原因:RTSP 默认 UDP 传输,交换机或网卡丢包就花屏;解码缓冲开太大,延迟越积越高;子码流码率给到 4 Mbps,老机器 CPU 解不过来。
解决:RTSP 改 TCP,命令行加-rtsp_transport tcp;预览用子码流,码率压到 1 Mbps 以内;解码缓冲加-fflags nobuffer和-flags low_delay,但这两个参数对 SDK 回调不适用,SDK 路线要调 SDK 的缓冲参数。抓图花屏先看 I 帧间隔,超过 100 就调小。现场别用 WiFi 传主码流,有线是底线。
5.3 识别率忽高忽低:快门、曝光与抓图时机
现象:白天识别率 95%,夜里掉到 60%;同一辆车有时出结果,有时不出。
原因:夜间快门变慢,车牌运动模糊;车灯直射导致车牌过曝,字符边缘没了;抓图时机落在车牌刚进画面或快出画面,字符被截断。
解决:设备端快门优先,车牌识别场景常见 1/500 秒以上,配合补光;曝光模式别用自动慢快门,改成手动或快门优先;抓图连续 3 帧取清晰度最高,评分看灰度方差和亮度均值,方差低、亮度超 200 的帧直接丢。识别区画在画面中间,别贴边。
5.4 中文乱码:ANSI 工程里的 UTF-8 边界
现象:车牌第一个汉字显示问号或方块,英文和数字正常;数据库里存进去也是乱码。
原因:Delphi7 默认 ANSI,HTTP 返回 UTF-8,直接赋给string就按本地代码页解释;数据库连接字符集没设 UTF-8;控件字体不支持中文。
解决:HTTP 响应先按字节收,边界用Utf8ToWide转WideString;显示控件用支持中文的字体;数据库连接串显式指定 UTF-8,写入用参数化。不要用AnsiString一路带到数据库,中间任何一次转换都可能丢字。
5.5 长时间运行崩溃:句柄泄漏与跨线程绘制
现象:跑几小时到一天,程序突然卡死或弹出访问违例;重启后正常,过一段时间又复现。
原因:回调线程直接操作 VCL;CreateProcess或 SDK 句柄没关;TBitmap每帧Create不Free;临界区里做了耗时操作导致死锁。
解决:回调只入队,绘制在主线程;CreateProcess的hProcess、hThread用try/finally关;TBitmap复用前后台两块;临界区只包Assign,不包文件 IO。任务管理器看句柄数和 GDI 对象数,跑一小时涨几千就是泄漏,按代码路径逐段注释排查。
6. 进阶技巧:用帧质量评分和重试队列把识别链路压稳
识别链路想稳,不要靠调高置信度阈值硬卡。阈值调高,误报少了,漏报更多;阈值调低,重复记录刷屏。更有效的做法是在抓图之后加一道帧质量评分,把模糊帧、过曝帧挡在识别之前。下面这个评分函数按采样步长算灰度方差和亮度均值,Delphi7 里直接能跑。
function FrameScore(Bmp: TBitmap; Step: Integer): Double; var x, y, N: Integer; Gray: Byte; Sum, SumSq: Int64; begin Sum := 0; SumSq := 0; N := 0; for y := 0 to (Bmp.Height - 1) div Step do for x := 0 to (Bmp.Width - 1) div Step do begin Gray := (GetRValue(Bmp.Canvas.Pixels[x * Step, y * Step]) * <p> <a href="https://download.csdn.net/download/gekun/12638720" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>